react-transclusion
Version:
Render arbitrary components into outlets for use in dynamic layouts.
211 lines (183 loc) • 6.21 kB
JavaScript
'use strict';
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var React = require('react');
var PropTypes = React.PropTypes;
var _React$PropTypes = React.PropTypes,
string = _React$PropTypes.string,
object = _React$PropTypes.object,
node = _React$PropTypes.node,
bool = _React$PropTypes.bool,
func = _React$PropTypes.func;
/**
* @namespace UI.Components
*
* An outlet is a container element that allows you to render other components
* inside of it. These elements may be registered at boot-time, and they
* will be rendered in the correct place in the UI at the correct time.
*/
module.exports = React.createClass({
displayName: 'Outlet',
contextTypes: {
outletManager: PropTypes.object.isRequired
},
propTypes: {
/**
* @property {String}
* A unique name for this outlet. Elements will use this name to
* plug into it.
*/
name: string.isRequired,
/**
* @property {String} [tagName="div"]
* The HTML tag to use for the outlet root node.
*/
tagName: string,
/**
* @property {Object} [tagProps={}]
*/
tagProps: object,
/**
* @property {Object} [elementProps={}]
* The props to inject into the rendered elements, if any.
*/
elementProps: object,
options: object,
/**
* @property {React.Component}
*
* Children passed to the outlet are handled in a special way based on
* the flags you specify. Generally, if children were rendered, they will
* be placed *after* the outlet elements.
*
* The default behavior is to render the children only if no elements
* were rendered (either none were defined, or none matched), but:
*
* - when [[@forwardChildren]] is turned on, the outlet will simply pass
* those children to the elements.
*
* - when [[@alwaysRenderChildren]] is turned on, the outlet will insert
* the children after any rendered elements
*/
children: node,
/**
* @property {Boolean} [firstMatchingElement=false]
*
* Render only a single element at all times and that is the first one
* that matches (ie, yields true in a `match()` routine it defined when it
* was registered.)
*/
firstMatchingElement: bool,
/**
* @property {Boolean} [alwaysRenderChildren=false]
*
* Whether we should unconditionally render the children you pass to the
* outlet.
*/
alwaysRenderChildren: bool,
/**
* @property {Boolean} [forwardChildren=false]
*
* Whether we should not render the children ourselves, and instead pass
* them on to the outlet elements to render for themselves.
*
* This likely assumes you're expecting a single element and that it's
* responsible for rendering those children, which is usually the case for
* layout components.
*/
forwardChildren: bool,
/**
* @property {Outlet~fnRenderElementCallback}
*
* Override the routine that renders a single element.
*
* @callback Outlet~fnRenderElementCallback
*
* @param {String} key
* The key to use for the rendered element. This *MUST* be placed.
*
* @param {Object} elementProps
* The props to render the element with.
*
* @param {React.Component} Component
* The element component type.
*/
fnRenderElement: func
},
getDefaultProps: function getDefaultProps() {
return {
children: null,
tagName: 'div',
tagProps: {},
elementProps: {},
options: {},
alwaysRenderChildren: false,
forwardChildren: false,
fnRenderElement: null
};
},
render: function render() {
var children = [];
var elementProps = ElementProps(this.props);
var elementInstances = this.renderElements(elementProps);
var hasElements = [].concat(elementInstances).filter(truthy).length > 0;
if (hasElements) {
children.push(elementInstances);
}
if (!hasElements || this.props.alwaysRenderChildren) {
children.push(this.props.children);
}
return React.createElement(
this.props.tagName,
this.props.tagProps,
children
);
},
renderElements: function renderElements(elementProps) {
var elements = this.context.outletManager.getElements(this.props.name);
if (this.props.firstMatchingElement) {
return this.renderFirstMatchingElement(elements, elementProps);
}
if (elements.length === 0) {
return null;
} else if (elements.length === 1) {
return this.renderElement(elements[0], elementProps);
} else {
return elements.map(this.renderElementWithProps(elementProps));
}
},
renderElement: function renderElement(element, elementProps) {
if (element.match && !element.match(elementProps)) {
return null;
}
var Component = element.component;
if (this.props.fnRenderElement) {
return this.props.fnRenderElement(element.key, elementProps, Component, this.props.options);
} else {
return React.createElement(Component, _extends({
key: element.key,
$outletOptions: this.props.options
}, elementProps));
}
},
renderElementWithProps: function renderElementWithProps(elementProps) {
var _this = this;
return function (element) {
return _this.renderElement(element, elementProps);
};
},
renderFirstMatchingElement: function renderFirstMatchingElement(elements, elementProps) {
for (var i = 0; i < elements.length; ++i) {
var element = elements[i];
if (element.match && element.match(elementProps)) {
return this.renderElement(element, elementProps);
}
}
return null;
}
});
function ElementProps(props) {
return props.forwardChildren ? Object.assign({}, props.elementProps, { children: props.children }) : props.elementProps;
}
function truthy(x) {
return !!x;
}