@microsoft/webui-framework 0.0.29 → 0.0.30
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 +2 -2
- package/dist/decorators.js +42 -8
- package/dist/element.d.ts +12 -0
- package/dist/element.js +122 -1
- package/dist/template-element.d.ts +4 -2
- package/dist/template-element.js +23 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ This package is the browser-side runtime used by `webui build --plugin=webui`. I
|
|
|
12
12
|
|
|
13
13
|
If you are building WebUI apps in this repo, this is the component model used by examples like `examples/app/todo-webui`, `examples/app/commerce`, and `examples/app/contact-book-manager`.
|
|
14
14
|
|
|
15
|
-
> 📖 **Full documentation at [microsoft.github.io/webui](https://microsoft.github.io/webui)**, see the [Interactivity Guide](https://microsoft.github.io/webui/guide/concepts/interactivity) for component authoring patterns. For framework internals (hydration, path resolution, reactive update model), see [
|
|
15
|
+
> 📖 **Full documentation at [microsoft.github.io/webui](https://microsoft.github.io/webui)**, see the [Interactivity Guide](https://microsoft.github.io/webui/guide/concepts/interactivity) for component authoring patterns. For framework internals (hydration, path resolution, reactive update model), see [the hydration contract](https://github.com/microsoft/webui/blob/main/specs/hydration.md).
|
|
16
16
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
@@ -328,7 +328,7 @@ update that DOM, so the write is dropped and the runtime logs a
|
|
|
328
328
|
`[WebUI] Hydration mismatch` warning naming the properties. Seed such values in
|
|
329
329
|
the SSR state, or assign them from `hydratedCallback()`. The warning is
|
|
330
330
|
development-only and is dead-code-eliminated from production bundles via the
|
|
331
|
-
`__WEBUI_DEV__` compile-time flag (on by default; `webui
|
|
331
|
+
`__WEBUI_DEV__` compile-time flag (on by default; `webui press build` sets it to
|
|
332
332
|
`false`). See the
|
|
333
333
|
[Interactivity Guide](https://microsoft.github.io/webui/guide/concepts/interactivity#setting-observable-state-during-setup).
|
|
334
334
|
|
package/dist/decorators.js
CHANGED
|
@@ -162,6 +162,12 @@ function createReactiveProperty(proto, name, attrDefinition) {
|
|
|
162
162
|
return this[backingKey];
|
|
163
163
|
},
|
|
164
164
|
set(newValue) {
|
|
165
|
+
const captured = this['$preUpgradeProperties'];
|
|
166
|
+
if (captured?.has(name) && this.matches(':defined')) {
|
|
167
|
+
captured.delete(name);
|
|
168
|
+
if (captured.size === 0)
|
|
169
|
+
this['$preUpgradeProperties'] = undefined;
|
|
170
|
+
}
|
|
165
171
|
const oldValue = this[backingKey];
|
|
166
172
|
if (Object.is(oldValue, newValue)) {
|
|
167
173
|
if (this['$deferredSSR'] === true) {
|
|
@@ -172,12 +178,33 @@ function createReactiveProperty(proto, name, attrDefinition) {
|
|
|
172
178
|
return;
|
|
173
179
|
}
|
|
174
180
|
this[backingKey] = newValue;
|
|
175
|
-
|
|
176
|
-
|
|
181
|
+
let canReflect = false;
|
|
182
|
+
if (attrDefinition) {
|
|
183
|
+
const checkReflection = this['$canRunAuthoredEffects'];
|
|
184
|
+
if (typeof checkReflection === 'function' &&
|
|
185
|
+
checkReflection.call(this)) {
|
|
186
|
+
reflectPropertyToAttribute(this, attrDefinition, newValue);
|
|
187
|
+
canReflect = true;
|
|
188
|
+
}
|
|
177
189
|
}
|
|
178
|
-
const
|
|
179
|
-
if (typeof
|
|
180
|
-
|
|
190
|
+
const callback = this[changedKey];
|
|
191
|
+
if (typeof callback === 'function') {
|
|
192
|
+
if (this['$authoredPhase'] === 1 &&
|
|
193
|
+
(attrDefinition ? canReflect : this.isConnected)) {
|
|
194
|
+
callback
|
|
195
|
+
.call(this, oldValue, newValue);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const record = this['$recordPropertyChange'];
|
|
199
|
+
if (typeof record === 'function') {
|
|
200
|
+
record
|
|
201
|
+
.call(this, name, oldValue);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
callback
|
|
205
|
+
.call(this, oldValue, newValue);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
181
208
|
}
|
|
182
209
|
if (this.isConnected ||
|
|
183
210
|
this['$deferredSSR'] === true) {
|
|
@@ -234,9 +261,16 @@ function applyAttr(target, name, options) {
|
|
|
234
261
|
const definition = attrDefinitionFor(this.constructor, attribute);
|
|
235
262
|
if (definition !== undefined &&
|
|
236
263
|
this[reflectingAttribute] !== attribute) {
|
|
237
|
-
this[
|
|
238
|
-
|
|
239
|
-
|
|
264
|
+
const captured = this['$preUpgradeProperties'];
|
|
265
|
+
if (captured)
|
|
266
|
+
this['$preUpgradeProperties'] = undefined;
|
|
267
|
+
try {
|
|
268
|
+
this[definition.property] = definition.boolean ? newVal !== null : newVal;
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
if (captured)
|
|
272
|
+
this['$preUpgradeProperties'] = captured;
|
|
273
|
+
}
|
|
240
274
|
}
|
|
241
275
|
if (origACB) {
|
|
242
276
|
origACB.call(this, attribute, oldVal, newVal);
|
package/dist/element.d.ts
CHANGED
|
@@ -4,6 +4,18 @@ import type { TemplateBlockMeta, TemplateMeta, TemplateNodeIndex } from './templ
|
|
|
4
4
|
import type { ScopeFrame, TemplateInstance } from './element/types.js';
|
|
5
5
|
export declare class WebUIElement extends TemplateElement {
|
|
6
6
|
private $lazyHydrationMode;
|
|
7
|
+
private $authoredPhase;
|
|
8
|
+
private $pendingPropertyChanges;
|
|
9
|
+
private $preUpgradeProperties;
|
|
10
|
+
constructor();
|
|
11
|
+
connectedCallback(): void;
|
|
12
|
+
$destroy(): void;
|
|
13
|
+
$recordPropertyChange(name: string, oldValue: unknown): void;
|
|
14
|
+
protected $reconcileAuthoredState(): boolean;
|
|
15
|
+
$flushUpdates(): void;
|
|
16
|
+
private $flushAuthoredChanges;
|
|
17
|
+
private $invokePropertyChange;
|
|
18
|
+
private $releasePropertyChanges;
|
|
7
19
|
protected $shouldDeferSSRHydration(meta?: TemplateMeta): boolean;
|
|
8
20
|
protected $didDeferSSRHydration(): void;
|
|
9
21
|
protected $didDeferStreamedSSRHydration(state: Record<string, unknown> | undefined): void;
|
package/dist/element.js
CHANGED
|
@@ -7,6 +7,9 @@ const RENDER_ATTR = 'w-render';
|
|
|
7
7
|
const HYDRATE_EAGER = 'eager';
|
|
8
8
|
const INTERACTION_HYDRATION = 3;
|
|
9
9
|
const LAZY_RENDER_INTERACTION = 4;
|
|
10
|
+
const AUTHORED_LIVE = 1;
|
|
11
|
+
const AUTHORED_PENDING = 2;
|
|
12
|
+
const AUTHORED_FLUSHING = 4;
|
|
10
13
|
const DEV = typeof __WEBUI_DEV__ === 'undefined' || __WEBUI_DEV__;
|
|
11
14
|
let warnedMissingLazyHydrationEntry = false;
|
|
12
15
|
function warnMissingLazyHydrationEntry(tag, policy) {
|
|
@@ -22,6 +25,121 @@ function warnMissingLazyHydrationEntry(tag, policy) {
|
|
|
22
25
|
'your component definitions to enable visibility-deferred hydration.');
|
|
23
26
|
}
|
|
24
27
|
export class WebUIElement extends TemplateElement {
|
|
28
|
+
constructor() {
|
|
29
|
+
super();
|
|
30
|
+
const names = getObservableNames(this.constructor);
|
|
31
|
+
for (const name of names) {
|
|
32
|
+
if (!Object.prototype.hasOwnProperty.call(this, name))
|
|
33
|
+
continue;
|
|
34
|
+
(this.$preUpgradeProperties ??= new Map()).set(name, this[name]);
|
|
35
|
+
delete this[name];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
connectedCallback() {
|
|
39
|
+
const properties = this.$preUpgradeProperties;
|
|
40
|
+
if (properties) {
|
|
41
|
+
this.$preUpgradeProperties = undefined;
|
|
42
|
+
for (const [name, value] of properties) {
|
|
43
|
+
this[name] = value;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
super.connectedCallback();
|
|
47
|
+
}
|
|
48
|
+
$destroy() {
|
|
49
|
+
if (this.$authoredPhase !== undefined)
|
|
50
|
+
this.$authoredPhase &= ~AUTHORED_LIVE;
|
|
51
|
+
super.$destroy();
|
|
52
|
+
}
|
|
53
|
+
$recordPropertyChange(name, oldValue) {
|
|
54
|
+
if (this.$authoredPhase === undefined)
|
|
55
|
+
return;
|
|
56
|
+
const changes = this.$pendingPropertyChanges;
|
|
57
|
+
if ((this.$authoredPhase & AUTHORED_LIVE) && this.$canRunAuthoredEffects()) {
|
|
58
|
+
if (changes?.has(name)) {
|
|
59
|
+
oldValue = changes.get(name);
|
|
60
|
+
changes.delete(name);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
this.$invokePropertyChange(name, oldValue);
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
this.$releasePropertyChanges();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const queued = changes ?? (this.$pendingPropertyChanges = new Map());
|
|
71
|
+
if (!queued.has(name))
|
|
72
|
+
queued.set(name, oldValue);
|
|
73
|
+
this.$authoredPhase |= AUTHORED_PENDING;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
$reconcileAuthoredState() {
|
|
77
|
+
if (!this.$canRunAuthoredEffects())
|
|
78
|
+
return !this.$pendingPropertyChanges;
|
|
79
|
+
if (this.$authoredPhase === undefined) {
|
|
80
|
+
const names = getObservableNames(this.constructor);
|
|
81
|
+
for (const name of names) {
|
|
82
|
+
if (typeof this[`${name}Changed`] !== 'function')
|
|
83
|
+
continue;
|
|
84
|
+
this.$authoredPhase = AUTHORED_LIVE;
|
|
85
|
+
if (Object.prototype.hasOwnProperty.call(this, `_${name}`)) {
|
|
86
|
+
(this.$pendingPropertyChanges ??= new Map()).set(name, undefined);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (this.$authoredPhase === undefined)
|
|
91
|
+
return true;
|
|
92
|
+
this.$authoredPhase |= AUTHORED_LIVE;
|
|
93
|
+
if (this.$authoredPhase & AUTHORED_FLUSHING)
|
|
94
|
+
return false;
|
|
95
|
+
if (this.$pendingPropertyChanges) {
|
|
96
|
+
this.$authoredPhase |= AUTHORED_PENDING;
|
|
97
|
+
this.$flushAuthoredChanges();
|
|
98
|
+
}
|
|
99
|
+
return !this.$pendingPropertyChanges;
|
|
100
|
+
}
|
|
101
|
+
$flushUpdates() {
|
|
102
|
+
if (this.$pendingPropertyChanges)
|
|
103
|
+
this.$notifyHydrated();
|
|
104
|
+
super.$flushUpdates();
|
|
105
|
+
}
|
|
106
|
+
$flushAuthoredChanges() {
|
|
107
|
+
const changes = this.$pendingPropertyChanges;
|
|
108
|
+
if (!changes || !this.$canRunAuthoredEffects())
|
|
109
|
+
return;
|
|
110
|
+
this.$authoredPhase = AUTHORED_LIVE | AUTHORED_PENDING | AUTHORED_FLUSHING;
|
|
111
|
+
try {
|
|
112
|
+
for (const [name, oldValue] of changes) {
|
|
113
|
+
if (!this.$canRunAuthoredEffects()) {
|
|
114
|
+
this.$authoredPhase &= ~AUTHORED_LIVE;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
changes.delete(name);
|
|
118
|
+
this.$invokePropertyChange(name, oldValue);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
this.$authoredPhase &= ~AUTHORED_FLUSHING;
|
|
123
|
+
this.$releasePropertyChanges();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
$invokePropertyChange(name, oldValue) {
|
|
127
|
+
const value = this[name];
|
|
128
|
+
if (Object.is(oldValue, value))
|
|
129
|
+
return;
|
|
130
|
+
const callback = this[`${name}Changed`];
|
|
131
|
+
if (typeof callback === 'function') {
|
|
132
|
+
callback.call(this, oldValue, value);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
$releasePropertyChanges() {
|
|
136
|
+
const phase = this.$authoredPhase;
|
|
137
|
+
if (phase !== undefined && this.$pendingPropertyChanges?.size === 0 &&
|
|
138
|
+
!(phase & AUTHORED_FLUSHING)) {
|
|
139
|
+
this.$pendingPropertyChanges = undefined;
|
|
140
|
+
this.$authoredPhase = phase & ~AUTHORED_PENDING;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
25
143
|
$shouldDeferSSRHydration(meta) {
|
|
26
144
|
this.$lazyHydrationMode = this.$resolveLazyHydrationMode(meta);
|
|
27
145
|
return this.$lazyHydrationMode !== undefined;
|
|
@@ -90,6 +208,8 @@ export class WebUIElement extends TemplateElement {
|
|
|
90
208
|
this.$activateDeferredSSR(state);
|
|
91
209
|
}
|
|
92
210
|
disconnectedCallback() {
|
|
211
|
+
if (this.$authoredPhase !== undefined)
|
|
212
|
+
this.$authoredPhase &= ~AUTHORED_LIVE;
|
|
93
213
|
if (this.$lazyHydrationMode !== undefined) {
|
|
94
214
|
disconnectLazyHydration(this);
|
|
95
215
|
}
|
|
@@ -103,7 +223,8 @@ export class WebUIElement extends TemplateElement {
|
|
|
103
223
|
return attribute === undefined || !this.hasAttribute(attribute);
|
|
104
224
|
}
|
|
105
225
|
$syncAuthoredAttributes() {
|
|
106
|
-
|
|
226
|
+
if (this.isConnected)
|
|
227
|
+
syncAttrProperties(this, this.constructor);
|
|
107
228
|
}
|
|
108
229
|
$emit(name, detail) {
|
|
109
230
|
return this.dispatchEvent(new CustomEvent(name, {
|
|
@@ -19,7 +19,7 @@ export declare class TemplateElement extends HTMLElement {
|
|
|
19
19
|
private $hydrated;
|
|
20
20
|
private $deferredClientMount;
|
|
21
21
|
private $resetClientShadow;
|
|
22
|
-
private $
|
|
22
|
+
private $mountPhase;
|
|
23
23
|
private $deferredSSR;
|
|
24
24
|
private $activatingDeferredSSR;
|
|
25
25
|
private $deferredAncestor;
|
|
@@ -78,7 +78,7 @@ export declare class TemplateElement extends HTMLElement {
|
|
|
78
78
|
protected $shouldApplySSRState(_key: string): boolean;
|
|
79
79
|
protected $syncAuthoredAttributes(): void;
|
|
80
80
|
protected hydratedCallback(): void;
|
|
81
|
-
|
|
81
|
+
protected $notifyHydrated(): void;
|
|
82
82
|
private $finishHydration;
|
|
83
83
|
protected $shouldApplyTemplateStateFromSSR(_key: string): boolean;
|
|
84
84
|
protected $setTemplateState(key: string, value: unknown): boolean;
|
|
@@ -93,6 +93,8 @@ export declare class TemplateElement extends HTMLElement {
|
|
|
93
93
|
private $recordDeferredWrite;
|
|
94
94
|
private $replayDeferredWrites;
|
|
95
95
|
$flushUpdates(): void;
|
|
96
|
+
protected $reconcileAuthoredState(): boolean;
|
|
97
|
+
protected $canRunAuthoredEffects(): boolean;
|
|
96
98
|
private $flush;
|
|
97
99
|
private $recordPreReadyWrite;
|
|
98
100
|
private $checkHydrationMismatch;
|
package/dist/template-element.js
CHANGED
|
@@ -204,6 +204,8 @@ function hasAuthoredMember(instance, key) {
|
|
|
204
204
|
}
|
|
205
205
|
return false;
|
|
206
206
|
}
|
|
207
|
+
const MOUNTED = 1;
|
|
208
|
+
const HYDRATION_NOTIFIED = 2;
|
|
207
209
|
export class TemplateElement extends HTMLElement {
|
|
208
210
|
constructor() {
|
|
209
211
|
super(...arguments);
|
|
@@ -212,7 +214,7 @@ export class TemplateElement extends HTMLElement {
|
|
|
212
214
|
this.$hydrated = false;
|
|
213
215
|
this.$deferredClientMount = false;
|
|
214
216
|
this.$resetClientShadow = false;
|
|
215
|
-
this.$
|
|
217
|
+
this.$mountPhase = 0;
|
|
216
218
|
this.$deferredSSR = false;
|
|
217
219
|
this.$activatingDeferredSSR = false;
|
|
218
220
|
this.$hasUnknownScopes = false;
|
|
@@ -322,6 +324,8 @@ export class TemplateElement extends HTMLElement {
|
|
|
322
324
|
hydrationStart();
|
|
323
325
|
try {
|
|
324
326
|
this.$ready = true;
|
|
327
|
+
this.$syncAuthoredAttributes();
|
|
328
|
+
this.$notifyHydrated();
|
|
325
329
|
this.$update();
|
|
326
330
|
}
|
|
327
331
|
finally {
|
|
@@ -329,7 +333,7 @@ export class TemplateElement extends HTMLElement {
|
|
|
329
333
|
}
|
|
330
334
|
return;
|
|
331
335
|
}
|
|
332
|
-
if (!this.$
|
|
336
|
+
if (!this.$mountPhase &&
|
|
333
337
|
isStreamingHydrationMode() &&
|
|
334
338
|
this.hasAttribute(STREAMED_HOST_ATTR)) {
|
|
335
339
|
this.$deferredSSR = true;
|
|
@@ -346,7 +350,7 @@ export class TemplateElement extends HTMLElement {
|
|
|
346
350
|
return;
|
|
347
351
|
}
|
|
348
352
|
this.$meta = meta;
|
|
349
|
-
if (this.$
|
|
353
|
+
if (this.$mountPhase) {
|
|
350
354
|
this.$remount(meta);
|
|
351
355
|
}
|
|
352
356
|
else {
|
|
@@ -856,10 +860,14 @@ export class TemplateElement extends HTMLElement {
|
|
|
856
860
|
hydratedCallback() {
|
|
857
861
|
}
|
|
858
862
|
$notifyHydrated() {
|
|
859
|
-
if (this.$
|
|
863
|
+
if (!this.$mountPhase)
|
|
864
|
+
this.$mountPhase = MOUNTED;
|
|
865
|
+
if (!this.$reconcileAuthoredState())
|
|
860
866
|
return;
|
|
861
|
-
this.$
|
|
862
|
-
|
|
867
|
+
if (this.$mountPhase === MOUNTED) {
|
|
868
|
+
this.$mountPhase = HYDRATION_NOTIFIED;
|
|
869
|
+
this.hydratedCallback();
|
|
870
|
+
}
|
|
863
871
|
}
|
|
864
872
|
$finishHydration() {
|
|
865
873
|
let callbackFailed = false;
|
|
@@ -1031,9 +1039,18 @@ export class TemplateElement extends HTMLElement {
|
|
|
1031
1039
|
this.$flush(true);
|
|
1032
1040
|
}
|
|
1033
1041
|
$flushUpdates() {
|
|
1042
|
+
if (this.$mountPhase === MOUNTED)
|
|
1043
|
+
this.$notifyHydrated();
|
|
1034
1044
|
if (this.$pendingFlush)
|
|
1035
1045
|
this.$flush();
|
|
1036
1046
|
}
|
|
1047
|
+
$reconcileAuthoredState() {
|
|
1048
|
+
return true;
|
|
1049
|
+
}
|
|
1050
|
+
$canRunAuthoredEffects() {
|
|
1051
|
+
return this.$hydrated && this.$root !== null && this.$ready &&
|
|
1052
|
+
!this.$deferredClientMount && this.isConnected;
|
|
1053
|
+
}
|
|
1037
1054
|
$flush(requireKnownState = this.$guardUnknownState === true) {
|
|
1038
1055
|
if (!this.$ready || !this.$root) {
|
|
1039
1056
|
this.$dirtyPaths = null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@microsoft/webui-framework",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.30",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"tag": "latest"
|
|
6
6
|
},
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dist/"
|
|
44
44
|
],
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@microsoft/webui-test-support": "0.0.
|
|
46
|
+
"@microsoft/webui-test-support": "0.0.30",
|
|
47
47
|
"@playwright/test": "^1.63.0",
|
|
48
48
|
"@types/node": "^26.5.1",
|
|
49
49
|
"typescript": "7.0.2"
|