@microsoft/webui-framework 0.0.16 → 0.0.18
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 +112 -33
- package/dist/component-asset.d.ts +30 -0
- package/dist/component-asset.js +213 -0
- package/dist/decorators.d.ts +2 -53
- package/dist/decorators.js +93 -171
- package/dist/element/diff.d.ts +0 -20
- package/dist/element/diff.js +25 -62
- package/dist/element/markers.d.ts +0 -44
- package/dist/element/markers.js +5 -54
- package/dist/element/styles.d.ts +0 -10
- package/dist/element/styles.js +2 -63
- package/dist/element/types.d.ts +4 -39
- package/dist/element/types.js +0 -3
- package/dist/element.d.ts +13 -131
- package/dist/element.js +101 -1355
- package/dist/hydration-mismatch.d.ts +22 -0
- package/dist/hydration-mismatch.js +87 -0
- package/dist/index.d.ts +0 -19
- package/dist/index.js +2 -21
- package/dist/lifecycle.d.ts +0 -8
- package/dist/lifecycle.js +0 -32
- package/dist/static-host.d.ts +1 -0
- package/dist/static-host.js +58 -0
- package/dist/template-element.d.ts +76 -0
- package/dist/template-element.js +1252 -0
- package/dist/template-events.d.ts +6 -0
- package/dist/template-events.js +23 -0
- package/dist/template-roots.d.ts +5 -0
- package/dist/template-roots.js +35 -0
- package/dist/template-types.d.ts +11 -15
- package/dist/template-types.js +0 -2
- package/dist/template.d.ts +2 -18
- package/dist/template.js +9 -2
- package/package.json +11 -5
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@ This package is the browser-side runtime used by `webui build --plugin=webui`. I
|
|
|
6
6
|
|
|
7
7
|
- `WebUIElement` for SSR hydration and client-created elements
|
|
8
8
|
- `@observable`, `@attr`, and `@volatile` decorators
|
|
9
|
-
-
|
|
9
|
+
- direct DOM binding updates
|
|
10
10
|
- light DOM or shadow DOM rendering (`--dom=light|shadow` flag)
|
|
11
|
-
- SSR state seeding
|
|
11
|
+
- SSR state seeding
|
|
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
|
|
|
@@ -87,13 +87,33 @@ Build with `--dom=shadow` (default) to wrap in a declarative shadow root, or `--
|
|
|
87
87
|
<counter-card label="Taps"></counter-card>
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
### HTML-only components
|
|
91
|
+
|
|
92
|
+
If a component has no event handlers, custom lifecycle code, or client-only
|
|
93
|
+
methods, it can ship only `component.html` and optional `component.css`.
|
|
94
|
+
|
|
95
|
+
When HTML-only components receive server or route state, import
|
|
96
|
+
`@microsoft/webui-framework` somewhere in the browser entry. The framework root
|
|
97
|
+
installs the static host runtime, which only claims compiler-owned HTML-only
|
|
98
|
+
components whose templates need hidden state or observed host attributes. Fully
|
|
99
|
+
static HTML-only components stay as plain SSR DOM.
|
|
100
|
+
|
|
101
|
+
Create a custom element only for an Interactive Island: event handlers, custom
|
|
102
|
+
lifecycle code, imperative methods, or state that TypeScript code reads or
|
|
103
|
+
mutates. `@observable` and `@attr` are optional; add them when JavaScript needs
|
|
104
|
+
to access the value or when the value is part of the component's public API.
|
|
105
|
+
|
|
90
106
|
### Build with the WebUI plugin
|
|
91
107
|
|
|
92
108
|
```bash
|
|
93
109
|
cargo run -p microsoft-webui-cli -- build ./src --out ./dist --plugin=webui
|
|
94
110
|
```
|
|
95
111
|
|
|
96
|
-
The
|
|
112
|
+
The WebUI plugin prepares component templates for the browser. Bundle your
|
|
113
|
+
source browser entry directly. Import `@microsoft/webui-framework` from authored
|
|
114
|
+
component modules, or once from the browser entry when the app has no authored
|
|
115
|
+
components but still uses HTML-only components that receive server or route
|
|
116
|
+
state.
|
|
97
117
|
|
|
98
118
|
### Property binding lifecycle
|
|
99
119
|
|
|
@@ -103,7 +123,22 @@ Property bindings use the `:` prefix to pass values directly to child DOM proper
|
|
|
103
123
|
<profile-card :config="{{settings}}"></profile-card>
|
|
104
124
|
```
|
|
105
125
|
|
|
106
|
-
For client-created component trees,
|
|
126
|
+
For client-created component trees, WebUI applies initial property bindings
|
|
127
|
+
before child `connectedCallback` methods run. A child can read an initial
|
|
128
|
+
parent-provided property in `connectedCallback`. If the parent value is not set,
|
|
129
|
+
the child may initialize its own fallback there, and later parent updates still
|
|
130
|
+
flow through the live binding.
|
|
131
|
+
|
|
132
|
+
During SSR hydration the framework trusts the server-rendered DOM and does not
|
|
133
|
+
re-render it. An `@observable` written before hydration finishes — in a field
|
|
134
|
+
initializer, the `constructor`, or before `super.connectedCallback()` — cannot
|
|
135
|
+
update that DOM, so the write is dropped and the runtime logs a
|
|
136
|
+
`[WebUI] Hydration mismatch` warning naming the properties. Seed such values in
|
|
137
|
+
the SSR state, or assign them after `super.connectedCallback()`. The warning is
|
|
138
|
+
development-only and is dead-code-eliminated from production bundles via the
|
|
139
|
+
`__WEBUI_DEV__` compile-time flag (on by default; `webui-press build` sets it to
|
|
140
|
+
`false`). See the
|
|
141
|
+
[Interactivity Guide](https://microsoft.github.io/webui/guide/concepts/interactivity#setting-observable-state-during-setup).
|
|
107
142
|
|
|
108
143
|
### DOM strategy (`--dom`)
|
|
109
144
|
|
|
@@ -135,15 +170,50 @@ Base class for framework components.
|
|
|
135
170
|
| `static define(tagName)` | Register the class as a custom element |
|
|
136
171
|
| `$emit(name, detail?)` | Dispatch a bubbling, composed `CustomEvent` |
|
|
137
172
|
| `$update()` | Force a reactive update (normally called automatically) |
|
|
138
|
-
| `setState(state)` | Populate `@observable` properties from router/server state |
|
|
139
173
|
| `disconnectedCallback()` | Override for cleanup (global listeners, etc.) |
|
|
140
174
|
|
|
141
175
|
In most components you do not call `$update()` directly. Property changes through `@observable` and `@attr` trigger updates for you.
|
|
142
176
|
|
|
177
|
+
### Static component assets
|
|
178
|
+
|
|
179
|
+
`webui build --plugin=webui --emit-component-assets settings-dialog` emits
|
|
180
|
+
`settings-dialog.webui.js` next to `protocol.bin`. Load the ESM asset before
|
|
181
|
+
creating the component when you are not using `@microsoft/webui-router`:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { settingsAssets } from './lazy-assets.js';
|
|
185
|
+
|
|
186
|
+
settingsAssets.preload('settings-dialog');
|
|
187
|
+
panelSlot.replaceChildren(await settingsAssets.create('settings-dialog'));
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
// lazy-assets.ts
|
|
192
|
+
import { defineComponentAssets } from '@microsoft/webui-framework/component-asset.js';
|
|
193
|
+
|
|
194
|
+
export const settingsAssets = defineComponentAssets({
|
|
195
|
+
'settings-dialog': {
|
|
196
|
+
asset: '/settings-dialog.webui.js',
|
|
197
|
+
module: () => import('./settings-dialog/settings-dialog.js'),
|
|
198
|
+
data: async () => await (await fetch('/settings-dialog-data.json')).json(),
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The asset module carries the component's template and style payload. Use
|
|
204
|
+
`preload(tag)` to start template, module, and optional data work early, then
|
|
205
|
+
`create(tag)` to create the element after template/module work is ready.
|
|
206
|
+
Concurrent asset requests share one in-flight load and CSS module styles are
|
|
207
|
+
deduped. `create(tag)` does not block on optional data by default. Use
|
|
208
|
+
`create(tag, { awaitData: true, dataTimeoutMs: 150 })` only when a component must
|
|
209
|
+
wait briefly for state before mounting.
|
|
210
|
+
|
|
143
211
|
### `@observable`
|
|
144
212
|
|
|
145
|
-
Marks a property as reactive.
|
|
146
|
-
|
|
213
|
+
Marks a property as reactive. When the value changes, the framework
|
|
214
|
+
updates template bindings that reference it. Use it for state that TypeScript
|
|
215
|
+
code reads or mutates. Values used only by the template do not need an
|
|
216
|
+
`@observable` class field.
|
|
147
217
|
|
|
148
218
|
```ts
|
|
149
219
|
class SearchPanel extends WebUIElement {
|
|
@@ -170,7 +240,7 @@ Notes:
|
|
|
170
240
|
|
|
171
241
|
- default attribute names use kebab-case
|
|
172
242
|
- attribute values arrive as strings
|
|
173
|
-
- use `@observable` for
|
|
243
|
+
- use `@observable` for state that client code reads or mutates
|
|
174
244
|
|
|
175
245
|
### `@volatile`
|
|
176
246
|
|
|
@@ -189,7 +259,7 @@ class CartSummary extends WebUIElement {
|
|
|
189
259
|
|
|
190
260
|
## Template Features
|
|
191
261
|
|
|
192
|
-
The WebUI plugin
|
|
262
|
+
The WebUI plugin supports these template features:
|
|
193
263
|
|
|
194
264
|
- text bindings: `{{title}}`
|
|
195
265
|
- attribute bindings: `href="{{item.href}}"`
|
|
@@ -198,6 +268,9 @@ The WebUI plugin compiles these template features into runtime metadata:
|
|
|
198
268
|
- conditionals: `<if condition="...">`
|
|
199
269
|
- repeats: `<for each="item in items">`
|
|
200
270
|
|
|
271
|
+
Components that use `@event` must have authored `.ts` or `.js` code that
|
|
272
|
+
defines a `WebUIElement` for the tag; HTML-only components are declarative only.
|
|
273
|
+
|
|
201
274
|
Example from `examples/app/todo-webui`:
|
|
202
275
|
|
|
203
276
|
```html
|
|
@@ -222,11 +295,15 @@ Root-level events (e.g. `@toggle-item="{onToggleItem(e)}"`) can be declared on t
|
|
|
222
295
|
|
|
223
296
|
## Recommended Patterns
|
|
224
297
|
|
|
225
|
-
- Treat decorated properties as the source of truth
|
|
298
|
+
- Treat decorated properties as the source of truth for state used by
|
|
299
|
+
TypeScript code.
|
|
226
300
|
- Update state with property assignments such as `this.open = !this.open`.
|
|
227
301
|
- Use `$emit()` for child-to-parent communication.
|
|
228
302
|
- Use `w-ref` for true DOM-only concerns like focus or reading input values.
|
|
229
|
-
-
|
|
303
|
+
- Omit `@observable` for values that are only read by the template and seeded
|
|
304
|
+
externally after construction.
|
|
305
|
+
- Omit the TypeScript class for HTML-only components that only need template
|
|
306
|
+
bindings and router/server state.
|
|
230
307
|
|
|
231
308
|
Avoid imperative DOM mutation for application state that can be represented by reactive properties.
|
|
232
309
|
|
|
@@ -407,7 +484,7 @@ sequenceDiagram
|
|
|
407
484
|
CE->>CE: attributeChangedCallback (pre-existing attrs)
|
|
408
485
|
CE->>FW: connectedCallback() → $mount()
|
|
409
486
|
FW->>FW: SSR DOM detected (shadow root or children exist)
|
|
410
|
-
FW->>FW: $applySSRState() — seed
|
|
487
|
+
FW->>FW: $applySSRState() — seed decorated + template state
|
|
411
488
|
FW->>FW: $hydrate() — template-parallel path resolution
|
|
412
489
|
FW->>FW: $resolveSSR() — match SSR nodes via ordinal traversal
|
|
413
490
|
FW->>FW: $wireEvents() + $wireRefs()
|
|
@@ -454,15 +531,14 @@ interface TemplateMeta {
|
|
|
454
531
|
tx?: [slot, parts][]; // Text run locators
|
|
455
532
|
a?: CompiledAttrMeta[]; // Attribute bindings
|
|
456
533
|
ag?: [path, start, count][]; // Attribute target groups
|
|
457
|
-
c?: [conditionAST, blockIndex][];
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
rl?: SlotPath[]; // Repeat anchor slots
|
|
461
|
-
e?: [event, handler, argSpecs, targetPath][]; // Events
|
|
534
|
+
c?: [conditionAST, blockIndex, slot][]; // Conditional blocks
|
|
535
|
+
r?: [collection, itemVar, blockIdx, slot][]; // Repeat blocks
|
|
536
|
+
eg?: [event, [[handler, argSpecs, targetPath, usesEvent?]]][]; // Events
|
|
462
537
|
b?: TemplateBlockMeta[]; // Nested block metadata
|
|
463
538
|
sa?: string; // Adopted stylesheet specifier
|
|
464
539
|
sd?: boolean; // Shadow DOM flag for client-created
|
|
465
540
|
re?: [event, handler, argSpecs][]; // Root-level events
|
|
541
|
+
th?: 1; // Compiler-owned static host
|
|
466
542
|
}
|
|
467
543
|
```
|
|
468
544
|
|
|
@@ -482,7 +558,7 @@ Compiled metadata:
|
|
|
482
558
|
[[[0], 0], [["title"]]], // slot in <h1>, dynamic "title"
|
|
483
559
|
[[[1], 1], ["Count: ", ["count"]]] // slot in <button>, static + dynamic
|
|
484
560
|
],
|
|
485
|
-
|
|
561
|
+
eg: [["click", [["increment", [], [1]]]]] // click -> increment, no event args
|
|
486
562
|
}
|
|
487
563
|
```
|
|
488
564
|
|
|
@@ -523,15 +599,16 @@ sequenceDiagram
|
|
|
523
599
|
### Why Updates Are O(affected)
|
|
524
600
|
|
|
525
601
|
After hydration, every dynamic value in the template is connected to a direct
|
|
526
|
-
DOM node reference stored in a binding array.
|
|
527
|
-
|
|
602
|
+
DOM node reference stored in a binding array. A per-path index maps each
|
|
603
|
+
decorated property or compiled template root to the subset of bindings that
|
|
604
|
+
reference it.
|
|
528
605
|
|
|
529
606
|
When `this.count = 5` fires, the `@observable` setter calls `$update('count')`,
|
|
530
607
|
which looks up `'count'` in the index and only patches the bindings that
|
|
531
608
|
actually depend on `count` — not every binding in the component.
|
|
532
609
|
|
|
533
|
-
Computed/volatile getters
|
|
534
|
-
under a wildcard key and always included in targeted updates.
|
|
610
|
+
Computed/volatile getters and other paths that are not known state roots are
|
|
611
|
+
stored under a wildcard key and always included in targeted updates.
|
|
535
612
|
|
|
536
613
|
```typescript
|
|
537
614
|
// Targeted update (simplified):
|
|
@@ -553,27 +630,29 @@ path index ensures only affected pointers are visited.
|
|
|
553
630
|
|
|
554
631
|
## SSR State Seeding
|
|
555
632
|
|
|
556
|
-
When the server renders `<span>42</span>` for
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
633
|
+
When the server renders `<span>42</span>` for a template binding, the browser
|
|
634
|
+
sees `42` in the DOM before the component's JavaScript state exists. Without
|
|
635
|
+
seeding, the first `$update()` would overwrite the SSR content with the wrong
|
|
636
|
+
value.
|
|
560
637
|
|
|
561
638
|
State seeding uses `window.__webui.state` — a JSON object loaded from the
|
|
562
639
|
server-emitted `#webui-data` block. Like Preact's props, this delivers the
|
|
563
|
-
same data used for SSR rendering to the client.
|
|
564
|
-
`$applySSRState()` writes matching keys directly to observable backing
|
|
565
|
-
|
|
640
|
+
same data used for SSR rendering to the client. During `$mount()`,
|
|
641
|
+
`$applySSRState()` writes matching decorated keys directly to observable backing
|
|
642
|
+
fields and stores undecorated template roots in hidden framework state before
|
|
643
|
+
any bindings are wired:
|
|
566
644
|
|
|
567
645
|
```mermaid
|
|
568
646
|
flowchart LR
|
|
569
647
|
SCRIPT["<script type='application/json' id='webui-data'><br/>{ state: { count: 42, title: 'Hello' } }"] --> APPLY["$applySSRState()"]
|
|
570
|
-
APPLY --> SEED["Write
|
|
648
|
+
APPLY --> SEED["Write decorated fields + hidden template state"]
|
|
571
649
|
SEED --> HYDRATE["$hydrate() — bindings match<br/>server-rendered DOM"]
|
|
572
650
|
```
|
|
573
651
|
|
|
574
|
-
`$applySSRState()` only
|
|
575
|
-
|
|
576
|
-
field (`_prop`) directly,
|
|
652
|
+
`$applySSRState()` only accepts keys that are decorated properties or compiled
|
|
653
|
+
template roots. Unknown keys are ignored. Decorated writes go to the backing
|
|
654
|
+
field (`_prop`) directly, and undecorated template roots stay internal, avoiding
|
|
655
|
+
reactive updates before bindings are wired.
|
|
577
656
|
|
|
578
657
|
---
|
|
579
658
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type CompiledConditionFn, type TemplateMeta } from './template.js';
|
|
2
|
+
export interface ComponentAsset {
|
|
3
|
+
type?: 'webui-component-asset';
|
|
4
|
+
version?: number;
|
|
5
|
+
components?: string[];
|
|
6
|
+
templateStyles?: string[];
|
|
7
|
+
templates?: Record<string, TemplateMeta>;
|
|
8
|
+
templateFunctions?: Record<string, CompiledConditionFn[]>;
|
|
9
|
+
}
|
|
10
|
+
export type ComponentAssetState = Record<string, unknown>;
|
|
11
|
+
export interface ComponentAssetManifestEntry<Data extends ComponentAssetState = ComponentAssetState> {
|
|
12
|
+
asset: string | URL;
|
|
13
|
+
module?: () => Promise<unknown>;
|
|
14
|
+
data?: () => Promise<Data>;
|
|
15
|
+
}
|
|
16
|
+
export type ComponentAssetManifest = Record<string, ComponentAssetManifestEntry>;
|
|
17
|
+
export interface ComponentAssetPreload<Data extends ComponentAssetState = ComponentAssetState> {
|
|
18
|
+
asset: Promise<void>;
|
|
19
|
+
module?: Promise<unknown>;
|
|
20
|
+
data?: Promise<Data>;
|
|
21
|
+
}
|
|
22
|
+
export interface ComponentAssetCreateOptions {
|
|
23
|
+
awaitData?: boolean;
|
|
24
|
+
dataTimeoutMs?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface ComponentAssetRegistry {
|
|
27
|
+
preload<Data extends ComponentAssetState = ComponentAssetState>(tag: string): ComponentAssetPreload<Data>;
|
|
28
|
+
create(tag: string, options?: ComponentAssetCreateOptions): Promise<HTMLElement>;
|
|
29
|
+
}
|
|
30
|
+
export declare function defineComponentAssets(manifest: ComponentAssetManifest): ComponentAssetRegistry;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { getTemplate, registerTemplateData, } from './template.js';
|
|
2
|
+
const ASSET_TYPE = 'webui-component-asset';
|
|
3
|
+
const ASSET_VERSION = 1;
|
|
4
|
+
const injectedAssetStyles = new Set();
|
|
5
|
+
const assetLoadPromises = new Map();
|
|
6
|
+
let assetStylesSeeded = false;
|
|
7
|
+
function assetGlobal() {
|
|
8
|
+
return window.__webui;
|
|
9
|
+
}
|
|
10
|
+
export function defineComponentAssets(manifest) {
|
|
11
|
+
const preloads = new Map();
|
|
12
|
+
function preload(tag) {
|
|
13
|
+
const existing = preloads.get(tag);
|
|
14
|
+
if (existing)
|
|
15
|
+
return existing;
|
|
16
|
+
const entry = manifest[tag];
|
|
17
|
+
if (!entry) {
|
|
18
|
+
throw new Error(`[WebUI] No component asset manifest entry for <${tag}>.`);
|
|
19
|
+
}
|
|
20
|
+
const next = {
|
|
21
|
+
asset: loadComponentAsset(tag, entry.asset),
|
|
22
|
+
};
|
|
23
|
+
if (entry.module) {
|
|
24
|
+
next.module = entry.module();
|
|
25
|
+
}
|
|
26
|
+
if (entry.data) {
|
|
27
|
+
next.data = entry.data();
|
|
28
|
+
}
|
|
29
|
+
next.asset.catch(() => { });
|
|
30
|
+
next.module?.catch(() => { });
|
|
31
|
+
next.data?.catch(() => { });
|
|
32
|
+
preloads.set(tag, next);
|
|
33
|
+
return next;
|
|
34
|
+
}
|
|
35
|
+
async function create(tag, options = {}) {
|
|
36
|
+
const pending = preload(tag);
|
|
37
|
+
await waitForElementResources(pending);
|
|
38
|
+
const element = document.createElement(tag);
|
|
39
|
+
if (pending.data) {
|
|
40
|
+
if (options.awaitData) {
|
|
41
|
+
const state = options.dataTimeoutMs === undefined
|
|
42
|
+
? await pending.data
|
|
43
|
+
: await dataWithTimeout(pending.data, options.dataTimeoutMs);
|
|
44
|
+
if (state) {
|
|
45
|
+
applyState(element, state);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
applyDataWhenReady(element, pending.data);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
applyDataWhenReady(element, pending.data);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return element;
|
|
56
|
+
}
|
|
57
|
+
return { preload, create };
|
|
58
|
+
}
|
|
59
|
+
async function waitForElementResources(pending) {
|
|
60
|
+
await pending.asset;
|
|
61
|
+
if (pending.module)
|
|
62
|
+
await pending.module;
|
|
63
|
+
}
|
|
64
|
+
function applyState(element, state) {
|
|
65
|
+
const setState = element.setState;
|
|
66
|
+
if (typeof setState === 'function') {
|
|
67
|
+
setState.call(element, state);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function applyDataWhenReady(element, data) {
|
|
71
|
+
const elementRef = new WeakRef(element);
|
|
72
|
+
void data.then(state => {
|
|
73
|
+
const liveElement = elementRef.deref();
|
|
74
|
+
if (liveElement)
|
|
75
|
+
applyState(liveElement, state);
|
|
76
|
+
}).catch(() => { });
|
|
77
|
+
}
|
|
78
|
+
function dataWithTimeout(data, timeoutMs) {
|
|
79
|
+
if (timeoutMs < 0)
|
|
80
|
+
return data;
|
|
81
|
+
return Promise.race([
|
|
82
|
+
data,
|
|
83
|
+
new Promise(resolve => {
|
|
84
|
+
setTimeout(() => resolve(undefined), timeoutMs);
|
|
85
|
+
}),
|
|
86
|
+
]);
|
|
87
|
+
}
|
|
88
|
+
function loadComponentAsset(tag, url) {
|
|
89
|
+
if (getTemplate(tag))
|
|
90
|
+
return Promise.resolve();
|
|
91
|
+
const assetUrl = new URL(url, document.baseURI);
|
|
92
|
+
const href = assetUrl.href;
|
|
93
|
+
let promise = assetLoadPromises.get(href);
|
|
94
|
+
if (promise)
|
|
95
|
+
return promise;
|
|
96
|
+
promise = importAndRegisterComponentAsset(assetUrl)
|
|
97
|
+
.finally(() => {
|
|
98
|
+
assetLoadPromises.delete(href);
|
|
99
|
+
});
|
|
100
|
+
assetLoadPromises.set(href, promise);
|
|
101
|
+
return promise;
|
|
102
|
+
}
|
|
103
|
+
function registerComponentAsset(asset) {
|
|
104
|
+
validateAsset(asset);
|
|
105
|
+
if (asset.templates && templatesAlreadyRegistered(asset.templates))
|
|
106
|
+
return;
|
|
107
|
+
registerAssetStyles(asset.templateStyles, readNonce());
|
|
108
|
+
if (asset.templates) {
|
|
109
|
+
registerTemplateData(asset.templates, asset.templateFunctions);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async function importAndRegisterComponentAsset(assetUrl) {
|
|
113
|
+
const imported = await import(assetUrl.href);
|
|
114
|
+
registerComponentAsset(readComponentAssetModule(imported));
|
|
115
|
+
}
|
|
116
|
+
function readComponentAssetModule(module) {
|
|
117
|
+
if (!isObject(module) || !isObject(module.default)) {
|
|
118
|
+
throw new Error('[WebUI] Component asset module must default-export an asset object.');
|
|
119
|
+
}
|
|
120
|
+
return module.default;
|
|
121
|
+
}
|
|
122
|
+
function isObject(value) {
|
|
123
|
+
return typeof value === 'object' && value !== null;
|
|
124
|
+
}
|
|
125
|
+
function validateAsset(asset) {
|
|
126
|
+
if (asset.type !== ASSET_TYPE) {
|
|
127
|
+
throw new Error(`[WebUI] Invalid component asset type: ${String(asset.type)}`);
|
|
128
|
+
}
|
|
129
|
+
if (asset.version !== ASSET_VERSION) {
|
|
130
|
+
throw new Error(`[WebUI] Unsupported component asset version: ${String(asset.version)}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function templatesAlreadyRegistered(templates) {
|
|
134
|
+
const tags = Object.keys(templates);
|
|
135
|
+
if (tags.length === 0)
|
|
136
|
+
return false;
|
|
137
|
+
for (let i = 0; i < tags.length; i++) {
|
|
138
|
+
if (!getTemplate(tags[i]))
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
function readNonce() {
|
|
144
|
+
const nonce = assetGlobal()?.nonce;
|
|
145
|
+
if (nonce)
|
|
146
|
+
return nonce;
|
|
147
|
+
const meta = document.querySelector('meta[name="webui-nonce"]');
|
|
148
|
+
return meta?.content ?? '';
|
|
149
|
+
}
|
|
150
|
+
function seedAssetStyleSet() {
|
|
151
|
+
if (assetStylesSeeded)
|
|
152
|
+
return;
|
|
153
|
+
assetStylesSeeded = true;
|
|
154
|
+
const styles = assetGlobal()?.styles;
|
|
155
|
+
if (!styles)
|
|
156
|
+
return;
|
|
157
|
+
for (let i = 0; i < styles.length; i++) {
|
|
158
|
+
injectedAssetStyles.add(styles[i]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function registerAssetStyles(templateStyles, nonce) {
|
|
162
|
+
if (!templateStyles || templateStyles.length === 0)
|
|
163
|
+
return;
|
|
164
|
+
seedAssetStyleSet();
|
|
165
|
+
for (let i = 0; i < templateStyles.length; i++) {
|
|
166
|
+
const imports = parseImportMap(templateStyles[i]);
|
|
167
|
+
const nextImports = {};
|
|
168
|
+
let hasNewImport = false;
|
|
169
|
+
const specifiers = Object.keys(imports);
|
|
170
|
+
for (let j = 0; j < specifiers.length; j++) {
|
|
171
|
+
const specifier = specifiers[j];
|
|
172
|
+
if (injectedAssetStyles.has(specifier))
|
|
173
|
+
continue;
|
|
174
|
+
injectedAssetStyles.add(specifier);
|
|
175
|
+
nextImports[specifier] = imports[specifier];
|
|
176
|
+
hasNewImport = true;
|
|
177
|
+
}
|
|
178
|
+
if (!hasNewImport)
|
|
179
|
+
continue;
|
|
180
|
+
const script = document.createElement('script');
|
|
181
|
+
script.type = 'importmap';
|
|
182
|
+
if (nonce)
|
|
183
|
+
script.nonce = nonce;
|
|
184
|
+
script.textContent = JSON.stringify({ imports: nextImports });
|
|
185
|
+
document.head.appendChild(script);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function parseImportMap(scriptMarkup) {
|
|
189
|
+
const trimmed = scriptMarkup.trim();
|
|
190
|
+
if (!trimmed.startsWith('<script')) {
|
|
191
|
+
throw new Error('[WebUI] Component asset templateStyles entry must be a <script type="importmap"> tag.');
|
|
192
|
+
}
|
|
193
|
+
const openTagEnd = trimmed.indexOf('>');
|
|
194
|
+
const closeTagStart = trimmed.lastIndexOf('</script>');
|
|
195
|
+
if (openTagEnd < 0 || closeTagStart <= openTagEnd) {
|
|
196
|
+
throw new Error('[WebUI] Component asset importmap tag is malformed.');
|
|
197
|
+
}
|
|
198
|
+
const parsed = JSON.parse(trimmed.substring(openTagEnd + 1, closeTagStart));
|
|
199
|
+
if (!parsed.imports || typeof parsed.imports !== 'object') {
|
|
200
|
+
throw new Error('[WebUI] Component asset importmap is missing an imports object.');
|
|
201
|
+
}
|
|
202
|
+
const imports = {};
|
|
203
|
+
const specifiers = Object.keys(parsed.imports);
|
|
204
|
+
for (let i = 0; i < specifiers.length; i++) {
|
|
205
|
+
const specifier = specifiers[i];
|
|
206
|
+
const uri = parsed.imports[specifier];
|
|
207
|
+
if (typeof uri !== 'string' || !uri.startsWith('data:text/css,')) {
|
|
208
|
+
throw new Error(`[WebUI] Component asset importmap entry "${specifier}" must be a data:text/css URI.`);
|
|
209
|
+
}
|
|
210
|
+
imports[specifier] = uri;
|
|
211
|
+
}
|
|
212
|
+
return imports;
|
|
213
|
+
}
|
package/dist/decorators.d.ts
CHANGED
|
@@ -1,62 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Convert a camelCase DOM property name into its kebab-case HTML attribute form.
|
|
3
|
-
*
|
|
4
|
-
* This function is optimized for framework-level hot paths where attribute
|
|
5
|
-
* normalization may run thousands of times per render. It performs three
|
|
6
|
-
* progressively cheaper checks:
|
|
7
|
-
*
|
|
8
|
-
* 1. **Direct lookup for irregular mappings**
|
|
9
|
-
* Many DOM properties (e.g., `readOnly`, `tabIndex`, `crossOrigin`) do not
|
|
10
|
-
* follow simple camelCase → kebab-case rules. These are resolved through a
|
|
11
|
-
* precomputed `propertyToAttribute` map for O(1) returns with no string
|
|
12
|
-
* processing.
|
|
13
|
-
*
|
|
14
|
-
* 2. **Fast path for ARIA attributes**
|
|
15
|
-
* ARIA properties always begin with `aria` followed by an uppercase letter
|
|
16
|
-
* (e.g., `ariaDescribedBy`). These map to `aria-` + the lowercase remainder.
|
|
17
|
-
* This branch avoids the general loop and uses the engine-optimized
|
|
18
|
-
* `.toLowerCase()` for the suffix.
|
|
19
|
-
*
|
|
20
|
-
* 3. **General camelCase → kebab-case conversion**
|
|
21
|
-
* For all other inputs, the function performs a tight ASCII-only scan:
|
|
22
|
-
* uppercase A–Z (65–90) are converted to lowercase and prefixed with `-`,
|
|
23
|
-
* while all other characters are copied as-is. This avoids regex engines,
|
|
24
|
-
* callback allocations, and match objects, producing predictable,
|
|
25
|
-
* allocation-minimal performance ideal for DOM attribute reflection.
|
|
26
|
-
*
|
|
27
|
-
* The result is a predictable, JIT-friendly transformation suitable for
|
|
28
|
-
* attribute diffing, SSR serialization, and runtime DOM patching.
|
|
29
|
-
*/
|
|
30
1
|
export declare function toKebabCase(str: string): string;
|
|
31
2
|
export declare function getObservableNames(ctor: Function): Set<string>;
|
|
32
|
-
/**
|
|
33
|
-
* Marks a property as observable. When the value changes the decorator will:
|
|
34
|
-
* 1. Call `this.<prop>Changed(oldValue, newValue)` if defined.
|
|
35
|
-
* 2. Call `this.$update(name)` if the element is connected, targeting
|
|
36
|
-
* only bindings that reference this property.
|
|
37
|
-
*/
|
|
38
|
-
export declare function observable(target: object, name: string): void;
|
|
39
3
|
export declare function isAttributeProperty(ctor: Function, property: string): boolean;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
* (kebab-case). The decorator patches `observedAttributes` and
|
|
43
|
-
* `attributeChangedCallback` on the class so changes flow in both directions.
|
|
44
|
-
*
|
|
45
|
-
* @example
|
|
46
|
-
* ```ts
|
|
47
|
-
* class MyEl extends WebUIElement {
|
|
48
|
-
* @attr myProp = 'default';
|
|
49
|
-
* // syncs with attribute "my-prop"
|
|
50
|
-
* }
|
|
51
|
-
* ```
|
|
52
|
-
*/
|
|
4
|
+
export declare function syncAttrProperties(instance: object, ctor: Function): void;
|
|
5
|
+
export declare function observable(target: object, name: string): void;
|
|
53
6
|
export interface AttrOptions {
|
|
54
7
|
attribute?: string;
|
|
55
|
-
/** When `'boolean'`, the property is `true` when the attribute is present
|
|
56
|
-
* and `false` when absent — matching native HTML boolean attribute semantics.
|
|
57
|
-
* Default is string mode (property receives the attribute string value). */
|
|
58
8
|
mode?: 'boolean';
|
|
59
9
|
}
|
|
60
|
-
export declare function syncAttrProperties(instance: object, ctor: Function): void;
|
|
61
10
|
export declare function attr(target: object, name: string): void;
|
|
62
11
|
export declare function attr(options: AttrOptions): (target: object, name: string) => void;
|