Katochimoto/xblocks-core

View on GitHub
src/xblocks/utils/Component.js

Summary

Maintainability
A
2 hrs
Test Coverage
import React, { PropTypes } from 'react';
import get from 'lodash/get';
import isPlainObject from 'lodash/isPlainObject';
import identity from 'lodash/identity';
import clone from 'lodash/clone';
import global from '../../context';
import { getFactory } from '../view';
import Constants from '../constants';

/**
 * The basic component. Required for the formation of context.
 *
 * @example
 * import Component from 'xblocks-core/utils/Component';
 * React.render(Component({ property: 'test' }), node);
 *
 * @module xblocks-core/utils/Component
 * @returns {function}
 */
export default React.createFactory(React.createClass({
    displayName: 'xb-component',

    /**
     * Types of context
     * @property {Object} childContextTypes
     * @property {HTMLElement} childContextTypes.container the node associated with the view
     * @property {function} childContextTypes.content
     * @property {function} childContextTypes.template
     * @property {function} childContextTypes.isEmptyContent check empty content
     */
    childContextTypes: {
        container: PropTypes.any,
        content: PropTypes.func,
        template: PropTypes.func,
        isEmptyContent: PropTypes.func
    },

    /**
     * The context value
     * @returns {{ container: HTMLElement, content: function }}}
     */
    getChildContext() {
        return {
            container: this.props._container,

            /**
             * Output function user content
             * @param {ReactElement} element
             * @returns {ReactElement}
             */
            content: (element) => (
                <ComponentContent {...{ element }}>
                    {this.props.children}
                </ComponentContent>
            ),

            /**
             * Create node by template.
             * @param {string} tmplName template name
             * @param {ReactElement} element
             * @param {function} [interceptor] custom conversion template
             * @returns {?ReactElement}
             */
            template: (tmplName, element, interceptor) => (
                <ComponentTemplate {...{ tmplName, element, interceptor }} />
            ),

            /**
             * Check empty content.
             * @returns {boolean}
             */
            isEmptyContent: () => {
                const isShadow = get(this.props._container, [ Constants.BLOCK, 'isShadow' ], false);

                if (isShadow) {
                    return !this.props._container.hasChildNodes();

                } else {
                    return !this.props.children;
                }
            }
        };
    },

    /**
     * @returns {ReactElement}
     */
    render() {
        const tagName = this.props._container[ Constants.TAGNAME ];
        const props = clone(this.props);

        delete props._container;
        props.ref = (ref) => this.userComponent = ref;

        return getFactory(tagName)(props);
    }
}));

/**
 * Item output custom content.
 * @param {Object} props
 * @param {ReactElement} [props.element]
 * @param {string} props.children
 * @param {Object} context
 * @param {HTMLElement} context.container the node associated with the view
 * @returns {ReactElement}
 * @private
 */
const ComponentContent = function (props, context) {
    const isShadow = get(context.container, [ Constants.BLOCK, 'isShadow' ], false);

    if (isShadow) {
        if (global.HTMLSlotElement) {
            return <slot />;

        } else {
            return <content />;
        }
    }

    const elementProps = {
        'data-xb-content': context.container[ Constants.UID ],
        'dangerouslySetInnerHTML': { __html: props.children }
    };

    if (props.element) {
        return React.cloneElement(props.element, elementProps);

    } else {
        return <span {...elementProps} />;
    }
};

/**
 * Types of context
 * @property {Object} contextTypes
 * @property {HTMLElement} contextTypes.container the node associated with the view
 */
ComponentContent.contextTypes = {
    container: PropTypes.any
};

/**
 * Item output custom template content.
 * @param {Object} props
 * @param {string} props.tmplName
 * @param {ReactElement} [props.element]
 * @param {function} [props.interceptor]
 * @param {Object} context
 * @param {HTMLElement} context.container the node associated with the view
 * @returns {ReactElement}
 * @private
 */
const ComponentTemplate = function (props, context) {
    const isShadow = get(context.container, [ Constants.BLOCK, 'isShadow' ], false);

    if (isShadow) {
        if (global.HTMLSlotElement) {
            return <slot name={props.tmplName} />;

        } else {
            return <content select={props.tmplName} />;
        }
    }

    const templates = context.container[ Constants.TMPL ];

    if (isPlainObject(templates) && templates.hasOwnProperty(props.tmplName)) {
        const interceptor = props.interceptor || identity;
        const elementProps = {
            'dangerouslySetInnerHTML': { __html: interceptor(templates[ props.tmplName ]) }
        };

        if (props.element) {
            return React.cloneElement(props.element, elementProps);

        } else {
            return <div {...elementProps} />;
        }
    }

    return null;
};

/**
 * Types of context
 * @property {Object} contextTypes
 * @property {HTMLElement} contextTypes.container the node associated with the view
 */
ComponentTemplate.contextTypes = {
    container: PropTypes.any
};