@microsoft/fast-element 2.10.1 → 2.10.3
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/ARCHITECTURE_FASTELEMENT.md +1 -1
- package/ARCHITECTURE_HTML_TAGGED_TEMPLATE_LITERAL.md +3 -1
- package/ARCHITECTURE_INTRO.md +1 -1
- package/ARCHITECTURE_OVERVIEW.md +1 -1
- package/CHANGELOG.json +153 -1
- package/CHANGELOG.md +18 -2
- package/DESIGN.md +506 -0
- package/biome.json +4 -0
- package/dist/context/context.api.json +17 -1
- package/dist/di/di.api.json +17 -1
- package/dist/dts/hydration/target-builder.d.ts +17 -1
- package/dist/dts/templating/html-binding-directive.d.ts +39 -4
- package/dist/dts/templating/html-directive.d.ts +7 -1
- package/dist/dts/templating/template.d.ts +19 -1
- package/dist/dts/templating/view.d.ts +11 -0
- package/dist/dts/testing/models.d.ts +20 -0
- package/dist/dts/tsdoc-metadata.json +1 -1
- package/dist/esm/components/element-controller.js +3 -0
- package/dist/esm/components/hydration.js +17 -0
- package/dist/esm/hydration/target-builder.js +22 -1
- package/dist/esm/templating/compiler.js +29 -11
- package/dist/esm/templating/html-binding-directive.js +59 -4
- package/dist/esm/templating/html-directive.js +7 -1
- package/dist/esm/templating/install-hydratable-view-templates.js +6 -0
- package/dist/esm/templating/markup.js +8 -0
- package/dist/esm/templating/template.js +19 -1
- package/dist/esm/templating/view.js +13 -0
- package/dist/esm/testing/models.js +58 -0
- package/dist/fast-element.api.json +20 -4
- package/dist/fast-element.debug.js +179 -227
- package/dist/fast-element.debug.min.js +2 -2
- package/dist/fast-element.js +179 -227
- package/dist/fast-element.min.js +2 -2
- package/dist/fast-element.untrimmed.d.ts +76 -6
- package/docs/api-report.api.md +3 -3
- package/package.json +7 -19
- package/playwright.config.ts +8 -0
- package/test/main.ts +95 -1
- package/tsconfig.api-extractor.json +6 -0
- package/.eslintrc.json +0 -19
- package/karma.conf.cjs +0 -148
|
@@ -143,7 +143,13 @@ export declare const HTMLDirective: Readonly<{
|
|
|
143
143
|
*/
|
|
144
144
|
define<TType extends Constructable<HTMLDirective>>(type: TType, options?: PartialHTMLDirectiveDefinition): TType;
|
|
145
145
|
/**
|
|
146
|
-
*
|
|
146
|
+
* Determines the DOM aspect type for a directive based on attribute name prefix.
|
|
147
|
+
* The prefix convention maps to aspect types as follows:
|
|
148
|
+
* - No prefix (e.g. "class") → DOMAspect.attribute
|
|
149
|
+
* - ":" prefix (e.g. ":value") → DOMAspect.property (":classList" → DOMAspect.tokenList)
|
|
150
|
+
* - "?" prefix (e.g. "?disabled") → DOMAspect.booleanAttribute
|
|
151
|
+
* - `@` prefix (e.g. `@click`) → DOMAspect.event
|
|
152
|
+
* - Falsy or absent value → DOMAspect.content (see remarks)
|
|
147
153
|
* @param directive - The directive to assign the aspect to.
|
|
148
154
|
* @param value - The value to base the aspect determination on.
|
|
149
155
|
* @remarks
|
|
@@ -142,7 +142,25 @@ export declare class ViewTemplate<TSource = any, TParent = any> implements Eleme
|
|
|
142
142
|
*/
|
|
143
143
|
render(source: TSource, host: Node, hostBindingTarget?: Element): HTMLView<TSource, TParent>;
|
|
144
144
|
/**
|
|
145
|
-
*
|
|
145
|
+
* Processes the tagged template literal's static strings and interpolated values and
|
|
146
|
+
* creates a ViewTemplate.
|
|
147
|
+
*
|
|
148
|
+
* For each interpolated value:
|
|
149
|
+
* 1. Functions (binding expressions, e.g., `x => x.name`) → wrapped in a one-way HTMLBindingDirective
|
|
150
|
+
* 2. Binding instances → wrapped in an HTMLBindingDirective
|
|
151
|
+
* 3. HTMLDirective instances → used as-is
|
|
152
|
+
* 4. Static values (strings, numbers) → wrapped in a one-time HTMLBindingDirective
|
|
153
|
+
*
|
|
154
|
+
* Each directive's createHTML() is called with an `add` callback that registers
|
|
155
|
+
* the factory in the factories record under a unique ID and returns that ID.
|
|
156
|
+
* The directive inserts a placeholder marker (e.g., `fast-abc123{0}fast-abc123`) into
|
|
157
|
+
* the HTML string so the compiler can later find and associate it with the factory.
|
|
158
|
+
*
|
|
159
|
+
* Aspect detection happens here too: the `lastAttributeNameRegex` checks whether
|
|
160
|
+
* the placeholder appears inside an attribute value, and if so, assignAspect()
|
|
161
|
+
* sets the correct DOMAspect (attribute, property, event, etc.) based on the
|
|
162
|
+
* attribute name prefix.
|
|
163
|
+
*
|
|
146
164
|
* @param strings - The static strings to create the template with.
|
|
147
165
|
* @param values - The dynamic values to create the template with.
|
|
148
166
|
* @param policy - The DOMPolicy to associated with the template.
|
|
@@ -197,6 +197,17 @@ export declare class HTMLView<TSource = any, TParent = any> extends DefaultExecu
|
|
|
197
197
|
}): void;
|
|
198
198
|
/**
|
|
199
199
|
* Binds a view's behaviors to its binding source.
|
|
200
|
+
*
|
|
201
|
+
* On the first call, this iterates through all compiled factories, calling
|
|
202
|
+
* createBehavior() on each to produce a ViewBehavior instance (e.g., an
|
|
203
|
+
* HTMLBindingDirective), and then immediately binds it. This is where event
|
|
204
|
+
* listeners are registered, expression observers are created, and initial
|
|
205
|
+
* DOM values are set.
|
|
206
|
+
*
|
|
207
|
+
* On subsequent calls with a new source, existing behaviors are re-bound
|
|
208
|
+
* to the new data source, which re-evaluates all binding expressions and
|
|
209
|
+
* updates the DOM accordingly.
|
|
210
|
+
*
|
|
200
211
|
* @param source - The binding source for the view's binding behaviors.
|
|
201
212
|
* @param context - The execution context to run the behaviors within.
|
|
202
213
|
*/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
declare class ChildModel {
|
|
2
|
+
value: string;
|
|
3
|
+
}
|
|
4
|
+
declare class Model {
|
|
5
|
+
childChangedCalled: boolean;
|
|
6
|
+
trigger: number;
|
|
7
|
+
value: number;
|
|
8
|
+
child: ChildModel;
|
|
9
|
+
child2: ChildModel;
|
|
10
|
+
childChanged(): void;
|
|
11
|
+
incrementTrigger(): void;
|
|
12
|
+
decrementTrigger(): void;
|
|
13
|
+
get ifConditional(): number;
|
|
14
|
+
}
|
|
15
|
+
declare class DerivedModel extends Model {
|
|
16
|
+
child2ChangedCalled: boolean;
|
|
17
|
+
child2Changed(): void;
|
|
18
|
+
derivedChild: ChildModel;
|
|
19
|
+
}
|
|
20
|
+
export { ChildModel, DerivedModel, Model };
|
|
@@ -731,6 +731,9 @@ export class HydratableElementController extends ElementController {
|
|
|
731
731
|
// Initialize needsHydration on first connect
|
|
732
732
|
this.needsHydration =
|
|
733
733
|
(_a = this.needsHydration) !== null && _a !== void 0 ? _a : this.source.hasAttribute(needsHydrationAttribute);
|
|
734
|
+
if (this.needsHydration) {
|
|
735
|
+
this.addHydratingInstance();
|
|
736
|
+
}
|
|
734
737
|
// If the `defer-hydration` attribute exists on the source,
|
|
735
738
|
// wait for it to be removed before continuing connection behavior.
|
|
736
739
|
if (this.source.hasAttribute(deferHydrationAttribute)) {
|
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import { FAST } from "../platform.js";
|
|
2
|
+
/**
|
|
3
|
+
* Regex patterns for parsing hydration markers embedded as HTML comments by the SSR renderer.
|
|
4
|
+
* Each marker type encodes factory indices so the client can map markers back to ViewBehaviorFactories.
|
|
5
|
+
*
|
|
6
|
+
* Content binding markers bracket text/template content:
|
|
7
|
+
* <!-- fe-b$$start$$<factoryIndex>$$<uniqueId>$$fe-b -->
|
|
8
|
+
* ...content...
|
|
9
|
+
* <!-- fe-b$$end$$<factoryIndex>$$<uniqueId>$$fe-b -->
|
|
10
|
+
*
|
|
11
|
+
* Repeat markers bracket each repeated item:
|
|
12
|
+
* <!-- fe-repeat$$start$$<itemIndex>$$fe-repeat -->
|
|
13
|
+
* <!-- fe-repeat$$end$$<itemIndex>$$fe-repeat -->
|
|
14
|
+
*
|
|
15
|
+
* Element boundary markers demarcate nested custom elements so parent walkers can skip them:
|
|
16
|
+
* <!-- fe-eb$$start$$<elementId>$$fe-eb -->
|
|
17
|
+
* <!-- fe-eb$$end$$<elementId>$$fe-eb -->
|
|
18
|
+
*/
|
|
2
19
|
const bindingStartMarker = /fe-b\$\$start\$\$(\d+)\$\$(.+)\$\$fe-b/;
|
|
3
20
|
const bindingEndMarker = /fe-b\$\$end\$\$(\d+)\$\$(.+)\$\$fe-b/;
|
|
4
21
|
const repeatViewStartMarker = /fe-repeat\$\$start\$\$(\d+)\$\$fe-repeat/;
|
|
@@ -43,7 +43,23 @@ function isShadowRoot(node) {
|
|
|
43
43
|
return node instanceof DocumentFragment && "mode" in node;
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
|
-
* Maps
|
|
46
|
+
* Maps compiled ViewBehaviorFactory IDs to their corresponding DOM nodes in the
|
|
47
|
+
* server-rendered shadow root. Uses a TreeWalker to scan the existing DOM between
|
|
48
|
+
* firstNode and lastNode, parsing hydration markers to build the targets map.
|
|
49
|
+
*
|
|
50
|
+
* For element nodes: parses `data-fe-b` (or variant) attributes to identify which
|
|
51
|
+
* factories target each element, then removes the marker attribute.
|
|
52
|
+
*
|
|
53
|
+
* For comment nodes: parses content binding markers (`fe-b$$start/end$$`) to find
|
|
54
|
+
* the DOM range controlled by each content binding. Single text nodes become the
|
|
55
|
+
* direct target; multi-node ranges are stored in boundaries for structural directives.
|
|
56
|
+
* Element boundary markers (`fe-eb$$start/end$$`) cause the walker to skip over
|
|
57
|
+
* nested custom elements that handle their own hydration.
|
|
58
|
+
*
|
|
59
|
+
* Host bindings (targetNodeId='h') appear at the start of the factories array but
|
|
60
|
+
* have no SSR markers — getHydrationIndexOffset() computes how many to skip so that
|
|
61
|
+
* marker indices align with the correct non-host factories.
|
|
62
|
+
*
|
|
47
63
|
* @param firstNode - The first node of the view.
|
|
48
64
|
* @param lastNode - The last node of the view.
|
|
49
65
|
* @param factories - The Compiled View Behavior Factories that belong to the view.
|
|
@@ -169,6 +185,11 @@ function skipToElementBoundaryEndMarker(node, walker) {
|
|
|
169
185
|
current = walker.nextSibling();
|
|
170
186
|
}
|
|
171
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Counts how many factories at the start of the array are host bindings (targetNodeId='h').
|
|
190
|
+
* Host bindings target the custom element itself and are not represented by SSR markers,
|
|
191
|
+
* so the marker indices must be offset by this count to align with the correct factory.
|
|
192
|
+
*/
|
|
172
193
|
function getHydrationIndexOffset(factories) {
|
|
173
194
|
let offset = 0;
|
|
174
195
|
for (let i = 0, ii = factories.length; i < ii; ++i) {
|
|
@@ -7,6 +7,12 @@ import { HTMLBindingDirective } from "./html-binding-directive.js";
|
|
|
7
7
|
import { HTMLDirective, } from "./html-directive.js";
|
|
8
8
|
import { nextId, Parser } from "./markup.js";
|
|
9
9
|
import { HTMLView } from "./view.js";
|
|
10
|
+
/**
|
|
11
|
+
* Builds a hierarchical node ID by appending the child index to the parent's ID.
|
|
12
|
+
* For example, the third child of root is "r.2", and its first child is "r.2.0".
|
|
13
|
+
* These IDs are used as property names on the targets prototype so that each
|
|
14
|
+
* binding's target DOM node can be lazily resolved via a chain of childNodes lookups.
|
|
15
|
+
*/
|
|
10
16
|
const targetIdFrom = (parentId, nodeIndex) => `${parentId}.${nodeIndex}`;
|
|
11
17
|
const descriptorCache = {};
|
|
12
18
|
// used to prevent creating lots of objects just to track node and index while compiling
|
|
@@ -56,6 +62,13 @@ class CompilationContext {
|
|
|
56
62
|
this.proto = Object.create(null, this.descriptors);
|
|
57
63
|
return this;
|
|
58
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Registers a lazy getter on the targets prototype that resolves a DOM node
|
|
67
|
+
* by navigating from its parent's childNodes at the given index. Getters are
|
|
68
|
+
* chained: accessing targets["r.0.2"] first resolves targets["r.0"] (which
|
|
69
|
+
* resolves targets["r"]), then returns childNodes[2]. Results are cached so
|
|
70
|
+
* each node is resolved at most once per view instance.
|
|
71
|
+
*/
|
|
59
72
|
addTargetDescriptor(parentId, targetId, targetIndex) {
|
|
60
73
|
const descriptors = this.descriptors;
|
|
61
74
|
if (targetId === "r" || // root
|
|
@@ -66,7 +79,7 @@ class CompilationContext {
|
|
|
66
79
|
if (!descriptors[parentId]) {
|
|
67
80
|
const index = parentId.lastIndexOf(".");
|
|
68
81
|
const grandparentId = parentId.substring(0, index);
|
|
69
|
-
const childIndex = parseInt(parentId.substring(index + 1));
|
|
82
|
+
const childIndex = parseInt(parentId.substring(index + 1), 10);
|
|
70
83
|
this.addTargetDescriptor(grandparentId, parentId, childIndex);
|
|
71
84
|
}
|
|
72
85
|
let descriptor = descriptorCache[targetId];
|
|
@@ -81,13 +94,20 @@ class CompilationContext {
|
|
|
81
94
|
}
|
|
82
95
|
descriptors[targetId] = descriptor;
|
|
83
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Creates a new HTMLView by cloning the compiled DocumentFragment and building
|
|
99
|
+
* a targets object. The targets prototype contains lazy getters that resolve
|
|
100
|
+
* each binding's target DOM node via childNodes traversal. Accessing every
|
|
101
|
+
* registered nodeId eagerly triggers the getter chain so all nodes are resolved
|
|
102
|
+
* before behaviors are bound.
|
|
103
|
+
*/
|
|
84
104
|
createView(hostBindingTarget) {
|
|
85
105
|
const fragment = this.fragment.cloneNode(true);
|
|
86
106
|
const targets = Object.create(this.proto);
|
|
87
|
-
targets.r = fragment;
|
|
88
|
-
targets.h = hostBindingTarget !== null && hostBindingTarget !== void 0 ? hostBindingTarget : warningHost;
|
|
107
|
+
targets.r = fragment; // root — the cloned DocumentFragment
|
|
108
|
+
targets.h = hostBindingTarget !== null && hostBindingTarget !== void 0 ? hostBindingTarget : warningHost; // host — the custom element
|
|
89
109
|
for (const id of this.nodeIds) {
|
|
90
|
-
targets
|
|
110
|
+
Reflect.get(targets, id); // trigger lazy getter to resolve and cache the DOM node
|
|
91
111
|
}
|
|
92
112
|
return new HTMLView(fragment, this.factories, targets);
|
|
93
113
|
}
|
|
@@ -107,7 +127,6 @@ function compileAttributes(context, parentId, node, nodeId, nodeIndex, includeBa
|
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
129
|
else {
|
|
110
|
-
/* eslint-disable-next-line @typescript-eslint/no-use-before-define */
|
|
111
130
|
result = Compiler.aggregate(parseResult, context.policy);
|
|
112
131
|
}
|
|
113
132
|
if (result !== null) {
|
|
@@ -152,7 +171,6 @@ function compileChildren(context, parent, parentId) {
|
|
|
152
171
|
let nodeIndex = 0;
|
|
153
172
|
let childNode = parent.firstChild;
|
|
154
173
|
while (childNode) {
|
|
155
|
-
/* eslint-disable-next-line @typescript-eslint/no-use-before-define */
|
|
156
174
|
const result = compileNode(context, parentId, childNode, nodeIndex);
|
|
157
175
|
childNode = result.node;
|
|
158
176
|
nodeIndex = result.index;
|
|
@@ -167,14 +185,14 @@ function compileNode(context, parentId, node, nodeIndex) {
|
|
|
167
185
|
break;
|
|
168
186
|
case 3: // text node
|
|
169
187
|
return compileContent(context, node, parentId, nodeId, nodeIndex);
|
|
170
|
-
case 8:
|
|
188
|
+
case 8: {
|
|
189
|
+
// comment
|
|
171
190
|
const parts = Parser.parse(node.data, context.directives);
|
|
172
191
|
if (parts !== null) {
|
|
173
|
-
context.addFactory(
|
|
174
|
-
/* eslint-disable-next-line @typescript-eslint/no-use-before-define */
|
|
175
|
-
Compiler.aggregate(parts), parentId, nodeId, nodeIndex, null);
|
|
192
|
+
context.addFactory(Compiler.aggregate(parts), parentId, nodeId, nodeIndex, null);
|
|
176
193
|
}
|
|
177
194
|
break;
|
|
195
|
+
}
|
|
178
196
|
}
|
|
179
197
|
next.index = nodeIndex + 1;
|
|
180
198
|
next.node = node.nextSibling;
|
|
@@ -182,7 +200,7 @@ function compileNode(context, parentId, node, nodeIndex) {
|
|
|
182
200
|
}
|
|
183
201
|
function isMarker(node, directives) {
|
|
184
202
|
return (node &&
|
|
185
|
-
node.nodeType
|
|
203
|
+
node.nodeType === 8 &&
|
|
186
204
|
Parser.parse(node.data, directives) !== null);
|
|
187
205
|
}
|
|
188
206
|
const templateTag = "TEMPLATE";
|
|
@@ -9,6 +9,14 @@ import { HydrationStage } from "./view.js";
|
|
|
9
9
|
function isContentTemplate(value) {
|
|
10
10
|
return value.create !== undefined;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Sink function for DOMAspect.content bindings (text content interpolation).
|
|
14
|
+
* Handles two cases:
|
|
15
|
+
* - If the value is a ContentTemplate (has a create() method), it composes a child
|
|
16
|
+
* view into the DOM, managing view lifecycle (create/reuse/remove/bind).
|
|
17
|
+
* - If the value is a primitive, it sets target.textContent directly, first removing
|
|
18
|
+
* any previously composed view.
|
|
19
|
+
*/
|
|
12
20
|
function updateContent(target, aspect, value, controller) {
|
|
13
21
|
// If there's no actual value, then this equates to the
|
|
14
22
|
// empty string for the purposes of content bindings.
|
|
@@ -77,6 +85,12 @@ function updateContent(target, aspect, value, controller) {
|
|
|
77
85
|
target.textContent = value;
|
|
78
86
|
}
|
|
79
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Sink function for DOMAspect.tokenList bindings (e.g., :classList).
|
|
90
|
+
* Uses a versioning scheme to efficiently track which CSS classes were added
|
|
91
|
+
* in the current update vs. the previous one. Classes from the previous version
|
|
92
|
+
* that aren't present in the new value are automatically removed.
|
|
93
|
+
*/
|
|
80
94
|
function updateTokenList(target, aspect, value) {
|
|
81
95
|
var _a;
|
|
82
96
|
const lookup = `${this.id}-t`;
|
|
@@ -109,6 +123,12 @@ function updateTokenList(target, aspect, value) {
|
|
|
109
123
|
}
|
|
110
124
|
}
|
|
111
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* Maps each DOMAspect type to its corresponding DOM update ("sink") function.
|
|
128
|
+
* When a binding value changes, the sink function for the binding's aspect type
|
|
129
|
+
* is called to push the new value into the DOM. Events are handled separately
|
|
130
|
+
* via addEventListener in bind(), so the event sink is a no-op.
|
|
131
|
+
*/
|
|
112
132
|
const sinkLookup = {
|
|
113
133
|
[DOMAspect.attribute]: DOM.setAttribute,
|
|
114
134
|
[DOMAspect.booleanAttribute]: DOM.setBooleanAttribute,
|
|
@@ -118,7 +138,18 @@ const sinkLookup = {
|
|
|
118
138
|
[DOMAspect.event]: () => void 0,
|
|
119
139
|
};
|
|
120
140
|
/**
|
|
121
|
-
*
|
|
141
|
+
* The central binding directive that bridges data expressions and DOM updates.
|
|
142
|
+
*
|
|
143
|
+
* HTMLBindingDirective fulfills three roles simultaneously:
|
|
144
|
+
* - **HTMLDirective**: Produces placeholder HTML via createHTML() during template authoring.
|
|
145
|
+
* - **ViewBehaviorFactory**: Creates behaviors (returns itself) during view creation.
|
|
146
|
+
* - **ViewBehavior / EventListener**: Attaches to a DOM node during bind, manages
|
|
147
|
+
* expression observers for reactive updates, and handles DOM events directly.
|
|
148
|
+
*
|
|
149
|
+
* The aspectType (set by HTMLDirective.assignAspect during template processing)
|
|
150
|
+
* determines which DOM "sink" function is used to apply values — e.g.,
|
|
151
|
+
* setAttribute for attributes, addEventListener for events, textContent for content.
|
|
152
|
+
*
|
|
122
153
|
* @public
|
|
123
154
|
*/
|
|
124
155
|
export class HTMLBindingDirective {
|
|
@@ -157,7 +188,18 @@ export class HTMLBindingDirective {
|
|
|
157
188
|
}
|
|
158
189
|
return this;
|
|
159
190
|
}
|
|
160
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* Attaches this binding to its target DOM node.
|
|
193
|
+
* - For events: stores the controller reference on the target element and registers
|
|
194
|
+
* this directive as the EventListener via addEventListener. The directive's
|
|
195
|
+
* handleEvent() method will be called when the event fires.
|
|
196
|
+
* - For content bindings: registers an unbind handler, then falls through to the
|
|
197
|
+
* default path.
|
|
198
|
+
* - For all non-event bindings: creates (or reuses) an ExpressionObserver, evaluates
|
|
199
|
+
* the binding expression, and applies the result to the DOM via the updateTarget
|
|
200
|
+
* sink function. The observer will call handleChange() on future data changes.
|
|
201
|
+
* @internal
|
|
202
|
+
*/
|
|
161
203
|
bind(controller) {
|
|
162
204
|
var _a;
|
|
163
205
|
const target = controller.targets[this.targetNodeId];
|
|
@@ -196,7 +238,14 @@ export class HTMLBindingDirective {
|
|
|
196
238
|
view.needsBindOnly = true;
|
|
197
239
|
}
|
|
198
240
|
}
|
|
199
|
-
/**
|
|
241
|
+
/**
|
|
242
|
+
* Implements the EventListener interface. When a DOM event fires on the target
|
|
243
|
+
* element, this method retrieves the ViewController stored on the element,
|
|
244
|
+
* sets the event on the ExecutionContext so `c.event` is available to the
|
|
245
|
+
* binding expression, and evaluates the expression. If the expression returns
|
|
246
|
+
* anything other than `true`, the event's default action is prevented.
|
|
247
|
+
* @internal
|
|
248
|
+
*/
|
|
200
249
|
handleEvent(event) {
|
|
201
250
|
const controller = event.currentTarget[this.data];
|
|
202
251
|
if (controller.isBound) {
|
|
@@ -208,7 +257,13 @@ export class HTMLBindingDirective {
|
|
|
208
257
|
}
|
|
209
258
|
}
|
|
210
259
|
}
|
|
211
|
-
/**
|
|
260
|
+
/**
|
|
261
|
+
* Called by the ExpressionObserver when a tracked dependency changes.
|
|
262
|
+
* Re-evaluates the binding expression via observer.bind() and pushes
|
|
263
|
+
* the new value to the DOM through the updateTarget sink function.
|
|
264
|
+
* This is the reactive update path that keeps the DOM in sync with data.
|
|
265
|
+
* @internal
|
|
266
|
+
*/
|
|
212
267
|
handleChange(binding, observer) {
|
|
213
268
|
const target = observer.target;
|
|
214
269
|
const controller = observer.controller;
|
|
@@ -29,7 +29,13 @@ export const HTMLDirective = Object.freeze({
|
|
|
29
29
|
return type;
|
|
30
30
|
},
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
32
|
+
* Determines the DOM aspect type for a directive based on attribute name prefix.
|
|
33
|
+
* The prefix convention maps to aspect types as follows:
|
|
34
|
+
* - No prefix (e.g. "class") → DOMAspect.attribute
|
|
35
|
+
* - ":" prefix (e.g. ":value") → DOMAspect.property (":classList" → DOMAspect.tokenList)
|
|
36
|
+
* - "?" prefix (e.g. "?disabled") → DOMAspect.booleanAttribute
|
|
37
|
+
* - `@` prefix (e.g. `@click`) → DOMAspect.event
|
|
38
|
+
* - Falsy or absent value → DOMAspect.content (see remarks)
|
|
33
39
|
* @param directive - The directive to assign the aspect to.
|
|
34
40
|
* @param value - The value to base the aspect determination on.
|
|
35
41
|
* @remarks
|
|
@@ -5,6 +5,12 @@ import { HydrationView } from "./view.js";
|
|
|
5
5
|
// and a hydrate method. Augmenting the hydration features is done by
|
|
6
6
|
// property assignment instead of class extension to better allow the
|
|
7
7
|
// hydration feature to be tree-shaken.
|
|
8
|
+
//
|
|
9
|
+
// When hydrate() is called, it creates a HydrationView that wraps the
|
|
10
|
+
// pre-rendered DOM range (firstChild → lastChild) instead of cloning a
|
|
11
|
+
// compiled DocumentFragment. The HydrationView will then use
|
|
12
|
+
// buildViewBindingTargets() to scan for hydration markers and attach
|
|
13
|
+
// reactive bindings to the existing DOM nodes.
|
|
8
14
|
Object.defineProperties(ViewTemplate.prototype, {
|
|
9
15
|
[Hydratable]: { value: Hydratable, enumerable: false, configurable: false },
|
|
10
16
|
hydrate: {
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A unique per-session random marker string used to create placeholder tokens in HTML.
|
|
3
|
+
* Bindings embedded in template literals are replaced with interpolation markers
|
|
4
|
+
* of the form `fast-xxxxxx{id}fast-xxxxxx` so the compiler can later locate them in the
|
|
5
|
+
* parsed DOM and associate each marker with its ViewBehaviorFactory.
|
|
6
|
+
*/
|
|
1
7
|
const marker = `fast-${Math.random().toString(36).substring(2, 8)}`;
|
|
2
8
|
const interpolationStart = `${marker}{`;
|
|
3
9
|
const interpolationEnd = `}${marker}`;
|
|
@@ -49,6 +55,8 @@ export const Parser = Object.freeze({
|
|
|
49
55
|
* directives or null if no directives are found in the string.
|
|
50
56
|
*/
|
|
51
57
|
parse(value, factories) {
|
|
58
|
+
// Split on the interpolation start marker. If there's only one part,
|
|
59
|
+
// no placeholders exist and we return null to signal "no directives here."
|
|
52
60
|
const parts = value.split(interpolationStart);
|
|
53
61
|
if (parts.length === 1) {
|
|
54
62
|
return null;
|
|
@@ -122,7 +122,25 @@ export class ViewTemplate {
|
|
|
122
122
|
return view;
|
|
123
123
|
}
|
|
124
124
|
/**
|
|
125
|
-
*
|
|
125
|
+
* Processes the tagged template literal's static strings and interpolated values and
|
|
126
|
+
* creates a ViewTemplate.
|
|
127
|
+
*
|
|
128
|
+
* For each interpolated value:
|
|
129
|
+
* 1. Functions (binding expressions, e.g., `x => x.name`) → wrapped in a one-way HTMLBindingDirective
|
|
130
|
+
* 2. Binding instances → wrapped in an HTMLBindingDirective
|
|
131
|
+
* 3. HTMLDirective instances → used as-is
|
|
132
|
+
* 4. Static values (strings, numbers) → wrapped in a one-time HTMLBindingDirective
|
|
133
|
+
*
|
|
134
|
+
* Each directive's createHTML() is called with an `add` callback that registers
|
|
135
|
+
* the factory in the factories record under a unique ID and returns that ID.
|
|
136
|
+
* The directive inserts a placeholder marker (e.g., `fast-abc123{0}fast-abc123`) into
|
|
137
|
+
* the HTML string so the compiler can later find and associate it with the factory.
|
|
138
|
+
*
|
|
139
|
+
* Aspect detection happens here too: the `lastAttributeNameRegex` checks whether
|
|
140
|
+
* the placeholder appears inside an attribute value, and if so, assignAspect()
|
|
141
|
+
* sets the correct DOMAspect (attribute, property, event, etc.) based on the
|
|
142
|
+
* attribute name prefix.
|
|
143
|
+
*
|
|
126
144
|
* @param strings - The static strings to create the template with.
|
|
127
145
|
* @param values - The dynamic values to create the template with.
|
|
128
146
|
* @param policy - The DOMPolicy to associated with the template.
|
|
@@ -177,6 +177,17 @@ export class HTMLView extends DefaultExecutionContext {
|
|
|
177
177
|
}
|
|
178
178
|
/**
|
|
179
179
|
* Binds a view's behaviors to its binding source.
|
|
180
|
+
*
|
|
181
|
+
* On the first call, this iterates through all compiled factories, calling
|
|
182
|
+
* createBehavior() on each to produce a ViewBehavior instance (e.g., an
|
|
183
|
+
* HTMLBindingDirective), and then immediately binds it. This is where event
|
|
184
|
+
* listeners are registered, expression observers are created, and initial
|
|
185
|
+
* DOM values are set.
|
|
186
|
+
*
|
|
187
|
+
* On subsequent calls with a new source, existing behaviors are re-bound
|
|
188
|
+
* to the new data source, which re-evaluates all binding expressions and
|
|
189
|
+
* updates the DOM accordingly.
|
|
190
|
+
*
|
|
180
191
|
* @param source - The binding source for the view's binding behaviors.
|
|
181
192
|
* @param context - The execution context to run the behaviors within.
|
|
182
193
|
*/
|
|
@@ -186,6 +197,8 @@ export class HTMLView extends DefaultExecutionContext {
|
|
|
186
197
|
}
|
|
187
198
|
let behaviors = this.behaviors;
|
|
188
199
|
if (behaviors === null) {
|
|
200
|
+
// First bind: create behaviors from factories and bind each one.
|
|
201
|
+
// The view (this) acts as the ViewController, providing targets and source.
|
|
189
202
|
this.source = source;
|
|
190
203
|
this.context = context;
|
|
191
204
|
this.behaviors = behaviors = new Array(this.factories.length);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Observable, observable } from "../observation/observable.js";
|
|
2
|
+
class ChildModel {
|
|
3
|
+
}
|
|
4
|
+
observable(ChildModel.prototype, "value");
|
|
5
|
+
ChildModel.prototype.value = "value";
|
|
6
|
+
class Model {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.childChangedCalled = false;
|
|
9
|
+
}
|
|
10
|
+
childChanged() {
|
|
11
|
+
this.childChangedCalled = true;
|
|
12
|
+
}
|
|
13
|
+
incrementTrigger() {
|
|
14
|
+
this.trigger++;
|
|
15
|
+
}
|
|
16
|
+
decrementTrigger() {
|
|
17
|
+
this.trigger--;
|
|
18
|
+
}
|
|
19
|
+
get ifConditional() {
|
|
20
|
+
Observable.trackVolatile();
|
|
21
|
+
if (this.trigger < 1) {
|
|
22
|
+
return 42;
|
|
23
|
+
}
|
|
24
|
+
return this.value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
observable(Model.prototype, "child");
|
|
28
|
+
Model.prototype.child = new ChildModel();
|
|
29
|
+
observable(Model.prototype, "child2");
|
|
30
|
+
Model.prototype.child2 = new ChildModel();
|
|
31
|
+
observable(Model.prototype, "trigger");
|
|
32
|
+
Model.prototype.trigger = 0;
|
|
33
|
+
observable(Model.prototype, "value");
|
|
34
|
+
Model.prototype.value = 10;
|
|
35
|
+
Object.defineProperty(Model.prototype, "ternaryConditional", {
|
|
36
|
+
get() {
|
|
37
|
+
Observable.trackVolatile();
|
|
38
|
+
return this.trigger < 1 ? 42 : this.value;
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
Object.defineProperty(Model.prototype, "andCondition", {
|
|
42
|
+
get() {
|
|
43
|
+
Observable.trackVolatile();
|
|
44
|
+
return this.trigger && this.value;
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
class DerivedModel extends Model {
|
|
48
|
+
constructor() {
|
|
49
|
+
super(...arguments);
|
|
50
|
+
this.child2ChangedCalled = false;
|
|
51
|
+
}
|
|
52
|
+
child2Changed() {
|
|
53
|
+
this.child2ChangedCalled = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
observable(DerivedModel.prototype, "derivedChild");
|
|
57
|
+
DerivedModel.prototype.derivedChild = new ChildModel();
|
|
58
|
+
export { ChildModel, DerivedModel, Model };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"metadata": {
|
|
3
3
|
"toolPackage": "@microsoft/api-extractor",
|
|
4
|
-
"toolVersion": "7.
|
|
4
|
+
"toolVersion": "7.57.7",
|
|
5
5
|
"schemaVersion": 1011,
|
|
6
6
|
"oldestForwardsCompatibleVersion": 1001,
|
|
7
7
|
"tsdocConfig": {
|
|
@@ -114,6 +114,22 @@
|
|
|
114
114
|
"tagName": "@virtual",
|
|
115
115
|
"syntaxKind": "modifier"
|
|
116
116
|
},
|
|
117
|
+
{
|
|
118
|
+
"tagName": "@jsx",
|
|
119
|
+
"syntaxKind": "block"
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
"tagName": "@jsxRuntime",
|
|
123
|
+
"syntaxKind": "block"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"tagName": "@jsxFrag",
|
|
127
|
+
"syntaxKind": "block"
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"tagName": "@jsxImportSource",
|
|
131
|
+
"syntaxKind": "block"
|
|
132
|
+
},
|
|
117
133
|
{
|
|
118
134
|
"tagName": "@betaDocumentation",
|
|
119
135
|
"syntaxKind": "modifier"
|
|
@@ -10541,7 +10557,7 @@
|
|
|
10541
10557
|
{
|
|
10542
10558
|
"kind": "Class",
|
|
10543
10559
|
"canonicalReference": "@microsoft/fast-element!HTMLBindingDirective:class",
|
|
10544
|
-
"docComment": "/**\n *
|
|
10560
|
+
"docComment": "/**\n * The central binding directive that bridges data expressions and DOM updates.\n *\n * HTMLBindingDirective fulfills three roles simultaneously: - **HTMLDirective**: Produces placeholder HTML via createHTML() during template authoring. - **ViewBehaviorFactory**: Creates behaviors (returns itself) during view creation. - **ViewBehavior / EventListener**: Attaches to a DOM node during bind, manages expression observers for reactive updates, and handles DOM events directly.\n *\n * The aspectType (set by HTMLDirective.assignAspect during template processing) determines which DOM \"sink\" function is used to apply values — e.g., setAttribute for attributes, addEventListener for events, textContent for content.\n *\n * @public\n */\n",
|
|
10545
10561
|
"excerptTokens": [
|
|
10546
10562
|
{
|
|
10547
10563
|
"kind": "Content",
|
|
@@ -11807,7 +11823,7 @@
|
|
|
11807
11823
|
{
|
|
11808
11824
|
"kind": "Method",
|
|
11809
11825
|
"canonicalReference": "@microsoft/fast-element!HTMLView#bind:member(1)",
|
|
11810
|
-
"docComment": "/**\n * Binds a view's behaviors to its binding source.\n *\n * @param source - The binding source for the view's binding behaviors.\n *\n * @param context - The execution context to run the behaviors within.\n */\n",
|
|
11826
|
+
"docComment": "/**\n * Binds a view's behaviors to its binding source.\n *\n * On the first call, this iterates through all compiled factories, calling createBehavior() on each to produce a ViewBehavior instance (e.g., an HTMLBindingDirective), and then immediately binds it. This is where event listeners are registered, expression observers are created, and initial DOM values are set.\n *\n * On subsequent calls with a new source, existing behaviors are re-bound to the new data source, which re-evaluates all binding expressions and updates the DOM accordingly.\n *\n * @param source - The binding source for the view's binding behaviors.\n *\n * @param context - The execution context to run the behaviors within.\n */\n",
|
|
11811
11827
|
"excerptTokens": [
|
|
11812
11828
|
{
|
|
11813
11829
|
"kind": "Content",
|
|
@@ -23074,7 +23090,7 @@
|
|
|
23074
23090
|
{
|
|
23075
23091
|
"kind": "Method",
|
|
23076
23092
|
"canonicalReference": "@microsoft/fast-element!ViewTemplate.create:member(1)",
|
|
23077
|
-
"docComment": "/**\n *
|
|
23093
|
+
"docComment": "/**\n * Processes the tagged template literal's static strings and interpolated values and creates a ViewTemplate.\n *\n * For each interpolated value: 1. Functions (binding expressions, e.g., `x => x.name`) → wrapped in a one-way HTMLBindingDirective 2. Binding instances → wrapped in an HTMLBindingDirective 3. HTMLDirective instances → used as-is 4. Static values (strings, numbers) → wrapped in a one-time HTMLBindingDirective\n *\n * Each directive's createHTML() is called with an `add` callback that registers the factory in the factories record under a unique ID and returns that ID. The directive inserts a placeholder marker (e.g., `fast-abc123{0}fast-abc123`) into the HTML string so the compiler can later find and associate it with the factory.\n *\n * Aspect detection happens here too: the `lastAttributeNameRegex` checks whether the placeholder appears inside an attribute value, and if so, assignAspect() sets the correct DOMAspect (attribute, property, event, etc.) based on the attribute name prefix.\n *\n * @remarks\n *\n * This API should not be used directly under normal circumstances because constructing a template in this way, if not done properly, can open up the application to XSS attacks. When using this API, provide a strong DOMPolicy that can properly sanitize and also be sure to manually sanitize all static strings particularly if they can come from user input.\n *\n * @param strings - The static strings to create the template with.\n *\n * @param values - The dynamic values to create the template with.\n *\n * @param policy - The DOMPolicy to associated with the template.\n *\n * @returns A ViewTemplate.\n */\n",
|
|
23078
23094
|
"excerptTokens": [
|
|
23079
23095
|
{
|
|
23080
23096
|
"kind": "Content",
|