hyperhtml
Version:
A Fast & Light Virtual DOM Alternative
164 lines (158 loc) • 5.38 kB
JavaScript
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}
}
);