Home Reference Source

src/Component.js


import { el } from './renderer';
import { convertToNode } from './converter';

/**
 * Class for a component.
 * ```
 *   class HelloWorldComponent extends Component {
 *      render() {
 *          return (
 *              <h1>{this.props.message}</h1>
 *          );
 *      }
 *   }
 *
 *   renderToDom('container', <HelloWorldComponent messsage="Hello World!"/>);
 * ```
 */
export class Component {
    static isComponent = true;

    /**
     * The state of the current store
     * @type {Object}
     */
    get state() {
        return this.store.state;
    }

    /**
     * Constructor of a component.
     * @param {Object} props the props (attributes pass to dom node + children)
     * @param {Store} store the store.
     */
    constructor(props, store = undefined) {
        /**
         * The props  (attributes pass to dom node + children).
         * @type {Object}
         */
        this.props = props;
        /**
         * The store share between all components.
         * @type {Store}
         */
        this.store = store;

        Object.getOwnPropertyNames(Object.getPrototypeOf(this))
            .filter(name => name !== 'constructor')
            .filter(name => this[name] instanceof Function)
            .forEach(name => this[name] = this[name].bind(this));

        for (const event of this.eventsToSubscribe()) {
            this.store.subscribe(event, this.reactToChangeState.bind(this), this);
        }

        for (const event of this.eventsToReact()) {
            this.store.subscribe(event, this.react.bind(this), this);
        }

        if (this.componentDidUnmount) {
            this.store.componentsToUnmount.push(this);
        }

        this.props.ref && this.props.ref(this);
    }


    /**
     * Do not touch :)
     * @param {Node} node parent node of the component.
     */
    nodeRefHandler(node) {
        /**
         * Parent node of the component.
         * @type {Node}
         */
        this.node = node;
        if (this.otherRef) {
            this.otherRef(node);
        }
    }

    /**
     * Method to call to refresh the component without the use of the store.
     * Useful for internal state with this.
     */
    refresh() {
        const componentList = [];
        const oldNode = this.node;
        if (!oldNode) {
            console.warn("It's you've done double refresh on same component, please don't do this", new Error());
        }
        this.node = null;
        let newNode = convertToNode(this.renderComponent(this.otherRef), this.store, componentList);
        if (newNode === undefined || newNode === null) {
            oldNode && oldNode.parentNode && oldNode.parentNode.removeChild(oldNode);
            this.componentDidMount();
            return;
        }
        oldNode && oldNode.parentNode && oldNode.parentNode.replaceChild(newNode, oldNode);
        this.store.refreshComponentsToObserve && this.store.refreshComponentsToObserve();

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

    /**
     * Internally method which is called when an event of {@link eventsToSubscribe}
     * You can override the method if you want a specific
     * @param {string} event event received in the store.
     * @param {Object} newState the new state.
     * @param {Object} oldState the old state.
     */
    reactToChangeState(event, newState, oldState) {
        if (!this.mustRefresh(oldState, newState) || !this.node) {
            return;
        }

        this.componentDidUnmount && this.componentDidUnmount();
        this.refresh();
        this.componentDidMount();
    }

    /**
     * Method to implement to react when an event is send to {@link Store}
     * @return {Array} array of string events to react.
     */
    eventsToSubscribe() {
        return [];
    }


    /**
     * Method to implement to react when an event that does not imply re rendering is sent to {@link Store}
     * @return {Array} array of string events to react without rendering.
     */
    eventsToReact() {
        return [];
    }

    /**
     * Method to implement called when an event that does not imply re rendering is sent to {@link Store}
     * @param {String} event to react on
     */
    react(event) {
    }

    /**
     * Return false to avoid call to render on an event.
     * @param {Object} oldState the old state.
     * @param {Object} newState the new state.
     */
    mustRefresh(oldState, newState) {
        return true;
    }

    /**
     * Method called after rendered into DOM.
     * @abstract
     */
    componentDidMount() {
    }

    /**
     * Method use internally to render a component.
     * @return {Component|Object} This return a component or result of SimpleDom.el.
     */
    renderComponent(otherRef) {
        const result = this.render();
        this.otherRef = otherRef;
        if (result === undefined || result === null) {
            return el('div',
                {
                    ref: node => this.nodeRefHandler(node),
                    style: {
                        width: 0,
                        height: 0
                    }
                }
            );
        }

        if (result.isElem) {
            let oldRef = result.attrs.ref;
            result.attrs.ref = node => {
                oldRef && oldRef(node);
                this.nodeRefHandler(node);
            };
        } else if (result.isComponent) {
            result.otherRef = node => this.nodeRefHandler(node);
        } else {
            console.error('Unkown result type for a component', result);
        }

        return result;
    }

    /**
     * Method to implement to render something.
     * @abstract
     * @return {Component|Object} This return a component or result of {@link el}.
     */
    render() {
        return undefined;
    }

}