Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/demo/e2e/devtools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ test.describe('DevTools', () => {
await page.goto('');
const errors = [];
page.on('pageerror', (error) => errors.push(error));
await page.getByRole('link', { name: 'DevTools' }).click();
await page.getByRole('link', { name: 'DevTools', exact: true }).click();
await expect(
page.getByRole('row', { name: 'Go for a walk' }),
).toBeVisible();
Expand All @@ -30,7 +30,7 @@ test.describe('DevTools', () => {
},
};
});
await page.getByRole('link', { name: 'DevTools' }).click();
await page.getByRole('link', { name: 'DevTools', exact: true }).click();
await page
.getByRole('row', { name: 'Go for a walk' })
.getByRole('checkbox')
Expand Down
1 change: 1 addition & 0 deletions apps/demo/src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<mat-drawer mode="side" opened>
<mat-nav-list>
<a mat-list-item routerLink="/todo">DevTools</a>
<a mat-list-item routerLink="/events-sample">Events + DevTools Sample</a>
<a mat-list-item routerLink="/flight-search">withRedux</a>
<a mat-list-item routerLink="/flight-search-data-service-simple"
>withDataService (Simple)</a
Expand Down
16 changes: 16 additions & 0 deletions apps/demo/src/app/events-sample/book-events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { type } from '@ngrx/signals';
import { eventGroup } from '@ngrx/signals/events';
import { Book } from './book.model';

export const bookEvents = eventGroup({
source: 'Book Store',
events: {
loadBooks: type<void>(),
bookSelected: type<{ bookId: string }>(),
selectionCleared: type<void>(),
filterUpdated: type<{ filter: string }>(),
stockToggled: type<{ bookId: string }>(),
bookAdded: type<{ book: Book }>(),
bookRemoved: type<{ bookId: string }>(),
},
});
51 changes: 51 additions & 0 deletions apps/demo/src/app/events-sample/book.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
export interface Book {
id: string;
title: string;
author: string;
year: number;
isbn: string;
inStock: boolean;
}

export const mockBooks: Book[] = [
{
id: '1',
title: 'The Great Gatsby',
author: 'F. Scott Fitzgerald',
year: 1925,
isbn: '978-0-7432-7356-5',
inStock: true,
},
{
id: '2',
title: '1984',
author: 'George Orwell',
year: 1949,
isbn: '978-0-452-28423-4',
inStock: true,
},
{
id: '3',
title: 'To Kill a Mockingbird',
author: 'Harper Lee',
year: 1960,
isbn: '978-0-06-112008-4',
inStock: false,
},
{
id: '4',
title: 'Pride and Prejudice',
author: 'Jane Austen',
year: 1813,
isbn: '978-0-14-143951-8',
inStock: true,
},
{
id: '5',
title: 'The Catcher in the Rye',
author: 'J.D. Salinger',
year: 1951,
isbn: '978-0-316-76948-0',
inStock: false,
},
];
81 changes: 81 additions & 0 deletions apps/demo/src/app/events-sample/book.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {
withDevtools,
withEventsTracking,
} from '@angular-architects/ngrx-toolkit';
import { signalStore, withComputed, withHooks, withState } from '@ngrx/signals';
import { injectDispatch, on, withReducer } from '@ngrx/signals/events';
import { bookEvents } from './book-events';
import { Book, mockBooks } from './book.model';

export const BookStore = signalStore(
{ providedIn: 'root' },
withDevtools('book-store-events', withEventsTracking()),
withState({
books: [] as Book[],
selectedBookId: null as string | null,
filter: '',
}),

withComputed((store) => ({
selectedBook: () => {
const id = store.selectedBookId();
return id ? store.books().find((b) => b.id === id) || null : null;
},

filteredBooks: () => {
const filter = store.filter().toLowerCase();
if (!filter) return store.books();

return store
.books()
.filter(
(book) =>
book.title.toLowerCase().includes(filter) ||
book.author.toLowerCase().includes(filter),
);
},

totalBooks: () => store.books().length,

availableBooks: () => store.books().filter((book) => book.inStock).length,
})),

withReducer(
on(bookEvents.loadBooks, () => ({
books: mockBooks,
})),

on(bookEvents.bookSelected, ({ payload }) => ({
selectedBookId: payload.bookId,
})),

on(bookEvents.selectionCleared, () => ({
selectedBookId: null,
})),

on(bookEvents.filterUpdated, ({ payload }) => ({
filter: payload.filter,
})),

on(bookEvents.stockToggled, (event, state) => ({
books: state.books.map((book) =>
book.id === event.payload.bookId
? { ...book, inStock: !book.inStock }
: book,
),
})),

on(bookEvents.bookAdded, (event, state) => ({
books: [...state.books, event.payload.book],
})),

on(bookEvents.bookRemoved, (event, state) => ({
books: state.books.filter((book) => book.id !== event.payload.bookId),
})),
),
withHooks({
onInit() {
injectDispatch(bookEvents).loadBooks();
},
}),
);
196 changes: 196 additions & 0 deletions apps/demo/src/app/events-sample/events-sample.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatToolbarModule } from '@angular/material/toolbar';
import { injectDispatch } from '@ngrx/signals/events';
import { bookEvents } from './book-events';
import { BookStore } from './book.store';

@Component({
selector: 'demo-events-sample',
standalone: true,
imports: [
CommonModule,
FormsModule,
MatCardModule,
MatButtonModule,
MatInputModule,
MatFormFieldModule,
MatIconModule,
MatChipsModule,
MatGridListModule,
MatToolbarModule,
],
template: `
<mat-toolbar color="primary">
<span>Book Store with Event Tracking</span>
</mat-toolbar>

<mat-card>
<mat-card-content>
<mat-form-field appearance="outline">
<mat-label>Search books</mat-label>
<input
matInput
[(ngModel)]="filterText"
(ngModelChange)="dispatch.filterUpdated({ filter: $event })"
placeholder="Filter by title or author..."
/>
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>

<button mat-raised-button color="primary" (click)="addRandomBook()">
<mat-icon>add</mat-icon> Add Book
</button>
<button mat-raised-button (click)="dispatch.selectionCleared()">
Clear Selection
</button>
</mat-card-content>
</mat-card>

<mat-card>
<mat-card-content>
<mat-chip-set>
<mat-chip>Total: {{ store.totalBooks() }}</mat-chip>
<mat-chip>In Stock: {{ store.availableBooks() }}</mat-chip>
<mat-chip>Filtered: {{ store.filteredBooks().length }}</mat-chip>
</mat-chip-set>
</mat-card-content>
</mat-card>

<mat-grid-list cols="3" rowHeight="350px" gutterSize="16">
@for (book of store.filteredBooks(); track book.id) {
<mat-grid-tile>
<mat-card
[style.border]="
store.selectedBook()?.id === book.id
? '2px solid #4caf50'
: 'none'
"
(click)="dispatch.bookSelected({ bookId: book.id })"
>
<mat-card-header>
<mat-card-title>{{ book.title }}</mat-card-title>
<mat-card-subtitle
>{{ book.author }} ({{ book.year }})</mat-card-subtitle
>
</mat-card-header>
<mat-card-content>
<p>ISBN: {{ book.isbn }}</p>
<mat-chip [color]="book.inStock ? 'primary' : 'warn'">
{{ book.inStock ? 'In Stock' : 'Out of Stock' }}
</mat-chip>
</mat-card-content>
<mat-card-actions>
<button mat-button (click)="toggleStock(book.id, $event)">
Toggle Stock
</button>
<button
mat-button
color="warn"
(click)="removeBook(book.id, $event)"
>
Remove
</button>
</mat-card-actions>
</mat-card>
</mat-grid-tile>
} @empty {
<mat-grid-tile [colspan]="3">
<p>
@if (store.filter()) {
No books found matching "{{ store.filter() }}"
} @else {
No books available
}
</p>
</mat-grid-tile>
}
</mat-grid-list>

@if (store.selectedBook(); as book) {
<mat-card>
<mat-card-header>
<mat-card-title>Selected: {{ book.title }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>Author: {{ book.author }}</p>
<p>Year: {{ book.year }}</p>
<p>ISBN: {{ book.isbn }}</p>
<p>Status: {{ book.inStock ? 'In Stock' : 'Out of Stock' }}</p>
</mat-card-content>
</mat-card>
}
`,
styles: [
`
mat-card {
margin: 16px;
}

mat-form-field {
margin-right: 16px;
}

button {
margin-right: 8px;
}

mat-grid-tile mat-card {
width: 100%;
cursor: pointer;
}
`,
],
})
export class EventsSampleComponent {
readonly store = inject(BookStore);
readonly dispatch = injectDispatch(bookEvents);
filterText = '';

toggleStock(bookId: string, event: Event) {
event.stopPropagation();
this.dispatch.stockToggled({ bookId });
}

removeBook(bookId: string, event: Event) {
event.stopPropagation();
this.dispatch.bookRemoved({ bookId });
}

addRandomBook() {
const titles = [
'The Hobbit',
'Brave New World',
'Fahrenheit 451',
'The Road',
'Dune',
];
const authors = [
'J.R.R. Tolkien',
'Aldous Huxley',
'Ray Bradbury',
'Cormac McCarthy',
'Frank Herbert',
];
const randomIndex = Math.floor(Math.random() * titles.length);

this.dispatch.bookAdded({
book: {
id: crypto.randomUUID(),
title: titles[randomIndex],
author: authors[randomIndex],
year: 1950 + Math.floor(Math.random() * 70),
isbn: `978-${Math.floor(Math.random() * 10)}-${Math.floor(Math.random() * 100000)}`,
inStock: Math.random() > 0.5,
},
});
}
}
2 changes: 2 additions & 0 deletions apps/demo/src/app/lazy-routes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Route } from '@angular/router';
import { TodoComponent } from './devtools/todo.component';
import { EventsSampleComponent } from './events-sample/events-sample.component';
import { FlightEditDynamicComponent } from './flight-search-data-service-dynamic/flight-edit.component';
import { FlightSearchDynamicComponent } from './flight-search-data-service-dynamic/flight-search.component';
import { FlightEditSimpleComponent } from './flight-search-data-service-simple/flight-edit-simple.component';
Expand All @@ -13,6 +14,7 @@ import { TodoStorageSyncComponent } from './todo-storage-sync/todo-storage-sync.

export const lazyRoutes: Route[] = [
{ path: 'todo', component: TodoComponent },
{ path: 'events-sample', component: EventsSampleComponent },
{ path: 'flight-search', component: FlightSearchComponent },
{
path: 'flight-search-data-service-simple',
Expand Down
1 change: 1 addition & 0 deletions libs/ngrx-toolkit/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { withDisabledNameIndices } from './lib/devtools/features/with-disabled-name-indicies';
export { withEventsTracking } from './lib/devtools/features/with-events-tracking';
export { withGlitchTracking } from './lib/devtools/features/with-glitch-tracking';
export { withMapper } from './lib/devtools/features/with-mapper';
export {
Expand Down
Loading