@webjsdev/core 0.7.65 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +22 -21
- 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 +18 -9
- package/index.d.ts +26 -10
- package/index.js +21 -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.js +12 -13
- package/src/async-directive.d.ts +18 -0
- package/src/async-directive.js +88 -0
- package/src/attribute-directives.d.ts +6 -0
- package/src/attribute-directives.js +49 -0
- package/src/attribute-reader.js +213 -25
- package/src/binding-prefixes.js +10 -53
- package/src/component/lifecycle.js +921 -252
- package/src/component/reactive.js +7 -14
- package/src/component/server-element.js +23 -1
- package/src/component.d.ts +160 -44
- 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/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 +40 -1
- package/src/directives.js +160 -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 +8 -0
- package/src/html.js +55 -2
- 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 +250 -0
- package/src/render-client/parts.js +790 -42
- package/src/render-client/reconciler.js +84 -23
- package/src/render-client/template-compiler.js +152 -43
- package/src/render-client.d.ts +6 -1
- package/src/render-server/attrs.js +2 -2
- package/src/render-server/compile.js +50 -2
- package/src/render-server/errors.js +37 -0
- package/src/render-server/expand.js +100 -20
- package/src/render-server/served-attrs.js +78 -0
- package/src/render-server/walk.js +177 -5
- 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/anchors.js +3 -1
- package/src/router-client/boundaries.js +15 -1
- package/src/router-client/constants.js +30 -0
- package/src/router-client/dom-differ.js +17 -0
- package/src/router-client/fetch-apply.js +107 -5
- package/src/router-client/frames.js +6 -3
- package/src/router-client/head-merge.js +58 -10
- package/src/router-client/navigator.js +49 -9
- package/src/router-client/owned-styles.js +372 -0
- package/src/router-client/prefetch.js +6 -4
- package/src/router-client/root-attrs.js +43 -0
- package/src/router-client/stream.js +7 -0
- package/src/router-client/stylesheets.js +191 -0
- package/src/router-client/swap.js +53 -10
- package/src/routes.d.ts +75 -18
- package/src/sentinels.d.ts +2 -0
- package/src/sentinels.js +4 -0
- 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/webjs-config.d.ts +2 -9
|
@@ -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.
|
|
@@ -84,7 +94,7 @@ const isBrowser = typeof window !== 'undefined' && typeof HTMLElement !== 'undef
|
|
|
84
94
|
* @property {(newValue: unknown, oldValue: unknown) => boolean} [hasChanged]
|
|
85
95
|
* Custom dirty-check function. Called by the generated setter before
|
|
86
96
|
* scheduling an update. Return `true` to trigger a re-render, `false`
|
|
87
|
-
* to skip. Default:
|
|
97
|
+
* to skip. Default: `(a, b) => !Object.is(a, b)`.
|
|
88
98
|
*
|
|
89
99
|
* Note: this fires on the FIRST assignment too, with `oldValue` set
|
|
90
100
|
* to `undefined`. Numeric comparators that subtract against undefined
|
|
@@ -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,48 +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
|
-
// render. During the update phase the scheduler short-circuits and
|
|
159
|
-
// the entry folds into the current cycle's changedProperties.
|
|
160
|
-
this.requestUpdate(propName, oldVal);
|
|
161
|
-
},
|
|
162
|
-
});
|
|
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);
|
|
163
222
|
}
|
|
164
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
|
+
}
|
|
165
227
|
return plan;
|
|
166
228
|
}
|
|
167
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
|
+
|
|
168
254
|
class WebComponentBase extends Base {
|
|
169
255
|
/** Whether to use shadow DOM. Default: false (light DOM). @type {boolean} */
|
|
170
256
|
static shadow = false;
|
|
171
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
|
+
|
|
172
267
|
/**
|
|
173
268
|
* Hydration strategy for this component.
|
|
174
269
|
*
|
|
@@ -196,19 +291,49 @@ class WebComponentBase extends Base {
|
|
|
196
291
|
* that trigger re-renders on change, coerce attribute values by type,
|
|
197
292
|
* and optionally reflect back to attributes.
|
|
198
293
|
*
|
|
199
|
-
* Properties with `state: true`
|
|
200
|
-
*
|
|
294
|
+
* Properties with `state: true` (internal state) or `attribute: false` (a
|
|
295
|
+
* property-only input) are excluded from `observedAttributes` and never
|
|
296
|
+
* reflect.
|
|
201
297
|
*
|
|
202
298
|
* @type {Record<string, PropertyDeclaration>}
|
|
203
299
|
*/
|
|
204
300
|
static properties = {};
|
|
205
301
|
|
|
206
302
|
/**
|
|
207
|
-
* Styles to adopt into the shadow root
|
|
208
|
-
*
|
|
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}
|
|
209
308
|
*/
|
|
210
309
|
static styles = null;
|
|
211
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
|
+
|
|
212
337
|
/**
|
|
213
338
|
* Register this class as a custom element under `tag`.
|
|
214
339
|
*
|
|
@@ -226,27 +351,210 @@ class WebComponentBase extends Base {
|
|
|
226
351
|
register(tag, this);
|
|
227
352
|
}
|
|
228
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
|
+
|
|
229
389
|
/**
|
|
230
390
|
* Returns the list of attribute names the browser should observe.
|
|
231
|
-
* Properties with `state: true` are excluded: they
|
|
232
|
-
*
|
|
391
|
+
* Properties with `state: true` or `attribute: false` are excluded: they
|
|
392
|
+
* do not correspond to any HTML attribute.
|
|
233
393
|
*
|
|
234
394
|
* @returns {string[]}
|
|
235
395
|
*/
|
|
236
396
|
static get observedAttributes() {
|
|
397
|
+
this.finalize();
|
|
237
398
|
// Derived from the same walk `resolveAttributeProperty` matches against, so
|
|
238
399
|
// every observed name resolves and nothing else does. A camelCase prop with
|
|
239
|
-
// no custom `attribute` is observed under
|
|
240
|
-
//
|
|
241
|
-
//
|
|
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.
|
|
242
403
|
return observedAttributeNames(this);
|
|
243
404
|
}
|
|
244
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
|
+
|
|
245
545
|
constructor() {
|
|
246
546
|
super();
|
|
247
|
-
|
|
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;
|
|
248
554
|
this._scheduled = false;
|
|
249
555
|
this._connected = false;
|
|
556
|
+
/** Pending from construction until the first update runs, as in Lit. */
|
|
557
|
+
this._initialUpdatePending = true;
|
|
250
558
|
|
|
251
559
|
/**
|
|
252
560
|
* Set of attached reactive controllers, created by the first
|
|
@@ -300,43 +608,14 @@ class WebComponentBase extends Base {
|
|
|
300
608
|
*/
|
|
301
609
|
this._isUpdating = false;
|
|
302
610
|
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
// 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`.
|
|
308
614
|
this._initializeProperties();
|
|
309
|
-
}
|
|
310
615
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
* `extends WebComponent({ … })` factory, which stamps {@link FACTORY_PROPS}
|
|
315
|
-
* on the subclass it generates; a `static properties` written by hand in a
|
|
316
|
-
* class body carries no such marker and is rejected here (issue #598).
|
|
317
|
-
*
|
|
318
|
-
* The walk stops at {@link WebComponentBase} (whose `static properties = {}`
|
|
319
|
-
* default is internal) and is memoized per class so it runs once.
|
|
320
|
-
* @private
|
|
321
|
-
*/
|
|
322
|
-
_assertFactoryProperties() {
|
|
323
|
-
const Ctor = /** @type {any} */ (this.constructor);
|
|
324
|
-
if (_propsChecked.has(Ctor)) return;
|
|
325
|
-
let C = Ctor;
|
|
326
|
-
while (C && C !== WebComponentBase) {
|
|
327
|
-
if (Object.hasOwn(C, 'properties') && !Object.hasOwn(C, FACTORY_PROPS)) {
|
|
328
|
-
const name = C.name || 'a component';
|
|
329
|
-
throw new Error(
|
|
330
|
-
`${name}: \`static properties\` is no longer supported. Declare reactive ` +
|
|
331
|
-
`properties via the factory instead: \`class ${name} extends WebComponent({ ` +
|
|
332
|
-
`count: Number })\`. Use the \`prop()\` helper for options ` +
|
|
333
|
-
`(\`prop(Number, { reflect: true })\`) and set defaults in the ` +
|
|
334
|
-
`constructor after \`super()\`. See https://webjs.dev/docs/components.`,
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
C = Object.getPrototypeOf(C);
|
|
338
|
-
}
|
|
339
|
-
_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);
|
|
340
619
|
}
|
|
341
620
|
|
|
342
621
|
/**
|
|
@@ -346,17 +625,13 @@ class WebComponentBase extends Base {
|
|
|
346
625
|
* once per class. Per instance this only lifts a value written before the
|
|
347
626
|
* element upgraded and applies the declared `default`s.
|
|
348
627
|
*
|
|
349
|
-
* Called once from the constructor.
|
|
350
|
-
* (`
|
|
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.
|
|
351
631
|
* @private
|
|
352
632
|
*/
|
|
353
633
|
_initializeProperties() {
|
|
354
634
|
const Ctor = /** @type {any} */ (this.constructor);
|
|
355
|
-
const props = Ctor.properties;
|
|
356
|
-
if (!props || typeof props !== 'object') return;
|
|
357
|
-
|
|
358
|
-
/** @type {Record<string, unknown>} */
|
|
359
|
-
this.__propValues = {};
|
|
360
635
|
|
|
361
636
|
// The accessors live on the PROTOTYPE, installed once per class (#1646).
|
|
362
637
|
// They used to be defined on every instance, two closures and a
|
|
@@ -364,27 +639,32 @@ class WebComponentBase extends Base {
|
|
|
364
639
|
// cost of constructing a component on the SSR path (one instance per
|
|
365
640
|
// rendered element) and a measurable one on upgrade in the browser.
|
|
366
641
|
let plan = _accessorPlans.get(Ctor);
|
|
367
|
-
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
|
+
}
|
|
368
648
|
|
|
369
649
|
for (let i = 0; i < plan.length; i++) {
|
|
370
|
-
const { propName, decl: d } = plan[i];
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
|
|
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.
|
|
375
655
|
if (Object.hasOwn(this, propName)) {
|
|
376
|
-
|
|
656
|
+
(this.__instanceProperties ??= new Map()).set(propName, /** @type {any} */ (this)[propName]);
|
|
377
657
|
delete (/** @type {any} */ (this))[propName];
|
|
378
658
|
}
|
|
379
|
-
if (
|
|
380
|
-
|
|
381
|
-
} else if (d.default !== undefined) {
|
|
659
|
+
if (!builtIn) continue;
|
|
660
|
+
if (d.default !== undefined) {
|
|
382
661
|
// Declarative `default` option (lit-parity). A function default is
|
|
383
662
|
// CALLED per instance, so an object / array default is a fresh value
|
|
384
663
|
// per element. Written straight to the backing store; an applied
|
|
385
664
|
// attribute (attributeChangedCallback runs later) overrides it.
|
|
386
|
-
|
|
387
|
-
|
|
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);
|
|
388
668
|
}
|
|
389
669
|
}
|
|
390
670
|
}
|
|
@@ -400,8 +680,12 @@ class WebComponentBase extends Base {
|
|
|
400
680
|
* @private
|
|
401
681
|
*/
|
|
402
682
|
_reflectAttribute(propName, value, decl) {
|
|
403
|
-
// A
|
|
404
|
-
|
|
683
|
+
// A property with no attribute (`state: true` / `attribute: false`) has
|
|
684
|
+
// nothing to reflect to.
|
|
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;
|
|
405
689
|
// Guard against re-entrant loops: attributeChangedCallback fires when
|
|
406
690
|
// we call setAttribute, which would call the setter again.
|
|
407
691
|
if (this.__reflectingAttribute) return;
|
|
@@ -560,10 +844,16 @@ class WebComponentBase extends Base {
|
|
|
560
844
|
const n = attrs[i].name;
|
|
561
845
|
if (n.startsWith('data-webjs-prop-')) names.push(n);
|
|
562
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();
|
|
563
852
|
for (const fullName of names) {
|
|
564
853
|
const raw = this.getAttribute(fullName);
|
|
565
854
|
this.removeAttribute(fullName);
|
|
566
855
|
if (raw == null) continue;
|
|
856
|
+
self.__webjsPropAttrValues.set(fullName, raw);
|
|
567
857
|
const propName = camelCase(fullName.slice('data-webjs-prop-'.length));
|
|
568
858
|
try {
|
|
569
859
|
/** @type any */ (this)[propName] = deserializeProp(raw);
|
|
@@ -577,6 +867,17 @@ class WebComponentBase extends Base {
|
|
|
577
867
|
|
|
578
868
|
_activate() {
|
|
579
869
|
this._connected = true;
|
|
870
|
+
// A value written on the element before its class was defined is an own
|
|
871
|
+
// data property, and it hides any accessor of the same name for good.
|
|
872
|
+
// `_initializeProperties` saves the declared properties in the
|
|
873
|
+
// constructor; an accessor the subclass defines itself (a getter/setter
|
|
874
|
+
// over a private signal, the shape internal state another element assigns
|
|
875
|
+
// takes) gets the same lift here, on the first connect, since its private
|
|
876
|
+
// field does not exist yet while the base constructor runs.
|
|
877
|
+
if (!this.__accessorsLifted) {
|
|
878
|
+
this.__accessorsLifted = true;
|
|
879
|
+
liftShadowedAccessors(this);
|
|
880
|
+
}
|
|
580
881
|
// Reflect declared reflect:true properties from their current value now
|
|
581
882
|
// that the element is connected. Constructor / willUpdate defaults were
|
|
582
883
|
// set while disconnected (the setter skips reflection then), so this is
|
|
@@ -595,34 +896,94 @@ class WebComponentBase extends Base {
|
|
|
595
896
|
if (Ctor.shadow !== true && !this.hasAttribute('data-wj-host')) {
|
|
596
897
|
this.setAttribute('data-wj-host', '');
|
|
597
898
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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);
|
|
602
910
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
+
);
|
|
615
977
|
}
|
|
616
|
-
|
|
617
|
-
|
|
978
|
+
this._renderRoot = root;
|
|
979
|
+
}
|
|
980
|
+
if (Ctor.shadow !== true && this._renderRoot === this) {
|
|
618
981
|
// Light DOM: static styles is not supported (no shadow root for
|
|
619
|
-
// adoptedStyleSheets)
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
`For light DOM, use global CSS or <style> in render().`
|
|
625
|
-
);
|
|
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()));
|
|
626
987
|
}
|
|
627
988
|
// Light-DOM slot capture: ONCE per host lifetime (#1015). Three
|
|
628
989
|
// sub-paths, all of which resolve to "the record is populated
|
|
@@ -648,11 +1009,17 @@ class WebComponentBase extends Base {
|
|
|
648
1009
|
// c. First mount, no SSR. Partition authored children into the
|
|
649
1010
|
// record before _performRender wipes the host.
|
|
650
1011
|
//
|
|
651
|
-
// After capture, installSlotInterception + installSlotSensors
|
|
1012
|
+
// After capture, installSlotInterception + installSlotSensors in `_activate` make
|
|
652
1013
|
// native DOM writes on a mounted host live (appendChild, slot= flips,
|
|
653
1014
|
// innerHTML, HTMLSlotElement.assign), so light-DOM slots match shadow DOM
|
|
654
1015
|
// through the standard API.
|
|
655
|
-
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)) {
|
|
656
1023
|
// (a) Reconnection. Record already populated. Sweep any direct child
|
|
657
1024
|
// added by a raw bypass write while the host was disconnected (no
|
|
658
1025
|
// sensor was live to catch it), then re-arm the sensors below.
|
|
@@ -679,53 +1046,136 @@ class WebComponentBase extends Base {
|
|
|
679
1046
|
// (c) First mount with author-written children. Capture them.
|
|
680
1047
|
captureAuthoredChildren(this);
|
|
681
1048
|
}
|
|
682
|
-
// Install native-write interception AFTER capture (capture uses the
|
|
683
|
-
// host's still-native methods), then arm the sensors. Together they make
|
|
684
|
-
// appendChild / insertBefore / removeChild / innerHTML / slot= flips on a
|
|
685
|
-
// mounted light host drive the slot record, restoring full shadow-DOM
|
|
686
|
-
// parity through the standard DOM API. Interception installs once;
|
|
687
|
-
// sensors are armed on every connect and torn down on disconnect.
|
|
688
|
-
installSlotInterception(this);
|
|
689
|
-
installSlotSensors(this);
|
|
690
1049
|
}
|
|
1050
|
+
}
|
|
691
1051
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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);
|
|
695
1071
|
}
|
|
1072
|
+
}
|
|
696
1073
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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;
|
|
715
1123
|
}
|
|
716
|
-
this.
|
|
717
|
-
|
|
718
|
-
this.
|
|
719
|
-
|
|
720
|
-
this.
|
|
721
|
-
|
|
722
|
-
|
|
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();
|
|
723
1175
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
// installed on connect. There is no WebJs-specific slot API.
|
|
728
|
-
});
|
|
1176
|
+
adoptStyles(root, list);
|
|
1177
|
+
}
|
|
1178
|
+
return root;
|
|
729
1179
|
}
|
|
730
1180
|
|
|
731
1181
|
/**
|
|
@@ -773,9 +1223,12 @@ class WebComponentBase extends Base {
|
|
|
773
1223
|
// Dispose the signal watcher so dependency edges drop. Without
|
|
774
1224
|
// this the element holds references to module-scope signals
|
|
775
1225
|
// (and vice versa) forever.
|
|
776
|
-
|
|
777
|
-
this
|
|
778
|
-
|
|
1226
|
+
for (const slot of ['__signalWatcher', '__postWatcher']) {
|
|
1227
|
+
const self = /** @type {any} */ (this);
|
|
1228
|
+
if (self[slot]) {
|
|
1229
|
+
self[slot].dispose();
|
|
1230
|
+
self[slot] = undefined;
|
|
1231
|
+
}
|
|
779
1232
|
}
|
|
780
1233
|
for (const c of this.__controllers || NO_CONTROLLERS) {
|
|
781
1234
|
if (c.hostDisconnected) c.hostDisconnected();
|
|
@@ -801,30 +1254,72 @@ class WebComponentBase extends Base {
|
|
|
801
1254
|
const resolved = resolveAttributeProperty(this.constructor, name);
|
|
802
1255
|
if (resolved === undefined) return;
|
|
803
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;
|
|
804
1261
|
|
|
805
1262
|
// One reader for both sides, in `attribute-reader.js`: this and
|
|
806
1263
|
// `applyAttrsToInstance` in `render-server.js` call the same function, so
|
|
807
1264
|
// precedence and every fallback are shared rather than mirrored (#1340).
|
|
808
1265
|
// It takes an already-decoded value, which the DOM has done here and
|
|
809
1266
|
// `decodeAttrEntities` does at the SSR call site (#1341).
|
|
810
|
-
|
|
1267
|
+
let v = readAttributeValue(def, value, propName);
|
|
1268
|
+
if (v == null && def.useDefault && this.__defaultValues?.has(propName)) v = this.__defaultValues.get(propName);
|
|
811
1269
|
|
|
812
|
-
|
|
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 {
|
|
813
1275
|
this[propName] = v;
|
|
814
|
-
|
|
1276
|
+
} finally {
|
|
1277
|
+
this.__attributeToProperty = undefined;
|
|
815
1278
|
}
|
|
816
1279
|
}
|
|
817
1280
|
|
|
818
1281
|
/**
|
|
819
|
-
* Schedule a re-render
|
|
820
|
-
*
|
|
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.
|
|
821
1288
|
*
|
|
822
1289
|
* @param {string} [name] Property name that changed
|
|
823
1290
|
* @param {unknown} [oldValue] Previous value of the property
|
|
1291
|
+
* @param {PropertyDeclaration} [options] The property's declaration
|
|
824
1292
|
*/
|
|
825
|
-
requestUpdate(name, oldValue) {
|
|
826
|
-
if (name !== undefined
|
|
827
|
-
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
|
+
}
|
|
828
1323
|
}
|
|
829
1324
|
this._scheduleUpdate();
|
|
830
1325
|
}
|
|
@@ -845,21 +1340,21 @@ class WebComponentBase extends Base {
|
|
|
845
1340
|
});
|
|
846
1341
|
}
|
|
847
1342
|
if (this._isUpdating) return;
|
|
848
|
-
|
|
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;
|
|
849
1346
|
this._scheduled = true;
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
}
|
|
862
|
-
});
|
|
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'));
|
|
863
1358
|
}
|
|
864
1359
|
|
|
865
1360
|
/**
|
|
@@ -874,18 +1369,92 @@ class WebComponentBase extends Base {
|
|
|
874
1369
|
* 7. `firstUpdated(changedProperties)` runs once, on the first render only
|
|
875
1370
|
* 8. `updated(changedProperties)` runs after every commit
|
|
876
1371
|
* 9. Resolve `updateComplete` promise
|
|
1372
|
+
*
|
|
1373
|
+
* Every signal the SYNCHRONOUS part of that cycle reads is tracked, not
|
|
1374
|
+
* only the ones `render()` reads, which is the span lit-labs/signals tracks
|
|
1375
|
+
* by running `performUpdate` inside a computed. So a signal read only in
|
|
1376
|
+
* `willUpdate()` or `updated()` (a host attribute derived from internal
|
|
1377
|
+
* state) re-renders the component when it changes. Two watchers split it
|
|
1378
|
+
* (see {@link _tracked}): one over steps 2-6 (`render()` included), one
|
|
1379
|
+
* over the post-commit hooks, so a write `updated()` makes to something
|
|
1380
|
+
* `render()` read still schedules the next update. The DOM commits are
|
|
1381
|
+
* the parts left untracked (see `update()`).
|
|
877
1382
|
* @private
|
|
878
1383
|
*/
|
|
879
1384
|
_performRender() {
|
|
880
1385
|
if (!this._renderRoot) return;
|
|
1386
|
+
this.__cycleRendered = false;
|
|
1387
|
+
const postCommit = this._tracked('__signalWatcher', () => this._runUpdateCycle());
|
|
1388
|
+
// A sync commit runs its post-commit hooks OUTSIDE the first watcher's
|
|
1389
|
+
// phase, under their own watcher.
|
|
1390
|
+
if (postCommit) postCommit();
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* Run one phase of the update cycle with the signals it reads recorded on
|
|
1395
|
+
* the watcher in `slot`, creating the watcher on first use. A watcher
|
|
1396
|
+
* re-records its read set on every phase it observes, and a later change
|
|
1397
|
+
* to any of those signals schedules an update.
|
|
1398
|
+
*
|
|
1399
|
+
* Three rules keep that safe:
|
|
1400
|
+
* - A phase ignores notifications from its OWN writes. A write to a signal
|
|
1401
|
+
* the phase read earlier is the phase's business, not a reason to run it
|
|
1402
|
+
* again: before a commit it is already folded into this pass, and after
|
|
1403
|
+
* one it is often not the component's code at all (an event `updated()`
|
|
1404
|
+
* dispatches, whose listener does a read-modify-write on a store), which
|
|
1405
|
+
* would otherwise loop.
|
|
1406
|
+
* - A pre-commit phase that never reached `render()` (`shouldUpdate()`
|
|
1407
|
+
* returned false, or a hook threw) keeps the signals the last render
|
|
1408
|
+
* read on top of the ones it read itself, or a skipped cycle would
|
|
1409
|
+
* silently unsubscribe the component.
|
|
1410
|
+
* - A watcher disposed during its own phase (a hook that detached the
|
|
1411
|
+
* host) is disposed again afterwards, since the phase kept recording.
|
|
1412
|
+
*
|
|
1413
|
+
* @template T
|
|
1414
|
+
* @param {'__signalWatcher' | '__postWatcher'} slot
|
|
1415
|
+
* @param {() => T} fn
|
|
1416
|
+
* @returns {T}
|
|
1417
|
+
* @private
|
|
1418
|
+
*/
|
|
1419
|
+
_tracked(slot, fn) {
|
|
1420
|
+
const self = /** @type {any} */ (this);
|
|
1421
|
+
/** @type {any} */
|
|
1422
|
+
let watcher = self[slot];
|
|
1423
|
+
if (!watcher) {
|
|
1424
|
+
watcher = self[slot] = new Signal.subtle.Watcher(() => {
|
|
1425
|
+
if (!watcher.__inPhase && this._connected) this.requestUpdate();
|
|
1426
|
+
});
|
|
1427
|
+
}
|
|
1428
|
+
const before = slot === '__signalWatcher' ? Signal.subtle.introspectSources(watcher) : null;
|
|
1429
|
+
watcher.__inPhase = true;
|
|
1430
|
+
try {
|
|
1431
|
+
return watcher.observe(fn);
|
|
1432
|
+
} finally {
|
|
1433
|
+
watcher.__inPhase = false;
|
|
1434
|
+
if (before && !this.__cycleRendered) {
|
|
1435
|
+
watcher.watch(...new Set([...before, ...Signal.subtle.introspectSources(watcher)]));
|
|
1436
|
+
}
|
|
1437
|
+
if (!this._connected) {
|
|
1438
|
+
watcher.dispose();
|
|
1439
|
+
if (self[slot] === watcher) self[slot] = undefined;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* Steps 1-6 of the update cycle, the phase {@link _performRender} tracks
|
|
1446
|
+
* with the first watcher. Returns the post-commit half to run for a sync
|
|
1447
|
+
* commit, or nothing (an async commit runs it when it lands).
|
|
1448
|
+
* @returns {(() => void) | undefined}
|
|
1449
|
+
* @private
|
|
1450
|
+
*/
|
|
1451
|
+
_runUpdateCycle() {
|
|
881
1452
|
|
|
882
1453
|
// Hold a stable reference to the current Map so all hooks see the same
|
|
883
1454
|
// snapshot. During the update phase (steps 3-6) the Map is mutated in
|
|
884
1455
|
// place when property setters fire, folding those changes into THIS
|
|
885
|
-
// cycle. The Map is replaced
|
|
886
|
-
//
|
|
887
|
-
// On `shouldUpdate=false` or hook errors, the Map is preserved so the
|
|
888
|
-
// accumulated changes survive into the next render.
|
|
1456
|
+
// cycle. The Map is replaced when this phase finishes, including skipped
|
|
1457
|
+
// updates and hook errors, so post-commit writes belong to the NEXT cycle.
|
|
889
1458
|
const changedProperties = this._changedProperties;
|
|
890
1459
|
|
|
891
1460
|
// --- 1. Mark we're inside an update cycle ---
|
|
@@ -923,6 +1492,7 @@ class WebComponentBase extends Base {
|
|
|
923
1492
|
// in _commitAsync.
|
|
924
1493
|
if (this.__renderAbort) this.__renderAbort.abort();
|
|
925
1494
|
this.__renderAbort = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
1495
|
+
this.__cycleRendered = true;
|
|
926
1496
|
try {
|
|
927
1497
|
const r = this.update(changedProperties);
|
|
928
1498
|
if (r && typeof r.then === 'function') pendingCommit = r;
|
|
@@ -931,26 +1501,18 @@ class WebComponentBase extends Base {
|
|
|
931
1501
|
}
|
|
932
1502
|
|
|
933
1503
|
if (!pendingCommit) {
|
|
934
|
-
// --- 6. controllers' hostUpdated (sync commit) ---
|
|
935
|
-
for (const c of this.__controllers || NO_CONTROLLERS) {
|
|
936
|
-
if (c.hostUpdated) c.hostUpdated();
|
|
937
|
-
}
|
|
938
1504
|
didCommit = true;
|
|
939
1505
|
}
|
|
940
1506
|
}
|
|
941
|
-
// shouldUpdate=false: preserve _changedProperties so the next
|
|
942
|
-
// requestUpdate keeps accumulating on top of the entries that
|
|
943
|
-
// didn't render this cycle.
|
|
944
1507
|
} catch (preCommitError) {
|
|
945
1508
|
console.error(`[webjs] lifecycle hook threw during update phase:`, preCommitError);
|
|
946
1509
|
} finally {
|
|
947
1510
|
this._isUpdating = false;
|
|
948
|
-
//
|
|
949
|
-
// render
|
|
950
|
-
//
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
}
|
|
1511
|
+
// Consume this phase's snapshot even when shouldUpdate skips it. For an
|
|
1512
|
+
// async render, changes during the fetch start a new cycle whose token
|
|
1513
|
+
// supersedes the in-flight commit.
|
|
1514
|
+
// `update()` already swapped it when it committed (see there).
|
|
1515
|
+
if (this._changedProperties === changedProperties) this._changedProperties = new Map();
|
|
954
1516
|
}
|
|
955
1517
|
|
|
956
1518
|
// --- Async render: finish the cycle when the pending commit settles. ---
|
|
@@ -967,10 +1529,6 @@ class WebComponentBase extends Base {
|
|
|
967
1529
|
this.__pendingAsyncCommits--;
|
|
968
1530
|
// A newer render superseded this one; let the newer cycle finish.
|
|
969
1531
|
if (token !== this.__renderToken) return;
|
|
970
|
-
// --- 6. controllers' hostUpdated (after the async commit) ---
|
|
971
|
-
for (const c of this.__controllers || NO_CONTROLLERS) {
|
|
972
|
-
if (c.hostUpdated) c.hostUpdated();
|
|
973
|
-
}
|
|
974
1532
|
// --- 7-8 + updateComplete ---
|
|
975
1533
|
this._postCommit(changedProperties);
|
|
976
1534
|
};
|
|
@@ -1008,16 +1566,21 @@ class WebComponentBase extends Base {
|
|
|
1008
1566
|
return;
|
|
1009
1567
|
}
|
|
1010
1568
|
|
|
1011
|
-
// --- 7-8. Post-commit hooks (sync path)
|
|
1012
|
-
// updateComplete promise always
|
|
1569
|
+
// --- 7-8. Post-commit hooks (sync path), returned so they run outside
|
|
1570
|
+
// this phase. Errors are caught so the updateComplete promise always
|
|
1571
|
+
// resolves.
|
|
1013
1572
|
if (didCommit) {
|
|
1014
|
-
this._postCommit(changedProperties);
|
|
1573
|
+
return () => this._postCommit(changedProperties);
|
|
1015
1574
|
} else if (!this.__pendingAsyncCommits) {
|
|
1016
1575
|
// A non-committing cycle resolves updateComplete only when no async
|
|
1017
1576
|
// commit is in flight; otherwise the pending commit resolves it once it
|
|
1018
1577
|
// lands, so `await el.updateComplete` never returns before that DOM.
|
|
1019
1578
|
this._resolveUpdate();
|
|
1020
1579
|
}
|
|
1580
|
+
// No post-commit phase this cycle: re-arm its watcher, so a change it
|
|
1581
|
+
// was notified of meanwhile does not leave it deaf to the next one.
|
|
1582
|
+
if (!didCommit && /** @type {any} */ (this).__postWatcher) /** @type {any} */ (this).__postWatcher.watch();
|
|
1583
|
+
return undefined;
|
|
1021
1584
|
}
|
|
1022
1585
|
|
|
1023
1586
|
/**
|
|
@@ -1028,7 +1591,7 @@ class WebComponentBase extends Base {
|
|
|
1028
1591
|
*/
|
|
1029
1592
|
_resolveUpdate() {
|
|
1030
1593
|
if (this._updateResolve) {
|
|
1031
|
-
const settled = this.
|
|
1594
|
+
const settled = !this._scheduled;
|
|
1032
1595
|
this._updateResolve(settled);
|
|
1033
1596
|
this._updateResolve = null;
|
|
1034
1597
|
}
|
|
@@ -1060,8 +1623,17 @@ class WebComponentBase extends Base {
|
|
|
1060
1623
|
this._isUpdating = true;
|
|
1061
1624
|
try {
|
|
1062
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.
|
|
1063
1630
|
for (const c of this.__controllers || NO_CONTROLLERS) {
|
|
1064
|
-
if (c.hostUpdate)
|
|
1631
|
+
if (!c.hostUpdate) continue;
|
|
1632
|
+
try {
|
|
1633
|
+
c.hostUpdate();
|
|
1634
|
+
} catch (err) {
|
|
1635
|
+
reportSsrControllerError(this, c, err);
|
|
1636
|
+
}
|
|
1065
1637
|
}
|
|
1066
1638
|
this._reflectDeclaredAttributes();
|
|
1067
1639
|
} finally {
|
|
@@ -1087,10 +1659,16 @@ class WebComponentBase extends Base {
|
|
|
1087
1659
|
* @private
|
|
1088
1660
|
*/
|
|
1089
1661
|
_reflectDeclaredAttributes() {
|
|
1090
|
-
const props =
|
|
1091
|
-
for (const name of
|
|
1662
|
+
const props = declaredProperties(this.constructor);
|
|
1663
|
+
for (const name of Reflect.ownKeys(props)) {
|
|
1092
1664
|
const decl = props[name];
|
|
1093
|
-
if (!decl || !decl.reflect
|
|
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;
|
|
1094
1672
|
this._reflectAttribute(name, this[name], decl);
|
|
1095
1673
|
}
|
|
1096
1674
|
}
|
|
@@ -1154,16 +1732,14 @@ class WebComponentBase extends Base {
|
|
|
1154
1732
|
* @param {Map<string, unknown>} _changedProperties
|
|
1155
1733
|
*/
|
|
1156
1734
|
update(_changedProperties) {
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
if (!this.__signalWatcher) {
|
|
1163
|
-
this.__signalWatcher = new Signal.subtle.Watcher(() => {
|
|
1164
|
-
if (this._connected) this.requestUpdate();
|
|
1165
|
-
});
|
|
1735
|
+
const pendingReflection = this.__reflectPending;
|
|
1736
|
+
this.__reflectPending = undefined;
|
|
1737
|
+
for (const name of pendingReflection || []) {
|
|
1738
|
+
const def = declaredProperties(this.constructor)[name];
|
|
1739
|
+
this._reflectAttribute(name, this[name], def);
|
|
1166
1740
|
}
|
|
1741
|
+
// The signal reads render() makes are tracked by the watcher
|
|
1742
|
+
// _performRender runs the whole cycle under.
|
|
1167
1743
|
let tpl;
|
|
1168
1744
|
// Bind this render's abort signal (#492) around the synchronous part of
|
|
1169
1745
|
// render() ONLY, so the RPC stub ties an action called there to this
|
|
@@ -1176,7 +1752,7 @@ class WebComponentBase extends Base {
|
|
|
1176
1752
|
const outer = activeActionSignal();
|
|
1177
1753
|
setActiveActionSignal(this.__renderAbort ? this.__renderAbort.signal : null);
|
|
1178
1754
|
try {
|
|
1179
|
-
|
|
1755
|
+
tpl = this.render();
|
|
1180
1756
|
} finally {
|
|
1181
1757
|
setActiveActionSignal(outer || null);
|
|
1182
1758
|
}
|
|
@@ -1191,10 +1767,33 @@ class WebComponentBase extends Base {
|
|
|
1191
1767
|
if (tpl && typeof (/** @type any */ (tpl).then) === 'function') {
|
|
1192
1768
|
return this._commitAsync(/** @type {Promise<unknown>} */ (tpl));
|
|
1193
1769
|
}
|
|
1194
|
-
|
|
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();
|
|
1774
|
+
// The commit is untracked: a signal read while the DOM is patched (a
|
|
1775
|
+
// `watch()` part, a child's synchronous work) belongs to whatever read
|
|
1776
|
+
// it, not to this component's next render.
|
|
1777
|
+
Signal.subtle.untrack(() => clientRender(tpl, this._renderRoot, { host: this }));
|
|
1195
1778
|
return undefined;
|
|
1196
1779
|
}
|
|
1197
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
|
+
|
|
1198
1797
|
/**
|
|
1199
1798
|
* Commit a promise-returning render() with stale-while-revalidate
|
|
1200
1799
|
* semantics (#469). The current DOM stays untouched until the template
|
|
@@ -1220,7 +1819,7 @@ class WebComponentBase extends Base {
|
|
|
1220
1819
|
if (this.__firstRendered && this._overridesRenderFallback()) {
|
|
1221
1820
|
try {
|
|
1222
1821
|
const fb = this.renderFallback();
|
|
1223
|
-
if (fb !== undefined) clientRender(fb, this._renderRoot);
|
|
1822
|
+
if (fb !== undefined) Signal.subtle.untrack(() => clientRender(fb, this._renderRoot, { host: this }));
|
|
1224
1823
|
} catch (e) {
|
|
1225
1824
|
console.error(`[webjs] renderFallback() threw:`, e);
|
|
1226
1825
|
}
|
|
@@ -1239,7 +1838,7 @@ class WebComponentBase extends Base {
|
|
|
1239
1838
|
// The sync path routes an identical throw to the same boundary, so
|
|
1240
1839
|
// this is what makes the two paths agree.
|
|
1241
1840
|
try {
|
|
1242
|
-
clientRender(tpl, this._renderRoot);
|
|
1841
|
+
clientRender(tpl, this._renderRoot, { host: this });
|
|
1243
1842
|
} catch (commitError) {
|
|
1244
1843
|
this._handleRenderError(
|
|
1245
1844
|
commitError instanceof Error ? commitError : new Error(String(commitError)),
|
|
@@ -1276,7 +1875,7 @@ class WebComponentBase extends Base {
|
|
|
1276
1875
|
);
|
|
1277
1876
|
try {
|
|
1278
1877
|
const fallback = this.renderError(error);
|
|
1279
|
-
if (fallback !== undefined) clientRender(fallback, this._renderRoot);
|
|
1878
|
+
if (fallback !== undefined) Signal.subtle.untrack(() => clientRender(fallback, this._renderRoot, { host: this }));
|
|
1280
1879
|
} catch (fallbackError) {
|
|
1281
1880
|
console.error(`[webjs] renderError() also threw:`, fallbackError);
|
|
1282
1881
|
}
|
|
@@ -1289,16 +1888,32 @@ class WebComponentBase extends Base {
|
|
|
1289
1888
|
* @private
|
|
1290
1889
|
*/
|
|
1291
1890
|
_postCommit(changedProperties) {
|
|
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
|
+
}
|
|
1292
1903
|
try {
|
|
1293
|
-
|
|
1294
|
-
this.
|
|
1295
|
-
this.
|
|
1296
|
-
|
|
1297
|
-
|
|
1904
|
+
this._tracked('__postWatcher', () => {
|
|
1905
|
+
for (const c of this.__controllers || NO_CONTROLLERS) c.hostUpdated?.();
|
|
1906
|
+
if (!this.__firstRendered) {
|
|
1907
|
+
this.__firstRendered = true;
|
|
1908
|
+
this.firstUpdated(changedProperties);
|
|
1909
|
+
}
|
|
1910
|
+
this.updated(changedProperties);
|
|
1911
|
+
});
|
|
1298
1912
|
} catch (postCommitError) {
|
|
1299
1913
|
console.error(`[webjs] lifecycle hook threw during post-commit phase:`, postCommitError);
|
|
1300
1914
|
} finally {
|
|
1301
|
-
this.
|
|
1915
|
+
this.__settlingPromise = undefined;
|
|
1916
|
+
resolve?.(!this._scheduled);
|
|
1302
1917
|
}
|
|
1303
1918
|
}
|
|
1304
1919
|
|
|
@@ -1337,7 +1952,7 @@ class WebComponentBase extends Base {
|
|
|
1337
1952
|
*
|
|
1338
1953
|
* ```js
|
|
1339
1954
|
* firstUpdated(changedProperties) {
|
|
1340
|
-
* this.
|
|
1955
|
+
* this.renderRoot.querySelector('input')?.focus();
|
|
1341
1956
|
* }
|
|
1342
1957
|
* ```
|
|
1343
1958
|
*
|
|
@@ -1381,6 +1996,14 @@ class WebComponentBase extends Base {
|
|
|
1381
1996
|
* @returns {Promise<boolean>}
|
|
1382
1997
|
*/
|
|
1383
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
|
+
}
|
|
1384
2007
|
return this._updatePromise;
|
|
1385
2008
|
}
|
|
1386
2009
|
|
|
@@ -1439,11 +2062,13 @@ class WebComponentBase extends Base {
|
|
|
1439
2062
|
}
|
|
1440
2063
|
|
|
1441
2064
|
/**
|
|
1442
|
-
* 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.
|
|
1443
2068
|
* @returns {unknown}
|
|
1444
2069
|
*/
|
|
1445
2070
|
render() {
|
|
1446
|
-
return
|
|
2071
|
+
return noChange;
|
|
1447
2072
|
}
|
|
1448
2073
|
|
|
1449
2074
|
/**
|
|
@@ -1501,10 +2126,33 @@ class WebComponentBase extends Base {
|
|
|
1501
2126
|
}
|
|
1502
2127
|
}
|
|
1503
2128
|
|
|
1504
|
-
/**
|
|
1505
|
-
|
|
1506
|
-
|
|
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
|
+
);
|
|
1507
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
|
+
*/
|
|
1508
2156
|
/** @param {string} s */
|
|
1509
2157
|
function camelCase(s) {
|
|
1510
2158
|
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
@@ -1524,11 +2172,32 @@ export function WebComponent(properties) {
|
|
|
1524
2172
|
} else {
|
|
1525
2173
|
return class extends WebComponentBase {
|
|
1526
2174
|
static properties = properties;
|
|
1527
|
-
static [FACTORY_PROPS] = true;
|
|
1528
2175
|
};
|
|
1529
2176
|
}
|
|
1530
2177
|
}
|
|
1531
2178
|
|
|
2179
|
+
/**
|
|
2180
|
+
* Re-assign every own data property of `el` that hides a setter defined on
|
|
2181
|
+
* its class chain below the WebJs base, so the value goes through the
|
|
2182
|
+
* accessor instead of shadowing it.
|
|
2183
|
+
*
|
|
2184
|
+
* @param {any} el
|
|
2185
|
+
*/
|
|
2186
|
+
function liftShadowedAccessors(el) {
|
|
2187
|
+
for (const key of Object.keys(el)) {
|
|
2188
|
+
for (let p = Object.getPrototypeOf(el); p && p !== WebComponentBase.prototype; p = Object.getPrototypeOf(p)) {
|
|
2189
|
+
const d = Object.getOwnPropertyDescriptor(p, key);
|
|
2190
|
+
if (!d) continue;
|
|
2191
|
+
if (d.set) {
|
|
2192
|
+
const value = el[key];
|
|
2193
|
+
delete el[key];
|
|
2194
|
+
el[key] = value;
|
|
2195
|
+
}
|
|
2196
|
+
break;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
1532
2201
|
// Ensure static inheritance and instanceof checks work
|
|
1533
2202
|
Object.setPrototypeOf(WebComponent, WebComponentBase);
|
|
1534
2203
|
WebComponent.prototype = WebComponentBase.prototype;
|