Router Component
- Component Function
- Component Template
- Component Props
- Component Context
- Component Page Events
- DOM Events Handling
- Component Root Element
- Single File Component
- Virtual DOM
- Main App Component
- Custom Components
- Template Recepies
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 Function
Component is a function that receives props
and context
and should return render function.
Component render function should return Tagged template literal with component HTML content.
For example:
const MyComponent = (props, context) => {
// some component logic
let value = 'foo';
// return render function
return () => context.$h`
<div class="page">
<p>Value is ${value}</p>
</div>
`;
}
Component Template
As mentioned above component render function should return Tagged template literal with component HTML content. It has few important things to pay attention to.
All self-closing tags must be closed!. If you will have not closed self-closing tags like <br>
, <img src="">
, <input ...>
, then compiler will throw an error.
All empty elements can be self-closed:
<div class="my-div"></div>
<!-- also valid as -->
<div class="my-div" />
Component Props
First argument that receives component function is props
. This object will contain all props that you will pass in navigate method, and all route parameters.
For example if we have the following route:
{
path: '/blog/:id',
component: MyComponent
}
And when we navigate to route via /blog/34/
URL then it will have props.id
equal to '34'
.
And also when we navigate to component using API like so:
router.navigate('/blog/34/', {
props: {
foo: 'bar'
}
})
Then props
will be the following object: { id: '34', foo: 'bar' }
Also props will contain properties passed to custom component as attributes. If custom component has such attributes:
<my-component foo="bar" id="25" user=${{name: 'John'}} number=${30}></my-component>
then $props
will be:
{
foo: 'bar',
id: '25',
user: {
name: 'John'
},
number: 30
}
Component Context
context
object contains a lot of useful helpers:
Property | Description |
---|---|
$h | Special Tagged template literal which is must be used to wrap component render function result and all HTML entries inside:
|
$el | Object where
|
$ | Dom7 library:
|
$f7 | Framework7 app instance
|
$store | Store instance. Check Store documentation for more details and examples. |
$f7route | Current route. Contains object with route query , hash , params , path and url |
$f7router | Related router instance
|
$theme | Object with
|
$update(callback) | This method tells that this component and its children need to be re-rendered with the updated state
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 |
$ref(initialValue) | This method creates reactive "variable", which after updating automatically updates component without need to call It returns an object with
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 |
$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 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:
|
$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.
|
Events | |
$on | Function to attach DOM events handlers to component root element
Such event handlers will be automatically detached when component destroyed |
$once | Function to attach DOM events handlers to component root element. Same as |
$emit(event, data) | Function to emit custom DOM events in re-usable custom components:
And in other parent component:
|
Lifecycle Hooks | |
$onBeforeMount | Called right before component will be added to DOM |
$onMounted | Called right after component was be added to DOM
|
$onBeforeUpdate | Called right after component before VDOM will be patched/updated |
$onUpdated | Called right after component VDOM has been patched/updated |
$onBeforeUnmount | Called right before component will be unmounted (detached from the DOM) |
$onUnmounted | Called when component unmounted and destroyed |
So the example route with page component may look like:
routes = [
// ...
{
path: '/some-page/',
// Component
component: (props, { $h, $f7, $on }) => {
const title = 'Component Page';
const names = ['John', 'Vladimir', 'Timo'];
const openAlert = () => {
$f7.dialog.alert('Hello world!');
}
$on('pageInit', (e, page) => {
// do something on page init
});
$on('pageAfterOut', (e, page) => {
// page has left the view
});
return () => $h`
<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>
${names.map((name) => $h`
<li>${name}</li>
`)}
</ul>
</div>
</div>
</div>
`;
},
},
// ...
]
Component Page Events
Component page events handlers can be passed in $on
component event handler. 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 event handler name must be specified in camelCase format (page:init
-> pageInit
):
const MyComponent = (props, { $on }) => {
$on('pageMounted', (e, page) => {
console.log('page mounted');
});
$on('pageInit', (e, page) => {
console.log('page init');
});
$on('pageBeforeIn', (e, page) => {
console.log('page before in');
});
$on('pageAfterIn', (e, page) => {
console.log('page after in');
});
$on('pageBeforeOut', (e, page) => {
console.log('page before out');
});
$on('pageAfterOut', (e, page) => {
console.log('page after out');
});
$on('pageBeforeUnmount', (e, page) => {
console.log('page before unmount');
});
$on('pageBeforeRemove', (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 scope.
Such event handler attribute value must be a function:
const MyComponent = (props, { $h, $update }) => {
let value = 10;
const addValue = (number) => {
value += number;
$update();
}
const onClick = () => {
console.log('click');
}
return () => $h`
<div class="page">
<!-- pass function to attribute -->
<button @click=${onClick}>Button</button>
<!-- also work -->
<button @click=${() => onClick()}>Button</button>
<!-- will not work, attribute value "onClick" is just a string -->
<button @click="onClick">Button</button>
<!-- passing dynamic data will work as expected -->
<button @click=${() => addValue(15)}>Button</button>
</div>
`
}
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!
const MyComponent = (props, { $h, $on }) => {
const onClick = () => {
console.log('click');
}
$on('pageInit', (e, page) => {
// this won't work
page.$el.append('<a @click="onClick">Link</a>');
});
return () => $h`
<div class="page">
</div>
`
}
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 put component into single file:
routes = [
...
{
path: '/some-page/',
componentUrl: './some-page.f7',
},
..
];
And in some-page.f7
:
<!-- component template, uses same tagged template literals -->
<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>
${names.map((name) => $h`
<li>${name}</li>
`)}
</ul>
</div>
</div>
</div>
</template>
<!-- component styles -->
<style>
.red-link {
color: red;
}
</style>
<!-- rest of component logic -->
<script>
// script must return/export component function
export default (props, { $f7, $on }) => {
const title = 'Component Page';
const names = ['John', 'Vladimir', 'Timo'];
const openAlert = () => {
$f7.dialog.alert('Hello world!');
}
$on('pageInit', () => {
// do something on page init
});
$on('pageAfterOut', () => {
// page has left the view
});
// component function must return render function
return $render;
}
</script>
Well, now it is much cleaner. The <template>
and <style>
tags will be automatically converted to the same properties of exported component.
It is mandatory to have return $render
in the end of the component function, as it will be replaced by parser with content of the <template>
tag.
Usage With Webpack and Vite
For Webpack there is a special framework7-loader plugin 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.
For Vite.js there is also special rollup-plugin-framework7 plugin to bundle Single-File Components.
These plugins parse 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 plugin is configured, we need to store Single-File components in .f7
(or in .f7.html
for Webpack) files and use export default
for component export:
<template>
<div class="page">
...
</div>
</template>
<script>
export default () => {
let foo = 'bar';
const doThis = () => {
// ...
}
return $render;
}
</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 () => {
let foo = 'bar';
let now = utils.now();
const doThis = () => {
// ...
}
return $render;
}
</script>
And then we can import it and add to routes:
// routes.js
import NewsPage from './path/to/news.f7';
import ServicesPage from './path/to/services.f7';
export default [
{
path: '/news/',
component: NewsPage,
},
{
path: '/services/',
component: ServicesPage,
}
]
JSX
Template literals doesn't have good syntax highlighting inside of HTML documents. But when using with webpack or Vite, it is also possible to write components in JSX syntax.
To make it work, we need to store components in .f7.jsx
files and write them using JSX:
export default (props, { $update }) => {
let value = 10;
const items = ['Item 1', 'Item 2'];
const addValue = (number) => {
value += number;
$update();
}
//- render function should returns JSX
return () => (
<div class="page">
<p>The value is {value}</p>
<p>
{/* JSX doesn't support @ in attribute name so event handlers should start from "on" */}
<button onClick={() => addValue(10)}>Add Value</button>
</p>
<ul>
{items.map((item) => (
<li>{item}</li>
))}
</ul>
</div>
)
}
And import them in the same way in routes.js
:
import NewsPage from './path/to/news.f7.jsx';
import ServicesPage from './path/to/services.f7.jsx';
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">
${user && $h`
<!-- 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>
`}
${!user && $h`
<!-- Otherwise show preloader -->
<div class="block block-strong text-align-center">
<div class="preloader"></div>
</div>
`}
</div>
</div>
</template>
<script>
export default (props, { $on, $f7, $update }) => {
// empty initial user data
let user = null;
$on('pageInit', () => {
// request user data on page init
$f7.request.get('https://api.website.com/get-user-profile').then((res) => {
// update user with new data
user = res.data;
// trigger re-render
$update();
});
})
return $render;
}
</script>
Note, that direct assignment to component state won't trigger layout update. Use $update
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>
${items.map((item) => $h`
<li key=${item.id}>...</li>
`)}
</ul>
...
</template>
<script>
export default () => {
const items = [
{
id: 1,
title: 'Item A'
},
{
id: 2,
title: 'Item B'
},
];
return $render;
}
</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">
${gaugeVisible && $h`
<!-- 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>
`}
...
<a href="#" class="button" @click=${showGauge}>Show Gauge</a>
</div>
</div>
</template>
<script>
export default (props, { $update }) => {
let gaugeVisible = false;
const showGauge = () => {
gaugeVisible = true;
$update();
}
return $render;
}
</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.
innerHTML
If we need to insert HTML string (for example, received from API endpoint), we need to use special innerHTML
element prop/attribute:
<template>
<div class="page">
...
<div class="block" innerHTML=${customHTML}></div>
</div>
</template>
<script>
export default (props) => {
const customHTML = '<p>Hello <b>World!</b></p>';
return $render;
}
</script>
Using innerHTML
on element will override all its children.
HTML content passed in innerHTML
is just a string and, for example, component event handlers (like @click
attribute) won't work.
Main App Component
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 Vite:
<!-- app.f7 -->
<template>
<div id="app">
${loggedIn.value && $h`
<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>
`}
${!loggedIn.value && $h`
<div class="login-screen modal-in">
<div class="view view-init" id="view-auth" data-url="/auth/"></div>
</div>
`}
</div>
</template>
<script>
export default (props, { $store }) => {
const loggedIn = $store.getters.loggedIn;
return $render;
}
</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';
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',
})
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 (props, { $f7ready, $f7 }) => {
$f7ready(() => {
// now it is safe to call Framework7 APIs
$f7.dialog.alert('Hello!');
})
}
</script>
Custom Components
Register 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 function
Note, at the moment, it is possible to use custom components only in router components (components loaded by router).
Framework7.registerComponent(
// component name
'my-list-item',
// component function
(props, { $h }) => {
let foo = 'bar';
return () => $h`
<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
.
Local Components
It is possible to create local custom components in components:
<template>
<ul>
<!-- use tag names as variables -->
<${ListItem} title="Item 1" />
<${ListItem} title="Item 2" />
<${ListItem} title="Item 3" />
</ul>
</template>
<script>
// create local component
const ListItem = (props, { $h }) => {
return () => $h`<li>${props.title}</li>`;
}
// export main component
export default () => {
return $render;
}
</script>
Or they can be imported:
<template>
<ul>
<!-- use tag names as variables -->
<${ListItem} title="Item 1" />
<${ListItem} title="Item 2" />
<${ListItem} title="Item 3" />
</ul>
</template>
<script>
// import component
import ListItem from 'path/to/list-item.f7';
// export main component
export default () => {
return $render;
}
</script>
With JSX:
const ListItem = (props) => {
return (
<li>{props.title}</li>
)
}
/* or
import ListItem from 'path/to/list-item.f7.jsx'
*/
export default () => {
return () => (
<ul>
<ListItem title="Item 1" />
<ListItem title="Item 2" />
<ListItem title="Item 3" />
</ul>
)
}
In JSX, it can be created inside of the main component:
export default () => {
const ListItem = (props) => {
return (
<li>{props.title}</li>
)
}
return () => (
<ul>
<ListItem title="Item 1" />
<ListItem title="Item 2" />
<ListItem title="Item 3" />
</ul>
)
}
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 similar to Web Components slots.
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>
Template Recepies
Conditional Rendering
To implement conditions in JavaScript we usually use if
(if-else
) statements. Inside of templates and JSX we can't use them directly and should use JavaScript operators.
if
For if
statement we should use logical AND (&&
) operator:
<template>
<div class="page">
${someVar && $h`
<p>Text will be visible when "someVar" is truthy</p>
`}
${someVar === 1 && $h`
<p>Text will be visible when "someVar" equals to 1</p>
`}
</div>
</template>
<script>
export default () => {
const someVar = 1;
return $render;
}
</script>
Same using JSX:
export default () => {
const someVar = 1;
return () => (
<div class="page">
{someVar && (
<p>Text will be visible when "someVar" is truthy</p>
)}
{someVar === 1 && (
<p>Text will be visible when "someVar" equals to 1</p>
)}
</div>
)
}
if-else
For if-else
we can use Ternary operator (?:
) or combination of &&
and !
operators:
<template>
<div class="page">
${someVar ? $h`
<p>Text will be visible when "someVar" is truthy</p>
` : $h`
<p>Text will be visible when "someVar" is falsy</p>
`}
{someVar && (
<p>Text will be visible when "someVar" is truthy</p>
)}
{!someVar && (
<p>Text will be visible when "someVar" is falsy</p>
)}
</div>
</template>
<script>
export default () => {
const someVar = 1;
return $render;
}
</script>
Same using JSX:
export default () => {
const someVar = 1;
return () => (
<div class="page">
{someVar ? (
<p>Text will be visible when "someVar" is truthy</p>
) : (
<p>Text will be visible when "someVar" is falsy</p>
)}
{someVar && (
<p>Text will be visible when "someVar" is truthy</p>
)}
{!someVar && (
<p>Text will be visible when "someVar" is falsy</p>
)}
</div>
)
}
Mapping Array To Elements
To map array to elements we use Array's .map()
method:
<template>
<div class="page">
<ul>
${items.map((item) => $h`
<li>${item}</li>
`)}
</ul>
</div>
</template>
<script>
export default () => {
const items = [
'item 1',
'item 2',
'item 3',
];
return $render;
}
</script>
Same using JSX:
export default () => {
const items = [
'item 1',
'item 2',
'item 3',
];
return () => (
<div class="page">
<ul>
{items.map((item) => (
<li>{item}</li>
))}
</ul>
</div>
)
}