Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "",
"packageName": "@microsoft/fast-element",
"email": "hello@mohamedmansour.com",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "",
"packageName": "@microsoft/fast-html",
"email": "hello@mohamedmansour.com",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ function updateContent(
) {
const viewNodes = controller.bindingViewBoundaries[this.targetNodeId];
view = value.hydrate(viewNodes.first, viewNodes.last);
} else if (
isHydratable(controller) &&
controller.hydrationStage !== HydrationStage.hydrated &&
controller.bindingViewBoundaries[this.targetNodeId] === undefined
) {
// During hydration, if a template is provided but no boundaries
// exist, the server did not render this content. Skip creating
// a new view to avoid a hydration mismatch.
return;
} else {
view = value.create();
}
Expand Down
12 changes: 11 additions & 1 deletion packages/fast-html/src/components/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -963,7 +963,17 @@ export function resolveWhen(
level,
schema
);
return (x: boolean, c: any) => binding(x, c);
return (x: boolean, c: any) => {
const result = binding(x, c);
// During hydration, trust the server-rendered state. If the condition
// evaluates to falsy (e.g. property not defined on the client element),
// return true so the inner template is hydrated and its bindings
// (event listeners, etc.) are properly attached to the existing DOM.
if (!result && c?.hydrationStage && c.hydrationStage !== "hydrated") {
return true;
}
return result;
};
}

type DataType = "array" | "object" | "primitive";
Expand Down
20 changes: 20 additions & 0 deletions packages/fast-html/test/fixtures/when-event/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title></title>
<script type="module" src="./main.ts"></script>
</head>
<body>
<!-- Template uses "serverOnly" which is NOT a property on test-element.
This simulates <if condition="serverOnly"> where the server state has serverOnly=true
but the client class doesn't know about it. -->
<f-template name="test-element">
<template><f-when value="{{serverOnly}}"><button @click="{handleClick()}">Click me</button></f-when></template>
</f-template>
<!-- SSR: condition was true, so button is rendered with hydration markers -->
<test-element id="when-event-show">
<template shadowrootmode="open"><!--fe-b$$start$$0$$MRl5Rw6tl3$$fe-b--><button data-fe-b-0>Click me</button><!--fe-b$$end$$0$$MRl5Rw6tl3$$fe-b--></template>
</test-element>
</body>
</html>
22 changes: 22 additions & 0 deletions packages/fast-html/test/fixtures/when-event/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { RenderableFASTElement, TemplateElement } from "@microsoft/fast-html";
import { FASTElement } from "@microsoft/fast-element";

// This element intentionally does NOT define "serverOnly" as a property.
// The f-when condition references "serverOnly" which only exists in server state,
// simulating the real-world scenario where <if condition="..."> uses server-only data.
class TestElement extends FASTElement {
public clickCount: number = 0;

public handleClick = (): void => {
this.clickCount++;
console.log("clicked:" + this.clickCount);
};
}
RenderableFASTElement(TestElement).defineAsync({
name: "test-element",
templateOptions: "defer-and-hydrate",
});

TemplateElement.define({
name: "f-template",
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<f-when value="{{serverOnly}}"><button @click="{handleClick()}">Click me</button></f-when>
3 changes: 3 additions & 0 deletions packages/fast-html/test/fixtures/when-event/when-event.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"serverOnly": true
}
23 changes: 23 additions & 0 deletions packages/fast-html/test/fixtures/when-event/when-event.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { expect, test } from "@playwright/test";

test.describe("f-when with event binding", async () => {
test("event binding inside f-when should fire after hydration", async ({ page }) => {
await page.goto("/fixtures/when-event/");

const customElement = page.locator("#when-event-show");

// Button should be visible (SSR rendered, condition true)
const button = customElement.locator("button");
await expect(button).toHaveText("Click me");

// Click the button - event binding should work
await button.click();

// Verify the click handler fired
await expect(customElement).toHaveJSProperty("clickCount", 1);

// Click again to confirm repeated clicks work
await button.click();
await expect(customElement).toHaveJSProperty("clickCount", 2);
});
});
Loading