src/decorators/utils.js
import pick from '@f/pick';
import mapObj from '@f/map-obj';
import t from 'tcomb';
import { vnode as element } from 'deku';
const { isThunk, isText, isEmpty } = element;
export function normalize(comp) {
const component = t.Function.is(comp) ? { render: comp } : comp;
if(!t.Object.is(component) && !t.Function.is(component.render))
throw new TypeError('component must be function or object with render function');
if(!component.name && component.render.name !== '') component.name = component.render.name;
return component;
}
export function mapAttributes(node, f, deep = false) {
if(isText(node) || isEmpty(node)) return node;
const name = isThunk(node) ? 'props' : 'attributes';
const spec = {};
spec[ name ] = mapObj(f, node[ name ]);
if(deep) spec.children = node.children.map(child => mapAttributes(child, f, deep));
return Object.assign({}, node, spec);
}
export const isHandler = name => (
name.length > 2 &&
name.slice(0, 2) === 'on' &&
name[ 2 ] === name[ 2 ].toUpperCase()
);
const defaultOpts = {
deep: false,
cache: null,
model: (component, model) => model,
render: (component, model) => component.render(model),
onCreate: (component, model) => component.onCreate ? component.onCreate(model) : null,
onUpdate: (component, model) => component.onUpdate ? component.onUpdate(model) : null,
onRemove: (component, model) => component.onRemove ? component.onRemove(model) : null
};
function applySpec(spec, rawComponent) {
const cacheEntry = spec.cache ? spec.cache.get(rawComponent) : null;
if(cacheEntry) return cacheEntry;
const component = normalize(rawComponent);
const hooks = pick(['onCreate', 'onUpdate', 'onRemove'], spec);
const componentSpec = mapObj(hook => model => hook(component, spec.model(component, model)), hooks);
componentSpec.render = function(model) {
const decoratedModel = spec.model(component, model);
const node = spec.render(component, decoratedModel);
return spec.deep ? decorateNode(spec, node) : node;
};
const decorated = Object.assign({}, component, componentSpec);
if(spec.cache) spec.cache.set(rawComponent, decorated);
return decorated;
}
function decorateNode(spec, node) {
const nodeSpec = {};
if(isThunk(node)) nodeSpec.component = applySpec(spec, node.component);
if(!t.Nil.is(node.children)) nodeSpec.children = node.children.map(child => decorateNode(spec, child));
return Object.assign({}, node, nodeSpec);
}
export function specDecorator(options) {
const cache = options.deep && !options.cache ? new WeakMap() : options.cache;
const spec = Object.assign({}, defaultOpts, options, { cache });
return function(component) {
const decorated = applySpec(spec, component);
return decorated;
};
}