diff --git a/projects/www/src/app/examples/testing-store/src/app/book-collection/book-collection.component.ts b/projects/www/src/app/examples/testing-store/src/app/book-collection/book-collection.component.ts
new file mode 100644
index 0000000000..98903e75ef
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/book-collection/book-collection.component.ts
@@ -0,0 +1,12 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Book } from '../book-list/books.model';
+
+@Component({
+ selector: 'app-book-collection',
+ templateUrl: './book-collection.component.html',
+ styleUrls: ['./book-collection.component.css'],
+})
+export class BookCollectionComponent {
+ @Input() books: Array;
+ @Output() remove = new EventEmitter();
+}
diff --git a/projects/www/src/app/examples/testing-store/src/app/book-list/book-list.component.css b/projects/www/src/app/examples/testing-store/src/app/book-list/book-list.component.css
new file mode 100644
index 0000000000..374d31d7be
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/book-list/book-list.component.css
@@ -0,0 +1,11 @@
+div {
+ padding: 10px;
+}
+span {
+ margin: 0 10px 0 2px;
+}
+p {
+ display: inline-block;
+ font-style: italic;
+ margin: 0;
+}
\ No newline at end of file
diff --git a/projects/www/src/app/examples/testing-store/src/app/book-list/book-list.component.html b/projects/www/src/app/examples/testing-store/src/app/book-list/book-list.component.html
new file mode 100644
index 0000000000..b3fb0e12b0
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/book-list/book-list.component.html
@@ -0,0 +1,10 @@
+
+
{{book.volumeInfo.title}}
by {{book.volumeInfo.authors}}
+
+
\ No newline at end of file
diff --git a/projects/www/src/app/examples/testing-store/src/app/book-list/book-list.component.ts b/projects/www/src/app/examples/testing-store/src/app/book-list/book-list.component.ts
new file mode 100644
index 0000000000..7dbcc6cbe2
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/book-list/book-list.component.ts
@@ -0,0 +1,12 @@
+import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Book } from './books.model';
+
+@Component({
+ selector: 'app-book-list',
+ templateUrl: './book-list.component.html',
+ styleUrls: ['./book-list.component.css'],
+})
+export class BookListComponent {
+ @Input() books: Array;
+ @Output() add = new EventEmitter();
+}
diff --git a/projects/www/src/app/examples/testing-store/src/app/book-list/books.model.ts b/projects/www/src/app/examples/testing-store/src/app/book-list/books.model.ts
new file mode 100644
index 0000000000..7d3a12a440
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/book-list/books.model.ts
@@ -0,0 +1,7 @@
+export interface Book {
+ id: string;
+ volumeInfo: {
+ title: string;
+ authors: Array;
+ };
+}
diff --git a/projects/www/src/app/examples/testing-store/src/app/book-list/books.service.ts b/projects/www/src/app/examples/testing-store/src/app/book-list/books.service.ts
new file mode 100644
index 0000000000..7e59ee4434
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/book-list/books.service.ts
@@ -0,0 +1,19 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+import { Book } from './books.model';
+
+@Injectable({ providedIn: 'root' })
+export class GoogleBooksService {
+ constructor(private http: HttpClient) {}
+
+ getBooks(): Observable> {
+ return this.http
+ .get<{ items: Book[] }>(
+ 'https://www.googleapis.com/books/v1/volumes?maxResults=5&orderBy=relevance&q=oliver%20sacks'
+ )
+ .pipe(map((books) => books.items || []));
+ }
+}
diff --git a/projects/www/src/app/examples/testing-store/src/app/integration.spec.ts b/projects/www/src/app/examples/testing-store/src/app/integration.spec.ts
new file mode 100644
index 0000000000..b320fc5577
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/integration.spec.ts
@@ -0,0 +1,111 @@
+import { TestBed, ComponentFixture, waitForAsync } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { StoreModule } from '@ngrx/store';
+import {
+ HttpClientTestingModule,
+ HttpTestingController,
+} from '@angular/common/http/testing';
+
+import { BookListComponent } from './book-list/book-list.component';
+import { GoogleBooksService } from './book-list/books.service';
+import { BookCollectionComponent } from './book-collection/book-collection.component';
+import { AppComponent } from './app.component';
+import { collectionReducer } from './state/collection.reducer';
+import { booksReducer } from './state/books.reducer';
+
+describe('AppComponent Integration Test', () => {
+ let component: AppComponent;
+ let fixture: ComponentFixture;
+ let httpMock: HttpTestingController;
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [AppComponent, BookListComponent, BookCollectionComponent],
+ imports: [
+ HttpClientTestingModule,
+ StoreModule.forRoot({
+ books: booksReducer,
+ collection: collectionReducer,
+ }),
+ ],
+ providers: [GoogleBooksService],
+ }).compileComponents();
+
+ httpMock = TestBed.inject(HttpTestingController);
+
+ fixture = TestBed.createComponent(AppComponent);
+ component = fixture.debugElement.componentInstance;
+
+ fixture.detectChanges();
+
+ const req = httpMock.expectOne(
+ 'https://www.googleapis.com/books/v1/volumes?maxResults=5&orderBy=relevance&q=oliver%20sacks'
+ );
+ req.flush({
+ items: [
+ {
+ id: 'firstId',
+ volumeInfo: {
+ title: 'First Title',
+ authors: ['First Author'],
+ },
+ },
+ {
+ id: 'secondId',
+ volumeInfo: {
+ title: 'Second Title',
+ authors: ['Second Author'],
+ },
+ },
+ ],
+ });
+
+ fixture.detectChanges();
+ }));
+
+ afterEach(() => {
+ httpMock.verify();
+ });
+
+ it('should create the component', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('buttons should work as expected', () => {
+ it('should add to collection when add button is clicked and remove from collection when remove button is clicked', () => {
+ const addButton = getBookList()[1].query(
+ By.css('[data-test=add-button]')
+ );
+
+ click(addButton);
+
+ expect(getBookTitle(getCollection()[0])).toBe('Second Title');
+
+ const removeButton = getCollection()[0].query(
+ By.css('[data-test=remove-button]')
+ );
+
+ click(removeButton);
+
+ expect(getCollection().length).toBe(0);
+ });
+ });
+
+ function getCollection() {
+ return fixture.debugElement.queryAll(By.css('.book-collection .book-item'));
+ }
+
+ function getBookList() {
+ return fixture.debugElement.queryAll(By.css('.book-list .book-item'));
+ }
+
+ function getBookTitle(element) {
+ return element.query(By.css('p')).nativeElement.textContent;
+ }
+
+ function click(element) {
+ const el: HTMLElement = element.nativeElement;
+ el.click();
+ fixture.detectChanges();
+ }
+});
diff --git a/projects/www/src/app/examples/testing-store/src/app/reducers/auth.reducer.ts b/projects/www/src/app/examples/testing-store/src/app/reducers/auth.reducer.ts
new file mode 100644
index 0000000000..a67e7baee8
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/reducers/auth.reducer.ts
@@ -0,0 +1,18 @@
+import { createReducer, on } from '@ngrx/store';
+import { AuthActions } from '../actions';
+
+export interface State {
+ username: string;
+}
+
+export const initialState: State = {
+ username: '',
+};
+
+export const reducer = createReducer(
+ initialState,
+ on(AuthActions.login, ({ username }): State => ({ username })),
+ on(AuthActions.logout, (): State => ({ username: initialState.username }))
+);
+
+export const getUsername = (state: State) => state.username;
diff --git a/projects/www/src/app/examples/testing-store/src/app/reducers/index.ts b/projects/www/src/app/examples/testing-store/src/app/reducers/index.ts
new file mode 100644
index 0000000000..e406569b10
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/reducers/index.ts
@@ -0,0 +1,22 @@
+import { createSelector, createFeatureSelector } from '@ngrx/store';
+import * as fromAuth from './auth.reducer';
+
+export interface AuthState {
+ status: fromAuth.State;
+}
+
+export interface State {
+ auth: AuthState;
+}
+
+export const selectAuthState = createFeatureSelector('auth');
+
+export const selectAuthStatusState = createSelector(
+ selectAuthState,
+ (state: AuthState) => state.status
+);
+
+export const getUsername = createSelector(
+ selectAuthStatusState,
+ fromAuth.getUsername
+);
diff --git a/projects/www/src/app/examples/testing-store/src/app/state/app.state.ts b/projects/www/src/app/examples/testing-store/src/app/state/app.state.ts
new file mode 100644
index 0000000000..b33494b040
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/state/app.state.ts
@@ -0,0 +1,6 @@
+import { Book } from '../book-list/books.model';
+
+export interface AppState {
+ books: ReadonlyArray;
+ collection: ReadonlyArray;
+}
diff --git a/projects/www/src/app/examples/testing-store/src/app/state/books.actions.ts b/projects/www/src/app/examples/testing-store/src/app/state/books.actions.ts
new file mode 100644
index 0000000000..015f097b31
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/state/books.actions.ts
@@ -0,0 +1,16 @@
+import { createAction, props } from '@ngrx/store';
+
+export const addBook = createAction(
+ '[Book List] Add Book',
+ props<{ bookId }>()
+);
+
+export const removeBook = createAction(
+ '[Book Collection] Remove Book',
+ props<{ bookId }>()
+);
+
+export const retrievedBookList = createAction(
+ '[Book List/API] Retrieve Books Success',
+ props<{ Book }>()
+);
diff --git a/projects/www/src/app/examples/testing-store/src/app/state/books.reducer.spec.ts b/projects/www/src/app/examples/testing-store/src/app/state/books.reducer.spec.ts
new file mode 100644
index 0000000000..0a7b7e0036
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/state/books.reducer.spec.ts
@@ -0,0 +1,37 @@
+import * as fromReducer from './books.reducer';
+import { retrievedBookList } from './books.actions';
+import { Book } from '../book-list/books.model';
+
+describe('BooksReducer', () => {
+ describe('unknown action', () => {
+ it('should return the default state', () => {
+ const { initialState } = fromReducer;
+ const action = {
+ type: 'Unknown',
+ };
+ const state = fromReducer.booksReducer(initialState, action);
+
+ expect(state).toBe(initialState);
+ });
+ });
+
+ describe('retrievedBookList action', () => {
+ it('should retrieve all books and update the state in an immutable way', () => {
+ const { initialState } = fromReducer;
+ const newState: Array = [
+ {
+ id: 'firstId',
+ volumeInfo: {
+ title: 'First Title',
+ authors: ['First Author'],
+ },
+ },
+ ];
+ const action = retrievedBookList({ Book: newState });
+ const state = fromReducer.booksReducer(initialState, action);
+
+ expect(state).toEqual(newState);
+ expect(state).not.toBe(initialState);
+ });
+ });
+});
diff --git a/projects/www/src/app/examples/testing-store/src/app/state/books.reducer.ts b/projects/www/src/app/examples/testing-store/src/app/state/books.reducer.ts
new file mode 100644
index 0000000000..e835d5df09
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/state/books.reducer.ts
@@ -0,0 +1,11 @@
+import { createReducer, on, Action } from '@ngrx/store';
+
+import { retrievedBookList } from './books.actions';
+import { Book } from '../book-list/books.model';
+
+export const initialState: ReadonlyArray = [];
+
+export const booksReducer = createReducer(
+ initialState,
+ on(retrievedBookList, (state, { Book }) => [...Book])
+);
diff --git a/projects/www/src/app/examples/testing-store/src/app/state/books.selectors.spec.ts b/projects/www/src/app/examples/testing-store/src/app/state/books.selectors.spec.ts
new file mode 100644
index 0000000000..f2e6cd6fb7
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/state/books.selectors.spec.ts
@@ -0,0 +1,39 @@
+import { selectBooks, selectBookCollection } from './books.selectors';
+import { AppState } from './app.state';
+
+describe('Selectors', () => {
+ const initialState: AppState = {
+ books: [
+ {
+ id: 'firstId',
+ volumeInfo: {
+ title: 'First Title',
+ authors: ['First Author'],
+ },
+ },
+ {
+ id: 'secondId',
+ volumeInfo: {
+ title: 'Second Title',
+ authors: ['Second Author'],
+ },
+ },
+ ],
+ collection: ['firstId'],
+ };
+
+ it('should select the book list', () => {
+ const result = selectBooks.projector(initialState.books);
+ expect(result.length).toEqual(2);
+ expect(result[1].id).toEqual('secondId');
+ });
+
+ it('should select the book collection', () => {
+ const result = selectBookCollection.projector(
+ initialState.books,
+ initialState.collection
+ );
+ expect(result.length).toEqual(1);
+ expect(result[0].id).toEqual('firstId');
+ });
+});
diff --git a/projects/www/src/app/examples/testing-store/src/app/state/books.selectors.ts b/projects/www/src/app/examples/testing-store/src/app/state/books.selectors.ts
new file mode 100644
index 0000000000..834850f209
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/state/books.selectors.ts
@@ -0,0 +1,15 @@
+import { createSelector, createFeatureSelector } from '@ngrx/store';
+import { Book } from '../book-list/books.model';
+
+export const selectBooks = createFeatureSelector>('books');
+
+export const selectCollectionState =
+ createFeatureSelector>('collection');
+
+export const selectBookCollection = createSelector(
+ selectBooks,
+ selectCollectionState,
+ (books, collection) => {
+ return collection.map((id) => books.find((book) => book.id === id)!);
+ }
+);
diff --git a/projects/www/src/app/examples/testing-store/src/app/state/collection.reducer.spec.ts b/projects/www/src/app/examples/testing-store/src/app/state/collection.reducer.spec.ts
new file mode 100644
index 0000000000..ccf2097f96
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/state/collection.reducer.spec.ts
@@ -0,0 +1,47 @@
+import * as fromReducer from './collection.reducer';
+import { addBook, removeBook } from './books.actions';
+
+describe('CollectionReducer', () => {
+ describe('unknown action', () => {
+ it('should return the previous state', () => {
+ const { initialState } = fromReducer;
+ const action = {
+ type: 'Unknown',
+ };
+ const state = fromReducer.collectionReducer(initialState, action);
+
+ expect(state).toBe(initialState);
+ });
+ });
+
+ describe('add action', () => {
+ it('should add an item from the book list and update the state in an immutable way', () => {
+ const initialState: Array = ['firstId', 'secondId'];
+
+ const action = addBook({ bookId: 'thirdId' });
+ const state = fromReducer.collectionReducer(initialState, action);
+
+ expect(state[2]).toBe('thirdId');
+ });
+
+ it('should not add a bookId to collection when that bookId is already in the collection', () => {
+ const initialState: Array = ['firstId', 'secondId'];
+
+ const action = addBook({ bookId: 'secondId' });
+ const state = fromReducer.collectionReducer(initialState, action);
+
+ expect(state[2]).toEqual(undefined);
+ expect(state[1]).toBe('secondId');
+ });
+ });
+
+ describe('remove action', () => {
+ it('should remove the selected book from the collection update the state in an immutable way', () => {
+ const initialState: Array = ['firstId', 'secondId'];
+ const action = removeBook({ bookId: 'secondId' });
+ const state = fromReducer.collectionReducer(initialState, action);
+
+ expect(state[1]).toEqual(undefined);
+ });
+ });
+});
diff --git a/projects/www/src/app/examples/testing-store/src/app/state/collection.reducer.ts b/projects/www/src/app/examples/testing-store/src/app/state/collection.reducer.ts
new file mode 100644
index 0000000000..25bcf4585e
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/state/collection.reducer.ts
@@ -0,0 +1,14 @@
+import { createReducer, on, Action } from '@ngrx/store';
+import { addBook, removeBook } from './books.actions';
+
+export const initialState: ReadonlyArray = [];
+
+export const collectionReducer = createReducer(
+ initialState,
+ on(removeBook, (state, { bookId }) => state.filter((id) => id !== bookId)),
+ on(addBook, (state, { bookId }) => {
+ if (state.indexOf(bookId) > -1) return state;
+
+ return [...state, bookId];
+ })
+);
diff --git a/projects/www/src/app/examples/testing-store/src/app/user-greeting.component.spec.ts b/projects/www/src/app/examples/testing-store/src/app/user-greeting.component.spec.ts
new file mode 100644
index 0000000000..7df8ca52cb
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/user-greeting.component.spec.ts
@@ -0,0 +1,40 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { MemoizedSelector } from '@ngrx/store';
+import { provideMockStore, MockStore } from '@ngrx/store/testing';
+import { UserGreetingComponent } from './user-greeting.component';
+import * as fromAuth from './reducers';
+
+describe('User Greeting Component', () => {
+ let fixture: ComponentFixture;
+ let mockStore: MockStore;
+ let mockUsernameSelector: MemoizedSelector;
+ const queryDivText = () =>
+ fixture.debugElement.queryAll(By.css('div'))[0].nativeElement.textContent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ providers: [provideMockStore()],
+ declarations: [UserGreetingComponent],
+ });
+
+ fixture = TestBed.createComponent(UserGreetingComponent);
+ mockStore = TestBed.inject(MockStore);
+ mockUsernameSelector = mockStore.overrideSelector(
+ fromAuth.getUsername,
+ 'John'
+ );
+ fixture.detectChanges();
+ });
+
+ it('should greet John when the username is John', () => {
+ expect(queryDivText()).toBe('Greetings, John!');
+ });
+
+ it('should greet Brandon when the username is Brandon', () => {
+ mockUsernameSelector.setResult('Brandon');
+ mockStore.refreshState();
+ fixture.detectChanges();
+ expect(queryDivText()).toBe('Greetings, Brandon!');
+ });
+});
diff --git a/projects/www/src/app/examples/testing-store/src/app/user-greeting.component.ts b/projects/www/src/app/examples/testing-store/src/app/user-greeting.component.ts
new file mode 100644
index 0000000000..3ca6d0ab4b
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/app/user-greeting.component.ts
@@ -0,0 +1,13 @@
+import { Component } from '@angular/core';
+import { Store } from '@ngrx/store';
+import * as fromAuth from './reducers';
+
+@Component({
+ selector: 'user-greeting',
+ template: `
Greetings, {{ username$ | async }}!
`,
+})
+export class UserGreetingComponent {
+ username$ = this.store.select(fromAuth.getUsername);
+
+ constructor(private store: Store) {}
+}
diff --git a/projects/www/src/app/examples/testing-store/src/index.html b/projects/www/src/app/examples/testing-store/src/index.html
new file mode 100644
index 0000000000..f902fba22d
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+ NgRx Tutorial
+
+
+
+
+
+
+
+
diff --git a/projects/www/src/app/examples/testing-store/src/main-test.ts b/projects/www/src/app/examples/testing-store/src/main-test.ts
new file mode 100644
index 0000000000..2e578fde01
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/main-test.ts
@@ -0,0 +1,19 @@
+import './polyfills';
+
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+
+platformBrowserDynamic()
+ .bootstrapModule(AppModule)
+ .then((ref) => {
+ // Ensure Angular destroys itself on hot reloads.
+ if (window['ngRef']) {
+ window['ngRef'].destroy();
+ }
+ window['ngRef'] = ref;
+
+ // Otherwise, log the boot error
+ })
+ .catch((err) => console.error(err));
diff --git a/projects/www/src/app/examples/testing-store/src/main.ts b/projects/www/src/app/examples/testing-store/src/main.ts
new file mode 100644
index 0000000000..27d21e9dac
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/main.ts
@@ -0,0 +1,12 @@
+// main app entry point
+import { enableProdMode } from '@angular/core';
+import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+
+import { AppModule } from './app/app.module';
+import { environment } from './environments/environment';
+
+if (environment.production) {
+ enableProdMode();
+}
+
+platformBrowserDynamic().bootstrapModule(AppModule);
diff --git a/projects/www/src/app/examples/testing-store/src/styles.css b/projects/www/src/app/examples/testing-store/src/styles.css
new file mode 100644
index 0000000000..eb1cca00b7
--- /dev/null
+++ b/projects/www/src/app/examples/testing-store/src/styles.css
@@ -0,0 +1,4 @@
+/* Master Styles */
+* {
+ font-family: Arial, Helvetica, sans-serif;
+}
diff --git a/projects/www/src/app/pages/guide/component-store/usage.md b/projects/www/src/app/pages/guide/component-store/usage.md
index 03f13c2353..66b0e024f8 100644
--- a/projects/www/src/app/pages/guide/component-store/usage.md
+++ b/projects/www/src/app/pages/guide/component-store/usage.md
@@ -82,7 +82,7 @@ You can see the full example at StackBlitz:
+
@@ -104,12 +104,6 @@ First, the state for the component needs to be identified. In `SlideToggleCompon
path="component-store-slide-toggle/src/app/slide-toggle.component.ts"
region="state">
-```ts
-export interface SlideToggleState {
- checked: boolean;
-}
-```
-
Then we need to provide `ComponentStore` in the component's providers, so that each new instance of `SlideToggleComponent` has its own `ComponentStore`. It also has to be injected into the constructor.
@@ -120,17 +114,11 @@ In this example `ComponentStore` is provided directly in the component. This wor
-
-```ts
-@Component({
- selector: 'mat-slide-toggle',
- templateUrl: 'slide-toggle.html',
-```
-
Next, the default state for the component needs to be set. It could be done lazily, however it needs to be done before any of `updater`s are executed, because they rely on the state to be present and would throw an error if the state is not initialized by the time they are invoked.
@@ -152,17 +140,6 @@ When it is called with a callback, the state is updated.
path="component-store-slide-toggle/src/app/slide-toggle.component.ts"
region="init">
-```ts
-constructor(
- private readonly componentStore: ComponentStore
-) {
- // set defaults
- this.componentStore.setState({
- checked: false,
- });
-}
-```
-
#### Step 2. Updating state
@@ -173,17 +150,11 @@ In the slide-toggle example, the state is updated either through `@Input` or by
When a user clicks the toggle (triggering a 'change' event), instead of calling the same updater directly, the `onChangeEvent` effect is called. This is done because we also need to have the side-effect of `event.source.stopPropagation` to prevent this event from bubbling up (slide-toggle output event in named 'change' as well) and only after that the `setChecked` updater is called with the value of the input element.
-
-```ts
-@Input() set checked(value: boolean) {
- this.setChecked(value);
- }
-```
-
#### Step 3. Reading the state
@@ -198,14 +169,6 @@ Finally, the state is aggregated with selectors into two properties:
path="component-store-slide-toggle/src/app/slide-toggle.component.ts"
region="selector">
-```ts
-// Observable used instead of EventEmitter
- @Output() readonly change = this.componentStore.select((state) => ({
- source: this,
- checked: state.checked,
- }));
-```
-
This example does not have a lot of business logic, however it is still fully reactive.
@@ -239,7 +202,7 @@ You can see the examples at StackBlitz:
-
+
@@ -263,24 +226,6 @@ With `ComponentStore` extracted into `PaginatorStore`, the developer is now usin
path="component-store-paginator-service/src/app/paginator.component.ts"
region="inputs">
-```ts
-@Input() set pageIndex(value: string | number) {
- this.paginatorStore.setPageIndex(value);
- }
-
- @Input() set length(value: string | number) {
- this.paginatorStore.setLength(value);
- }
-
- @Input() set pageSize(value: string | number) {
- this.paginatorStore.setPageSize(value);
- }
-
- @Input() set pageSizeOptions(value: readonly number[]) {
- this.paginatorStore.setPageSizeOptions(value);
- }
-```
-
Not all `updater`s have to be called in the `@Input`. For example, `changePageSize` is called from the template.
@@ -292,24 +237,6 @@ Effects are used to perform additional validation and get extra information from
path="component-store-paginator-service/src/app/paginator.component.ts"
region="updating-state">
-```ts
-changePageSize(newPageSize: number) {
- this.paginatorStore.changePageSize(newPageSize);
- }
- nextPage() {
- this.paginatorStore.nextPage();
- }
- firstPage() {
- this.paginatorStore.firstPage();
- }
- previousPage() {
- this.paginatorStore.previousPage();
- }
- lastPage() {
- this.paginatorStore.lastPage();
- }
-```
-
#### Reading the state
diff --git a/projects/www/src/app/pages/guide/data/entity-actions.md b/projects/www/src/app/pages/guide/data/entity-actions.md
index 8d0f8a9ca9..f388c2f2d8 100644
--- a/projects/www/src/app/pages/guide/data/entity-actions.md
+++ b/projects/www/src/app/pages/guide/data/entity-actions.md
@@ -21,7 +21,7 @@ It's optional `payload` carries the message data necessary to perform the operat
An `EntityAction` is a super-set of the _NgRx `Action`_.
It has additional properties that guide NgRx Data's handling of the action. Here's the full interface.
-
+
```ts
export interface EntityAction
diff --git a/projects/www/src/app/pages/guide/data/entity-change-tracker.md b/projects/www/src/app/pages/guide/data/entity-change-tracker.md
index d2202dade2..14611ffe16 100644
--- a/projects/www/src/app/pages/guide/data/entity-change-tracker.md
+++ b/projects/www/src/app/pages/guide/data/entity-change-tracker.md
@@ -93,7 +93,7 @@ described [below](#disable-change-tracking).
A `changeState` map adheres to the following interface
-
+
```ts
export interface ChangeState {
@@ -144,7 +144,7 @@ Delete (remove) is a special case with special rules.
Here are the most important `EntityOps` that record an entity in the `changeState` map:
-
+
```ts
// Save operations when isOptimistic flag is true
@@ -188,7 +188,7 @@ Operations that put that entity in the store also remove it from the `changeStat
Here are the operations that remove one or more specified entities from the `changeState` map.
-
+
```ts
QUERY_ALL_SUCCESS;
@@ -216,7 +216,7 @@ UNDO_MANY;
The `EntityOps` that replace or remove every entity in the collection also reset the `changeState` to an empty object.
All entities in the collection (if any) become "unchanged".
-
+
```ts
ADD_ALL;
diff --git a/projects/www/src/app/pages/guide/data/entity-dataservice.md b/projects/www/src/app/pages/guide/data/entity-dataservice.md
index 5b0402ea90..946084a448 100644
--- a/projects/www/src/app/pages/guide/data/entity-dataservice.md
+++ b/projects/www/src/app/pages/guide/data/entity-dataservice.md
@@ -176,7 +176,7 @@ To support this feature, we 'll create a `HeroDataService` class that implements
In the sample app the `HeroDataService` derives from the NgRx Data `DefaultDataService` in order to leverage its base functionality.
It only overrides what it really needs.
-
+
```ts
import { Injectable } from '@angular/core';
@@ -239,7 +239,7 @@ Finally, we must tell NgRx Data about this new data service.
The sample app provides `HeroDataService` and registers it by calling the `registerService()` method on the `EntityDataService` in the app's _entity store module_:
-
+
```ts
import { EntityDataService } from '@ngrx/data'; // <-- import the NgRx Data data service registry
diff --git a/projects/www/src/app/pages/guide/data/save-entities.md b/projects/www/src/app/pages/guide/data/save-entities.md
index bb61bfe2bd..8c5012ad0f 100644
--- a/projects/www/src/app/pages/guide/data/save-entities.md
+++ b/projects/www/src/app/pages/guide/data/save-entities.md
@@ -49,7 +49,7 @@ We assume a server is ready to handle such a request.
First create the changes (each a `ChangeSetItem`) for the `ChangeSet`.
-
+
```ts
import { ChangeSetOperation } from '@ngrx/data';
@@ -134,7 +134,7 @@ This complicated dance is standard NgRx. Fortunately, all you have to know is th
The `ChangeSet` interface is a simple structure with only one critical property,
`changes`, which holds the entity data to save.
-
+
```ts
export interface ChangeSet {
diff --git a/projects/www/src/app/pages/guide/router-store/selectors.md b/projects/www/src/app/pages/guide/router-store/selectors.md
index dfd3b0c714..3825de08f8 100644
--- a/projects/www/src/app/pages/guide/router-store/selectors.md
+++ b/projects/www/src/app/pages/guide/router-store/selectors.md
@@ -20,117 +20,18 @@ You can see the full example at StackBlitz:
-```ts
-import {
- getRouterSelectors,
- RouterReducerState,
-} from '@ngrx/router-store';
-
-// `router` is used as the default feature name. You can use the feature name
-// of your choice by creating a feature selector and pass it to the `getRouterSelectors` function
-// export const selectRouter = createFeatureSelector('yourFeatureName');
-
-export const {
- selectCurrentRoute, // select the current route
- selectFragment, // select the current route fragment
- selectQueryParams, // select the current route query params
- selectQueryParam, // factory function to select a query param
- selectRouteParams, // select the current route params
- selectRouteParam, // factory function to select a route param
- selectRouteData, // select the current route data
- selectRouteDataParam, // factory function to select a route data param
- selectUrl, // select the current url
- selectTitle, // select the title if available
-} = getRouterSelectors();
-```
-
-```ts
-import { createReducer, on } from '@ngrx/store';
-import { EntityState, createEntityAdapter } from '@ngrx/entity';
-import { appInit } from './car.actions';
-
-export interface Car {
- id: string;
- year: string;
- make: string;
- model: string;
-}
-
-export type CarState = EntityState;
-
-export const carAdapter = createEntityAdapter({
- selectId: (car) => car.id,
-});
-
-const initialState = carAdapter.getInitialState();
-
-export const reducer = createReducer(
- initialState,
- on(appInit, (state, { cars }) => carAdapter.addMany(cars, state))
-);
-```
-
-```ts
-import { createFeatureSelector, createSelector } from '@ngrx/store';
-import { selectRouteParams } from '../router.selectors';
-import { carAdapter, CarState } from './car.reducer';
-
-export const carsFeatureSelector =
- createFeatureSelector('cars');
-
-const { selectEntities, selectAll } = carAdapter.getSelectors();
-
-export const selectCarEntities = createSelector(
- carsFeatureSelector,
- selectEntities
-);
-
-export const selectCars = createSelector(
- carsFeatureSelector,
- selectAll
-);
-
-// you can combine the `selectRouteParams` with `selectCarEntities`
-// to get a selector for the active car for this component based
-// on the route
-export const selectCar = createSelector(
- selectCarEntities,
- selectRouteParams,
- (cars, { carId }) => cars[carId]
-);
-```
-
-```ts
-import { Component, inject } from '@angular/core';
-import { Store } from '@ngrx/store';
-import { selectCar } from './car.selectors';
-import { AsyncPipe, JsonPipe } from '@angular/common';
-
-@Component({
- standalone: true,
- selector: 'app-car',
- templateUrl: './car.component.html',
- styleUrls: ['./car.component.css'],
- imports: [AsyncPipe, JsonPipe],
-})
-export class CarComponent {
- private store = inject(Store);
- car$ = this.store.select(selectCar);
-}
-```
-
## Extracting all params in the current route
diff --git a/projects/www/src/app/pages/guide/signals/signal-state.md b/projects/www/src/app/pages/guide/signals/signal-state.md
index 0c7e41ed40..420669dfbd 100644
--- a/projects/www/src/app/pages/guide/signals/signal-state.md
+++ b/projects/www/src/app/pages/guide/signals/signal-state.md
@@ -121,7 +121,7 @@ patchState(userState, setFirstName('Stevie'), setAdmin());
### Example 1: SignalState in a Component
-
+
```ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
@@ -159,7 +159,7 @@ export class Counter {
### Example 2: SignalState in a Service
-
+
```ts
diff --git a/projects/www/src/app/pages/guide/signals/signal-store/linked-state.md b/projects/www/src/app/pages/guide/signals/signal-store/linked-state.md
index cb9865d615..4f493e8012 100644
--- a/projects/www/src/app/pages/guide/signals/signal-store/linked-state.md
+++ b/projects/www/src/app/pages/guide/signals/signal-store/linked-state.md
@@ -10,7 +10,7 @@ These linked state slices become an integral part of the SignalStore's state and
When a computation function is provided, the SignalStore wraps it in a `linkedSignal()`.
As a result, the linked state slice is updated automatically whenever any of its dependent signals change.
-
+
```ts
diff --git a/projects/www/src/app/pages/guide/signals/signal-store/private-store-members.md b/projects/www/src/app/pages/guide/signals/signal-store/private-store-members.md
index 9c523ff8a9..c03cc60af8 100644
--- a/projects/www/src/app/pages/guide/signals/signal-store/private-store-members.md
+++ b/projects/www/src/app/pages/guide/signals/signal-store/private-store-members.md
@@ -3,7 +3,7 @@
SignalStore allows defining private members that cannot be accessed from outside the store by using the `_` prefix.
This includes root-level state slices, properties, and methods.
-
+
```ts
diff --git a/projects/www/src/app/pages/guide/store/index.md b/projects/www/src/app/pages/guide/store/index.md
index bee15234fc..9e5811553e 100644
--- a/projects/www/src/app/pages/guide/store/index.md
+++ b/projects/www/src/app/pages/guide/store/index.md
@@ -43,76 +43,24 @@ The following tutorial shows you how to manage the state of a counter, and how t
-```ts
-import { createAction } from '@ngrx/store';
-
-export const increment = createAction(
- '[Counter Component] Increment'
-);
-export const decrement = createAction(
- '[Counter Component] Decrement'
-);
-export const reset = createAction('[Counter Component] Reset');
-```
-
3. Define a reducer function to handle changes in the counter value based on the provided actions.
-```ts
-import { createReducer, on } from '@ngrx/store';
-import { increment, decrement, reset } from './counter.actions';
-
-export const initialState = 0;
-
-export const counterReducer = createReducer(
- initialState,
- on(increment, (state) => state + 1),
- on(decrement, (state) => state - 1),
- on(reset, (state) => 0)
-);
-```
-
4. Import the `StoreModule` from `@ngrx/store` and the `counter.reducer` file.
-```ts
-import { StoreModule } from '@ngrx/store';
-import { counterReducer } from './counter.reducer';
-```
-
5. Add the `StoreModule.forRoot` function in the `imports` array of your `AppModule` with an object containing the `count` and the `counterReducer` that manages the state of the counter. The `StoreModule.forRoot()` method registers the global providers needed to access the `Store` throughout your application.
-```ts
-import { BrowserModule } from '@angular/platform-browser';
-import { NgModule } from '@angular/core';
-
-import { AppComponent } from './app.component';
-
-import { StoreModule } from '@ngrx/store';
-import { counterReducer } from './counter.reducer';
-
-@NgModule({
- declarations: [AppComponent],
- imports: [
- BrowserModule,
- StoreModule.forRoot({ count: counterReducer }),
- ],
- providers: [],
- bootstrap: [AppComponent],
-})
-export class AppModule {}
-```
-
6. Create a new file called `my-counter.component.ts` in a folder named `my-counter` within the `app` folder that will define a new component called `MyCounterComponent`. This component will render buttons that allow the user to change the count state. Also, create the `my-counter.component.html` file within this same folder.
diff --git a/projects/www/src/app/pages/guide/store/testing.md b/projects/www/src/app/pages/guide/store/testing.md
index 3baaf02e25..71b7df1a3a 100644
--- a/projects/www/src/app/pages/guide/store/testing.md
+++ b/projects/www/src/app/pages/guide/store/testing.md
@@ -73,94 +73,10 @@ Usage:
-```ts
-import { createSelector, createFeatureSelector } from '@ngrx/store';
-import { Book } from '../book-list/books.model';
-
-export const selectBooks =
- createFeatureSelector>('books');
-
-export const selectCollectionState =
- createFeatureSelector>('collection');
-
-export const selectBookCollection = createSelector(
- selectBooks,
- selectCollectionState,
- (books, collection) => {
- return collection.map(
- (id) => books.find((book) => book.id === id)!
- );
- }
-);
-```
-
-```ts
-mockBooksSelector = store.overrideSelector(selectBooks, [
- {
- id: 'firstId',
- volumeInfo: {
- title: 'First Title',
- authors: ['First Author'],
- },
- },
-]);
-
-mockBookCollectionSelector = store.overrideSelector(
- selectBookCollection,
- []
-);
-
-fixture.detectChanges();
-spyOn(store, 'dispatch').and.callFake(() => {});
-
-it('should update the UI when the store changes', () => {
- mockBooksSelector.setResult([
- {
- id: 'firstId',
- volumeInfo: {
- title: 'First Title',
- authors: ['First Author'],
- },
- },
- {
- id: 'secondId',
- volumeInfo: {
- title: 'Second Title',
- authors: ['Second Author'],
- },
- },
- ]);
-
- mockBookCollectionSelector.setResult([
- {
- id: 'firstId',
- volumeInfo: {
- title: 'First Title',
- authors: ['First Author'],
- },
- },
- ]);
-
- store.refreshState();
- fixture.detectChanges();
-
- expect(
- fixture.debugElement.queryAll(By.css('.book-list .book-item'))
- .length
- ).toBe(2);
-
- expect(
- fixture.debugElement.queryAll(
- By.css('.book-collection .book-item')
- ).length
- ).toBe(1);
-});
-```
-
In this example based on the [walkthrough](guide/store/walkthrough), we mock the `selectBooks` selector by using `overrideSelector`, passing in the `selectBooks` selector with a default mocked return value of an array of books. Similarly, we mock the `selectBookCollection` selector and pass the selector together with another array. In the test, we use `setResult()` to update the mock selectors to return new array values, then we use `MockStore.refreshState()` to trigger an emission from the `selectBooks` and `selectBookCollection` selectors.
@@ -169,54 +85,6 @@ You can reset selectors by calling the `MockStore.resetSelectors()` method in th
-```ts
-describe('AppComponent reset selectors', () => {
- let store: MockStore;
-
- afterEach(() => {
- store?.resetSelectors();
- });
-
- it('should return the mocked value', (done: any) => {
- TestBed.configureTestingModule({
- providers: [
- provideMockStore({
- selectors: [
- {
- selector: selectBooks,
- value: [
- {
- id: 'mockedId',
- volumeInfo: {
- title: 'Mocked Title',
- authors: ['Mocked Author'],
- },
- },
- ],
- },
- ],
- }),
- ],
- });
-
- store = TestBed.inject(MockStore);
-
- store.select(selectBooks).subscribe((mockBooks) => {
- expect(mockBooks).toEqual([
- {
- id: 'mockedId',
- volumeInfo: {
- title: 'Mocked Title',
- authors: ['Mocked Author'],
- },
- },
- ]);
- done();
- });
- });
-});
-```
-
Try the .
@@ -227,78 +95,12 @@ An integration test should verify that the `Store` coherently works together wit
-```ts
-TestBed.configureTestingModule({
- declarations: [
- AppComponent,
- BookListComponent,
- BookCollectionComponent,
- ],
- imports: [
- HttpClientTestingModule,
- StoreModule.forRoot({
- books: booksReducer,
- collection: collectionReducer,
- }),
- ],
- providers: [GoogleBooksService],
-}).compileComponents();
-
-fixture = TestBed.createComponent(AppComponent);
-component = fixture.debugElement.componentInstance;
-
-fixture.detectChanges();
-```
-
The integration test sets up the dependent `Store` by importing the `StoreModule`. In this part of the example, we assert that clicking the `add` button dispatches the corresponding action and is correctly emitted by the `collection` selector.
-```ts
-describe('buttons should work as expected', () => {
- it('should add to collection when add button is clicked and remove from collection when remove button is clicked', () => {
- const addButton = getBookList()[1].query(
- By.css('[data-test=add-button]')
- );
-
- click(addButton);
- expect(getBookTitle(getCollection()[0])).toBe('Second Title');
-
- const removeButton = getCollection()[0].query(
- By.css('[data-test=remove-button]')
- );
- click(removeButton);
-
- expect(getCollection().length).toBe(0);
- });
-});
-
-//functions used in the above test
-function getCollection() {
- return fixture.debugElement.queryAll(
- By.css('.book-collection .book-item')
- );
-}
-
-function getBookList() {
- return fixture.debugElement.queryAll(
- By.css('.book-list .book-item')
- );
-}
-
-function getBookTitle(element) {
- return element.query(By.css('p')).nativeElement.textContent;
-}
-
-function click(element) {
- const el: HTMLElement = element.nativeElement;
- el.click();
- fixture.detectChanges();
-}
-```
-
### Testing selectors
@@ -307,48 +109,6 @@ You can use the projector function used by the selector by accessing the `.proje
-```ts
-import { selectBooks, selectBookCollection } from './books.selectors';
-import { AppState } from './app.state';
-
-describe('Selectors', () => {
- const initialState: AppState = {
- books: [
- {
- id: 'firstId',
- volumeInfo: {
- title: 'First Title',
- authors: ['First Author'],
- },
- },
- {
- id: 'secondId',
- volumeInfo: {
- title: 'Second Title',
- authors: ['Second Author'],
- },
- },
- ],
- collection: ['firstId'],
- };
-
- it('should select the book list', () => {
- const result = selectBooks.projector(initialState.books);
- expect(result.length).toEqual(2);
- expect(result[1].id).toEqual('secondId');
- });
-
- it('should select the book collection', () => {
- const result = selectBookCollection.projector(
- initialState.books,
- initialState.collection
- );
- expect(result.length).toEqual(1);
- expect(result[0].id).toEqual('firstId');
- });
-});
-```
-
### Testing reducers
@@ -357,46 +117,6 @@ The following example tests the `booksReducer` from the [walkthrough](guide/stor
-```ts
-import * as fromReducer from './books.reducer';
-import { retrievedBookList } from './books.actions';
-import { Book } from '../book-list/books.model';
-
-describe('BooksReducer', () => {
- describe('unknown action', () => {
- it('should return the default state', () => {
- const { initialState } = fromReducer;
- const action = {
- type: 'Unknown',
- };
- const state = fromReducer.booksReducer(initialState, action);
-
- expect(state).toBe(initialState);
- });
- });
-
- describe('retrievedBookList action', () => {
- it('should retrieve all books and update the state in an immutable way', () => {
- const { initialState } = fromReducer;
- const newState: Array = [
- {
- id: 'firstId',
- volumeInfo: {
- title: 'First Title',
- authors: ['First Author'],
- },
- },
- ];
- const action = retrievedBookList({ Book: newState });
- const state = fromReducer.booksReducer(initialState, action);
-
- expect(state).toEqual(newState);
- expect(state).not.toBe(initialState);
- });
- });
-});
-```
-
### Testing without `TestBed`
diff --git a/projects/www/src/app/pages/guide/store/walkthrough.md b/projects/www/src/app/pages/guide/store/walkthrough.md
index 0b25c64f95..a43f1d1200 100644
--- a/projects/www/src/app/pages/guide/store/walkthrough.md
+++ b/projects/www/src/app/pages/guide/store/walkthrough.md
@@ -8,233 +8,64 @@ The following example more extensively utilizes the key concepts of store to man
-```ts
-export interface Book {
- id: string;
- volumeInfo: {
- title: string;
- authors: Array;
- };
-}
-```
-
2. Right click on the `app` folder to create a state management folder `state`. Within the new folder, create a new file `books.actions.ts` to describe the book actions. Book actions include the book list retrieval, and the add and remove book actions.
-```ts
-import { createActionGroup, props } from '@ngrx/store';
-import { Book } from '../book-list/books.model';
-
-export const BooksActions = createActionGroup({
- source: 'Books',
- events: {
- 'Add Book': props<{ bookId: string }>(),
- 'Remove Book': props<{ bookId: string }>(),
- },
-});
-
-export const BooksApiActions = createActionGroup({
- source: 'Books API',
- events: {
- 'Retrieved Book List': props<{ books: ReadonlyArray }>(),
- },
-});
-```
-
3. Right click on the `state` folder and create a new file labeled `books.reducer.ts`. Within this file, define a reducer function to handle the retrieval of the book list from the state and consequently, update the state.
-```ts
-import { createReducer, on } from '@ngrx/store';
-
-import { BooksApiActions } from './books.actions';
-import { Book } from '../book-list/books.model';
-
-export const initialState: ReadonlyArray = [];
-
-export const booksReducer = createReducer(
- initialState,
- on(BooksApiActions.retrievedBookList, (_state, { books }) => books)
-);
-```
-
4. Create another file named `collection.reducer.ts` in the `state` folder to handle actions that alter the user's book collection. Define a reducer function that handles the add action by appending the book's ID to the collection, including a condition to avoid duplicate book IDs. Define the same reducer to handle the remove action by filtering the collection array with the book ID.
-```ts
-import { createReducer, on } from '@ngrx/store';
-import { BooksActions } from './books.actions';
-
-export const initialState: ReadonlyArray = [];
-
-export const collectionReducer = createReducer(
- initialState,
- on(BooksActions.removeBook, (state, { bookId }) =>
- state.filter((id) => id !== bookId)
- ),
- on(BooksActions.addBook, (state, { bookId }) => {
- if (state.indexOf(bookId) > -1) return state;
-
- return [...state, bookId];
- })
-);
-```
-
5. Import the `StoreModule` from `@ngrx/store` and the `books.reducer` and `collection.reducer` file.
-```ts
-import { HttpClientModule } from '@angular/common/http';
-import { booksReducer } from './state/books.reducer';
-import { collectionReducer } from './state/collection.reducer';
-import { StoreModule } from '@ngrx/store';
-```
-
6. Add the `StoreModule.forRoot` function in the `imports` array of your `AppModule` with an object containing the `books` and `booksReducer`, as well as the `collection` and `collectionReducer` that manage the state of the book list and the collection. The `StoreModule.forRoot()` method registers the global providers needed to access the `Store` throughout your application.
-```ts
-@NgModule({
- imports: [
- BrowserModule,
- StoreModule.forRoot({
- books: booksReducer,
- collection: collectionReducer,
- }),
- HttpClientModule,
- ],
- declarations: [AppComponent],
- bootstrap: [AppComponent],
-})
-export class AppModule {}
-```
-
7. Create the book list and collection selectors to ensure we get the correct information from the store. As you can see, the `selectBookCollection` selector combines two other selectors in order to build its return value.
-```ts
-import { createSelector, createFeatureSelector } from '@ngrx/store';
-import { Book } from '../book-list/books.model';
-
-export const selectBooks =
- createFeatureSelector>('books');
-
-export const selectCollectionState =
- createFeatureSelector>('collection');
-
-export const selectBookCollection = createSelector(
- selectBooks,
- selectCollectionState,
- (books, collection) => {
- return collection.map(
- (id) => books.find((book) => book.id === id)!
- );
- }
-);
-```
-
8. In the `book-list` folder, we want to have a service that fetches the data needed for the book list from an API. Create a file in the `book-list` folder named `books.service.ts`, which will call the Google Books API and return a list of books.
-```ts
-import { HttpClient } from '@angular/common/http';
-import { Injectable } from '@angular/core';
-
-import { Observable } from 'rxjs';
-import { map } from 'rxjs/operators';
-import { Book } from './books.model';
-
-@Injectable({ providedIn: 'root' })
-export class GoogleBooksService {
- constructor(private http: HttpClient) {}
-
- getBooks(): Observable> {
- return this.http
- .get<{ items: Book[] }>(
- 'https://www.googleapis.com/books/v1/volumes?maxResults=5&orderBy=relevance&q=oliver%20sacks'
- )
- .pipe(map((books) => books.items || []));
- }
-}
-```
-
9. In the same folder (`book-list`), create the `BookListComponent` with the following template. Update the `BookListComponent` class to dispatch the `add` event.
-```html
-
-
{{book.volumeInfo.title}}
- by {{book.volumeInfo.authors}}
-
-
-```
-
-```ts
-import {
- Component,
- EventEmitter,
- Input,
- Output,
-} from '@angular/core';
-import { Book } from './books.model';
-
-@Component({
- selector: 'app-book-list',
- templateUrl: './book-list.component.html',
- styleUrls: ['./book-list.component.css'],
-})
-export class BookListComponent {
- @Input() books: ReadonlyArray = [];
- @Output() add = new EventEmitter();
-}
-```
-
10. Create a new _Component_ named `book-collection` in the `app` folder. Update the `BookCollectionComponent` template and class.
-```html
-