UNPKG

hyperhtml

Version:

A Fast & Light Virtual DOM Alternative

164 lines (158 loc) 5.38 kB
import CustomEvent from '@ungap/custom-event'; import Map from '@ungap/essential-map'; import WeakMap from '@ungap/weakmap'; // hyperHTML.Component is a very basic class // able to create Custom Elements like components // including the ability to listen to connect/disconnect // events via onconnect/ondisconnect attributes // Components can be created imperatively or declaratively. // The main difference is that declared components // will not automatically render on setState(...) // to simplify state handling on render. export default function Component() { return this; // this is needed in Edge !!! } // Component is lazily setup because it needs // wire mechanism as lazy content export function setup(content) { // there are various weakly referenced variables in here // and mostly are to use Component.for(...) static method. const children = new WeakMap; const create = Object.create; const createEntry = (wm, id, component) => { wm.set(id, component); return component; }; const get = (Class, info, context, id) => { const relation = info.get(Class) || relate(Class, info); switch (typeof id) { case 'object': case 'function': const wm = relation.w || (relation.w = new WeakMap); return wm.get(id) || createEntry(wm, id, new Class(context)); default: const sm = relation.p || (relation.p = create(null)); return sm[id] || (sm[id] = new Class(context)); } }; const relate = (Class, info) => { const relation = {w: null, p: null}; info.set(Class, relation); return relation; }; const set = context => { const info = new Map; children.set(context, info); return info; }; // The Component Class Object.defineProperties( Component, { // Component.for(context[, id]) is a convenient way // to automatically relate data/context to children components // If not created yet, the new Component(context) is weakly stored // and after that same instance would always be returned. for: { configurable: true, value(context, id) { return get( this, children.get(context) || set(context), context, id == null ? 'default' : id ); } } } ); Object.defineProperties( Component.prototype, { // all events are handled with the component as context handleEvent: {value(e) { const ct = e.currentTarget; this[ ('getAttribute' in ct && ct.getAttribute('data-call')) || ('on' + e.type) ](e); }}, // components will lazily define html or svg properties // as soon as these are invoked within the .render() method // Such render() method is not provided by the base class // but it must be available through the Component extend. // Declared components could implement a // render(props) method too and use props as needed. html: lazyGetter('html', content), svg: lazyGetter('svg', content), // the state is a very basic/simple mechanism inspired by Preact state: lazyGetter('state', function () { return this.defaultState; }), // it is possible to define a default state that'd be always an object otherwise defaultState: {get() { return {}; }}, // dispatch a bubbling, cancelable, custom event // through the first known/available node dispatch: {value(type, detail) { const {_wire$} = this; if (_wire$) { const event = new CustomEvent(type, { bubbles: true, cancelable: true, detail }); event.component = this; return (_wire$.dispatchEvent ? _wire$ : _wire$.firstChild ).dispatchEvent(event); } return false; }}, // setting some property state through a new object // or a callback, triggers also automatically a render // unless explicitly specified to not do so (render === false) setState: {value(state, render) { const target = this.state; const source = typeof state === 'function' ? state.call(this, target) : state; for (const key in source) target[key] = source[key]; if (render !== false) this.render(); return this; }} } ); } // instead of a secret key I could've used a WeakMap // However, attaching a property directly will result // into better performance with thousands of components // hanging around, and less memory pressure caused by the WeakMap const lazyGetter = (type, fn) => { const secret = '_' + type + '$'; return { get() { return this[secret] || setValue(this, secret, fn.call(this, type)); }, set(value) { setValue(this, secret, value); } }; }; // shortcut to set value on get or set(value) const setValue = (self, secret, value) => Object.defineProperty(self, secret, { configurable: true, value: typeof value === 'function' ? function () { return (self._wire$ = value.apply(this, arguments)); } : value })[secret] ; Object.defineProperties( Component.prototype, { // used to distinguish better than instanceof ELEMENT_NODE: {value: 1}, nodeType: {value: -1} } );