Router Component
- Component Structure
- Component Context
- Component Page Events
- DOM Events Handling
- Component Root Element
- Single File Component
- Virtual DOM
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 Structure
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):
Property | Type | Description |
---|---|---|
template | string | Template7 template string. Will be compiled as Template7 template |
render | function | Render function to render component. Must return full html string or HTMLElement |
data | function | Component data, function must return component context data |
style | string | Component 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) |
methods | object | Object with additional component methods which extend component context |
on | object | Object with page events handlers |
Lifecycle Hooks | ||
beforeCreate | function | Called synchronously immediately after the component has been initialized, before data and event/watcher setup. |
created | function | Called synchronously after the component is created, context data and methods are available and component element $el is also created and available |
beforeMount | function | Called right before component will be added to DOM |
mounted | function | Called right after component was be added to DOM |
updated | function | Called right after component VDOM has been patched |
beforeDestroy | function | Called right before component will be destoyed |
destroyed | function | Called 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-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:
Property | Type | Desctiption |
---|---|---|
$el | object | Dom7 instance with component HTML element
|
$ $$ $dom7 | function | Dom7 library:
|
$app $f7 | object | Framework7 app instance
|
$root | object | Root data and methods you have specified in
|
$route $f7route | object | Current route. Contains object with route query , hash , params , path and url |
$router $f7router | Router instance | Related router instance
|
$id | string | Component id, used as scoped styles id |
$theme | object | Object with
|
$setState() | function | Component method where you pass Such mechanism is similar to React's approach and its Note, that direct assignment to component state won't trigger layout update. If we use |
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-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-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 NewsPages from './path/to/news.f7.html';
import ServicePages from './path/to/services.f7.html';
export default [
{
path: '/news/',
component: NewsPages,
},
{
path: '/services/',
component: ServicesPages,
}
]
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-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 anid
attribute, thenid
attribute will be used as virtual node unique key.