Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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": "minor",
"comment": "Add the svg tagged template literal, which creates template content in the SVG namespace so that SVG elements authored in nested templates, such as a repeat item template, render correctly.",
"packageName": "@microsoft/fast-element",
"email": "144495202+AKnassa@users.noreply.github.com",
"dependentChangeType": "none"
}
16 changes: 15 additions & 1 deletion packages/fast-element/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,20 @@ No DOM nodes are created at this point; compilation is deferred.

See [docs/architecture/html-tagged-template-literal.md](./docs/architecture/html-tagged-template-literal.md) for more detail on directives and the `Markup`/`Parser` helpers.

#### svg Tagged Template Literal

Each tagged template literal is parsed into its own `DocumentFragment`, so a template nested inside an `svg` element through a directive (for example the item template of a `repeat`) has no SVG parsing context and its elements would be created in the HTML namespace, where they never render. The `svg` tag builds the same `ViewTemplate` but marks it so that `compile()` parses its html inside a temporary `svg` element, which puts the resulting nodes in the SVG namespace:

```typescript
const template = html<MyElement>`
<svg viewBox="0 0 100 100">
${repeat(x => x.points, svg`<circle cx="${x => x.x}" cy="${x => x.y}" r="2"></circle>`)}
</svg>
`;
```

The template must not include the `svg` element itself. Elements written directly inside an `svg` element in the same `html` template need no special handling.

---

### ViewTemplate & Compiler
Expand Down Expand Up @@ -656,7 +670,7 @@ src/
│ ├── one-time.ts # oneTime
│ └── normalize.ts # normalizeBinding helper
├── templating/
│ ├── template.ts # ViewTemplate, html tag, InlineTemplateDirective
│ ├── template.ts # ViewTemplate, html/svg tags, InlineTemplateDirective
│ ├── compiler.ts # Compiler, CompilationContext
│ ├── view.ts # HTMLView, ElementView, SyntheticView
│ ├── html-directive.ts # HTMLDirective, ViewBehavior, ViewBehaviorFactory
Expand Down
8 changes: 4 additions & 4 deletions packages/fast-element/SIZES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Bundle sizes for `@microsoft/fast-element` exports.

| Export | Minified | Gzip | Brotli |
|--------|----------|------|--------|
| CDN Rollup Bundle | 78.61 KB | 23.40 KB | 20.77 KB |
| CDN Rollup Bundle | 79.08 KB | 23.51 KB | 20.88 KB |
| FASTElement (@microsoft/fast-element/fast-element.js) | 23.11 KB | 7.12 KB | 6.41 KB |
| Updates (@microsoft/fast-element/updates.js) | 473 B | 335 B | 290 B |
| Observable (@microsoft/fast-element/observable.js) | 6.75 KB | 2.51 KB | 2.23 KB |
Expand All @@ -15,11 +15,11 @@ Bundle sizes for `@microsoft/fast-element` exports.
| slotted (@microsoft/fast-element/slotted.js) | 4.66 KB | 1.81 KB | 1.59 KB |
| volatile (@microsoft/fast-element/volatile.js) | 6.84 KB | 2.54 KB | 2.26 KB |
| when (@microsoft/fast-element/when.js) | 1.88 KB | 731 B | 589 B |
| html (@microsoft/fast-element/html.js) | 27.73 KB | 8.96 KB | 8.02 KB |
| html (@microsoft/fast-element/html.js) | 28.06 KB | 9.05 KB | 8.11 KB |
| repeat (@microsoft/fast-element/repeat.js) | 31.80 KB | 10.00 KB | 9.03 KB |
| css (@microsoft/fast-element/css.js) | 2.43 KB | 1.00 KB | 911 B |
| enableHydration (@microsoft/fast-element/hydration.js) | 46.53 KB | 13.91 KB | 12.49 KB |
| declarativeTemplate (@microsoft/fast-element/declarative.js) | 62.15 KB | 19.46 KB | 17.42 KB |
| enableHydration (@microsoft/fast-element/hydration.js) | 46.86 KB | 14.02 KB | 12.59 KB |
| declarativeTemplate (@microsoft/fast-element/declarative.js) | 62.48 KB | 19.56 KB | 17.50 KB |
| ArrayObserver (@microsoft/fast-element/arrays.js) | 12.55 KB | 4.46 KB | 4.03 KB |
| observerMap (@microsoft/fast-element/observer-map.js) | 21.96 KB | 7.73 KB | 6.97 KB |
| attributeMap (@microsoft/fast-element/attribute-map.js) | 15.31 KB | 5.41 KB | 4.88 KB |
7 changes: 5 additions & 2 deletions packages/fast-element/docs/api-report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,9 @@ export interface SubtreeDirectiveOptions<T = any> extends NodeBehaviorOptions<T>
subtree: boolean;
}

// @public
export const svg: <TSource = any, TParent = any>(strings: TemplateStringsArray, ...values: TemplateValue<TSource, TParent>[]) => ViewTemplate<TSource, TParent>;

// @public
export interface SyntheticView<TSource = any, TParent = any> extends View<TSource, TParent> {
readonly firstChild: Node;
Expand Down Expand Up @@ -1259,11 +1262,11 @@ export interface ViewController<TSource = any, TParent = any> extends Expression

// @public
export class ViewTemplate<TSource = any, TParent = any> implements ElementViewTemplate<TSource, TParent>, SyntheticViewTemplate<TSource, TParent> {
constructor(html: string | HTMLTemplateElement, factories?: Record<string, ViewBehaviorFactory>, policy?: DOMPolicy | undefined);
constructor(html: string | HTMLTemplateElement, factories?: Record<string, ViewBehaviorFactory>, policy?: DOMPolicy | undefined, isSVG?: boolean);
// @internal (undocumented)
compile(): HTMLTemplateCompilationResult<TSource, TParent>;
create(hostBindingTarget?: Element): HTMLView<TSource, TParent>;
static create<TSource = any, TParent = any>(strings: string[], values: TemplateValue<TSource, TParent>[], policy?: DOMPolicy): ViewTemplate<TSource, TParent>;
static create<TSource = any, TParent = any>(strings: string[], values: TemplateValue<TSource, TParent>[], policy?: DOMPolicy, isSVG?: boolean): ViewTemplate<TSource, TParent>;
readonly factories: Record<string, ViewBehaviorFactory>;
readonly html: string | HTMLTemplateElement;
inline(): CaptureType<TSource, TParent>;
Expand Down
4 changes: 2 additions & 2 deletions packages/fast-element/docs/declarative/api-report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,11 +414,11 @@ export interface ViewBehaviorFactory {

// @public
export class ViewTemplate<TSource = any, TParent = any> implements ElementViewTemplate<TSource, TParent>, SyntheticViewTemplate<TSource, TParent> {
constructor(html: string | HTMLTemplateElement, factories?: Record<string, ViewBehaviorFactory>, policy?: DOMPolicy | undefined);
constructor(html: string | HTMLTemplateElement, factories?: Record<string, ViewBehaviorFactory>, policy?: DOMPolicy | undefined, isSVG?: boolean);
// @internal (undocumented)
compile(): HTMLTemplateCompilationResult<TSource, TParent>;
create(hostBindingTarget?: Element): HTMLView<TSource, TParent>;
static create<TSource = any, TParent = any>(strings: string[], values: TemplateValue<TSource, TParent>[], policy?: DOMPolicy): ViewTemplate<TSource, TParent>;
static create<TSource = any, TParent = any>(strings: string[], values: TemplateValue<TSource, TParent>[], policy?: DOMPolicy, isSVG?: boolean): ViewTemplate<TSource, TParent>;
readonly factories: Record<string, ViewBehaviorFactory>;
readonly html: string | HTMLTemplateElement;
inline(): CaptureType<TSource, TParent>;
Expand Down
1 change: 1 addition & 0 deletions packages/fast-element/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ export {
html,
InlineTemplateDirective,
type SyntheticViewTemplate,
svg,
type TemplateValue,
ViewTemplate,
} from "./templating/template.js";
Expand Down
211 changes: 211 additions & 0 deletions packages/fast-element/src/templating/template.pw.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1514,3 +1514,214 @@ test.describe("The ViewTemplate", () => {
expect(result).toBe(true);
});
});

test.describe("The svg tag template helper", () => {
test("creates elements in the SVG namespace when used as a repeat item template", async ({
page,
}) => {
await page.goto("/");

const result = await page.evaluate(async () => {
// @ts-expect-error: Client module.
const { html, svg, repeat, Updates } = await import("/main.js");

const host = document.createElement("div");
document.body.appendChild(host);

const template = html`
<svg viewBox="0 0 100 100" width="100" height="100">
${repeat(
x => x.items,
svg`
<text x="0" y="20">${x => x.label}</text>
`,
)}
</svg>
`;

template.render({ items: [{ label: "hello" }] }, host);
await Updates.next();

const text = host.querySelector("text")!;
const result = {
namespaceURI: text.namespaceURI,
isSVGElement: text instanceof SVGElement,
isRendered:
text instanceof SVGGraphicsElement && text.getBBox().width > 0,
};

host.remove();
return result;
});

expect(result).toEqual({
namespaceURI: "http://www.w3.org/2000/svg",
isSVGElement: true,
isRendered: true,
});
});

test("binds attributes on SVG elements", async ({ page }) => {
await page.goto("/");

const result = await page.evaluate(async () => {
// @ts-expect-error: Client module.
const { html, svg, repeat, Observable, Updates } = await import("/main.js");

const host = document.createElement("div");
document.body.appendChild(host);

const item: { cx: number } = {} as any;
Observable.defineProperty(item, "cx");
item.cx = 10;
const source = { items: [item] };

const template = html`
<svg viewBox="0 0 100 100">
${repeat(
x => x.items,
svg`
<circle cx="${x => x.cx}" cy="10" r="5"></circle>
`,
)}
</svg>
`;

template.render(source, host);
await Updates.next();

const circle = host.querySelector("circle")!;
const initial = circle.getAttribute("cx");

item.cx = 42;
await Updates.next();

const result = { initial, updated: circle.getAttribute("cx") };
host.remove();
return result;
});

expect(result).toEqual({ initial: "10", updated: "42" });
});

test("creates foreignObject children in the HTML namespace", async ({ page }) => {
await page.goto("/");

const result = await page.evaluate(async () => {
// @ts-expect-error: Client module.
const { html, svg, repeat, Updates } = await import("/main.js");

const host = document.createElement("div");
document.body.appendChild(host);

const template = html`
<svg viewBox="0 0 100 100">
${repeat(
x => x.items,
svg`
<foreignObject width="100" height="100">
<div>${x => x.label}</div>
</foreignObject>
`,
)}
</svg>
`;

template.render({ items: [{ label: "hello" }] }, host);
await Updates.next();

const result = {
foreignObject: host.querySelector("foreignObject")!.namespaceURI,
div: host.querySelector("div")!.namespaceURI,
};

host.remove();
return result;
});

expect(result).toEqual({
foreignObject: "http://www.w3.org/2000/svg",
div: "http://www.w3.org/1999/xhtml",
});
});

test("creates elements in the SVG namespace for nested directives", async ({
page,
}) => {
await page.goto("/");

const result = await page.evaluate(async () => {
// @ts-expect-error: Client module.
const { html, svg, repeat, when, Updates } = await import("/main.js");

const host = document.createElement("div");
document.body.appendChild(host);

const template = html`
<svg viewBox="0 0 100 100">
${repeat(
x => x.items,
svg`
<g>
${when(
x => x.visible,
svg`
<circle cx="10" cy="10" r="5"></circle>
`,
)}
</g>
`,
)}
</svg>
`;

template.render({ items: [{ visible: true }] }, host);
await Updates.next();

const result = {
g: host.querySelector("g")!.namespaceURI,
circle: host.querySelector("circle")!.namespaceURI,
};

host.remove();
return result;
});

expect(result).toEqual({
g: "http://www.w3.org/2000/svg",
circle: "http://www.w3.org/2000/svg",
});
});

test("does not change the namespace of elements created by the html tag", async ({
page,
}) => {
await page.goto("/");

const result = await page.evaluate(async () => {
// @ts-expect-error: Client module.
const { html, repeat, Updates } = await import("/main.js");

const host = document.createElement("div");
document.body.appendChild(host);

const template = html`
${repeat(
x => x.items,
html`
<span>${x => x.label}</span>
`,
)}
`;

template.render({ items: [{ label: "hello" }] }, host);
await Updates.next();

const result = host.querySelector("span")!.namespaceURI;
host.remove();
return result;
});

expect(result).toBe("http://www.w3.org/1999/xhtml");
});
});
Loading