Skip to content

Commit b3da3df

Browse files
committed
Implement InnerTemplatePart class
The [3.3. Conditionals and Loops using Nested Templates][] section of the specification mentions special treatment of `<template>` elements with `[directive]` and `[expression]` attributes within `<template>` elements. They're to be treated as parts of their own, represented by an `InnerTemplatePart` interface: ```ts InnerTemplatePart : NodeTemplatePart { HTMLTemplateElement template; attribute DOMString directive; } ``` This commit introduces that class, along with special treatment whenever collecting parts from an `HTMLTemplateElement` that also has a `[directive]` attribute. To demonstrate their utility, this commit includes a test case that exercises a naive implementation of an `if` conditional. As a caveat, it's worth mentioning that the specification proposal explicitly mentions the nuance surrounding looping and conditional rendering: > this approach involves the template process callback cloning template > parts along with other nodes, or let author scripts manually specify to > which element each template part belongs. This quickly becomes an > entangled mess because now we could have multiple template parts that > refer to a single DOM location or an attribute, and we have to start > dealing with multiple template parts trying to override one another even > though there is no good use case for such a behavior. > > We like the idea of supporting very basic control flow such as `if` and > `foreach` in the default template process callback but we don't think it's > a show stopper if the default template process callback didn't support > them in the initial cut. This commit does not aim to introduce a canonical implementation for conditionals or looping, but it should enable a change like that in the future. [3.3. Conditionals and Loops using Nested Templates]: https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Template-Instantiation.md#33-conditionals-and-loops-using-nested-templates
1 parent 79b4ee6 commit b3da3df

File tree

5 files changed

+72
-9
lines changed

5 files changed

+72
-9
lines changed

src/inner-template-part.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import {NodeTemplatePart} from './node-template-part.js'
2+
3+
export class InnerTemplatePart extends NodeTemplatePart {
4+
constructor(public template: HTMLTemplateElement) {
5+
super(template, template.getAttribute('expression') ?? '')
6+
}
7+
8+
get directive(): string {
9+
return this.template.getAttribute('directive') ?? ''
10+
}
11+
}

src/processors.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import type {TemplatePart, TemplateTypeInit} from './types.js'
22
import type {TemplateInstance} from './template-instance.js'
33
import {AttributeTemplatePart} from './attribute-template-part.js'
44

5-
type PartProcessor = (part: TemplatePart, value: unknown) => void
5+
type PartProcessor = (part: TemplatePart, value: unknown, state: unknown) => void
66

77
export function createProcessor(processPart: PartProcessor): TemplateTypeInit {
88
return {
9-
processCallback(_: TemplateInstance, parts: Iterable<TemplatePart>, params: unknown): void {
10-
if (typeof params !== 'object' || !params) return
9+
processCallback(_: TemplateInstance, parts: Iterable<TemplatePart>, state: unknown): void {
10+
if (typeof state !== 'object' || !state) return
1111
for (const part of parts) {
12-
if (part.expression in params) {
13-
const value = (params as Record<string, unknown>)[part.expression] ?? ''
14-
processPart(part, value)
12+
if (part.expression in state) {
13+
const value = (state as Record<string, unknown>)[part.expression] ?? ''
14+
processPart(part, value, state)
1515
}
1616
}
1717
},

src/template-instance.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {parse} from './template-string-parser.js'
22
import {AttributeValueSetter, AttributeTemplatePart} from './attribute-template-part.js'
3+
import {InnerTemplatePart} from './inner-template-part.js'
34
import {NodeTemplatePart} from './node-template-part.js'
45
import {propertyIdentity} from './processors.js'
56
import {TemplatePart, TemplateTypeInit} from './types.js'
@@ -9,8 +10,12 @@ function* collectParts(el: DocumentFragment): Generator<TemplatePart> {
910
let node
1011
while ((node = walker.nextNode())) {
1112
if (node instanceof HTMLTemplateElement) {
12-
for (const part of collectParts(node.content)) {
13-
yield part
13+
if (node.hasAttribute('directive')) {
14+
yield new InnerTemplatePart(node)
15+
} else {
16+
for (const part of collectParts(node.content)) {
17+
yield part
18+
}
1419
}
1520
} else if (node instanceof Element && node.hasAttributes()) {
1621
for (let i = 0; i < node.attributes.length; i += 1) {

test/processors.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {expect} from '@open-wc/testing'
22
import {TemplateInstance} from '../src/template-instance'
3+
import {InnerTemplatePart} from '../src/inner-template-part'
34
import type {TemplateTypeInit} from '../src/types'
45
import {createProcessor} from '../src/processors'
56
describe('createProcessor', () => {
@@ -29,4 +30,23 @@ describe('createProcessor', () => {
2930
instance.update({y: 'world'})
3031
expect(calls).to.eql(0)
3132
})
33+
34+
describe('handling InnerTemplatePart', () => {
35+
beforeEach(() => {
36+
processor = createProcessor(part => {
37+
if (part instanceof InnerTemplatePart) calls += 1
38+
})
39+
})
40+
41+
it('detects InnerTemplatePart instances with <template> element', () => {
42+
template.innerHTML = '<template directive="if" expression="x">{{x}}</template>'
43+
new TemplateInstance(template, {x: true}, processor)
44+
expect(calls).to.eql(1)
45+
})
46+
47+
it('does not detect InnerTemplatePart instances without <template> element', () => {
48+
new TemplateInstance(template, {x: true}, processor)
49+
expect(calls).to.eql(0)
50+
})
51+
})
3252
})

test/template-instance.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import {expect} from '@open-wc/testing'
22
import {TemplateInstance} from '../src/template-instance'
33
import {NodeTemplatePart} from '../src/node-template-part'
4-
import {propertyIdentityOrBooleanAttribute, createProcessor} from '../src/processors'
4+
import {InnerTemplatePart} from '../src/inner-template-part'
5+
import {processPropertyIdentity, propertyIdentityOrBooleanAttribute, createProcessor} from '../src/processors'
56

67
describe('template-instance', () => {
78
it('applies data to templated text nodes', () => {
@@ -354,5 +355,31 @@ describe('template-instance', () => {
354355
expect(processCallCount).to.equal(2)
355356
})
356357
})
358+
359+
describe('handling InnerTemplatePart', () => {
360+
it('makes outer state available to inner parts', () => {
361+
const processor = createProcessor((part, value, state) => {
362+
if (part instanceof InnerTemplatePart && part.directive === 'if') {
363+
if (typeof state === 'object' && (state as Record<string, unknown>)[part.expression]) {
364+
part.replace(new TemplateInstance(part.template, state, processor))
365+
} else {
366+
part.replace()
367+
}
368+
} else {
369+
processPropertyIdentity(part, value)
370+
}
371+
})
372+
const template = Object.assign(document.createElement('template'), {
373+
innerHTML: '{{x}}<template directive="if" expression="y">{{y}}</template>',
374+
})
375+
376+
const root = document.createElement('div')
377+
root.appendChild(new TemplateInstance(template, {x: 'x', y: 'y'}, processor))
378+
expect(root.innerHTML).to.equal('xy')
379+
380+
root.replaceChildren(new TemplateInstance(template, {x: 'x', y: false}, processor))
381+
expect(root.innerHTML).to.equal('x')
382+
})
383+
})
357384
})
358385
})

0 commit comments

Comments
 (0)