template.ts
Version:
A powerful, lightweight TypeScript template engine with reactive data binding, conditional rendering, loops, and events
733 lines (566 loc) ⢠16.6 kB
Markdown
# Template.Ts
A powerful, lightweight TypeScript template engine with reactive data binding, conditional rendering, loops, and event handling.
[](https://opensource.org/licenses/MIT)
[](https://www.typescriptlang.org/)
## 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
- `@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/](https://xoboto.github.io/template.ts/)**
## Installation
### Using npm
```bash
npm install template.ts
```
### Using yarn
```bash
yarn add template.ts
```
### Using CDN
```html
<script type="module">
import { TemplateBinder } from 'https://unpkg.com/xoboto.ts/dist/template.js';
</script>
```
## Quick Start
### 1. Create your HTML template
```html
<div id="app">
<h1>{{ title }}</h1>
<p>{{ description }}</p>
<button @on:click="handleClick">Click Me</button>
</div>
```
### 2. Bind data with TypeScript
```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
```typescript
// 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:
```html
<div id="app">
<h1>{{ title }}</h1>
<p>User: {{ username }}</p>
<p>Score: {{ score }}</p>
</div>
```
```typescript
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:
```html
<ul>
<li @for="items">
{{ item.name }} - ${{ item.price }}
</li>
</ul>
```
```typescript
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:
```html
<tr @for="rows">
<td @for="columns">{{ getValue(parent, item.key) }}</td>
</tr>
```
### Conditional Rendering with @if
Show or hide elements based on conditions:
```html
<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>
```
```typescript
const state = {
isLoggedIn: true,
username: 'Alice',
items: []
};
const binder = new TemplateBinder('#app', state);
binder.bind();
```
### Dynamic Attributes with @att:
Bind any HTML attribute dynamically:
```html
<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>
```
```typescript
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:
```html
<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>
```
```typescript
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
### Event Handling with @on:
Attach event listeners to elements:
```html
<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>
```
```typescript
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:
```html
<ul>
<li @for="todos">
<input type="checkbox" @on:change="toggleTodo" />
<span>{{ item.text }}</span>
<button @on:click="deleteTodo">Delete</button>
</li>
</ul>
```
```typescript
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:
```html
<div id="app">
<p>Total: {{ calculateTotal() }}</p>
<ul>
<li @for="items" @att:class="getItemClass(index)">
{{ item.name }}
</li>
</ul>
</div>
```
```typescript
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
```html
<!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>
```
```typescript
// 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
```typescript
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:**
```typescript
// 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:
```typescript
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:
```typescript
// 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:
```typescript
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:
```typescript
binder.autoUpdate = true; // Enable
binder.autoUpdate = false; // Disable
```
#### Methods
##### `bind()`
Processes the template and performs initial rendering.
```typescript
binder.bind();
```
##### `update(withAnimation?: boolean)`
Updates the DOM with the current state. Call this after modifying state values (unless autoUpdate is enabled).
```typescript
state.title = 'New Title';
binder.update(); // With animation (default: true)
binder.update(false); // Without animation
```
##### `setState(key, value)`
Updates a single state value.
```typescript
binder.setState('title', 'New Title');
```
##### `getState()`
Returns the state proxy object.
```typescript
const state = binder.getState();
```
##### `destroy()`
Cleans up bindings and restores original HTML.
```typescript
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" />` |
| `@on:event="handler"` | Event listener | `<button @on:click="handleClick">Click</button>` |
## TypeScript Support
Full TypeScript support with proper typing:
```typescript
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
```typescript
const binder = new TemplateBinder('#app', state, 'my-transition');
```
```css
.my-transition {
transition: all 0.3s ease;
transform: scale(1.1);
color: #1976d2;
}
```
### More Transition Examples
```css
/* 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**.
## 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/](https://xoboto.github.io/template.ts/)
### š» Run Examples Locally
Check out the `/examples` folder for more complete examples:
```bash
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](LICENSE) file for details.
## Author
Xoboto Contributors
## Links
- [GitHub Repository](https://github.com/xoboto/template.ts)
- [Issues](https://github.com/xoboto/template.ts/issues)
- [NPM Package](https://www.npmjs.com/package/template.ts)
---
Made with ā¤ļø using TypeScript