├── .gitignore ├── README.md ├── demo.html ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── example-template-processor.ts ├── html-template-element-spec.ts ├── html-template-element.ts ├── template-assembly.ts ├── template-definition-spec.ts ├── template-definition.ts ├── template-instance.ts ├── template-part.ts ├── template-processor.ts ├── template-rule.ts ├── template-string-parser-spec.ts └── template-string-parser.ts ├── test.html ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | *.swp 4 | .DS_Store 5 | template-instantiation.js 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Template Instantiation Strawman Prollyfill 2 | 3 | Template Instantiation is a new web platform feature with ongoing, developing 4 | proposals coming out of a few corners of the ecosystem. 5 | 6 | The most developed proposal so far comes from Ryosuke Niwa at Apple and can be 7 | found [here](https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Template-Instantiation.md). 8 | 9 | This change proposes a Template Instantiation prollyfill implementation based 10 | on Ryosuke Niwa's proposal, and adapted with idioms and features found in 11 | the Polymer Team's own [lit-html](https://github.com/polymerlabs/lit-html). 12 | 13 | ## Concepts and Domain 14 | 15 | Template Instantiation is meant to offer a standard API for enabling some 16 | level of dynamism in otherwise static HTML templates. There are several 17 | relevant use cases outlined in [the Apple proposal](https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Template-Instantiation.md#2-use-cases). 18 | 19 | Conceptually, this implementation consists of many of the same constructs. 20 | However, there are a few notable differences. 21 | 22 | Compared to the Apple proposal, the implementation here is lacking: 23 | 24 | - Special support for "inner" templates via an `InnerTemplatePart`. 25 | - ~Ability to `replace` or `replaceHTML` on `NodeTemplatePart`.~ 26 | 27 | Areas where this implementation diverges from Apple's proposal include: 28 | 29 | - No template processor registry. The desired processor is passed by the user 30 | when creating an instance. 31 | - `TemplatePart` is updated by value, which has special meaning depending on 32 | the implementation of the derived `TemplatePart` derivative in question 33 | (a la lit-html). 34 | 35 | Features included in this implementation that are not covered by Apple's 36 | proposal: 37 | 38 | - An incremental construct pairing a parsed template and a future set of 39 | values to fill that template (`TemplateAssembly`) 40 | - ~Explicit support for `TemplateAssembly` as a value type that may be assigned 41 | to a `TemplateNodePart`.~ 42 | - ~Ability to customize `TemplatePart` creation via `TemplateProcessor`'s 43 | `partCalback` (a la lit-html)~ 44 | - No default implementation for `TemplateProcessor`, with the expectation that 45 | in v0 an author will be expected to provide their own. 46 | 47 | ### Template string parsing 48 | 49 | - Handled by a naive handlebars parser implementation. 50 | - Parser takes a templatized string, and returns a set of static strings and 51 | a set of expressions, very similarly to how JavaScript template literal 52 | tag function arguments work (although in this case the expressions are 53 | string literals and not values meant to be inserted directly). 54 | 55 | ### TemplateRule 56 | 57 | - Represents a spot/range in the otherwise static template that is deemed to 58 | be dynamic 59 | - Has a node index that is a walker-specific index of the node it cares about 60 | - Knows the rule(s) for the dynamic spot, the inner raw string text of 61 | the template part 62 | - Two specializations: `NodeTemplateRule` and `AttributeTemplateRule`. 63 | - A list of these is parsed from an `HTMLTemplateElement` and held by a 64 | `TemplateDefinition` 65 | 66 | ### TemplatePart 67 | 68 | - A value setter for a dynamic spot in a `TemplateInstance`. 69 | - Has a reference to a related `TemplateRule`, where it can access the 70 | related rule. 71 | - Has a reference to a node, which might refer to an attribute or a 72 | positionally relevant `Text` node somewhere. 73 | - Has a reference to its originating `TemplateInstance` (not sure why yet) 74 | - A list of these is generated by cross-referencing a set of `TemplateRule` 75 | instances with a related `DocumentFragment` and held by a 76 | `TemplateInstance`. 77 | 78 | ### TemplateDefinition 79 | 80 | - Prollyfill-specific construct, not yet obviously useful for authors 81 | - Unique per `HTMLTemplateElement` (for cacheability, cache can be overridden 82 | if desired). 83 | - Has a reference to HTMLTemplateElement 84 | - Has a parsed list of dynamic `TemplateRule` spots 85 | - Internally holds a reference to a "parsed" version of the static template 86 | for faster cloning into a `TemplateInstance`. 87 | 88 | ### TemplateAssembly 89 | 90 | - Maps closely to `lit-html`'s `TemplateResult`. 91 | - Has a `TemplateDefinition` instance. 92 | - Has some state that will eventually be set on a `TemplateInstance` generated 93 | from the `TemplateDefinition`. 94 | - Can be assigned as the value of a `NodeTemplatePart`, enabling 95 | `TemplateInstance` nesting. 96 | - Not sure how an author would actually create this yet. 97 | 98 | ### TemplateProcessor 99 | 100 | - Defines stateless-ish updates for template parts. 101 | - ~Implements `TemplatePart` creation via `partCallback` (working name 102 | borrowed from lit-html).~ 103 | - Implements `TemplatePart` updating via `processCallback` (working name 104 | borrowed from Apple's proposal). 105 | - Trivial example processor provided in this project 106 | 107 | ### TemplateInstance 108 | 109 | - Created using `TemplateDefinition#createInstance(processor, state?)` (not 110 | to be invoked directly by author at this time). 111 | - Has a reference to its `TemplateProcessor` 112 | - Has a list of `TemplatePart` instances, generated by cross-referencing the 113 | `TemplateDefinition`'s `TemplateRules` against its own content and 114 | invoking the `TemplateProcessor`'s `partCallback` method. 115 | - Has a reference to the previously set state (maybe useful in 116 | `TemplateProcessor`, which is otherwise stateless) 117 | - Can be updated by calling `TemplateInstance#update(newState)` 118 | - Can handle a `TemplateAssembly` as a state value 119 | 120 | ### HTMLTemplateElement 121 | 122 | - Invoking `HTMLTemplateElement#createInstance(processor, state?)` creates 123 | a `TemplateDefinition` for the `HTMLTemplateElement` if one does not exist, 124 | then invokes `TemplateDefinition#createInstance(processor, state?)` and 125 | returns that value. 126 | 127 | 128 | -------------------------------------------------------------------------------- /demo.html: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 31 | 32 | 57 | 58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "template-instantiation-prollyfill", 3 | "version": "0.0.4", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@polymer/ristretto": { 8 | "version": "0.3.1", 9 | "resolved": "https://registry.npmjs.org/@polymer/ristretto/-/ristretto-0.3.1.tgz", 10 | "integrity": "sha512-PzPJ2igfht8jmctbpd7bWlYxTsCMICf/QDPrZHFRHeDj7TtC01KOYHyt6RIcUZTotiBAugKAVDklZS7Fuq1msA==", 11 | "dev": true 12 | }, 13 | "@types/chai": { 14 | "version": "4.0.10", 15 | "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.10.tgz", 16 | "integrity": "sha512-Ejh1AXTY8lm+x91X/yar3G2z4x9RyKwdTVdyyu7Xj3dNB35fMNCnEWqTO9FgS3zjzlRNqk1MruYhgb8yhRN9rA==", 17 | "dev": true 18 | }, 19 | "assertion-error": { 20 | "version": "1.0.2", 21 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", 22 | "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", 23 | "dev": true 24 | }, 25 | "chai": { 26 | "version": "4.1.2", 27 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", 28 | "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", 29 | "dev": true, 30 | "requires": { 31 | "assertion-error": "1.0.2", 32 | "check-error": "1.0.2", 33 | "deep-eql": "3.0.1", 34 | "get-func-name": "2.0.0", 35 | "pathval": "1.1.0", 36 | "type-detect": "4.0.5" 37 | } 38 | }, 39 | "check-error": { 40 | "version": "1.0.2", 41 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 42 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 43 | "dev": true 44 | }, 45 | "deep-eql": { 46 | "version": "3.0.1", 47 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 48 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 49 | "dev": true, 50 | "requires": { 51 | "type-detect": "4.0.5" 52 | } 53 | }, 54 | "get-func-name": { 55 | "version": "2.0.0", 56 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 57 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 58 | "dev": true 59 | }, 60 | "pathval": { 61 | "version": "1.1.0", 62 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", 63 | "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", 64 | "dev": true 65 | }, 66 | "rollup": { 67 | "version": "0.56.3", 68 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.56.3.tgz", 69 | "integrity": "sha512-/iH4RfioboHgBjo7TbQcdMad/ifVGY/ToOB1AsW7oZHUhfhm+low6QlrImUSaJO1JqklOpWEKlD+b3MZYLuptA==", 70 | "dev": true 71 | }, 72 | "type-detect": { 73 | "version": "4.0.5", 74 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.5.tgz", 75 | "integrity": "sha512-N9IvkQslUGYGC24RkJk1ba99foK6TkwC2FHAEBlQFBP0RxQZS8ZpJuAZcwiY/w9ZJHFQb1aOXBI60OdxhTrwEQ==", 76 | "dev": true 77 | }, 78 | "typescript": { 79 | "version": "2.6.2", 80 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.6.2.tgz", 81 | "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=", 82 | "dev": true 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "template-instantiation-prollyfill", 3 | "version": "0.0.4", 4 | "description": "A prollyfill for HTML template instantiation", 5 | "main": "template-instantiation.js", 6 | "files": [ 7 | "lib/*" 8 | ], 9 | "scripts": { 10 | "build": "tsc", 11 | "watch": "tsc -w", 12 | "bundle": "rollup -c ./rollup.config.js", 13 | "prepublishOnly": "npm run build && npm run bundle" 14 | }, 15 | "author": "The Polymer Authors", 16 | "license": "BSD-3-Clause", 17 | "devDependencies": { 18 | "@polymer/ristretto": "^0.3.1", 19 | "@types/chai": "^4.0.10", 20 | "chai": "^4.1.2", 21 | "rollup": "^0.56.3", 22 | "typescript": "^2.6.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at 5 | * http://polymer.github.io/LICENSE.txt 6 | * The complete set of authors may be found at 7 | * http://polymer.github.io/AUTHORS.txt 8 | * The complete set of contributors may be found at 9 | * http://polymer.github.io/CONTRIBUTORS.txt 10 | * Code distributed by Google as part of the polymer project is also 11 | * subject to an additional IP rights grant found at 12 | * http://polymer.github.io/PATENTS.txt 13 | */ 14 | 15 | export default { 16 | input: './lib/html-template-element.js', 17 | output: { 18 | format: 'umd', 19 | file: './template-instantiation.js', 20 | name: 'TemplateInstantiation' 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/example-template-processor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at http:polymer.github.io/LICENSE.txt 5 | * The complete set of authors may be found at http:polymer.github.io/AUTHORS.txt 6 | * The complete set of contributors may be found at http:polymer.github.io/CONTRIBUTORS.txt 7 | * Code distributed by Google as part of the polymer project is also 8 | * subject to an additional IP rights grant found at http:polymer.github.io/PATENTS.txt 9 | */ 10 | 11 | import { TemplateProcessor } from './template-processor.js'; 12 | import { 13 | TemplatePart, 14 | NodeTemplatePart, 15 | AttributeTemplatePart, 16 | InnerTemplatePart 17 | } from './template-part.js'; 18 | import { 19 | NodeTemplateRule, 20 | AttributeTemplateRule 21 | } from './template-rule.js'; 22 | 23 | export class ExampleTemplateProcessor extends TemplateProcessor { 24 | createdCallback(_parts: TemplatePart[], _state?: any): void {} 25 | 26 | processCallback(parts: TemplatePart[], state?: any): void { 27 | for (const part of parts) { 28 | if (part instanceof InnerTemplatePart) { 29 | // TODO 30 | } else if (part instanceof NodeTemplatePart) { 31 | const { expression } = part.rule as NodeTemplateRule; 32 | part.value = state && expression && state[expression]; 33 | } else if (part instanceof AttributeTemplatePart) { 34 | const { expressions } = part.rule as AttributeTemplateRule; 35 | part.value = state && expressions && 36 | expressions.map(expression => state && state[expression]); 37 | } 38 | } 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /src/html-template-element-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at http:polymer.github.io/LICENSE.txt 5 | * The complete set of authors may be found at http:polymer.github.io/AUTHORS.txt 6 | * The complete set of contributors may be found at http:polymer.github.io/CONTRIBUTORS.txt 7 | * Code distributed by Google as part of the polymer project is also 8 | * subject to an additional IP rights grant found at http:polymer.github.io/PATENTS.txt 9 | */ 10 | 11 | import { Spec } from '../../@polymer/ristretto/lib/spec.js'; 12 | import { Fixturable } from 13 | '../../@polymer/ristretto/lib/mixins/fixturable.js'; 14 | import '../../chai/chai.js'; 15 | import { ExampleTemplateProcessor } from './example-template-processor.js'; 16 | import './html-template-element.js'; 17 | 18 | const spec = new (Fixturable(Spec))(); 19 | const { describe, it, fixture } = spec; 20 | const { expect } = chai; 21 | 22 | describe('HTMLTemplateElement', () => { 23 | it('has a method createInstance', () => { 24 | expect(document.createElement('template').createInstance).to.be.ok; 25 | }); 26 | 27 | describe('createInstance', () => { 28 | fixture(() => { 29 | const template = document.createElement('template'); 30 | template.innerHTML = `
{{content}}
`; 31 | return { template }; 32 | }); 33 | 34 | describe('without arguments', () => { 35 | it('throws due to missing processor', ({ template }: any) => { 36 | let threw = false; 37 | 38 | try { 39 | template.createInstance(); 40 | } catch (e) { 41 | threw = true; 42 | } 43 | 44 | expect(threw).to.be.equal(true); 45 | }); 46 | }); 47 | 48 | describe('given a processor', () => { 49 | fixture((context: any) => { 50 | return { ...context, processor: new ExampleTemplateProcessor }; 51 | }); 52 | 53 | it('returns a DocumentFragment', ({ template, processor }: any) => { 54 | const instance = template.createInstance(processor); 55 | expect(instance).to.be.instanceof(DocumentFragment); 56 | }); 57 | 58 | it('returns a TemplateInstance', ({ template, processor }: any) => { 59 | const instance = template.createInstance(processor); 60 | expect(instance).to.be.instanceof(DocumentFragment); 61 | }); 62 | 63 | describe('with initial state', () => { 64 | fixture((context: any) => { 65 | return { ...context, state: { content: 'Hello world.' } }; 66 | }); 67 | 68 | it('puts the state in the DOM', ({ template, processor, state }: any) => { 69 | const instance = template.createInstance(processor, state); 70 | expect(instance.childNodes[0].innerText).to.be.equal(state.content); 71 | }); 72 | }); 73 | }); 74 | }); 75 | }); 76 | 77 | export const htmlTemplateElementSpec: Spec = spec; 78 | -------------------------------------------------------------------------------- /src/html-template-element.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at http:polymer.github.io/LICENSE.txt 5 | * The complete set of authors may be found at http:polymer.github.io/AUTHORS.txt 6 | * The complete set of contributors may be found at http:polymer.github.io/CONTRIBUTORS.txt 7 | * Code distributed by Google as part of the polymer project is also 8 | * subject to an additional IP rights grant found at http:polymer.github.io/PATENTS.txt 9 | */ 10 | 11 | import { TemplateProcessor } from './template-processor.js'; 12 | import { TemplateDefinition } from './template-definition.js'; 13 | import { TemplateInstance } from './template-instance.js'; 14 | 15 | const templateDefinitionCache: Map = new Map(); 16 | 17 | declare global { 18 | interface HTMLTemplateElement { 19 | createInstance( 20 | processor: TemplateProcessor, 21 | state?: any, 22 | overrideDiagramCache?: boolean): TemplateInstance 23 | } 24 | } 25 | 26 | HTMLTemplateElement.prototype.createInstance = function( 27 | processor: TemplateProcessor, 28 | state?: any, 29 | overrideDefinitionCache = false): TemplateInstance { 30 | if (processor == null) { 31 | throw new Error('The first argument of createInstance must be an implementation of TemplateProcessor'); 32 | } 33 | 34 | if (!templateDefinitionCache.has(this) || overrideDefinitionCache) { 35 | templateDefinitionCache.set(this, new TemplateDefinition(this)); 36 | } 37 | 38 | const definition = templateDefinitionCache.get(this)!; 39 | 40 | return new TemplateInstance(definition, processor, state); 41 | }; 42 | -------------------------------------------------------------------------------- /src/template-assembly.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at http:polymer.github.io/LICENSE.txt 5 | * The complete set of authors may be found at http:polymer.github.io/AUTHORS.txt 6 | * The complete set of contributors may be found at http:polymer.github.io/CONTRIBUTORS.txt 7 | * Code distributed by Google as part of the polymer project is also 8 | * subject to an additional IP rights grant found at http:polymer.github.io/PATENTS.txt 9 | */ 10 | 11 | import { TemplateDefinition } from './template-definition.js'; 12 | import { TemplateProcessor } from './template-processor.js'; 13 | 14 | export class TemplateAssembly { 15 | constructor(public definition: TemplateDefinition, 16 | public processor: TemplateProcessor, 17 | public state?: any) {} 18 | }; 19 | -------------------------------------------------------------------------------- /src/template-definition-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at http:polymer.github.io/LICENSE.txt 5 | * The complete set of authors may be found at http:polymer.github.io/AUTHORS.txt 6 | * The complete set of contributors may be found at http:polymer.github.io/CONTRIBUTORS.txt 7 | * Code distributed by Google as part of the polymer project is also 8 | * subject to an additional IP rights grant found at http:polymer.github.io/PATENTS.txt 9 | */ 10 | 11 | import { Spec } from '../../@polymer/ristretto/lib/spec.js'; 12 | import { Fixturable } from '../../@polymer/ristretto/lib/mixins/fixturable.js'; 13 | import '../../chai/chai.js'; 14 | import { TemplateDefinition } from './template-definition.js'; 15 | import { NodeTemplateRule, AttributeTemplateRule } from './template-rule.js'; 16 | 17 | const spec = new (Fixturable(Spec))(); 18 | const { describe, it, fixture } = spec; 19 | const { expect } = chai; 20 | 21 | describe('TemplateDefinition', () => { 22 | describe('with an empty template', () => { 23 | fixture(() => { 24 | const template = document.createElement('template'); 25 | template.innerHTML = ``; 26 | return { 27 | template, 28 | definition: new TemplateDefinition(template) 29 | }; 30 | }); 31 | 32 | it('generates no rules', (context: any) => { 33 | const { definition } = context; 34 | expect(definition.rules.length).to.be.equal(0); 35 | }); 36 | }); 37 | 38 | describe('with a dynamic node part', () => { 39 | fixture(() => { 40 | const template = document.createElement('template'); 41 | template.innerHTML = `
{{foo}}
`; 42 | return { 43 | template, 44 | definition: new TemplateDefinition(template) 45 | }; 46 | }); 47 | 48 | it('generates a node sentinel', (context: any) => { 49 | const { definition } = context; 50 | expect(definition.rules.length).to.be.equal(1); 51 | expect(definition.rules[0]).to.be.instanceof(NodeTemplateRule); 52 | }); 53 | }); 54 | 55 | describe('with a dynamic attribute part', () => { 56 | fixture(() => { 57 | const template = document.createElement('template'); 58 | template.innerHTML = `
`; 59 | return { 60 | template, 61 | definition: new TemplateDefinition(template) 62 | }; 63 | }); 64 | 65 | it('generates an attribute sentinel', (context: any) => { 66 | const { definition } = context; 67 | expect(definition.rules.length).to.be.equal(1); 68 | expect(definition.rules[0]).to.be.instanceof(AttributeTemplateRule); 69 | }); 70 | }); 71 | 72 | describe('with a variety of dynamic parts', () => { 73 | fixture(() => { 74 | const template = document.createElement('template'); 75 | template.innerHTML = ` 76 |
{{baz}}
77 | prefix {{qux}} suffix 78 | 79 | {{lur}} 80 | `; 81 | return { 82 | template, 83 | definition: new TemplateDefinition(template) 84 | }; 85 | }); 86 | 87 | it('generates several rules', (context: any) => { 88 | const { definition } = context; 89 | const { rules } = definition; 90 | 91 | expect(rules.length).to.be.equal(5); 92 | }); 93 | 94 | it('generates rules in tree order', (context: any) => { 95 | const { definition } = context; 96 | const { rules } = definition; 97 | const [ bar, baz, qux, rak, lur ] = rules; 98 | 99 | expect(bar).to.be.instanceof(AttributeTemplateRule); 100 | expect(bar.expressions).to.be.eql(['bar']); 101 | 102 | expect(baz).to.be.instanceof(NodeTemplateRule); 103 | expect(baz.expression).to.be.equal('baz'); 104 | 105 | expect(qux).to.be.instanceof(NodeTemplateRule); 106 | expect(qux.expression).to.be.equal('qux'); 107 | 108 | expect(rak).to.be.instanceof(AttributeTemplateRule); 109 | expect(rak.expressions).to.be.eql(['rak']); 110 | 111 | expect(lur).to.be.instanceof(NodeTemplateRule); 112 | expect(lur.expression).to.be.eql('lur'); 113 | }); 114 | }); 115 | }); 116 | 117 | export const templateDefinitionSpec: Spec = spec; 118 | -------------------------------------------------------------------------------- /src/template-definition.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at http:polymer.github.io/LICENSE.txt 5 | * The complete set of authors may be found at http:polymer.github.io/AUTHORS.txt 6 | * The complete set of contributors may be found at http:polymer.github.io/CONTRIBUTORS.txt 7 | * Code distributed by Google as part of the polymer project is also 8 | * subject to an additional IP rights grant found at http:polymer.github.io/PATENTS.txt 9 | */ 10 | 11 | import { parse } from './template-string-parser.js'; 12 | import { 13 | TemplateRule, 14 | NodeTemplateRule, 15 | AttributeTemplateRule, 16 | InnerTemplateRule 17 | } from './template-rule.js'; 18 | 19 | // Edge needs all 4 parameters present; IE11 needs 3rd parameter to be null 20 | export const createTreeWalker = (node: Node) => document.createTreeWalker( 21 | node, 22 | NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, 23 | null as any, 24 | false); 25 | 26 | export class TemplateDefinition { 27 | rules: TemplateRule[]; 28 | 29 | parsedTemplate: HTMLTemplateElement; 30 | 31 | constructor(public template: HTMLTemplateElement) { 32 | this.parseAndGenerateRules(); 33 | } 34 | 35 | cloneContent() { 36 | return this.parsedTemplate.content.cloneNode(true); 37 | } 38 | 39 | protected parseAndGenerateRules() { 40 | const { template } = this; 41 | const content = template.content.cloneNode(true); 42 | const rules: TemplateRule[] = []; 43 | 44 | const walker = createTreeWalker(content); 45 | let nodeIndex = -1; 46 | 47 | while (walker.nextNode()) { 48 | nodeIndex++; 49 | 50 | const node = walker.currentNode as Element; 51 | 52 | if (node.nodeType === Node.ELEMENT_NODE) { 53 | if (!node.hasAttributes()) { 54 | continue; 55 | } 56 | 57 | if (node instanceof HTMLTemplateElement) { 58 | const { parentNode } = node; 59 | const partNode = document.createTextNode(''); 60 | 61 | parentNode!.replaceChild(partNode, node); 62 | 63 | rules.push(new InnerTemplateRule(nodeIndex, node)); 64 | } else { 65 | const { attributes } = node; 66 | 67 | // TODO(cdata): Fix IE/Edge attribute order here 68 | // @see https://github.com/Polymer/lit-html/blob/master/src/lit-html.ts#L220-L229 69 | 70 | for (let i = 0; i < attributes.length;) { 71 | const attribute = attributes[i]; 72 | const { name, value } = attribute; 73 | 74 | const [ strings, values ] = parse(value); 75 | 76 | if (strings.length === 1) { 77 | ++i; 78 | continue; 79 | } 80 | 81 | rules.push(new AttributeTemplateRule( 82 | nodeIndex, name, strings, values)); 83 | 84 | node.removeAttribute(name); 85 | } 86 | } 87 | } else if (node.nodeType === Node.TEXT_NODE) { 88 | const [ strings, values ] = parse(node.nodeValue || ''); 89 | const { parentNode } = node; 90 | const document = node.ownerDocument; 91 | 92 | if (strings.length === 1) { 93 | continue; 94 | } 95 | 96 | for (let i = 0; i < values.length; ++i) { 97 | const partNode = document.createTextNode(strings[i]); 98 | 99 | // @see https://github.com/Polymer/lit-html/blob/master/src/lit-html.ts#L267-L272 100 | parentNode!.insertBefore(partNode, node); 101 | rules.push(new NodeTemplateRule(nodeIndex++, values[i])); 102 | } 103 | 104 | node.nodeValue = strings[strings.length - 1]; 105 | } 106 | } 107 | 108 | this.rules = rules; 109 | 110 | this.parsedTemplate = document.createElement('template'); 111 | this.parsedTemplate.content.appendChild(content); 112 | } 113 | }; 114 | 115 | -------------------------------------------------------------------------------- /src/template-instance.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at http:polymer.github.io/LICENSE.txt 5 | * The complete set of authors may be found at http:polymer.github.io/AUTHORS.txt 6 | * The complete set of contributors may be found at http:polymer.github.io/CONTRIBUTORS.txt 7 | * Code distributed by Google as part of the polymer project is also 8 | * subject to an additional IP rights grant found at http:polymer.github.io/PATENTS.txt 9 | */ 10 | 11 | import { TemplateDefinition, createTreeWalker } from './template-definition.js'; 12 | import { TemplateProcessor } from 13 | './template-processor.js'; 14 | import { 15 | TemplatePart, 16 | AttributeTemplatePart, 17 | NodeTemplatePart, 18 | InnerTemplatePart 19 | } from './template-part.js'; 20 | import { 21 | TemplateRule, 22 | AttributeTemplateRule, 23 | NodeTemplateRule, 24 | InnerTemplateRule 25 | } from './template-rule.js'; 26 | 27 | export class TemplateInstance extends DocumentFragment { 28 | protected createdCallbackInvoked: boolean = false; 29 | protected previousState: any = null; 30 | protected parts: TemplatePart[]; 31 | 32 | update(state?: any) { 33 | if (!this.createdCallbackInvoked) { 34 | this.processor.createdCallback(this.parts, state); 35 | this.createdCallbackInvoked = true; 36 | } 37 | 38 | this.processor.processCallback(this.parts, state); 39 | this.previousState = state; 40 | } 41 | 42 | constructor(public definition: TemplateDefinition, 43 | public processor: TemplateProcessor, 44 | state?: any) { 45 | super(); 46 | 47 | this.appendChild(definition.cloneContent()); 48 | this.generateParts(); 49 | this.update(state); 50 | } 51 | 52 | protected generateParts() { 53 | const { definition } = this; 54 | const { rules } = definition; 55 | const parts = []; 56 | 57 | const walker = createTreeWalker(this); 58 | 59 | let walkerIndex = -1; 60 | 61 | for (let i = 0; i < rules.length; ++i) { 62 | const rule = rules[i]; 63 | const { nodeIndex } = rule; 64 | 65 | while (walkerIndex < nodeIndex) { 66 | walkerIndex++; 67 | walker.nextNode(); 68 | } 69 | 70 | const part = this.createPart(rule, walker.currentNode); 71 | 72 | parts.push(part); 73 | } 74 | 75 | this.parts = parts; 76 | } 77 | 78 | // NOTE(cdata): In the original pass, this was exposed in the 79 | // TemplateProcessor to be optionally overridden so that parts could 80 | // have custom implementations. 81 | protected createPart(rule: TemplateRule, node: Node): TemplatePart { 82 | if (rule instanceof AttributeTemplateRule) { 83 | return new AttributeTemplatePart(this, rule, node as HTMLElement); 84 | } else if (rule instanceof InnerTemplateRule) { 85 | return new InnerTemplatePart(this, rule, node); 86 | } else if (rule instanceof NodeTemplateRule) { 87 | return new NodeTemplatePart(this, rule, node); 88 | } 89 | 90 | throw new Error(`Unknown rule type.`); 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /src/template-part.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at http:polymer.github.io/LICENSE.txt 5 | * The complete set of authors may be found at http:polymer.github.io/AUTHORS.txt 6 | * The complete set of contributors may be found at http:polymer.github.io/CONTRIBUTORS.txt 7 | * Code distributed by Google as part of the polymer project is also 8 | * subject to an additional IP rights grant found at http:polymer.github.io/PATENTS.txt 9 | */ 10 | 11 | import { TemplateInstance } from './template-instance.js'; 12 | import { 13 | TemplateRule, 14 | AttributeTemplateRule, 15 | NodeTemplateRule, 16 | InnerTemplateRule 17 | } from './template-rule.js'; 18 | 19 | 20 | 21 | export abstract class TemplatePart { 22 | protected sourceValue: any; 23 | 24 | constructor(readonly templateInstance: TemplateInstance, 25 | readonly rule: TemplateRule) {} 26 | 27 | get value(): any { 28 | return this.sourceValue; 29 | } 30 | 31 | set value(value: any) { 32 | if (value !== this.sourceValue) { 33 | this.sourceValue = value; 34 | this.applyValue(value); 35 | } 36 | } 37 | 38 | abstract clear(): void; 39 | protected abstract applyValue(value: any): void; 40 | } 41 | 42 | 43 | 44 | export class AttributeTemplatePart extends TemplatePart { 45 | constructor(readonly templateInstance: TemplateInstance, 46 | readonly rule: AttributeTemplateRule, 47 | readonly element: HTMLElement) { 48 | super(templateInstance, rule); 49 | } 50 | 51 | clear() { 52 | this.element.removeAttribute(this.rule.attributeName); 53 | } 54 | 55 | protected applyValue(value: any) { 56 | if (value == null) { 57 | value = []; 58 | } else if (!Array.isArray(value)) { 59 | value = [value]; 60 | } 61 | 62 | const { rule, element } = this; 63 | const { strings, attributeName } = rule; 64 | const valueFragments = []; 65 | 66 | for (let i = 0; i < (strings.length - 1); ++i) { 67 | valueFragments.push(strings[i]); 68 | valueFragments.push(value[i] || ''); 69 | } 70 | 71 | const attributeValue = valueFragments.join(''); 72 | 73 | if (attributeValue != null) { 74 | element.setAttribute(attributeName, attributeValue); 75 | } else { 76 | element.removeAttribute(attributeName); 77 | } 78 | } 79 | } 80 | 81 | 82 | 83 | export class NodeTemplatePart extends TemplatePart { 84 | parentNode: Node; 85 | previousSibling: Node; 86 | nextSibling: Node | null; 87 | 88 | currentNodes: Node[] = []; 89 | 90 | constructor(readonly templateInstance: TemplateInstance, 91 | readonly rule: NodeTemplateRule, 92 | protected startNode: Node) { 93 | super(templateInstance, rule); 94 | this.move(startNode); 95 | } 96 | 97 | replace(...nodes: Array) { 98 | this.clear(); 99 | 100 | for (let i = 0; i < nodes.length; ++i) { 101 | let node = nodes[i]; 102 | 103 | if (typeof node === 'string') { 104 | node = document.createTextNode(node); 105 | } 106 | 107 | // SPECIAL NOTE(cdata): This implementation supports NodeTemplatePart as 108 | // a replacement node. Usefulness TBD. 109 | if (node instanceof NodeTemplatePart) { 110 | const part = node as NodeTemplatePart; 111 | node = part.startNode; 112 | this.appendNode(node); 113 | part.move(node); 114 | } else if (node.nodeType === Node.DOCUMENT_FRAGMENT_NODE || 115 | node.nodeType === Node.DOCUMENT_NODE) { 116 | // NOTE(cdata): Apple's proposal explicit forbid's document fragments 117 | // @see https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Template-Instantiation.md 118 | throw new DOMException('InvalidNodeTypeError'); 119 | } else { 120 | this.appendNode(node); 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * Forks the current part, inserting a new part after the current one and 127 | * returning it. The forked part shares the TemplateInstance and the 128 | * TemplateRule of the current part. 129 | */ 130 | fork(): NodeTemplatePart { 131 | const node = document.createTextNode(''); 132 | 133 | this.parentNode!.insertBefore(node, this.nextSibling); 134 | this.nextSibling = node; 135 | 136 | return new NodeTemplatePart(this.templateInstance, this.rule, node); 137 | } 138 | 139 | /** 140 | * Creates a new inner part that is enclosed completely by the current 141 | * part and returns it. The enclosed part shares the TemplateInstance and the 142 | * TemplateRule of the current part. 143 | */ 144 | enclose(): NodeTemplatePart { 145 | const node = document.createTextNode(''); 146 | 147 | this.parentNode!.insertBefore(node, this.previousSibling.nextSibling); 148 | 149 | return new NodeTemplatePart(this.templateInstance, this.rule, node); 150 | } 151 | 152 | move(startNode: Node) { 153 | const { currentNodes, startNode: currentStartNode } = this; 154 | 155 | if (currentStartNode != null && 156 | currentStartNode !== startNode && 157 | currentNodes.length) { 158 | this.clear(); 159 | } 160 | 161 | this.parentNode = startNode.parentNode!; 162 | this.previousSibling = startNode; 163 | this.nextSibling = startNode.nextSibling; 164 | this.startNode = startNode; 165 | 166 | if (currentNodes && currentNodes.length) { 167 | this.replace(...currentNodes); 168 | } 169 | } 170 | 171 | // SPECIAL NOTE(cdata): This clear is specialized a la lit-html to accept a 172 | // starting node from which to clear. This supports efficient cleanup of 173 | // subparts of a part (subparts are also particular to lit-html compared to 174 | // Apple's proposal). 175 | clear(startNode: Node = this.previousSibling.nextSibling!) { 176 | if (this.parentNode === null) { 177 | return; 178 | } 179 | 180 | let node = startNode; 181 | 182 | while (node !== this.nextSibling) { 183 | const nextNode: Node | null = node.nextSibling; 184 | this.parentNode.removeChild(node); 185 | node = nextNode as Node; 186 | } 187 | 188 | this.currentNodes = []; 189 | } 190 | 191 | protected appendNode(node: Node) { 192 | this.parentNode!.insertBefore(node, this.nextSibling); 193 | this.currentNodes.push(node); 194 | } 195 | 196 | protected applyValue(value: any) { 197 | if (this.currentNodes.length === 1 && 198 | this.currentNodes[0].nodeType === Node.TEXT_NODE) { 199 | this.currentNodes[0].nodeValue = value; 200 | } else { 201 | this.replace(document.createTextNode(value)); 202 | } 203 | } 204 | } 205 | 206 | 207 | 208 | export class InnerTemplatePart extends NodeTemplatePart { 209 | constructor(readonly templateInstance: TemplateInstance, 210 | readonly rule: InnerTemplateRule, 211 | protected startNode: Node) { 212 | super(templateInstance, rule, startNode); 213 | } 214 | 215 | get template(): HTMLTemplateElement { 216 | return this.rule.template; 217 | } 218 | } 219 | 220 | -------------------------------------------------------------------------------- /src/template-processor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at http:polymer.github.io/LICENSE.txt 5 | * The complete set of authors may be found at http:polymer.github.io/AUTHORS.txt 6 | * The complete set of contributors may be found at http:polymer.github.io/CONTRIBUTORS.txt 7 | * Code distributed by Google as part of the polymer project is also 8 | * subject to an additional IP rights grant found at http:polymer.github.io/PATENTS.txt 9 | */ 10 | 11 | import { TemplatePart } from './template-part.js'; 12 | 13 | export abstract class TemplateProcessor { 14 | abstract createdCallback(parts: TemplatePart[], state?: any): void; 15 | abstract processCallback(parts: TemplatePart[], state?: any): void; 16 | } 17 | -------------------------------------------------------------------------------- /src/template-rule.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at http:polymer.github.io/LICENSE.txt 5 | * The complete set of authors may be found at http:polymer.github.io/AUTHORS.txt 6 | * The complete set of contributors may be found at http:polymer.github.io/CONTRIBUTORS.txt 7 | * Code distributed by Google as part of the polymer project is also 8 | * subject to an additional IP rights grant found at http:polymer.github.io/PATENTS.txt 9 | */ 10 | 11 | export class TemplateRule { 12 | constructor(public nodeIndex: number) {} 13 | } 14 | 15 | export class NodeTemplateRule extends TemplateRule { 16 | constructor(public nodeIndex: number, public expression: string) { 17 | super(nodeIndex); 18 | } 19 | } 20 | 21 | export class AttributeTemplateRule extends TemplateRule { 22 | constructor(public nodeIndex: number, 23 | public attributeName: string, 24 | public strings: string[], 25 | public expressions: string[]) { 26 | super(nodeIndex); 27 | } 28 | } 29 | 30 | export class InnerTemplateRule extends NodeTemplateRule { 31 | constructor(public nodeIndex: number, public template: HTMLTemplateElement) { 32 | super(nodeIndex, template.getAttribute('expression') || ''); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/template-string-parser-spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at http:polymer.github.io/LICENSE.txt 5 | * The complete set of authors may be found at http:polymer.github.io/AUTHORS.txt 6 | * The complete set of contributors may be found at http:polymer.github.io/CONTRIBUTORS.txt 7 | * Code distributed by Google as part of the polymer project is also 8 | * subject to an additional IP rights grant found at http:polymer.github.io/PATENTS.txt 9 | */ 10 | 11 | import { Spec } from '../../@polymer/ristretto/lib/spec.js'; 12 | import { Fixturable } from 13 | '../../@polymer/ristretto/lib/mixins/fixturable.js'; 14 | import '../../chai/chai.js'; 15 | import { parse } from './template-string-parser.js'; 16 | 17 | const spec = new (Fixturable(Spec))(); 18 | const { describe, it } = spec; 19 | const { expect } = chai; 20 | 21 | describe('Template string parsing', () => { 22 | describe('an empty string', () => { 23 | it('returns a single string and zero expressions', () => { 24 | expect(parse('')).to.be.eql([[''], []]); 25 | }); 26 | }); 27 | 28 | describe('a string that is just a part', () => { 29 | it('returns two empty strings and one expression', () => { 30 | expect(parse('{{foo}}')).to.be.eql([['', ''], ['foo']]); 31 | }); 32 | }); 33 | 34 | describe('a non-empty string with one part', () => { 35 | it('returns prefix and suffix strings and one expression', () => { 36 | expect(parse('prefix {{foo}}suffix')) 37 | .to.be.eql([['prefix ', 'suffix'], ['foo']]); 38 | }); 39 | }); 40 | 41 | describe('a string with many parts', () => { 42 | it('returns static strings and expressions in order', () => { 43 | expect(parse('prefix {{foo}} middle {{bar}} suffix {{baz}}')) 44 | .to.be.eql([['prefix ', ' middle ', ' suffix ', ''], 45 | ['foo', 'bar', 'baz']]); 46 | }); 47 | }); 48 | }); 49 | 50 | export const templateStringParserSpec: Spec = spec; 51 | -------------------------------------------------------------------------------- /src/template-string-parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright (c) 2018 The Polymer Project Authors. All rights reserved. 4 | * This code may only be used under the BSD style license found at http:polymer.github.io/LICENSE.txt 5 | * The complete set of authors may be found at http:polymer.github.io/AUTHORS.txt 6 | * The complete set of contributors may be found at http:polymer.github.io/CONTRIBUTORS.txt 7 | * Code distributed by Google as part of the polymer project is also 8 | * subject to an additional IP rights grant found at http:polymer.github.io/PATENTS.txt 9 | */ 10 | 11 | const partOpenRe = /{{/g; 12 | const partCloseRe = /}}/g; 13 | 14 | export const parse = (templateString: string): 15 | [string[], string[]] => { 16 | const strings: string[] = []; 17 | const expressions: string[] = []; 18 | const boundaryIndex = templateString.length + 1; 19 | 20 | let lastExpressionIndex = 21 | partOpenRe.lastIndex = 22 | partCloseRe.lastIndex = 0; 23 | 24 | while (lastExpressionIndex < boundaryIndex) { 25 | const openResults = partOpenRe.exec(templateString); 26 | 27 | if (openResults == null) { 28 | strings.push(templateString.substring( 29 | lastExpressionIndex, boundaryIndex)); 30 | break; 31 | } else { 32 | const openIndex = openResults.index; 33 | 34 | partCloseRe.lastIndex = partOpenRe.lastIndex = openIndex + 2; 35 | 36 | const closeResults = partCloseRe.exec(templateString); 37 | 38 | if (closeResults == null) { 39 | strings.push(templateString.substring( 40 | lastExpressionIndex, boundaryIndex)); 41 | } else { 42 | const closeIndex = closeResults.index; 43 | 44 | strings.push(templateString.substring( 45 | lastExpressionIndex, openIndex)); 46 | 47 | expressions.push(templateString.substring( 48 | openIndex + 2, closeIndex)); 49 | 50 | lastExpressionIndex = closeIndex + 2; 51 | } 52 | } 53 | } 54 | 55 | return [strings, expressions]; 56 | }; 57 | 58 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "es2015", 5 | "lib": ["es2017", "esnext.asynciterable", "dom", "dom.iterable"], 6 | "declaration": true, 7 | "sourceMap": true, 8 | "inlineSources": true, 9 | "outDir": "./lib", 10 | "strict": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "rootDirs": [ 16 | "../", 17 | "./node_modules" 18 | ] 19 | }, 20 | "include": [ 21 | "src/**/*.ts" 22 | ], 23 | "exclude": [ 24 | "src/old/**/*.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "arrow-parens": true, 4 | "class-name": true, 5 | "indent": [ 6 | true, 7 | "spaces", 8 | 2 9 | ], 10 | "prefer-const": true, 11 | "no-duplicate-variable": true, 12 | "no-eval": true, 13 | "no-internal-module": true, 14 | "no-trailing-whitespace": true, 15 | "no-var-keyword": true, 16 | "one-line": [ 17 | true, 18 | "check-open-brace", 19 | "check-whitespace" 20 | ], 21 | "quotemark": [ 22 | true, 23 | "single", 24 | "avoid-escape" 25 | ], 26 | "semicolon": [ 27 | true, 28 | "always" 29 | ], 30 | "trailing-comma": [ 31 | true, 32 | "multiline" 33 | ], 34 | "triple-equals": [ 35 | true, 36 | "allow-null-check" 37 | ], 38 | "typedef-whitespace": [ 39 | true, 40 | { 41 | "call-signature": "nospace", 42 | "index-signature": "nospace", 43 | "parameter": "nospace", 44 | "property-declaration": "nospace", 45 | "variable-declaration": "nospace" 46 | } 47 | ], 48 | "variable-name": [ 49 | true, 50 | "ban-keywords" 51 | ], 52 | "whitespace": [ 53 | true, 54 | "check-branch", 55 | "check-decl", 56 | "check-operator", 57 | "check-separator", 58 | "check-type" 59 | ] 60 | } 61 | } 62 | --------------------------------------------------------------------------------