Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/Freak613/domc

DOM compiler
https://github.com/Freak613/domc

Last synced: 15 days ago
JSON representation

DOM compiler

Awesome Lists containing this project

README

        

# 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

```javascript
import domc from 'domc'

// Given following node
//


//
{{ name }}

//
{{ surname }}

//

// 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

![Performance](performance.png?raw=true "Performance")

## 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](https://github.com/infernojs/inferno/blob/master/README.md#linkevent-package-inferno) 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 ``
- Or using `is` attribute: ``. It's used to overcome some DOM limitations, when you can't put custom component in DOM tree, for example `tbody` could have only `tr` 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.

```javascript
domc.component('todo-item', `


  • {{ todo.text }}


  • `)
    domc.component('app-body', `




    `)
    ```

    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.

    ```javascript
    domc.component('app-body', `





    `)
    ```

    So, basic example looks like this:

    ```javascript
    // 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: `


  • {{ item.text }} {{ localVar }} {{ someCond }}


  • `
    })

    // domc.app returns function to render component as starting point of application
    const c = domc.app(`





    `)

    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.

    ```javascript
    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: `


    {{ myVar }}


    `
    })
    ```

    ## Slots

    It's possible to insert other Nodes into template.

    ```javascript
    domc.component('app', `




    `,
    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

    ```javascript
    domc.component('container-component', `




    `)

    domc.component('app', `

    1

    2

    3

    `)
    ```

    ## Styles

    Inline styles are supported in following ways:
    - As a simple non-dynamic argument, `

    `
    - As dynamic object argument: `
    `. 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.

    ```javascript
    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.

    ```javascript
    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
    //


    ```

    ## 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](https://github.com/patrick-steele-idem/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.

    ```javascript
    // Given following node
    //
    // {{ item.id }}
    //
    // {{ item.label }}
    //
    //
    //
    //

    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;
    })
    ```