@webjsdev/core 0.7.57 → 0.7.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/webjs-core-browser.js +4 -4
- package/dist/webjs-core-browser.js.map +2 -2
- package/dist/webjs-core-testing.js +13 -13
- package/dist/webjs-core-testing.js.map +2 -2
- package/dist/webjs-core.js +5 -5
- package/dist/webjs-core.js.map +2 -2
- package/package.json +1 -1
- package/src/attribute-reader.js +55 -7
- package/src/component/lifecycle.js +28 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjsdev/core",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.59",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The runtime for WebJs, a full-stack JavaScript framework built on web components with server-side rendering and no build step. Ships the html and css template tags, the WebComponent base class, signals, directives, and the isomorphic renderers.",
|
|
6
6
|
"types": "./index.d.ts",
|
package/src/attribute-reader.js
CHANGED
|
@@ -109,9 +109,8 @@ export function readAttributeValue(def, value) {
|
|
|
109
109
|
//
|
|
110
110
|
// The two readers now see the SAME attribute set, so this fallback is not
|
|
111
111
|
// the only thing they agree on (#1341). Both resolve a name through
|
|
112
|
-
// `resolveAttributeProperty` below, which matches the same
|
|
113
|
-
// `
|
|
114
|
-
// and nothing else, so it skips a `state: true` prop and ignores a name that
|
|
112
|
+
// `resolveAttributeProperty` below, which matches the same names
|
|
113
|
+
// `observedAttributes` lists (via `observedAttributeNames`) and nothing else, so it skips a `state: true` prop and ignores a name that
|
|
115
114
|
// maps to no attribute-backed property exactly as the observed list does.
|
|
116
115
|
// The SSR caller lowercases its source name first, because the browser
|
|
117
116
|
// lowercases while parsing. And both are handed a value with every
|
|
@@ -148,8 +147,9 @@ function hyphenate(s) {
|
|
|
148
147
|
* SSR read MORE than a browser can deliver, which is the bug this function
|
|
149
148
|
* exists to remove, so do not add it.
|
|
150
149
|
*
|
|
151
|
-
* The match is EXACTLY `
|
|
152
|
-
* `
|
|
150
|
+
* The match is EXACTLY the names `observedAttributeNames` lists (the
|
|
151
|
+
* `d.attribute || hyphenate(k)` primary plus, for a camelCase prop with no
|
|
152
|
+
* custom attribute, its lowercased alias, #1540), and nothing else. A `props[attrName] ||
|
|
153
153
|
* props[camelCase(attrName)]` fallback used to sit after the loop, so a prop
|
|
154
154
|
* declaring a custom attribute also answered to its PROPERTY name. Both readers
|
|
155
155
|
* carried that code, which read like an agreement and was not one: the browser
|
|
@@ -168,11 +168,59 @@ function hyphenate(s) {
|
|
|
168
168
|
* which case the caller must IGNORE the attribute entirely (#1341).
|
|
169
169
|
*/
|
|
170
170
|
export function resolveAttributeProperty(Cls, attrName) {
|
|
171
|
+
// Primary names first, so a prop literally named `foobar` keeps `foobar`
|
|
172
|
+
// even when a sibling `fooBar` would answer to it as an alias.
|
|
173
|
+
let aliasHit;
|
|
174
|
+
for (const { propName, def, primary, alias } of attributeBackedProps(Cls)) {
|
|
175
|
+
if (primary === attrName) return { propName, def };
|
|
176
|
+
if (aliasHit === undefined && alias === attrName) aliasHit = { propName, def };
|
|
177
|
+
}
|
|
178
|
+
return aliasHit;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Every attribute name the browser must observe for `Cls`, the list
|
|
183
|
+
* `observedAttributes` returns. Built from the same `attributeBackedProps` walk
|
|
184
|
+
* `resolveAttributeProperty` matches against, so a name is observed exactly
|
|
185
|
+
* when it resolves, on both sides (#1341, #1540).
|
|
186
|
+
*
|
|
187
|
+
* @param {any} Cls the component class
|
|
188
|
+
* @returns {string[]}
|
|
189
|
+
*/
|
|
190
|
+
export function observedAttributeNames(Cls) {
|
|
191
|
+
const names = new Set();
|
|
192
|
+
const entries = attributeBackedProps(Cls);
|
|
193
|
+
for (const { primary } of entries) names.add(primary);
|
|
194
|
+
for (const { alias } of entries) if (alias) names.add(alias);
|
|
195
|
+
return [...names];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* The attribute-backed props of `Cls` with the names each answers to.
|
|
200
|
+
*
|
|
201
|
+
* `primary` is the custom `attribute` option, matched VERBATIM, or else the
|
|
202
|
+
* kebab-cased property name. `alias` is the LOWERCASED property name, lit's
|
|
203
|
+
* default attribute name, present only when the prop has no custom `attribute`
|
|
204
|
+
* and the lowercased name differs from the kebab one. Without it,
|
|
205
|
+
* `<quiz-player quizId=${id}>` against `{ quizId: Number }` reached neither
|
|
206
|
+
* side: the parser lowercases the name to `quizid`, which matched nothing, so
|
|
207
|
+
* the prop silently stayed undefined on the server AND the client (#1540).
|
|
208
|
+
* The alias is only ever lowercase, which is the one form the platform can
|
|
209
|
+
* deliver, so this does not reopen the SSR-reads-more gap #1341 closed.
|
|
210
|
+
*
|
|
211
|
+
* @param {any} Cls
|
|
212
|
+
* @returns {{ propName: string, def: PropertyDeclaration, primary: string, alias: string | undefined }[]}
|
|
213
|
+
*/
|
|
214
|
+
function attributeBackedProps(Cls) {
|
|
171
215
|
const props = (Cls && Cls.properties) || {};
|
|
216
|
+
const out = [];
|
|
172
217
|
for (const [k, decl] of Object.entries(props)) {
|
|
173
218
|
const d = typeof decl === 'object' && decl !== null ? decl : { type: decl };
|
|
174
219
|
if (d.state) continue;
|
|
175
|
-
|
|
220
|
+
const primary = d.attribute || hyphenate(k);
|
|
221
|
+
const lower = k.toLowerCase();
|
|
222
|
+
const alias = !d.attribute && lower !== primary ? lower : undefined;
|
|
223
|
+
out.push({ propName: k, def: d, primary, alias });
|
|
176
224
|
}
|
|
177
|
-
return
|
|
225
|
+
return out;
|
|
178
226
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { render as clientRender } from '../render-client.js';
|
|
2
|
-
import { readAttributeValue, resolveAttributeProperty } from '../attribute-reader.js';
|
|
3
|
-
import { setActiveActionSignal } from '../action-abort-client.js';
|
|
2
|
+
import { readAttributeValue, resolveAttributeProperty, observedAttributeNames } from '../attribute-reader.js';
|
|
3
|
+
import { setActiveActionSignal, activeActionSignal } from '../action-abort-client.js';
|
|
4
4
|
import { carriesFunction } from '../form-action.js';
|
|
5
5
|
import { isCSS, adoptStyles } from '../css.js';
|
|
6
6
|
import { register, tagOf } from '../registry.js';
|
|
@@ -184,10 +184,12 @@ class WebComponentBase extends Base {
|
|
|
184
184
|
* @returns {string[]}
|
|
185
185
|
*/
|
|
186
186
|
static get observedAttributes() {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
187
|
+
// Derived from the same walk `resolveAttributeProperty` matches against, so
|
|
188
|
+
// every observed name resolves and nothing else does. A camelCase prop with
|
|
189
|
+
// no custom `attribute` is observed under its kebab name AND its lowercased
|
|
190
|
+
// name (lit's default), since the parser turns `quizId=` into `quizid`
|
|
191
|
+
// (#1540).
|
|
192
|
+
return observedAttributeNames(this);
|
|
191
193
|
}
|
|
192
194
|
|
|
193
195
|
constructor() {
|
|
@@ -872,23 +874,18 @@ class WebComponentBase extends Base {
|
|
|
872
874
|
// by-sync case (#469); a shouldUpdate=false cycle never reaches here,
|
|
873
875
|
// so it does not invalidate an in-flight async render.
|
|
874
876
|
this.__renderToken = (this.__renderToken || 0) + 1;
|
|
875
|
-
// Abort the superseded render's in-flight action fetches (#492)
|
|
876
|
-
// fresh controller for this render
|
|
877
|
-
//
|
|
878
|
-
// AbortError is dropped by the render-token guard
|
|
877
|
+
// Abort the superseded render's in-flight action fetches (#492) and
|
|
878
|
+
// start a fresh controller for this render. update() binds its signal
|
|
879
|
+
// around render() alone (see there), never the DOM commit. A
|
|
880
|
+
// superseded fetch's AbortError is dropped by the render-token guard
|
|
881
|
+
// in _commitAsync.
|
|
879
882
|
if (this.__renderAbort) this.__renderAbort.abort();
|
|
880
883
|
this.__renderAbort = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
881
|
-
setActiveActionSignal(this.__renderAbort ? this.__renderAbort.signal : null);
|
|
882
884
|
try {
|
|
883
885
|
const r = this.update(changedProperties);
|
|
884
886
|
if (r && typeof r.then === 'function') pendingCommit = r;
|
|
885
887
|
} catch (error) {
|
|
886
888
|
this._handleRenderError(/** @type {Error} */ (error));
|
|
887
|
-
} finally {
|
|
888
|
-
// The synchronous portion of render() has run; actions invoked there
|
|
889
|
-
// already captured the signal. Clear it so a later event handler is
|
|
890
|
-
// not bound to a stale render's controller.
|
|
891
|
-
setActiveActionSignal(null);
|
|
892
889
|
}
|
|
893
890
|
|
|
894
891
|
if (!pendingCommit) {
|
|
@@ -1126,7 +1123,21 @@ class WebComponentBase extends Base {
|
|
|
1126
1123
|
});
|
|
1127
1124
|
}
|
|
1128
1125
|
let tpl;
|
|
1129
|
-
this
|
|
1126
|
+
// Bind this render's abort signal (#492) around the synchronous part of
|
|
1127
|
+
// render() ONLY, so the RPC stub ties an action called there to this
|
|
1128
|
+
// render and the next render cancels it. The commit below runs unbound:
|
|
1129
|
+
// it connects the child elements this template creates, and an action a
|
|
1130
|
+
// child starts in its connectedCallback belongs to the child, not to this
|
|
1131
|
+
// render, so the parent's next render must not cancel it (#1514). The
|
|
1132
|
+
// previous binding is restored rather than cleared, so a render nested
|
|
1133
|
+
// inside another's synchronous work leaves the outer one intact.
|
|
1134
|
+
const outer = activeActionSignal();
|
|
1135
|
+
setActiveActionSignal(this.__renderAbort ? this.__renderAbort.signal : null);
|
|
1136
|
+
try {
|
|
1137
|
+
this.__signalWatcher.observe(() => { tpl = this.render(); });
|
|
1138
|
+
} finally {
|
|
1139
|
+
setActiveActionSignal(outer || null);
|
|
1140
|
+
}
|
|
1130
1141
|
// Bare-await async render (#469): render() returned a promise. Use
|
|
1131
1142
|
// stale-while-revalidate: keep the current DOM (the SSR first paint on
|
|
1132
1143
|
// hydration, or the prior content on a re-fetch) visible until the new
|