Björn be2ee1311e | 5 years ago | |
---|---|---|
.. | ||
dist | 5 years ago | |
LICENSE | 5 years ago | |
README.md | 5 years ago | |
package.json | 5 years ago |
import { template, expressionTypes } from '@riotjs/dom-bindings'
// Create the app template
const tmpl = template('<p><!----></p>', [{
selector: 'p',
expressions: [
{
type: expressionTypes.TEXT,
childNodeIndex: 0,
evaluate: scope => scope.greeting,
},
],
}])
// Mount the template to any DOM node
const target = document.getElementById('app')
const app = tmpl.mount(target, {
greeting: 'Hello World'
})
The template method is the most important of this package.
It will create a TemplateChunk
that could be mounted, updated and unmounted to any DOM node.
A template will always need a string as first argument and a list of Bindings
to work properly.
Consider the following example:
const tmpl = template('<p><!----></p>', [{
selector: 'p',
expressions: [
{
type: expressionTypes.TEXT,
childNodeIndex: 0,
evaluate: scope => scope.greeting
}
],
}])
The template object above will bind a simple binding to the <p>
tag.
Object containing all the type of bindings supported
Object containing all the expressions types supported
A binding is simply an object that will be used internally to map the data structure provided to a DOM tree.
expressions
Array<Expression>
true
type
Number
bindingTypes.SIMPLE
true
bindingTypes
objectselector
String
true
The bindings supported are only of 4 different types:
simple
to bind simply the expressions to a DOM structureeach
to render DOM listsif
to handle conditional DOM structurestag
to mount a coustom tag template to any DOM nodeCombining the bindings above we can map any javascript object to a DOM template.
These kind of bindings will be only used to connect the expressions to DOM nodes in order to manipulate them.
Simple bindings will never modify the DOM tree structure, they will only target a single node.
A simple binding must always contain at least one of the following expression:
attribute
to update the node attributesevent
to set the event handlingtext
to update the node contentvalue
to update the node valueFor example, let's consider the following binding:
const pGreetingBinding = {
selector: 'p',
expressions: [{
type: expressionTypes.Text,
childNodeIndex: 0,
evaluate: scope => scope.greeting,
}]
}
template('<article><p><!----></p></article>', [pGreeting])
In this case we have created a binding to update only the content of a p
tag.
Notice that the p
tag has an empty comment that will be replaced with the value of the binding expression whenever the template will be mounted
The simple binding supports DOM manipulations only via expressions.
evaluate
Function
type
Number
expressionTypes
objectThe attribute expression allows to update all the DOM node attributes.
// update only the class attribute
{ type: expressionTypes.ATTRIBUTE, name: 'class', evaluate(scope) { return scope.attr }}
If the name
key will not be defined and the return of the evaluate
function will be an object, this expression will set all the pairs key, value
as DOM attributes.
Given the current scope { attr: { class: 'hello', 'name': 'world' }}
, the following expression will allow to set all the object attributes:
{ type: expressionTypes.ATTRIBUTE, evaluate(scope) { return scope.attr }}
If the return value of the evaluate function will be a Boolean
the attribute will be considered a boolean attribute like checked
or selected
...
The event expression is really simple, It must contain the name
attribute and it will set the callback as dom[name] = callback
.
// add an event listener
{ type: expressionTypes.EVENT, name: 'onclick', evaluate(scope) { return function() { console.log('Hello There') } }}
To remove an event listener you should only return null
via evaluate function:
// remove an event listener
{ type: expressionTypes.EVENT, name: 'onclick', evaluate(scope) { return null } }}
The text expression must contain the childNodeIndex
that will be used to identify which childNode from the element.childNodes
collection will need to update its text content.
<p><b>Your name is:</b><i>user_icon</i><!----></p>
we could use the following text expression to replace the CommentNode with a TextNode
{ type: expressionTypes.TEXT, childNodeIndex: 2, evaluate(scope) { return 'Gianluca' } }}
The value expression will just set the element.value
with the value received from the evaluate function.
{ type: expressionTypes.VALUE, evaluate(scope) { return scope.val }}
The each
binding is used to create multiple DOM nodes of the same type. This binding is typically used in to render javascript collections.
each
bindings will need a template that will be cloned, mounted and updated for all the instances of the collection.
An each binding should contain the following properties:
itemName
String
true
indexName
Number
true
evaluate
Function
true
template
TemplateChunk
true
condition
Function
true
The each bindings have the highest hierarchical priority compared to the other riot bindings.
The following binding will loop through the scope.items
collection creating several p
tags having as TextNode child value dependent loop item received
const eachBinding = {
type: bindingTypes.EACH,
itemName: 'val',
indexName: 'index'
evaluate: scope => scope.items,
template: template('<!---->', [{
expressions: [
{
type: expressionTypes.TEXT,
childNodeIndex: 0,
evaluate: scope => `${scope.val} - ${scope.index}`
}
]
}
}
template('<p></p>', [eachBinding])
The if
bindings are needed to handle conditionally entire parts of your components templates
if
bindings will need a template that will be mounted and unmounted depending on the return value of the evaluate function.
An if binding should contain the following properties:
evaluate
Function
true
template
TemplateChunk
true
The following binding will render the b
tag only if the scope.isVisible
property will be truthy. Otherwise the b
tag will be removed from the template
const ifBinding = {
type: bindingTypes.IF,
evaluate: scope => scope.isVisible,
selector: 'b'
template: template('<!---->', [{
expressions: [
{
type: expressionTypes.TEXT,
childNodeIndex: 0,
evaluate: scope => scope.name
}
]
}])
}
template('<p>Hello there <b></b></p>', [ifBinding])
The tag
bindings are needed to mount custom components implementations
tag
bindings will enhance any child node with a custom component factory function. These bindings are likely riot components that must be mounted as children in a parent component template
A tag binding might contain the following properties:
getComponent
Function
true
evaluate
Function
true
getComponent
functionslots
Array<Slot>
true
attributes
Array<AttributeExpression>
true
The following tag binding will upgrade the time
tag using the human-readable-time
template.
This is how the human-readable-time
template might look like
import moment from 'moment'
export default function HumanReadableTime({ attributes }) {
const dateTimeAttr = attributes.find(({ name }) => name === 'datetime')
return template('<!---->', [{
expressions: [{
type: expressionTypes.TEXT,
childNodeIndex: 0,
evaluate(scope) {
const dateTimeValue = dateTimeAttr.evaluate(scope)
return moment(new Date(dateTimeValue)).fromNow()
}
}, ...attributes.map(attr => {
return {
...attr,
type: expressionTypes.ATTRIBUTE
}
})]
}])
}
Here it's how the previous tag might be used in a tag
binding
import HumanReadableTime from './human-readable-time'
const tagBinding = {
type: bindingTypes.TAG,
evaluate: () => 'human-readable-time',
getComponent: () => HumanReadableTime,
selector: 'time',
attributes: [{
evaluate: scope => scope.time,
name: 'datetime'
}]
}
template('<p>Your last commit was: <time></time></p>', [tagBinding]).mount(app, {
time: '2017-02-14'
})
The tag
bindings have always a lower priority compared to the if
and each
bindings
The slot binding will be used to manage nested slotted templates that will be update using parent scope
evaluate
Function
type
Number
expressionTypes
objectname
String
// slots array that will be mounted receiving the scope of the parent template
const slots = [{
id: 'foo',
bindings: [{
selector: '[expr1]',
expressions: [{
type: expressionTypes.TEXT,
childNodeIndex: 0,
evaluate: scope => scope.text
}]
}],
html: '<p expr1><!----></p>'
}]
const el = template('<article><slot expr0/></article>', [{
type: bindingTypes.SLOT,
selector: '[expr0]',
name: 'foo'
}]).mount(app, {
slots
}, { text: 'hello' })
If the same DOM node has multiple bindings bound to it, they should be created following the order below:
Let's see some cases where we might combine multiple bindings on the same DOM node and how to handle them properly.
Let's consider for example a DOM node that sould handle in parallel the Each and If bindings.
In that case we could skip the If Binding
and just use the condition
function provided by the Each Binding
Each bindings will handle conditional rendering internally without the need of extra logic.
A custom tag having an Each Binding bound to it should be handled giving the priority to the Eeach Binding. For example:
const components = {
'my-tag': function({ slots, attributes }) {
return {
mount(el, scope) {
// do stuff on the mount
},
unmount() {
// do stuff on the unmount
}
}
}
}
const el = template('<ul><li expr0></li></ul>', [{
type: bindingTypes.EACH,
itemName: 'val',
selector: '[expr0]',
evaluate: scope => scope.items,
template: template(null, [{
type: bindingTypes.TAG,
name: 'my-tag',
getComponent(name) {
// name here will be 'my-tag'
return components[name]
}
}])
}]).mount(target, { items: [1, 2] })
The template for the Each Binding above will be created receiving null
as first argument because we suppose that the custom tag template was already stored and registered somewhere else.
Similar to the previous example, If Bindings have always the priority on the Tag Bindings. For example:
const el = template('<ul><li expr0></li></ul>', [{
type: bindingTypes.IF,
selector: '[expr0]',
evaluate: scope => scope.isVisible,
template: template(null, [{
type: bindingTypes.TAG,
evaluate: () => 'my-tag',
getComponent(name) {
// name here will be 'my-tag'
return components[name]
}
}])
}]).mount(target, { isVisible: true })
The template for the IF Binding will mount/unmount the Tag Binding on its own DOM node.