@microsoft/webui-framework 0.0.28 → 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/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, {
package/dist/lifecycle.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isStreamingHydrationMode } from './streaming-mode.js';
1
2
  let pendingCount = 0;
2
3
  let started = false;
3
4
  let completed = false;
@@ -74,6 +75,9 @@ function tryComplete() {
74
75
  streamingGateAborted ||
75
76
  pendingCount !== 0)
76
77
  return;
78
+ if (!streamingGateActive && hasBrowserDocument && isStreamingHydrationMode()) {
79
+ streamingGateActive = true;
80
+ }
77
81
  if (!started && !streamingGateActive)
78
82
  return;
79
83
  if (streamingGateActive &&
@@ -1,6 +1,6 @@
1
1
  import { consumePendingParentState, TemplateElement, } from './template-element.js';
2
2
  import { hasComponentStyleWork } from './element/styles.js';
3
- import { getTemplateRegistry } from './template.js';
3
+ import { getTemplateRegistry, installTemplateDefinitionPreparation, } from './template.js';
4
4
  import { templateNeedsStaticHost } from './template-roots.js';
5
5
  import { ACTIVATION_STATIC_HOST_OPT_OUT, isStreamingHydrationMode, STREAMED_HOST_ATTR, STREAMING_BOUNDARY_ACTIVATE, } from './streaming-mode.js';
6
6
  import { TEMPLATES_REGISTERED_EVENT, templateRegistrationDetail, } from './template-events.js';
@@ -97,6 +97,7 @@ export function installTemplateElementRuntime() {
97
97
  if (typeof window === 'undefined' || typeof document === 'undefined')
98
98
  return;
99
99
  runtimeInstalled = true;
100
+ installTemplateDefinitionPreparation(null);
100
101
  window.addEventListener(TEMPLATES_REGISTERED_EVENT, (event) => {
101
102
  const detail = templateRegistrationDetail(event);
102
103
  if (!detail?.templates)
@@ -1,5 +1,5 @@
1
- import { registerTemplateData } from './template.js';
2
- import { registerComponentStyles } from './element/styles.js';
1
+ import { registerTemplateData } from './template-registry.js';
2
+ import { registerComponentStyles } from './element/style-catalog.js';
3
3
  export function applyBoundaryBootstrap(bootstrap) {
4
4
  registerComponentStyles(bootstrap.componentStyles);
5
5
  if (bootstrap.templates)
@@ -1,8 +1,10 @@
1
1
  import { markLateActivationPending, settleLateActivation, } from './lifecycle.js';
2
2
  import { abandonDeferredDescendants, abandonDeferredElement, removeStreamingAttributes, } from './streaming-cleanup.js';
3
3
  import { firstNodeWithin, MAX_ELEMENTS_PER_BOUNDARY, MAX_MARKER_SCAN_NODES, nextAfterSubtreeWithin, nextWithinRoot, STREAMING_ENCLOSING_SPAN_ATTR, streamingErrorMessage, } from './streaming-dom.js';
4
- import { ACTIVATION_ACTIVATED, ACTIVATION_ANCESTOR_BARRIER, ACTIVATION_MISSING_TEMPLATE, ACTIVATION_STATIC_HOST_OPT_OUT, PENDING_ROOT_CONNECTED, STREAMED_HOST_ATTR, STREAMING_BOUNDARY_ACTIVATE, } from './streaming-mode.js';
4
+ import { ACTIVATION_ACTIVATED, ACTIVATION_ANCESTOR_BARRIER, ACTIVATION_MISSING_TEMPLATE, ACTIVATION_STATIC_HOST_OPT_OUT, registerStreamingRootResume, STREAMED_HOST_ATTR, STREAMING_BOUNDARY_ACTIVATE, } from './streaming-mode.js';
5
5
  import { applyStateUpdate } from './streaming-state.js';
6
+ import { templateNeedsStaticHost } from './template-roots.js';
7
+ import { invalidateTemplateHostRuntime, loadTemplateHostRuntime, resetTemplateHostRuntimeForTests, } from './streaming-template-hosts.js';
6
8
  export const ELEMENT_IGNORED = 10;
7
9
  export const ELEMENT_DEFERRED = 11;
8
10
  export const ELEMENT_LIMIT_FAILURE = 12;
@@ -20,9 +22,11 @@ const pendingTagWaiters = new Map();
20
22
  const pendingBarrierRoots = new Set();
21
23
  let pendingUndefinedRoots = 0;
22
24
  let activationGeneration = 0;
25
+ let templateHostRuntimeRequested = false;
23
26
  let failureHandler = null;
24
27
  let invalidActivationOutcome;
25
- const PENDING_RECORD = Symbol();
28
+ let pendingRootRecords;
29
+ registerStreamingRootResume(resumeRetainedElement);
26
30
  export function configureStreamingFailureHandler(handler) {
27
31
  failureHandler = handler;
28
32
  }
@@ -89,23 +93,45 @@ function activateMarkedElement(el, state, updates, bypass) {
89
93
  customElements
90
94
  .whenDefined(tag)
91
95
  .then(() => onTagDefined(tag, generation));
96
+ requestTemplateHostRuntime(tag, generation);
92
97
  }
93
98
  if (!waiter.roots.has(el)) {
94
99
  waiter.roots.add(el);
95
100
  pendingUndefinedRoots++;
96
101
  }
97
- el[PENDING_ROOT_CONNECTED] = resumePendingRoot;
98
102
  return ELEMENT_DEFERRED;
99
103
  }
104
+ function resumeRetainedElement(el) {
105
+ if (!hasPendingRecord(el))
106
+ return false;
107
+ if (pendingBarrierRoots.has(el))
108
+ resumeBarrierRoot.call(el);
109
+ else
110
+ resumePendingRoot.call(el);
111
+ return true;
112
+ }
113
+ function requestTemplateHostRuntime(tag, generation) {
114
+ if (templateHostRuntimeRequested)
115
+ return;
116
+ const meta = window.__webui?.templates?.[tag];
117
+ if (!meta || !templateNeedsStaticHost(meta))
118
+ return;
119
+ if (window.__webui?.templateHostExclusions?.has(tag))
120
+ return;
121
+ templateHostRuntimeRequested = true;
122
+ void loadTemplateHostRuntime()?.catch((error) => {
123
+ if (generation === activationGeneration) {
124
+ fail(`failed to load template-host runtime: ${streamingErrorMessage(error)}`);
125
+ }
126
+ });
127
+ }
100
128
  function deferBehindBarrier(el, state, updates, bypass) {
101
129
  stashPendingRecord(el, state, updates, bypass);
102
130
  pendingBarrierRoots.add(el);
103
- el[PENDING_ROOT_CONNECTED] = resumeBarrierRoot;
104
131
  }
105
132
  function activatePendingBarrierRoot(el) {
106
133
  if (!pendingBarrierRoots.delete(el))
107
134
  return ELEMENT_IGNORED;
108
- delete el[PENDING_ROOT_CONNECTED];
109
135
  const record = takePendingRecord(el);
110
136
  const updates = releaseUpdates(record);
111
137
  try {
@@ -180,7 +206,6 @@ function resumePendingRoot() {
180
206
  if (!waiter ||
181
207
  waiter.generation !== activationGeneration ||
182
208
  !waiter.roots.has(this)) {
183
- delete this[PENDING_ROOT_CONNECTED];
184
209
  return;
185
210
  }
186
211
  activatePendingRoot(tag, waiter, this);
@@ -189,7 +214,6 @@ function activatePendingRoot(tag, waiter, el) {
189
214
  if (!waiter.roots.delete(el))
190
215
  return;
191
216
  pendingUndefinedRoots--;
192
- delete el[PENDING_ROOT_CONNECTED];
193
217
  const record = takePendingRecord(el);
194
218
  const updates = releaseUpdates(record);
195
219
  const state = record?.state;
@@ -309,6 +333,8 @@ function abandonDeferredTree(el) {
309
333
  abandonDeferredElement(el);
310
334
  }
311
335
  export function abandonPendingWaiters() {
336
+ activationGeneration++;
337
+ invalidateTemplateHostRuntime();
312
338
  if (pendingBarrierRoots.size !== 0) {
313
339
  for (const el of pendingBarrierRoots)
314
340
  clearPendingRoot(el);
@@ -323,28 +349,30 @@ export function abandonPendingWaiters() {
323
349
  pendingTagWaiters.clear();
324
350
  }
325
351
  pendingUndefinedRoots = 0;
352
+ pendingRootRecords = undefined;
326
353
  }
327
354
  function clearPendingRoot(el) {
328
355
  releaseUpdates(takePendingRecord(el));
329
- delete el[PENDING_ROOT_CONNECTED];
330
356
  abandonDeferredTree(el);
331
357
  }
332
358
  function stashPendingRecord(el, state, updates, bypass) {
333
- el[PENDING_RECORD] = {
359
+ (pendingRootRecords ??= new WeakMap()).set(el, {
334
360
  state,
335
361
  updates,
336
362
  bypass,
337
- };
363
+ });
338
364
  if (updates)
339
365
  updates.pendingRoots++;
340
366
  }
341
367
  function hasPendingRecord(el) {
342
- return Object.prototype.hasOwnProperty.call(el, PENDING_RECORD);
368
+ return pendingRootRecords?.has(el) ?? false;
343
369
  }
344
370
  function takePendingRecord(el) {
345
- const store = el;
346
- const record = store[PENDING_RECORD];
347
- delete store[PENDING_RECORD];
371
+ const record = pendingRootRecords?.get(el);
372
+ pendingRootRecords?.delete(el);
373
+ if (pendingUndefinedRoots === 0 && pendingBarrierRoots.size === 0) {
374
+ pendingRootRecords = undefined;
375
+ }
348
376
  return record;
349
377
  }
350
378
  function releaseUpdates(record) {
@@ -384,7 +412,8 @@ function invokeActivationHook(el, state, bypass) {
384
412
  }
385
413
  export function resetDeferredActivationForTests() {
386
414
  abandonPendingWaiters();
387
- activationGeneration++;
415
+ templateHostRuntimeRequested = false;
416
+ resetTemplateHostRuntimeForTests();
388
417
  }
389
418
  export function pendingTagWaiterCountForTests() {
390
419
  return pendingTagWaiters.size;
@@ -1,7 +1,8 @@
1
1
  import { beginStreamingGate } from './lifecycle.js';
2
- import { installTemplateElementRuntime } from './static-host.js';
3
2
  import { enqueueStreamingSentinel, installStreamingTruncationGuard, } from './streaming-coordinator.js';
4
3
  import { isStreamingHydrationMode } from './streaming-mode.js';
4
+ import { installTemplateDefinitionPreparation } from './template-registry.js';
5
+ import { prepareTemplateDefinitions } from './streaming-template-hosts.js';
5
6
  const SENTINEL_TAG = 'webui-hydrate';
6
7
  let installed = false;
7
8
  class WebUiHydrateSentinel extends HTMLElement {
@@ -13,12 +14,12 @@ export function installStreamingCoordinator() {
13
14
  if (installed || !isStreamingHydrationMode())
14
15
  return;
15
16
  installed = true;
17
+ installTemplateDefinitionPreparation(prepareTemplateDefinitions);
16
18
  if (!customElements.get(SENTINEL_TAG)) {
17
19
  customElements.define(SENTINEL_TAG, WebUiHydrateSentinel);
18
20
  }
19
21
  beginStreamingGate();
20
22
  installStreamingTruncationGuard();
21
- installTemplateElementRuntime();
22
23
  }
23
24
  export function resetStreamingInstallForTests() {
24
25
  installed = false;
@@ -1,5 +1,6 @@
1
1
  export declare const STREAMING_BOUNDARY_ACTIVATE: unique symbol;
2
- export declare const PENDING_ROOT_CONNECTED: unique symbol;
2
+ export declare let resumeStreamingRoot: ((element: Element) => boolean) | undefined;
3
+ export declare function registerStreamingRootResume(resume: typeof resumeStreamingRoot): void;
3
4
  export declare const STREAMED_HOST_ATTR = "data-ws";
4
5
  export declare const ACTIVATION_ACTIVATED = 1;
5
6
  export declare const ACTIVATION_STATIC_HOST_OPT_OUT = 2;
@@ -1,6 +1,9 @@
1
1
  let cached;
2
2
  export const STREAMING_BOUNDARY_ACTIVATE = Symbol.for('microsoft.webui.boundaryActivate');
3
- export const PENDING_ROOT_CONNECTED = Symbol.for('microsoft.webui.pendingRootConnected');
3
+ export let resumeStreamingRoot;
4
+ export function registerStreamingRootResume(resume) {
5
+ resumeStreamingRoot = resume;
6
+ }
4
7
  export const STREAMED_HOST_ATTR = 'data-ws';
5
8
  export const ACTIVATION_ACTIVATED = 1;
6
9
  export const ACTIVATION_STATIC_HOST_OPT_OUT = 2;
@@ -0,0 +1,5 @@
1
+ import type { TemplateMeta } from './template-types.js';
2
+ export declare function loadTemplateHostRuntime(): Promise<void> | undefined;
3
+ export declare function prepareTemplateDefinitions(templates: Record<string, TemplateMeta>, names: readonly string[]): Promise<void> | undefined;
4
+ export declare function invalidateTemplateHostRuntime(): void;
5
+ export declare function resetTemplateHostRuntimeForTests(): void;
@@ -0,0 +1,39 @@
1
+ import { templateNeedsStaticHost } from './template-roots.js';
2
+ let generation = 0;
3
+ let installed = false;
4
+ let pending;
5
+ export function loadTemplateHostRuntime() {
6
+ if (installed)
7
+ return;
8
+ if (!pending) {
9
+ const current = generation;
10
+ pending = import('./static-host.js').then(({ installTemplateElementRuntime }) => {
11
+ if (current !== generation) {
12
+ throw new Error('[WebUI] Template-host activation was abandoned.');
13
+ }
14
+ installTemplateElementRuntime();
15
+ installed = true;
16
+ pending = undefined;
17
+ });
18
+ }
19
+ return pending;
20
+ }
21
+ export function prepareTemplateDefinitions(templates, names) {
22
+ const exclusions = window.__webui?.templateHostExclusions;
23
+ for (let i = 0; i < names.length; i++) {
24
+ const tag = names[i];
25
+ if (templateNeedsStaticHost(templates[tag]) &&
26
+ !exclusions?.has(tag) &&
27
+ !customElements.get(tag)) {
28
+ return loadTemplateHostRuntime();
29
+ }
30
+ }
31
+ }
32
+ export function invalidateTemplateHostRuntime() {
33
+ generation++;
34
+ pending = undefined;
35
+ }
36
+ export function resetTemplateHostRuntimeForTests() {
37
+ invalidateTemplateHostRuntime();
38
+ installed = false;
39
+ }
@@ -13,5 +13,6 @@ export interface TemplateStylesheetDescriptor {
13
13
  readonly type: string;
14
14
  }
15
15
  export declare function cloneTemplateContent(meta: TemplateBlockMeta): DocumentFragment;
16
+ export declare function getTemplateFragment(meta: TemplateBlockMeta): DocumentFragment;
16
17
  export declare function getTemplateStylesheets(meta: TemplateBlockMeta): readonly TemplateStylesheetDescriptor[] | undefined;
17
18
  export declare function templateHtmlMayContainLink(html: string): boolean;
@@ -1,8 +1,12 @@
1
+ import { setTemplateContent } from './trusted-types-policy.js';
1
2
  const EMPTY_STYLESHEETS = Object.freeze([]);
2
3
  const NO_LINK_STYLESHEETS = Object.freeze([]);
3
4
  const templateContentCache = new WeakMap();
4
5
  export function cloneTemplateContent(meta) {
5
- return getTemplateContent(meta).fragment.cloneNode(true);
6
+ return getTemplateFragment(meta).cloneNode(true);
7
+ }
8
+ export function getTemplateFragment(meta) {
9
+ return getTemplateContent(meta).fragment;
6
10
  }
7
11
  export function getTemplateStylesheets(meta) {
8
12
  const stylesheets = getTemplateContent(meta, true).stylesheets;
@@ -37,7 +41,7 @@ function getTemplateContent(meta, mayContainLink) {
37
41
  if (cached)
38
42
  return cached;
39
43
  const template = document.createElement('template');
40
- template.innerHTML = meta.h;
44
+ setTemplateContent(template, meta);
41
45
  const fragment = template.content;
42
46
  const stylesheets = (mayContainLink ?? templateHtmlMayContainLink(meta.h))
43
47
  ? collectStylesheetDescriptors(fragment)
@@ -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;