{"id":15662374,"url":"https://github.com/luwes/template-extensions","last_synced_at":"2025-10-25T04:38:26.149Z","repository":{"id":62276245,"uuid":"559318411","full_name":"luwes/template-extensions","owner":"luwes","description":"An umbrella of low-level JavaScript API's to process templates and HTML","archived":false,"fork":false,"pushed_at":"2024-02-22T04:00:00.000Z","size":749,"stargazers_count":18,"open_issues_count":2,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-10T16:53:40.255Z","etag":null,"topics":["dom","html"],"latest_commit_sha":null,"homepage":"https://template-extensions.vercel.app/examples/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/luwes.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-10-29T18:34:14.000Z","updated_at":"2024-05-20T13:14:33.000Z","dependencies_parsed_at":"2024-10-23T07:49:14.105Z","dependency_job_id":"a4938f67-2845-4b16-8da7-49fbac8afa52","html_url":"https://github.com/luwes/template-extensions","commit_stats":{"total_commits":71,"total_committers":1,"mean_commits":71.0,"dds":0.0,"last_synced_commit":"34acf0a0c28e373d743da64ad239ff24472827b3"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luwes%2Ftemplate-extensions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luwes%2Ftemplate-extensions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luwes%2Ftemplate-extensions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/luwes%2Ftemplate-extensions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/luwes","download_url":"https://codeload.github.com/luwes/template-extensions/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252596375,"owners_count":21773844,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dom","html"],"created_at":"2024-10-03T13:32:11.173Z","updated_at":"2025-10-25T04:38:21.129Z","avatar_url":"https://github.com/luwes.png","language":"JavaScript","readme":"# Template Extensions \n\n[![npm version](https://img.shields.io/npm/v/template-extensions?style=flat-square)](http://npmjs.org/template-extensions)\n[![size](https://img.shields.io/bundlephobia/minzip/template-extensions?label=size\u0026style=flat-square)](https://bundlephobia.com/result?p=template-extensions) \n\n- Friendly HTML-based template syntax w/ efficient updates via DOM parts\n- Custom syntax with template processors\n- Clear HTML / JS separation\n- Progressive enhancement (SSR/SSG)\n- Based on [web component](https://github.com/WICG/webcomponents) spec proposals\n  - [Template Instantiation](https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Template-Instantiation.md)\n  - [DOM Parts](https://github.com/WICG/webcomponents/blob/gh-pages/proposals/DOM-Parts.md)\n\n\n\n## Examples\n\n- [Counter](https://template-extensions.vercel.app/examples/)\n- [Media Chrome Themes](https://github.com/muxinc/media-chrome/blob/main/src/js/media-theme-element.js) \nin [Mux Player](https://github.com/muxinc/elements/blob/main/packages/mux-player/src/media-theme-mux.html) \n([demo](https://stream.new/v/DVBhwqkhxkOiLRjUAYJS6mCBJSuC00tB4iWjJmEofJoo))\n- [TodoMVC with SSR and hydration](https://github.com/luwes/template-extensions-todomvc) \n([demo](https://template-extensions-todomvc.pages.dev/))\n- [Create your own UI library (Lit like API)](./examples/interhtml.js) \n([demo](https://template-extensions.vercel.app/examples/todos.html))\n\n\n## Install\n\n- **npm**: `npm i template-extensions`  \n- **cdn**: https://cdn.jsdelivr.net/npm/template-extensions  \n\n## Basics\n\n```html\n\u003ctemplate id=\"tpl\"\u003e\n  \u003cdiv class=\"foo {{y}}\"\u003e{{x}} world\u003c/div\u003e\n\u003c/template\u003e\n```\n\nWhen passing the above template to the `TemplateInstance` constructor\n2 DOM parts are created that represent `{{x}}` and `{{y}}` in the HTML. \nThe second constructor argument is the state object (or params) that will fill \nin the values of the DOM parts.\n\n```js\nimport { TemplateInstance } from 'template-extensions';\n\nconst tplInst = new TemplateInstance(tpl, { x: 'Hello', y: 'bar'} /*, processor*/);\ndocument.append(tplInst);\n```\n\nA `TemplateInstance` instance is a subclass of `DocumentFragment` so it can\nbe appended directly to the DOM. In addition it gets a new `update(params)`\nmethod that can be called to efficiently update the DOM parts.\n\nAn optional third argument is de [template processor](#template-processor). \nThis hook allows you to handle each expression and DOM part in a specialized\nway. It allows you to write your own template language syntax, think Vue. \nEverything between the curly braces can be parsed and evaluated as you see fit.\n\n## Usage\n\n### Simple render\n\n```html\n\u003ctemplate id=\"info\"\u003e\n  \u003csection\u003e\n    \u003ch1\u003e{{name}}\u003c/h1\u003e\n    Email: \u003ca href=\"mailto:{{email}}\"\u003e{{email}}\u003c/a\u003e\n  \u003c/section\u003e\n\u003c/template\u003e\n\u003cscript type=\"module\"\u003e\n  import { TemplateInstance } from 'template-extensions';\n\n  const params = { name: 'Ryosuke Niwa', email: 'rniwa@webkit.org' };\n  const content = new TemplateInstance(info, params);\n  document.body.append(content);\n  // later on\n  content.update({ name: 'Ryosuke Niwa', email: 'rniwa@apple.com' });\n\u003c/script\u003e\n```\n\n### Simple hydrate ([Codesandbox](https://codesandbox.io/s/template-extensions-2v4m2y?file=/index.html))\n\nNote that assigning a template instance to an existing element is only\nconcerned about the structure and content leading up to the expressions.\nIn the example below it's fine to skip the static text content of the paragraph\nin the template.\n\n```html\n\u003cdiv id=\"root\"\u003e\n  \u003csection\u003e\n    \u003ch1\u003eRyosuke Niwa\u003c/h1\u003e\n    \u003cp\u003e\n      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod\n      tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim\n      veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea\n      commodo consequat. Duis aute irure dolor in reprehenderit in voluptate\n      velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat\n      cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id\n      est laborum.\n    \u003c/p\u003e\n    Email: \u003ca href=\"mailto:rniwa@webkit.org\"\u003erniwa@webkit.org\u003c/a\u003e\n  \u003c/section\u003e\n\u003c/div\u003e\n\u003ctemplate id=\"info\"\u003e\n  \u003csection\u003e\n    \u003ch1\u003e{{name}}\u003c/h1\u003e\n    \u003cp\u003e\u003c/p\u003e\n    Email: \u003ca href=\"mailto:{{email}}\"\u003e{{email}}\u003c/a\u003e\n  \u003c/section\u003e\n\u003c/template\u003e\n\u003cscript type=\"module\"\u003e\n  import { AssignedTemplateInstance } from 'template-extensions';\n\n  const params = { name: 'Ryosuke Niwa', email: 'rniwa@webkit.org' };\n  const content = new AssignedTemplateInstance(root, info, params);\n  // later on\n  content.update({ name: 'Ryosuke Niwa', email: 'rniwa@apple.com' });\n\u003c/script\u003e\n```\n\n## Template Processor\n\n### Default processor\n\nThe default processor looks a bit like the function below. Each time \n`templateInstance.update(params)` is called this function runs and\niterates through each DOM part and evaluates what needs to happen.\n\n```js\nfunction processCallback(instance, parts, params) {\n  if (!params) return;\n  for (const [expression, part] of parts) {\n    if (expression in params) {\n      const value = params[expression];\n      if (\n        typeof value === 'boolean' \u0026\u0026\n        part instanceof AttrPart \u0026\u0026\n        typeof part.element[part.attributeName] === 'boolean'\n      ) {\n        part.booleanValue = value;\n      } else if (typeof value === 'function' \u0026\u0026 part instanceof AttrPart) {\n        part.value = undefined;\n        part.element[part.attributeName] = value;\n      } else {\n        part.value = value;\n      }\n    }\n  }\n}\n```\n\nThe default processor handles property identity (i.e. `part.value = params[expression]`),\nboolean attribute or function.\n\n```js\nconst el = document.createElement('template')\nel.innerHTML = `\u003cdiv x={{x}} hidden={{hidden}} onclick={{onclick}}\u003e\u003c/div\u003e`\n\nconst tpl = new TemplateInstance(el, { x: 'Hello', hidden: false, onclick: () =\u003e {} })\nel.getAttribute('x') // 'Hello'\nel.hasAttribute('hidden') // false\nel.onclick // function\n```\n\n\n## InnerTemplatePart\n\nThe [`InnerTemplatePart`](https://github.com/WICG/webcomponents/blob/gh-pages/proposals/Template-Instantiation.md#33-conditionals-and-loops-using-nested-templates) \nenables you to write conditionals and loops in your templates. The inner templates\nare expected to have a `directive` and `expression` attribute.\n\n### if\n\n```html\n\u003ctemplate\u003e\n  \u003cdiv\u003e\n    \u003ctemplate directive=\"if\" expression=\"isActive\"\u003e{{x}}\u003c/template\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n```\n\n### foreach\n\n```html\n\u003ctemplate\u003e\n  \u003cdiv\u003e\n    \u003ctemplate directive=\"foreach\" expression=\"items\"\u003e\n      \u003cli class={{class}} data-value={{value}}\u003e{{label}}\u003c/li\u003e\n    \u003c/template\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n```\n\n```js\nexport const processor = {\n  processCallback(instance, parts, params) {\n    if (!params) return;\n    for (const [expression, part] of parts) {\n      if (part instanceof InnerTemplatePart) {\n        switch (part.directive) {\n          case 'foreach': {\n            part.replace((params[expression] ?? []).map(item =\u003e {\n              return new TemplateInstance(part.template, item, processor);\n            }));\n            break;\n          }\n        }\n        continue;\n      }\n      // handle rest of expressions...\n      if (expression in params) {\n    }\n  }\n};\n```\n\n\n## `renderToString(html, params, processor=defaultProcessor)`\n\nRenders HTML with expressions and inner templates to a string. No JSDOM required.  \nIt's possible to use the same template processor for client and server.\n\n```js\nimport { renderToString } from 'template-extensions/src/extras/ssr.js';\n\nconsole.log(renderToString(`\u003cdiv class=\"my-{{x}}-state {{y}}\"\u003e{{z}}\u003c/div\u003e`, {\n  x: 'foo',\n  y: 'bar',\n  z: 'baz',\n}))\n// \u003cdiv class=\"my-foo-state bar\"\u003ebaz\u003c/div\u003e\n```\n\n\n## Credit\n\nBig thanks to everyone who contributed to the Template Instantiation and\nDom Parts proposals.\n\nThe Template Instantiation and DOM parts code is based on the great work of\n[Keith Cirkel](https://github.com/keithamus) and [Dmitry Iv.](https://github.com/dy).\n\n- https://github.com/github/template-parts\n- https://github.com/github/jtml\n- https://github.com/dy/template-parts\n\n\n## Interfaces\n\n```ts\nexport class AssignedTemplateInstance {\n  constructor(\n    container: HTMLElement | ShadowRoot,\n    template: HTMLTemplateElement,\n    params: unknown,\n    processor?: TemplateTypeInit\n  );\n  update(params: unknown): void;\n}\n\nexport class TemplateInstance extends DocumentFragment {\n  constructor(\n    template: HTMLTemplateElement,\n    params: unknown,\n    processor?: TemplateTypeInit\n  );\n  update(params: unknown): void;\n}\n\ntype Expression = string;\n\ntype TemplateProcessCallback = (\n  instance: TemplateInstance,\n  parts: Iterable\u003c[Expression, Part]\u003e,\n  params: unknown\n) =\u003e void;\n\nexport type TemplateTypeInit = {\n  processCallback: TemplateProcessCallback;\n  createCallback?: TemplateProcessCallback;\n};\n\nexport interface Part {\n  value: string | null;\n  toString(): string;\n}\n\nexport class AttrPart implements Part {\n  constructor(element: Element, attributeName: string, namespaceURI?: string);\n  get element(): Element;\n  get attributeName(): string;\n  get attributeNamespace(): string | null;\n  get value(): string | null;\n  set value(value: string | null);\n  get booleanValue(): boolean;\n  set booleanValue(value: boolean);\n  get list(): AttrPartList;\n}\n\nexport class AttrPartList {\n  get length(): number;\n  item(index: number): AttrPart;\n  append(...items): void;\n  toString(): string;\n}\n\nexport class ChildNodePart implements Part {\n  constructor(parentNode: Element, nodes: Node[]);\n  get parentNode(): Element;\n  get value(): string;\n  set value(string: string);\n  get previousSibling(): ChildNode | null;\n  get nextSibling(): ChildNode | null;\n  replace(...nodes: Array\u003cstring | ChildNode\u003e): void;\n}\n\nexport class InnerTemplatePart extends ChildNodePart {\n  constructor(parentNode: Element, template: HTMLTemplateElement);\n  get directive(): string | null;\n  get expression(): Expression | null;\n}\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fluwes%2Ftemplate-extensions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fluwes%2Ftemplate-extensions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fluwes%2Ftemplate-extensions/lists"}