fix(fast-html): bind event handlers to correct host inside f-repeat#7331
fix(fast-html): bind event handlers to correct host inside f-repeat#7331mohamedmansour wants to merge 9 commits intomicrosoft:mainfrom
Conversation
8e9458c to
11db239
Compare
There was a problem hiding this comment.
In a repeat directive, the template (second arg) expressions are bound to the result of the data binding result (first arg):
type Item = string;
type Items = Item[];
class MyElement extends FASTElement {
public items: Items = ["one", "two", "three"];
public someValue: string = "some value";
public itemClickHandler(event: Event) {
console.log("item clicked", event);
}
}
export const template = html<MyElement>`
${repeat(
x => x.items, // <-- the data binding object (aka `x` for bindings within the repeat's template)
html<Item, MyElement>`
<!-- the template data binding is `Item`, the template binding context is `MyElement` -->
<button
@click="${(currentItem: Item, thisBindingContext) =>
thisBindingContext.parent.itemClickHandler(thisBindingContext.event)}"
>
${
// bindingContext allows access to the parent in any binding, anywhere in template expressions
(currentItem, bindingContext) => bindingContext.parent.someValue
}
</button>
`
)}
`;In FAST HTML, we need to add a way to access the template binding context from within the repeat template, instead of re-contextualizing all bindings within an f-repeat. Providing access to the context object in any binding expression in declarative templates would be the right way to approach this problem.
## Summary
Adds `c.` prefix support in declarative binding expressions to access
the repeat directive's ExecutionContext. This lets event handlers inside
`<f-repeat>` explicitly reference the host element via `c.parent` and
the DOM event via `c.event`, matching how `repeat()` works in
imperative fast-element templates. Fixes microsoft#7329.
## Changes
- **`utilities.ts`**: Added `c.` prefix detection in `pathResolver`
and `bindingResolver`. When a path starts with `c.`, it resolves
directly from the ExecutionContext (skipping schema tracking and
automatic level-based context walking).
- **`template.ts`**: Updated event binding to detect `c.` prefix on
the method path. When present, the `.bind()` target is computed
from the context (e.g. `c.parent`) instead of defaulting to `x`.
## What still works
- **Existing bindings unchanged**: Non-`c.` prefixed paths work
exactly as before — level-based parent walking is untouched.
- **All binding types**: `c.` prefix works in content (`{{c.parent.val}}`),
attribute (`:prop="{c.parent.val}"`), and event bindings.
- **All existing tests pass** (197 Chromium tests).
11db239 to
64e409d
Compare
| <ul> | ||
| <f-repeat value="{{item in items}}"> | ||
| <li> | ||
| <button @click="{c.parent.handleItemClick(c.event)}">{{item.name}}</button> |
There was a problem hiding this comment.
Aren't we doing $c as a prefix for the context when passing arguments to an event handler (#7287)? Also, I would think that item.handleItemClick($c.event) would result in trying to find a function on the array item, and handleItemClick($c.event) would result in finding a function on the test-element-repeat-event custom element. This is in line with how our other bindings work, see {{item.name}} which targets the array item, and {{name}} which would target a name property on the class.
If we provide $c as a generic construct for our dot syntax that is one thing, but using c seems unintuitive and risky.
What are we trying to solve here exactly?
There was a problem hiding this comment.
I've updated and changed c to $c, and added documentation for using $c.
Pull Request
📖 Description
Inside
<f-repeat>, the data source (x) is the repeat item rather than the parent template. Event handlers declared in the template are bound to whateverxis, which works fine at the top level (wherexis the host), but inside a repeat block there was no way to reference or bind events to the host element. The same gap applied to<f-when>conditions nested inside<f-repeat>that needed to evaluate a property on the host.This PR introduces a
$cprefix for declarative binding expressions that maps to the execution context argument (c) from imperative fast-element template bindings. In JavaScript templates, every binding expression receives both the data source and the context —${(x, c) => c.parent.handleClick(c.event)}— but declarative<f-template>expressions had no equivalent way to accessc. The$cprefix fills that gap, allowing expressions like{{$c.parent.handleClick($c.event)}}and{{$c.parent.showNames}}to work in declarative templates the same way they would in code.🎫 Issues
👩💻 Reviewer Notes
The core change is small and scoped to three touch points:
pathResolver(utilities.ts) — when a split path starts with$c, it returns a function that resolves fromcontextdirectly instead of walking the accessor chain.bindingResolver(utilities.ts) — same early-return for$c.prefixed paths, andexpressionResolverskips schema tracking for$c.paths since they don't correspond to observed properties on the data model.template.tsevent binding — detects$c.prefix on the method path and computes the.bind()target by walking the context (e.g.,context.parent) instead of defaulting to the repeat itemx.Non-
$cpaths are completely untouched — all existing binding behavior is preserved.📑 Test Plan
New Playwright test fixture (
test/fixtures/repeat-event/) covers two scenarios:f-repeat: dynamically populates items, clicks a button, and assertsclickedItemNameis set on the host element (provingthisis bound correctly).f-whenwith$c.parentinsidef-repeat: verifies conditional rendering based on a host property, event handling through the nestedf-when, and toggling the condition to remove elements.All 197 existing Chromium tests continue to pass.
✅ Checklist
General
$ npm run change