Skip to content

A powerful, lightweight TypeScript template engine with reactive data binding, conditional rendering, loops, and events

License

Notifications You must be signed in to change notification settings

Xoboto/template.ts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

35 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Template.Ts

A powerful, lightweight TypeScript template engine with reactive data binding, conditional rendering, loops, and event handling.

License: MIT TypeScript

Overview

Template.Ts is a simple yet powerful template engine that allows you to create dynamic HTML templates with TypeScript. It provides an intuitive syntax for data binding, loops, conditionals, and event handling without the complexity of larger frameworks.

Features

✨ Simple Syntax - Easy-to-learn template directives

  • {{ expression }} - Text interpolation
  • @for="array" - Loop through arrays
  • @if="condition" - Conditional rendering
  • @att:name="value" - Dynamic attribute binding
  • @batt:name="condition" - Boolean attribute binding
  • @prop:name="value" - Custom element property binding
  • @on:event="handler" - Event handling

πŸš€ Lightweight - Zero dependencies, minimal footprint

⚑ Efficient Updates - Only changed parts of the DOM are updated

πŸ”’ Type Safe - Full TypeScript support with proper typing

🎯 Framework Agnostic - Works with any project setup

πŸš€ Live Examples

Try it out right now: https://xoboto.github.io/template.ts/

Installation

Using npm

npm install template.ts

Using yarn

yarn add template.ts

Using CDN

<script type="module">
  import { TemplateBinder } from 'https://unpkg.com/xoboto.ts/dist/template.js';
</script>

Quick Start

1. Create your HTML template

<div id="app">
  <h1>{{ title }}</h1>
  <p>{{ description }}</p>
  <button @on:click="handleClick">Click Me</button>
</div>

2. Bind data with TypeScript

import { TemplateBinder } from 'template.ts';

const state = {
  title: 'Hello World',
  description: 'Welcome to Template.Ts',
  handleClick: function() {
    alert('Button clicked!');
  }
};

const binder = new TemplateBinder('#app', state);
binder.bind();

3. Update the view

// Update state
state.title = 'Updated Title';
state.description = 'Content has changed';

// Refresh the view
binder.update();

Usage Guide

Text Interpolation

Use double curly braces {{ }} to display data:

<div id="app">
  <h1>{{ title }}</h1>
  <p>User: {{ username }}</p>
  <p>Score: {{ score }}</p>
</div>
const state = {
  title: 'Dashboard',
  username: 'John Doe',
  score: 100
};

const binder = new TemplateBinder('#app', state);
binder.bind();

Loops with @for

Iterate through arrays with the @for directive:

<ul>
  <li @for="items">
    {{ item.name }} - ${{ item.price }}
  </li>
</ul>
const state = {
  items: [
    { name: 'Apple', price: 1.99 },
    { name: 'Banana', price: 0.99 },
    { name: 'Orange', price: 2.49 }
  ]
};

const binder = new TemplateBinder('#app', state);
binder.bind();

Inside loops, you have access to:

  • item - Current item in the array
  • index - Current index (0-based)
  • items - The entire array

Nested Loops: In nested loops, use parent to access the outer loop's item:

<tr @for="rows">
  <td @for="columns">{{ getValue(parent, item.key) }}</td>
</tr>

Conditional Rendering with @if

Show or hide elements based on conditions:

<div id="app">
  <p @if="isLoggedIn">Welcome back, {{ username }}!</p>
  <p @if="!isLoggedIn">Please log in.</p>
  
  <ul @if="items.length > 0">
    <li @for="items">{{ item.name }}</li>
  </ul>
  <p @if="items.length === 0">No items available.</p>
</div>
const state = {
  isLoggedIn: true,
  username: 'Alice',
  items: []
};

const binder = new TemplateBinder('#app', state);
binder.bind();

Dynamic Attributes with @att:

Bind any HTML attribute dynamically:

<div id="app">
  <input @att:type="inputType" @att:placeholder="placeholderText" />
  <button @batt:disabled="isDisabled">Submit</button>
  <div @att:class="containerClass">Content</div>
  <img @att:src="imageUrl" @att:alt="imageAlt" />
</div>
const state = {
  inputType: 'email',
  placeholderText: 'Enter your email',
  isDisabled: true, // String value for disabled attribute
  containerClass: 'container active',
  imageUrl: '/images/logo.png',
  imageAlt: 'Company Logo'
};

const binder = new TemplateBinder('#app', state);
binder.bind();

Boolean Attributes with @batt:

Use @batt: for boolean attributes that should be present or absent based on a condition:

<div id="app">
  <input type="checkbox" @batt:checked="isSelected" />
  <button @batt:disabled="isLoading">Submit</button>
  <option @batt:selected="isDefaultOption">Default</option>
  <input @batt:required="isRequired" />
  <details @batt:open="isExpanded">
    <summary>Click to expand</summary>
    <p>Content here</p>
  </details>
</div>
const state = {
  isSelected: true,      // Adds checked="" attribute
  isLoading: false,      // Removes disabled attribute
  isDefaultOption: true, // Adds selected="" attribute
  isRequired: true,      // Adds required="" attribute
  isExpanded: false      // Removes open attribute
};

const binder = new TemplateBinder('#app', state);
binder.bind();

Difference between @att: and @batt:

  • @att:disabled="value" β†’ Sets disabled="value" (always present with the value)
  • @batt:disabled="condition" β†’ Adds disabled="" if condition is truthy, removes it if falsy

Custom Element Properties with @prop:

Use @prop: to bind JavaScript properties directly to custom elements (Web Components). Unlike @att: which sets HTML attributes, @prop: sets element properties:

<div id="app">
  <data-table 
    @prop:columns="columns" 
    @prop:data="tableData"
    @prop:searchable="true"
    @prop:empty-message="emptyMessage">
  </data-table>
</div>
const state = {
  columns: ['Name', 'Email', 'Role'],
  tableData: [
    { name: 'John', email: 'john@example.com', role: 'Admin' },
    { name: 'Jane', email: 'jane@example.com', role: 'User' }
  ],
  emptyMessage: 'No data available'
};

const binder = new TemplateBinder('#app', state);
binder.bind();

Key Points:

  • Attribute names in HTML are automatically converted from kebab-case to camelCase
  • @prop:empty-message becomes element.emptyMessage
  • Perfect for passing complex data (objects, arrays) to Web Components
  • Properties are set directly on the element object, not as HTML attributes

When to use @prop: vs @att::

  • Use @prop: for Web Components and custom elements
  • Use @att: for standard HTML attributes on native elements

Event Handling with @on:

Attach event listeners to elements:

<div id="app">
  <button @on:click="handleClick">Click Me</button>
  <input @on:input="handleInput" @on:focus="handleFocus" />
  <form @on:submit="handleSubmit">
    <button type="submit">Submit</button>
  </form>
</div>
const state = {
  handleClick: function() {
    console.log('Button clicked!');
  },
  handleInput: function(e: Event) {
    const value = (e.target as HTMLInputElement).value;
    console.log('Input:', value);
  },
  handleFocus: function() {
    console.log('Input focused');
  },
  handleSubmit: function(e: Event) {
    e.preventDefault();
    console.log('Form submitted');
  }
};

const binder = new TemplateBinder('#app', state);
binder.bind();

Event Handlers in Loops

Event handlers in loops receive the item and index as parameters:

<ul>
  <li @for="todos">
    <input type="checkbox" @on:change="toggleTodo" />
    <span>{{ item.text }}</span>
    <button @on:click="deleteTodo">Delete</button>
  </li>
</ul>
interface Todo {
  text: string;
  completed: boolean;
}

const state = {
  todos: [
    { text: 'Learn Template.Ts', completed: false },
    { text: 'Build an app', completed: false }
  ],
  toggleTodo: function(e: Event, item: Todo, index: number) {
    this.todos[index].completed = !this.todos[index].completed;
    binder.update();
  },
  deleteTodo: function(e: Event, item: Todo, index: number) {
    this.todos.splice(index, 1);
    binder.update();
  }
};

const binder = new TemplateBinder('#app', state);
binder.bind();

Function Calls in Templates

Call functions within your templates:

<div id="app">
  <p>Total: {{ calculateTotal() }}</p>
  <ul>
    <li @for="items" @att:class="getItemClass(index)">
      {{ item.name }}
    </li>
  </ul>
</div>
const state = {
  items: [
    { name: 'Item 1', price: 10 },
    { name: 'Item 2', price: 20 }
  ],
  calculateTotal: function() {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  },
  getItemClass: function(index: number) {
    return index % 2 === 0 ? 'even' : 'odd';
  }
};

const binder = new TemplateBinder('#app', state);
binder.bind();

Complete Example: Todo List

<!DOCTYPE html>
<html>
<head>
  <title>Todo List</title>
</head>
<body>
  <div id="app">
    <h1>{{ title }}</h1>
    
    <input 
      @att:value="newTodo" 
      @on:input="updateNewTodo" 
      @att:placeholder="'Add a new todo...'" 
    />
    <button @on:click="addTodo">Add</button>
    
    <ul @if="todos.length > 0">
      <li @for="todos" @att:class="item.completed ? 'completed' : ''">
        <input 
          type="checkbox" 
          @batt:checked="item.completed" 
          @on:change="toggleTodo" 
        />
        <span>{{ item.text }}</span>
        <button @on:click="deleteTodo">Delete</button>
      </li>
    </ul>
    
    <p @if="todos.length === 0">No todos yet!</p>
    <p @if="todos.length > 0">
      Total: {{ todos.length }} | Completed: {{ completedCount() }}
    </p>
  </div>
  
  <script type="module" src="./app.js"></script>
</body>
</html>
// app.ts
import { TemplateBinder } from 'template.ts';

interface Todo {
  text: string;
  completed: boolean;
}

interface TodoState {
  title: string;
  todos: Todo[];
  newTodo: string;
  addTodo: () => void;
  updateNewTodo: (e: Event) => void;
  toggleTodo: (e: Event, item: Todo, index: number) => void;
  deleteTodo: (e: Event, item: Todo, index: number) => void;
  completedCount: () => number;
}

const state: TodoState = {
  title: 'My Todo List',
  todos: [],
  newTodo: '',
  
  addTodo: function() {
    if (this.newTodo.trim()) {
      this.todos.push({
        text: this.newTodo,
        completed: false
      });
      this.newTodo = '';
      binder.update();
    }
  },
  
  updateNewTodo: function(e: Event) {
    this.newTodo = (e.target as HTMLInputElement).value;
  },
  
  toggleTodo: function(e: Event, item: Todo, index: number) {
    this.todos[index].completed = !this.todos[index].completed;
    binder.update();
  },
  
  deleteTodo: function(e: Event, item: Todo, index: number) {
    this.todos.splice(index, 1);
    binder.update();
  },
  
  completedCount: function() {
    return this.todos.filter(t => t.completed).length;
  }
};

const binder = new TemplateBinder('#app', state);
binder.bind();

API Reference

TemplateBinder

Constructor

new TemplateBinder(selectorOrElement: string | Element, initialState: State, transitionClass?: string)

Parameters:

  • selectorOrElement: CSS selector string (e.g., "#app") or HTMLElement reference
  • initialState: Object containing your state and methods
  • transitionClass (optional): CSS class name for transition effects (default: "updated")

Examples:

// Using CSS selector
const binder = new TemplateBinder('#app', state);

// Using Element reference
const element = document.getElementById('app');
const binder = new TemplateBinder(element, state);

// With custom transition class
const binder = new TemplateBinder('#app', state, 'my-transition');

// Element with custom transition
const binder = new TemplateBinder(element, state, 'fade-effect');

Auto-Update Property

Enable automatic DOM updates after event handlers execute by setting the autoUpdate property:

const binder = new TemplateBinder('#app', state);
binder.autoUpdate = true; // Enable auto-update
binder.bind();

When autoUpdate is enabled, you don't need to manually call update() after event handlers:

// Without auto-update (manual)
const state = {
  count: 0,
  increment: function() {
    this.count++;
    binder.update(); // Must call manually
  }
};
const binder = new TemplateBinder('#app', state);
binder.bind();

// With auto-update (automatic)
const state = {
  count: 0,
  increment: function() {
    this.count++; // update() called automatically!
  }
};
const binder = new TemplateBinder('#app', state);
binder.autoUpdate = true; // Enable auto-update
binder.bind();

Async Handler Support: Auto-update also works with async handlers - it waits for Promises to resolve:

const state = {
  data: null,
  loadData: async function() {
    this.data = await fetch('/api/data').then(r => r.json());
    // update() called automatically after Promise resolves!
  }
};
const binder = new TemplateBinder('#app', state);
binder.autoUpdate = true;
binder.bind();

Toggle Auto-Update: You can enable or disable auto-update at any time:

binder.autoUpdate = true;  // Enable
binder.autoUpdate = false; // Disable

Methods

bind()

Processes the template and performs initial rendering.

binder.bind();
update(withAnimation?: boolean)

Updates the DOM with the current state. Call this after modifying state values (unless autoUpdate is enabled).

state.title = 'New Title';
binder.update(); // With animation (default: true)
binder.update(false); // Without animation
setState(key, value)

Updates a single state value.

binder.setState('title', 'New Title');
getState()

Returns the state proxy object.

const state = binder.getState();
destroy()

Cleans up bindings and restores original HTML.

binder.destroy();

Template Directives

Directive Description Example
{{ expression }} Text interpolation {{ username }}
@for="arrayName" Loop through array <li @for="items">{{ item.name }}</li>
@if="condition" Conditional rendering <p @if="isVisible">Hello</p>
@att:name="value" Dynamic attribute <div @att:class="className"></div>
@batt:name="condition" Boolean attribute <input @batt:checked="isSelected" />
@prop:name="value" Element property <data-table @prop:data="items"></data-table>
@on:event="handler" Event listener <button @on:click="handleClick">Click</button>

TypeScript Support

Full TypeScript support with proper typing:

import { TemplateBinder } from 'template.ts';

interface AppState {
  count: number;
  increment: () => void;
}

const state: AppState = {
  count: 0,
  increment: function(this: AppState) {
    this.count++;
    binder.update();
  }
};

const binder = new TemplateBinder('#app', state);
binder.bind();

Transition Effects

Template.Ts automatically adds a CSS class when values change, allowing you to create smooth transition effects without extra code!

Limitation: This feature available only on value update. It has no effect on list or conditional view change.

How It Works

When any bound value updates, the transition class is automatically added to the element, then removed after the animation completes. You just need to define the CSS:

Custom Transition Class

const binder = new TemplateBinder('#app', state, 'my-transition');
.my-transition {
  transition: all 0.3s ease;
  transform: scale(1.1);
  color: #1976d2;
}

More Transition Examples

/* Fade effect */
.transition-fade {
  animation: fade 0.3s ease-in-out;
}

@keyframes fade {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

/* Scale pulse */
.transition-scale-pluse {
  animation: pulse 0.4s ease-in-out;
}

@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.1); }
}

/* Slide effect */
.transition-slide {
  animation: slide 0.3s ease-out;
}

@keyframes slide {
  0% { transform: translateX(-10px); opacity: 0; }
  100% { transform: translateX(0); opacity: 1; }
}

/* Color change */
.transition-color {
  transition: color 0.5s ease;
  color: #d32f2f;
}

That's it! No extra JavaScript needed - just define your CSS and the transitions happen automatically when values update.

NOTE: The transition css will apply to element once .update() call.

NOTE: .update method can be called with withAnimation parameter to control animation when needed to bypass animation. the default value is true.

Creating Custom Binders

You can extend Template.Ts by creating custom binders that implement the IBinder interface:

import { IBinder, BinderContext } from 'template.ts';

class MyCustomBinder implements IBinder {
  readonly priority = 70; // Lower numbers execute first
  private bindings: any[] = [];

  // Check if this binder should handle the element
  canHandle(element: Element, _context: BinderContext): boolean {
    return element.hasAttribute('@custom:action');
  }

  // Process the element (called once during bind)
  processElement(element: Element, context: BinderContext): void | 'skip-children' {
    const action = element.getAttribute('@custom:action');
    
    if (context.isStaticBinding) {
      // Handle static binding (e.g., in loops)
      // Evaluate immediately
    } else {
      // Store for dynamic updates
      this.bindings.push({ element, action });
    }
    
    element.removeAttribute('@custom:action');
    // Return 'skip-children' to prevent processing child elements
  }

  // Legacy method - not used with hierarchical walker
  process(_context: BinderContext): void {}

  // Update bound elements when state changes
  update(context: BinderContext, _withAnimation?: boolean): void {
    this.bindings.forEach(binding => {
      // Update logic here
    });
  }

  // Clean up bindings
  clear(_context: BinderContext): void {
    this.bindings = [];
  }
}

// Register your custom binder
const binder = new TemplateBinder('#app', state);
binder.addBinder(new MyCustomBinder());
binder.bind();

Priority Order:

  • 10: LoopBinder (@for)
  • 20: ConditionalBinder (@if)
  • 30: TextBinder ({{ }})
  • 40: AttributeBinder (@att:, @batt:)
  • 50: PropertyBinder (@prop:)
  • 60: EventBinder (@on:)
  • 70+: Your custom binders

Browser Support

  • Chrome/Edge (latest)
  • Firefox (latest)
  • Safari (latest)
  • Modern browsers with ES2020+ support

Examples

🌐 Live Examples

Try it online: https://xoboto.github.io/template.ts/

πŸ’» Run Examples Locally

Check out the /examples folder for more complete examples:

npm run example

This will start a local server and open the examples in your browser.

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

MIT License - see the LICENSE file for details.

Author

Xoboto Contributors

Links


Made with ❀️ using TypeScript

Packages

No packages published

Contributors 2

  •  
  •