@webjsdev/core 0.7.64 → 0.8.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/dist/webjs-core-browser.js +5 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +21 -22
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +6 -7
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +6 -2
- package/index.d.ts +7 -3
- package/index.js +6 -2
- package/package.json +1 -1
- package/src/attribute-directives.d.ts +6 -0
- package/src/attribute-directives.js +49 -0
- package/src/attribute-reader.js +37 -3
- package/src/binding-prefixes.js +10 -41
- package/src/component/lifecycle.js +261 -101
- package/src/component/reactive.js +5 -4
- package/src/component.d.ts +11 -5
- package/src/directives.d.ts +2 -0
- package/src/directives.js +2 -0
- package/src/escape.js +12 -2
- package/src/form-action.js +7 -7
- package/src/html.d.ts +4 -0
- package/src/html.js +22 -1
- package/src/optimistic.js +3 -3
- package/src/registry.d.ts +5 -0
- package/src/registry.js +35 -4
- package/src/render-client/adopt-slots.js +245 -0
- package/src/render-client/parts.js +53 -19
- package/src/render-client/reconciler.js +28 -12
- package/src/render-client/template-compiler.js +19 -5
- package/src/render-client.d.ts +2 -1
- package/src/render-server/attrs.js +5 -4
- package/src/render-server/compile.js +433 -0
- package/src/render-server/dsd.js +11 -435
- package/src/render-server/errors.js +84 -0
- package/src/render-server/expand.js +346 -0
- package/src/render-server/html-scan.js +19 -358
- package/src/render-server/served-attrs.js +79 -0
- package/src/render-server/stream.js +11 -13
- package/src/render-server/template-renderer.js +13 -936
- package/src/render-server/walk.js +798 -0
- package/src/router-client/anchors.js +3 -1
- package/src/router-client/boundaries.js +15 -1
- package/src/router-client/constants.js +30 -0
- package/src/router-client/fetch-apply.js +21 -0
- package/src/router-client/frames.js +6 -3
- package/src/router-client/head-merge.js +58 -10
- package/src/router-client/navigator.js +4 -0
- package/src/router-client/owned-styles.js +372 -0
- package/src/router-client/prefetch.js +3 -0
- package/src/router-client/snapshot-cache.js +2 -12
- package/src/router-client/stylesheets.js +191 -0
- package/src/router-client/swap.js +49 -9
- package/src/sentinels.d.ts +2 -0
- package/src/sentinels.js +4 -0
- package/src/slot.js +1 -1
- package/src/webjs-config.d.ts +1 -1
- package/src/render-server/slots.js +0 -213
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { render as clientRender } from '../render-client.js';
|
|
2
|
-
import { readAttributeValue, resolveAttributeProperty, observedAttributeNames } from '../attribute-reader.js';
|
|
2
|
+
import { readAttributeValue, resolveAttributeProperty, observedAttributeNames, isAttributeBacked } from '../attribute-reader.js';
|
|
3
3
|
import { setActiveActionSignal, activeActionSignal } from '../action-abort-client.js';
|
|
4
4
|
import { carriesFunction } from '../form-action.js';
|
|
5
5
|
import { isCSS, adoptStyles } from '../css.js';
|
|
@@ -84,7 +84,7 @@ const isBrowser = typeof window !== 'undefined' && typeof HTMLElement !== 'undef
|
|
|
84
84
|
* @property {(newValue: unknown, oldValue: unknown) => boolean} [hasChanged]
|
|
85
85
|
* Custom dirty-check function. Called by the generated setter before
|
|
86
86
|
* scheduling an update. Return `true` to trigger a re-render, `false`
|
|
87
|
-
* to skip. Default:
|
|
87
|
+
* to skip. Default: `(a, b) => !Object.is(a, b)`.
|
|
88
88
|
*
|
|
89
89
|
* Note: this fires on the FIRST assignment too, with `oldValue` set
|
|
90
90
|
* to `undefined`. Numeric comparators that subtract against undefined
|
|
@@ -115,6 +115,61 @@ const FACTORY_PROPS = Symbol('webjs.factoryProps');
|
|
|
115
115
|
// Per-class memo so the constructor-time enforcement walk runs once per class.
|
|
116
116
|
const _propsChecked = new WeakSet();
|
|
117
117
|
|
|
118
|
+
/** A settled `updateComplete` shared by every instance until its first update. */
|
|
119
|
+
const RESOLVED_TRUE = Promise.resolve(true);
|
|
120
|
+
/** Iterated in place of a controller set no instance has created. */
|
|
121
|
+
const NO_CONTROLLERS = new Set();
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Per class, the declared reactive properties with their accessors installed
|
|
125
|
+
* on the prototype.
|
|
126
|
+
* @type {WeakMap<Function, { propName: string, decl: any }[]>}
|
|
127
|
+
*/
|
|
128
|
+
const _accessorPlans = new WeakMap();
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Define the getter/setter pair for every declared property on the class's
|
|
132
|
+
* prototype, once, and record the plan the constructor applies per instance
|
|
133
|
+
* (own-value capture and `default`). The setter coerces nothing itself (the
|
|
134
|
+
* attribute readers do), runs `hasChanged`, reflects, and schedules an update.
|
|
135
|
+
* @param {any} Ctor
|
|
136
|
+
* @param {Record<string, unknown>} props
|
|
137
|
+
*/
|
|
138
|
+
function installAccessors(Ctor, props) {
|
|
139
|
+
/** @type {{ propName: string, decl: any }[]} */
|
|
140
|
+
const plan = [];
|
|
141
|
+
for (const [propName, decl] of Object.entries(props)) {
|
|
142
|
+
const d = typeof decl === 'object' && decl !== null ? decl : { type: decl };
|
|
143
|
+
plan.push({ propName, decl: d });
|
|
144
|
+
const changedCheck = d.hasChanged || defaultHasChanged;
|
|
145
|
+
Object.defineProperty(Ctor.prototype, propName, {
|
|
146
|
+
configurable: true,
|
|
147
|
+
enumerable: true,
|
|
148
|
+
get() { return this.__propValues[propName]; },
|
|
149
|
+
set(newVal) {
|
|
150
|
+
const oldVal = this.__propValues[propName];
|
|
151
|
+
this.__propValues[propName] = newVal;
|
|
152
|
+
if (!changedCheck(newVal, oldVal)) return;
|
|
153
|
+
// Reflect to attribute if requested (_reflectAttribute skips a prop
|
|
154
|
+
// with no attribute: `state: true` or `attribute: false`).
|
|
155
|
+
if (d.reflect) {
|
|
156
|
+
if (this._connected) {
|
|
157
|
+
this._reflectAttribute(propName, newVal, d);
|
|
158
|
+
} else {
|
|
159
|
+
(this.__reflectPending ??= new Set()).add(propName);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// requestUpdate records the (name, oldValue) entry AND schedules a
|
|
163
|
+
// render. During the update phase the scheduler short-circuits and
|
|
164
|
+
// the entry folds into the current cycle's changedProperties.
|
|
165
|
+
this.requestUpdate(propName, oldVal);
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
_accessorPlans.set(Ctor, plan);
|
|
170
|
+
return plan;
|
|
171
|
+
}
|
|
172
|
+
|
|
118
173
|
class WebComponentBase extends Base {
|
|
119
174
|
/** Whether to use shadow DOM. Default: false (light DOM). @type {boolean} */
|
|
120
175
|
static shadow = false;
|
|
@@ -146,8 +201,9 @@ class WebComponentBase extends Base {
|
|
|
146
201
|
* that trigger re-renders on change, coerce attribute values by type,
|
|
147
202
|
* and optionally reflect back to attributes.
|
|
148
203
|
*
|
|
149
|
-
* Properties with `state: true`
|
|
150
|
-
*
|
|
204
|
+
* Properties with `state: true` (internal state) or `attribute: false` (a
|
|
205
|
+
* property-only input) are excluded from `observedAttributes` and never
|
|
206
|
+
* reflect.
|
|
151
207
|
*
|
|
152
208
|
* @type {Record<string, PropertyDeclaration>}
|
|
153
209
|
*/
|
|
@@ -178,8 +234,8 @@ class WebComponentBase extends Base {
|
|
|
178
234
|
|
|
179
235
|
/**
|
|
180
236
|
* Returns the list of attribute names the browser should observe.
|
|
181
|
-
* Properties with `state: true` are excluded: they
|
|
182
|
-
*
|
|
237
|
+
* Properties with `state: true` or `attribute: false` are excluded: they
|
|
238
|
+
* do not correspond to any HTML attribute.
|
|
183
239
|
*
|
|
184
240
|
* @returns {string[]}
|
|
185
241
|
*/
|
|
@@ -199,10 +255,12 @@ class WebComponentBase extends Base {
|
|
|
199
255
|
this._connected = false;
|
|
200
256
|
|
|
201
257
|
/**
|
|
202
|
-
* Set of attached reactive controllers
|
|
203
|
-
*
|
|
258
|
+
* Set of attached reactive controllers, created by the first
|
|
259
|
+
* `addController` (#1646: most instances never attach one, and the SSR
|
|
260
|
+
* walk constructs an instance per rendered element).
|
|
261
|
+
* @type {Set<ReactiveController> | null}
|
|
204
262
|
*/
|
|
205
|
-
this.__controllers =
|
|
263
|
+
this.__controllers = null;
|
|
206
264
|
|
|
207
265
|
/**
|
|
208
266
|
* Whether the component has completed its first render.
|
|
@@ -235,7 +293,7 @@ class WebComponentBase extends Base {
|
|
|
235
293
|
* @type {Promise<boolean>}
|
|
236
294
|
* @private
|
|
237
295
|
*/
|
|
238
|
-
this._updatePromise =
|
|
296
|
+
this._updatePromise = RESOLVED_TRUE;
|
|
239
297
|
|
|
240
298
|
/**
|
|
241
299
|
* Set while the component is inside the update phase (between
|
|
@@ -288,13 +346,14 @@ class WebComponentBase extends Base {
|
|
|
288
346
|
}
|
|
289
347
|
|
|
290
348
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
349
|
+
* Give the instance its reactive-property backing store and make sure the
|
|
350
|
+
* class's getter/setter pairs (which run `hasChanged`, schedule updates and
|
|
351
|
+
* optionally reflect to the HTML attribute) are installed on its PROTOTYPE,
|
|
352
|
+
* once per class. Per instance this only lifts a value written before the
|
|
353
|
+
* element upgraded and applies the declared `default`s.
|
|
294
354
|
*
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
* prototype.
|
|
355
|
+
* Called once from the constructor. The backing store is a plain object
|
|
356
|
+
* (`this.__propValues`), so the accessors never collide with it.
|
|
298
357
|
* @private
|
|
299
358
|
*/
|
|
300
359
|
_initializeProperties() {
|
|
@@ -305,35 +364,24 @@ class WebComponentBase extends Base {
|
|
|
305
364
|
/** @type {Record<string, unknown>} */
|
|
306
365
|
this.__propValues = {};
|
|
307
366
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
this._reflectAttribute(propName, newVal, d);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// requestUpdate records the (name, oldValue) entry AND schedules
|
|
330
|
-
// a render. When called during the update phase (willUpdate /
|
|
331
|
-
// hostUpdate / update / hostUpdated), the scheduler short-circuits
|
|
332
|
-
// and the entry folds into the current cycle's changedProperties.
|
|
333
|
-
this.requestUpdate(propName, oldVal);
|
|
334
|
-
},
|
|
335
|
-
});
|
|
336
|
-
|
|
367
|
+
// The accessors live on the PROTOTYPE, installed once per class (#1646).
|
|
368
|
+
// They used to be defined on every instance, two closures and a
|
|
369
|
+
// defineProperty per declared property, which was the largest single
|
|
370
|
+
// cost of constructing a component on the SSR path (one instance per
|
|
371
|
+
// rendered element) and a measurable one on upgrade in the browser.
|
|
372
|
+
let plan = _accessorPlans.get(Ctor);
|
|
373
|
+
if (plan === undefined) plan = installAccessors(Ctor, props);
|
|
374
|
+
|
|
375
|
+
for (let i = 0; i < plan.length; i++) {
|
|
376
|
+
const { propName, decl: d } = plan[i];
|
|
377
|
+
// Capture any value set before the accessor could apply (an own data
|
|
378
|
+
// property written on the element before it upgraded). It shadows the
|
|
379
|
+
// prototype accessor, so it is lifted into the backing store.
|
|
380
|
+
let initial;
|
|
381
|
+
if (Object.hasOwn(this, propName)) {
|
|
382
|
+
initial = /** @type {any} */ (this)[propName];
|
|
383
|
+
delete (/** @type {any} */ (this))[propName];
|
|
384
|
+
}
|
|
337
385
|
if (initial !== undefined) {
|
|
338
386
|
this.__propValues[propName] = initial;
|
|
339
387
|
} else if (d.default !== undefined) {
|
|
@@ -358,6 +406,9 @@ class WebComponentBase extends Base {
|
|
|
358
406
|
* @private
|
|
359
407
|
*/
|
|
360
408
|
_reflectAttribute(propName, value, decl) {
|
|
409
|
+
// A property with no attribute (`state: true` / `attribute: false`) has
|
|
410
|
+
// nothing to reflect to.
|
|
411
|
+
if (!isAttributeBacked(decl)) return;
|
|
361
412
|
// A custom `attribute` option wins over the kebab-cased property name.
|
|
362
413
|
const attrName = decl.attribute || hyphenate(propName);
|
|
363
414
|
// Guard against re-entrant loops: attributeChangedCallback fires when
|
|
@@ -535,6 +586,17 @@ class WebComponentBase extends Base {
|
|
|
535
586
|
|
|
536
587
|
_activate() {
|
|
537
588
|
this._connected = true;
|
|
589
|
+
// A value written on the element before its class was defined is an own
|
|
590
|
+
// data property, and it hides any accessor of the same name for good.
|
|
591
|
+
// `_initializeProperties` lifts the declared properties in the
|
|
592
|
+
// constructor; an accessor the subclass defines itself (a getter/setter
|
|
593
|
+
// over a private signal, the shape internal state another element assigns
|
|
594
|
+
// takes) gets the same lift here, on the first connect, since its private
|
|
595
|
+
// field does not exist yet while the base constructor runs.
|
|
596
|
+
if (!this.__accessorsLifted) {
|
|
597
|
+
this.__accessorsLifted = true;
|
|
598
|
+
liftShadowedAccessors(this);
|
|
599
|
+
}
|
|
538
600
|
// Reflect declared reflect:true properties from their current value now
|
|
539
601
|
// that the element is connected. Constructor / willUpdate defaults were
|
|
540
602
|
// set while disconnected (the setter skips reflection then), so this is
|
|
@@ -596,7 +658,7 @@ class WebComponentBase extends Base {
|
|
|
596
658
|
// b. SSR hydration (first mount, <!--webjs-hydrate--> marker
|
|
597
659
|
// present). Children are already inside their
|
|
598
660
|
// <slot data-webjs-light data-projection="actual"> elements
|
|
599
|
-
// (
|
|
661
|
+
// (the SSR walk placed them). Adopt those assignments BEFORE
|
|
600
662
|
// _performRender so we retain references to the SSR'd nodes;
|
|
601
663
|
// the renderer's createInstance().replaceChildren() will
|
|
602
664
|
// detach them, but the slot-apply step re-attaches the same
|
|
@@ -648,7 +710,7 @@ class WebComponentBase extends Base {
|
|
|
648
710
|
}
|
|
649
711
|
|
|
650
712
|
// Notify all controllers that the host is connected.
|
|
651
|
-
for (const c of this.__controllers) {
|
|
713
|
+
for (const c of this.__controllers || NO_CONTROLLERS) {
|
|
652
714
|
if (c.hostConnected) c.hostConnected();
|
|
653
715
|
}
|
|
654
716
|
|
|
@@ -731,11 +793,14 @@ class WebComponentBase extends Base {
|
|
|
731
793
|
// Dispose the signal watcher so dependency edges drop. Without
|
|
732
794
|
// this the element holds references to module-scope signals
|
|
733
795
|
// (and vice versa) forever.
|
|
734
|
-
|
|
735
|
-
this
|
|
736
|
-
|
|
796
|
+
for (const slot of ['__signalWatcher', '__postWatcher']) {
|
|
797
|
+
const self = /** @type {any} */ (this);
|
|
798
|
+
if (self[slot]) {
|
|
799
|
+
self[slot].dispose();
|
|
800
|
+
self[slot] = undefined;
|
|
801
|
+
}
|
|
737
802
|
}
|
|
738
|
-
for (const c of this.__controllers) {
|
|
803
|
+
for (const c of this.__controllers || NO_CONTROLLERS) {
|
|
739
804
|
if (c.hostDisconnected) c.hostDisconnected();
|
|
740
805
|
}
|
|
741
806
|
}
|
|
@@ -832,18 +897,92 @@ class WebComponentBase extends Base {
|
|
|
832
897
|
* 7. `firstUpdated(changedProperties)` runs once, on the first render only
|
|
833
898
|
* 8. `updated(changedProperties)` runs after every commit
|
|
834
899
|
* 9. Resolve `updateComplete` promise
|
|
900
|
+
*
|
|
901
|
+
* Every signal the SYNCHRONOUS part of that cycle reads is tracked, not
|
|
902
|
+
* only the ones `render()` reads, which is the span lit-labs/signals tracks
|
|
903
|
+
* by running `performUpdate` inside a computed. So a signal read only in
|
|
904
|
+
* `willUpdate()` or `updated()` (a host attribute derived from internal
|
|
905
|
+
* state) re-renders the component when it changes. Two watchers split it
|
|
906
|
+
* (see {@link _tracked}): one over steps 2-6 (`render()` included), one
|
|
907
|
+
* over the post-commit hooks, so a write `updated()` makes to something
|
|
908
|
+
* `render()` read still schedules the next update. The DOM commits are
|
|
909
|
+
* the parts left untracked (see `update()`).
|
|
835
910
|
* @private
|
|
836
911
|
*/
|
|
837
912
|
_performRender() {
|
|
838
913
|
if (!this._renderRoot) return;
|
|
914
|
+
this.__cycleRendered = false;
|
|
915
|
+
const postCommit = this._tracked('__signalWatcher', () => this._runUpdateCycle());
|
|
916
|
+
// A sync commit runs its post-commit hooks OUTSIDE the first watcher's
|
|
917
|
+
// phase, under their own watcher.
|
|
918
|
+
if (postCommit) postCommit();
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Run one phase of the update cycle with the signals it reads recorded on
|
|
923
|
+
* the watcher in `slot`, creating the watcher on first use. A watcher
|
|
924
|
+
* re-records its read set on every phase it observes, and a later change
|
|
925
|
+
* to any of those signals schedules an update.
|
|
926
|
+
*
|
|
927
|
+
* Three rules keep that safe:
|
|
928
|
+
* - A phase ignores notifications from its OWN writes. A write to a signal
|
|
929
|
+
* the phase read earlier is the phase's business, not a reason to run it
|
|
930
|
+
* again: before a commit it is already folded into this pass, and after
|
|
931
|
+
* one it is often not the component's code at all (an event `updated()`
|
|
932
|
+
* dispatches, whose listener does a read-modify-write on a store), which
|
|
933
|
+
* would otherwise loop.
|
|
934
|
+
* - A pre-commit phase that never reached `render()` (`shouldUpdate()`
|
|
935
|
+
* returned false, or a hook threw) keeps the signals the last render
|
|
936
|
+
* read on top of the ones it read itself, or a skipped cycle would
|
|
937
|
+
* silently unsubscribe the component.
|
|
938
|
+
* - A watcher disposed during its own phase (a hook that detached the
|
|
939
|
+
* host) is disposed again afterwards, since the phase kept recording.
|
|
940
|
+
*
|
|
941
|
+
* @template T
|
|
942
|
+
* @param {'__signalWatcher' | '__postWatcher'} slot
|
|
943
|
+
* @param {() => T} fn
|
|
944
|
+
* @returns {T}
|
|
945
|
+
* @private
|
|
946
|
+
*/
|
|
947
|
+
_tracked(slot, fn) {
|
|
948
|
+
const self = /** @type {any} */ (this);
|
|
949
|
+
/** @type {any} */
|
|
950
|
+
let watcher = self[slot];
|
|
951
|
+
if (!watcher) {
|
|
952
|
+
watcher = self[slot] = new Signal.subtle.Watcher(() => {
|
|
953
|
+
if (!watcher.__inPhase && this._connected) this.requestUpdate();
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
const before = slot === '__signalWatcher' ? Signal.subtle.introspectSources(watcher) : null;
|
|
957
|
+
watcher.__inPhase = true;
|
|
958
|
+
try {
|
|
959
|
+
return watcher.observe(fn);
|
|
960
|
+
} finally {
|
|
961
|
+
watcher.__inPhase = false;
|
|
962
|
+
if (before && !this.__cycleRendered) {
|
|
963
|
+
watcher.watch(...new Set([...before, ...Signal.subtle.introspectSources(watcher)]));
|
|
964
|
+
}
|
|
965
|
+
if (!this._connected) {
|
|
966
|
+
watcher.dispose();
|
|
967
|
+
if (self[slot] === watcher) self[slot] = undefined;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Steps 1-6 of the update cycle, the phase {@link _performRender} tracks
|
|
974
|
+
* with the first watcher. Returns the post-commit half to run for a sync
|
|
975
|
+
* commit, or nothing (an async commit runs it when it lands).
|
|
976
|
+
* @returns {(() => void) | undefined}
|
|
977
|
+
* @private
|
|
978
|
+
*/
|
|
979
|
+
_runUpdateCycle() {
|
|
839
980
|
|
|
840
981
|
// Hold a stable reference to the current Map so all hooks see the same
|
|
841
982
|
// snapshot. During the update phase (steps 3-6) the Map is mutated in
|
|
842
983
|
// place when property setters fire, folding those changes into THIS
|
|
843
|
-
// cycle. The Map is replaced
|
|
844
|
-
//
|
|
845
|
-
// On `shouldUpdate=false` or hook errors, the Map is preserved so the
|
|
846
|
-
// accumulated changes survive into the next render.
|
|
984
|
+
// cycle. The Map is replaced when this phase finishes, including skipped
|
|
985
|
+
// updates and hook errors, so post-commit writes belong to the NEXT cycle.
|
|
847
986
|
const changedProperties = this._changedProperties;
|
|
848
987
|
|
|
849
988
|
// --- 1. Mark we're inside an update cycle ---
|
|
@@ -862,7 +1001,7 @@ class WebComponentBase extends Base {
|
|
|
862
1001
|
this.willUpdate(changedProperties);
|
|
863
1002
|
|
|
864
1003
|
// --- 4. controllers' hostUpdate ---
|
|
865
|
-
for (const c of this.__controllers) {
|
|
1004
|
+
for (const c of this.__controllers || NO_CONTROLLERS) {
|
|
866
1005
|
if (c.hostUpdate) c.hostUpdate();
|
|
867
1006
|
}
|
|
868
1007
|
|
|
@@ -881,6 +1020,7 @@ class WebComponentBase extends Base {
|
|
|
881
1020
|
// in _commitAsync.
|
|
882
1021
|
if (this.__renderAbort) this.__renderAbort.abort();
|
|
883
1022
|
this.__renderAbort = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
1023
|
+
this.__cycleRendered = true;
|
|
884
1024
|
try {
|
|
885
1025
|
const r = this.update(changedProperties);
|
|
886
1026
|
if (r && typeof r.then === 'function') pendingCommit = r;
|
|
@@ -889,26 +1029,17 @@ class WebComponentBase extends Base {
|
|
|
889
1029
|
}
|
|
890
1030
|
|
|
891
1031
|
if (!pendingCommit) {
|
|
892
|
-
// --- 6. controllers' hostUpdated (sync commit) ---
|
|
893
|
-
for (const c of this.__controllers) {
|
|
894
|
-
if (c.hostUpdated) c.hostUpdated();
|
|
895
|
-
}
|
|
896
1032
|
didCommit = true;
|
|
897
1033
|
}
|
|
898
1034
|
}
|
|
899
|
-
// shouldUpdate=false: preserve _changedProperties so the next
|
|
900
|
-
// requestUpdate keeps accumulating on top of the entries that
|
|
901
|
-
// didn't render this cycle.
|
|
902
1035
|
} catch (preCommitError) {
|
|
903
1036
|
console.error(`[webjs] lifecycle hook threw during update phase:`, preCommitError);
|
|
904
1037
|
} finally {
|
|
905
1038
|
this._isUpdating = false;
|
|
906
|
-
//
|
|
907
|
-
// render
|
|
908
|
-
//
|
|
909
|
-
|
|
910
|
-
this._changedProperties = new Map();
|
|
911
|
-
}
|
|
1039
|
+
// Consume this phase's snapshot even when shouldUpdate skips it. For an
|
|
1040
|
+
// async render, changes during the fetch start a new cycle whose token
|
|
1041
|
+
// supersedes the in-flight commit.
|
|
1042
|
+
this._changedProperties = new Map();
|
|
912
1043
|
}
|
|
913
1044
|
|
|
914
1045
|
// --- Async render: finish the cycle when the pending commit settles. ---
|
|
@@ -925,10 +1056,6 @@ class WebComponentBase extends Base {
|
|
|
925
1056
|
this.__pendingAsyncCommits--;
|
|
926
1057
|
// A newer render superseded this one; let the newer cycle finish.
|
|
927
1058
|
if (token !== this.__renderToken) return;
|
|
928
|
-
// --- 6. controllers' hostUpdated (after the async commit) ---
|
|
929
|
-
for (const c of this.__controllers) {
|
|
930
|
-
if (c.hostUpdated) c.hostUpdated();
|
|
931
|
-
}
|
|
932
1059
|
// --- 7-8 + updateComplete ---
|
|
933
1060
|
this._postCommit(changedProperties);
|
|
934
1061
|
};
|
|
@@ -966,16 +1093,21 @@ class WebComponentBase extends Base {
|
|
|
966
1093
|
return;
|
|
967
1094
|
}
|
|
968
1095
|
|
|
969
|
-
// --- 7-8. Post-commit hooks (sync path)
|
|
970
|
-
// updateComplete promise always
|
|
1096
|
+
// --- 7-8. Post-commit hooks (sync path), returned so they run outside
|
|
1097
|
+
// this phase. Errors are caught so the updateComplete promise always
|
|
1098
|
+
// resolves.
|
|
971
1099
|
if (didCommit) {
|
|
972
|
-
this._postCommit(changedProperties);
|
|
1100
|
+
return () => this._postCommit(changedProperties);
|
|
973
1101
|
} else if (!this.__pendingAsyncCommits) {
|
|
974
1102
|
// A non-committing cycle resolves updateComplete only when no async
|
|
975
1103
|
// commit is in flight; otherwise the pending commit resolves it once it
|
|
976
1104
|
// lands, so `await el.updateComplete` never returns before that DOM.
|
|
977
1105
|
this._resolveUpdate();
|
|
978
1106
|
}
|
|
1107
|
+
// No post-commit phase this cycle: re-arm its watcher, so a change it
|
|
1108
|
+
// was notified of meanwhile does not leave it deaf to the next one.
|
|
1109
|
+
if (!didCommit && /** @type {any} */ (this).__postWatcher) /** @type {any} */ (this).__postWatcher.watch();
|
|
1110
|
+
return undefined;
|
|
979
1111
|
}
|
|
980
1112
|
|
|
981
1113
|
/**
|
|
@@ -986,7 +1118,7 @@ class WebComponentBase extends Base {
|
|
|
986
1118
|
*/
|
|
987
1119
|
_resolveUpdate() {
|
|
988
1120
|
if (this._updateResolve) {
|
|
989
|
-
const settled = this.
|
|
1121
|
+
const settled = !this._scheduled;
|
|
990
1122
|
this._updateResolve(settled);
|
|
991
1123
|
this._updateResolve = null;
|
|
992
1124
|
}
|
|
@@ -1018,7 +1150,7 @@ class WebComponentBase extends Base {
|
|
|
1018
1150
|
this._isUpdating = true;
|
|
1019
1151
|
try {
|
|
1020
1152
|
this.willUpdate(changedProperties);
|
|
1021
|
-
for (const c of this.__controllers) {
|
|
1153
|
+
for (const c of this.__controllers || NO_CONTROLLERS) {
|
|
1022
1154
|
if (c.hostUpdate) c.hostUpdate();
|
|
1023
1155
|
}
|
|
1024
1156
|
this._reflectDeclaredAttributes();
|
|
@@ -1048,7 +1180,7 @@ class WebComponentBase extends Base {
|
|
|
1048
1180
|
const props = /** @type {any} */ (this.constructor).properties || {};
|
|
1049
1181
|
for (const name of Object.keys(props)) {
|
|
1050
1182
|
const decl = props[name];
|
|
1051
|
-
if (!decl || !decl.reflect
|
|
1183
|
+
if (!decl || !decl.reflect) continue;
|
|
1052
1184
|
this._reflectAttribute(name, this[name], decl);
|
|
1053
1185
|
}
|
|
1054
1186
|
}
|
|
@@ -1112,16 +1244,14 @@ class WebComponentBase extends Base {
|
|
|
1112
1244
|
* @param {Map<string, unknown>} _changedProperties
|
|
1113
1245
|
*/
|
|
1114
1246
|
update(_changedProperties) {
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
if (!this.__signalWatcher) {
|
|
1121
|
-
this.__signalWatcher = new Signal.subtle.Watcher(() => {
|
|
1122
|
-
if (this._connected) this.requestUpdate();
|
|
1123
|
-
});
|
|
1247
|
+
const pendingReflection = this.__reflectPending;
|
|
1248
|
+
this.__reflectPending = undefined;
|
|
1249
|
+
for (const name of pendingReflection || []) {
|
|
1250
|
+
const def = this.constructor.properties[name];
|
|
1251
|
+
this._reflectAttribute(name, this[name], def);
|
|
1124
1252
|
}
|
|
1253
|
+
// The signal reads render() makes are tracked by the watcher
|
|
1254
|
+
// _performRender runs the whole cycle under.
|
|
1125
1255
|
let tpl;
|
|
1126
1256
|
// Bind this render's abort signal (#492) around the synchronous part of
|
|
1127
1257
|
// render() ONLY, so the RPC stub ties an action called there to this
|
|
@@ -1134,7 +1264,7 @@ class WebComponentBase extends Base {
|
|
|
1134
1264
|
const outer = activeActionSignal();
|
|
1135
1265
|
setActiveActionSignal(this.__renderAbort ? this.__renderAbort.signal : null);
|
|
1136
1266
|
try {
|
|
1137
|
-
|
|
1267
|
+
tpl = this.render();
|
|
1138
1268
|
} finally {
|
|
1139
1269
|
setActiveActionSignal(outer || null);
|
|
1140
1270
|
}
|
|
@@ -1149,7 +1279,10 @@ class WebComponentBase extends Base {
|
|
|
1149
1279
|
if (tpl && typeof (/** @type any */ (tpl).then) === 'function') {
|
|
1150
1280
|
return this._commitAsync(/** @type {Promise<unknown>} */ (tpl));
|
|
1151
1281
|
}
|
|
1152
|
-
|
|
1282
|
+
// The commit is untracked: a signal read while the DOM is patched (a
|
|
1283
|
+
// `watch()` part, a child's synchronous work) belongs to whatever read
|
|
1284
|
+
// it, not to this component's next render.
|
|
1285
|
+
Signal.subtle.untrack(() => clientRender(tpl, this._renderRoot, { host: this }));
|
|
1153
1286
|
return undefined;
|
|
1154
1287
|
}
|
|
1155
1288
|
|
|
@@ -1178,7 +1311,7 @@ class WebComponentBase extends Base {
|
|
|
1178
1311
|
if (this.__firstRendered && this._overridesRenderFallback()) {
|
|
1179
1312
|
try {
|
|
1180
1313
|
const fb = this.renderFallback();
|
|
1181
|
-
if (fb !== undefined) clientRender(fb, this._renderRoot);
|
|
1314
|
+
if (fb !== undefined) Signal.subtle.untrack(() => clientRender(fb, this._renderRoot, { host: this }));
|
|
1182
1315
|
} catch (e) {
|
|
1183
1316
|
console.error(`[webjs] renderFallback() threw:`, e);
|
|
1184
1317
|
}
|
|
@@ -1197,7 +1330,7 @@ class WebComponentBase extends Base {
|
|
|
1197
1330
|
// The sync path routes an identical throw to the same boundary, so
|
|
1198
1331
|
// this is what makes the two paths agree.
|
|
1199
1332
|
try {
|
|
1200
|
-
clientRender(tpl, this._renderRoot);
|
|
1333
|
+
clientRender(tpl, this._renderRoot, { host: this });
|
|
1201
1334
|
} catch (commitError) {
|
|
1202
1335
|
this._handleRenderError(
|
|
1203
1336
|
commitError instanceof Error ? commitError : new Error(String(commitError)),
|
|
@@ -1234,7 +1367,7 @@ class WebComponentBase extends Base {
|
|
|
1234
1367
|
);
|
|
1235
1368
|
try {
|
|
1236
1369
|
const fallback = this.renderError(error);
|
|
1237
|
-
if (fallback !== undefined) clientRender(fallback, this._renderRoot);
|
|
1370
|
+
if (fallback !== undefined) Signal.subtle.untrack(() => clientRender(fallback, this._renderRoot, { host: this }));
|
|
1238
1371
|
} catch (fallbackError) {
|
|
1239
1372
|
console.error(`[webjs] renderError() also threw:`, fallbackError);
|
|
1240
1373
|
}
|
|
@@ -1247,16 +1380,21 @@ class WebComponentBase extends Base {
|
|
|
1247
1380
|
* @private
|
|
1248
1381
|
*/
|
|
1249
1382
|
_postCommit(changedProperties) {
|
|
1383
|
+
const resolve = this._updateResolve;
|
|
1384
|
+
this._updateResolve = null;
|
|
1250
1385
|
try {
|
|
1251
|
-
|
|
1252
|
-
this.
|
|
1253
|
-
this.
|
|
1254
|
-
|
|
1255
|
-
|
|
1386
|
+
this._tracked('__postWatcher', () => {
|
|
1387
|
+
for (const c of this.__controllers || NO_CONTROLLERS) c.hostUpdated?.();
|
|
1388
|
+
if (!this.__firstRendered) {
|
|
1389
|
+
this.__firstRendered = true;
|
|
1390
|
+
this.firstUpdated(changedProperties);
|
|
1391
|
+
}
|
|
1392
|
+
this.updated(changedProperties);
|
|
1393
|
+
});
|
|
1256
1394
|
} catch (postCommitError) {
|
|
1257
1395
|
console.error(`[webjs] lifecycle hook threw during post-commit phase:`, postCommitError);
|
|
1258
1396
|
} finally {
|
|
1259
|
-
this.
|
|
1397
|
+
resolve?.(!this._scheduled);
|
|
1260
1398
|
}
|
|
1261
1399
|
}
|
|
1262
1400
|
|
|
@@ -1374,7 +1512,7 @@ class WebComponentBase extends Base {
|
|
|
1374
1512
|
* @param {ReactiveController} controller
|
|
1375
1513
|
*/
|
|
1376
1514
|
addController(controller) {
|
|
1377
|
-
this.__controllers.add(controller);
|
|
1515
|
+
(this.__controllers ??= new Set()).add(controller);
|
|
1378
1516
|
if (this._connected && controller.hostConnected) {
|
|
1379
1517
|
controller.hostConnected();
|
|
1380
1518
|
}
|
|
@@ -1393,7 +1531,7 @@ class WebComponentBase extends Base {
|
|
|
1393
1531
|
* @param {ReactiveController} controller
|
|
1394
1532
|
*/
|
|
1395
1533
|
removeController(controller) {
|
|
1396
|
-
this.__controllers.delete(controller);
|
|
1534
|
+
if (this.__controllers) this.__controllers.delete(controller);
|
|
1397
1535
|
}
|
|
1398
1536
|
|
|
1399
1537
|
/**
|
|
@@ -1487,6 +1625,28 @@ export function WebComponent(properties) {
|
|
|
1487
1625
|
}
|
|
1488
1626
|
}
|
|
1489
1627
|
|
|
1628
|
+
/**
|
|
1629
|
+
* Re-assign every own data property of `el` that hides a setter defined on
|
|
1630
|
+
* its class chain below the WebJs base, so the value goes through the
|
|
1631
|
+
* accessor instead of shadowing it.
|
|
1632
|
+
*
|
|
1633
|
+
* @param {any} el
|
|
1634
|
+
*/
|
|
1635
|
+
function liftShadowedAccessors(el) {
|
|
1636
|
+
for (const key of Object.keys(el)) {
|
|
1637
|
+
for (let p = Object.getPrototypeOf(el); p && p !== WebComponentBase.prototype; p = Object.getPrototypeOf(p)) {
|
|
1638
|
+
const d = Object.getOwnPropertyDescriptor(p, key);
|
|
1639
|
+
if (!d) continue;
|
|
1640
|
+
if (d.set) {
|
|
1641
|
+
const value = el[key];
|
|
1642
|
+
delete el[key];
|
|
1643
|
+
el[key] = value;
|
|
1644
|
+
}
|
|
1645
|
+
break;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1490
1650
|
// Ensure static inheritance and instanceof checks work
|
|
1491
1651
|
Object.setPrototypeOf(WebComponent, WebComponentBase);
|
|
1492
1652
|
WebComponent.prototype = WebComponentBase.prototype;
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { tagOf } from '../registry.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Default change detection:
|
|
4
|
+
* Default change detection: Object.is inequality (NaN is stable; signed zero changes).
|
|
5
5
|
* @param {unknown} a
|
|
6
6
|
* @param {unknown} b
|
|
7
7
|
* @returns {boolean}
|
|
8
8
|
*/
|
|
9
9
|
export function defaultHasChanged(a, b) {
|
|
10
|
-
return a
|
|
10
|
+
return !Object.is(a, b);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -108,8 +108,9 @@ export function warnUnserializableReflection(host, propName, attrName, detail) {
|
|
|
108
108
|
* while staying JSDoc-only and no-build.
|
|
109
109
|
*
|
|
110
110
|
* Subclasses declare:
|
|
111
|
-
* -
|
|
112
|
-
*
|
|
111
|
+
* - reactive properties, through the `WebComponent({ ... })` factory:
|
|
112
|
+
* attribute/property declarations with type info, reflection and custom
|
|
113
|
+
* converters (internal state is an instance signal, not a property)
|
|
113
114
|
* - `static styles`: CSSResult or array thereof (only meaningful with
|
|
114
115
|
* `static shadow = true`; light-DOM components inherit global CSS)
|
|
115
116
|
* - `static shadow`: set `true` to opt in to shadow DOM (default: `false`
|
package/src/component.d.ts
CHANGED
|
@@ -85,11 +85,11 @@ interface WebComponentBase extends HTMLElement {
|
|
|
85
85
|
* Schedule a re-render. Optionally record a property change so lifecycle
|
|
86
86
|
* hooks can branch on what changed via `changedProperties`.
|
|
87
87
|
*
|
|
88
|
-
* For instance-local state,
|
|
89
|
-
*
|
|
90
|
-
* `@webjsdev/core`'s `signal()`. The framework's built-in
|
|
91
|
-
*
|
|
92
|
-
*
|
|
88
|
+
* For instance-local state, use a reactive property (`WebComponent({
|
|
89
|
+
* open: prop<boolean>({ state: true }) })`) or a signal from
|
|
90
|
+
* `@webjsdev/core`'s `signal()`. The framework's built-in SignalWatcher
|
|
91
|
+
* wires every signal the update cycle reads to a re-render automatically;
|
|
92
|
+
* explicit `requestUpdate()` is rarely needed.
|
|
93
93
|
*/
|
|
94
94
|
requestUpdate(name?: string, oldValue?: unknown): void;
|
|
95
95
|
/** A Promise that resolves after the next render commit. */
|
|
@@ -210,6 +210,12 @@ export declare function prop<T>(
|
|
|
210
210
|
opts?: Omit<PropertyDeclaration<T>, 'type'>
|
|
211
211
|
): PropertyDeclaration<T>;
|
|
212
212
|
|
|
213
|
+
// No type constructor and no attribute: nothing says what the value is, so it
|
|
214
|
+
// is `unknown` until narrowed (`prop<boolean>({ state: true })`).
|
|
215
|
+
export declare function prop<T = unknown>(
|
|
216
|
+
opts: PropertyDeclaration<T> & ({ state: true } | { attribute: false })
|
|
217
|
+
): PropertyDeclaration<T>;
|
|
218
|
+
|
|
213
219
|
export declare function prop<T = string>(
|
|
214
220
|
opts?: PropertyDeclaration<T>
|
|
215
221
|
): PropertyDeclaration<T>;
|