@webjsdev/core 0.8.0 → 0.9.1
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 +5 -0
- package/dist/webjs-core-browser.js +6 -5
- package/dist/webjs-core-browser.js.map +3 -3
- package/dist/webjs-core-testing.js +20 -19
- package/dist/webjs-core-testing.js.map +3 -3
- package/dist/webjs-core.js +7 -6
- package/dist/webjs-core.js.map +3 -3
- package/index-browser.js +14 -9
- package/index.d.ts +53 -10
- package/index.js +17 -9
- package/package.json +15 -2
- package/src/action-effects-client.d.ts +5 -0
- package/src/action-effects-client.js +84 -0
- package/src/action-seed-client.js +5 -5
- package/src/asset-url.d.ts +18 -0
- package/src/asset-url.js +12 -13
- package/src/async-directive.d.ts +18 -0
- package/src/async-directive.js +88 -0
- package/src/attribute-reader.js +197 -23
- package/src/binding-prefixes.js +0 -12
- package/src/component/lifecycle.js +760 -209
- package/src/component/reactive.js +2 -10
- package/src/component/server-element.js +23 -1
- package/src/component.d.ts +151 -41
- package/src/context.d.ts +94 -18
- package/src/context.js +341 -217
- package/src/converters.d.ts +7 -0
- package/src/converters.js +38 -0
- package/src/csp-nonce.d.ts +18 -0
- package/src/css.d.ts +32 -6
- package/src/css.js +178 -14
- package/src/directive.d.ts +58 -0
- package/src/directive.js +119 -0
- package/src/directives.d.ts +38 -1
- package/src/directives.js +158 -1
- package/src/form-action.d.ts +13 -0
- package/src/form-action.js +139 -4
- package/src/hot.js +23 -0
- package/src/html.d.ts +4 -0
- package/src/html.js +35 -3
- package/src/is-server.d.ts +2 -0
- package/src/is-server.js +18 -0
- package/src/jsx-guard.js +130 -0
- package/src/metadata.d.ts +22 -3
- package/src/nav.d.ts +4 -1
- package/src/nav.js +44 -11
- package/src/optimistic.js +2 -2
- package/src/refresh-header.js +52 -0
- package/src/registry.js +10 -1
- package/src/render-client/adopt-slots.js +8 -3
- package/src/render-client/parts.js +745 -31
- package/src/render-client/reconciler.js +56 -11
- package/src/render-client/template-compiler.js +139 -44
- package/src/render-client.d.ts +5 -1
- package/src/render-server/attrs.js +2 -2
- package/src/render-server/compile.js +34 -0
- package/src/render-server/errors.js +37 -0
- package/src/render-server/expand.js +61 -8
- package/src/render-server/served-attrs.js +1 -2
- package/src/render-server/walk.js +135 -3
- package/src/render-server.d.ts +3 -0
- package/src/render-server.js +2 -0
- package/src/repeat.d.ts +7 -2
- package/src/repeat.js +20 -6
- package/src/router-client/dom-differ.js +17 -0
- package/src/router-client/fetch-apply.js +86 -5
- package/src/router-client/navigator.js +45 -9
- package/src/router-client/prefetch.js +3 -4
- package/src/router-client/root-attrs.js +43 -0
- package/src/router-client/stream.js +7 -0
- package/src/router-client/swap.js +4 -1
- package/src/routes.d.ts +75 -18
- package/src/serialize.d.ts +2 -0
- package/src/serialize.js +35 -4
- package/src/signal.d.ts +7 -0
- package/src/signal.js +26 -6
- package/src/slot/polyfills.js +3 -3
- package/src/slot.js +4 -8
- package/src/static-html.d.ts +22 -0
- package/src/static-html.js +144 -0
- package/src/task.d.ts +63 -20
- package/src/task.js +326 -293
- package/src/testing.d.ts +42 -0
- package/src/webjs-config.d.ts +1 -8
- package/src/websocket-client.d.ts +24 -0
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { render as clientRender } from '../render-client.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
readAttributeValue, resolveAttributeProperty, observedAttributeNames, declaredProperties,
|
|
4
|
+
normalizeDecl, addCreatedProperty, attributeNameFor, declarationsEpoch,
|
|
5
|
+
} from '../attribute-reader.js';
|
|
3
6
|
import { setActiveActionSignal, activeActionSignal } from '../action-abort-client.js';
|
|
4
7
|
import { carriesFunction } from '../form-action.js';
|
|
5
|
-
import {
|
|
8
|
+
import { adoptStyles, finalizeStyles, lightDomStylesMessage } from '../css.js';
|
|
6
9
|
import { register, tagOf } from '../registry.js';
|
|
7
10
|
import { parse as deserializeProp } from '../serialize.js';
|
|
8
11
|
import { Signal } from '../signal.js';
|
|
12
|
+
import { noChange } from '../sentinels.js';
|
|
9
13
|
import {
|
|
10
14
|
captureAuthoredChildren,
|
|
11
15
|
adoptSSRAssignments,
|
|
@@ -72,9 +76,15 @@ const isBrowser = typeof window !== 'undefined' && typeof HTMLElement !== 'undef
|
|
|
72
76
|
*
|
|
73
77
|
* @property {boolean} [reflect]
|
|
74
78
|
* When `true`, writing to the property also sets the corresponding
|
|
75
|
-
* HTML attribute on the element (
|
|
79
|
+
* HTML attribute on the element (Lit's lowercased property name unless
|
|
80
|
+
* `attribute` names one). Useful when you want
|
|
76
81
|
* CSS attribute selectors like `my-el[mode="dark"]` to work.
|
|
77
82
|
*
|
|
83
|
+
* @property {boolean} [useDefault]
|
|
84
|
+
* Lit's `useDefault`: the property's first value is its default. That
|
|
85
|
+
* value is not reflected, and removing the attribute restores it instead
|
|
86
|
+
* of the converter's `null`.
|
|
87
|
+
*
|
|
78
88
|
* @property {boolean} [state]
|
|
79
89
|
* When `true`, the property is *internal-only*: it is NOT exposed as an
|
|
80
90
|
* HTML attribute (excluded from `observedAttributes`) and never reflects.
|
|
@@ -105,15 +115,6 @@ const isBrowser = typeof window !== 'undefined' && typeof HTMLElement !== 'undef
|
|
|
105
115
|
// Base class choice: real HTMLElement on the browser, the shim on the server.
|
|
106
116
|
const Base = isBrowser ? HTMLElement : /** @type {any} */ (ServerElement);
|
|
107
117
|
|
|
108
|
-
/**
|
|
109
|
-
* Marker stamped on the anonymous subclass the `WebComponent({...})` factory
|
|
110
|
-
* produces. It lets `_assertFactoryProperties` tell the framework's own
|
|
111
|
-
* factory-generated `static properties` (allowed) apart from a `static
|
|
112
|
-
* properties` a user wrote by hand in a class body (no longer allowed).
|
|
113
|
-
*/
|
|
114
|
-
const FACTORY_PROPS = Symbol('webjs.factoryProps');
|
|
115
|
-
// Per-class memo so the constructor-time enforcement walk runs once per class.
|
|
116
|
-
const _propsChecked = new WeakSet();
|
|
117
118
|
|
|
118
119
|
/** A settled `updateComplete` shared by every instance until its first update. */
|
|
119
120
|
const RESOLVED_TRUE = Promise.resolve(true);
|
|
@@ -127,53 +128,142 @@ const NO_CONTROLLERS = new Set();
|
|
|
127
128
|
*/
|
|
128
129
|
const _accessorPlans = new WeakMap();
|
|
129
130
|
|
|
131
|
+
/** Classes whose `finalize()` has run. @type {WeakSet<Function>} */
|
|
132
|
+
const FINALIZED = new WeakSet();
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Per class, the memoized `elementProperties` Map: valid while no class's
|
|
136
|
+
* declarations changed (`declarationsEpoch`), and after a change while the
|
|
137
|
+
* `declaredProperties` map it was built from is the same object, which it
|
|
138
|
+
* stays unless this class or a superclass changed (a `createProperty`).
|
|
139
|
+
* @type {WeakMap<Function, { epoch: number, merged: Record<string, any>, map: Map<string, any> }>}
|
|
140
|
+
*/
|
|
141
|
+
const ELEMENT_PROPERTIES = new WeakMap();
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Classes with a subclass whose accessors are installed (an instance of it was
|
|
145
|
+
* constructed). `createProperty` on one of them would reach the subclass's
|
|
146
|
+
* declarations but never its installed accessors, so it is refused, as it is
|
|
147
|
+
* for the class itself.
|
|
148
|
+
* @type {WeakSet<Function>}
|
|
149
|
+
*/
|
|
150
|
+
const HAS_CONSTRUCTED_SUBCLASS = new WeakSet();
|
|
151
|
+
|
|
152
|
+
/** Per class, the memoized `elementStyles` list. @type {WeakMap<Function, any[]>} */
|
|
153
|
+
const ELEMENT_STYLES = new WeakMap();
|
|
154
|
+
|
|
155
|
+
/** Light-DOM classes already told that `static styles` needs shadow DOM. @type {WeakSet<Function>} */
|
|
156
|
+
const WARNED_LIGHT_STYLES = new WeakSet();
|
|
157
|
+
|
|
158
|
+
/** Controller classes whose `hostUpdate()` already threw at SSR. @type {WeakSet<Function>} */
|
|
159
|
+
const REPORTED_SSR_CONTROLLERS = new WeakSet();
|
|
160
|
+
|
|
161
|
+
/** What `getPropertyOptions()` answers for an undeclared name, Lit's default declaration. */
|
|
162
|
+
const DEFAULT_DECLARATION = Object.freeze({ type: String });
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The setters WebJs generated, mapped to the author accessor each one wraps
|
|
166
|
+
* (or `null` for the built-in per-instance storage). A subclass constructed
|
|
167
|
+
* after its parent finds the parent's generated pair first; this map lets it
|
|
168
|
+
* wrap the author's accessor underneath instead of mistaking the wrapper for
|
|
169
|
+
* one.
|
|
170
|
+
* @type {WeakMap<Function, { get?: () => unknown, set?: (v: unknown) => void } | null>}
|
|
171
|
+
*/
|
|
172
|
+
const GENERATED_SETTERS = new WeakMap();
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The nearest author-written accessor for `name` on `Cls`'s prototype chain,
|
|
176
|
+
* below the WebJs base: what Lit's `getPropertyDescriptor` wraps when a class
|
|
177
|
+
* declares a reactive property AND defines its own `get` / `set` for it.
|
|
178
|
+
* Returns null when there is none (the property uses built-in storage).
|
|
179
|
+
* @param {any} Cls
|
|
180
|
+
* @param {string} name
|
|
181
|
+
*/
|
|
182
|
+
function authorAccessor(Cls, name) {
|
|
183
|
+
for (let p = Cls.prototype; p && p !== WebComponentBase.prototype; p = Object.getPrototypeOf(p)) {
|
|
184
|
+
const d = Object.getOwnPropertyDescriptor(p, name);
|
|
185
|
+
if (!d) continue;
|
|
186
|
+
if (d.set && GENERATED_SETTERS.has(d.set)) return GENERATED_SETTERS.get(d.set) ?? null;
|
|
187
|
+
if (d.get || d.set) return { get: d.get, set: d.set };
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
130
193
|
/**
|
|
131
194
|
* Define the getter/setter pair for every declared property on the class's
|
|
132
195
|
* prototype, once, and record the plan the constructor applies per instance
|
|
133
|
-
* (own-value capture and `default`). The
|
|
134
|
-
*
|
|
196
|
+
* (own-value capture and `default`). The descriptor comes from the class's
|
|
197
|
+
* `static getPropertyDescriptor()`, Lit's override point, so a subclass can
|
|
198
|
+
* wrap it.
|
|
135
199
|
* @param {any} Ctor
|
|
136
200
|
* @param {Record<string, unknown>} props
|
|
137
201
|
*/
|
|
138
202
|
function installAccessors(Ctor, props) {
|
|
139
203
|
/** @type {{ propName: string, decl: any }[]} */
|
|
140
204
|
const plan = [];
|
|
141
|
-
for (const
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
});
|
|
205
|
+
for (const propName of Reflect.ownKeys(props)) {
|
|
206
|
+
const decl = props[propName];
|
|
207
|
+
const d = normalizeDecl(decl);
|
|
208
|
+
// `noAccessor` (Lit): the author's own accessor stays and calls
|
|
209
|
+
// `requestUpdate()` itself. A wrapped author accessor owns its storage, so
|
|
210
|
+
// the constructor does not apply a `default` to it (its private fields may
|
|
211
|
+
// not exist yet); a pre-upgrade own value is assigned through the accessor
|
|
212
|
+
// at the first update, as for every declared property.
|
|
213
|
+
const author = authorAccessor(Ctor, propName);
|
|
214
|
+
const builtIn = !d.noAccessor && author === null;
|
|
215
|
+
// Lit's `wrapped`: the property sits on an author accessor, whose initial
|
|
216
|
+
// value the first update records (see `_prepareFirstUpdate`).
|
|
217
|
+
const key = Symbol(String(propName));
|
|
218
|
+
plan.push({ propName, decl: d, builtIn, wrapped: author !== null, key });
|
|
219
|
+
if (d.noAccessor) continue;
|
|
220
|
+
const descriptor = Ctor.getPropertyDescriptor(propName, key, d);
|
|
221
|
+
if (descriptor !== undefined) Object.defineProperty(Ctor.prototype, propName, descriptor);
|
|
168
222
|
}
|
|
169
223
|
_accessorPlans.set(Ctor, plan);
|
|
224
|
+
for (let p = Object.getPrototypeOf(Ctor); p && p !== Function.prototype; p = Object.getPrototypeOf(p)) {
|
|
225
|
+
HAS_CONSTRUCTED_SUBCLASS.add(p);
|
|
226
|
+
}
|
|
170
227
|
return plan;
|
|
171
228
|
}
|
|
172
229
|
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The declarative CLOSED shadow root the parser attached to `el`, through
|
|
233
|
+
* `ElementInternals.shadowRoot`, or null when there is none or internals are
|
|
234
|
+
* unavailable. `attachInternals()` may be called once per element, so the
|
|
235
|
+
* internals object is handed back to the component's own later call instead
|
|
236
|
+
* of that call throwing. A component that already took its internals (in its
|
|
237
|
+
* constructor, the form-associated pattern) makes this throw, and then the
|
|
238
|
+
* caller falls back to attachShadow(), Lit's behaviour.
|
|
239
|
+
* @param {HTMLElement} el
|
|
240
|
+
* @returns {ShadowRoot | null}
|
|
241
|
+
*/
|
|
242
|
+
function declarativeClosedRoot(el) {
|
|
243
|
+
if (typeof el.attachInternals !== 'function') return null;
|
|
244
|
+
let internals;
|
|
245
|
+
try {
|
|
246
|
+
internals = el.attachInternals();
|
|
247
|
+
} catch {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
Object.defineProperty(el, 'attachInternals', { value: () => internals, configurable: true, writable: true });
|
|
251
|
+
return internals.shadowRoot ?? null;
|
|
252
|
+
}
|
|
253
|
+
|
|
173
254
|
class WebComponentBase extends Base {
|
|
174
255
|
/** Whether to use shadow DOM. Default: false (light DOM). @type {boolean} */
|
|
175
256
|
static shadow = false;
|
|
176
257
|
|
|
258
|
+
/**
|
|
259
|
+
* Lit's `shadowRootOptions`: the options `createRenderRoot()` passes to
|
|
260
|
+
* `attachShadow()`, and the attributes the server writes on the Declarative
|
|
261
|
+
* Shadow DOM template (`delegatesFocus`, `clonable`, `serializable`, and a
|
|
262
|
+
* `closed` mode). Read only with `static shadow = true`.
|
|
263
|
+
* @type {ShadowRootInit}
|
|
264
|
+
*/
|
|
265
|
+
static shadowRootOptions = { mode: 'open' };
|
|
266
|
+
|
|
177
267
|
/**
|
|
178
268
|
* Hydration strategy for this component.
|
|
179
269
|
*
|
|
@@ -210,11 +300,40 @@ class WebComponentBase extends Base {
|
|
|
210
300
|
static properties = {};
|
|
211
301
|
|
|
212
302
|
/**
|
|
213
|
-
* Styles to adopt into the shadow root
|
|
214
|
-
*
|
|
303
|
+
* Styles to adopt into the shadow root, Lit's `static styles`: a `css`
|
|
304
|
+
* result, a `CSSStyleSheet`, or nested arrays of them. Shadow DOM only
|
|
305
|
+
* (`static shadow = true`); a light-DOM component has no root to adopt
|
|
306
|
+
* them into, so declaring them there is reported as an error.
|
|
307
|
+
* @type {unknown}
|
|
215
308
|
*/
|
|
216
309
|
static styles = null;
|
|
217
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Lit's `elementStyles`: `static styles` flattened by `finalizeStyles()`,
|
|
313
|
+
* the list `createRenderRoot()` adopts and the server inlines. Memoized per
|
|
314
|
+
* class.
|
|
315
|
+
* @type {(import('../css.js').CSSResult | CSSStyleSheet)[]}
|
|
316
|
+
*/
|
|
317
|
+
static get elementStyles() {
|
|
318
|
+
let list = ELEMENT_STYLES.get(this);
|
|
319
|
+
if (list === undefined) {
|
|
320
|
+
list = this.finalizeStyles(this.styles);
|
|
321
|
+
ELEMENT_STYLES.set(this, list);
|
|
322
|
+
}
|
|
323
|
+
return list;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Lit's `finalizeStyles(styles)`: flatten nested arrays and drop
|
|
328
|
+
* duplicates, keeping each style's LAST position so a later override still
|
|
329
|
+
* wins. Override it to post-process the list.
|
|
330
|
+
* @param {unknown} styles
|
|
331
|
+
* @returns {(import('../css.js').CSSResult | CSSStyleSheet)[]}
|
|
332
|
+
*/
|
|
333
|
+
static finalizeStyles(styles) {
|
|
334
|
+
return finalizeStyles(styles);
|
|
335
|
+
}
|
|
336
|
+
|
|
218
337
|
/**
|
|
219
338
|
* Register this class as a custom element under `tag`.
|
|
220
339
|
*
|
|
@@ -232,6 +351,41 @@ class WebComponentBase extends Base {
|
|
|
232
351
|
register(tag, this);
|
|
233
352
|
}
|
|
234
353
|
|
|
354
|
+
/**
|
|
355
|
+
* The node `render()` commits into, Lit's `renderRoot`: the element itself
|
|
356
|
+
* for a light-DOM component, its shadow root for `static shadow = true`.
|
|
357
|
+
* Undefined until the element first connects, and always undefined at SSR,
|
|
358
|
+
* which has no DOM. Query rendered children through it
|
|
359
|
+
* (`this.renderRoot.querySelector('input')`) so the same line works in
|
|
360
|
+
* either mode.
|
|
361
|
+
* @type {HTMLElement | ShadowRoot | DocumentFragment | undefined}
|
|
362
|
+
*/
|
|
363
|
+
get renderRoot() {
|
|
364
|
+
return this._renderRoot;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
set renderRoot(value) {
|
|
368
|
+
this._renderRoot = value;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* True once the component has completed its first update, Lit's
|
|
373
|
+
* `hasUpdated`. It is still false inside the first `willUpdate()` and
|
|
374
|
+
* `render()`, and true from `firstUpdated()` on.
|
|
375
|
+
* @type {boolean}
|
|
376
|
+
*/
|
|
377
|
+
get hasUpdated() {
|
|
378
|
+
return !!this.__firstRendered;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* True while an update is queued or running, Lit's `isUpdatePending`.
|
|
383
|
+
* @type {boolean}
|
|
384
|
+
*/
|
|
385
|
+
get isUpdatePending() {
|
|
386
|
+
return this._scheduled || this._isUpdating || this._initialUpdatePending;
|
|
387
|
+
}
|
|
388
|
+
|
|
235
389
|
/**
|
|
236
390
|
* Returns the list of attribute names the browser should observe.
|
|
237
391
|
* Properties with `state: true` or `attribute: false` are excluded: they
|
|
@@ -240,19 +394,167 @@ class WebComponentBase extends Base {
|
|
|
240
394
|
* @returns {string[]}
|
|
241
395
|
*/
|
|
242
396
|
static get observedAttributes() {
|
|
397
|
+
this.finalize();
|
|
243
398
|
// Derived from the same walk `resolveAttributeProperty` matches against, so
|
|
244
399
|
// every observed name resolves and nothing else does. A camelCase prop with
|
|
245
|
-
// no custom `attribute` is observed under
|
|
246
|
-
//
|
|
247
|
-
//
|
|
400
|
+
// no custom `attribute` is observed under Lit's lowercased name (what it
|
|
401
|
+
// reflects to, and what the parser turns `quizId=` into) AND its kebab
|
|
402
|
+
// name, kept as a read alias.
|
|
248
403
|
return observedAttributeNames(this);
|
|
249
404
|
}
|
|
250
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Lit's `elementProperties`: every reactive property this class declares,
|
|
408
|
+
* its own and its superclasses', as a Map of name to normalised
|
|
409
|
+
* declaration. Built from the same merge the accessors and the attribute
|
|
410
|
+
* readers use (`static properties`, the factory, `createProperty`).
|
|
411
|
+
* @type {Map<string, PropertyDeclaration>}
|
|
412
|
+
*/
|
|
413
|
+
static get elementProperties() {
|
|
414
|
+
const epoch = declarationsEpoch();
|
|
415
|
+
const hit = ELEMENT_PROPERTIES.get(this);
|
|
416
|
+
if (hit && hit.epoch === epoch) return hit.map;
|
|
417
|
+
const merged = declaredProperties(this);
|
|
418
|
+
if (hit && hit.merged === merged) { hit.epoch = epoch; return hit.map; }
|
|
419
|
+
const map = new Map();
|
|
420
|
+
for (const name of Reflect.ownKeys(merged)) map.set(name, normalizeDecl(merged[name]));
|
|
421
|
+
ELEMENT_PROPERTIES.set(this, { epoch, merged, map });
|
|
422
|
+
return map;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Lit's `getPropertyOptions(name)`: the declaration for `name`, or the
|
|
427
|
+
* default declaration (`{ type: String }`) when it is not declared.
|
|
428
|
+
* @param {string} name
|
|
429
|
+
* @returns {PropertyDeclaration}
|
|
430
|
+
*/
|
|
431
|
+
static getPropertyOptions(name) {
|
|
432
|
+
return this.elementProperties.get(name) ?? DEFAULT_DECLARATION;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Lit's `createProperty(name, options)`: declare a reactive property from
|
|
437
|
+
* code instead of `static properties`. It must run before the first
|
|
438
|
+
* instance is constructed (accessors are installed then, once per class),
|
|
439
|
+
* which is where Lit calls it from, so a later call throws. So does a call
|
|
440
|
+
* on a class one of whose SUBCLASSES has an instance: that subclass's
|
|
441
|
+
* accessors are installed already and could never gain the new property.
|
|
442
|
+
* @param {string} name
|
|
443
|
+
* @param {PropertyDeclaration} [options]
|
|
444
|
+
*/
|
|
445
|
+
static createProperty(name, options = {}) {
|
|
446
|
+
if (_accessorPlans.has(this) || HAS_CONSTRUCTED_SUBCLASS.has(this)) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
`[webjs] ${this.name || 'A component'}.createProperty('${name}') ran after an instance of the class${HAS_CONSTRUCTED_SUBCLASS.has(this) && !_accessorPlans.has(this) ? ', or of a subclass,' : ''} was constructed. ` +
|
|
449
|
+
`Declare the property in \`static properties\` (or the WebComponent({ ... }) factory), or call createProperty ` +
|
|
450
|
+
`at class definition time, before any element is created.`,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
addCreatedProperty(this, name, options);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Lit's `getPropertyDescriptor(name, key, options)`: the accessor pair
|
|
458
|
+
* installed on the prototype for one reactive property. Override it to wrap
|
|
459
|
+
* the default (call `super.getPropertyDescriptor(...)` and delegate to its
|
|
460
|
+
* `get` / `set`); return `undefined` to install no accessor.
|
|
461
|
+
*
|
|
462
|
+
* When the class defines its own `get` / `set` for a declared property, the
|
|
463
|
+
* default wraps it, as Lit's does: reads go to the author's getter, and a
|
|
464
|
+
* write calls the author's setter and then `requestUpdate(name, oldValue,
|
|
465
|
+
* options)`. Otherwise the value is stored on the instance under `key`. Either way
|
|
466
|
+
* `requestUpdate` runs `hasChanged`, records the change, reflects, and
|
|
467
|
+
* schedules the update.
|
|
468
|
+
* @param {string} name
|
|
469
|
+
* @param {symbol} key the instance slot the default accessor stores the value in, as in Lit
|
|
470
|
+
* @param {PropertyDeclaration} options
|
|
471
|
+
* @returns {PropertyDescriptor | undefined}
|
|
472
|
+
*/
|
|
473
|
+
static getPropertyDescriptor(name, key, options) {
|
|
474
|
+
const author = authorAccessor(this, name);
|
|
475
|
+
/** @type {PropertyDescriptor} */
|
|
476
|
+
let descriptor;
|
|
477
|
+
if (author) {
|
|
478
|
+
const { get, set } = author;
|
|
479
|
+
descriptor = {
|
|
480
|
+
configurable: true,
|
|
481
|
+
enumerable: true,
|
|
482
|
+
get() { return get?.call(this); },
|
|
483
|
+
set(value) {
|
|
484
|
+
const oldValue = get?.call(this);
|
|
485
|
+
set?.call(this, value);
|
|
486
|
+
this.requestUpdate(name, oldValue, options);
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
} else {
|
|
490
|
+
descriptor = {
|
|
491
|
+
configurable: true,
|
|
492
|
+
enumerable: true,
|
|
493
|
+
get() { return this[key]; },
|
|
494
|
+
set(value) {
|
|
495
|
+
const oldValue = this[key];
|
|
496
|
+
this[key] = value;
|
|
497
|
+
// requestUpdate runs hasChanged, records the (name, oldValue) entry,
|
|
498
|
+
// reflects, AND schedules a render. During the update phase the
|
|
499
|
+
// scheduler short-circuits and the entry folds into the current
|
|
500
|
+
// cycle's changedProperties.
|
|
501
|
+
this.requestUpdate(name, oldValue, options);
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
GENERATED_SETTERS.set(/** @type {Function} */ (descriptor.set), author);
|
|
506
|
+
return descriptor;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Lit's `finalize()`: prepare the class's metadata once. Idempotent and
|
|
511
|
+
* called for you (from `observedAttributes` and `register()`), so calling it
|
|
512
|
+
* yourself is never required.
|
|
513
|
+
*/
|
|
514
|
+
static finalize() {
|
|
515
|
+
if (FINALIZED.has(this)) return;
|
|
516
|
+
FINALIZED.add(this);
|
|
517
|
+
const parent = Object.getPrototypeOf(this);
|
|
518
|
+
if (parent && typeof parent.finalize === 'function') parent.finalize();
|
|
519
|
+
// Lit creates every `static properties` entry through `createProperty`,
|
|
520
|
+
// so an override of it (a default declaration mixed into each) applies to
|
|
521
|
+
// them too. The base implementation needs no call.
|
|
522
|
+
if (this.createProperty !== WebComponentBase.createProperty && Object.hasOwn(this, 'properties')) {
|
|
523
|
+
const own = this.properties;
|
|
524
|
+
if (own && typeof own === 'object') for (const name of Reflect.ownKeys(own)) this.createProperty(name, normalizeDecl(own[name]));
|
|
525
|
+
}
|
|
526
|
+
void this.elementProperties;
|
|
527
|
+
void this.elementStyles;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Lit's `addInitializer(initializer)`: run `initializer(instance)` in the
|
|
532
|
+
* constructor of every instance of this class and its subclasses. A
|
|
533
|
+
* subclass inherits its parent's initializers; one added to a subclass does
|
|
534
|
+
* not reach the parent.
|
|
535
|
+
* @param {(element: any) => void} initializer
|
|
536
|
+
*/
|
|
537
|
+
static addInitializer(initializer) {
|
|
538
|
+
if (!Object.hasOwn(this, '_initializers')) {
|
|
539
|
+
const inherited = /** @type {any} */ (Object.getPrototypeOf(this))._initializers;
|
|
540
|
+
this._initializers = inherited ? [...inherited] : [];
|
|
541
|
+
}
|
|
542
|
+
this._initializers.push(initializer);
|
|
543
|
+
}
|
|
544
|
+
|
|
251
545
|
constructor() {
|
|
252
546
|
super();
|
|
253
|
-
|
|
547
|
+
/**
|
|
548
|
+
* The node `render()` commits into: the element itself for light DOM, its
|
|
549
|
+
* shadow root for shadow DOM. Undefined until the element connects (and at
|
|
550
|
+
* SSR, where nothing is connected), as in Lit.
|
|
551
|
+
* @type {HTMLElement | ShadowRoot | DocumentFragment | undefined}
|
|
552
|
+
*/
|
|
553
|
+
this._renderRoot = undefined;
|
|
254
554
|
this._scheduled = false;
|
|
255
555
|
this._connected = false;
|
|
556
|
+
/** Pending from construction until the first update runs, as in Lit. */
|
|
557
|
+
this._initialUpdatePending = true;
|
|
256
558
|
|
|
257
559
|
/**
|
|
258
560
|
* Set of attached reactive controllers, created by the first
|
|
@@ -306,43 +608,14 @@ class WebComponentBase extends Base {
|
|
|
306
608
|
*/
|
|
307
609
|
this._isUpdating = false;
|
|
308
610
|
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
// Install reactive property accessors for `static properties` declarations.
|
|
611
|
+
// Install the reactive property accessors. Props come from Lit's
|
|
612
|
+
// `static properties` and from the `WebComponent({ ... })` factory alike,
|
|
613
|
+
// merged down the class chain by `declaredProperties`.
|
|
314
614
|
this._initializeProperties();
|
|
315
|
-
}
|
|
316
615
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
* `extends WebComponent({ … })` factory, which stamps {@link FACTORY_PROPS}
|
|
321
|
-
* on the subclass it generates; a `static properties` written by hand in a
|
|
322
|
-
* class body carries no such marker and is rejected here (issue #598).
|
|
323
|
-
*
|
|
324
|
-
* The walk stops at {@link WebComponentBase} (whose `static properties = {}`
|
|
325
|
-
* default is internal) and is memoized per class so it runs once.
|
|
326
|
-
* @private
|
|
327
|
-
*/
|
|
328
|
-
_assertFactoryProperties() {
|
|
329
|
-
const Ctor = /** @type {any} */ (this.constructor);
|
|
330
|
-
if (_propsChecked.has(Ctor)) return;
|
|
331
|
-
let C = Ctor;
|
|
332
|
-
while (C && C !== WebComponentBase) {
|
|
333
|
-
if (Object.hasOwn(C, 'properties') && !Object.hasOwn(C, FACTORY_PROPS)) {
|
|
334
|
-
const name = C.name || 'a component';
|
|
335
|
-
throw new Error(
|
|
336
|
-
`${name}: \`static properties\` is no longer supported. Declare reactive ` +
|
|
337
|
-
`properties via the factory instead: \`class ${name} extends WebComponent({ ` +
|
|
338
|
-
`count: Number })\`. Use the \`prop()\` helper for options ` +
|
|
339
|
-
`(\`prop(Number, { reflect: true })\`) and set defaults in the ` +
|
|
340
|
-
`constructor after \`super()\`. See https://webjs.dev/docs/components.`,
|
|
341
|
-
);
|
|
342
|
-
}
|
|
343
|
-
C = Object.getPrototypeOf(C);
|
|
344
|
-
}
|
|
345
|
-
_propsChecked.add(Ctor);
|
|
616
|
+
// Lit's `addInitializer` callbacks, after the properties exist.
|
|
617
|
+
const initializers = /** @type {any} */ (this.constructor)._initializers;
|
|
618
|
+
if (initializers) for (const init of initializers) init(this);
|
|
346
619
|
}
|
|
347
620
|
|
|
348
621
|
/**
|
|
@@ -352,17 +625,13 @@ class WebComponentBase extends Base {
|
|
|
352
625
|
* once per class. Per instance this only lifts a value written before the
|
|
353
626
|
* element upgraded and applies the declared `default`s.
|
|
354
627
|
*
|
|
355
|
-
* Called once from the constructor.
|
|
356
|
-
* (`
|
|
628
|
+
* Called once from the constructor. Each value is stored on the instance
|
|
629
|
+
* under a per-property symbol (Lit's `key`), so the accessors never collide
|
|
630
|
+
* with it.
|
|
357
631
|
* @private
|
|
358
632
|
*/
|
|
359
633
|
_initializeProperties() {
|
|
360
634
|
const Ctor = /** @type {any} */ (this.constructor);
|
|
361
|
-
const props = Ctor.properties;
|
|
362
|
-
if (!props || typeof props !== 'object') return;
|
|
363
|
-
|
|
364
|
-
/** @type {Record<string, unknown>} */
|
|
365
|
-
this.__propValues = {};
|
|
366
635
|
|
|
367
636
|
// The accessors live on the PROTOTYPE, installed once per class (#1646).
|
|
368
637
|
// They used to be defined on every instance, two closures and a
|
|
@@ -370,27 +639,32 @@ class WebComponentBase extends Base {
|
|
|
370
639
|
// cost of constructing a component on the SSR path (one instance per
|
|
371
640
|
// rendered element) and a measurable one on upgrade in the browser.
|
|
372
641
|
let plan = _accessorPlans.get(Ctor);
|
|
373
|
-
if (plan === undefined)
|
|
642
|
+
if (plan === undefined) {
|
|
643
|
+
// Defining or registering the class finalizes it already; a class
|
|
644
|
+
// constructed without either is finalized here, before its accessors.
|
|
645
|
+
Ctor.finalize();
|
|
646
|
+
plan = installAccessors(Ctor, declaredProperties(Ctor));
|
|
647
|
+
}
|
|
374
648
|
|
|
375
649
|
for (let i = 0; i < plan.length; i++) {
|
|
376
|
-
const { propName, decl: d } = plan[i];
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
|
|
650
|
+
const { propName, decl: d, builtIn, key } = plan[i];
|
|
651
|
+
// A value set before the element upgraded is an own data property that
|
|
652
|
+
// shadows the prototype accessor. Lit's `__saveInstanceProperties`: take
|
|
653
|
+
// it off now and assign it through the accessor at the first update, so
|
|
654
|
+
// it wins over a default the constructor body writes.
|
|
381
655
|
if (Object.hasOwn(this, propName)) {
|
|
382
|
-
|
|
656
|
+
(this.__instanceProperties ??= new Map()).set(propName, /** @type {any} */ (this)[propName]);
|
|
383
657
|
delete (/** @type {any} */ (this))[propName];
|
|
384
658
|
}
|
|
385
|
-
if (
|
|
386
|
-
|
|
387
|
-
} else if (d.default !== undefined) {
|
|
659
|
+
if (!builtIn) continue;
|
|
660
|
+
if (d.default !== undefined) {
|
|
388
661
|
// Declarative `default` option (lit-parity). A function default is
|
|
389
662
|
// CALLED per instance, so an object / array default is a fresh value
|
|
390
663
|
// per element. Written straight to the backing store; an applied
|
|
391
664
|
// attribute (attributeChangedCallback runs later) overrides it.
|
|
392
|
-
|
|
393
|
-
|
|
665
|
+
const value = typeof d.default === 'function' ? d.default() : d.default;
|
|
666
|
+
/** @type {any} */ (this)[key] = value;
|
|
667
|
+
if (d.useDefault) (this.__defaultValues ??= new Map()).set(propName, value);
|
|
394
668
|
}
|
|
395
669
|
}
|
|
396
670
|
}
|
|
@@ -408,9 +682,10 @@ class WebComponentBase extends Base {
|
|
|
408
682
|
_reflectAttribute(propName, value, decl) {
|
|
409
683
|
// A property with no attribute (`state: true` / `attribute: false`) has
|
|
410
684
|
// nothing to reflect to.
|
|
411
|
-
|
|
412
|
-
//
|
|
413
|
-
const attrName =
|
|
685
|
+
// The attribute name comes from the one rule the readers use, so a prop
|
|
686
|
+
// reflects to exactly the name it observes.
|
|
687
|
+
const attrName = attributeNameFor(propName, decl);
|
|
688
|
+
if (attrName === undefined) return;
|
|
414
689
|
// Guard against re-entrant loops: attributeChangedCallback fires when
|
|
415
690
|
// we call setAttribute, which would call the setter again.
|
|
416
691
|
if (this.__reflectingAttribute) return;
|
|
@@ -569,10 +844,16 @@ class WebComponentBase extends Base {
|
|
|
569
844
|
const n = attrs[i].name;
|
|
570
845
|
if (n.startsWith('data-webjs-prop-')) names.push(n);
|
|
571
846
|
}
|
|
847
|
+
// The last value each binding carried, so a later morph re-reads only the
|
|
848
|
+
// ones the server changed (router-client/dom-differ.js), the way Lit
|
|
849
|
+
// commits a property binding only when its value changed.
|
|
850
|
+
/** @type any */ const self = this;
|
|
851
|
+
if (!self.__webjsPropAttrValues) self.__webjsPropAttrValues = new Map();
|
|
572
852
|
for (const fullName of names) {
|
|
573
853
|
const raw = this.getAttribute(fullName);
|
|
574
854
|
this.removeAttribute(fullName);
|
|
575
855
|
if (raw == null) continue;
|
|
856
|
+
self.__webjsPropAttrValues.set(fullName, raw);
|
|
576
857
|
const propName = camelCase(fullName.slice('data-webjs-prop-'.length));
|
|
577
858
|
try {
|
|
578
859
|
/** @type any */ (this)[propName] = deserializeProp(raw);
|
|
@@ -588,7 +869,7 @@ class WebComponentBase extends Base {
|
|
|
588
869
|
this._connected = true;
|
|
589
870
|
// A value written on the element before its class was defined is an own
|
|
590
871
|
// data property, and it hides any accessor of the same name for good.
|
|
591
|
-
// `_initializeProperties`
|
|
872
|
+
// `_initializeProperties` saves the declared properties in the
|
|
592
873
|
// constructor; an accessor the subclass defines itself (a getter/setter
|
|
593
874
|
// over a private signal, the shape internal state another element assigns
|
|
594
875
|
// takes) gets the same lift here, on the first connect, since its private
|
|
@@ -615,34 +896,94 @@ class WebComponentBase extends Base {
|
|
|
615
896
|
if (Ctor.shadow !== true && !this.hasAttribute('data-wj-host')) {
|
|
616
897
|
this.setAttribute('data-wj-host', '');
|
|
617
898
|
}
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
899
|
+
this._setupRenderRoot();
|
|
900
|
+
if (Ctor.shadow !== true && this._renderRoot === this) {
|
|
901
|
+
// Install native-write interception AFTER capture (capture uses the
|
|
902
|
+
// host's still-native methods), then arm the sensors. Together they make
|
|
903
|
+
// appendChild / insertBefore / removeChild / innerHTML / slot= flips on a
|
|
904
|
+
// mounted light host drive the slot record, restoring full shadow-DOM
|
|
905
|
+
// parity through the standard DOM API. Interception installs once;
|
|
906
|
+
// sensors are armed on every connect and torn down on disconnect.
|
|
907
|
+
if (hasSlotState(this)) {
|
|
908
|
+
installSlotInterception(this);
|
|
909
|
+
installSlotSensors(this);
|
|
622
910
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Notify all controllers that the host is connected.
|
|
914
|
+
for (const c of this.__controllers || NO_CONTROLLERS) {
|
|
915
|
+
if (c.hostConnected) c.hostConnected();
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// First render is deferred to a microtask, matching lit's
|
|
919
|
+
// performUpdate scheduling. The user's connectedCallback override
|
|
920
|
+
// body runs synchronously to completion BEFORE the first render,
|
|
921
|
+
// so subclass setup that needs to inspect attributes / state / etc.
|
|
922
|
+
// observes consistent timing whether SSR-hydrating or first mount.
|
|
923
|
+
// Render-root setup (shadow attachment, light-DOM SSR adoption,
|
|
924
|
+
// controller hostConnected, _connected=true) all stay synchronous
|
|
925
|
+
// above; only the template commit + post-commit slot observers
|
|
926
|
+
// defer. Authoring contract: post-render DOM setup goes in
|
|
927
|
+
// firstUpdated(), NOT connectedCallback().
|
|
928
|
+
//
|
|
929
|
+
// The scheduling matches `_scheduleUpdate`'s wrapper so lifecycle
|
|
930
|
+
// throws are surfaced as console errors instead of bricking the
|
|
931
|
+
// element.
|
|
932
|
+
if (this._updateResolve === null) {
|
|
933
|
+
this._updatePromise = new Promise((resolve) => {
|
|
934
|
+
this._updateResolve = resolve;
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
// Lit's `enableUpdating(requestedUpdate)` hook: called once the element is
|
|
938
|
+
// ready to update, before the first update is queued.
|
|
939
|
+
this.__updatingEnabled = true;
|
|
940
|
+
this.enableUpdating(true);
|
|
941
|
+
if (this._scheduled) return;
|
|
942
|
+
this._scheduled = true;
|
|
943
|
+
// The renderer's slot parts place the record content as part of the
|
|
944
|
+
// commit; native DOM writes on the host (appendChild, slot= flips,
|
|
945
|
+
// innerHTML) drive the record live through the interception + sensors
|
|
946
|
+
// installed on connect. There is no WebJs-specific slot API.
|
|
947
|
+
queueMicrotask(() => this._runScheduledUpdate('initial render'));
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/**
|
|
951
|
+
* Create the render root once (Lit's `createRenderRoot()`) and, for a
|
|
952
|
+
* light-DOM host, settle its slot record: capture the authored children,
|
|
953
|
+
* adopt a server-rendered projection, or sweep on a reconnection. Runs on
|
|
954
|
+
* every connect, and from the first update of an element flushed before it
|
|
955
|
+
* ever connected.
|
|
956
|
+
* @private
|
|
957
|
+
*/
|
|
958
|
+
_setupRenderRoot() {
|
|
959
|
+
const Ctor = /** @type any */ (this.constructor);
|
|
960
|
+
// Lit's `createRenderRoot()`, called once per element. The server renders
|
|
961
|
+
// from `static shadow` alone (it has no DOM to call this against), so a
|
|
962
|
+
// root that contradicts it would hydrate against markup of the other kind:
|
|
963
|
+
// refuse that loudly instead.
|
|
964
|
+
if (this._renderRoot === undefined) {
|
|
965
|
+
const root = this.createRenderRoot();
|
|
966
|
+
const isShadowRoot = typeof ShadowRoot !== 'undefined' && root instanceof ShadowRoot;
|
|
967
|
+
if (isShadowRoot !== (Ctor.shadow === true)) {
|
|
968
|
+
const name = tagOf(Ctor) || this.tagName?.toLowerCase();
|
|
969
|
+
throw new Error(
|
|
970
|
+
isShadowRoot
|
|
971
|
+
? `[webjs] <${name}> createRenderRoot() returned a shadow root, but the class does not declare \`static shadow = true\`. ` +
|
|
972
|
+
`WebJs renders on the server from \`static shadow\`, so add \`static shadow = true\` (with \`static shadowRootOptions\` for the ` +
|
|
973
|
+
`attachShadow options) and drop the override, or call \`super.createRenderRoot()\` from it.`
|
|
974
|
+
: `[webjs] <${name}> declares \`static shadow = true\`, but createRenderRoot() returned something other than a shadow root. ` +
|
|
975
|
+
`Return \`super.createRenderRoot()\` from the override, or remove \`static shadow = true\` to render into light DOM.`,
|
|
976
|
+
);
|
|
635
977
|
}
|
|
636
|
-
|
|
637
|
-
|
|
978
|
+
this._renderRoot = root;
|
|
979
|
+
}
|
|
980
|
+
if (Ctor.shadow !== true && this._renderRoot === this) {
|
|
638
981
|
// Light DOM: static styles is not supported (no shadow root for
|
|
639
|
-
// adoptedStyleSheets)
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
`For light DOM, use global CSS or <style> in render().`
|
|
645
|
-
);
|
|
982
|
+
// adoptedStyleSheets), so they would silently never apply. Say so,
|
|
983
|
+
// loudly, once per class.
|
|
984
|
+
if (Ctor.styles && !WARNED_LIGHT_STYLES.has(Ctor)) {
|
|
985
|
+
WARNED_LIGHT_STYLES.add(Ctor);
|
|
986
|
+
console.error(lightDomStylesMessage(tagOf(Ctor) || this.tagName?.toLowerCase()));
|
|
646
987
|
}
|
|
647
988
|
// Light-DOM slot capture: ONCE per host lifetime (#1015). Three
|
|
648
989
|
// sub-paths, all of which resolve to "the record is populated
|
|
@@ -668,11 +1009,17 @@ class WebComponentBase extends Base {
|
|
|
668
1009
|
// c. First mount, no SSR. Partition authored children into the
|
|
669
1010
|
// record before _performRender wipes the host.
|
|
670
1011
|
//
|
|
671
|
-
// After capture, installSlotInterception + installSlotSensors
|
|
1012
|
+
// After capture, installSlotInterception + installSlotSensors in `_activate` make
|
|
672
1013
|
// native DOM writes on a mounted host live (appendChild, slot= flips,
|
|
673
1014
|
// innerHTML, HTMLSlotElement.assign), so light-DOM slots match shadow DOM
|
|
674
1015
|
// through the standard API.
|
|
675
|
-
if (
|
|
1016
|
+
if (this.render === WebComponentBase.prototype.render) {
|
|
1017
|
+
// A render-less component (no `render()` override). Lit's default
|
|
1018
|
+
// `render()` returns `noChange`, so its children are never touched:
|
|
1019
|
+
// there is no template to project them into, so they are neither
|
|
1020
|
+
// captured nor intercepted, and the commit leaves them in place.
|
|
1021
|
+
this.removeAttribute('data-wj-serialized');
|
|
1022
|
+
} else if (hasSlotState(this)) {
|
|
676
1023
|
// (a) Reconnection. Record already populated. Sweep any direct child
|
|
677
1024
|
// added by a raw bypass write while the host was disconnected (no
|
|
678
1025
|
// sensor was live to catch it), then re-arm the sensors below.
|
|
@@ -699,53 +1046,136 @@ class WebComponentBase extends Base {
|
|
|
699
1046
|
// (c) First mount with author-written children. Capture them.
|
|
700
1047
|
captureAuthoredChildren(this);
|
|
701
1048
|
}
|
|
702
|
-
// Install native-write interception AFTER capture (capture uses the
|
|
703
|
-
// host's still-native methods), then arm the sensors. Together they make
|
|
704
|
-
// appendChild / insertBefore / removeChild / innerHTML / slot= flips on a
|
|
705
|
-
// mounted light host drive the slot record, restoring full shadow-DOM
|
|
706
|
-
// parity through the standard DOM API. Interception installs once;
|
|
707
|
-
// sensors are armed on every connect and torn down on disconnect.
|
|
708
|
-
installSlotInterception(this);
|
|
709
|
-
installSlotSensors(this);
|
|
710
1049
|
}
|
|
1050
|
+
}
|
|
711
1051
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
1052
|
+
/**
|
|
1053
|
+
* The microtask both scheduling sites queue: hand the update to
|
|
1054
|
+
* `scheduleUpdate()` (Lit's override point for WHEN an update runs) and
|
|
1055
|
+
* surface a throw, or the rejection of an async override, as a console
|
|
1056
|
+
* error instead of bricking the element. `_performRender` already resets
|
|
1057
|
+
* `_isUpdating` and settles `updateComplete` in its own `finally` blocks.
|
|
1058
|
+
* @param {string} phase
|
|
1059
|
+
* @private
|
|
1060
|
+
*/
|
|
1061
|
+
_runScheduledUpdate(phase) {
|
|
1062
|
+
try {
|
|
1063
|
+
const r = this.scheduleUpdate();
|
|
1064
|
+
if (r && typeof (/** @type {any} */ (r).then) === 'function') {
|
|
1065
|
+
/** @type {Promise<unknown>} */ (r).then(undefined, (err) => {
|
|
1066
|
+
console.error(`[webjs] lifecycle hook threw during ${phase}:`, err);
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
console.error(`[webjs] lifecycle hook threw during ${phase}:`, err);
|
|
715
1071
|
}
|
|
1072
|
+
}
|
|
716
1073
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1074
|
+
/**
|
|
1075
|
+
* Lit's `scheduleUpdate()`: called from the update microtask to run the
|
|
1076
|
+
* pending update. Override it to change WHEN an update runs, for example to
|
|
1077
|
+
* wait for a frame, and call `super.scheduleUpdate()` (or
|
|
1078
|
+
* `this.performUpdate()`) when it should run. `updateComplete` waits for it.
|
|
1079
|
+
*
|
|
1080
|
+
* ```js
|
|
1081
|
+
* async scheduleUpdate() {
|
|
1082
|
+
* await new Promise((r) => requestAnimationFrame(r));
|
|
1083
|
+
* super.scheduleUpdate();
|
|
1084
|
+
* }
|
|
1085
|
+
* ```
|
|
1086
|
+
*
|
|
1087
|
+
* @returns {void | Promise<unknown>}
|
|
1088
|
+
*/
|
|
1089
|
+
scheduleUpdate() {
|
|
1090
|
+
return this.performUpdate();
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
/**
|
|
1094
|
+
* Lit's `performUpdate()`: run the pending update now, synchronously. Does
|
|
1095
|
+
* nothing when no update is pending, so calling it to flush a queued update
|
|
1096
|
+
* early is safe.
|
|
1097
|
+
* @returns {void}
|
|
1098
|
+
*/
|
|
1099
|
+
performUpdate() {
|
|
1100
|
+
// As in Lit, an element is pending from construction until its first
|
|
1101
|
+
// update, so a flush before it ever connected still updates.
|
|
1102
|
+
if (!this._scheduled && !this._initialUpdatePending) return;
|
|
1103
|
+
if (!this.__firstRendered) this._prepareFirstUpdate();
|
|
1104
|
+
this._scheduled = false;
|
|
1105
|
+
this._initialUpdatePending = false;
|
|
1106
|
+
this._performRender();
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* The first-update half of Lit's `performUpdate`: create the render root
|
|
1111
|
+
* when the element was never connected, assign the values saved from before
|
|
1112
|
+
* the upgrade, and record each author accessor's initial value (Lit's
|
|
1113
|
+
* `wrapped` properties, which no setter reported). Runs while the update is
|
|
1114
|
+
* still marked pending, so the assignments fold into it.
|
|
1115
|
+
* @private
|
|
1116
|
+
*/
|
|
1117
|
+
_prepareFirstUpdate() {
|
|
1118
|
+
if (this._renderRoot === undefined) this._setupRenderRoot();
|
|
1119
|
+
const saved = this.__instanceProperties;
|
|
1120
|
+
if (saved) {
|
|
1121
|
+
this.__instanceProperties = undefined;
|
|
1122
|
+
for (const [name, value] of saved) /** @type {any} */ (this)[name] = value;
|
|
735
1123
|
}
|
|
736
|
-
this.
|
|
737
|
-
|
|
738
|
-
this.
|
|
739
|
-
|
|
740
|
-
this.
|
|
741
|
-
|
|
742
|
-
|
|
1124
|
+
for (const { propName, decl, wrapped } of _accessorPlans.get(this.constructor) || []) {
|
|
1125
|
+
const value = /** @type {any} */ (this)[propName];
|
|
1126
|
+
if (!wrapped || this._changedProperties.has(propName) || value === undefined) continue;
|
|
1127
|
+
if (decl.useDefault && !(this.__defaultValues ??= new Map()).has(propName)) {
|
|
1128
|
+
this.__defaultValues.set(propName, value);
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
this._changedProperties.set(propName, undefined);
|
|
1132
|
+
if (decl.reflect) (this.__reflectPending ??= new Set()).add(propName);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Lit's `enableUpdating(requestedUpdate)` hook, called when the element
|
|
1138
|
+
* connects, right before its first update is queued. The default does
|
|
1139
|
+
* nothing.
|
|
1140
|
+
* @param {boolean} _requestedUpdate
|
|
1141
|
+
*/
|
|
1142
|
+
enableUpdating(_requestedUpdate) {}
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* Lit's `createRenderRoot()`: create the node `render()` commits into. The
|
|
1146
|
+
* default returns the element itself for a light-DOM component, and for
|
|
1147
|
+
* `static shadow = true` the shadow root, attached with
|
|
1148
|
+
* `static shadowRootOptions` (default `{ mode: 'open' }`) unless the server
|
|
1149
|
+
* already rendered one as Declarative Shadow DOM, with `static styles`
|
|
1150
|
+
* adopted into it. Override it for Lit's `createRenderRoot() { return this; }`
|
|
1151
|
+
* (already the default) or to post-process the root; a shadow root is
|
|
1152
|
+
* accepted only with `static shadow = true`, because the server renders
|
|
1153
|
+
* from that flag.
|
|
1154
|
+
* @returns {HTMLElement | ShadowRoot | DocumentFragment}
|
|
1155
|
+
*/
|
|
1156
|
+
createRenderRoot() {
|
|
1157
|
+
const Ctor = /** @type any */ (this.constructor);
|
|
1158
|
+
if (Ctor.shadow !== true) return this;
|
|
1159
|
+
const options = Ctor.shadowRootOptions ?? { mode: 'open' };
|
|
1160
|
+
// A CLOSED declarative root is invisible to `this.shadowRoot`, and
|
|
1161
|
+
// attachShadow() on an element that has one empties it (the spec reuses
|
|
1162
|
+
// the root but removes its children), so the server-rendered markup would
|
|
1163
|
+
// vanish and the first render would build from nothing. Lit has the same
|
|
1164
|
+
// gap; ElementInternals reaches the declarative root, so use it.
|
|
1165
|
+
const existing = this.shadowRoot ?? (options.mode === 'closed' ? declarativeClosedRoot(this) : null);
|
|
1166
|
+
const root = existing ?? /** @type any */ (this).attachShadow(options);
|
|
1167
|
+
const list = Ctor.elementStyles;
|
|
1168
|
+
if (list.length) {
|
|
1169
|
+
// A shadow root that came from Declarative Shadow DOM (SSR) carries the
|
|
1170
|
+
// inline <style> the server emitted. Remove it before switching to
|
|
1171
|
+
// adoptedStyleSheets so the rules are not applied twice.
|
|
1172
|
+
if (existing) {
|
|
1173
|
+
const ssrStyle = existing.querySelector('style');
|
|
1174
|
+
if (ssrStyle) ssrStyle.remove();
|
|
743
1175
|
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
// installed on connect. There is no WebJs-specific slot API.
|
|
748
|
-
});
|
|
1176
|
+
adoptStyles(root, list);
|
|
1177
|
+
}
|
|
1178
|
+
return root;
|
|
749
1179
|
}
|
|
750
1180
|
|
|
751
1181
|
/**
|
|
@@ -824,30 +1254,72 @@ class WebComponentBase extends Base {
|
|
|
824
1254
|
const resolved = resolveAttributeProperty(this.constructor, name);
|
|
825
1255
|
if (resolved === undefined) return;
|
|
826
1256
|
const { propName, def } = resolved;
|
|
1257
|
+
// An accessor that stores its value in the attribute writes it back while
|
|
1258
|
+
// this very read assigns it; Lit's `__reflectingProperty` check ignores
|
|
1259
|
+
// that nested callback instead of recursing.
|
|
1260
|
+
if (this.__attributeToProperty === propName) return;
|
|
827
1261
|
|
|
828
1262
|
// One reader for both sides, in `attribute-reader.js`: this and
|
|
829
1263
|
// `applyAttrsToInstance` in `render-server.js` call the same function, so
|
|
830
1264
|
// precedence and every fallback are shared rather than mirrored (#1340).
|
|
831
1265
|
// It takes an already-decoded value, which the DOM has done here and
|
|
832
1266
|
// `decodeAttrEntities` does at the SSR call site (#1341).
|
|
833
|
-
|
|
1267
|
+
let v = readAttributeValue(def, value, propName);
|
|
1268
|
+
if (v == null && def.useDefault && this.__defaultValues?.has(propName)) v = this.__defaultValues.get(propName);
|
|
834
1269
|
|
|
835
|
-
|
|
1270
|
+
// Assigned unconditionally, as Lit's `_$attributeToProperty` does: the
|
|
1271
|
+
// setter's `hasChanged` decides whether it is a change, so a declared
|
|
1272
|
+
// `hasChanged` that rejects the value schedules no update.
|
|
1273
|
+
this.__attributeToProperty = propName;
|
|
1274
|
+
try {
|
|
836
1275
|
this[propName] = v;
|
|
837
|
-
|
|
1276
|
+
} finally {
|
|
1277
|
+
this.__attributeToProperty = undefined;
|
|
838
1278
|
}
|
|
839
1279
|
}
|
|
840
1280
|
|
|
841
1281
|
/**
|
|
842
|
-
* Schedule a re-render
|
|
843
|
-
*
|
|
1282
|
+
* Schedule a re-render, Lit's `requestUpdate(name, oldValue, options)`.
|
|
1283
|
+
* With a name, the property's `hasChanged` (from `options`, else its
|
|
1284
|
+
* declaration) compares its current value with `oldValue`: an unchanged
|
|
1285
|
+
* value schedules nothing. A change is recorded in `changedProperties`
|
|
1286
|
+
* and reflected when the property declares `reflect: true`. With no
|
|
1287
|
+
* arguments it always schedules an update.
|
|
844
1288
|
*
|
|
845
1289
|
* @param {string} [name] Property name that changed
|
|
846
1290
|
* @param {unknown} [oldValue] Previous value of the property
|
|
1291
|
+
* @param {PropertyDeclaration} [options] The property's declaration
|
|
847
1292
|
*/
|
|
848
|
-
requestUpdate(name, oldValue) {
|
|
849
|
-
if (name !== undefined
|
|
850
|
-
this.
|
|
1293
|
+
requestUpdate(name, oldValue, options) {
|
|
1294
|
+
if (name !== undefined) {
|
|
1295
|
+
const decl = options ?? /** @type {any} */ (this.constructor).getPropertyOptions(name);
|
|
1296
|
+
const hasChanged = decl.hasChanged || defaultHasChanged;
|
|
1297
|
+
const value = /** @type {any} */ (this)[name];
|
|
1298
|
+
// Lit's `useDefault`: the first value written is the default, recorded
|
|
1299
|
+
// and neither counted as a change nor reflected.
|
|
1300
|
+
if (decl.useDefault && !(this.__defaultValues ??= new Map()).has(name)) {
|
|
1301
|
+
this.__defaultValues.set(name, oldValue !== undefined ? oldValue : value);
|
|
1302
|
+
if (oldValue === undefined) { this._scheduleUpdate(); return; }
|
|
1303
|
+
}
|
|
1304
|
+
// Writing the unreflected default back IS a change when the attribute
|
|
1305
|
+
// is absent, since it now reflects (Lit's corner case).
|
|
1306
|
+
const attrName = decl.useDefault && decl.reflect ? attributeNameFor(name, decl) : undefined;
|
|
1307
|
+
const restoresDefault = attrName !== undefined
|
|
1308
|
+
&& Object.is(value, this.__defaultValues?.get(name)) && !this.hasAttribute(attrName);
|
|
1309
|
+
if (!hasChanged(value, oldValue) && !restoresDefault) return;
|
|
1310
|
+
// Before the first update the recorded old value is `undefined`, except
|
|
1311
|
+
// with `useDefault`, as in Lit's `_$changeProperty`.
|
|
1312
|
+
if (!this._changedProperties.has(name)) {
|
|
1313
|
+
this._changedProperties.set(name, this.__firstRendered || decl.useDefault ? oldValue : undefined);
|
|
1314
|
+
}
|
|
1315
|
+
// Reflect to the attribute if requested (_reflectAttribute skips a prop
|
|
1316
|
+
// with no attribute: `state: true` or `attribute: false`). Disconnected,
|
|
1317
|
+
// it waits for the next update. A value just read FROM the attribute is
|
|
1318
|
+
// never written back to it.
|
|
1319
|
+
if (decl.reflect && this.__attributeToProperty !== name) {
|
|
1320
|
+
if (this._connected) this._reflectAttribute(name, value, decl);
|
|
1321
|
+
else (this.__reflectPending ??= new Set()).add(name);
|
|
1322
|
+
}
|
|
851
1323
|
}
|
|
852
1324
|
this._scheduleUpdate();
|
|
853
1325
|
}
|
|
@@ -868,21 +1340,21 @@ class WebComponentBase extends Base {
|
|
|
868
1340
|
});
|
|
869
1341
|
}
|
|
870
1342
|
if (this._isUpdating) return;
|
|
871
|
-
|
|
1343
|
+
// Lit runs updates once updating is enabled at the first connect, connected
|
|
1344
|
+
// or not afterwards, so a disconnected element still updates.
|
|
1345
|
+
if (this._scheduled || !this.__updatingEnabled) return;
|
|
872
1346
|
this._scheduled = true;
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
}
|
|
885
|
-
});
|
|
1347
|
+
// _performRender wraps the update phase in try/finally, so a throw from
|
|
1348
|
+
// shouldUpdate / willUpdate / hostUpdate / hostUpdated / firstUpdated /
|
|
1349
|
+
// updated leaves the component in a good state; the wrapper only
|
|
1350
|
+
// surfaces it.
|
|
1351
|
+
// Requested while a finished cycle is still settling (from `updated()`, or
|
|
1352
|
+
// after `super.update()`): run once that cycle's `updateComplete` has
|
|
1353
|
+
// resolved, after whoever was already awaiting it, as Lit's
|
|
1354
|
+
// `__enqueueUpdate` awaits the previous promise. Otherwise next microtask.
|
|
1355
|
+
const settling = this.__settlingPromise;
|
|
1356
|
+
if (settling) settling.then(() => this._runScheduledUpdate('update cycle'));
|
|
1357
|
+
else queueMicrotask(() => this._runScheduledUpdate('update cycle'));
|
|
886
1358
|
}
|
|
887
1359
|
|
|
888
1360
|
/**
|
|
@@ -1039,7 +1511,8 @@ class WebComponentBase extends Base {
|
|
|
1039
1511
|
// Consume this phase's snapshot even when shouldUpdate skips it. For an
|
|
1040
1512
|
// async render, changes during the fetch start a new cycle whose token
|
|
1041
1513
|
// supersedes the in-flight commit.
|
|
1042
|
-
|
|
1514
|
+
// `update()` already swapped it when it committed (see there).
|
|
1515
|
+
if (this._changedProperties === changedProperties) this._changedProperties = new Map();
|
|
1043
1516
|
}
|
|
1044
1517
|
|
|
1045
1518
|
// --- Async render: finish the cycle when the pending commit settles. ---
|
|
@@ -1150,8 +1623,17 @@ class WebComponentBase extends Base {
|
|
|
1150
1623
|
this._isUpdating = true;
|
|
1151
1624
|
try {
|
|
1152
1625
|
this.willUpdate(changedProperties);
|
|
1626
|
+
// Each controller is isolated: a `hostUpdate()` that reaches for the
|
|
1627
|
+
// browser (a third-party Lit controller written for the client) is
|
|
1628
|
+
// reported once per controller class and skipped, so the component
|
|
1629
|
+
// still renders its first paint instead of an error state.
|
|
1153
1630
|
for (const c of this.__controllers || NO_CONTROLLERS) {
|
|
1154
|
-
if (c.hostUpdate)
|
|
1631
|
+
if (!c.hostUpdate) continue;
|
|
1632
|
+
try {
|
|
1633
|
+
c.hostUpdate();
|
|
1634
|
+
} catch (err) {
|
|
1635
|
+
reportSsrControllerError(this, c, err);
|
|
1636
|
+
}
|
|
1155
1637
|
}
|
|
1156
1638
|
this._reflectDeclaredAttributes();
|
|
1157
1639
|
} finally {
|
|
@@ -1177,10 +1659,16 @@ class WebComponentBase extends Base {
|
|
|
1177
1659
|
* @private
|
|
1178
1660
|
*/
|
|
1179
1661
|
_reflectDeclaredAttributes() {
|
|
1180
|
-
const props =
|
|
1181
|
-
for (const name of
|
|
1662
|
+
const props = declaredProperties(this.constructor);
|
|
1663
|
+
for (const name of Reflect.ownKeys(props)) {
|
|
1182
1664
|
const decl = props[name];
|
|
1183
1665
|
if (!decl || !decl.reflect) continue;
|
|
1666
|
+
// A property never given a value has nothing to reflect: Lit reflects a
|
|
1667
|
+
// property only once it changes from `undefined`.
|
|
1668
|
+
if (this[name] === undefined) continue;
|
|
1669
|
+
// A `useDefault` value is not reflected while it is the default, and a
|
|
1670
|
+
// wrapped accessor's default is only recorded at the first update.
|
|
1671
|
+
if (decl.useDefault && (!this.__defaultValues?.has(name) || Object.is(this[name], this.__defaultValues.get(name)))) continue;
|
|
1184
1672
|
this._reflectAttribute(name, this[name], decl);
|
|
1185
1673
|
}
|
|
1186
1674
|
}
|
|
@@ -1247,7 +1735,7 @@ class WebComponentBase extends Base {
|
|
|
1247
1735
|
const pendingReflection = this.__reflectPending;
|
|
1248
1736
|
this.__reflectPending = undefined;
|
|
1249
1737
|
for (const name of pendingReflection || []) {
|
|
1250
|
-
const def = this.constructor
|
|
1738
|
+
const def = declaredProperties(this.constructor)[name];
|
|
1251
1739
|
this._reflectAttribute(name, this[name], def);
|
|
1252
1740
|
}
|
|
1253
1741
|
// The signal reads render() makes are tracked by the watcher
|
|
@@ -1279,6 +1767,10 @@ class WebComponentBase extends Base {
|
|
|
1279
1767
|
if (tpl && typeof (/** @type any */ (tpl).then) === 'function') {
|
|
1280
1768
|
return this._commitAsync(/** @type {Promise<unknown>} */ (tpl));
|
|
1281
1769
|
}
|
|
1770
|
+
// Marked before the commit, as LitElement calls `super.update()` between
|
|
1771
|
+
// `render()` and committing: a property a child sets while the template
|
|
1772
|
+
// commits (an event it dispatches from connectedCallback) is a new update.
|
|
1773
|
+
this._markUpdated();
|
|
1282
1774
|
// The commit is untracked: a signal read while the DOM is patched (a
|
|
1283
1775
|
// `watch()` part, a child's synchronous work) belongs to whatever read
|
|
1284
1776
|
// it, not to this component's next render.
|
|
@@ -1286,6 +1778,22 @@ class WebComponentBase extends Base {
|
|
|
1286
1778
|
return undefined;
|
|
1287
1779
|
}
|
|
1288
1780
|
|
|
1781
|
+
/**
|
|
1782
|
+
* Lit's `__markUpdated()`, the last step of the base `update()` once its
|
|
1783
|
+
* commit is done: the update is no longer pending, so a property set after
|
|
1784
|
+
* `super.update()` returns (in an `update()` override, or in `updated()`)
|
|
1785
|
+
* starts the next cycle, with its own `updateComplete`, instead of folding
|
|
1786
|
+
* into this one. This cycle's resolver is kept for `_postCommit`.
|
|
1787
|
+
* @private
|
|
1788
|
+
*/
|
|
1789
|
+
_markUpdated() {
|
|
1790
|
+
this._isUpdating = false;
|
|
1791
|
+
this._changedProperties = new Map();
|
|
1792
|
+
this.__cycleResolve = this._updateResolve;
|
|
1793
|
+
this.__settlingPromise = this._updatePromise;
|
|
1794
|
+
this._updateResolve = null;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1289
1797
|
/**
|
|
1290
1798
|
* Commit a promise-returning render() with stale-while-revalidate
|
|
1291
1799
|
* semantics (#469). The current DOM stays untouched until the template
|
|
@@ -1380,8 +1888,18 @@ class WebComponentBase extends Base {
|
|
|
1380
1888
|
* @private
|
|
1381
1889
|
*/
|
|
1382
1890
|
_postCommit(changedProperties) {
|
|
1383
|
-
|
|
1384
|
-
|
|
1891
|
+
// The resolver `update()` set aside when it marked the update done, or
|
|
1892
|
+
// else the pending one. A cycle `update()` scheduled after marking has its
|
|
1893
|
+
// own promise in `_updateResolve`, which this one must leave alone.
|
|
1894
|
+
let resolve;
|
|
1895
|
+
if (this.__cycleResolve !== undefined) {
|
|
1896
|
+
resolve = this.__cycleResolve;
|
|
1897
|
+
this.__cycleResolve = undefined;
|
|
1898
|
+
} else {
|
|
1899
|
+
resolve = this._updateResolve;
|
|
1900
|
+
this._updateResolve = null;
|
|
1901
|
+
this.__settlingPromise = this._updatePromise;
|
|
1902
|
+
}
|
|
1385
1903
|
try {
|
|
1386
1904
|
this._tracked('__postWatcher', () => {
|
|
1387
1905
|
for (const c of this.__controllers || NO_CONTROLLERS) c.hostUpdated?.();
|
|
@@ -1394,6 +1912,7 @@ class WebComponentBase extends Base {
|
|
|
1394
1912
|
} catch (postCommitError) {
|
|
1395
1913
|
console.error(`[webjs] lifecycle hook threw during post-commit phase:`, postCommitError);
|
|
1396
1914
|
} finally {
|
|
1915
|
+
this.__settlingPromise = undefined;
|
|
1397
1916
|
resolve?.(!this._scheduled);
|
|
1398
1917
|
}
|
|
1399
1918
|
}
|
|
@@ -1433,7 +1952,7 @@ class WebComponentBase extends Base {
|
|
|
1433
1952
|
*
|
|
1434
1953
|
* ```js
|
|
1435
1954
|
* firstUpdated(changedProperties) {
|
|
1436
|
-
* this.
|
|
1955
|
+
* this.renderRoot.querySelector('input')?.focus();
|
|
1437
1956
|
* }
|
|
1438
1957
|
* ```
|
|
1439
1958
|
*
|
|
@@ -1477,6 +1996,14 @@ class WebComponentBase extends Base {
|
|
|
1477
1996
|
* @returns {Promise<boolean>}
|
|
1478
1997
|
*/
|
|
1479
1998
|
getUpdateComplete() {
|
|
1999
|
+
// As in Lit, the promise read before the first update settles with that
|
|
2000
|
+
// update, not at once. Created on first read, so an element nothing awaits
|
|
2001
|
+
// (every SSR instance) never allocates one; the server never updates.
|
|
2002
|
+
if (isBrowser && this._initialUpdatePending && this._updateResolve === null) {
|
|
2003
|
+
this._updatePromise = new Promise((resolve) => {
|
|
2004
|
+
this._updateResolve = resolve;
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
1480
2007
|
return this._updatePromise;
|
|
1481
2008
|
}
|
|
1482
2009
|
|
|
@@ -1535,11 +2062,13 @@ class WebComponentBase extends Base {
|
|
|
1535
2062
|
}
|
|
1536
2063
|
|
|
1537
2064
|
/**
|
|
1538
|
-
* Override in subclasses to return a TemplateResult.
|
|
2065
|
+
* Override in subclasses to return a TemplateResult. The default returns
|
|
2066
|
+
* `noChange`, as Lit's does, so a component with no `render()` leaves its
|
|
2067
|
+
* children exactly as they are, on the server and in the browser.
|
|
1539
2068
|
* @returns {unknown}
|
|
1540
2069
|
*/
|
|
1541
2070
|
render() {
|
|
1542
|
-
return
|
|
2071
|
+
return noChange;
|
|
1543
2072
|
}
|
|
1544
2073
|
|
|
1545
2074
|
/**
|
|
@@ -1597,10 +2126,33 @@ class WebComponentBase extends Base {
|
|
|
1597
2126
|
}
|
|
1598
2127
|
}
|
|
1599
2128
|
|
|
1600
|
-
/**
|
|
1601
|
-
|
|
1602
|
-
|
|
2129
|
+
/**
|
|
2130
|
+
* Report a controller whose `hostUpdate()` threw during SSR, once per
|
|
2131
|
+
* controller class.
|
|
2132
|
+
* @param {any} host
|
|
2133
|
+
* @param {any} controller
|
|
2134
|
+
* @param {unknown} err
|
|
2135
|
+
*/
|
|
2136
|
+
function reportSsrControllerError(host, controller, err) {
|
|
2137
|
+
const Cls = controller && controller.constructor;
|
|
2138
|
+
if (Cls && REPORTED_SSR_CONTROLLERS.has(Cls)) return;
|
|
2139
|
+
if (Cls) REPORTED_SSR_CONTROLLERS.add(Cls);
|
|
2140
|
+
const tag = host.__ssrTag || tagOf(host.constructor) || 'a component';
|
|
2141
|
+
console.error(
|
|
2142
|
+
`[webjs] ${Cls && Cls.name ? Cls.name : 'A controller'}.hostUpdate() threw during SSR of <${tag}>, so it was skipped ` +
|
|
2143
|
+
`and the component rendered without it. Controllers' hostUpdate() runs on the server too; guard browser-only ` +
|
|
2144
|
+
`work with \`if (isServer) return;\` (isServer from '@webjsdev/core'), or move it to hostConnected() or hostUpdated(), ` +
|
|
2145
|
+
`which never run at SSR.`,
|
|
2146
|
+
err,
|
|
2147
|
+
);
|
|
1603
2148
|
}
|
|
2149
|
+
|
|
2150
|
+
/**
|
|
2151
|
+
* The error a light-DOM component with `static styles` gets, on the server
|
|
2152
|
+
* (dev) and in the browser.
|
|
2153
|
+
* @param {string | undefined} tag
|
|
2154
|
+
* @returns {string}
|
|
2155
|
+
*/
|
|
1604
2156
|
/** @param {string} s */
|
|
1605
2157
|
function camelCase(s) {
|
|
1606
2158
|
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
@@ -1620,7 +2172,6 @@ export function WebComponent(properties) {
|
|
|
1620
2172
|
} else {
|
|
1621
2173
|
return class extends WebComponentBase {
|
|
1622
2174
|
static properties = properties;
|
|
1623
|
-
static [FACTORY_PROPS] = true;
|
|
1624
2175
|
};
|
|
1625
2176
|
}
|
|
1626
2177
|
}
|