> For the complete documentation index, see [llms.txt](https://docs.aurelia.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.aurelia.io/getting-started/extended-tutorial/step-3-projects-overview.md).

# Step 3: Overview page + filters + events

In this step you will add real data, reusable components, query param syncing, and deep child-to-parent communication with the Event Aggregator.

## 1. Define shared models and data

Create `src/models.ts`:

```typescript
export type Task = {
  id: string;
  title: string;
  done: boolean;
};

export type Project = {
  id: string;
  name: string;
  tasks: Task[];
};
```

Create `src/project-data.ts`:

```typescript
import { Project } from './models';

export const PROJECTS: Project[] = [
  {
    id: 'alpha',
    name: 'Onboarding',
    tasks: [
      { id: 'alpha-1', title: 'Create welcome pack', done: false },
      { id: 'alpha-2', title: 'Schedule kickoff', done: false }
    ]
  },
  {
    id: 'beta',
    name: 'Release prep',
    tasks: [
      { id: 'beta-1', title: 'Finalize changelog', done: true },
      { id: 'beta-2', title: 'QA smoke test', done: false }
    ]
  }
];
```

## 2. Build reusable components

Create `src/components/project-card.ts`:

```typescript
import { bindable } from 'aurelia';
import { Project } from '../models';

export class ProjectCard {
  @bindable project!: Project;
  @bindable onRemove?: (project: Project) => void;

  remove(): void {
    this.onRemove?.(this.project);
  }
}
```

Create `src/components/project-card.html`:

```html
<import from="./task-list"></import>

<section class="project-card">
  <header class="project-card__header">
    <h3>${project.name}</h3>
    <span class="project-card__count">
      ${project.tasks.length} tasks
    </span>
  </header>

  <task-list tasks.bind="project.tasks" project-id.bind="project.id"></task-list>

  <footer class="project-card__footer">
    <a load="route: ../project-detail; params.bind: { id: project.id }">
      Open project
    </a>
    <button class="project-card__remove" click.trigger="remove()">
      Remove
    </button>
  </footer>
</section>
```

The `route:` instruction uses a route **id**. Because this link lives inside the Overview child route, we prefix with `../` so the router resolves the id from the parent (Projects) route context.

Create `src/components/task-list.ts`:

```typescript
import { bindable } from 'aurelia';
import { Task } from '../models';

export class TaskList {
  @bindable tasks: Task[] = [];
  @bindable projectId = '';
}
```

Create `src/components/task-list.html`:

```html
<import from="./task-item"></import>

<ul class="task-list">
  <li repeat.for="task of tasks">
    <task-item task.bind="task" project-id.bind="projectId"></task-item>
  </li>
</ul>
```

Create `src/components/task-item.ts`:

```typescript
import { IEventAggregator, resolve } from '@aurelia/kernel';
import { bindable } from 'aurelia';
import { Task } from '../models';

export class TaskItem {
  @bindable task!: Task;
  @bindable projectId = '';

  private readonly ea = resolve(IEventAggregator);

  notifyToggle(): void {
    this.ea.publish('task:toggled', {
      projectId: this.projectId,
      taskId: this.task.id,
      done: this.task.done
    });
  }
}
```

Create `src/components/task-item.html`:

```html
<label class="task-item">
  <input type="checkbox" checked.bind="task.done" change.trigger="notifyToggle()" />
  <span class.bind="task.done ? 'task-item__done' : ''">
    ${task.title}
  </span>
</label>
```

## 3. Replace the Overview page with real behavior

Update `src/pages/projects-overview-page.ts`:

```typescript
import { IEventAggregator, IDisposable, resolve } from '@aurelia/kernel';
import { IRouter, IRouteViewModel, Params, RouteNode } from '@aurelia/router';
import { observable } from 'aurelia';
import { Project } from '../models';
import { PROJECTS } from '../project-data';

export class ProjectsOverviewPage implements IRouteViewModel {
  @observable searchQuery = '';

  projects: Project[] = structuredClone(PROJECTS);
  filteredProjects: Project[] = this.projects;
  recentActivity: string[] = [];
  newProjectName = '';

  private readonly ea = resolve(IEventAggregator);
  private readonly router = resolve(IRouter);
  private subscription?: IDisposable;

  loading(_params: Params, next: RouteNode): void {
    const query = next.queryParams.get('q');
    this.searchQuery = query ?? '';
    this.applyFilter();
  }

  bound(): void {
    this.subscription = this.ea.subscribe('task:toggled', ({ projectId, taskId, done }) => {
      const project = this.projects.find(item => item.id === projectId);
      const task = project?.tasks.find(item => item.id === taskId);

      if (!project || !task) return;

      const status = done ? 'completed' : 'reopened';
      this.recentActivity.unshift(`${project.name}: ${task.title} ${status}`);
      this.recentActivity = this.recentActivity.slice(0, 5);
    });
  }

  unbinding(): void {
    this.subscription?.dispose();
  }

  searchQueryChanged(): void {
    this.applyFilter();
  }

  clearSearch(): void {
    this.searchQuery = '';
    this.applyFilter();
    this.syncQueryToUrl();
  }

  syncQueryToUrl(): void {
    void this.router.load('overview', {
      context: this,
      queryParams: this.searchQuery ? { q: this.searchQuery } : {}
    });
  }

  handleNewProjectKeydown(event: KeyboardEvent): void {
    if (event.key === 'Enter') {
      this.addProject();
    }
  }

  addProject(): void {
    const name = this.newProjectName.trim();
    if (!name) return;

    const id = name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
    this.projects = [
      ...this.projects,
      { id, name, tasks: [] }
    ];
    this.newProjectName = '';
    this.applyFilter();
  }

  removeProject(project: Project): void {
    this.projects = this.projects.filter(item => item !== project);
    this.applyFilter();
  }

  private applyFilter(): void {
    const term = this.searchQuery.trim().toLowerCase();
    this.filteredProjects = term
      ? this.projects.filter(project => project.name.toLowerCase().includes(term))
      : this.projects;
  }
}
```

Update `src/pages/projects-overview-page.html`:

```html
<import from="../components/project-card"></import>

<section class="toolbar">
  <input
    value.bind="searchQuery"
    placeholder="Search projects" />
  <button if.bind="searchQuery" click.trigger="clearSearch()">
    Clear
  </button>
  <button if.bind="searchQuery" click.trigger="syncQueryToUrl()">
    Share Filter
  </button>
</section>

<section class="project-create">
  <input
    value.bind="newProjectName"
    placeholder="New project name"
    keydown.trigger="handleNewProjectKeydown($event)" />
  <button disabled.bind="!newProjectName.trim()" click.trigger="addProject()">
    Add project
  </button>
</section>

<div class="project-grid">
  <project-card
    repeat.for="project of filteredProjects"
    project.bind="project"
    on-remove.bind="(project) => removeProject(project)">
  </project-card>
</div>

<aside class="activity" if.bind="recentActivity.length">
  <h2>Recent activity</h2>
  <ul>
    <li repeat.for="entry of recentActivity">${entry}</li>
  </ul>
</aside>
```

The `loading` hook reads the query params from `next.queryParams`. The Share Filter button uses `IRouter.load()` to update the URL.

## 4. Replace the Activity page with real data

Update `src/pages/projects-activity-page.ts`:

```typescript
import { PROJECTS } from '../project-data';

export class ProjectsActivityPage {
  totalProjects = PROJECTS.length;

  get totalTasks(): number {
    return PROJECTS.reduce((total, project) => total + project.tasks.length, 0);
  }

  get completedTasks(): number {
    return PROJECTS.reduce(
      (total, project) => total + project.tasks.filter(task => task.done).length,
      0
    );
  }
}
```

Update `src/pages/projects-activity-page.html`:

```html
<section class="activity-summary">
  <h2>Activity Summary</h2>
  <p>Total projects: ${totalProjects}</p>
  <p>Tasks completed: ${completedTasks} / ${totalTasks}</p>
</section>
```

Next step: [Step 4: Detail route + guards](/getting-started/extended-tutorial/step-4-project-detail-and-guards.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.aurelia.io/getting-started/extended-tutorial/step-3-projects-overview.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
