Skip to content

Aurelia List Rendering — Displaying Collections with Repeat

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Aurelia List Rendering. We cover key concepts, practical examples, and best practices to help you master this topic.

Aurelia's repeat.for template controller iterates over collections and renders a template for each item. It supports arrays, Maps, Sets, and iteration context variables like index, first, last, even, and odd for rich list rendering.

What You'll Learn

You will learn repeat.for syntax, context variables, nested repeats, performance optimization, sorting and filtering in repeat, and dynamic list manipulation.

Why It Matters

Lists are the most common UI pattern. Efficient list rendering with proper keys, minimal DOM updates, and clear empty states is essential for every application.

Real-World Use

A security logs viewer renders thousands of log entries using repeat.for with pagination, sorting, and filtering. Each row is efficiently updated without recreating the entire list.

flowchart LR
    A[Collection] --> B[repeat.for]
    B --> C[Template per item]
    C --> D[$index, $first]
    C --> E[$last, $even]
    B --> F[Empty state]
    B --> G[Performance]
    G --> H[Sorted]
    G --> I[Filtered]

Basic Repeat

<template>
  <!-- Simple list -->
  <ul>
    <li repeat.for="item of items">${item}</li>
  </ul>

  <!-- Object array -->
  <div repeat.for="user of users" class="user-card">
    <h3>${user.name}</h3>
    <p>${user.email}</p>
  </div>
</template>

Context Variables

<template>
  <ul>
    <li repeat.for="item of items; index = $index">
      <span class="index">${$index + 1}.</span>
      <span>${item.name}</span>
      <span if.bind="$first" class="badge">First</span>
      <span if.bind="$last" class="badge">Last</span>
      <span if.bind="$even" class="bg-light">Even</span>
      <span if.bind="$odd" class="bg-dark">Odd</span>
    </li>
  </ul>
</template>

Nested Repeats

<template>
  <div repeat.for="category of categories">
    <h2>${category.name}</h2>

    <ul>
      <li repeat.for="product of category.products">
        <!-- Access parent context -->
        ${category.name} > ${product.name} ($${product.price})
      </li>
    </ul>

    <!-- Access grandparent context via variable name -->
    <p if.bind="category.products.length === 0">
      No products in ${category.name}
    </p>
  </div>
</template>

Repeat with Objects and Maps

<template>
  <!-- Object iteration -->
  <table>
    <tr repeat.for="value of config | keys">
      <td>${value.key}</td>
      <td>${value.value}</td>
    </tr>
  </table>

  <!-- Map iteration -->
  <div repeat.for="entry of userMap">
    <strong>${entry.key}:</strong> ${entry.value.name}
  </div>
</template>
export class ConfigDemo {
  config = {
    theme: 'dark',
    language: 'en',
    timezone: 'UTC'
  };

  userMap = new Map([
    ['alice', { name: 'Alice' }],
    ['bob', { name: 'Bob' }]
  ]);
}

Sorting and Filtering Lists

<template>
  <!-- Sort in template (simple) -->
  <div repeat.for="user of users | sortBy:'name'">
    ${user.name}
  </div>

  <!-- Filter and sort -->
  <div repeat.for="user of users | filterBy:'isActive':true | sortBy:'createdAt'">
    ${user.name} — ${user.createdAt | dateFormat}
  </div>

  <!-- With search query -->
  <div repeat.for="user of filteredUsers">
    ${user.name}
  </div>
</template>
export class UserList {
  users = [];
  searchQuery = '';

  get filteredUsers() {
    if (!this.searchQuery) return this.users;
    const q = this.searchQuery.toLowerCase();
    return this.users.filter(u =>
      u.name.toLowerCase().includes(q) ||
      u.email.toLowerCase().includes(q)
    );
  }
}

Dynamic List Manipulation

export class ListManager {
  items = ['Item A', 'Item B', 'Item C'];
  nextId = 4;

  addItem() {
    this.items.push(`Item ${this.nextId++}`);
  }

  removeItem(index) {
    this.items.splice(index, 1);
  }

  insertItem(index) {
    this.items.splice(index, 0, `Inserted ${this.nextId++}`);
  }

  moveItem(fromIndex, toIndex) {
    const [item] = this.items.splice(fromIndex, 1);
    this.items.splice(toIndex, 0, item);
  }

  shuffle() {
    for (let i = this.items.length - 1; i > 0; i--) {
      const j = Math.floor(Math.random() * (i + 1));
      [this.items[i], this.items[j]] = [this.items[j], this.items[i]];
    }
  }

  clear() {
    this.items = [];
  }

  reset() {
    this.items = ['Item A', 'Item B', 'Item C'];
  }
}

Reorderable List with Drag and Drop

<template>
  <div class="sortable-list">
    <div repeat.for="item of items"
         draggable="true"
         dragstart.delegate="onDragStart($event, $index)"
         dragover.delegate="onDragOver($event, $index)"
         drop.delegate="onDrop($event, $index)"
         class="list-item ${$index === dragIndex ? 'dragging' : ''}">
      <span class="handle">&#9776;</span>
      ${item.name}
      <button click.delegate="removeItem($index)">X</button>
    </div>
  </div>
</template>

Pagination Pattern

<template>
  <div>
    <div repeat.for="item of pagedItems" class="item">
      ${item.name}
    </div>

    <div class="pagination" if.bind="totalPages > 1">
      <button click.delegate="goToPage(1)" disabled.bind="currentPage === 1">
        First
      </button>
      <button click.delegate="prevPage()" disabled.bind="currentPage === 1">
        Prev
      </button>

      <span repeat.for="page of pages" class="page">
        <button click.delegate="goToPage(page)"
                class="${page === currentPage ? 'active' : ''}">
          ${page}
        </button>
      </span>

      <button click.delegate="nextPage()" disabled.bind="currentPage === totalPages">
        Next
      </button>
      <button click.delegate="goToPage(totalPages)" disabled.bind="currentPage === totalPages">
        Last
      </button>
    </div>
  </div>
</template>

Performance Optimization

<template>
  <!-- Use track-by for stable identity -->
  <div repeat.for="item of items" track-by="$index">
    ${item.name}
  </div>

  <!-- Better: use a unique id -->
  <div repeat.for="item of items" track-by="id">
    ${item.name}
  </div>

  <!-- Virtual scrolling for large lists (requires plugin) -->
  <virtual-list items.bind="largeList" item-height="50">
    <template>
      <div class="virtual-item">${$item.name}</div>
    </template>
  </virtual-list>
</template>

Common Mistakes

  1. Not using track-by for stable identity. Without track-by, Aurelia replaces all DOM elements when the array changes. Use a unique identifier for stable identity.
  2. Mutating arrays by index assignment. this.items[0] = newItem does not trigger change detection. Use splice or create a new array.
  3. Forgetting $parent in nested repeats. Inner repeats cannot access outer repeat context directly. Use $parent or alias the outer item.
  4. Performing expensive operations in the template. Sorting and filtering in the template runs on every change detection cycle. Compute them in the ViewModel.
  5. Not handling empty state. A list with zero items should show a meaningful empty state. Always provide an if.bind for empty collections.

Practice Questions

  1. How do you access the current index in a repeat loop?
  2. What is track-by and why is it important?
  3. How do you filter a list before rendering?
  4. How do you access parent repeat context from a nested repeat?
  5. Challenge: Create a sortable, filterable, paginated data table with 1000 records. Implement column sorting by clicking headers. Add a search filter. Paginate with 20 items per page. Use track-by for efficient rendering. Add drag-and-drop row reordering.

FAQ

{{< faq "Can repeat.for iterate over objects?" "Yes. Use repeat.for=\"value of object | keys\" to iterate over object properties." >}}

What context variables are available in repeat?

$index, $first, $last, $even, $odd, $parent, $this.

How does Aurelia track array changes?

It observes push, pop, splice, shift, unshift, sort, and reverse. Direct index assignment is not observed.

Can I use repeat with async data?

Yes. When the bound array updates, the repeat re-renders automatically.

What is the performance limit for repeat?

500-1000 items render smoothly. For more, use virtual scrolling or pagination.

Mini Project

Build a file browser interface that displays files and folders in a table. Support sorting by name, size, date, and type. Support filtering by file type. Support multi-select with checkboxes. Support delete and rename actions. Show file size formatted with a converter. Show last modified with a time-ago converter. Show an empty state when no files match the filter.

What's Next

Now that you understand list rendering, learn Aurelia Composition for dynamic component loading. Then explore Aurelia Dependency Injection for service management.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro