Router Component

Router Component is a special type of content that can be loaded by Router when we specify route content using component or componentUrl properties.

It should help to better structure our apps, keep things in appropriate place, and make many things quicker and in a more clear and comfortable way.

Component Options

If you know what is Vue component, then it will be much easier to understand as it looks pretty similar. Router Component is basically an object with the following properties (all properties are optional):

PropertyTypeDescription
mixinsarrayArray with component mixins
templatestringTemplate7 template string. Will be compiled as Template7 template
renderfunctionRender function to render component. Must return full html string or HTMLElement
datafunction

Component data, function must return component context data or Promise that will be resolved with context data.

For example:

{
  ...
  data: function () {
    return {
      foo: 'bar',
    }
  },
  ...
}

Returning Promise:

{
  ...
  data() {
    return new Promise((resolve) => {
      fetch('some/path')
        .then(res => res.json())
        .then(user => resolve({ user }))
    });
  },
}

Or same as async function:

{
  ...
  async data() {
    const user = await fetch('some/path').then(res => res.json());
    return {
      user,
    };
  },
  ...
}
stylestringComponent CSS styles. Styles will be added to the document after component will be mounted (added to DOM), and removed after component will be destroyed (removed from the DOM)
methodsobjectObject with additional component methods which extend component context
onobjectObject with page events handlers
Lifecycle Hooks
beforeCreatefunctionCalled synchronously immediately after the component has been initialized, before data and event/watcher setup.
createdfunctionCalled synchronously after the component is created, context data and methods are available and component element $el is also created and available
beforeMountfunctionCalled right before component will be added to DOM
mountedfunctionCalled right after component was be added to DOM
updatedfunctionCalled right after component VDOM has been patched
beforeDestroyfunctionCalled right before component will be destoyed
destroyedfunctionCalled when component destroyed

All lifecycle hooks and methods automatically have their this context bound to the component context, so that you can access component data and methods. This means you should not use an arrow function to define a lifecycle method (e.g. created: () => this.doSomething()). The reason is arrow functions bind the parent context, so this will not be the component instance as you expect and this.doSomething will be undefined.

So the example route with page component may look like:

routes = [
  // ...
  {
    path: '/some-page/',
    // Component Object
    component: {
      template: `
        <div class="page">
          <div class="navbar">
            <div class="navbar-bg"></div>
            <div class="navbar-inner">
              <div class="title">{{title}}</div>
            </div>
          </div>
          <div class="page-content">
            <a @click="openAlert" class="red-link">Open Alert</a>
            <div class="list simple-list">
              <ul>
                {{#each names}}
                  <li>{{this}}</li>
                {{/each}}
              </ul>
            </div>
          </div>
        </div>
      `,
      style: `
        .red-link {
          color: red;
        }
      `,
      data: function () {
        return {
          title: 'Component Page',
          names: ['John', 'Vladimir', 'Timo'],
        }
      },
      methods: {
        openAlert: function () {
          var self = this;
          self.$app.dialog.alert('Hello world!');
        },
      },
      on: {
        pageInit: function (e, page) {
          // do something on page init
        },
        pageAfterOut: function (e, page) {
          // page has left the view
        },
      }
    },
  },
  // ...
]

Component Context

As we said above, all component methods and Template7 compiler are executed in the context of the component.

Component context is the object you have returned in component's data and methods from specified methods object, but also extended with the following useful properties:

PropertyDesctiption
$el

Dom7 instance with component HTML element

this.$el.find('p').addClass('red')
$
$$
$dom7

Dom7 library:

this.$$('p').text('hello world')
$app
$f7

Framework7 app instance

this.$app.dialog.alert('Hello world!')
/* or */
this.$f7.dialog.alert('Hello world!')
$root

If you use Main App Component then it refers to its instance.

Otherwise, it refers to root data and methods you have specified in data and methods properties on app init

var app = new Framework7({
  // root data
  data: function () {
    return {
      username: 'johndoe'
    }
  },
  // root methods
  methods: {
    helloWorld: function () {
      app.dialog.alert('Hello world!');
    }
  }
});

// then in component:
console.log(this.$root.username); // -> 'johndoe';

this.$root.helloWorld(); // -> call alert
$route
$f7route
Current route. Contains object with route query, hash, params, path and url
$router
$f7router

Related router instance

this.$router.back(); //navigate back
$id

Component id, used as scoped styles id

$theme

Object with md, ios and aurora boolean properties which indicating current theme. For example:

if (this.$theme.ios) { /* do something when iOS theme is active */ }
if (this.$theme.md) { /* do something when MD theme is active */ }
if (this.$theme.aurora) { /* do something when Aurora theme is active */ }
$props

Object with properties passed to custom component as attributes. If custom component has such attributes:

<my-component foo="bar" id="25"></my-component>

then $props will be:

{
  foo: 'bar',
  id: '25',
}
$slots

Used to programmatically access content distributed by slots. Each named slot has its own corresponding property (e.g. the contents of slot="foo" will be found at $slots.foo). The default property contains nodes not included in a named slot.

$setState(mergeState, callback)

Component method enqueues changes to the component state and tells that this component and its children need to be re-rendered with the updated state (passed as mergeState object)

Think of $setState() as a request rather than an immediate command to update the component. For better perceived performance, DOM update may be delayed, and then update several components in a single pass. It is not guaranteed that the DOM changes are applied immediately.

If you rely on DOM (e.g. need to get HTML content or attribute values after state changed) then pass callback function as second argument or use $tick method:

this.$setState({foo: 'bar'}, () => {
  // DOM updated
});

Such mechanism is similar to React's approach and its setState() method. It allows to control rendering and avoid unnecessary renders.

Note, that direct assignment to component state won't trigger layout update. If we use this.foo = 'bar' it will not be updated. Use $setState() or $update() whenever you need to update component layout!

$update(callback)

This method can be used instead of $setState() if you updated component context directly to force component update:

// update component context directly
this.foo = 'bar';
// call $update method
this.$update();

Same as with $setState it is not guaranteed that the DOM changes are applied immediately, so if you rely on DOM (e.g. need to get HTML content or attribute values after state changed) then pass callback function as argument or use $tick method

$tick(callback)

You can also use this method if you rely on DOM and need to be sure that component state and DOM updated after calling $setState() or $update() methods.

Passed callback will be executed on DOM update.

This method returns Promise that will also be resolved on DOM update.

So you can use it as this:

this.$setState({ foo: 'bar' });
this.$setState({ john: 'doe' });

this.$tick(function () {
  console.log('DOM and state updated');
});

// Or as Promise
this.$tick().then(() => {
  console.log('DOM and state updated');
})

// Or in async function/method as:
await this.$tick();
console.log('DOM and state updated');
$f7ready(callback)

This method need to be used only when you use Main App Component to make sure to call Framework7 APIs when app initialized.

export default {
  data() {
    ...
  },
  methods: {
    ...
  },
  mounted() {
    this.$f7ready(() => {
      // now it is safe to call Framework7 APIs
      this.$f7.dialog.alert('Hello!');
    });
  },
}

Component Page Events

Component page events handlers can be passed in on component property. They are usual DOM Page Events. Because they are DOM events, they accept event as first agrument, and Page Data as second argument. There only difference with usual DOM events is that their context (this) bound to component context and event handler name must be specified in camelCase format (page:init -> pageInit):

...
data: function () {
  return {
    username: 'johndoe',
  };
},
on: {
  pageMounted: function (e, page) {
    console.log('page mounted');
  },
  pageInit: function (e, page) {
    console.log(this.username); // -> 'johndoe'
  },
  pageBeforeIn: function (e, page) {
    console.log('page before in');
  },
  pageAfterIn: function (e, page) {
    console.log('page after in');
  },
  pageBeforeOut: function (e, page) {
    console.log('page before out');
  },
  pageAfterOut: function (e, page) {
    console.log('page after out');
  },
  pageBeforeUnmount: function (e, page) {
    console.log('page before unmount');
  },
  pageBeforeRemove: function (e, page) {
    console.log('page before remove');
  },
}

DOM Events Handling

Note that additional @ attribute in component template. It is a shorthand method to assign event listener to the specified element. Specified event handler will be searched in component methods.

Such event handlers are processed only on initial rendering, or for elements patched with VDOM. If you add such element to DOM manually it won't work!

{
  // ...
  methods: {
    onClick: function() {
      // ...
    }
  },
  on: {
    pageInit: function (page) {
      // this won't work
      page.$el.append('<a @click="onClick">Link</a>');
    }
  }
}

Component Root Element

Component template or render function must return only single HTML element. And it must be an element that is supported by router:

  • If you load pages as router component then router component must return Page element:

    <template>
      <div class="page">
        ...
      </div>
    </template>

  • If you load modal (Routable Modals) as router component then router component must return that modal element:

    <template>
      <div class="popup">
        ...
      </div>
    </template>

  • If you load panel (Routable Panels) as router component then router component must return Panel element:

    <template>
      <div class="panel panel-left panel-cover">
        ...
      </div>
    </template>

  • If you load tab content (Routable Tabs) as router component then router component must return Tab's child element that will be inserted inside of routable Tab:

    <template>
      <div class="some-element">
        ...
      </div>
    </template>
    

Single File Component

It is not very comfortable to specify all component routes under same routes array, especially if we have a lot of such routes. This is why we can use componentUrl instead and out component into single file:

routes = [
  ...
  {
    path: '/some-page/',
    componentUrl: './some-page.html',
  },
  ..
];

And in some-page.html:

<!-- component template -->
<template>
  <div class="page">
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner">
        <div class="title">{{title}}</div>
      </div>
    </div>
    <div class="page-content">
      <a @click="openAlert">Open Alert</a>
      <div class="list simple-list">
        <ul>
          {{#each names}}
            <li>{{this}}</li>
          {{/each}}
        </ul>
      </div>
    </div>
  </div>
</template>
<!-- component styles -->
<style>
  .red-link {
    color: red;
  }
</style>
<!-- rest of component data and methods -->
<script>
  // script must return component object
  return {
    data: function () {
      return {
        title: 'Component Page',
        names: ['John', 'Vladimir', 'Timo'],
      }
    },
    methods: {
      openAlert: function () {
        var self = this.$app.dialog.alert('Hello world!');
      },
    },
    on: {
      pageInit: function () {
        // do something on page init
      },
      pageAfterOut: function () {
        // page has left the view
      },
    }
  }
</script>

Well, now it is much cleaner. The <template> and <style> tags will be automatically converted to the same properties of exported component.

You may think that it is not valid to have a direct return statement in script, but it is ok because parser puts the content of the script tag into function body.

ES Template Literals

The feature available from Framework7 version 3.1.0.

When we use single file component, the everything what is under <template> tag is compiled as Template7 template. In some situations it may bring more complexity, if you need to do a lot of complex checks and modifications right in the template. With Template7 you may need to register a bunch of helpers.

So single file component template can be treated as native JavaScript Template literal.

Template literals are string literals allowing embedded expressions. You can use multi-line strings and string interpolation features with them. They were called "template strings" in prior editions of the ES2015 specification.

var a = 5;
var b = 10;
console.log(`Fifteen is ${a + b} and not ${2 * a + b}.`);

To enable your component template being treated as template literal we need to add es attribute to <template> tag. The template from previous example will look like:

<template es>
  <div class="page">
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner">
        <div class="title">${this.title}</div>
      </div>
    </div>
    <div class="page-content">
      <a @click="openAlert">Open Alert</a>
      <div class="list simple-list">
        <ul>
          ${this.names.map((name) => `
            <li>${name}</li>
          `).join('')}
        </ul>
      </div>
    </div>
  </div>
</template>

Scoped Styles

In case you want to scope component styles in single file component to this component only, you may add scoped attribute to component <style> tag:

<template>
  <!-- component template -->
</template>

<!-- style has additional "scoped" attribute -->
<style scoped>
  p {
    color: red;
  }
  a {
    text-decoration: none;
  }
</style>

<script>
  return {
    ...
  }
</script>

When scoped style added component element will have additional data-f7-[unique_id] where [unique_id] is the unique timestamp. And all styles will be refactored to have this unique scope id, for example:

[data-f7-3454d941c3] p {
  color: red;
}
[data-f7-3454d941c3] a {
  text-decoration: none;
}

In case you need to use more complex selector with including component parent reference, then you may use {{this}} keword to reference the component:

<template>
  <!-- component template -->
</template>

<!-- style has additional "scoped" attribute -->
<style scoped>
  /* all paragraphs in this component will be red under iOS theme */
  html.ios {{this}} p {
    color: red;
  }
  /* all paragraphs in this component will be green under MD theme */
  html.md {{this}} p {
    color: green;
  }
</style>

<script>
  return {
    ...
  }
</script>

Usage With Webpack

There is a special framework7-component-loader for Webpack that allows to bundle Single-File Components into main bundle and not to use XHR (e.g. componentUrl) to load and parse component files each time.

This loader parses Single-File component's file and transforms it to plain JS object during bundling process. So, potentially, it can increase app performance because there won't be runtime parsing and compilation.

When this loader is configured, we need to store Single-File components in .f7.html files and use export default for component export:

<template>
  <div class="page">
    ...
  </div>
</template>
<script>
  export default {
    data() {
      return {
        foo: 'bar',
      }
    },
    methods: {
      doThis() {
        // ...
      }
    }
  }
</script>

It also possible to import required dependencies and styles:

<template>
  <div class="page">
    ...
  </div>
</template>
<script>
  import './path/to/some-styles.css';
  import utils from './path/to/utils.js';

  export default {
    data() {
      return {
        foo: 'bar',
        now: utils.now(),
      }
    },
    methods: {
      doThis() {
        // ...
      }
    }
  }
</script>

And then we can import it and add to routes:

// routes.js

import NewsPage from './path/to/news.f7.html';
import ServicesPage from './path/to/services.f7.html';

export default [
  {
    path: '/news/',
    component: NewsPage,
  },
  {
    path: '/services/',
    component: ServicesPage,
  }
]

Virtual DOM

Virtual DOM and all VDOM related features available from Framework7 version 3.1.0.

The virtual DOM (VDOM) is a programming concept where an ideal, or "virtual", representation of a UI is kept in memory and synced with the "real" DOM. It allows us to express our application's view as a function of its state.

VDOM library called Snabbdom because it is extremely lightweight, fast and fits great for Framework7 environment.

So how does Framework7 router component VDOM rendering works? Component template is converted to VDOM instead of directly inserting to DOM. Later, when component state changes, it creates new VDOM and compares it with previous VDOM. And based on that diff it patches real DOM by changing only elements and attributes that need to be changed. And all this happens automatically!

Let's look at that user profile component example that will auto update layout when we request user data:

<template>
  <div class="page">
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner">
        <div class="title">Profile</div>
      </div>
    </div>
    <div class="page-content">
      {{#if user}}
      <!-- Show user list when it is loaded -->
      <div class="list simple-list">
        <ul>
          <li>First Name: {{user.firstName}}</li>
          <li>Last Name: {{user.lastName}}</li>
          <li>Age: {{user.age}}</li>
        </ul>
      </div>
      {{else}}
      <!-- Otherwise show preloader -->
      <div class="block block-strong text-align-center">
        <div class="preloader"></div>
      </div>
      {{/if}}
    </div>
  </div>
</template>
<script>
  return {
    data: function () {
      return {
        // empty initial user data
        user: null,
      }
    },
    on: {
      pageInit: function () {
        var self = this;
        var app = self.$app;
        // request user data on page init
        app.request.get('http://api.website.com/get-user-profile', (user) => {
          // update component state with new state
          self.$setState({
            user: user,
          });
        });
      },
    },
  };
</script>

Note, that direct assignment to component state won't trigger layout update. And if we in previous example used this.user = user it wouldn't be updated. Use $setState whenever you need to update component layout!

Keys in Lists & Auto-Init Components

When VDOM is updating a list of elements, by default it uses an "in-place patch" strategy. If the order of the data items has changed, instead of moving the DOM elements to match the order of the items, it will patch each element in-place and make sure it reflects what should be rendered at that particular index.

This default mode is efficient, but only suitable when your render output does not rely on child component state or temporary DOM state (e.g. form input values).

To give VDOM a hint so that it can track each node's identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item.

When rendering lists, an ideal value for key would be the unique id of each item:

<template>
  ...
  <ul>
    {{#each items}}
    <li key="{{this.id}}">...</li>
    {{/each}}
  </ul>
  ...
</template>
<script>
  return {
    data: function () {
      return {
        items: [
          {
            id: 1,
            title: 'Item A'
          },
          {
            id: 2,
            title: 'Item B'
          },
        ]
      }
    }
  }
</script>

Same with auto-initialized components like Range Slider, Gauge and others that should be automatically initialized (if they have range-slider-init, gauge-init) when they added to DOM, and automatically destroyed when they removed from DOM. So such elements must be also indentified with unique keys.

<template>
  <div class="page">
    ...
    <div class="page-content">
      {{#if gaugeVisible}}
      <!-- must have unique key -->
      <div key="gauge" class="gauge gauge-init" data-type="circle"
        data-value="0.60"
        data-value-text="60%"
        data-value-text-color="#ff9800"
        data-border-color="#ff9800"
      ></div>
      {{/if}}
      ...
      <a href="#" class="button" @click="showGauge">Show Gauge</a>
    </div>
  </div>
</template>
<script>
  return {
    data: function () {
      return {
        gaugeVisible: false,
      }
    },
    methods: {
      showGauge: function () {
        this.$setState({
          gaugeVisible: true
        })
      },
    }
  }
</script>
  • Note that key attribute must be unique accross single component.
  • If key attribute was not specified and element has an id attribute, then id attribute will be used as virtual node unique key.

Main App Component

Since Framework7 version 5.3.0 it is possible to make whole app layout as a component.

Note that due to VDOM implementation it is highly recommended to add unique id or key attribute to every auto initialized View (View with view-init class):

To enable it, first, we should keep app root element empty in index.html:

<body>
  <!-- empty app root element -->
  <div id="app"></div>
</body>

Then we need to create main app component, for example, Single File Component using webpack:

<!-- app.f7.html -->
<template>
  <div id="app">
    {{#if loggedIn}}
      <div class="panel panel-left panel-reveal panel-init">
        <!-- every View has unique ID attribute -->
        <div class="view view-init" id="view-panel" data-url="/panel/"></div>
      </div>
      <div class="view view-main view-init" id="view-main" data-url="/"></div>
    {{else}}
      <div class="login-screen modal-in">
        <div class="view view-init" id="view-auth" data-url="/auth/"></div>
      </div>
    {{/if}}
  </div>
</template>
<script>
  export default {
    data() {
      return {
        loggedIn: false,
      }
    },
    methods: {
      login() {
        this.$setState({ loggedIn: true });
      },
      logout() {
        this.$setState({ loggedIn: false });
      },
    },
  }
</script>

Finally, when we init Framework7, we need to specify app component on init:

// import main app component
import App from './path/to/app.f7.html';

var app = new Framework7({
  // specify main app component
  component: App,
})

Or, if we don't use webpack, we can also load it via XHR:

var app = new Framework7({
  // load main app component
  componentUrl: './path/to/app.f7.html',
})

Now, if we need to call main app component methods or change its data, we can reference its instance via app.rootComponent. And from other components it is available as this.$root.

For example, to call login() method of main app component in the example above from other page component, we can just call this.$root.login().

If you need to update Root component state from other component, do it in this way:

// update value
this.$root.firstName = 'Vladimir';
// call $update on root component
this.$root.$update();

or

this.$root.$setState({ firstName: 'Vladimir' })

In this case it will auto update all components that depend on $root state automatically.

Also note that main app component will be mounted (added to DOM) BEFORE app initialization process finished. So if you need to call Framework7 APIs immediately, use $f7ready callback:

<template>
  <div id="app">
    ...
  </div>
</template>
<script>
  export default {
    data() {
      ...
    },
    methods: {
      ...
    },
    mounted() {
      this.$f7ready(() => {
        // now it is safe to call Framework7 APIs
        this.$f7.dialog.alert('Hello!');
      });
    },
  }
</script>

Component Class

In addition to Object with properties syntax, there is a Class-based syntax available for components. It is designed to be used in single-file components with webpack with better TypeScript support.

In class-based component:

  • all component data and all lifecycle hooks should be declared as class prototype members.
  • component methods should be declared as class members as well.
  • all DOM events handlers (e.g. @click) must be bound to component context.

If we use it with webpack single file components, the syntax is the following:

<template>
  <div class="page">
    ...
    <a @click="onClick">Link</a>
  </div>
</template>
<style>
  ...
</style>
<script>
  // first we import super class
  import { Component } from 'framework7';

  // we need to export extended class
  export default class extends Component {
    // data
    data() {
      return {
        foo: 'bar',
      };
    }

    // hooks
    beforeCreate() {
      // Bind event handlers to component context
      this.onClick = this.onClick.bind(this);
    }
    mounted() {
      // call method
      this.doSomething();
    }

    beforeDestroy() {
      ...
    }

    // methods
    onClick() {
      ...
    }
    doSomething() {
      ...
    }
  }
</script>

If you want to use such syntax in usual single file components (loaded with componentUrl), then syntax will be the following:

<template>
  <div class="page">
    ...
    <a @click="onClick">Link</a>
  </div>
</template>
<style>
  ...
</style>
<script>
  return class extends Framework7.Component {
    // ...
  }
</script>

Component Mixins

Mixins are a flexible way to distribute reusable functionalities for components. A mixin object can contain any component options. When a component uses a mixin, all options in the mixin will be "mixed" into the component’s own options.

Amount of component mixins is not limited.

Mixins should be passed in component's mixins property:

var myMixin = {
  data: function() {
    return {
      foo: 'bar',
    }
  },
  mounted: function() {
    console.log('mounted')
  }
}

var component = {
  // pass mixins
  mixins: [myMixin],

  data: function () {
    // component already have foo: 'bar' in data
    return {
      john: 'doe',
    }
  },

  // component already have mounted hook inherited from mixin
  // in addition we can add one more mounted hook
  mounted: function () {
    console.log('component mounted')
  }
}

When we use single file components with webpack, we can keep mixins in separate files:

// mixin.js
export default {
  data: function() {
    return {
      foo: 'bar',
    }
  },
  mounted: function() {
    console.log('mounted')
  }
}
// page-component.f7.html
import myMixin from './mixins.js';

export default {
  // pass mixins
  mixins: [myMixin],

  data: function () {
    return {
      john: 'doe',
    }
  },

  mounted: function () {
    console.log('component mounted')
  }
}

We can also register global mixins. We need to do it BEFORE Framework7 initialization with the following method:

Framework7.registerComponentMixin(mixinName, mixin)- register global mixin

  • mixinName - string. Global mixin name, for example my-mixin
  • mixin - object. Mixin object (with component properties)

For example:

Framework7.registerComponentMixin('my-mixin', {
  data: function() {
    return {
      foo: 'bar',
    }
  },
});

And then in any component we can just pass such mixin by its name:

{
  // specify global mixin name
  mixins: ['my-mixin'],

  data: function () {
    return {
      john: 'doe'
    }
  },
  ...
}

When we use class-based syntax we can specify mixins as Class static property:

var myMixin = {
  data: function() {
    return {
      foo: 'bar',
    }
  },
}

class MyComponent extends Framework7.Component {
  ...
}
MyComponent.mixins = [myMixin];

Custom Components

It is possible to create custom reusable components. We need to do it BEFORE Framework7 initialization with the following method:

Framework7.registerComponent(tagName, component)- register custom component

  • tagName - string. Component tag name, e.g. my-component (will be used as <my-component>).

    Custom component tag name must contain a hyphen/dash character "-"

  • component - object or class. Component options object or component class

Note, at the moment, it is possible to use custom components only in router components (components loaded by router).

Framework7.registerComponent('my-list-item', {
  data() {
    return { foo: 'bar' }
  },
  template: `
    <li class="item-content" id="{{$props.id}}">...</li>
  `,
})

And use it in other components like:

<div class="list">
  <ul>
    <my-list-item id="item-1"></my-list-item>
  </ul>
</div>

Note, that attributes passed to custom component element available in component $props property.

Custom components in strict HTML structure require a small workaround. For example, if you have a custom component that returns a table row:

Framework7.registerComponent('my-table-row', {
  // ...
  template: `
    <tr>
      <td>Cell 1</td>
      <td>Cell 2</td>
    </tr>
  `,
  // ...
})

And if you use it directly in table (or tbody) it will produce a not valid HTML layout:

<table>
  <tbody>
    <my-table-row></my-table-row>
    ...
  </tbody>
</table>

To workaround such case, we just need usual <tr> element with component attribute:

<table>
  <tbody>
    <tr component="my-table-row"></tr>
    ...
  </tbody>
</table>

Events

You can assign DOM events for custom component in templates with same @{event} syntax. Event handler will be actually attached to custom component root element.

<template>
  <div class="page">
    ...
    <my-button @click="onClick">Click Me</my-button>
  </div>
</template>
<script>
  return {
    // ...
    methods: {
      onClick: function(e) {
        console.log('clicked');
      }
    },
    // ...
  }
</script>

Slots

If we need to pass children elements (or text) to custom component we need to use slots. Slots implementation here is totally the same as in Web Components or Vue.js

With slot tag we specify where component children should be placed. For example my-button component template:

<a class="button button-fill">
  <slot></slot>
</a>

Can be used then like this:

<my-button>Click Me</my-button>

To specify slot default value (when no children passed), we just put it inside <slot> tag:

<a class="button button-fill">
  <slot>Default Button Text</slot>
</a>

To distribute elements across component layout, we can use named slots. For example, template of my-container component:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

And we can use it like following:

<my-container>
  <h1 slot="header">Title</h1>

  <p>Text for main content.</p>
  <p>More text for main content.</p>

  <p slot="footer">Footer content</p>
</my-container>

And component result output will be:

<div class="container">
  <header>
    <h1>Title</h1>
  </header>
  <main>
    <p>Text for main content.</p>
    <p>More text for main content.</p>
  </main>
  <footer>
    <p>Footer content</p>
  </footer>
</div>

If we need to check in template if there are required slot children passed, we can check components' $slots property:

<div class="container">
  {{#if $slots.header}}
  <header>
    <slot name="header"></slot>
  </header>
  {{/if}}
  <!-- slots without name are called "default" -->
  {{#if $slots.default}}
  <main>
    <slot></slot>
  </main>
  {{/if}}
  {{#if $slots.footer}}
  <footer>
    <slot name="footer"></slot>
  </footer>
  {{/if}}
</div>