Templating
WildflowerJS uses standard HTML <template> elements for list rendering, providing clean separation between layout structure and repeated content.
HTML Template Elements
Use standard HTML <template> elements for clean, semantic templating:
<div data-component="todo-template-demo">
<h4>HTML Template Demo</h4>
<div class="mb-3">
<div class="d-flex gap-2">
<input type="text"
class="form-control"
data-model="newTodo"
placeholder="Add a new todo..."
data-action="keyup:handleKeyup">
<button class="btn btn-primary" data-action="addTodo">
Add
</button>
</div>
</div>
<div class="mb-3">
<button class="btn btn-secondary btn-sm me-2" data-action="addSampleTodos">
Add Sample Todos
</button>
<button class="btn btn-warning btn-sm me-2" data-action="toggleAllComplete">
Toggle All Complete
</button>
<button class="btn btn-danger btn-sm" data-action="clearCompleted">
Clear Completed
</button>
</div>
<div class="mb-3">
<span class="badge bg-primary me-2">Total: <span data-bind="totalCount"></span></span>
<span class="badge bg-success me-2">Completed: <span data-bind="completedCount"></span></span>
<span class="badge bg-warning">Remaining: <span data-bind="remainingCount"></span></span>
</div>
<!-- HTML5 Template List -->
<div data-list="todos" class="mt-3">
<template>
<div class="card mb-2" data-bind-class="completed ? 'bg-light' : ''">
<div class="card-body d-flex align-items-center gap-2 py-2">
<input type="checkbox"
class="form-check-input flex-shrink-0"
data-model="completed">
<span class="flex-grow-1" data-bind="text"
data-bind-class="completed ? 'text-decoration-line-through text-muted' : ''"></span>
<small class="text-muted flex-shrink-0">#<span data-bind="number"></span></small>
<button class="btn btn-sm btn-danger flex-shrink-0" data-action="removeTodo">Delete</button>
</div>
</div>
</template>
</div>
<div data-show="isEmpty" class="text-center text-muted py-4">
No todos yet. Add one above to get started!
</div>
</div>
wildflower.component('todo-template-demo', {
state: {
todos: [
{ text: 'Learn WildflowerJS templates', completed: false, number: 1 },
{ text: 'Build an awesome app', completed: false, number: 2 },
{ text: 'Deploy to production', completed: false, number: 3 }
],
newTodo: ''
},
computed: {
totalCount() {
return this.todos.length
},
completedCount() {
return this.todos.filter(todo => todo.completed).length
},
remainingCount() {
return this.totalCount - this.completedCount
},
isEmpty() {
return this.todos.length === 0
}
},
addTodo() {
const text = this.newTodo.trim()
if (text) {
this.todos.push({
text: text,
completed: false,
number: this.todos.length + 1
})
this.newTodo = ''
}
},
removeTodo(event, element, details) {
// Use details.index from the framework's action context
this.todos.splice(details.index, 1)
// Renumber remaining todos
this.todos.forEach((todo, i) => {
todo.number = i + 1
})
},
addSampleTodos() {
const samples = [
'Review code changes',
'Update documentation',
'Test new features',
'Fix reported bugs',
'Optimize performance'
]
samples.forEach(text => {
this.todos.push({
text: text,
completed: Math.random() > 0.7,
number: this.todos.length + 1
})
})
},
toggleAllComplete() {
const allCompleted = this.todos.every(todo => todo.completed)
this.todos.forEach(todo => {
todo.completed = !allCompleted
})
},
clearCompleted() {
this.todos = this.todos.filter(todo => !todo.completed)
},
handleKeyup(event) {
if (event.key === 'Enter') {
this.addTodo()
}
}
})
Nested Templates & Template Context
Templates can contain other lists and templates for complex nested structures, with full access to template context:
<div data-component="organization-demo">
<h4>Nested Templates & Context Demo</h4>
<div class="mb-3">
<button class="btn btn-primary btn-sm me-2" data-action="addDepartment">
Add Random Department
</button>
<button class="btn btn-success btn-sm me-2" data-action="addEmployeeToFirst">
Add Employee to First Dept
</button>
<button class="btn btn-danger btn-sm" data-action="clearAll">
Clear All
</button>
</div>
<div class="mb-3">
<span class="badge bg-primary me-2">Departments: <span data-bind="departmentCount"></span></span>
<span class="badge bg-success me-2">Total Employees: <span data-bind="totalEmployees"></span></span>
<span class="badge bg-info">Company Size: <span data-bind="companySize"></span></span>
</div>
<!-- Nested Template Structure -->
<div data-list="departments">
<template>
<div class="card mb-4">
<div class="card-header bg-light">
<div class="d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-1"><span data-bind="name"></span> <small class="text-muted">(Dept #<span data-bind="number"></span>)</small></h5>
<small>Manager: <span data-bind="manager"></span> | Budget: $<span data-bind="budget"></span>k</small>
</div>
<div>
<button class="btn btn-sm btn-primary me-2" data-action="addEmployee">
Add Employee
</button>
<button class="btn btn-sm btn-danger" data-action="removeDepartment">
Remove Dept
</button>
</div>
</div>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-md-6">
<strong>Team Members (<span data-bind="employeeCount"></span>):</strong>
</div>
<div class="col-md-6 text-end">
<small class="text-muted">Avg Salary: $<span data-bind="averageSalary"></span>k</small>
</div>
</div>
<!-- Nested Employee List -->
<div class="row" data-list="employees">
<template>
<div class="col-md-6 mb-3">
<div class="d-flex align-items-center p-2 border rounded bg-light">
<div class="rounded-circle me-3 employee-avatar d-flex align-items-center justify-content-center text-white fw-bold"
style="width:40px; height:40px; font-size:0.8rem;"
data-bind-style="{ backgroundColor: color }">
<span data-bind="initials"></span>
</div>
<div class="flex-grow-1">
<div class="fw-bold"><span data-bind="name"></span></div>
<small class="text-muted">
<span data-bind="role"></span> |
$<span data-bind="salary"></span>k
<br>ID: EMP-<span data-bind="number"></span>
</small>
</div>
<button class="btn btn-sm btn-danger" data-action="removeEmployee">
×
</button>
</div>
</div>
</template>
</div>
<div data-show="isEmpty" class="text-center text-muted py-3">
No employees in this department yet.
</div>
</div>
</div>
</template>
</div>
<div data-show="noData" class="text-center text-muted py-4">
No departments yet. Add one to see nested templates in action!
</div>
</div>
wildflower.component('organization-demo', {
state: {
departments: [
{
name: 'Engineering',
manager: 'Sarah Chen',
budget: 500,
number: 1,
employees: [
{ name: 'Alex Rodriguez', role: 'Senior Dev', salary: 120, initials: 'AR', color: '#4CAF50', number: 1 },
{ name: 'Jamie Park', role: 'Frontend Dev', salary: 95, initials: 'JP', color: '#2196F3', number: 2 }
]
},
{
name: 'Design',
manager: 'Jordan Kim',
budget: 300,
number: 2,
employees: [
{ name: 'Casey Johnson', role: 'UX Designer', salary: 85, initials: 'CJ', color: '#FF9800', number: 1 }
]
}
]
},
computed: {
departmentCount() {
return this.departments.length
},
totalEmployees() {
return this.departments.reduce((sum, dept) => sum + dept.employees.length, 0)
},
companySize() {
const total = this.totalEmployees
if (total < 10) return 'Startup'
if (total < 50) return 'Small'
if (total < 200) return 'Medium'
return 'Large'
},
noData() {
return this.departments.length === 0
},
// Department-level computed properties (work within list item context)
employeeCount() {
// Use fallback for when this is called from component level
const employees = this.employees || []
return employees.length
},
averageSalary() {
// Use fallback for when this is called from component level
const employees = this.employees || []
if (employees.length === 0) return '0'
const total = employees.reduce((sum, emp) => sum + emp.salary, 0)
return Math.round(total / employees.length)
},
isEmpty() {
// Use fallback for when this is called from component level
const employees = this.employees || []
return employees.length === 0
}
},
addDepartment() {
const deptNames = ['Marketing', 'Sales', 'HR', 'Finance', 'Operations', 'Legal', 'Support']
const managers = ['Alex Smith', 'Taylor Brown', 'Morgan Davis', 'Jordan Wilson', 'Casey Miller']
const budgets = [200, 300, 400, 500, 600]
const name = deptNames[Math.floor(Math.random() * deptNames.length)]
const manager = managers[Math.floor(Math.random() * managers.length)]
const budget = budgets[Math.floor(Math.random() * budgets.length)]
// Immutable update: create new departments array with new department
const newDepartment = {
name: name,
manager: manager,
budget: budget,
employees: [],
number: this.departments.length + 1
}
this.departments = [...this.departments, newDepartment]
},
addEmployee(event, element, details) {
// Use details.index for the department index from the framework's action context
this.addEmployeeToDepartment(details.index)
},
addEmployeeToFirst() {
if (this.departments.length > 0) {
this.addEmployeeToDepartment(0)
}
},
addEmployeeToDepartment(deptIndex) {
const names = ['Riley Johnson', 'Avery Williams', 'Quinn Brown', 'Dakota Smith', 'River Jones']
const roles = ['Developer', 'Designer', 'Analyst', 'Manager', 'Specialist', 'Coordinator']
const colors = ['E91E63', '9C27B0', 'FF5722', '795548', '607D8B']
const name = names[Math.floor(Math.random() * names.length)]
const role = roles[Math.floor(Math.random() * roles.length)]
const color = colors[Math.floor(Math.random() * colors.length)]
const salary = Math.floor(Math.random() * 80) + 60
const initials = name.split(' ').map(n => n[0]).join('')
// Create new employee
const newEmployee = {
name: name,
role: role,
salary: salary,
initials: initials,
color: '#' + color,
number: this.departments[deptIndex].employees.length + 1
}
// Immutable update: create new employees array for specific department
const currentEmployees = this.departments[deptIndex].employees
const newEmployees = [...currentEmployees, newEmployee]
// Update the specific department with new employees array
this.departments = [
...this.departments.slice(0, deptIndex),
{ ...this.departments[deptIndex], employees: newEmployees },
...this.departments.slice(deptIndex + 1)
]
},
removeEmployee(event, element, details) {
// Use details.index for employee index and details.parent.index for department
const empIndex = details.index
const deptIndex = details.parent.index
// Create new employees array with employee removed and renumbered
const currentEmployees = this.departments[deptIndex].employees
const newEmployees = currentEmployees
.filter((_, eIndex) => eIndex !== empIndex)
.map((employee, i) => ({ ...employee, number: i + 1 }))
// Update the specific department with new employees array
this.departments = [
...this.departments.slice(0, deptIndex),
{ ...this.departments[deptIndex], employees: newEmployees },
...this.departments.slice(deptIndex + 1)
]
},
removeDepartment(event, element, details) {
// Use details.index from the framework's action context
const deptIndex = details.index
// Immutable update: create new departments array with department removed and renumbered
this.departments = this.departments
.filter((_, index) => index !== deptIndex)
.map((dept, i) => ({ ...dept, number: i + 1 }))
},
clearAll() {
// Immutable update: replace with new empty array
this.departments = []
}
})
Template Context Reference
Within templates, you have access to both the list item data and list context variables:
| Context | Access Method | Description |
|---|---|---|
| List Item Data | data-bind="propertyName" |
Direct access to current item properties |
| Item Index | data-bind="_index" |
Zero-based index of current item |
| List Length | data-bind="_length" |
Total number of items in the list |
| First Item | _first |
Boolean: true if this is the first item |
| Last Item | _last |
Boolean: true if this is the last item |
| Component Computed | data-bind="propName" |
Computed properties resolve by name (computed: prefix optional) |
_first and _last in expressions like
data-bind-class="_first ? 'disabled' : ''" to style boundary items.
For action handlers, use details.index, details.first, details.last instead.
Template Performance
- Templates are cloned efficiently using DocumentFragment
- Template content is parsed once and reused
- Only changed list items are re-rendered
- Keyed reconciliation minimizes DOM operations
Large List Optimization
WildflowerJS automatically optimizes rendering for large lists:
// For very large datasets, consider using computed properties for filtering/sorting
computed: {
visibleItems() {
const start = this.currentPage * this.pageSize
const end = start + this.pageSize
return this.allItems.slice(start, end)
}
}