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()
        }
    }
})
Live Preview

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: {
        // Numbers are IDs, so they are assigned once from these counters
        // and never reused or reshuffled by a delete.
        nextDepartmentNumber: 3,
        departments: [
            {
                name: 'Engineering',
                manager: 'Sarah Chen',
                budget: 500,
                number: 1,
                nextEmployeeNumber: 3,
                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,
                nextEmployeeNumber: 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. In a list row, a computed
        // receives the row's item (here, the department) as its argument.
        employeeCount(dept) {
            return dept.employees.length
        },

        averageSalary(dept) {
            const employees = dept.employees
            if (employees.length === 0) return '0'
            const total = employees.reduce((sum, emp) => sum + emp.salary, 0)
            return Math.round(total / employees.length)
        },

        isEmpty(dept) {
            return dept.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.nextDepartmentNumber,
            nextEmployeeNumber: 1
        }

        this.nextDepartmentNumber++
        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. The ID comes from the department's counter,
        // never from array length: length + 1 collides after a delete.
        const dept = this.departments[deptIndex]
        const newEmployee = {
            name: name,
            role: role,
            salary: salary,
            initials: initials,
            color: '#' + color,
            number: dept.nextEmployeeNumber
        }

        // Immutable update: new employees array, counter advanced
        this.departments = [
            ...this.departments.slice(0, deptIndex),
            {
                ...dept,
                employees: [...dept.employees, newEmployee],
                nextEmployeeNumber: dept.nextEmployeeNumber + 1
            },
            ...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 the employee removed. IDs are
        // stable: the remaining employees keep the numbers they had.
        const currentEmployees = this.departments[deptIndex].employees
        const newEmployees = currentEmployees
            .filter((_, eIndex) => eIndex !== empIndex)

        // 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 the department
        // removed. Department numbers are stable IDs too.
        this.departments = this.departments
            .filter((_, index) => index !== deptIndex)
    },

    clearAll() {
        // Immutable update: replace with new empty array
        this.departments = []
    }
})
Live Preview

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)
Tip: Use _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

✅ Performance Features:
  • 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)
    }
}