@xpulse/template β Component Spec
Status: CONCEPT Β· Created March 2026
Parent: GLOBAL_concept-ecosystem.md, GLOBAL_concept-web.md
Overview
Template engine for the xPulse ecosystem. Renders .tpl.html files
into finished HTML.
One responsibility: render templates β nothing more.
Has no knowledge of HTTP, controllers, or routing.
Accepts a template path and a data object β returns HTML.
Dependencies
1 @xpulse /template2 βββ @xpulse /event β emit template :render :before/after, template :loaded etc.
File Structure in an xPart
No separate views/ folder β everything lives in src/templates/.
The controller references templates directly via their path.
1 src/ 2 templates/ 3 base.tpl.html β base layout 4 platform .tpl.html β 5 index .tpl.html β 6 info/ 7 index .tpl.html β 8 bla.tpl.html β 9 contact/ 10 index .tpl.html β 11 partials/ 12 card.tpl.html β partial β no extends 13 nav.tpl.html β partial β no extends 14 template-methods/ β custom method registrations 15 custom .js
Template Syntax
Two clearly distinct syntax forms:
1 {{ variable }} β output β outputs a value 2 {% directive %} β logic β control flow, inheritance, includes
Template Inheritance: extends + slots
A template can extend exactly one other template β stackable to any depth.
Slots are filled from the inside out.
1 2 <!DOCTYPE html > 3 <html lang ="de" > 4 <head > 5 <title > {% slot 'title' %}xPulse{% endslot %}</title > 6 </head > 7 <body > 8 {% slot 'body' %}{% endslot %} 9 </body > 10 </html >
1 2 {% extends 'base' %} 3 4 {% slot 'title' %}{{ title }} β xPulse{% endslot %} 5 6 {% slot 'body' %} 7 <nav > 8 <a href ="/" class ="{% if _route.path === '/' %}active{% endif %}" > Home</a > 9 <a href ="/info" class ="{% if _route.path === '/info' %}active{% endif %}" > Info</a > 10 </nav > 11 <main > 12 {% slot 'content' %}{% endslot %} 13 </main > 14 {% endslot %}
1 2 {% extends 'platform' %} 3 4 {% slot 'content' %} 5 <h1 > {{ title }}</h1 > 6 <ul > 7 {% each items as item %} 8 <li > {{ item }}</li > 9 {% endeach %} 10 </ul > 11 {% endslot %}
Variables
Output
1 {{ title }} 2 {{ user.name }} 3 {{ _route.path }}
Setting and Overriding
1 2 {% set lang = lang ?? 'de' %} 3 4 5 {% set label = 'Willkommen' %} 6 7 8 {% set title = title ?? 'Standardtitel' %} 9 10 11 {{ lang }} 12 {{ label }}
Methods
Methods extend variables with callable transformations.
Two call styles β both are equivalent:
1 {{ variable.methodName }} β dot notation 2 {% methodName(variable) %} β directive notation
Built-in Methods (String)
Method
Example
Description
upper
{{ title.upper }}
Uppercase
lower
{{ title.lower }}
Lowercase
trim
{{ text.trim }}
Remove whitespace
length
{{ text.length }}
Length
reverse
{{ text.reverse }}
Reverse
slice(a, b)
{{ text.slice(0, 50) }}
Substring
replace(a, b)
{{ text.replace('foo', 'bar') }}
Replace
includes(a)
{{ text.includes('foo') }}
Contains β returns true/false
startsWith(a)
{{ text.startsWith('http') }}
Starts with
endsWith(a)
{{ text.endsWith('.de') }}
Ends with
split(sep)
{{ text.split(',') }}
Split
Built-in Methods (Array)
Method
Example
Description
length
{{ items.length }}
Number of elements
first
{{ items.first }}
First element
last
{{ items.last }}
Last element
contains(v)
{{ items.contains('foo') }}
Contains value
join(sep)
{{ items.join(', ') }}
Join
reverse
{{ items.reverse }}
Reverse
Built-in Methods (Number)
Method
Example
Description
toFixed(n)
{{ price.toFixed(2) }}
Decimal places
round
{{ score.round }}
Round
floor
{{ score.floor }}
Round down
ceil
{{ score.ceil }}
Round up
abs
{{ diff.abs }}
Absolute value
Method Discovery
@xpulse/template discovers methods in three stages β analogous to controller discovery:
1 1. @xpulse/template /src/template -methods*.js β built-ins (always loaded)2 2. node_modules/@xpulse*.js β from other xPulse packages3 3. src/template -methods*.js β from the app itself (overrides)
Local overrides package β with a note in the debug log.
Method Base Class
1 2 import { Method } from '@xpulse/template' ;3 4 export default class CustomMethods extends Method {5 register ( ) { 6 this .add ('cool' , (data ) => data.split ('' ).reverse ().join ('' )); 7 this .add ('currency' , (data ) => `β¬ ${Number (data).toFixed(2 )} ` ); 8 this .add ('slug' , (data ) => data.toLowerCase ().replace (/\s+/g , '-' )); 9 } 10 }
In templates:
1 {{ title.cool }} 2 {{ price.currency }} 3 {{ title.slug }} 4 5 {% cool(title) %} 6 {% currency(price) %}
Include with Optional Parameters
1 2 {% include 'partials/card' %} 3 4 5 {% include 'partials/card' { title: 'Foo', active: true } %} 6 7 8 {% each tools as tool %} 9 {% include 'partials/card' { title: tool.name, id: tool.id, type: tool.type } %} 10 {% endeach %}
Parameters are only available within the partial's scope β no leak into the parent scope.
1 2 <div class ="card" > 3 <h2 > {{ title }}</h2 > 4 <span class ="type" > {{ type }}</span > 5 </div >
Directives Overview
Directive
Description
{% extends 'name' %}
This template extends another
{% slot 'name' %}...{% endslot %}
Define or fill a slot
{% set var = value %}
Set or override a variable
{% if condition %}...{% endif %}
Conditional output
{% if condition %}...{% else %}...{% endif %}
Conditional output with fallback
{% each items as item %}...{% endeach %}
Iterate over an array
{% each items as item, index %}...{% endeach %}
Iterate with index
{% include 'path/partial' %}
Include a partial
{% include 'path/partial' { key: val } %}
Include a partial with custom parameters
{% methodName(variable) %}
Call a custom or built-in method
API
Rendering
1 import template from '@xpulse/template' ;2 3 const html = await template.render ('info/bla' , {4 title : 'Bla' , 5 items : ['a' , 'b' , 'c' ], 6 });
template.render() automatically looks in src/templates/ β
the path is relative to it:
1 template.render ('info/bla' ) 2
_route is not passed manually β @xpulse/controller injects it
automatically via the template:render:before event (see below).
Emitted Events
Event
Payload
When
template:render:before
{ view, data }
before rendering β data is still mutable
template:render:after
{ view, html, duration }
after rendering β html is still mutable
template:loaded
{ path, cached }
when a .tpl.html file is loaded
template:methods:discovered
{ count, sources }
after method discovery
template:method:registered
{ name, source }
when an individual method is registered
Middleware via Events
1 import event from '@xpulse/event' ;2 3 4 event.on ('template:render:before' , ({ view, data } ) => { 5 data.appVersion = config.get ('release.current' ); 6 data.env = config.get ('app.env' ); 7 }); 8 9 10 event.on ('template:render:after' , ({ view, html } ) => { 11 12 });
Render Order
1 1. emit template:render:before β data still mutable 2 2. load src/templates/info/bla.tpl.html 3 3. emit template:loaded 4 4. {% extends 'platform' %} β load src/templates/platform.tpl.html + template:loaded 5 5. {% extends 'base' %} β load src/templates/base.tpl.html + template:loaded 6 6. fill slots from the inside out 7 7. resolve {% set %} variables 8 8. embed {% include %} partials (with their own scope) 9 9. resolve {% if %} / {% each %} directives 10 10. resolve methods β {{ var.method }} + {% method (var) %} 11 11. replace {{ variables }} 12 12. emit template:render:after β html still mutable 13 13. return finished HTML
Debug Integration
When @xpulse/debug is installed as a devDependency, @xpulse/template
automatically provides a DataCollector. @xpulse/debug discovers it via
node_modules/@xpulse/template/src/datacollectors/ β no configuration needed.
1 "optionalDependencies" : { 2 "@xpulse/debug" : "^1.0.0" 3 }
The TemplateCollector (name: 'template', icon: 'π') shows in the Web Profiler:
Main areas:
Summary (kv): Number of renders, Total Rendering Time
Extends Chain (list): inheritance chain of the first render (only when present)
Shared Template Data (list): data keys present in all renders of the request (only with multiple renders)
Sub-panels β one per rendered template:
Info (kv): View name, Duration, Resolved Path
Template Data / Unique Data (kv): template data with values β with multiple renders, only the unique keys of the respective template
For each template in the extends chain there is an additional sub-panel with
path info and a note that duration and data are measured at the child template.
The TemplateCollector also writes a Waterfall Span per render
(kind: 'template', duration from payload.duration).
Badge in the toolbar: number of renders (e.g. 3)
Package Structure
1 @xpulse/template/ 2 src / 3 index.js β default export: template object 4 loader.js β load + cache .tpl .html files 5 parser.js β parse template syntax 6 renderer.js β resolve slots, variables, directives 7 resolver.js β resolve extends chain 8 method.js β Method base class + discovery 9 template-methods/ 10 string.js β built-in String methods 11 array.js β built-in Array methods 12 number.js β built-in Number methods 13 test/ 14 parser.test .js 15 renderer.test .js 16 resolver.test .js 17 method.test .js 18 docs/ 19 index.md 20 api.md 21 _meta.json 22 Dockerfile 23 Makefile 24 README.md 25 package.json 26 xpulse.json