forked from WebReflection/hyperHTML
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathComponent.js
160 lines (156 loc) · 5.17 KB
/
Component.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
'use strict';
// 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.
class Component {}
Object.defineProperty(exports, '__esModule', {value: true}).default = Component
// Component is lazily setup because it needs
// wire mechanism as lazy content
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;
}}
}
);
}
exports.setup = setup
// 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}
}
);