Awesome
domc
Compile DOM node, with {{ value }}
mustache syntax for dynamic values inside, into function for using as a template.
To be done/Not implemented
- Conditional rendering with v-if
The Gist
import domc from 'domc'
// Given following node
// <div id="template">
// <div>{{ name }}</div>
// <div>{{ surname }}</div>
// </div>
// compile it into template
const template = domc(document.querySelector('#template'))
// Create template instance with given values
const node = template.createInstance({name: 'John', surname: 'Wick'})
container.appendChild(node)
...
// Update instance with new values
node.update({name: 'Bob', surname: 'Marley'})
Performance demo
https://github.com/Freak613/js-framework-benchmark/tree/master/frameworks/keyed/domc
Size
2.33Kb index.js
+ 1.08Kb vFor.js
Supported dynamic values syntax
Only following formats are supported currently:
- Just values:
{{ item }}
- Nested calls:
{{ item.id }}
- Function calls:
{{ rowClass(item.id) }}
Synthetic Events
To achieve desired performance, to not spread page with large amount of listeners
and to not consume a lot of memory, simple implementation of synthetic events has been added.
It's inspired by Inferno's linkEvent method.
It links handler function and fn argument to node, and use them during converting from native event.
Therefore, if you provide onclick="select(item)"
,
it will be parsed into select
call and item
as function argument.
Currently it has limitation: only one callback argument supported. Also, it doesn't do bubbling after first handler has been met.
Components
It's possible to create components in JS code.
scope
is key concept of domc. If you coming from React background, it's idea of props
and Context API merged into one entity. All templates work in some scope and scope is automatically passed down the DOM tree. All components and directives can extend scope, deeper you go and more extended scope becomes, having all scope vars starting from root component. It eliminates need to manually passing props if source and target have some intermediate components between them.
Components are defined using tag name, it follows Custom Elements naming convention i.e. tag should have at least one dash symbol in the name to be considered as a component. All components should be registered in domc before they've been used.
There are two ways of calling components in templates:
- Using tag directly
<my-component/>
- Or using
is
attribute:<tr is="my-component">
. It's used to overcome some DOM limitations, when you can't put custom component in DOM tree, for exampletbody
could have onlytr
children elements.
Components could be either stateless or stateful. If parent scope have enough data for component, it doesn't require own scope to be defined.
domc.component('todo-item', `
<li>
<div>{{ todo.text }}</div>
</li>
`)
domc.component('app-body', `
<div id="app-4">
<ol>
<todo-item v-for="todo of todos"/>
</ol>
</div>`)
To make stateful component or to redefine some props from parent scope, domc.component function accepts templateObject argument.
Components could have props defined in template directly. They will be mapped from parent's scope automatically. Currently no JS values allowed in mapped props.
domc.component('app-body', `
<div id="app-4">
<ol>
<todo-item v-for="todo of todos" some-custom-prop1="parentVar1" some-custom-prop2="parentVar2"/>
</ol>
</div>`)
So, basic example looks like this:
// Register component with tag 'todo-item'
domc.component('todo-item', {
// Components have two lifecycle events:
// `create` used for creating initial values and event handlers
create: scope => {
return {
localVar: 'me'
}
},
// `update` will be called on every node.update call, either from nodeRender or from render
// We can use it to react on scope changes
update: scope => {
return {
someCond: scope.localVar === 'me' ? 'yes' : 'no'
}
},
template: `
<li>
<div>{{ item.text }} {{ localVar }} {{ someCond }}</div>
</li>
`
})
// domc.app returns function to render component as starting point of application
const c = domc.app(`
<div id="app-4">
<ol>
<todo-item v-for="todo of todos" item="todo"/>
</ol>
</div>`)
const scope = {
todos: [
{text: 'me'},
{text: 'you'}
]
}
// Create app Node
const app = c(scope)
document.body.appendChild(app)
// Make some changes and update
scope.todos[0].text = 'you'
scope.todos[1].text = 'me'
app.update(scope)
If we have event handlers, we should manually call render
function that is automatically coming from scope. It will rerender full app. If we need to trigger update only on DOM tree branch starting component's node, we can call nodeRender
function from the scope. Both doesn't accepts any arguments, scope will be automatically used in updates.
domc.component('app-body', {
create: scope => {
const {render, nodeRender} = scope
const init = {
myVar: 'me',
update: () => {
// Init object should not be modified. We use `scope` to work with component state
scope.myVar = 'you'
// Rerender full app
render()
// OR rerender node
nodeRender()
}
}
return init
},
template: `
<div id="app-4" onclick="update">
<h1>{{ myVar }}</h1>
</div>`
})
Slots
It's possible to insert other Nodes into template.
domc.component('app', `
<div>
<!-- #oneNode -->
<!-- ##manyNodes -->
</div>
`,
scope => {
return {
oneNode: document.createElement('div'),
manyNodes: [document.createElement('div'), document.createElement('div')]
}
})
For Components, scope will contain children
array to insert in result Node
domc.component('container-component', `
<div>
<!-- ##children -->
</div>
`)
domc.component('app', `
<container-component>
<div>1</div>
<div>2</div>
<div>3</div>
</container-component>
`)
Styles
Inline styles are supported in following ways:
- As a simple non-dynamic argument,
<div style="display: flex;"></div>
- As dynamic object argument:
<div style="{{ style }}"></div>
. It will be diffed on update and only changed variable will be applied to element style.
As a way to organizing styles, domc have small styles
CSS-in-JS utility. It will generate necessary classNames and inline styles, along with update function. Style rules will be inserted into generated style
tag in the document head.
import styles from 'domc/styles'
// Static styles
const s = styles({
base: {
display: 'flex',
// pseudo-classes and pseudo-selectors are supported
'::before': {
content: '>'
}
}
})
// s will have s.base === 'base-a'
// styles will generate uniq alphabet tokens and append it to the end of className
// Dynamic styles
// Used to generate styles per component instance, from scope props
const s = styles(({color}) => ({
base: {
display: 'flex',
color
}
}))
const instanceStyles = s({color: 'black'})
// instanceStyles.base === 'base-b'
// With inline styles
// Used for highly dynamic values, when creating new style every time meaningless
const s = styles(({color}) => ({
base: {
display: 'flex',
color,
// inline should be function
inline: ({posX, posY}) => ({
top: posY + 'px',
left: posX + 'px'
})
}
}))
const instanceStyles = s({color: 'black', posX: 0, poxY: 0})
// instanceStyles.base === 'base-c'
// instanceStyles.inline.base === {top: '0px', left: '0px'}
...
instanceStyles({posX: 100, posY: 200})
// instanceStyles.inline.base === {top: '100px', left: '200px'}
Custom Directives
Example of directives can be found in implementation of v-for
and v-map
algorithms.
To create directive, we need to import customDirectives
from domc, it's plain object,
and assign setup code to corresponding v-key.
import {customDirectives} from 'domc'
// The setup function has following api:
// - node argument, that has this directive
// - directive is nodeAttribute value, which can be parsed for directive needs
function setup(node, directive) {
...
// It should return function, that accept scope argument and produce necessary updates on node
return function(scope) {
...
}
}
// To bind setup function to directive key, extend customDirectives object
customDirectives.myDirective = setup
// After that you can call directive from template
// <div v-myDirective="arg"></div>
How it works
The idea is simple:
- Walk the node and store references to dynamic parts,
- Use references in updater function to effectively update the node.
According to morphdom there are some DOM properties that doesn't require additional computation and therefore they're very fast to work with. Instead of building DOM by .createElement API calls with a lot of memory garbage, it's more effective to clone template node and use fast props to obtain references, with as less performance overhead as possible.
// Given following node
// <tr class="{{ item.id === selected ? 'danger' : '' }}">
// <td class="col-md-1">{{ item.id }}</td>
// <td class="col-md-4">
// <a onclick="select(item)">{{ item.label }}</a>
// </td>
// <td class="col-md-1"><a onclick="del(item)"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span></a></td>
// <td class="col-md-6"></td>
// </tr>
domc(node)
// This will produce function to clone and walk the node and get references to dynamic parts
// Also, codegen will produce update function, that can later be used to 'rerender' instance
(function anonymous(scope,node,utils,rehydrate
) {
if (rehydrate !== true) node = node.cloneNode(true);
let _f = node.firstChild;
let _f_f = _f.firstChild;
let _f_n = _f.nextSibling;
let _f_n_f = _f_n.firstChild;
let _f_n_f_f = _f_n_f.firstChild;
let _f_n_n = _f_n.nextSibling;
let _f_n_n_f = _f_n_n.firstChild;
let _f_n_n_f_f = _f_n_n_f.firstChild;
let _f_n_n_n = _f_n_n.nextSibling;
_f_n_f.__click = scope.select;
_f_n_n_f.__click = scope.del;
let current = {};
node.update = function(scope) {
const {item,selected,} = scope;
const vdom = {};
vdom.a = `${item.id === selected ? 'danger' : ''}`;
vdom.b = `${item.id}`;
vdom.c = item;
vdom.d = `${item.label}`;
vdom.e = item;
if (current.a !== vdom.a) node.className = vdom.a;
if (current.b !== vdom.b) _f_f.nodeValue = vdom.b;
if (current.c !== vdom.c) _f_n_f.__clickData = vdom.c;
if (current.d !== vdom.d) _f_n_f_f.nodeValue = vdom.d;
if (current.e !== vdom.e) _f_n_n_f.__clickData = vdom.e;
current = vdom;
}
return node;
})