How to write

your React

why that should not be done

the author

Andreev Sergey

vk: dragorWW

tw: dragorWW

fb: dragorWW

My way

idea and not a solution

prologue

frontend fears

Common

web applications

Architecture

solve your problem

web components

twig templates

component tree

Component

  • template.twig

  • index.js

  • style.less

// components/ComponentName/index.js
import  './style.less';
import Component from 'component-vdom/Component';

class ComponentName extends Component {}
{% component 'name' with {param: 'value'} %}
{% component 'name' embed %}
    {% block name %}
    {% endblock %}
{% endcomponent %}

Component

{% set ui = ui|default({}) %}
{% set ui = ui|merge({size: 'normal'}) %}
{% set ui = ui|merge({type: 'text'}) %}

<div class="field-text">
    <input class="field-text__input
                  field-text__input_size_{{ ui.size|default('normal') }}
                 "
           type="{{ ui.type|default('text') }}"
           {% if data.id %} id="{{ data.id }}" {% endif %}
           name="{{ data.name }}"
           {% if ui.placeholder %}
              placeholder="{{ ui.placeholder|e }}" 
           {% endif %}
           value="{{ data.value|e }}"
    >
</div>

awesome

  • just working

  • Backend can edit

  • SSR

Is growing

dynamics

problems

  • every reload page :(

  • rerender

  • reinit component

Hybrid

web applications

It is necessary

to rewrite all!

Between

SPA and common

Idea

maeta language

Works everywhere

SSR

It is good ?

virtual dom

Document

Object Model

Virtual DOM

virtual dom

  • vnode

  • vtree

  • vdom

  • VNode

  • VText

virtual-hyperscript

var h = require('virtual-dom/h')

var tree = h('div.foo#some-id', [
    h('span', 'some text'),
    h('input', { type: 'text', value: 'foo' })
])

Widget

var h = require("virtual-dom").h;
var createElement = require("virtual-dom").create;

class Widget {
    get type() {
        return 'Widget';
    }
    constructor() {}
    init() {
        return createElement(h("div", "Count is: " + this.count))
    }
    update(previous, domNode) {}
    destroy(domNode) {}
}

Hook

export default class Hook {
    constructor () {}
    hook (node, propertyName, previousValue) {}
    unhook () {}
}

Update

var diff = require("virtual-dom").diff
var patch = require("virtual-dom").patch

var myCounter = new OddCounterWidget()
var currentNode = myCounter
var rootNode = createElement(currentNode)

// A simple function to diff your widgets, and patch the dom
var update = function(nextNode) {
  var patches = diff(currentNode, nextNode)
  rootNode = patch(rootNode, patches)
  currentNode = nextNode
}

document.body.appendChild(rootNode)
setInterval(function(){
  update(new OddCounterWidget())
}, 1000)

Problems

  • every reload page :(

  • rerender

  • reinit component

proof of concept

Plan

profit

twig -> vdom

Augerening

function (data) {
    let html = '<input value="';
    html+=  twig
        .getData('data.value')
        .filter(twig.filter.e)
        .raw();
    html+= '">';
    return html;
}
<input value="{{ data.value|e }}">
function (data) {
    return h('input',{
        value: twig
            .getData('data.value')
            .filter(twig.filter.e)
            .raw()
    });
}

Reality

twig({ id: "template.twig",
    data: [{
        "type": "raw", "value": "<input value=\""
    }, {
        "type": "output",
        "stack": [{
            "type": "Twig.expression.type.variable",
            "value": "data",
            "match": ["data"]
        },{
            "type": "Twig.expression.type.filter",
            "value": "e",
            "match": ["|e", "e"]
        }]
    },
    {
        "type": "raw", "value": "\">"
    }],
});
<input value="{{ data.value|e }}">

Old Plan

twig -> fixed twig -> html with data -> vdom -> ... profit

twig -> vdom -> profit

new Plan

problems

  • save twig component

  • parse html to vdom

  • vdom witch component

Extending Twig

<div id="root">
    {% component 'entry/Main' with {data: userData} only %}
</div>

Extending parser

Twig.exports.extendTag({
    type: 'component',
    regex: /^component\s+(ignore missing\s+)?(.+?)\s*(?:with\s+([\S\s]+?))?\s*(only)?$/,
    next: [],
    open: true,
    parse: function (token, context, chain) {
        var data = encodeURI(JSON.stringify(innerContext));
        var str = '<component name="' + file + '" data="' + data + '"></component>';

        return {
            chain: chain,
            output: str
        };
    }
});

Base class

import BaseComponent from 'component-vdom/src/widget/Component';

export default class Component extends BaseComponent {
    getComponentCallback (name) {
        return require('components/' + name + '/index.js').default;
    }
}
import template from './template.twig';
import Component from 'components/Component';

export default class FieldText extends Component {
    get template () {
        return template;
    }
}

VNode component

function (getComponentCallback) {
    return (tagName, properties, children, key, namespace) => {
        if (tagName === 'component') {
            let data = JSON.parse(decodeURI(properties.data));

            let Component = getComponentCallback(properties.name);

            return new Component(data, children);
        }
        return new VNode(tagName, properties, children, key, namespace);
    }
}

problems

  • save twig component

  • parse html to vdom

  • vdom witch component

Features

// TODO:

  • ref node

  • onClick="function"

  • bind event selector

  • onMount, onUnMount

  • children

  • redux

ref node

export const isRef = (key) => 'ref' === key;

export default (vnode, component, first = false) => {
    if (hashAttributes(vnode)) {
        const attributes = Object.keys(vnode.properties.attributes);
        attributes
            .filter(isRef)
            .forEach((attr) => {
                const refName = vnode.properties.attributes[attr];
                vnode.properties.ref = new HookRef(component, refName);
            });
    }
}
class HookRef extends Hook {
    constructor (component, refName) {
        super();
        this.component = component;
        this.refName = refName;
    }

    hook (node) {
        this.component.refs[this.refName] = node;
    }
}

onClick="function"

export const isEventNamedAttr = (key) => key.startsWith('on');

export default (vnode, component, first = false) => {
    if (hashAttributes(vnode)) {
        const attributes = Object.keys(vnode.properties.attributes);
        attributes
            .filter(isEventNamedAttr)
            .forEach((attr) => {
                const functionName = vnode.properties.attributes[attr];
                if (component[functionName]) {
                    vnode.properties[attr] = component[functionName]
                        .bind(component, data);
                }
            });
    }
}

onMount, onUnMount

import HookMount from '../hook/Mount';
export default (vnode, component, first = false) =>  {
    if (first) {
        vnode.properties.hookMount = new HookMount(component);
    }
}
import nextTick from 'next-tick';

export default class HookMount extends Hook {
    hook (node) {
        nextTick(this.component.onMount.bind(this.component, node));
    }

    unhook () {
        this.component.onUnMount.bind(this.component);
    }
}

children

Redux

import App from 'component-vdom/src/App';
import EntryMain from  'components/entry/Main';
import {createStore} from 'redux';
const initialState = {value: 0};
const reducers = (state = initialState, action) => {
    switch (action.type) {
      case 'INCREMENT':
        return {value: value+1}
      case 'DECREMENT':
        return {value: value-1}
      default:
        return state
    }
};

let store = createStore(reducers);
let app = new App(initialState, EntryMain);
app.appendTo(document.querySelector('#root'));
store.subscribe(() => {
    app.update(store.getState());
});

Redux

{# Main/template.twig #}
<div class="entry-main">
        Clicked: {{value}} times
        <button onClick="increment">+</button>
        <button onClick="decrement">-</button>
</div>
export default class EntryMain extends Component {
    get template () {
        return template;
    }
    increment() {
        store.dispatch({ type: 'INCREMENT' })
    }
    decrement() {
        store.dispatch({ type: 'DECREMENT' })
    }
}

prologue

A few words about all

  • ~ 30 lib

  • rewrite twig & twig-loader

  • ~50 issues & pull request

  • html-to-vdom

  • main-loop

  • next-tick

  • virtual-dom

  • virtual-html

  • vtree-select

  • twig

  • twig-loader

frontend fears

open source

Thank you!

waiting for your questions

How to write your React - why that should not be done

By Sergey Andreev

How to write your React - why that should not be done

  • 1,567