@elenajs/core 1.0.0-rc.7 → 1.0.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 +66 -29
- package/dist/bundle.js +2 -2
- package/dist/common/props.d.ts +2 -2
- package/dist/common/props.d.ts.map +1 -1
- package/dist/common/render.d.ts +2 -2
- package/dist/common/render.d.ts.map +1 -1
- package/dist/common/utils.d.ts +7 -7
- package/dist/common/utils.d.ts.map +1 -1
- package/dist/elena.d.ts +6 -1
- package/dist/elena.d.ts.map +1 -1
- package/dist/elena.js +2 -2
- package/dist/props.js +1 -1
- package/dist/render.js +1 -1
- package/dist/utils.js +1 -1
- package/package.json +22 -10
- package/src/common/props.js +9 -10
- package/src/common/render.js +132 -68
- package/src/common/utils.js +47 -34
- package/src/elena.js +79 -69
package/src/elena.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { setProps, getProps, getPropValue, syncAttribute } from "./common/props.js";
|
|
16
|
-
import { defineElement, html, unsafeHTML, nothing } from "./common/utils.js";
|
|
16
|
+
import { defineElement, html, unsafeHTML, nothing, warn, prefix } from "./common/utils.js";
|
|
17
17
|
import { renderTemplate } from "./common/render.js";
|
|
18
18
|
|
|
19
19
|
export { html, unsafeHTML, nothing };
|
|
@@ -40,7 +40,7 @@ function elementResolver(selector) {
|
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
|
-
* @typedef {{ text: string, element: HTMLElement | null, render(): void, willUpdate(): void, firstUpdated(): void, updated(): void, connectedCallback(): void, disconnectedCallback(): void }} ElenaInstanceMembers
|
|
43
|
+
* @typedef {{ text: string, element: HTMLElement | null, updateComplete: Promise<void>, render(): void, willUpdate(): void, firstUpdated(): void, updated(): void, requestUpdate(): void, connectedCallback(): void, disconnectedCallback(): void, adoptedCallback(): void, attributeChangedCallback(prop: string, oldValue: string | null, newValue: string | null): void }} ElenaInstanceMembers
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
46
|
/**
|
|
@@ -49,7 +49,7 @@ function elementResolver(selector) {
|
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* @typedef {(new (...args: any[]) => HTMLElement & ElenaInstanceMembers) & {
|
|
52
|
-
* define(): void,
|
|
52
|
+
* define(registry?: CustomElementRegistry): void,
|
|
53
53
|
* readonly observedAttributes: string[],
|
|
54
54
|
* tagName?: string,
|
|
55
55
|
* props?: (string | ElenaPropObject)[],
|
|
@@ -57,11 +57,13 @@ function elementResolver(selector) {
|
|
|
57
57
|
* element?: string,
|
|
58
58
|
* shadow?: "open" | "closed",
|
|
59
59
|
* styles?: CSSStyleSheet | string | (CSSStyleSheet | string)[],
|
|
60
|
+
* registry?: CustomElementRegistry,
|
|
60
61
|
* }} ElenaElementConstructor
|
|
61
62
|
*/
|
|
62
63
|
|
|
63
64
|
// Tracks which component classes have already been set up.
|
|
64
65
|
const setupRegistry = new WeakSet();
|
|
66
|
+
const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
|
|
65
67
|
|
|
66
68
|
/**
|
|
67
69
|
* Creates an Elena component class by extending `superClass`.
|
|
@@ -90,8 +92,8 @@ export function Elena(superClass) {
|
|
|
90
92
|
* Updates the matching prop and re-renders if needed.
|
|
91
93
|
*
|
|
92
94
|
* @param {string} prop
|
|
93
|
-
* @param {string} oldValue
|
|
94
|
-
* @param {string} newValue
|
|
95
|
+
* @param {string | null} oldValue
|
|
96
|
+
* @param {string | null} newValue
|
|
95
97
|
*/
|
|
96
98
|
attributeChangedCallback(prop, oldValue, newValue) {
|
|
97
99
|
super.attributeChangedCallback?.(prop, oldValue, newValue);
|
|
@@ -101,17 +103,29 @@ export function Elena(superClass) {
|
|
|
101
103
|
return;
|
|
102
104
|
}
|
|
103
105
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
getProps(this, prop, oldValue, newValue);
|
|
108
|
-
this._syncing = false;
|
|
106
|
+
if (oldValue === newValue) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
if (this._hydrated && !this._isRendering) {
|
|
111
|
+
// The attribute is already set and we just need the coerced
|
|
112
|
+
// prop value stored for the next render.
|
|
113
|
+
const current = this._props.get(prop);
|
|
114
|
+
const type = typeof current;
|
|
115
|
+
const coerced =
|
|
116
|
+
type === "string" ? (newValue ?? "") : getPropValue(type, newValue, "toProp");
|
|
117
|
+
|
|
118
|
+
if (coerced !== current) {
|
|
119
|
+
this._props.set(prop, coerced);
|
|
120
|
+
}
|
|
114
121
|
this._safeRender();
|
|
122
|
+
|
|
123
|
+
// Runs pre-hydration or during render.
|
|
124
|
+
// Goes through the setter so _props is initialized correctly.
|
|
125
|
+
} else {
|
|
126
|
+
this._syncing = true;
|
|
127
|
+
getProps(this, prop, oldValue, newValue);
|
|
128
|
+
this._syncing = false;
|
|
115
129
|
}
|
|
116
130
|
}
|
|
117
131
|
|
|
@@ -136,8 +150,20 @@ export function Elena(superClass) {
|
|
|
136
150
|
super.connectedCallback?.();
|
|
137
151
|
this._setupStaticProps();
|
|
138
152
|
this._captureClassFieldDefaults();
|
|
139
|
-
this.
|
|
153
|
+
if (!this._hydrated && this._text === undefined) {
|
|
154
|
+
this.text = this.textContent.trim();
|
|
155
|
+
}
|
|
140
156
|
this._attachShadow();
|
|
157
|
+
this._root = this._shadow ?? this.shadowRoot ?? this;
|
|
158
|
+
|
|
159
|
+
this._runUpdate ??= () => {
|
|
160
|
+
try {
|
|
161
|
+
this._performUpdate();
|
|
162
|
+
} catch (e) {
|
|
163
|
+
console.error(prefix, e);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
141
167
|
this.willUpdate();
|
|
142
168
|
this._applyRender();
|
|
143
169
|
this._syncProps();
|
|
@@ -181,7 +207,7 @@ export function Elena(superClass) {
|
|
|
181
207
|
}
|
|
182
208
|
|
|
183
209
|
if (names.includes("text")) {
|
|
184
|
-
|
|
210
|
+
warn('"text" is reserved.');
|
|
185
211
|
}
|
|
186
212
|
|
|
187
213
|
setProps(component.prototype, names, noRef);
|
|
@@ -193,7 +219,7 @@ export function Elena(superClass) {
|
|
|
193
219
|
|
|
194
220
|
if (component._elenaEvents) {
|
|
195
221
|
for (const e of component._elenaEvents) {
|
|
196
|
-
if (!
|
|
222
|
+
if (!hasOwn(component.prototype, e)) {
|
|
197
223
|
component.prototype[e] = function (...args) {
|
|
198
224
|
return this.element[e](...args);
|
|
199
225
|
};
|
|
@@ -215,7 +241,7 @@ export function Elena(superClass) {
|
|
|
215
241
|
this._syncing = true;
|
|
216
242
|
|
|
217
243
|
for (const name of this.constructor._propNames) {
|
|
218
|
-
if (
|
|
244
|
+
if (hasOwn(this, name)) {
|
|
219
245
|
const value = this[name];
|
|
220
246
|
delete this[name];
|
|
221
247
|
this[name] = value;
|
|
@@ -225,27 +251,6 @@ export function Elena(superClass) {
|
|
|
225
251
|
this._syncing = false;
|
|
226
252
|
}
|
|
227
253
|
|
|
228
|
-
/**
|
|
229
|
-
* Saves any text inside the element before the first render.
|
|
230
|
-
*
|
|
231
|
-
* @internal
|
|
232
|
-
*/
|
|
233
|
-
_captureText() {
|
|
234
|
-
if (!this._hydrated && this._text === undefined) {
|
|
235
|
-
this.text = this.textContent.trim();
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* The root node to render into. Returns the shadow root when shadow mode
|
|
241
|
-
* is enabled, otherwise the host element itself.
|
|
242
|
-
*
|
|
243
|
-
* @type {ShadowRoot | HTMLElement}
|
|
244
|
-
*/
|
|
245
|
-
get _renderRoot() {
|
|
246
|
-
return this._shadow ?? this.shadowRoot ?? this;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
254
|
/**
|
|
250
255
|
* Attaches a shadow root and adopts styles on first connect.
|
|
251
256
|
* Only runs when `static shadow` is set on the component class.
|
|
@@ -263,7 +268,11 @@ export function Elena(superClass) {
|
|
|
263
268
|
// In that case skip attachShadow() but still adopt styles below.
|
|
264
269
|
// Store the reference so closed shadow roots remain accessible.
|
|
265
270
|
if (!this._shadow && !this.shadowRoot) {
|
|
266
|
-
|
|
271
|
+
const options = { mode: component.shadow };
|
|
272
|
+
if (component.registry) {
|
|
273
|
+
options.customElementRegistry = component.registry;
|
|
274
|
+
}
|
|
275
|
+
this._shadow = this.attachShadow(options);
|
|
267
276
|
}
|
|
268
277
|
|
|
269
278
|
const shadowRoot = this._shadow ?? this.shadowRoot;
|
|
@@ -275,7 +284,7 @@ export function Elena(superClass) {
|
|
|
275
284
|
// Normalize to array and cache converted CSSStyleSheet instances on the class.
|
|
276
285
|
// Avoids re-parsing CSS strings on every element instance.
|
|
277
286
|
if (!component._adoptedSheets) {
|
|
278
|
-
const stylesList =
|
|
287
|
+
const stylesList = [component.styles].flat();
|
|
279
288
|
|
|
280
289
|
component._adoptedSheets = stylesList.map(s => {
|
|
281
290
|
if (typeof s === "string") {
|
|
@@ -297,22 +306,23 @@ export function Elena(superClass) {
|
|
|
297
306
|
* @internal
|
|
298
307
|
*/
|
|
299
308
|
_applyRender() {
|
|
309
|
+
const constructor = this.constructor;
|
|
310
|
+
const root = this._root;
|
|
300
311
|
const result = this.render();
|
|
301
312
|
|
|
302
313
|
if (result && result.strings) {
|
|
303
|
-
const root = this._renderRoot;
|
|
304
314
|
const rebuilt = renderTemplate(root, result.strings, result.values);
|
|
305
315
|
|
|
306
316
|
// Re-resolve element ref when the DOM was fully rebuilt.
|
|
307
|
-
//
|
|
317
|
+
// patch() and morph() leave the DOM structure intact,
|
|
308
318
|
// so the existing ref is still valid.
|
|
309
319
|
if (rebuilt) {
|
|
310
320
|
const oldElement = this.element;
|
|
311
|
-
this.element =
|
|
321
|
+
this.element = constructor._resolver(root);
|
|
312
322
|
|
|
313
323
|
// Re-bind event listeners when the inner element was replaced.
|
|
314
324
|
if (this._events && oldElement && this.element !== oldElement) {
|
|
315
|
-
const events =
|
|
325
|
+
const events = constructor._elenaEvents;
|
|
316
326
|
|
|
317
327
|
for (const e of events) {
|
|
318
328
|
oldElement.removeEventListener(e, this);
|
|
@@ -324,12 +334,11 @@ export function Elena(superClass) {
|
|
|
324
334
|
|
|
325
335
|
// Resolve inner element on first render
|
|
326
336
|
if (!this.element) {
|
|
327
|
-
|
|
328
|
-
this.element = this.constructor._resolver(root);
|
|
337
|
+
this.element = constructor._resolver(root);
|
|
329
338
|
|
|
330
339
|
if (!this.element) {
|
|
331
|
-
if (
|
|
332
|
-
|
|
340
|
+
if (constructor.element) {
|
|
341
|
+
warn("Element not found.");
|
|
333
342
|
}
|
|
334
343
|
this.element = root.firstElementChild;
|
|
335
344
|
}
|
|
@@ -373,7 +382,7 @@ export function Elena(superClass) {
|
|
|
373
382
|
|
|
374
383
|
if (!this._events && events?.length) {
|
|
375
384
|
if (!this.element) {
|
|
376
|
-
|
|
385
|
+
warn("Cannot add events.");
|
|
377
386
|
} else {
|
|
378
387
|
this._events = true;
|
|
379
388
|
|
|
@@ -438,6 +447,7 @@ export function Elena(superClass) {
|
|
|
438
447
|
* events in Shadow DOM (change, submit, reset).
|
|
439
448
|
* Composed bubbling events (click, input) pass through on their own.
|
|
440
449
|
*
|
|
450
|
+
* @param {Event} event
|
|
441
451
|
* @internal
|
|
442
452
|
*/
|
|
443
453
|
handleEvent(event) {
|
|
@@ -445,7 +455,7 @@ export function Elena(superClass) {
|
|
|
445
455
|
return;
|
|
446
456
|
}
|
|
447
457
|
|
|
448
|
-
if (!event.bubbles || (!event.composed && this.
|
|
458
|
+
if (!event.bubbles || (!event.composed && this._root !== this)) {
|
|
449
459
|
/** @internal */
|
|
450
460
|
this.dispatchEvent(new Event(event.type, { bubbles: event.bubbles }));
|
|
451
461
|
}
|
|
@@ -475,12 +485,16 @@ export function Elena(superClass) {
|
|
|
475
485
|
* Registers the component as a custom element using `static tagName`.
|
|
476
486
|
* Call this on your component class after the class body is defined,
|
|
477
487
|
* not on the Elena mixin itself.
|
|
488
|
+
*
|
|
489
|
+
* @param {CustomElementRegistry} [registry] - A scoped registry to register in.
|
|
490
|
+
* When omitted, registers in the global `customElements` registry.
|
|
478
491
|
*/
|
|
479
|
-
static define() {
|
|
480
|
-
|
|
481
|
-
|
|
492
|
+
static define(registry) {
|
|
493
|
+
const tag = this.tagName;
|
|
494
|
+
if (tag) {
|
|
495
|
+
defineElement(tag, this, registry);
|
|
482
496
|
} else {
|
|
483
|
-
|
|
497
|
+
warn("define() without a tagName.");
|
|
484
498
|
}
|
|
485
499
|
}
|
|
486
500
|
|
|
@@ -496,16 +510,7 @@ export function Elena(superClass) {
|
|
|
496
510
|
}
|
|
497
511
|
if (!this._renderPending) {
|
|
498
512
|
this._renderPending = true;
|
|
499
|
-
this.
|
|
500
|
-
this._resolveUpdate = resolve;
|
|
501
|
-
});
|
|
502
|
-
queueMicrotask(() => {
|
|
503
|
-
try {
|
|
504
|
-
this._performUpdate();
|
|
505
|
-
} catch (e) {
|
|
506
|
-
console.error("░█ [ELENA]:", e);
|
|
507
|
-
}
|
|
508
|
-
});
|
|
513
|
+
queueMicrotask(this._runUpdate);
|
|
509
514
|
}
|
|
510
515
|
}
|
|
511
516
|
|
|
@@ -530,7 +535,7 @@ export function Elena(superClass) {
|
|
|
530
535
|
this.updated();
|
|
531
536
|
} finally {
|
|
532
537
|
this._updateComplete = null;
|
|
533
|
-
resolve();
|
|
538
|
+
resolve?.();
|
|
534
539
|
}
|
|
535
540
|
}
|
|
536
541
|
|
|
@@ -541,10 +546,15 @@ export function Elena(superClass) {
|
|
|
541
546
|
* @type {Promise<void>}
|
|
542
547
|
*/
|
|
543
548
|
get updateComplete() {
|
|
544
|
-
if (this.
|
|
545
|
-
return
|
|
549
|
+
if (!this._renderPending) {
|
|
550
|
+
return Promise.resolve();
|
|
551
|
+
}
|
|
552
|
+
if (!this._updateComplete) {
|
|
553
|
+
this._updateComplete = new Promise(resolve => {
|
|
554
|
+
this._resolveUpdate = resolve;
|
|
555
|
+
});
|
|
546
556
|
}
|
|
547
|
-
return
|
|
557
|
+
return this._updateComplete;
|
|
548
558
|
}
|
|
549
559
|
|
|
550
560
|
/**
|