Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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