@tobi2409/template-engine
v0.9.3
Published
Lightweight MVVM template engine with declarative HTML tags, reactive dependency mapping, and mapped-array support
Maintainers
Readme
TemplateEngine
A lightweight, vanilla JavaScript template engine with declarative HTML tags and reactive DOM updates.
Features
- Declarative templates with custom tags:
<get>,<each>,<if>,<template-use> - Reactive updates without full re-rendering
- Dependency-based refresh chaining (
dependenciesmap) - Efficient key-to-node tracking via internal node holders
- Nested context support for scoped access inside loops
- Array update support:
push,pop,shift,unshift,splice - Optional view-model helper:
createMappedArray(...)
Installation
import TemplateEngine from './template-engine.js'Examples (start here)
Browse the live demo files in the GitHub repository.
Featured demos:
Interesting snippets from those demos:
MVVM: computed fields + dependency chaining
const viewModel = TemplateEngine.reactive({
get fullName() {
return `${this.firstName} ${this.lastName}`
},
get showWage() {
return this.wage > 600
},
get fullInfo() {
return `${this.fullName} earns $${this.wage}`
},
getBeautifiedData() {
return createMappedArray(
model.rawPersonData,
(p) => ({
id: p.id,
name: p.name,
age: new Date().getFullYear() - p.birthyear,
showEdit: false
}),
{ name: 'name', age: 'birthyear' },
(item) => ({
id: item.id,
name: item.name,
birthyear: item.birthyear || (new Date().getFullYear() - item.age)
})
)
}
}, document.getElementById('app-template-use'), {
firstName: ['fullName'],
lastName: ['fullName'],
wage: ['showWage', 'fullInfo'],
fullName: ['fullInfo']
})Recursive template: self-referencing <template-use>
<template id="folder-template">
<each of="*list" as="item#">
<li>
📁 <get>item#.name</get>
<ul>
<template-use template-id="folder-template" data-list="item#.childs"></template-use>
</ul>
</li>
</each>
</template>toggleEdit: (e, dataElement) => {
dataElement.editing = !dataElement.editing
},
delete: (e, dataElement, _, contextStack) => {
const parent = contextStack.get(`item-level-${contextStack.size - 3}`)
if (parent?.data?.childs) {
const index = parent.data.childs.findIndex((c) => c === dataElement)
if (index !== -1) parent.data.childs.splice(index, 1)
}
}The examples use local file paths and are intended to be run directly from the cloned repository. To try them out, clone the repo and open the HTML files in a browser:
git clone https://github.com/tobi2409/template-engine.git cd template-engine
Quick Start
1) Define a <template>
<template id="user-template">
<div class="user">
<h2><get>name</get></h2>
<p>Email: <get>email</get></p>
<h3>Posts</h3>
<each of="posts" as="post">
<div class="post">
<strong><get>post.title</get></strong>
<p><get>post.content</get></p>
</div>
</each>
</div>
</template>
<div id="mount-point"></div>
<template-use template-id="user-template" mount-id="mount-point"></template-use>2) Initialize reactivity
const templateUse = document.querySelector('template-use')
const data = TemplateEngine.reactive(
{
name: 'Alice',
email: '[email protected]',
posts: [
{ title: 'First Post', content: 'Hello World!' },
{ title: 'Second Post', content: 'Learning TemplateEngine' }
]
},
templateUse
)3) Update data
data.name = 'Alice Smith'
data.posts.push({ title: 'Third Post', content: 'Advanced features!' })
data.posts.splice(1, 0, { title: 'Inserted Post', content: 'In the middle!' })Template Syntax
<get>key</get>
Renders a value from data/context.
<get>user.name</get><each of="array" as="item">...</each>
Loops over an array.
<each of="users" as="user">
<div><get>user.name</get></div>
</each>Note: Array items must be objects, not primitive values (strings, numbers, booleans). The engine uses a
WeakMapinternally to track item identity, which requires object references.❌ Primitives are not supported:
data.tags = ['news', 'tech', 'sports']✅ Wrap primitives in objects instead:
data.tags = [ { value: 'news' }, { value: 'tech' }, { value: 'sports' } ]<each of="tags" as="tag"> <span><get>tag.value</get></span> </each>
<if test="expr">...</if>
Conditionally renders content.
<if test="isVisible">
<span>Visible content</span>
</if><template-use ...></template-use>
Mounts a <template> by ID.
<template-use template-id="user-template" mount-id="mount-point"></template-use>API
TemplateEngine.reactive(data, templateUseNode, dependencies?)
Creates a reactive view-model around data using Object.defineProperties(...) and binds updates to DOM nodes generated from the referenced <template>.
data: source model objecttemplateUseNode:<template-use>elementdependencies(optional): dependency map for related refresh triggers
Returns: reactive view-model object
Known limitation: object replacement notifications
Replacing a nested object does not automatically notify all child keys. These patterns are currently not sufficient on their own:
d.person.address = { city: 'Köln' }
d.selectedPerson = { name: 'Mia' }Use an explicit child assignment afterwards to trigger the child-key refresh:
d.person.address = { city: 'Köln' }
d.person.address.city = 'Köln'
d.selectedPerson = { name: 'Mia' }
d.selectedPerson.name = 'Mia'Dependencies
Use the optional dependencies map when one property affects other derived properties.
const raw = { firstName: 'Alice', lastName: 'Smith' }
const data = TemplateEngine.reactive(
{
get firstName() { return raw.firstName },
set firstName(v) { raw.firstName = v },
get fullName() { return `${raw.firstName} ${raw.lastName}` }
},
document.querySelector('template-use'),
{
firstName: ['fullName']
}
)
data.firstName = 'Bob' // triggers refresh for firstName and fullNameWhy this matters:
- Keeps derived values in sync without manual DOM handling.
- Makes reactive chains explicit and maintainable.
- Works well for computed/display-only fields.
Mapped Array
createMappedArray(...) helps you build a mapped view-model array while keeping synchronization with the source array.
import { createMappedArray } from './src/mapped-array.js'
const source = [{ name: 'Alice', birthyear: 1995 }]
const vm = createMappedArray(
source,
(item) => ({
label: item.name,
age: new Date().getFullYear() - item.birthyear
}),
{ age: 'birthyear' },
(result) => ({
birthyear: new Date().getFullYear() - result.age,
name: result.label
})
)
vm[0].age = 25 // writes back to source[0].birthyearNotes:
- Keeps stable mapped object identity per source item (internal cache).
- Supports
push,pop,shift,unshift,splicevia source synchronization. - Add/insert/remove operations on mapped arrays are propagated back to the source model when
reverseTransformis provided.
Development
Run tests:
npm testStatus
Active development — API may evolve.
