Advanced Lists

Advanced list patterns including filtering, sorting, external data sources, and performance optimization.

Prerequisites: This page covers advanced list techniques. For basics, see Lists & Iteration.

Filtered and Sorted Lists

Use computed properties to create filtered and sorted views of your data:

<div data-component="product-catalog">
    <div class="mb-3">
        <input type="text" data-model="search"
               class="form-control" placeholder="Search products...">
    </div>

    <div class="d-flex flex-wrap gap-2 mb-3">
        <select data-model="category" class="form-select" style="width: auto;">
            <option value="">All Categories</option>
            <option value="electronics">Electronics</option>
            <option value="clothing">Clothing</option>
            <option value="books">Books</option>
        </select>
        <select data-model="sortBy" class="form-select" style="width: auto;">
            <option value="name">Sort by Name</option>
            <option value="price">Sort by Price</option>
        </select>
        <span class="badge bg-primary align-self-center">
            <span data-bind="filtered.length"></span> of
            <span data-bind="products.length"></span>
        </span>
    </div>

    <ul class="list-group" data-list="filtered">
        <template>
            <li class="list-group-item d-flex justify-content-between align-items-center">
                <div>
                    <strong data-bind="name"></strong>
                    <span class="badge bg-secondary ms-2" data-bind="category"></span>
                </div>
                <div class="d-flex align-items-center gap-2">
                    <span data-bind="'$' + price"></span>
                    <button data-action="remove" class="btn btn-sm btn-danger">&times;</button>
                </div>
            </li>
        </template>
    </ul>

    <div data-show="filtered.length === 0" class="text-muted text-center p-3">
        No products match your filters.
    </div>
</div>
wildflower.component('product-catalog', {
    state: {
        search: '',
        category: '',
        sortBy: 'name',
        products: [
            { name: 'Headphones', category: 'electronics', price: 79 },
            { name: 'T-Shirt', category: 'clothing', price: 25 },
            { name: 'JavaScript Guide', category: 'books', price: 35 },
            { name: 'USB Cable', category: 'electronics', price: 12 },
            { name: 'Running Shoes', category: 'clothing', price: 89 },
            { name: 'Design Patterns', category: 'books', price: 42 }
        ]
    },

    computed: {
        filtered() {
            let items = [...this.products]

            if (this.search) {
                const term = this.search.toLowerCase()
                items = items.filter(p =>
                    p.name.toLowerCase().includes(term))
            }

            if (this.category) {
                items = items.filter(p =>
                    p.category === this.category)
            }

            items.sort((a, b) => this.sortBy === 'price'
                ? a.price - b.price
                : a.name.localeCompare(b.name))

            return items
        }
    },

    remove(e, el, detail) {
        const idx = this.products.indexOf(detail.item)
        if (idx !== -1) this.products.splice(idx, 1)
    }
})
Live Preview

Lists from External Data Sources

For simple store-backed lists, use the $ accessor directly in HTML:

<!-- Simplest: bind to store array directly -->
<div data-list="$notification-store.items" data-key="id">
    <template>
        <div class="notification">
            <strong data-bind="title"></strong>
            <p data-bind="message"></p>
        </div>
    </template>
</div>

For filtering or transformation, use subscribe with this.stores in computed properties:

Store-Based Lists

Access store arrays reactively using computed properties when you need to transform the data:

<!-- HTML: Render a list from a store -->
<div data-component="notification-list">
    <h3>Notifications (<span data-bind="unreadCount">0</span> unread)</h3>

    <div data-list="notifications">
        <template>
            <div class="notification">
                <strong data-bind="title"></strong>
                <p data-bind="message"></p>
                <small data-bind="timestamp"></small>
            </div>
        </template>
    </div>
</div>
// JavaScript: Component with subscribe accessing store data
wildflower.component('notification-list', {
    subscribe: {
        'notification-store': ['items']
    },

    computed: {
        // Access store array via this.stores - automatically reactive
        notifications() {
            return this.stores['notification-store'].items || []
        },

        // Derived computed from store data
        unreadCount() {
            return this.notifications.filter(n => !n.read).length
        }
    },

    markAsRead(event) {
        const store = wildflower.getStore('notification-store')
        const id = event.target.dataset.notificationId
        store.markAsRead(id)
    }
})

Cross-Component Lists

Render lists that depend on data from sibling components:

// A shopping cart component displaying items from a product store
wildflower.component('cart-summary', {
    subscribe: {
        'cart-store': ['selectedProducts']
    },

    computed: {
        // Get selected items from store
        cartItems() {
            return this.stores['cart-store'].selectedProducts || []
        },

        // Apply transformations to store data
        cartItemsWithTotals() {
            return this.cartItems.map(item => ({
                ...item,
                total: item.price * item.quantity
            }))
        },

        grandTotal() {
            return this.cartItemsWithTotals.reduce(
                (sum, item) => sum + item.total, 0
            )
        }
    }
})

Combined Local and Store Data

Filter or sort store data using local component state:

wildflower.component('filtered-user-list', {
    state: {
        searchTerm: '',
        showOnlineOnly: false,
        sortBy: 'name'
    },

    subscribe: {
        'user-store': ['users']
    },

    computed: {
        // Get raw data from store
        allUsers() {
            return this.stores['user-store'].users || []
        },

        // Apply local filters to store data
        filteredUsers() {
            let users = this.allUsers

            // Filter by search term
            if (this.searchTerm) {
                const term = this.searchTerm.toLowerCase()
                users = users.filter(u =>
                    u.name.toLowerCase().includes(term) ||
                    u.email.toLowerCase().includes(term)
                )
            }

            // Filter by online status
            if (this.showOnlineOnly) {
                users = users.filter(u => u.isOnline)
            }

            // Sort by selected field
            users.sort((a, b) => {
                if (a[this.sortBy] < b[this.sortBy]) return -1
                if (a[this.sortBy] > b[this.sortBy]) return 1
                return 0
            })

            return users
        }
    }
})
Key Points:
  • Automatic Reactivity: When the source data changes (store or component), the list automatically updates
  • Computed Chaining: Chain computed properties to build complex filtered/sorted views
  • Local State Control: Use local state for filters/sorting while keeping source data in stores
  • Always Return Arrays: Ensure your computed properties return arrays (use || [] fallback)
Mini Tower Defense (data-list)

A full game on push-based data-list. Entities are tracked by the reactive proxy, and every entity is a DOM element. The same game on data-pool sits beside it for comparison.