Home Reference Source

src/renderer.js

import { convertToNode } from './converter';
import { flatten, dasherize, isFunction } from './util';
import { Store } from './Store';


/**
 * JSX factory function to create an object representing a dom node. Designed to be used with a JSX transpiler.
 * @param {Object|Component|string|function} element the name of the tag, or a {@link Component}.
 * @param {Object} attrs properties of the node, a plain old JS object. Not optional, if no value, put empty object.
 * @param {Array} children the children of the node, a vararg
 * @return {Object} an object representing a dom node.
 */
export function el(element, attrs, ...children) {
    if (element && element.isComponent) {
        const props = {
            ...attrs,
            children: (flatten(children) || []).filter(child => child !== null && child !== undefined)
        };
        return {
            isComponent: true,
            componentClass: element,
            props
        };
    } else {
        if (isFunction(element)) {
            return element(attrs, ...children);
        }
        return {
            name: element,
            attrs: attrs || {},
            children: (flatten(children) || []).filter(child => child !== null && child !== undefined),
            isElem: true
        };
    }
}

function cleanAnGetNode(node) {
    let realNode = node;
    if (typeof node === 'string') {
        realNode = document.getElementById(node);
    }

    while (realNode.firstChild) {
        realNode.removeChild(realNode.firstChild);
    }
    return realNode;
}

/**
 * Render a component to the dom.
 * @param {string|Node} node the id or the node where the component must be rendered.
 * @param {Component} component the component to render.
 * @param {Store} store the store
 */
export function renderToDom(node, component, store = new Store()) {

    renderComponents(node, [component], store);
}

function renderComponents(node, components, store = new Store()) {

    const realNode = cleanAnGetNode(node);

    const componentList = [];

    flatten(components).filter(component => component !== undefined && component !== null)
        .map(component => convertToNode(component, store, componentList))
        .forEach(node => realNode.appendChild(node));

    componentList.forEach(component => component.componentDidMount());

    if (componentList.length) {

        

        store.refreshComponentsToObserve = function() {
            if (!document.body.contains(realNode)) {
                store.mutationObserver && store.mutationObserver .disconnect();
                store.unsubscribeAll();
            }
            for (let index = store.componentsToUnmount.length -1; index >= 0; index--) {
                const component = store.componentsToUnmount[index];
                if (component.node && !realNode.contains(component.node)) {
                    component.componentDidUnmount();
                    store.componentsToUnmount.splice(index, 1);
                }
            }
            for (let index = store.componentsSubscribes.length - 1; index >= 0; index--) {
                const component = store.componentsSubscribes[index];
                if (component.component.node && !realNode.contains(component.component.node)) {
                    component.subscribes.forEach(({event, id}) => store.unsubscribeByEventAndId(event, id));
                    component.component.node = undefined;
                    store.componentsSubscribes.splice(index, 1);
                }
            }
        };
        
        store.mutationObserver = new MutationObserver(() =>
            store.refreshComponentsToObserve && store.refreshComponentsToObserve()
        );
        
        store.mutationObserver.observe(document.body, {childList: true, subtree: true});
    }
}

/**
 * Render some elements into a string.
 * @param {Array} elements elements returned by {@link el} or primitive like string.
 * @return {string} html as a string.
 */
export function renderToString(...elements) {
    return flatten(elements).map(el => {
        if (!el.name) {
            return '' + (el.__asHtml || el);
        }
        const attributes = Object.keys(el.attrs)
            .filter(attribute => !attribute.startsWith('on') && el.attrs[attribute] !== undefined && attribute !== 'ref')
            .map(attribute => {
                const key = dasherize(attribute === 'className' ? 'class' : attribute);
                let value = el.attrs[attribute];
                if (key === 'style' && typeof value === 'object') {
                    value = Object.keys(value)
                        .map(key => '' + dasherize(key) + ':' + value[key])
                        .join(';');
                } else if (key === 'class' && typeof value === 'object') {
                    value = Object.keys(value).filter(classValue => value[classValue])
                        .map(dasherize)
                        .join(' ');
                }

                return ` ${key}="${value}"`
            })
            .join('');
        const content = renderToString(...el.children);
        return `<${el.name}${attributes}>${content}</${el.name}>`
    }).join('');
}


/**
 * Render some elements into a node.
 * @param {string|Node} node the id or the node where the component must be rendered.
 * @param {Array} elements elements returned by {@link el} or primitive like string.
 */
export function renderTo(node, ...elements) {
    renderComponents(node, elements)
}