@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 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 [RENDERING.md](./RENDERING.md).
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-press build` sets it to
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
 
@@ -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
- if (attrDefinition && this['$ready'] === true) {
176
- reflectPropertyToAttribute(this, attrDefinition, newValue);
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 cb = this[changedKey];
179
- if (typeof cb === 'function') {
180
- cb.call(this, oldValue, newValue);
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[definition.property] = definition.boolean
238
- ? newVal !== null
239
- : newVal;
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
- syncAttrProperties(this, this.constructor);
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 $hasMounted;
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
- private $notifyHydrated;
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;
@@ -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.$hasMounted = false;
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.$hasMounted &&
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.$hasMounted) {
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.$hasMounted)
863
+ if (!this.$mountPhase)
864
+ this.$mountPhase = MOUNTED;
865
+ if (!this.$reconcileAuthoredState())
860
866
  return;
861
- this.$hasMounted = true;
862
- this.hydratedCallback();
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.29",
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.29",
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"