Lists & Iteration
WildflowerJS renders arrays as dynamic lists using data-list and HTML <template> elements, automatically updating only the items that change.
Basic List Rendering
Use data-list to render arrays with HTML templates:
<div data-component="simple-list">
<p class="text-muted">Demonstrates basic list operations with automatic updates.</p>
<div class="mb-3">
<button data-action="addItem" class="btn btn-primary me-2">
Add Item
</button>
<button data-action="addMultiple" class="btn btn-success me-2">
Add 3 Items
</button>
<button data-action="shuffle" class="btn btn-secondary me-2">
Shuffle Order
</button>
<button data-action="clearAll" class="btn btn-danger">
Clear All
</button>
</div>
<div class="row mb-3">
<div class="col-md-8">
<p><strong>Total items:</strong> <span data-bind="itemCount" class="badge bg-primary"></span></p>
</div>
<div class="col-md-4">
<small class="text-muted">Next ID: <span data-bind="nextId"></span></small>
</div>
</div>
<!-- List with HTML template -->
<ul class="list-group" data-list="items">
<template>
<li class="list-group-item d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center flex-grow-1">
<span class="badge bg-secondary me-3">#<span data-bind="id"></span></span>
<div>
<strong data-bind="name"></strong>
<br>
<small class="text-muted">
Created: <span data-bind="created"></span>
</small>
</div>
</div>
<button data-action="moveUp"
class="btn btn-sm me-2"
data-bind-class="_first || _length === 1 ? 'btn-secondary disabled' : 'btn-primary'">
↑
</button>
<button data-action="moveDown"
class="btn btn-sm me-2"
data-bind-class="_last || _length === 1 ? 'btn-secondary disabled' : 'btn-primary'">
↓
</button>
<button data-action="removeItem" class="btn btn-sm btn-danger">
Remove
</button>
</li>
</template>
</ul>
<!-- Empty state -->
<div data-show="isEmpty" class="text-center py-4">
<div class="text-muted">
<h5>No items in the list</h5>
<p>Click "Add Item" to get started!</p>
</div>
</div>
</div>
wildflower.component('simple-list', {
state: {
items: [
{ id: 1, name: 'First Item', created: '10:00:00' },
{ id: 2, name: 'Second Item', created: '10:01:00' },
{ id: 3, name: 'Third Item', created: '10:02:00' }
],
nextId: 4
},
computed: {
itemCount() {
return this.items.length
},
isEmpty() {
return this.items.length === 0
},
},
addItem() {
const newItem = {
id: this.nextId++,
name: `Dynamic Item ${this.nextId - 1}`,
created: new Date().toLocaleTimeString()
}
this.items.push(newItem)
console.log('Added item:', newItem)
},
addMultiple() {
const itemsToAdd = [
{ id: this.nextId++, name: `Batch Item A`, created: new Date().toLocaleTimeString() },
{ id: this.nextId++, name: `Batch Item B`, created: new Date().toLocaleTimeString() },
{ id: this.nextId++, name: `Batch Item C`, created: new Date().toLocaleTimeString() }
]
// Add all items at once
this.items.push(...itemsToAdd)
console.log('Added multiple items:', itemsToAdd)
},
removeItem(event, element, details) {
// Get the index from the framework's action context
const index = details.index
const removedItem = this.items[index]
this.items.splice(index, 1)
console.log('Removed item:', removedItem)
},
moveUp(event, element, details) {
if (details.first) return // Already at top
const { index, item } = details
this.items.splice(index, 1)
this.items.splice(index - 1, 0, item)
console.log('Moved item up:', item)
},
moveDown(event, element, details) {
if (details.last) return // Already at bottom
const { index, item } = details
this.items.splice(index, 1)
this.items.splice(index + 1, 0, item)
console.log('Moved item down:', item)
},
shuffle() {
// Fisher-Yates shuffle algorithm
const items = [...this.items]
for (let i = items.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[items[i], items[j]] = [items[j], items[i]]
}
this.items = items
console.log('Shuffled items')
},
clearAll() {
if (this.items.length > 0) {
const count = this.items.length
this.items = []
console.log(`Cleared ${count} items`)
}
}
})
Primitive Lists
Lists of strings, numbers, or other primitives use $item to bind the item value directly:
<!-- List of strings -->
<ul data-list="tags">
<template>
<li data-bind="$item"></li>
</template>
</ul>
wildflower.component('tag-list', {
state: {
tags: ['html', 'css', 'javascript']
}
})
$item refers to the current item itself, rather than a property on the item. This works with any primitive type:
<!-- List of numbers -->
<ol data-list="scores">
<template>
<li>Score: <span data-bind="$item"></span></li>
</template>
</ol>
$item is the only way to bind primitive list items. For object arrays, use property names directly (e.g., data-bind="name").
Interactive Lists with State
List items can have their own state and interactive elements:
<div data-component="todo-manager">
<p class="text-muted">A fully interactive todo list with individual item state and bulk operations.</p>
<!-- Add new todo form -->
<form data-action="addTodo" class="mb-3">
<div class="d-flex gap-2">
<input type="text"
data-model="newTodo"
placeholder="Add a new todo..."
class="form-control"
required>
<button type="submit" class="btn btn-primary">
Add Todo
</button>
</div>
</form>
<!-- Bulk operations -->
<div class="mb-3">
<button data-action="markAllComplete" class="btn btn-sm btn-success me-2">
Mark All Complete
</button>
<button data-action="clearCompleted"
class="btn btn-sm btn-warning me-2"
data-bind-attr="{ disabled: noCompleted }">
Clear Completed (<span data-bind="completedCount"></span>)
</button>
<button data-action="clearAll"
class="btn btn-sm btn-danger"
data-bind-attr="{ disabled: isEmpty }">
Clear All
</button>
</div>
<!-- Statistics -->
<div class="mb-3">
<span class="me-3">
Total: <span class="text-primary fw-bold" data-bind="totalCount"></span>
</span>
<span class="me-3">
Complete: <span class="text-success fw-bold" data-bind="completedCount"></span>
</span>
<span class="me-3">
Remaining: <span class="text-warning fw-bold" data-bind="remainingCount"></span>
</span>
<span>
Progress: <span class="text-info fw-bold" data-bind="progressPercentage"></span>%
</span>
</div>
<!-- Todo list with interactive items -->
<div data-list="todos">
<template>
<div class="card mb-2" data-bind-class="cardClass">
<div class="card-body py-2">
<div class="d-flex align-items-center gap-2">
<input type="checkbox"
data-model="completed"
class="form-check-input flex-shrink-0">
<div class="flex-grow-1">
<span data-bind="text"
data-bind-class="completed ? 'text-decoration-line-through text-muted' : ''"></span>
<br>
<small class="text-muted">
<span class="badge badge-sm me-1" data-bind-class="priorityBadgeClass">
<span data-bind="priority"></span>
</span>
<span data-bind="created"></span>
</small>
</div>
<div class="d-flex align-items-center flex-shrink-0">
<button data-action="duplicateTodo"
class="btn btn-sm btn-info me-1">
Copy
</button>
<button data-action="deleteTodo"
class="btn btn-sm btn-danger">
Delete
</button>
</div>
</div>
</div>
</div>
</template>
</div>
<!-- Empty state -->
<div data-show="isEmpty" class="text-center py-4">
<div class="text-muted">
<h5>No todos yet</h5>
<p>Add a todo above to get started with your task management!</p>
</div>
</div>
</div>
wildflower.component('todo-manager', {
state: {
todos: [
{
text: 'Learn WildflowerJS Lists',
completed: false,
priority: 'High',
created: '09:00',
id: 1
},
{
text: 'Build an interactive todo app',
completed: false,
priority: 'Medium',
created: '09:15',
id: 2
},
{
text: 'Write comprehensive documentation',
completed: true,
priority: 'Low',
created: '09:30',
id: 3
}
],
newTodo: '',
nextId: 4
},
computed: {
totalCount() {
return this.todos.length
},
completedCount() {
return this.todos.filter(todo => todo.completed).length
},
remainingCount() {
return this.todos.filter(todo => !todo.completed).length
},
progressPercentage() {
if (this.todos.length === 0) return 0
return Math.round((this.completedCount / this.todos.length) * 100)
},
isEmpty() {
return this.todos.length === 0
},
noCompleted() {
return this.completedCount === 0
},
// Item-level computed properties
cardClass() {
return this.completed ? 'bg-light border-success' : 'border-primary'
},
priorityBadgeClass() {
const classes = {
'High': 'bg-danger',
'Medium': 'bg-warning text-dark',
'Low': 'bg-success'
}
return classes[this.priority] || 'bg-secondary'
}
},
addTodo(event, element) {
// Prevent form submission
event.preventDefault()
const text = this.newTodo.trim()
if (text) {
const newTodo = {
text: text,
completed: false,
priority: 'Medium',
created: new Date().toLocaleTimeString().substring(0, 5),
id: this.nextId++
}
this.todos.push(newTodo)
this.newTodo = ''
console.log('Added todo:', newTodo)
}
},
deleteTodo(event, element, details) {
const index = details.index
const todo = this.todos[index]
if (confirm(`Delete "${todo.text}"?`)) {
this.todos.splice(index, 1)
console.log('Deleted todo:', todo)
}
},
duplicateTodo(event, element, details) {
const index = details.index
const originalTodo = this.todos[index]
const duplicatedTodo = {
text: `${originalTodo.text} (Copy)`,
completed: false,
priority: originalTodo.priority,
created: new Date().toLocaleTimeString().substring(0, 5),
id: this.nextId++
}
// Insert duplicate right after the original
this.todos.splice(index + 1, 0, duplicatedTodo)
console.log('Duplicated todo:', duplicatedTodo)
},
increasePriority(event, element, details) {
const index = details.index
const priorities = ['Low', 'Medium', 'High']
const currentIndex = priorities.indexOf(this.todos[index].priority)
if (currentIndex < priorities.length - 1) {
this.todos[index].priority = priorities[currentIndex + 1]
console.log('Increased priority for todo:', this.todos[index])
}
},
decreasePriority(event, element, details) {
const index = details.index
const priorities = ['Low', 'Medium', 'High']
const currentIndex = priorities.indexOf(this.todos[index].priority)
if (currentIndex > 0) {
this.todos[index].priority = priorities[currentIndex - 1]
console.log('Decreased priority for todo:', this.todos[index])
}
},
markAllComplete() {
const incompleteCount = this.remainingCount
this.todos.forEach(todo => {
todo.completed = true
})
console.log(`Marked ${incompleteCount} todos as complete`)
},
clearCompleted() {
const completedCount = this.completedCount
this.todos = this.todos.filter(todo => !todo.completed)
console.log(`Cleared ${completedCount} completed todos`)
},
clearAll() {
if (confirm('Clear all todos?')) {
const totalCount = this.todos.length
this.todos = []
console.log(`Cleared all ${totalCount} todos`)
}
}
})
Item-Level Computed Properties
Use item-level computed properties for per-item calculations that need access to external data (like stores) with automatic reactivity.
- Item-level:
name(item, index, info) { ... }runs per row in a list.itemis the current row,indexis its position,infois{ first, last, length }.thisis the component context, sothis.state.X,this.stores.X,this.props.X, andthis.computed.Xall work. - Component-level:
name() { ... }runs once per component.thisis the component. Used for derived values that don't depend on a list row.
item parameter. A zero-arg computed referenced inside a list-template binding is treated as component-level: same value for every row. If your binding renders the same value for every row when you expected per-row variation, your computed is missing the item parameter.
wildflower.store('cart', {
state: { items: [] },
addItem(id) {
const existing = this.items.find(i => i.id === id);
if (existing) {
existing.qty++;
this.items = [...this.items];
} else {
this.items = [...this.items, { id, qty: 1 }];
}
}
});
wildflower.component('product-list', {
state: {
products: [
{ id: 1, name: 'Widget', price: 10 },
{ id: 2, name: 'Gadget', price: 20 }
]
},
computed: {
// Item-level: receives (item, index) - evaluated per list item
inCartQty(item) {
const cart = wildflower.getStore('cart');
return cart.items.find(i => i.id === item.id)?.qty || 0;
},
isInCart(item) {
return this.computed.inCartQty(item) > 0; // Can call other item-level computeds
},
rowClass(item, index) {
return index % 2 === 0 ? 'even' : 'odd';
},
// Item-level computed returning a STYLE OBJECT for data-bind-style
itemStyle(item) {
return {
backgroundColor: item.featured ? '#fffde7' : '#ffffff',
borderColor: item.featured ? '#ffc107' : '#dee2e6'
};
}
},
addToCart(event, element, details) {
wildflower.getStore('cart').addItem(details.item.id);
}
});
<div data-component="product-list">
<div data-list="products" data-key="id">
<template>
<!-- data-bind-style uses computed that returns style object -->
<div class="product" data-bind-class="rowClass" data-bind-style="itemStyle">
<span data-bind="name"></span>
<span class="badge" data-bind="inCartQty"></span>
<span data-show="isInCart">IN CART</span>
<button data-action="addToCart">Add to Cart</button>
</div>
</template>
</div>
</div>
Benefits:
- When the cart store changes, only the affected item bindings update - not the entire list
- No need for pre-computed patterns that map over the entire array
- Works with every binding that takes an expression:
data-bind,data-show,data-render,data-bind-class,data-bind-style, anddata-bind-attr
data-bind-style with computeds, the computed must return a style object like {backgroundColor: '#fff', color: 'red'}. The syntax data-bind-style="backgroundColor:colorProp" is NOT valid.
Reusing One Computed Across Scopes (Group + Row)
Sometimes the same per-row decoration applies to two different list scopes: e.g. a status icon that paints both the group-header row and each issue row inside the group. Make the parameterised computed shape-portable by reading a field that exists on both shapes:
computed: {
// Outer list emits group rows shaped { id, status, statusName, count, rows }.
// Inner list emits issue rows that also carry .status. Both shapes have
// .status, so the same parameterised computed serves both.
statusIcon(item) {
if (!item || item.status === undefined) return '';
return wildflower.getStore('pm').getStatus(item.status)?.icon || '○';
}
}
Used as data-bind="statusIcon" inside both the group template and the row template. The framework passes whatever the current iteration's item is. No special form needed; just keep the field name consistent in your list-source design.
Item-Level Computeds in Compound Expressions
Bare references like data-bind-class="rowClass" are the simplest way to use an item-level computed, but you can also drop the same computed into any expression that an attribute accepts: object literals, ternaries, logical operators, string concatenation. The framework re-evaluates per row and tracks the same fine-grained dependencies.
Object syntax for class
<li data-bind-class="{ saved: isSaved }">
Ternary in class
<li data-bind-class="isSaved ? 'saved' : ''">
Object syntax for attr
<button data-bind-attr="({ 'aria-pressed': isSaved })">
Compound show / render
<span data-show="isSaved && !isLocked">
Ternary in text bind
<span data-bind="isSaved ? '✓ ' + name : name">
Style object
<li data-bind-style="({ borderColor: badgeColor })">
Each form reads isSaved(item) (or badgeColor(item)) once per row with that row's data. Mutating the state the computed reads — for example, this.state.saved[item.id] = true — re-runs only the affected row's bindings.
Live: per-item computed driving five binding types
This product list uses a single item-level computed, isSaved(item), to drive a class binding, two data-show branches, an attribute binding, and the row-level pill text. Click the bookmark to save; the row's bindings flip without re-rendering anything else.
<div data-component="saveable-products">
<div class="d-flex justify-content-between align-items-center mb-3">
<span class="text-muted small">
<strong data-bind="savedCount">0</strong>
<span data-bind="savedLabel"> saved</span>
</span>
<button class="btn btn-sm btn-outline-secondary"
data-show="hasSaved"
data-action="clearSaved">Clear saved</button>
</div>
<ul class="list-group" data-list="products" data-key="id">
<template>
<li class="list-group-item d-flex justify-content-between align-items-center"
data-bind-class="{ 'list-group-item-success': isSaved }">
<!-- Ternary in text bind: prepend ✓ when saved -->
<span data-bind="isSaved ? '✓ ' + name : name"></span>
<span class="d-flex align-items-center gap-2">
<span class="text-muted small" data-bind="price"></span>
<!-- Compound data-show with item-level computed -->
<span class="badge bg-success" data-show="isSaved">Saved</span>
<!-- Object-syntax attr binding (aria-pressed) plus
compound show/!show on the same button text -->
<button class="btn btn-sm btn-outline-primary"
data-action="toggleSave"
data-bind-attr="({ 'aria-pressed': isSaved })"
data-bind="isSaved ? 'Remove' : 'Save'"></button>
</span>
</li>
</template>
</ul>
</div>
wildflower.component('saveable-products', {
state: {
products: [
{ id: 'p1', name: 'Wireless headphones', price: '$89' },
{ id: 'p2', name: 'Mechanical keyboard', price: '$129' },
{ id: 'p3', name: 'USB-C hub', price: '$45' },
{ id: 'p4', name: 'Standing desk mat', price: '$72' },
{ id: 'p5', name: 'Webcam 1080p', price: '$58' }
],
// Sibling state keyed by item.id: the canonical lookup-map shape
// for item-level computeds.
saved: { p3: { savedAt: Date.now() } }
},
computed: {
// Zero-arg computeds resolve in component scope. Note the bare
// `this.saved`: no `this.state.` prefix needed; the context proxy
// routes it. Same for `this.savedCount` from another computed.
savedCount() { return Object.keys(this.saved).length; },
hasSaved() { return this.savedCount > 0; },
savedLabel() {
const n = this.savedCount;
return n === 1 ? ' product saved' : ' products saved';
},
// Item-level computed: fn.length > 0, called once per row.
// Reads from state.saved keyed by item.id. When state.saved.
// changes (direct mutation), only the affected row re-evaluates.
isSaved(item) { return !!this.saved[item.id]; }
},
toggleSave(event, element, details) {
const item = details && details.item;
if (!item) return;
// Direct nested mutation triggers reactivity. No spread needed.
if (this.saved[item.id]) {
delete this.saved[item.id];
} else {
this.saved[item.id] = { savedAt: Date.now() };
}
},
clearSaved() {
this.saved = {};
}
});
Single computed, five binding types — class, attr, two text bindings, two show branches — all driven from one source of truth.
Item-Level Computeds as Nested-List Sources
Item-level computeds also resolve as the source array of a nested data-list. Inside a parent list template, an inner data-list="someComputed" evaluates the computed against each parent row when the path isn't a raw field on the row. This means you don't have to pre-decorate parent rows with the inner array.
wildflower.component('thread', {
state: {
// Each comment carries raw `reactions` keyed by emoji.
comments: [
{ id: 'c1', body: 'Looks great!', reactions: { '👍': ['u-1', 'u-2'] } },
{ id: 'c2', body: 'Ship it.', reactions: {} }
],
currentUser: 'u-1'
},
computed: {
// Per-comment chip list. `this.id` and `this.reactions` are the
// current comment's fields when this evaluates inside the outer
// data-list template.
reactionChips() {
if (this.id === undefined) return []
const c = this
const me = this.currentUser
return ['👍','❤️','🚀'].map(emoji => {
const users = (c.reactions && c.reactions[emoji]) || []
return {
id: c.id + ':' + emoji,
emoji,
chipClass: users.indexOf(me) !== -1 ? 'chip is-mine' : 'chip'
}
})
}
}
})
<div data-list="comments" data-key="id">
<template>
<div class="comment">
<p data-bind="body"></p>
<!-- reactionChips is a computed; raw comments don't have it -->
<div class="reactions" data-list="reactionChips" data-key="id">
<template>
<button data-bind-class="chipClass" data-bind="emoji"></button>
</template>
</div>
</div>
</template>
</div>
The framework reads item[path] first (fast path) and falls back to evaluating an item-level computed of the same name when the field is undefined. Same behaviour data-bind already had, now extended to nested-list source paths.
Caveats. The fallback only fires when item[path] === undefined and path is a flat name (no dots). A raw field of the same name shadows the computed; if your row has a reactions field and you also want a derived view, name the computed differently (e.g., reactionChips) so the data-list can find it.
Nested Lists and Complex Data
Create nested list structures with complex data relationships:
<div data-component="project-manager">
<p class="text-muted">Nested lists demonstrating project-task relationships with real-time progress tracking.</p>
<!-- Project controls -->
<div class="mb-3">
<button data-action="addProject" class="btn btn-primary me-2">
Add Project
</button>
<button data-action="addSampleData" class="btn btn-secondary me-2">
Add Sample Data
</button>
<button data-action="clearAll" class="btn btn-danger">
Clear All
</button>
</div>
<!-- Overall statistics -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card bg-primary text-white">
<div class="card-body text-center">
<h5 class="card-title"><span data-bind="totalProjects"></span></h5>
<p class="card-text">Total Projects</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-success text-white">
<div class="card-body text-center">
<h5 class="card-title"><span data-bind="totalTasks"></span></h5>
<p class="card-text">Total Tasks</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-warning text-white">
<div class="card-body text-center">
<h5 class="card-title"><span data-bind="completedTasks"></span></h5>
<p class="card-text">Completed Tasks</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card bg-info text-white">
<div class="card-body text-center">
<h5 class="card-title"><span data-bind="overallProgress"></span>%</h5>
<p class="card-text">Overall Progress</p>
</div>
</div>
</div>
</div>
<!-- Projects list with nested tasks -->
<div data-list="projects">
<template>
<div class="card mb-3">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<h5 class="mb-0 me-3">
<span data-bind="name"></span>
<span class="badge bg-secondary ms-2">#<span data-bind="id"></span></span>
</h5>
<div class="me-3">
<div class="progress" style="width: 120px; height: 12px;">
<div class="progress-bar"
data-bind-style="progressBarStyle">
</div>
</div>
<small class="text-muted"><span data-bind="progressPercentage"></span>% complete</small>
</div>
</div>
<button data-action="addTask" class="btn btn-sm btn-primary me-2">
Add Task
</button>
<button data-action="toggleCollapse" class="btn btn-sm btn-secondary me-2">
<span data-bind="collapsed ? 'Expand' : 'Collapse'"></span>
</button>
<button data-action="deleteProject" class="btn btn-sm btn-danger">
Delete
</button>
</div>
</div>
<div class="card-body" data-bind-class="collapsed ? 'd-none' : ''">
<div class="row mb-3">
<div class="col-md-8">
<p class="text-muted mb-0"><span data-bind="description"></span></p>
</div>
<div class="col-md-4">
<small class="text-muted">
Created: <span data-bind="created"></span> |
Tasks: <span data-bind="taskCount"></span>
</small>
</div>
</div>
<!-- Task input form -->
<form data-action="addTaskForm" class="mb-3">
<div class="input-group">
<input type="text"
data-model="newTaskName"
placeholder="New task name..."
class="form-control"
required>
<select data-model="newTaskPriority" class="form-select" style="max-width: 120px;">
<option value="Low">Low</option>
<option value="Medium" selected>Medium</option>
<option value="High">High</option>
</select>
<button type="submit" class="btn btn-primary">Add Task</button>
</div>
</form>
<!-- Nested tasks list -->
<div data-list="tasks">
<template>
<div class="card mb-2 border-start border-3" data-bind-class="taskBorderClass">
<div class="card-body py-2">
<div class="d-flex align-items-center">
<input type="checkbox"
data-model="completed"
class="form-check-input me-3">
<div class="flex-grow-1">
<div class="d-flex justify-content-between align-items-start">
<div>
<strong data-bind="name" data-bind-class="completed ? 'text-decoration-line-through text-muted' : ''"></strong>
<span class="badge badge-sm ms-2" data-bind-class="priorityBadgeClass">
<span data-bind="priority"></span>
</span>
</div>
<button data-action="moveTaskUp"
class="btn btn-sm me-2"
data-bind-class="_first || _length === 1 ? 'btn-secondary disabled' : 'btn-primary'">
↑
</button>
<button data-action="moveTaskDown"
class="btn btn-sm me-2"
data-bind-class="_last || _length === 1 ? 'btn-secondary disabled' : 'btn-primary'">
↓
</button>
<button data-action="deleteTask"
class="btn btn-sm btn-danger">
Delete
</button>
</div>
<small class="text-muted">
Task #<span data-bind="id"></span> |
Created: <span data-bind="created"></span>
</small>
</div>
</div>
</div>
</div>
</template>
</div>
<!-- Empty tasks state -->
<div data-show="hasNoTasks" class="text-center py-3">
<p class="text-muted mb-0">No tasks yet. Add a task above to get started!</p>
</div>
</div>
</div>
</template>
</div>
<!-- Empty projects state -->
<div data-show="hasNoProjects" class="text-center py-4">
<div class="text-muted">
<h5>No projects yet</h5>
<p>Create your first project to start managing tasks!</p>
</div>
</div>
</div>
wildflower.component('project-manager', {
state: {
projects: [
{
id: 1,
name: 'WildflowerJS Documentation',
description: 'Create comprehensive documentation for the framework',
created: '09:00',
collapsed: false,
newTaskName: '',
newTaskPriority: 'Medium',
tasks: [
{
id: 1,
name: 'Setup documentation structure',
priority: 'High',
completed: true,
created: '09:15'
},
{
id: 2,
name: 'Write list examples',
priority: 'Medium',
completed: false,
created: '09:30'
}
]
},
{
id: 2,
name: 'Framework Testing',
description: 'Comprehensive testing suite for all framework features',
created: '10:00',
collapsed: false,
newTaskName: '',
newTaskPriority: 'Medium',
tasks: [
{
id: 3,
name: 'Unit tests for list rendering',
priority: 'High',
completed: false,
created: '10:15'
},
{
id: 4,
name: 'Integration tests',
priority: 'Medium',
completed: false,
created: '10:30'
},
{
id: 5,
name: 'Performance benchmarks',
priority: 'Low',
completed: true,
created: '10:45'
}
]
}
],
nextProjectId: 3,
nextTaskId: 6
},
computed: {
// Global statistics
totalProjects() {
return this.projects.length
},
totalTasks() {
return this.projects.reduce((total, project) => {
return total + project.tasks.length
}, 0)
},
completedTasks() {
return this.projects.reduce((total, project) => {
return total + project.tasks.filter(task => task.completed).length
}, 0)
},
overallProgress() {
const total = this.totalTasks
if (total === 0) return 0
return Math.round((this.completedTasks / total) * 100)
},
hasNoProjects() {
return this.projects.length === 0
},
// Project-level computed properties
taskCount() {
// Access tasks from current project context
return this.tasks ? this.tasks.length : 0
},
progressPercentage() {
// Access tasks from current project context
const tasks = this.tasks || []
if (tasks.length === 0) return 0
const completed = tasks.filter(task => task.completed).length
return Math.round((completed / tasks.length) * 100)
},
progressBarStyle() {
// Calculate progress inline (can't call this.computed in list context)
const tasks = this.tasks || []
let progress = 0
if (tasks.length > 0) {
const completed = tasks.filter(task => task.completed).length
progress = Math.round((completed / tasks.length) * 100)
}
let bgColor = '#dc3545' // danger/red
if (progress === 100) bgColor = '#198754' // success/green
else if (progress >= 75) bgColor = '#0dcaf0' // info/cyan
else if (progress >= 50) bgColor = '#ffc107' // warning/yellow
return {
width: progress + '%',
height: '100%',
backgroundColor: bgColor,
transition: 'width 0.3s ease'
}
},
hasNoTasks() {
const tasks = this.tasks || []
return tasks.length === 0
},
// Task-level computed properties
priorityBadgeClass() {
const classes = {
'High': 'bg-danger',
'Medium': 'bg-warning text-dark',
'Low': 'bg-success'
}
return classes[this.priority] || 'bg-secondary'
},
taskBorderClass() {
const classes = {
'High': 'border-danger',
'Medium': 'border-warning',
'Low': 'border-success'
}
return classes[this.priority] || 'border-secondary'
}
},
addProject() {
const newProject = {
id: this.nextProjectId++,
name: `Project ${this.nextProjectId - 1}`,
description: `Description for project ${this.nextProjectId - 1}`,
created: new Date().toLocaleTimeString().substring(0, 5),
collapsed: false,
newTaskName: '',
newTaskPriority: 'Medium',
tasks: []
}
this.projects.push(newProject)
console.log('Added project:', newProject)
},
addSampleData() {
const sampleProjects = [
{
id: this.nextProjectId++,
name: 'Mobile App Development',
description: 'Build responsive mobile application',
created: new Date().toLocaleTimeString().substring(0, 5),
collapsed: false,
newTaskName: '',
newTaskPriority: 'Medium',
tasks: [
{
id: this.nextTaskId++,
name: 'Design user interface',
priority: 'High',
completed: false,
created: new Date().toLocaleTimeString().substring(0, 5)
},
{
id: this.nextTaskId++,
name: 'Implement authentication',
priority: 'High',
completed: false,
created: new Date().toLocaleTimeString().substring(0, 5)
},
{
id: this.nextTaskId++,
name: 'Add push notifications',
priority: 'Medium',
completed: false,
created: new Date().toLocaleTimeString().substring(0, 5)
}
]
},
{
id: this.nextProjectId++,
name: 'Database Migration',
description: 'Migrate legacy database to new schema',
created: new Date().toLocaleTimeString().substring(0, 5),
collapsed: false,
newTaskName: '',
newTaskPriority: 'Medium',
tasks: [
{
id: this.nextTaskId++,
name: 'Backup existing data',
priority: 'High',
completed: true,
created: new Date().toLocaleTimeString().substring(0, 5)
},
{
id: this.nextTaskId++,
name: 'Create migration scripts',
priority: 'High',
completed: false,
created: new Date().toLocaleTimeString().substring(0, 5)
}
]
}
]
this.projects.push(...sampleProjects)
console.log('Added sample projects:', sampleProjects)
},
deleteProject(event, element, details) {
const projectIndex = details.index
const project = this.projects[projectIndex]
if (confirm(`Delete project "${project.name}" and all its tasks?`)) {
this.projects.splice(projectIndex, 1)
console.log('Deleted project:', project)
}
},
toggleCollapse(event, element, details) {
const projectIndex = details.index
const project = this.projects[projectIndex]
// Create new projects array with immutable update
const updatedProjects = [...this.projects]
updatedProjects[projectIndex] = {
...updatedProjects[projectIndex],
collapsed: !project.collapsed
}
this.projects = updatedProjects
console.log('Toggled collapse for project:', project.name)
},
addTask(event, element, details) {
const projectIndex = details.index
const newTask = {
id: this.nextTaskId++,
name: `Task ${this.nextTaskId - 1}`,
priority: 'Medium',
completed: false,
created: new Date().toLocaleTimeString().substring(0, 5)
}
// Create new projects array with immutable update
const updatedProjects = [...this.projects]
updatedProjects[projectIndex] = {
...updatedProjects[projectIndex],
tasks: [...updatedProjects[projectIndex].tasks, newTask]
}
this.projects = updatedProjects
console.log('Added task:', newTask)
},
addTaskForm(event, element, details) {
event.preventDefault()
const projectIndex = details.index
const project = this.projects[projectIndex]
const taskName = project.newTaskName?.trim() || '';
if (taskName) {
const newTask = {
id: this.nextTaskId++,
name: taskName,
priority: project.newTaskPriority,
completed: false,
created: new Date().toLocaleTimeString().substring(0, 5)
}
// Direct nested update: WF's deep reactive proxy handles this
const targetProject = this.projects[projectIndex];
targetProject.tasks = [...targetProject.tasks, newTask];
targetProject.newTaskName = '';
targetProject.newTaskPriority = 'Medium';
console.log('Added task via form:', newTask)
}
},
deleteTask(event, element, details) {
// Get indices from the framework's action context
const taskIndex = details.index
const projectIndex = details.parent.index
const project = this.projects[projectIndex]
const task = project.tasks[taskIndex]
if (confirm(`Delete task "${task.name}"?`)) {
// Create new projects array with immutable update
const updatedProjects = [...this.projects]
updatedProjects[projectIndex] = {
...updatedProjects[projectIndex],
tasks: updatedProjects[projectIndex].tasks.filter((_, index) => index !== taskIndex)
}
this.projects = updatedProjects
console.log('Deleted task:', task)
}
},
moveTaskUp(event, element, details) {
// Get indices from the framework's action context
const taskIndex = details.index
const projectIndex = details.parent.index
const project = this.projects[projectIndex]
if (taskIndex > 0) {
const task = project.tasks[taskIndex]
// Create new projects array with immutable update
const updatedProjects = [...this.projects]
const newTasks = [...project.tasks]
// Swap positions immutably
newTasks[taskIndex] = newTasks[taskIndex - 1]
newTasks[taskIndex - 1] = task
updatedProjects[projectIndex] = {
...updatedProjects[projectIndex],
tasks: newTasks
}
this.projects = updatedProjects
console.log('Moved task up:', task)
}
},
moveTaskDown(event, element, details) {
// Get indices from the framework's action context
const taskIndex = details.index
const projectIndex = details.parent.index
const project = this.projects[projectIndex]
if (taskIndex < project.tasks.length - 1) {
const task = project.tasks[taskIndex]
// Create new projects array with immutable update
const updatedProjects = [...this.projects]
const newTasks = [...project.tasks]
// Swap positions immutably
newTasks[taskIndex] = newTasks[taskIndex + 1]
newTasks[taskIndex + 1] = task
updatedProjects[projectIndex] = {
...updatedProjects[projectIndex],
tasks: newTasks
}
this.projects = updatedProjects
console.log('Moved task down:', task)
}
},
clearAll() {
if (confirm('Clear all projects and tasks?')) {
const projectCount = this.projects.length
const taskCount = this.totalTasks
this.projects = []
console.log(`Cleared ${projectCount} projects and ${taskCount} tasks`)
}
}
})
Components in Lists
When a component is rendered inside a data-list template, it can access the list item's data via this.listItem:
Template Setup
<div data-list="tasks" data-key="id">
<template>
<div data-component="task-card">...</div>
</template>
</div>
Component Definition
wildflower.component('task-card', {
state: {
taskId: null
},
beforeInit() {
// this.listItem contains the task object for this list item
if (this.listItem) {
this.taskId = this.listItem.id;
console.log('Task title:', this.listItem.title);
}
}
});
this.listItemis available inbeforeInit(),init(), and all lifecycle methods- It returns
nullfor components not rendered inside a list - It provides a live reference to the data object - changes to the source data are reflected
- For nested lists, it returns the immediate parent list's item, not an ancestor's
data-cloak inside list templates. Content inside <template> is inert and invisible until the framework renders it, so there is no flash-of-unstyled-content risk. Using data-cloak inside templates causes data-show toggling to silently fail on dynamically added list items. Use data-cloak only on top-level elements (modals, portals) outside of list templates.
Defensive Pattern
Components that may be used both inside and outside of lists can check for listItem:
beforeInit() {
// Works whether component is in a list or standalone
const data = this.listItem || this.element.dataset;
this.itemId = data.id || data.itemId;
}
Component Boundary Behavior
If a component is nested inside another component within a list, the inner component does not inherit the list item data:
<div data-list="items">
<template>
<div data-component="outer-comp">
<!-- outer-comp.listItem = item data -->
<div data-component="inner-comp">
<!-- inner-comp.listItem = null (blocked by component boundary) -->
</div>
</div>
</template>
</div>
This ensures components maintain clear ownership boundaries. If the inner component needs the data, the outer component should explicitly pass it via props or state.