@fruit-ui/core 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +3 -2
- package/src/index.js +763 -0
- package/dist/index.js +0 -1
package/README.md
CHANGED
|
@@ -63,8 +63,8 @@ Smaller apps don't always warrant heavyweight frameworks, but interfacing with t
|
|
|
63
63
|
## Getting started
|
|
64
64
|
|
|
65
65
|
There are three ways to use FRUIT in your projects:
|
|
66
|
-
- Download and copy the [Terser-compressed JS file](https://github.com/asantagata/fruit-ui/blob/main/dist/index.js) file into your project. (This is a compressed version built with Terser; you can just as well use the [non-compressed version](https://github.com/asantagata/fruit-ui/blob/main/src/index.js).) Then you can use `import { create, replaceWith, appendChild, insertBefore } from "./modules/fruit.js"` or `<script type="module" src="./modules/fruit.js">` to access FRUIT in your JS apps.
|
|
67
|
-
- Access via browser loading, i.e., `import { create, replaceWith, appendChild, insertBefore } from "https://cdn.jsdelivr.net/npm/@fruit-ui/core@latest/index.js"`.
|
|
66
|
+
- Download and copy the [Terser-compressed JS file](https://github.com/asantagata/fruit-ui/blob/main/dist/index.js) file into your project. (This is a compressed version built with Terser; you can just as well use the [non-compressed version](https://github.com/asantagata/fruit-ui/blob/main/src/index.js) which uses JSDoc annotations.) Then you can use `import { create, replaceWith, appendChild, insertBefore } from "./modules/fruit.js"` or `<script type="module" src="./modules/fruit.js">` to access FRUIT in your JS apps.
|
|
67
|
+
- Access via browser loading, i.e., `import { create, replaceWith, appendChild, insertBefore } from "https://cdn.jsdelivr.net/npm/@fruit-ui/core@latest/src/index.js"`.
|
|
68
68
|
- With NPM installed, run `npm install @fruit-ui/core`. Then use `import { create, replaceWith, appendChild, insertBefore } from "@fruit-ui/core"`.
|
|
69
69
|
|
|
70
70
|
## Contributing
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fruit-ui/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A vanilla JS toolkit for reactive UI",
|
|
5
|
-
"main": "
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"homepage": "https://asantagata.github.io/fruit-ui/",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
8
9
|
"build": "terser .\\src\\index.js -o .\\dist\\index.js"
|
package/src/index.js
ADDED
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-string properties of Templates.
|
|
3
|
+
* @typedef {object} TemplateSpecialProperties
|
|
4
|
+
* @property {string | string[] | Object.<string, any>} [class] - The element's class.
|
|
5
|
+
* @property {CSSStyleDeclaration} [style] - The element's style.
|
|
6
|
+
* @property {Object.<string, Function>} [on] - The element's listeners (and onmount function.)
|
|
7
|
+
* Within these, `this` refers to the nearest component's This (if applicable)
|
|
8
|
+
* and `this.element` refers to the element.
|
|
9
|
+
* @property {Elementable | Elementable[]} [children] - The element's children.
|
|
10
|
+
* @property {Element} [cloneFrom] - The element to deep-clone from for this element.
|
|
11
|
+
* This overrides all other properties.
|
|
12
|
+
* @property {Object.<string, string>} [dataset] - The element's dataset.
|
|
13
|
+
* @property {string} [key] - The element's key, used to distinguish re-ordered
|
|
14
|
+
* siblings and preserve state.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A JavaScript object describing a non-reactive HTML element.
|
|
19
|
+
* All element properties not explicitly listed (e.g., href, src, title, etc.)
|
|
20
|
+
* can also be written here.
|
|
21
|
+
* @typedef {Object.<string, string> & TemplateSpecialProperties} Template
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A function which produces a Template using a This.
|
|
26
|
+
* @typedef {(this: This) => Template} TemplateProducer
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A function which produces a Template with an exposed This.
|
|
31
|
+
* @typedef {() => Template} BoundTemplateProducer
|
|
32
|
+
* @property {This} this - The bounded This.
|
|
33
|
+
* @property {ComponentID} componentId - The corresponding ComponentId.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Any representation of an Element.
|
|
38
|
+
* Primitives (number, boolean) are stringified automatically.
|
|
39
|
+
* @typedef {Component | Template | string | number | boolean} Elementable
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* An auxiliary This argument for component TemplateProducers and listeners.
|
|
44
|
+
* Note listeners also have the property `.target` describing that element.
|
|
45
|
+
* @typedef {object} This
|
|
46
|
+
* @property {HTMLElement} [element] - The component, post-mount.
|
|
47
|
+
* @property {() => void} [rerender] - The rerender function, post-mount.
|
|
48
|
+
* @property {BoundTemplateProducer} [producer] - The producer function, post-mount.
|
|
49
|
+
* @property {object} state - The component's state.
|
|
50
|
+
* @property {object} setState - Functions to set the component's state with automatic reactivity.
|
|
51
|
+
* @property {Object.<string, Binding>} bindings - The component's bindings.
|
|
52
|
+
* @property {object} [memo] - The component's memo, if given.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A child, grandchild etc. to a Component.
|
|
57
|
+
* @typedef {object} Binding
|
|
58
|
+
* @property {HTMLElement} element - The bounded element.
|
|
59
|
+
* @property {() => void} rerender - The bounded element's rerender function.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* A component, which can be rerendered reactively.
|
|
64
|
+
* @typedef {object} Component
|
|
65
|
+
* @property {TemplateProducer} render - The main render function.
|
|
66
|
+
* @property {() => Object.<string, any>} [state] - The initializer for local state.
|
|
67
|
+
* @property {string} [key] - The element's key, used to distinguish re-ordered siblings and preserve state.
|
|
68
|
+
* @property {string} [binding] - The element's binding.
|
|
69
|
+
* @property {object} [memo] - The element's memoized props, used to control rerendering.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A function which takes in props and produces a Component.
|
|
74
|
+
* @typedef {(...args: any) => Component} ComponentProducer
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A component's ID.
|
|
79
|
+
* @typedef {string} ComponentID
|
|
80
|
+
*/
|
|
81
|
+
|
|
82
|
+
/** @type {number} */
|
|
83
|
+
let globalComponentCount = 0;
|
|
84
|
+
|
|
85
|
+
/** @type {Object.<ComponentID, This>} */
|
|
86
|
+
const thisRecord = {};
|
|
87
|
+
|
|
88
|
+
/** @type {Set<ComponentID>} */
|
|
89
|
+
const rerenderQueue = new Set();
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Rerender the elements in the rerenderQueue.
|
|
93
|
+
*/
|
|
94
|
+
function rerenderEnqueuedComponents() {
|
|
95
|
+
Array.from(rerenderQueue).forEach(cId => thisRecord[cId]?.rerender());
|
|
96
|
+
rerenderQueue.clear();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Enqueue a component for rerender.
|
|
101
|
+
* @param {ComponentID} componentId - the componentId.
|
|
102
|
+
*/
|
|
103
|
+
function enqueueToRerender(componentId) {
|
|
104
|
+
const enqueueRerenderTask = rerenderQueue.size === 0;
|
|
105
|
+
rerenderQueue.add(componentId);
|
|
106
|
+
if (enqueueRerenderTask)
|
|
107
|
+
queueMicrotask(rerenderEnqueuedComponents);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create a new This.
|
|
112
|
+
* @returns {This} the new This.
|
|
113
|
+
*/
|
|
114
|
+
function createThis(component) {
|
|
115
|
+
return {
|
|
116
|
+
state: {},
|
|
117
|
+
setState: {},
|
|
118
|
+
bindings: {},
|
|
119
|
+
memo: component.memo
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Initialize an empty This.
|
|
125
|
+
* @param {This} this - the This.
|
|
126
|
+
* @param {HTMLElement} element - the HTMLElement to use as this.element.
|
|
127
|
+
* @param {BoundTemplateProducer} producer - the producer function for rerendering.
|
|
128
|
+
*/
|
|
129
|
+
function initializeThis(element, producer) {
|
|
130
|
+
this.element = element;
|
|
131
|
+
this.producer = producer;
|
|
132
|
+
this.rerender = rerender.bind(this);
|
|
133
|
+
this.setState = new Proxy({}, {
|
|
134
|
+
get: (o, p, r) => {
|
|
135
|
+
return (x) => {
|
|
136
|
+
this.state[p] = x;
|
|
137
|
+
enqueueToRerender(producer.componentId);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Creates an HTMLElement from a Template.
|
|
145
|
+
* @param {This | undefined} this - the nearest Component's This, if applicable.
|
|
146
|
+
* @param {Template} template - the Template.
|
|
147
|
+
* @param {Function[]} onMounts - the aggregated onMounts.
|
|
148
|
+
* @param {BoundTemplateProducer} [producer] - the nearest Component's producer.
|
|
149
|
+
* @returns {HTMLElement} - An HTMLElement.
|
|
150
|
+
*/
|
|
151
|
+
function createElementFromTemplate(template, onMounts, producer = null) {
|
|
152
|
+
if (template.cloneFrom) {
|
|
153
|
+
const element = template.cloneFrom.cloneNode(true);
|
|
154
|
+
if (this && !this.element) {
|
|
155
|
+
initializeThis.call(this, element, producer);
|
|
156
|
+
}
|
|
157
|
+
return element;
|
|
158
|
+
}
|
|
159
|
+
const {tag, class: c, style, on, componentId, children, cloneFrom, dataset, key, binding, innerHTML, ...rest} = template;
|
|
160
|
+
const element = document.createElement(template.tag || 'div');
|
|
161
|
+
if (template.class) {
|
|
162
|
+
switch (typeof template.class) {
|
|
163
|
+
case 'string':
|
|
164
|
+
element.className = template.class;
|
|
165
|
+
break;
|
|
166
|
+
case 'object':
|
|
167
|
+
if (Array.isArray(template)) {
|
|
168
|
+
element.className = template.class.join(" ");
|
|
169
|
+
} else {
|
|
170
|
+
element.className = Object.keys(template.class)
|
|
171
|
+
.filter(c => template.class[c]).join(" ");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (template.style) {
|
|
176
|
+
for (const k in template.style) {
|
|
177
|
+
element.style[k] = template.style[k];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (template.on) {
|
|
181
|
+
const {mount: onMount, ...listeners} = template.on;
|
|
182
|
+
for (const type in listeners) {
|
|
183
|
+
if (!listeners[type]) continue;
|
|
184
|
+
element.addEventListener(type, (event) => listeners[type].call({...(this ?? {}), target: element}, event));
|
|
185
|
+
}
|
|
186
|
+
if (onMount) {
|
|
187
|
+
onMounts.push(() => onMount.call({...(this ?? {}), target: element}));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
for (const attribute in rest) {
|
|
191
|
+
if (!template[attribute]) continue;
|
|
192
|
+
element.setAttribute(attribute, template[attribute]);
|
|
193
|
+
}
|
|
194
|
+
if (template.dataset) {
|
|
195
|
+
for (const k in template.dataset) {
|
|
196
|
+
element.dataset[k] = template.dataset[k];
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (template.componentId) {
|
|
200
|
+
element.dataset.componentId = template.componentId;
|
|
201
|
+
}
|
|
202
|
+
if (template.key) {
|
|
203
|
+
element.dataset.key = template.key;
|
|
204
|
+
}
|
|
205
|
+
if (template.binding) {
|
|
206
|
+
element.dataset.binding = template.binding;
|
|
207
|
+
}
|
|
208
|
+
if (this && !this.element) {
|
|
209
|
+
initializeThis.call(this, element, producer);
|
|
210
|
+
}
|
|
211
|
+
if (template.innerHTML) {
|
|
212
|
+
element.innerHTML = template.innerHTML;
|
|
213
|
+
} else if ('children' in template) {
|
|
214
|
+
if (Array.isArray(template.children)) {
|
|
215
|
+
element.replaceChildren(
|
|
216
|
+
...template.children.map(ct => createElementFromElementable.call(this, ct, onMounts))
|
|
217
|
+
);
|
|
218
|
+
// handle bindings
|
|
219
|
+
for (let i = 0; i < template.children.length; i++) {
|
|
220
|
+
const childTm = template.children[i];
|
|
221
|
+
if (childTm.binding && this) {
|
|
222
|
+
setBinding.call(this, childTm.binding, element.childNodes[i]);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
element.replaceChildren(createElementFromElementable.call(this, template.children, onMounts));
|
|
227
|
+
// handle binding
|
|
228
|
+
if (template.children.binding && this) {
|
|
229
|
+
setBinding.call(this, template.children.binding, element.childNodes[0]);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return element;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Binds a TemplateProducer to a new This.
|
|
238
|
+
* @param {TemplateProducer} producer - the TemplateProducer.
|
|
239
|
+
* @param {Component} component - the Component to which the TemplateProducer belongs.
|
|
240
|
+
* @returns {BoundTemplateProducer} - the BoundTemplateProducer.
|
|
241
|
+
*/
|
|
242
|
+
function bindTemplateProducer(producer, component) {
|
|
243
|
+
const newThis = createThis(component);
|
|
244
|
+
const boundProducer = producer.bind(newThis);
|
|
245
|
+
boundProducer.this = newThis;
|
|
246
|
+
boundProducer.componentId = `component-${globalComponentCount++}`;
|
|
247
|
+
return boundProducer;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Determines whether an Elementable is a Component.
|
|
252
|
+
* @param {Elementable} elementable - the Elementable.
|
|
253
|
+
* @returns {boolean} - whether it is a Component.
|
|
254
|
+
*/
|
|
255
|
+
function elementableIsComponent(elementable) {
|
|
256
|
+
return !!elementable.render;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Creates an HTMLElement or string from any Elementable.
|
|
261
|
+
* @param {This} this - the nearest Component's This.
|
|
262
|
+
* @param {Elementable} elementable - the Elementable.
|
|
263
|
+
* @param {Function[]} onMounts - the aggregated onMounts.
|
|
264
|
+
* @returns {HTMLElement | string} - the HTMLElement or string.
|
|
265
|
+
*/
|
|
266
|
+
function createElementFromElementable(elementable, onMounts) {
|
|
267
|
+
if (typeof elementable === 'object') {
|
|
268
|
+
if (elementableIsComponent(elementable)) {
|
|
269
|
+
return createElementFromComponent(elementable, onMounts);
|
|
270
|
+
} else {
|
|
271
|
+
return createElementFromTemplate.call(this, elementable, onMounts);
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
return elementable.toString();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Creates an HTMLElement from a Component.
|
|
280
|
+
* @param {Component} component - the Component.
|
|
281
|
+
* @param {Function[]} onMounts - the aggregated onMounts.
|
|
282
|
+
* @returns {HTMLElement} - An HTMLElement.
|
|
283
|
+
*/
|
|
284
|
+
function createElementFromComponent(component, onMounts) {
|
|
285
|
+
const boundProducer = bindTemplateProducer(component.render, component);
|
|
286
|
+
thisRecord[boundProducer.componentId] = boundProducer.this;
|
|
287
|
+
boundProducer.this.state = component.state ? component.state() : {};
|
|
288
|
+
const template = boundProducer();
|
|
289
|
+
return createElementFromTemplate.call(boundProducer.this, giveTemplateComponentMetadata(template, boundProducer.componentId, component.key, component.binding), onMounts, boundProducer);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Returns a template imbued with component data.
|
|
294
|
+
* @param {Template} template - the Template.
|
|
295
|
+
* @param {ComponentID} componentId - the ComponentId.
|
|
296
|
+
* @param {Key} [key] - the key, if applicable.
|
|
297
|
+
* @param {string} [bindingName] - the name of the binding, if applicable.
|
|
298
|
+
* @returns {Template} - the imbued Template.
|
|
299
|
+
*/
|
|
300
|
+
function giveTemplateComponentMetadata(template, componentId, key, bindingName) {
|
|
301
|
+
return {
|
|
302
|
+
...template,
|
|
303
|
+
componentId,
|
|
304
|
+
key: key ?? template.key,
|
|
305
|
+
binding: bindingName ?? template.binding
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Wrap a function in onMounts handling.
|
|
311
|
+
* @param {(onMounts: Function[]) => void} func - the function.
|
|
312
|
+
*/
|
|
313
|
+
function doOnMountHandling(func) {
|
|
314
|
+
const onMounts = [];
|
|
315
|
+
func(onMounts);
|
|
316
|
+
onMounts.forEach(om => om());
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Rerenders a component using its This.
|
|
321
|
+
* @param {This} this - the This.
|
|
322
|
+
*/
|
|
323
|
+
function rerender() {
|
|
324
|
+
doOnMountHandling((onMounts) => {
|
|
325
|
+
const template = this.producer();
|
|
326
|
+
rerenderElementFromTemplate.call(this, this.element, giveTemplateComponentMetadata(template, this.producer.componentId), onMounts);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Rerenders one of a component's bindings using its This.
|
|
332
|
+
* @param {This} this - the This.
|
|
333
|
+
* @param {string} bindingName - the binding name of the bounded element.
|
|
334
|
+
*/
|
|
335
|
+
function rerenderByBinding(bindingName) {
|
|
336
|
+
const template = this.producer();
|
|
337
|
+
const subTemplate = findSubtemplate(template, bindingName);
|
|
338
|
+
const subElement = findSubelement(this.element, bindingName);
|
|
339
|
+
if (subTemplate && subElement) {
|
|
340
|
+
doOnMountHandling((onMounts) => {
|
|
341
|
+
if (elementableIsComponent(subTemplate)) {
|
|
342
|
+
if ('componentId' in subElement.dataset)
|
|
343
|
+
rerenderChildComponent(subElement, subTemplate, onMounts);
|
|
344
|
+
else subElement.replaceWith(createElementFromComponent(subTemplate, onMounts));
|
|
345
|
+
} else {
|
|
346
|
+
if ('componentId' in subElement.dataset) delete thisRecord[subElement.dataset.componentId];
|
|
347
|
+
rerenderElementFromTemplate.call(this, subElement, subTemplate, onMounts);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Finds a child with a given binding name. Stops at components.
|
|
355
|
+
* @param {HTMLElement} element - the HTMLElement.
|
|
356
|
+
* @param {string} bindingName - the binding name to search for.
|
|
357
|
+
* @param {boolean} atRoot - whether this is the root. True by default.
|
|
358
|
+
*/
|
|
359
|
+
function findSubelement(element, bindingName, atRoot = true) {
|
|
360
|
+
if (element.dataset.binding === bindingName && !atRoot) {
|
|
361
|
+
return element;
|
|
362
|
+
} else if (!atRoot && 'componentId' in element.dataset) {
|
|
363
|
+
return undefined;
|
|
364
|
+
}
|
|
365
|
+
return Array.from(element.children).find(c => !!findSubelement(c, bindingName, false));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Finds a subtree template by a binding name. Stops at components.
|
|
370
|
+
* Excludes the given Template (the root) from the search.
|
|
371
|
+
* @param {Template} template - the Template.
|
|
372
|
+
* @param {string} bindingName - the binding name.
|
|
373
|
+
* @param {boolean} [atRoot] - whether this is the root. True by default.
|
|
374
|
+
* @returns {Template | Component | undefined} the result.
|
|
375
|
+
*/
|
|
376
|
+
function findSubtemplate(template, bindingName, atRoot = true) {
|
|
377
|
+
if (template.binding === bindingName && !atRoot) {
|
|
378
|
+
return template;
|
|
379
|
+
}
|
|
380
|
+
if (template.children) {
|
|
381
|
+
if (Array.isArray(template.children)) {
|
|
382
|
+
return template.children.find(c => findSubtemplate(c, bindingName, false));
|
|
383
|
+
} else {
|
|
384
|
+
return findSubtemplate(template.children, bindingName, false);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Rerenders an HTMLElement from a Template.
|
|
392
|
+
* @param {This} this - the nearest Component's This.
|
|
393
|
+
* @param {HTMLElement} element - the Element.
|
|
394
|
+
* @param {Template} template - the Template.
|
|
395
|
+
* @param {Function[]} onMounts - the aggregated onMounts.
|
|
396
|
+
*/
|
|
397
|
+
function rerenderElementFromTemplate(element, template, onMounts) {
|
|
398
|
+
if (typeof template !== 'object') {
|
|
399
|
+
return element.replaceWith(template.toString());
|
|
400
|
+
}
|
|
401
|
+
if (template.cloneFrom && !element.isEqualNode(template.cloneFrom)) {
|
|
402
|
+
return element.replaceWith(template.cloneFrom.cloneNode(true));
|
|
403
|
+
}
|
|
404
|
+
if ((template.tag?.toUpperCase() || 'DIV') !== element.tagName) {
|
|
405
|
+
// tag cannot be changed
|
|
406
|
+
return element.replaceWith(createElementFromElementable.call(this, template, onMounts));
|
|
407
|
+
}
|
|
408
|
+
if (template.class) {
|
|
409
|
+
if (typeof template.class === 'string') {
|
|
410
|
+
element.className = template.class;
|
|
411
|
+
} else if (Array.isArray(template)) {
|
|
412
|
+
element.className = template.class.join(" ");
|
|
413
|
+
} else {
|
|
414
|
+
for (const key in template.class) {
|
|
415
|
+
if (template.class[key]) {
|
|
416
|
+
element.classList.add(key);
|
|
417
|
+
} else {
|
|
418
|
+
element.classList.remove(key);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (template.style) {
|
|
424
|
+
for (let key in template.style) {
|
|
425
|
+
element.style[key] = template.style[key];
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (template.dataset) {
|
|
429
|
+
for (let key in template.dataset) {
|
|
430
|
+
element.dataset[key] = template.dataset[key];
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
const {tag, cloneFrom, class: _, style, on, key, dataset, componentId, children, innerHTML, binding, ...rest} = template;
|
|
434
|
+
for (const attribute in rest) {
|
|
435
|
+
element.setAttribute(attribute, template[attribute]);
|
|
436
|
+
}
|
|
437
|
+
if (template.innerHTML) {
|
|
438
|
+
element.innerHTML = template.innerHTML;
|
|
439
|
+
} else if (template.children === undefined || template.children.length === 0) {
|
|
440
|
+
element.innerHTML = "";
|
|
441
|
+
} else {
|
|
442
|
+
rerenderChildren.call(this, element, template, onMounts);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Determines whether two values are deeply equal.
|
|
448
|
+
* @param {*} a
|
|
449
|
+
* @param {*} b
|
|
450
|
+
* @returns {boolean} whether they are equal.
|
|
451
|
+
*/
|
|
452
|
+
function deepEqual(a, b) {
|
|
453
|
+
if (typeof a !== typeof b) return false;
|
|
454
|
+
if (typeof a === 'function') return true;
|
|
455
|
+
if (typeof a === 'object' && a !== null) {
|
|
456
|
+
if (typeof a[Symbol.iterator] === "function") { // is iterable
|
|
457
|
+
if (a.length === b.length && a.size === b.size) {
|
|
458
|
+
const itA = a[Symbol.iterator](), itB = b[Symbol.iterator]();
|
|
459
|
+
while (true) {
|
|
460
|
+
const nextA = itA.next(), nextB = itB.next();
|
|
461
|
+
if (nextA.done === nextB.done) {
|
|
462
|
+
if (!deepEqual(nextA.value, nextB.value)) return false;
|
|
463
|
+
if (nextA.done) return true;
|
|
464
|
+
} else return false;
|
|
465
|
+
}
|
|
466
|
+
} else return false;
|
|
467
|
+
} else {
|
|
468
|
+
const keysA = Object.keys(a), keysB = Object.keys(b);
|
|
469
|
+
if (keysA.length !== keysB.length) return false;
|
|
470
|
+
return keysB.every(keyB => keyB in a && deepEqual(keysA[keyB], keysB[keyB]));
|
|
471
|
+
}
|
|
472
|
+
} else return a === b;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Returns true iff a component has a memo (functional or otherwise) and it is satisfied.
|
|
477
|
+
* @param {This} this - the This.
|
|
478
|
+
* @param {Component} component - the Component.
|
|
479
|
+
* @returns {boolean} whether the memo exists and is satisfied.
|
|
480
|
+
*/
|
|
481
|
+
function evaluateMemo(component) {
|
|
482
|
+
if (component.memo && this.memo) {
|
|
483
|
+
// for functional memos: rerender if returns false
|
|
484
|
+
if (typeof component.memo === 'function') return !!component.memo.call(this);
|
|
485
|
+
return deepEqual(component.memo, this.memo);
|
|
486
|
+
} else return false;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Rerenders a component HTMLElement from a Component, preserving state but updating the producer.
|
|
491
|
+
* @param {HTMLElement} element - the element.
|
|
492
|
+
* @param {Component} component - the Component.
|
|
493
|
+
* @param {Function[]} onMounts - the aggregated onMounts.
|
|
494
|
+
*/
|
|
495
|
+
function rerenderChildComponent(element, component, onMounts) {
|
|
496
|
+
const cmpThis = thisRecord[element.dataset.componentId];
|
|
497
|
+
if (evaluateMemo.call(cmpThis, component)) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
cmpThis.producer = component.render.bind(cmpThis);
|
|
501
|
+
rerenderElementFromTemplate.call(cmpThis, element, cmpThis.producer(), onMounts);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Re-creates a keyed child component with a known This (i.e., when changing order).
|
|
506
|
+
* @param {Component} component - the Component's new template.
|
|
507
|
+
* @param {ComponentID} componentId - the Component's old ComponentId.
|
|
508
|
+
* @param {Function[]} onMounts - the aggregated onMounts.
|
|
509
|
+
* @returns {HTMLElement} the re-created HTMLElement.
|
|
510
|
+
*/
|
|
511
|
+
function recreateKeyedChildComponent(component, componentId, onMounts) {
|
|
512
|
+
const cmpThis = thisRecord[componentId];
|
|
513
|
+
if (evaluateMemo.call(cmpThis, component)) {
|
|
514
|
+
return cmpThis.element;
|
|
515
|
+
}
|
|
516
|
+
cmpThis.producer = component.render.bind(cmpThis);
|
|
517
|
+
const template = cmpThis.producer();
|
|
518
|
+
const element = createElementFromTemplate.call(cmpThis, giveTemplateComponentMetadata(template, componentId, component.key, component.binding), onMounts, cmpThis.producer);
|
|
519
|
+
cmpThis.element = element;
|
|
520
|
+
return element;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Rerender an element's children into a Template.
|
|
525
|
+
* @param {This} this - the nearest Component's This.
|
|
526
|
+
* @param {HTMLElement} element - the Element.
|
|
527
|
+
* @param {Template} template - the Template.
|
|
528
|
+
* @param {Function[]} onMounts - the aggregated onMounts.
|
|
529
|
+
*/
|
|
530
|
+
function rerenderChildren(element, template, onMounts) {
|
|
531
|
+
const elChildrenArray = Array.from(element.children);
|
|
532
|
+
const tmChildNodeArray = Array.isArray(template.children) ? template.children : [template.children];
|
|
533
|
+
const tmChildrenArray = tmChildNodeArray.filter(c => typeof c === 'object');
|
|
534
|
+
|
|
535
|
+
// element nodes
|
|
536
|
+
if (elChildrenArray.length > 0 && tmChildrenArray.length > 0 && elChildrenArray.every(elChild => 'key' in elChild.dataset) && tmChildrenArray.every(tmChild => 'key' in tmChild)) {
|
|
537
|
+
|
|
538
|
+
// be smart with keys in here
|
|
539
|
+
const keyIndexInEl = {};
|
|
540
|
+
for (let i = 0; i < elChildrenArray.length; i++) {
|
|
541
|
+
keyIndexInEl[elChildrenArray[i].dataset.key] = i;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const keyIndexInTm = {};
|
|
545
|
+
for (let i = 0; i < tmChildrenArray.length; i++) {
|
|
546
|
+
keyIndexInTm[tmChildrenArray[i].key] = i;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const tmKeyIndexInEl = tmChildrenArray.map(k => keyIndexInEl[k.key] ?? -1);
|
|
550
|
+
|
|
551
|
+
const lis = LIS(tmKeyIndexInEl.filter(i => i > -1));
|
|
552
|
+
|
|
553
|
+
for (let i = 0; i < lis.length; i++) {
|
|
554
|
+
const elIndex = lis[i];
|
|
555
|
+
const childEl = elChildrenArray[elIndex];
|
|
556
|
+
const childTm = tmChildrenArray[keyIndexInTm[childEl.dataset.key]];
|
|
557
|
+
|
|
558
|
+
if (elementableIsComponent(childTm)) {
|
|
559
|
+
if ('componentId' in childEl.dataset) rerenderChildComponent(childEl, childTm, onMounts);
|
|
560
|
+
else childEl.replaceWith(createElementFromComponent(childTm, onMounts));
|
|
561
|
+
} else {
|
|
562
|
+
if ('componentId' in childEl.dataset) delete thisRecord[childEl.dataset.componentId];
|
|
563
|
+
rerenderElementFromTemplate.call(this, childEl, childTm, onMounts);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const setLis = new Set(lis);
|
|
568
|
+
const movedElKeyToComponentId = {};
|
|
569
|
+
|
|
570
|
+
for (let i = elChildrenArray.length - 1; i >= 0; i--) {
|
|
571
|
+
if (!setLis.has(i)) {
|
|
572
|
+
const childEl = elChildrenArray[i];
|
|
573
|
+
if ('componentId' in childEl.dataset) {
|
|
574
|
+
if (childEl.dataset.key in keyIndexInTm) {
|
|
575
|
+
movedElKeyToComponentId[childEl.dataset.key] = childEl.dataset.componentId;
|
|
576
|
+
} else {
|
|
577
|
+
delete thisRecord[childEl.dataset.componentId];
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if ('binding' in childEl.dataset) delete this.bindings[childEl.dataset.binding];
|
|
581
|
+
childEl.remove();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
for (let i = 0; i < tmChildrenArray.length; i++) {
|
|
586
|
+
const childTm = tmChildrenArray[i], childEl = element.children[i];
|
|
587
|
+
if (!childEl) {
|
|
588
|
+
const componentId = movedElKeyToComponentId[childTm.key];
|
|
589
|
+
if (componentId) {
|
|
590
|
+
element.appendChild(recreateKeyedChildComponent(childTm, componentId, onMounts));
|
|
591
|
+
} else {
|
|
592
|
+
element.appendChild(createElementFromElementable.call(this, childTm, onMounts));
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
if (childEl.dataset.key !== childTm.key) {
|
|
596
|
+
const componentId = movedElKeyToComponentId[childTm.key];
|
|
597
|
+
if (componentId) {
|
|
598
|
+
element.insertBefore(recreateKeyedChildComponent(childTm, componentId, onMounts), childEl);
|
|
599
|
+
} else {
|
|
600
|
+
element.insertBefore(createElementFromElementable.call(this, childTm, onMounts), childEl);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
} else if (tmChildrenArray.length > 0 && elChildrenArray.length > 0) {
|
|
606
|
+
// handle first Min(M,N) elements
|
|
607
|
+
for (let i = 0; i < Math.min(elChildrenArray.length, tmChildrenArray.length); i++) {
|
|
608
|
+
let childEl = elChildrenArray[i], childTm = tmChildrenArray[i];
|
|
609
|
+
if ('binding' in childEl.dataset && childEl.dataset.binding !== childTm.binding)
|
|
610
|
+
delete this.bindings[childEl.dataset.binding];
|
|
611
|
+
if (elementableIsComponent(childTm)) {
|
|
612
|
+
if ('componentId' in childEl.dataset) rerenderChildComponent(childEl, childTm, onMounts);
|
|
613
|
+
else childEl.replaceWith(createElementFromComponent(childTm, onMounts));
|
|
614
|
+
} else {
|
|
615
|
+
if ('componentId' in childEl.dataset) delete thisRecord[childEl.dataset.componentId];
|
|
616
|
+
rerenderElementFromTemplate.call(this, childEl, childTm, onMounts);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// prune at or append to end
|
|
621
|
+
if (elChildrenArray.length < tmChildrenArray.length) {
|
|
622
|
+
for (let i = elChildrenArray.length; i < tmChildrenArray.length; i++) {
|
|
623
|
+
let childTm = tmChildrenArray[i];
|
|
624
|
+
const newChild = createElementFromElementable.call(this, childTm, onMounts);
|
|
625
|
+
element.appendChild(newChild);
|
|
626
|
+
}
|
|
627
|
+
} else if (elChildrenArray.length > tmChildrenArray.length) {
|
|
628
|
+
for (let i = elChildrenArray.length - 1; i >= tmChildrenArray.length; i--) {
|
|
629
|
+
let childEl = elChildrenArray[i];
|
|
630
|
+
if ('componentId' in childEl.dataset) delete thisRecord[childEl.dataset.componentId];
|
|
631
|
+
if ('binding' in childEl.dataset) delete this.bindings[childEl.dataset.binding];
|
|
632
|
+
childEl.remove();
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// handle bindings
|
|
638
|
+
for (let i = 0; i < tmChildrenArray.length; i++) {
|
|
639
|
+
const childTm = tmChildrenArray[i];
|
|
640
|
+
if (childTm.binding) {
|
|
641
|
+
setBinding.call(this, childTm.binding, element.children[i]);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// handle text nodes
|
|
646
|
+
for (let i = 0; i < tmChildNodeArray.length; i++) {
|
|
647
|
+
if (i >= element.childNodes.length) {
|
|
648
|
+
element.appendChild(document.createTextNode(tmChildNodeArray[i]));
|
|
649
|
+
} else {
|
|
650
|
+
if (element.childNodes[i].nodeType === Node.TEXT_NODE) {
|
|
651
|
+
if (typeof tmChildNodeArray[i] !== 'object') {
|
|
652
|
+
element.childNodes[i].textContent = tmChildNodeArray[i];
|
|
653
|
+
} else {
|
|
654
|
+
while (element.childNodes[i].nodeType !== Node.ELEMENT_NODE) {
|
|
655
|
+
element.childNodes[i].remove();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
} else {
|
|
659
|
+
if (typeof tmChildNodeArray[i] !== 'object') {
|
|
660
|
+
element.insertBefore(document.createTextNode(tmChildNodeArray[i]), element.childNodes[i]);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
while (element.childNodes.length > tmChildNodeArray.length) {
|
|
666
|
+
element.lastChild.remove();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function LIS(arr) {
|
|
671
|
+
let N = arr.length;
|
|
672
|
+
let P = new Array(N).fill(0);
|
|
673
|
+
let M = new Array(N + 1).fill(0);
|
|
674
|
+
M[0] = -1;
|
|
675
|
+
let L = 0;
|
|
676
|
+
for (let i = 0; i < N; i++) {
|
|
677
|
+
let low = 1, high = L + 1;
|
|
678
|
+
while (low < high) {
|
|
679
|
+
let mid = low + ((high - low) >> 1);
|
|
680
|
+
if (arr[M[mid]] >= arr[i])
|
|
681
|
+
high = mid;
|
|
682
|
+
else
|
|
683
|
+
low = mid + 1;
|
|
684
|
+
}
|
|
685
|
+
let newL = low;
|
|
686
|
+
P[i] = M[newL - 1];
|
|
687
|
+
M[newL] = i;
|
|
688
|
+
if (newL > L) {
|
|
689
|
+
L = newL;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
let S = new Array(L);
|
|
693
|
+
let k = M[L];
|
|
694
|
+
for (let j = L-1; j >= 0; j--) {
|
|
695
|
+
S[j] = arr[k];
|
|
696
|
+
k = P[k];
|
|
697
|
+
}
|
|
698
|
+
return S;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Sets a binding in a This.
|
|
703
|
+
* @param {This} this - the This.
|
|
704
|
+
* @param {string} bindingName - the name of the binding.
|
|
705
|
+
* @param {HTMLElement} element - the element being bound.
|
|
706
|
+
*/
|
|
707
|
+
function setBinding(bindingName, element) {
|
|
708
|
+
this.bindings[bindingName] = {
|
|
709
|
+
element,
|
|
710
|
+
rerender: () => rerenderByBinding.call(this, bindingName)
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Creates an HTMLElement given an Elementable.
|
|
716
|
+
* @param {Elementable} elementable - the Elementable.
|
|
717
|
+
* @param {boolean} [includeOnMounts] - an option to include the aggregated onMounts functions. False by default.
|
|
718
|
+
* @returns {HTMLElement | Node | {element: HTMLElement | Node, onMounts: Function[]}} the resulting
|
|
719
|
+
* HTMLElement or text-node, or an object containing the HTMLElement or text-node
|
|
720
|
+
* and the aggregated onMounts functions.
|
|
721
|
+
*/
|
|
722
|
+
function create(elementable, includeOnMounts = false) {
|
|
723
|
+
const onMounts = [];
|
|
724
|
+
let element = createElementFromElementable(elementable, onMounts);
|
|
725
|
+
if (typeof element === 'string') element = document.createTextNode(element);
|
|
726
|
+
return includeOnMounts ? {element, onMounts} : element;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Replaces an HTMLElement with an Elementable. Handles onMounts.
|
|
731
|
+
* @param {HTMLElement} element - the HTMLElement.
|
|
732
|
+
* @param {Elementable} elementable - the Elementable.
|
|
733
|
+
*/
|
|
734
|
+
function replaceWith(element, elementable) {
|
|
735
|
+
const {element: newElement, onMounts} = create(elementable, true);
|
|
736
|
+
element.replaceWith(newElement);
|
|
737
|
+
onMounts.forEach(om => om());
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Appends an Elementable to an HTMLElement. Handles onMounts.
|
|
742
|
+
* @param {HTMLElement} element - the HTMLElement.
|
|
743
|
+
* @param {Elementable} elementable - the Elementable.
|
|
744
|
+
*/
|
|
745
|
+
function appendChild(element, elementable) {
|
|
746
|
+
const {element: newElement, onMounts} = create(elementable, true);
|
|
747
|
+
element.appendChild(newElement);
|
|
748
|
+
onMounts.forEach(om => om());
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Inserts an Elementable before an HTMLElement. Handles onMounts.
|
|
753
|
+
* @param {HTMLElement} parentElement - the parent HTMLElement.
|
|
754
|
+
* @param {HTMLElement | null} nextSiblingElement - the next sibling HTMLElement, or null to append.
|
|
755
|
+
* @param {Elementable} elementable - the Elementable.
|
|
756
|
+
*/
|
|
757
|
+
function insertBefore(parentElement, nextSiblingElement, elementable) {
|
|
758
|
+
const {element: newElement, onMounts} = create(elementable, true);
|
|
759
|
+
parentElement.insertBefore(newElement, nextSiblingElement);
|
|
760
|
+
onMounts.forEach(om => om());
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
export { create, replaceWith, appendChild, insertBefore };
|
package/dist/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
let globalComponentCount=0;const thisRecord={};const rerenderQueue=new Set;function rerenderEnqueuedComponents(){Array.from(rerenderQueue).forEach(cId=>thisRecord[cId]?.rerender());rerenderQueue.clear()}function enqueueToRerender(componentId){const enqueueRerenderTask=rerenderQueue.size===0;rerenderQueue.add(componentId);if(enqueueRerenderTask)queueMicrotask(rerenderEnqueuedComponents)}function createThis(){return{state:{},setState:{},bindings:{}}}function initializeThis(element,producer){this.element=element;this.producer=producer;this.rerender=rerender.bind(this);this.setState=new Proxy({},{get:(o,p,r)=>x=>{this.state[p]=x;enqueueToRerender(producer.componentId)}})}function createElementFromTemplate(template,onMounts,producer=null){if(template.cloneFrom){const element=template.cloneFrom.cloneNode(true);if(this&&!this.element){initializeThis.call(this,element,producer)}return element}const{tag:tag,class:c,style:style,on:on,componentId:componentId,children:children,cloneFrom:cloneFrom,dataset:dataset,key:key,binding:binding,innerHTML:innerHTML,...rest}=template;const element=document.createElement(template.tag||"div");if(template.class){switch(typeof template.class){case"string":element.className=template.class;break;case"object":if(Array.isArray(template)){element.className=template.class.join(" ")}else{element.className=Object.keys(template.class).filter(c=>template.class[c]).join(" ")}}}if(template.style){for(const k in template.style){element.style[k]=template.style[k]}}if(template.on){const{mount:onMount,...listeners}=template.on;for(const type in listeners){if(!listeners[type])continue;element.addEventListener(type,event=>listeners[type].call({...this??{},target:element},event))}if(onMount){onMounts.push(()=>onMount.call({...this??{},target:element}))}}for(const attribute in rest){if(!template[attribute])continue;element.setAttribute(attribute,template[attribute])}if(template.dataset){for(const k in template.dataset){element.dataset[k]=template.dataset[k]}}if(template.componentId){element.dataset.componentId=template.componentId}if(template.key){element.dataset.key=template.key}if(template.binding){element.dataset.binding=template.binding}if(this&&!this.element){initializeThis.call(this,element,producer)}if(template.innerHTML){element.innerHTML=template.innerHTML}else if("children"in template){if(Array.isArray(template.children)){element.replaceChildren(...template.children.map(ct=>createElementFromElementable.call(this,ct,onMounts)));for(let i=0;i<template.children.length;i++){const childTm=template.children[i];if(childTm.binding&&this){setBinding.call(this,childTm.binding,element.childNodes[i])}}}else{element.replaceChildren(createElementFromElementable.call(this,template.children,onMounts));if(template.children.binding&&this){setBinding.call(this,template.children.binding,element.childNodes[0])}}}return element}function bindTemplateProducer(producer){const newThis=createThis();const boundProducer=producer.bind(newThis);boundProducer.this=newThis;boundProducer.componentId=`component-${globalComponentCount++}`;return boundProducer}function elementableIsComponent(elementable){return!!elementable.render}function createElementFromElementable(elementable,onMounts){if(typeof elementable==="object"){if(elementableIsComponent(elementable)){return createElementFromComponent(elementable,onMounts)}else{return createElementFromTemplate.call(this,elementable,onMounts)}}else{return elementable.toString()}}function createElementFromComponent(component,onMounts){const boundProducer=bindTemplateProducer(component.render);thisRecord[boundProducer.componentId]=boundProducer.this;boundProducer.this.state=component.state?component.state():{};const template=boundProducer();return createElementFromTemplate.call(boundProducer.this,giveTemplateComponentMetadata(template,boundProducer.componentId,component.key,component.binding),onMounts,boundProducer)}function giveTemplateComponentMetadata(template,componentId,key,bindingName){return{...template,componentId:componentId,key:key??template.key,binding:bindingName??template.binding}}function doOnMountHandling(func){const onMounts=[];func(onMounts);onMounts.forEach(om=>om())}function rerender(){doOnMountHandling(onMounts=>{const template=this.producer();rerenderElementFromTemplate.call(this,this.element,giveTemplateComponentMetadata(template,this.producer.componentId),onMounts)})}function rerenderByBinding(bindingName){const template=this.producer();const subTemplate=findSubtemplate(template,bindingName);const subElement=findSubelement(this.element,bindingName);if(subTemplate&&subElement){doOnMountHandling(onMounts=>{if(elementableIsComponent(subTemplate)){if("componentId"in subElement.dataset)rerenderChildComponent(subElement,subTemplate,onMounts);else subElement.replaceWith(createElementFromComponent(subTemplate,onMounts))}else{if("componentId"in subElement.dataset)delete thisRecord[subElement.dataset.componentId];rerenderElementFromTemplate.call(this,subElement,subTemplate,onMounts)}})}}function findSubelement(element,bindingName,atRoot=true){if(element.dataset.binding===bindingName&&!atRoot){return element}else if(!atRoot&&"componentId"in element.dataset){return undefined}return Array.from(element.children).find(c=>!!findSubelement(c,bindingName,false))}function findSubtemplate(template,bindingName,atRoot=true){if(template.binding===bindingName&&!atRoot){return template}if(template.children){if(Array.isArray(template.children)){return template.children.find(c=>findSubtemplate(c,bindingName,false))}else{return findSubtemplate(template.children,bindingName,false)}}return undefined}function rerenderElementFromTemplate(element,template,onMounts){if(typeof template!=="object"){return element.replaceWith(template.toString())}if(template.cloneFrom&&!element.isEqualNode(template.cloneFrom)){return element.replaceWith(template.cloneFrom.cloneNode(true))}if((template.tag?.toUpperCase()||"DIV")!==element.tagName){return element.replaceWith(createElementFromElementable.call(this,template,onMounts))}if(template.class){if(typeof template.class==="string"){element.className=template.class}else if(Array.isArray(template)){element.className=template.class.join(" ")}else{for(const key in template.class){if(template.class[key]){element.classList.add(key)}else{element.classList.remove(key)}}}}if(template.style){for(let key in template.style){element.style[key]=template.style[key]}}if(template.dataset){for(let key in template.dataset){element.dataset[key]=template.dataset[key]}}const{tag:tag,cloneFrom:cloneFrom,class:_,style:style,on:on,key:key,dataset:dataset,componentId:componentId,children:children,innerHTML:innerHTML,binding:binding,...rest}=template;for(const attribute in rest){element.setAttribute(attribute,template[attribute])}if(template.innerHTML){element.innerHTML=template.innerHTML}else if(template.children===undefined||template.children.length===0){element.innerHTML=""}else{rerenderChildren.call(this,element,template,onMounts)}}function rerenderChildComponent(element,component,onMounts){const cmpThis=thisRecord[element.dataset.componentId];cmpThis.producer=component.render.bind(cmpThis);rerenderElementFromTemplate.call(cmpThis,element,cmpThis.producer(),onMounts)}function recreateKeyedChildComponent(component,componentId,onMounts){const cmpThis=thisRecord[componentId];cmpThis.producer=component.render.bind(cmpThis);const template=cmpThis.producer();const element=createElementFromTemplate.call(cmpThis,giveTemplateComponentMetadata(template,componentId,component.key,component.binding),onMounts,cmpThis.producer);cmpThis.element=element;return element}function rerenderChildren(element,template,onMounts){const elChildrenArray=Array.from(element.children);const tmChildNodeArray=Array.isArray(template.children)?template.children:[template.children];const tmChildrenArray=tmChildNodeArray.filter(c=>typeof c==="object");if(elChildrenArray.length>0&&tmChildrenArray.length>0&&elChildrenArray.every(elChild=>"key"in elChild.dataset)&&tmChildrenArray.every(tmChild=>"key"in tmChild)){const keyIndexInEl={};for(let i=0;i<elChildrenArray.length;i++){keyIndexInEl[elChildrenArray[i].dataset.key]=i}const keyIndexInTm={};for(let i=0;i<tmChildrenArray.length;i++){keyIndexInTm[tmChildrenArray[i].key]=i}const tmKeyIndexInEl=tmChildrenArray.map(k=>keyIndexInEl[k.key]??-1);const lis=LIS(tmKeyIndexInEl.filter(i=>i>-1));for(let i=0;i<lis.length;i++){const elIndex=lis[i];const childEl=elChildrenArray[elIndex];const childTm=tmChildrenArray[keyIndexInTm[childEl.dataset.key]];if(elementableIsComponent(childTm)){if("componentId"in childEl.dataset)rerenderChildComponent(childEl,childTm,onMounts);else childEl.replaceWith(createElementFromComponent(childTm,onMounts))}else{if("componentId"in childEl.dataset)delete thisRecord[childEl.dataset.componentId];rerenderElementFromTemplate.call(this,childEl,childTm,onMounts)}}const setLis=new Set(lis);const movedElKeyToComponentId={};for(let i=elChildrenArray.length-1;i>=0;i--){if(!setLis.has(i)){const childEl=elChildrenArray[i];if("componentId"in childEl.dataset){if(childEl.dataset.key in keyIndexInTm){movedElKeyToComponentId[childEl.dataset.key]=childEl.dataset.componentId}else{delete thisRecord[childEl.dataset.componentId]}}if("binding"in childEl.dataset)delete this.bindings[childEl.dataset.binding];childEl.remove()}}for(let i=0;i<tmChildrenArray.length;i++){const childTm=tmChildrenArray[i],childEl=element.children[i];if(!childEl){const componentId=movedElKeyToComponentId[childTm.key];if(componentId){element.appendChild(recreateKeyedChildComponent(childTm,componentId,onMounts))}else{element.appendChild(createElementFromElementable.call(this,childTm,onMounts))}}else{if(childEl.dataset.key!==childTm.key){const componentId=movedElKeyToComponentId[childTm.key];if(componentId){element.insertBefore(recreateKeyedChildComponent(childTm,componentId,onMounts),childEl)}else{element.insertBefore(createElementFromElementable.call(this,childTm,onMounts),childEl)}}}}}else if(tmChildrenArray.length>0&&elChildrenArray.length>0){for(let i=0;i<Math.min(elChildrenArray.length,tmChildrenArray.length);i++){let childEl=elChildrenArray[i],childTm=tmChildrenArray[i];if("binding"in childEl.dataset&&childEl.dataset.binding!==childTm.binding)delete this.bindings[childEl.dataset.binding];if(elementableIsComponent(childTm)){if("componentId"in childEl.dataset)rerenderChildComponent(childEl,childTm,onMounts);else childEl.replaceWith(createElementFromComponent(childTm,onMounts))}else{if("componentId"in childEl.dataset)delete thisRecord[childEl.dataset.componentId];rerenderElementFromTemplate.call(this,childEl,childTm,onMounts)}}if(elChildrenArray.length<tmChildrenArray.length){for(let i=elChildrenArray.length;i<tmChildrenArray.length;i++){let childTm=tmChildrenArray[i];const newChild=createElementFromElementable.call(this,childTm,onMounts);element.appendChild(newChild)}}else if(elChildrenArray.length>tmChildrenArray.length){for(let i=elChildrenArray.length-1;i>=tmChildrenArray.length;i--){let childEl=elChildrenArray[i];if("componentId"in childEl.dataset)delete thisRecord[childEl.dataset.componentId];if("binding"in childEl.dataset)delete this.bindings[childEl.dataset.binding];childEl.remove()}}}for(let i=0;i<tmChildrenArray.length;i++){const childTm=tmChildrenArray[i];if(childTm.binding){setBinding.call(this,childTm.binding,element.children[i])}}for(let i=0;i<tmChildNodeArray.length;i++){if(i>=element.childNodes.length){element.appendChild(document.createTextNode(tmChildNodeArray[i]))}else{if(element.childNodes[i].nodeType===Node.TEXT_NODE){if(typeof tmChildNodeArray[i]!=="object"){element.childNodes[i].textContent=tmChildNodeArray[i]}else{while(element.childNodes[i].nodeType!==Node.ELEMENT_NODE){element.childNodes[i].remove()}}}else{if(typeof tmChildNodeArray[i]!=="object"){element.insertBefore(document.createTextNode(tmChildNodeArray[i]),element.childNodes[i])}}}}while(element.childNodes.length>tmChildNodeArray.length){element.lastChild.remove()}}function LIS(arr){let N=arr.length;let P=new Array(N).fill(0);let M=new Array(N+1).fill(0);M[0]=-1;let L=0;for(let i=0;i<N;i++){let low=1,high=L+1;while(low<high){let mid=low+(high-low>>1);if(arr[M[mid]]>=arr[i])high=mid;else low=mid+1}let newL=low;P[i]=M[newL-1];M[newL]=i;if(newL>L){L=newL}}let S=new Array(L);let k=M[L];for(let j=L-1;j>=0;j--){S[j]=arr[k];k=P[k]}return S}function setBinding(bindingName,element){this.bindings[bindingName]={element:element,rerender:()=>rerenderByBinding.call(this,bindingName)}}function create(elementable,includeOnMounts=false){const onMounts=[];let element=createElementFromElementable(elementable,onMounts);if(typeof element==="string")element=document.createTextNode(element);return includeOnMounts?{element:element,onMounts:onMounts}:element}function replaceWith(element,elementable){const{element:newElement,onMounts:onMounts}=create(elementable,true);element.replaceWith(newElement);onMounts.forEach(om=>om())}function appendChild(element,elementable){const{element:newElement,onMounts:onMounts}=create(elementable,true);element.appendChild(newElement);onMounts.forEach(om=>om())}function insertBefore(parentElement,nextSiblingElement,elementable){const{element:newElement,onMounts:onMounts}=create(elementable,true);parentElement.insertBefore(newElement,nextSiblingElement);onMounts.forEach(om=>om())}export{create,replaceWith,appendChild,insertBefore};
|