From 0c1ff86ac8941105fcab0eeb927e777868991155 Mon Sep 17 00:00:00 2001 From: Sheik Althaf Date: Thu, 8 Aug 2024 07:02:20 +0530 Subject: [PATCH 01/10] docs: update example app to standalone Updated the example app to standalone, removed all the modules from app. --- projects/example-app/src/app/app.config.ts | 78 ++++++++++++++++++ projects/example-app/src/app/app.module.ts | 81 ------------------- .../{app-routing.module.ts => app.routing.ts} | 22 ++--- .../src/app/auth/auth-routing.module.ts | 13 --- .../example-app/src/app/auth/auth.module.ts | 37 --------- .../example-app/src/app/auth/auth.routes.ts | 10 +++ .../components/login-form.component.spec.ts | 6 +- .../auth/components/login-form.component.ts | 6 +- ...gout-confirmation-dialog.component.spec.ts | 4 +- .../logout-confirmation-dialog.component.ts | 3 + .../containers/login-page.component.spec.ts | 9 +-- .../auth/containers/login-page.component.ts | 4 + projects/example-app/src/app/auth/index.ts | 1 - .../src/app/books/books-routing.module.ts | 34 -------- .../example-app/src/app/books/books.module.ts | 69 ---------------- .../example-app/src/app/books/books.routes.ts | 54 +++++++++++++ .../components/book-authors.component.ts | 4 + .../books/components/book-detail.component.ts | 5 ++ .../components/book-preview-list.component.ts | 4 + .../components/book-preview.component.ts | 13 +++ .../books/components/book-search.component.ts | 4 + .../collection-page.component.spec.ts | 22 +---- .../containers/collection-page.component.ts | 5 ++ .../find-book-page.component.spec.ts | 30 +------ .../containers/find-book-page.component.ts | 4 + .../selected-book-page.component.spec.ts | 19 +---- .../selected-book-page.component.ts | 4 + .../view-book-page.component.spec.ts | 16 +--- .../containers/view-book-page.component.ts | 3 + .../app/core/components/layout.component.ts | 3 + .../app/core/components/nav-item.component.ts | 5 ++ .../app/core/components/sidenav.component.ts | 3 + .../app/core/components/toolbar.component.ts | 3 + .../src/app/core/containers/app.component.ts | 19 +++++ .../containers/not-found-page.component.ts | 4 + .../example-app/src/app/core/core.module.ts | 31 ------- projects/example-app/src/app/core/index.ts | 1 - .../src/app/material/material.module.ts | 38 +++------ .../src/app/shared/pipes/add-commas.pipe.ts | 5 +- .../src/app/shared/pipes/ellipsis.pipe.ts | 5 +- projects/example-app/src/main.ts | 9 +-- 41 files changed, 286 insertions(+), 404 deletions(-) create mode 100644 projects/example-app/src/app/app.config.ts delete mode 100644 projects/example-app/src/app/app.module.ts rename projects/example-app/src/app/{app-routing.module.ts => app.routing.ts} (50%) delete mode 100644 projects/example-app/src/app/auth/auth-routing.module.ts delete mode 100644 projects/example-app/src/app/auth/auth.module.ts create mode 100644 projects/example-app/src/app/auth/auth.routes.ts delete mode 100644 projects/example-app/src/app/auth/index.ts delete mode 100644 projects/example-app/src/app/books/books-routing.module.ts delete mode 100644 projects/example-app/src/app/books/books.module.ts create mode 100644 projects/example-app/src/app/books/books.routes.ts delete mode 100644 projects/example-app/src/app/core/core.module.ts delete mode 100644 projects/example-app/src/app/core/index.ts diff --git a/projects/example-app/src/app/app.config.ts b/projects/example-app/src/app/app.config.ts new file mode 100644 index 0000000000..ea8ea569c0 --- /dev/null +++ b/projects/example-app/src/app/app.config.ts @@ -0,0 +1,78 @@ +import { ApplicationConfig } from '@angular/core'; +import { provideHttpClient } from '@angular/common/http'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; + +import { provideState, provideStore } from '@ngrx/store'; +import { provideEffects } from '@ngrx/effects'; +import { provideRouterStore } from '@ngrx/router-store'; +import { provideStoreDevtools } from '@ngrx/store-devtools'; + +import { rootReducers, metaReducers } from '@example-app/reducers'; + +import { APP_ROUTES } from '@example-app/app.routing'; +import { UserEffects, RouterEffects } from '@example-app/core/effects'; +import { provideRouter, withHashLocation } from '@angular/router'; +import * as fromAuth from '@example-app/auth/reducers'; +import { AuthEffects } from './auth/effects'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideAnimationsAsync(), + provideHttpClient(), + provideRouter(APP_ROUTES, withHashLocation()), + + /** + * provideStore() is imported once in the root providers, accepting a reducer + * function or object map of reducer functions. If passed an object of + * reducers, combineReducers will be run creating your application + * meta-reducer. This returns all providers for an @ngrx/store + * based application. + */ + provideStore(rootReducers, { + metaReducers, + runtimeChecks: { + // strictStateImmutability and strictActionImmutability are enabled by default + strictStateSerializability: true, + strictActionSerializability: true, + strictActionWithinNgZone: true, + strictActionTypeUniqueness: true, + }, + }), + + /** + * @ngrx/router-store keeps router state up-to-date in the store. + */ + provideRouterStore(), + + /** + * Store devtools instrument the store retaining past versions of state + * and recalculating new states. This enables powerful time-travel + * debugging. + * + * To use the debugger, install the Redux Devtools extension for either + * Chrome or Firefox + * + * See: https://github.com/zalmoxisus/redux-devtools-extension + */ + provideStoreDevtools({ + name: 'NgRx Book Store App', + // In a production build you would want to disable the Store Devtools + // logOnly: !isDevMode(), + }), + + /** + * The provideEffects() function is used to register effect classes + * so they are initialized when the application starts. + */ + provideEffects(UserEffects, RouterEffects, AuthEffects), + + /** + * The Auth state is provided here to ensure that the login details + * are available as soon as the application starts. + */ + provideState({ + name: fromAuth.authFeatureKey, + reducer: fromAuth.reducers, + }), + ], +}; diff --git a/projects/example-app/src/app/app.module.ts b/projects/example-app/src/app/app.module.ts deleted file mode 100644 index 1f66f73f6a..0000000000 --- a/projects/example-app/src/app/app.module.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { HttpClientModule } from '@angular/common/http'; -import { BrowserModule } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; - -import { StoreModule } from '@ngrx/store'; -import { EffectsModule } from '@ngrx/effects'; -import { StoreRouterConnectingModule } from '@ngrx/router-store'; -import { StoreDevtoolsModule } from '@ngrx/store-devtools'; - -import { AuthModule } from '@example-app/auth'; - -import { rootReducers, metaReducers } from '@example-app/reducers'; - -import { CoreModule } from '@example-app/core'; -import { AppRoutingModule } from '@example-app/app-routing.module'; -import { UserEffects, RouterEffects } from '@example-app/core/effects'; -import { AppComponent } from '@example-app/core/containers'; - -@NgModule({ - imports: [ - CommonModule, - BrowserModule, - BrowserAnimationsModule, - HttpClientModule, - AuthModule, - AppRoutingModule, - - /** - * StoreModule.forRoot is imported once in the root module, accepting a reducer - * function or object map of reducer functions. If passed an object of - * reducers, combineReducers will be run creating your application - * meta-reducer. This returns all providers for an @ngrx/store - * based application. - */ - StoreModule.forRoot(rootReducers, { - metaReducers, - runtimeChecks: { - // strictStateImmutability and strictActionImmutability are enabled by default - strictStateSerializability: true, - strictActionSerializability: true, - strictActionWithinNgZone: true, - strictActionTypeUniqueness: true, - }, - }), - - /** - * @ngrx/router-store keeps router state up-to-date in the store. - */ - StoreRouterConnectingModule.forRoot(), - - /** - * Store devtools instrument the store retaining past versions of state - * and recalculating new states. This enables powerful time-travel - * debugging. - * - * To use the debugger, install the Redux Devtools extension for either - * Chrome or Firefox - * - * See: https://github.com/zalmoxisus/redux-devtools-extension - */ - StoreDevtoolsModule.instrument({ - name: 'NgRx Book Store App', - // In a production build you would want to disable the Store Devtools - // logOnly: !isDevMode(), - }), - - /** - * EffectsModule.forRoot() is imported once in the root module and - * sets up the effects class to be initialized immediately when the - * application starts. - * - * See: https://ngrx.io/guide/effects#registering-root-effects - */ - EffectsModule.forRoot(UserEffects, RouterEffects), - CoreModule, - ], - bootstrap: [AppComponent], -}) -export class AppModule {} diff --git a/projects/example-app/src/app/app-routing.module.ts b/projects/example-app/src/app/app.routing.ts similarity index 50% rename from projects/example-app/src/app/app-routing.module.ts rename to projects/example-app/src/app/app.routing.ts index cba7ce2327..d4606d572a 100644 --- a/projects/example-app/src/app/app-routing.module.ts +++ b/projects/example-app/src/app/app.routing.ts @@ -1,15 +1,19 @@ -import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; +import { Routes } from '@angular/router'; import { authGuard } from '@example-app/auth/services'; import { NotFoundPageComponent } from '@example-app/core/containers'; -export const routes: Routes = [ +export const APP_ROUTES: Routes = [ + { + path: 'login', + loadChildren: () => + import('@example-app/auth/auth.routes').then((m) => m.AUTH_ROUTES), + }, { path: '', redirectTo: '/books', pathMatch: 'full' }, { path: 'books', loadChildren: () => - import('@example-app/books/books.module').then((m) => m.BooksModule), + import('@example-app/books/books.routes').then((m) => m.BOOKS_ROUTES), canActivate: [authGuard], }, { @@ -18,13 +22,3 @@ export const routes: Routes = [ data: { title: 'Not found' }, }, ]; - -@NgModule({ - imports: [ - RouterModule.forRoot(routes, { - useHash: true, - }), - ], - exports: [RouterModule], -}) -export class AppRoutingModule {} diff --git a/projects/example-app/src/app/auth/auth-routing.module.ts b/projects/example-app/src/app/auth/auth-routing.module.ts deleted file mode 100644 index 62ceda18b6..0000000000 --- a/projects/example-app/src/app/auth/auth-routing.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; -import { LoginPageComponent } from '@example-app/auth/containers'; - -const routes: Routes = [ - { path: 'login', component: LoginPageComponent, data: { title: 'Login' } }, -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class AuthRoutingModule {} diff --git a/projects/example-app/src/app/auth/auth.module.ts b/projects/example-app/src/app/auth/auth.module.ts deleted file mode 100644 index 39527caa8a..0000000000 --- a/projects/example-app/src/app/auth/auth.module.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { ReactiveFormsModule } from '@angular/forms'; -import { StoreModule } from '@ngrx/store'; -import { EffectsModule } from '@ngrx/effects'; -import { LoginPageComponent } from '@example-app/auth/containers'; -import { - LoginFormComponent, - LogoutConfirmationDialogComponent, -} from '@example-app/auth/components'; - -import { AuthEffects } from '@example-app/auth/effects'; -import * as fromAuth from '@example-app/auth/reducers'; -import { MaterialModule } from '@example-app/material'; -import { AuthRoutingModule } from './auth-routing.module'; - -export const COMPONENTS = [ - LoginPageComponent, - LoginFormComponent, - LogoutConfirmationDialogComponent, -]; - -@NgModule({ - imports: [ - CommonModule, - ReactiveFormsModule, - MaterialModule, - AuthRoutingModule, - StoreModule.forFeature({ - name: fromAuth.authFeatureKey, - reducer: fromAuth.reducers, - }), - EffectsModule.forFeature(AuthEffects), - ], - declarations: COMPONENTS, -}) -export class AuthModule {} diff --git a/projects/example-app/src/app/auth/auth.routes.ts b/projects/example-app/src/app/auth/auth.routes.ts new file mode 100644 index 0000000000..7197a2c553 --- /dev/null +++ b/projects/example-app/src/app/auth/auth.routes.ts @@ -0,0 +1,10 @@ +import { Routes } from '@angular/router'; +import { LoginPageComponent } from '@example-app/auth/containers'; + +export const AUTH_ROUTES: Routes = [ + { + path: '', + component: LoginPageComponent, + data: { title: 'Login' }, + }, +]; diff --git a/projects/example-app/src/app/auth/components/login-form.component.spec.ts b/projects/example-app/src/app/auth/components/login-form.component.spec.ts index 52aba4fbe0..0cb1e3456a 100644 --- a/projects/example-app/src/app/auth/components/login-form.component.spec.ts +++ b/projects/example-app/src/app/auth/components/login-form.component.spec.ts @@ -1,7 +1,7 @@ import { TestBed, ComponentFixture } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { LoginFormComponent } from '@example-app/auth/components'; -import { ReactiveFormsModule } from '@angular/forms'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; describe('Login Page', () => { let fixture: ComponentFixture; @@ -9,8 +9,8 @@ describe('Login Page', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ReactiveFormsModule], - declarations: [LoginFormComponent], + imports: [LoginFormComponent], + providers: [provideNoopAnimations()], schemas: [NO_ERRORS_SCHEMA], }); diff --git a/projects/example-app/src/app/auth/components/login-form.component.ts b/projects/example-app/src/app/auth/components/login-form.component.ts index 2357726616..269d298e86 100644 --- a/projects/example-app/src/app/auth/components/login-form.component.ts +++ b/projects/example-app/src/app/auth/components/login-form.component.ts @@ -1,9 +1,13 @@ +import { NgIf } from '@angular/common'; import { Component, Input, Output, EventEmitter } from '@angular/core'; -import { FormGroup, FormControl } from '@angular/forms'; +import { FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms'; import { Credentials } from '@example-app/auth/models'; +import { MaterialModule } from '@example-app/material'; @Component({ + standalone: true, selector: 'bc-login-form', + imports: [MaterialModule, ReactiveFormsModule, NgIf], template: ` Login diff --git a/projects/example-app/src/app/auth/components/logout-confirmation-dialog.component.spec.ts b/projects/example-app/src/app/auth/components/logout-confirmation-dialog.component.spec.ts index 78066eadee..0f3fe7d746 100644 --- a/projects/example-app/src/app/auth/components/logout-confirmation-dialog.component.spec.ts +++ b/projects/example-app/src/app/auth/components/logout-confirmation-dialog.component.spec.ts @@ -1,14 +1,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { LogoutConfirmationDialogComponent } from '@example-app/auth/components'; -import { MaterialModule } from '@example-app/material'; describe('Logout Confirmation Dialog', () => { let fixture: ComponentFixture; beforeEach(() => { TestBed.configureTestingModule({ - imports: [MaterialModule], - declarations: [LogoutConfirmationDialogComponent], + imports: [LogoutConfirmationDialogComponent], }); fixture = TestBed.createComponent(LogoutConfirmationDialogComponent); diff --git a/projects/example-app/src/app/auth/components/logout-confirmation-dialog.component.ts b/projects/example-app/src/app/auth/components/logout-confirmation-dialog.component.ts index 1ee60f1102..eeff134db7 100644 --- a/projects/example-app/src/app/auth/components/logout-confirmation-dialog.component.ts +++ b/projects/example-app/src/app/auth/components/logout-confirmation-dialog.component.ts @@ -1,10 +1,13 @@ import { Component } from '@angular/core'; +import { MaterialModule } from '@example-app/material'; /** * The dialog will close with true if user clicks the ok button, * otherwise it will close with undefined. */ @Component({ + standalone: true, + imports: [MaterialModule], template: ` Logout Are you sure you want to logout? diff --git a/projects/example-app/src/app/auth/containers/login-page.component.spec.ts b/projects/example-app/src/app/auth/containers/login-page.component.spec.ts index b9433c4573..7f367b213f 100644 --- a/projects/example-app/src/app/auth/containers/login-page.component.spec.ts +++ b/projects/example-app/src/app/auth/containers/login-page.component.spec.ts @@ -1,12 +1,9 @@ import { TestBed, ComponentFixture } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { LoginPageComponent } from '@example-app/auth/containers'; -import { LoginFormComponent } from '@example-app/auth/components'; import * as fromAuth from '@example-app/auth/reducers'; import { LoginPageActions } from '@example-app/auth/actions/login-page.actions'; import { provideMockStore, MockStore } from '@ngrx/store/testing'; -import { MaterialModule } from '@example-app/material'; describe('Login Page', () => { let fixture: ComponentFixture; @@ -15,9 +12,9 @@ describe('Login Page', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, MaterialModule, ReactiveFormsModule], - declarations: [LoginPageComponent, LoginFormComponent], + imports: [LoginPageComponent], providers: [ + provideNoopAnimations(), provideMockStore({ selectors: [ { selector: fromAuth.selectLoginPagePending, value: false }, diff --git a/projects/example-app/src/app/auth/containers/login-page.component.ts b/projects/example-app/src/app/auth/containers/login-page.component.ts index d5dec368ee..e85d256c94 100644 --- a/projects/example-app/src/app/auth/containers/login-page.component.ts +++ b/projects/example-app/src/app/auth/containers/login-page.component.ts @@ -3,9 +3,13 @@ import { Store } from '@ngrx/store'; import { Credentials } from '@example-app/auth/models'; import * as fromAuth from '@example-app/auth/reducers'; import { LoginPageActions } from '@example-app/auth/actions/login-page.actions'; +import { LoginFormComponent } from '../components'; +import { AsyncPipe } from '@angular/common'; @Component({ + standalone: true, selector: 'bc-login-page', + imports: [LoginFormComponent, AsyncPipe], template: ` Written By: diff --git a/projects/example-app/src/app/books/components/book-detail.component.ts b/projects/example-app/src/app/books/components/book-detail.component.ts index ae1c59958f..3453300464 100644 --- a/projects/example-app/src/app/books/components/book-detail.component.ts +++ b/projects/example-app/src/app/books/components/book-detail.component.ts @@ -1,9 +1,14 @@ +import { NgIf } from '@angular/common'; import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Book } from '@example-app/books/models'; +import { MaterialModule } from '@example-app/material'; +import { BookAuthorsComponent } from './book-authors.component'; @Component({ + standalone: true, selector: 'bc-book-detail', + imports: [MaterialModule, NgIf, BookAuthorsComponent], template: ` diff --git a/projects/example-app/src/app/books/components/book-preview-list.component.ts b/projects/example-app/src/app/books/components/book-preview-list.component.ts index b93c3e83e7..7ef391e47e 100644 --- a/projects/example-app/src/app/books/components/book-preview-list.component.ts +++ b/projects/example-app/src/app/books/components/book-preview-list.component.ts @@ -1,9 +1,13 @@ import { Component, Input } from '@angular/core'; import { Book } from '@example-app/books/models'; +import { BookPreviewComponent } from './book-preview.component'; +import { NgFor } from '@angular/common'; @Component({ + standalone: true, selector: 'bc-book-preview-list', + imports: [BookPreviewComponent, NgFor], template: ` `, diff --git a/projects/example-app/src/app/books/components/book-preview.component.ts b/projects/example-app/src/app/books/components/book-preview.component.ts index 41a5f4c14d..eb56168424 100644 --- a/projects/example-app/src/app/books/components/book-preview.component.ts +++ b/projects/example-app/src/app/books/components/book-preview.component.ts @@ -1,9 +1,22 @@ import { Component, Input } from '@angular/core'; +import { RouterLink } from '@angular/router'; import { Book } from '@example-app/books/models'; +import { MaterialModule } from '@example-app/material'; +import { EllipsisPipe } from '@example-app/shared/pipes/ellipsis.pipe'; +import { BookAuthorsComponent } from './book-authors.component'; +import { NgIf } from '@angular/common'; @Component({ + standalone: true, selector: 'bc-book-preview', + imports: [ + MaterialModule, + RouterLink, + EllipsisPipe, + BookAuthorsComponent, + NgIf, + ], template: ` diff --git a/projects/example-app/src/app/books/components/book-search.component.ts b/projects/example-app/src/app/books/components/book-search.component.ts index 1ae6822056..28c4f161d7 100644 --- a/projects/example-app/src/app/books/components/book-search.component.ts +++ b/projects/example-app/src/app/books/components/book-search.component.ts @@ -1,7 +1,11 @@ +import { NgIf } from '@angular/common'; import { Component, Output, Input, EventEmitter } from '@angular/core'; +import { MaterialModule } from '@example-app/material'; @Component({ + standalone: true, selector: 'bc-book-search', + imports: [MaterialModule, NgIf], template: ` Find a Book diff --git a/projects/example-app/src/app/books/containers/collection-page.component.spec.ts b/projects/example-app/src/app/books/containers/collection-page.component.spec.ts index 5addaa4aef..257cf36940 100644 --- a/projects/example-app/src/app/books/containers/collection-page.component.spec.ts +++ b/projects/example-app/src/app/books/containers/collection-page.component.spec.ts @@ -1,20 +1,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterTestingModule } from '@angular/router/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { CollectionPageActions } from '@example-app/books/actions/collection-page.actions'; -import { - BookAuthorsComponent, - BookPreviewComponent, - BookPreviewListComponent, -} from '@example-app/books/components'; import { CollectionPageComponent } from '@example-app/books/containers'; import * as fromBooks from '@example-app/books/reducers'; -import { AddCommasPipe } from '@example-app/shared/pipes/add-commas.pipe'; -import { EllipsisPipe } from '@example-app/shared/pipes/ellipsis.pipe'; -import { MaterialModule } from '@example-app/material'; describe('Collection Page', () => { let fixture: ComponentFixture; @@ -22,16 +13,9 @@ describe('Collection Page', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, MaterialModule, RouterTestingModule], - declarations: [ - CollectionPageComponent, - BookPreviewListComponent, - BookPreviewComponent, - BookAuthorsComponent, - AddCommasPipe, - EllipsisPipe, - ], + imports: [CollectionPageComponent], providers: [ + provideNoopAnimations(), provideMockStore({ selectors: [{ selector: fromBooks.selectBookCollection, value: [] }], }), diff --git a/projects/example-app/src/app/books/containers/collection-page.component.ts b/projects/example-app/src/app/books/containers/collection-page.component.ts index 48acf84839..c0e6308cfd 100644 --- a/projects/example-app/src/app/books/containers/collection-page.component.ts +++ b/projects/example-app/src/app/books/containers/collection-page.component.ts @@ -6,10 +6,15 @@ import { Observable } from 'rxjs'; import { CollectionPageActions } from '@example-app/books/actions/collection-page.actions'; import { Book } from '@example-app/books/models'; import * as fromBooks from '@example-app/books/reducers'; +import { MaterialModule } from '@example-app/material'; +import { BookPreviewListComponent } from '../components'; +import { AsyncPipe } from '@angular/common'; @Component({ + standalone: true, selector: 'bc-collection-page', changeDetection: ChangeDetectionStrategy.OnPush, + imports: [MaterialModule, BookPreviewListComponent, AsyncPipe], template: ` My Collection diff --git a/projects/example-app/src/app/books/containers/find-book-page.component.spec.ts b/projects/example-app/src/app/books/containers/find-book-page.component.spec.ts index 719d1a31ae..0811c8ba76 100644 --- a/projects/example-app/src/app/books/containers/find-book-page.component.spec.ts +++ b/projects/example-app/src/app/books/containers/find-book-page.component.spec.ts @@ -1,22 +1,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { RouterTestingModule } from '@angular/router/testing'; +import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { FindBookPageActions } from '@example-app/books/actions/find-book-page.actions'; -import { - BookAuthorsComponent, - BookPreviewComponent, - BookPreviewListComponent, - BookSearchComponent, -} from '@example-app/books/components'; import { FindBookPageComponent } from '@example-app/books/containers'; import * as fromBooks from '@example-app/books/reducers'; -import { AddCommasPipe } from '@example-app/shared/pipes/add-commas.pipe'; -import { EllipsisPipe } from '@example-app/shared/pipes/ellipsis.pipe'; -import { MaterialModule } from '@example-app/material'; describe('Find Book Page', () => { let fixture: ComponentFixture; @@ -25,22 +14,9 @@ describe('Find Book Page', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ - NoopAnimationsModule, - RouterTestingModule, - MaterialModule, - ReactiveFormsModule, - ], - declarations: [ - FindBookPageComponent, - BookSearchComponent, - BookPreviewComponent, - BookPreviewListComponent, - BookAuthorsComponent, - AddCommasPipe, - EllipsisPipe, - ], + imports: [FindBookPageComponent], providers: [ + provideNoopAnimations(), provideMockStore({ selectors: [ { selector: fromBooks.selectSearchQuery, value: '' }, diff --git a/projects/example-app/src/app/books/containers/find-book-page.component.ts b/projects/example-app/src/app/books/containers/find-book-page.component.ts index 254727e58f..86ba1f132b 100644 --- a/projects/example-app/src/app/books/containers/find-book-page.component.ts +++ b/projects/example-app/src/app/books/containers/find-book-page.component.ts @@ -7,10 +7,14 @@ import { take } from 'rxjs/operators'; import { FindBookPageActions } from '@example-app/books/actions/find-book-page.actions'; import { Book } from '@example-app/books/models'; import * as fromBooks from '@example-app/books/reducers'; +import { BookPreviewListComponent, BookSearchComponent } from '../components'; +import { AsyncPipe } from '@angular/common'; @Component({ + standalone: true, selector: 'bc-find-book-page', changeDetection: ChangeDetectionStrategy.OnPush, + imports: [BookSearchComponent, AsyncPipe, BookPreviewListComponent], template: ` { let fixture: ComponentFixture; @@ -20,14 +15,8 @@ describe('Selected Book Page', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, MaterialModule], - declarations: [ - SelectedBookPageComponent, - BookDetailComponent, - BookAuthorsComponent, - AddCommasPipe, - ], - providers: [provideMockStore()], + imports: [SelectedBookPageComponent], + providers: [provideNoopAnimations(), provideMockStore()], }); fixture = TestBed.createComponent(SelectedBookPageComponent); diff --git a/projects/example-app/src/app/books/containers/selected-book-page.component.ts b/projects/example-app/src/app/books/containers/selected-book-page.component.ts index 292666869b..e16ba31976 100644 --- a/projects/example-app/src/app/books/containers/selected-book-page.component.ts +++ b/projects/example-app/src/app/books/containers/selected-book-page.component.ts @@ -6,10 +6,14 @@ import { Observable } from 'rxjs'; import { SelectedBookPageActions } from '@example-app/books/actions/selected-book-page.actions'; import { Book } from '@example-app/books/models'; import * as fromBooks from '@example-app/books/reducers'; +import { BookDetailComponent } from '../components'; +import { AsyncPipe } from '@angular/common'; @Component({ + standalone: true, selector: 'bc-selected-book-page', changeDetection: ChangeDetectionStrategy.OnPush, + imports: [BookDetailComponent, AsyncPipe], template: ` { let fixture: ComponentFixture; @@ -21,7 +14,7 @@ describe('View Book Page', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [MaterialModule], + imports: [ViewBookPageComponent], providers: [ { provide: ActivatedRoute, @@ -29,13 +22,6 @@ describe('View Book Page', () => { }, provideMockStore(), ], - declarations: [ - ViewBookPageComponent, - SelectedBookPageComponent, - BookDetailComponent, - BookAuthorsComponent, - AddCommasPipe, - ], }); fixture = TestBed.createComponent(ViewBookPageComponent); diff --git a/projects/example-app/src/app/books/containers/view-book-page.component.ts b/projects/example-app/src/app/books/containers/view-book-page.component.ts index f4bf63b54c..a6295f1860 100644 --- a/projects/example-app/src/app/books/containers/view-book-page.component.ts +++ b/projects/example-app/src/app/books/containers/view-book-page.component.ts @@ -5,6 +5,7 @@ import { Subscription } from 'rxjs'; import { map } from 'rxjs/operators'; import { ViewBookPageActions } from '@example-app/books/actions/view-book-page.actions'; +import { SelectedBookPageComponent } from './selected-book-page.component'; /** * Note: Container components are also reusable. Whether or not @@ -17,8 +18,10 @@ import { ViewBookPageActions } from '@example-app/books/actions/view-book-page.a * SelectedBookPageComponent */ @Component({ + standalone: true, selector: 'bc-view-book-page', changeDetection: ChangeDetectionStrategy.OnPush, + imports: [SelectedBookPageComponent], template: ` `, }) export class ViewBookPageComponent implements OnDestroy { diff --git a/projects/example-app/src/app/core/components/layout.component.ts b/projects/example-app/src/app/core/components/layout.component.ts index 0975f50bd0..8510b0f925 100644 --- a/projects/example-app/src/app/core/components/layout.component.ts +++ b/projects/example-app/src/app/core/components/layout.component.ts @@ -1,7 +1,10 @@ import { Component } from '@angular/core'; +import { MaterialModule } from '@example-app/material'; @Component({ + standalone: true, selector: 'bc-layout', + imports: [MaterialModule], template: ` diff --git a/projects/example-app/src/app/core/components/nav-item.component.ts b/projects/example-app/src/app/core/components/nav-item.component.ts index 535d744b94..28e8753929 100644 --- a/projects/example-app/src/app/core/components/nav-item.component.ts +++ b/projects/example-app/src/app/core/components/nav-item.component.ts @@ -1,7 +1,12 @@ +import { NgIf } from '@angular/common'; import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { MaterialModule } from '@example-app/material'; @Component({ + standalone: true, selector: 'bc-nav-item', + imports: [MaterialModule, RouterLink, NgIf], template: ` {{ icon }} diff --git a/projects/example-app/src/app/core/components/sidenav.component.ts b/projects/example-app/src/app/core/components/sidenav.component.ts index a9f4c1c103..702433fff2 100644 --- a/projects/example-app/src/app/core/components/sidenav.component.ts +++ b/projects/example-app/src/app/core/components/sidenav.component.ts @@ -1,7 +1,10 @@ import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { MaterialModule } from '@example-app/material'; @Component({ + standalone: true, selector: 'bc-sidenav', + imports: [MaterialModule], template: ` diff --git a/projects/example-app/src/app/core/containers/app.component.ts b/projects/example-app/src/app/core/containers/app.component.ts index 46a26eb8f0..ec3268f364 100644 --- a/projects/example-app/src/app/core/containers/app.component.ts +++ b/projects/example-app/src/app/core/containers/app.component.ts @@ -6,10 +6,29 @@ import { AuthActions } from '@example-app/auth/actions/auth.actions'; import * as fromAuth from '@example-app/auth/reducers'; import * as fromRoot from '@example-app/reducers'; import { LayoutActions } from '@example-app/core/actions/layout.actions'; +import { + LayoutComponent, + NavItemComponent, + SidenavComponent, + ToolbarComponent, +} from '../components'; +import { RouterLink, RouterOutlet } from '@angular/router'; +import { AsyncPipe, NgIf } from '@angular/common'; @Component({ + standalone: true, selector: 'bc-app', changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + RouterOutlet, + NgIf, + AsyncPipe, + RouterLink, + LayoutComponent, + SidenavComponent, + NavItemComponent, + ToolbarComponent, + ], template: ` diff --git a/projects/example-app/src/app/core/containers/not-found-page.component.ts b/projects/example-app/src/app/core/containers/not-found-page.component.ts index fb61d30cf5..79e0d34b45 100644 --- a/projects/example-app/src/app/core/containers/not-found-page.component.ts +++ b/projects/example-app/src/app/core/containers/not-found-page.component.ts @@ -1,8 +1,12 @@ import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { MaterialModule } from '@example-app/material'; @Component({ + standalone: true, selector: 'bc-not-found-page', changeDetection: ChangeDetectionStrategy.OnPush, + imports: [MaterialModule, RouterLink], template: ` 404: Not Found diff --git a/projects/example-app/src/app/core/core.module.ts b/projects/example-app/src/app/core/core.module.ts deleted file mode 100644 index 546403a618..0000000000 --- a/projects/example-app/src/app/core/core.module.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; - -import { MaterialModule } from '@example-app/material'; -import { - LayoutComponent, - NavItemComponent, - SidenavComponent, - ToolbarComponent, -} from '@example-app/core/components'; -import { - AppComponent, - NotFoundPageComponent, -} from '@example-app/core/containers'; - -export const COMPONENTS = [ - AppComponent, - NotFoundPageComponent, - LayoutComponent, - NavItemComponent, - SidenavComponent, - ToolbarComponent, -]; - -@NgModule({ - imports: [CommonModule, RouterModule, MaterialModule], - declarations: COMPONENTS, - exports: COMPONENTS, -}) -export class CoreModule {} diff --git a/projects/example-app/src/app/core/index.ts b/projects/example-app/src/app/core/index.ts deleted file mode 100644 index a30d0c7161..0000000000 --- a/projects/example-app/src/app/core/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './core.module'; diff --git a/projects/example-app/src/app/material/material.module.ts b/projects/example-app/src/app/material/material.module.ts index 734b31afb3..bf569067a6 100644 --- a/projects/example-app/src/app/material/material.module.ts +++ b/projects/example-app/src/app/material/material.module.ts @@ -1,5 +1,3 @@ -import { NgModule } from '@angular/core'; - import { MatInputModule } from '@angular/material/input'; import { MatCardModule } from '@angular/material/card'; import { MatButtonModule } from '@angular/material/button'; @@ -10,28 +8,14 @@ import { MatToolbarModule } from '@angular/material/toolbar'; import { MatDialogModule } from '@angular/material/dialog'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -@NgModule({ - imports: [ - MatInputModule, - MatCardModule, - MatButtonModule, - MatSidenavModule, - MatListModule, - MatIconModule, - MatToolbarModule, - MatProgressSpinnerModule, - MatDialogModule, - ], - exports: [ - MatInputModule, - MatCardModule, - MatButtonModule, - MatSidenavModule, - MatListModule, - MatIconModule, - MatToolbarModule, - MatProgressSpinnerModule, - MatDialogModule, - ], -}) -export class MaterialModule {} +export const MaterialModule = [ + MatInputModule, + MatCardModule, + MatButtonModule, + MatSidenavModule, + MatListModule, + MatIconModule, + MatToolbarModule, + MatProgressSpinnerModule, + MatDialogModule, +]; diff --git a/projects/example-app/src/app/shared/pipes/add-commas.pipe.ts b/projects/example-app/src/app/shared/pipes/add-commas.pipe.ts index b64b4209c0..5f7dad2a35 100644 --- a/projects/example-app/src/app/shared/pipes/add-commas.pipe.ts +++ b/projects/example-app/src/app/shared/pipes/add-commas.pipe.ts @@ -1,6 +1,9 @@ import { Pipe, PipeTransform } from '@angular/core'; -@Pipe({ name: 'bcAddCommas' }) +@Pipe({ + standalone: true, + name: 'bcAddCommas', +}) export class AddCommasPipe implements PipeTransform { transform(authors: null | string[]) { if (!authors) { diff --git a/projects/example-app/src/app/shared/pipes/ellipsis.pipe.ts b/projects/example-app/src/app/shared/pipes/ellipsis.pipe.ts index f92a414b65..cd1170f2ec 100644 --- a/projects/example-app/src/app/shared/pipes/ellipsis.pipe.ts +++ b/projects/example-app/src/app/shared/pipes/ellipsis.pipe.ts @@ -1,6 +1,9 @@ import { Pipe, PipeTransform } from '@angular/core'; -@Pipe({ name: 'bcEllipsis' }) +@Pipe({ + standalone: true, + name: 'bcEllipsis', +}) export class EllipsisPipe implements PipeTransform { transform(str: string, strLength = 250) { const withoutHtml = str.replace(/(<([^>]+)>)/gi, ''); diff --git a/projects/example-app/src/main.ts b/projects/example-app/src/main.ts index 1d91a20a57..6a1ec69979 100644 --- a/projects/example-app/src/main.ts +++ b/projects/example-app/src/main.ts @@ -1,6 +1,5 @@ -import './polyfills'; +import { bootstrapApplication } from '@angular/platform-browser'; +import { AppComponent } from './app/core/containers/app.component'; +import { appConfig } from './app/app.config'; -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { AppModule } from './app/app.module'; - -platformBrowserDynamic().bootstrapModule(AppModule); +bootstrapApplication(AppComponent, appConfig); From 7ce0ef3a3a1f1f7e922a94c1266f6b3121bcbc89 Mon Sep 17 00:00:00 2001 From: Rainer Hahnekamp Date: Wed, 14 Aug 2024 16:52:38 +0200 Subject: [PATCH 02/10] docs: switch to esbuild and sync project.json with latest one --- projects/example-app/project.json | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/projects/example-app/project.json b/projects/example-app/project.json index 9a194298aa..bba5104512 100644 --- a/projects/example-app/project.json +++ b/projects/example-app/project.json @@ -7,12 +7,12 @@ "generators": {}, "targets": { "build": { - "executor": "@angular-devkit/build-angular:browser", + "executor": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/projects/example-app", "index": "projects/example-app/src/index.html", - "main": "projects/example-app/src/main.ts", - "polyfills": "projects/example-app/src/polyfills.ts", + "browser": "projects/example-app/src/main.ts", + "polyfills": ["zone.js"], "tsConfig": "projects/example-app/tsconfig.app.json", "assets": [ "projects/example-app/src/favicon.ico", @@ -24,20 +24,23 @@ "configurations": { "production": { "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, { "type": "anyComponentStyle", - "maximumWarning": "6kb" + "maximumWarning": "2kb", + "maximumError": "4kb" } ], "outputHashing": "all" }, "development": { - "buildOptimizer": false, "optimization": false, - "vendorChunk": true, "extractLicenses": false, - "sourceMap": true, - "namedChunks": true + "sourceMap": true } }, "defaultConfiguration": "production", From b06c9204987c966defc060b6c0f4e69ab7892ee3 Mon Sep 17 00:00:00 2001 From: Rainer Hahnekamp Date: Mon, 16 Sep 2024 00:03:04 +0200 Subject: [PATCH 03/10] docs: move to createFeature --- projects/example-app/src/app/app.config.ts | 28 +++++---- .../src/app/auth/reducers/auth.reducer.ts | 19 +++--- .../src/app/auth/reducers/index.ts | 63 +++++-------------- .../app/auth/reducers/login-page.reducer.ts | 46 +++++++------- .../src/app/core/containers/app.component.ts | 3 +- .../src/app/core/reducers/layout.reducer.ts | 28 ++++++--- .../example-app/src/app/reducers/index.ts | 15 ----- .../src/app/app.component.spec.ts | 11 +++- .../standalone-app/src/app/app.effects.ts | 6 +- 9 files changed, 94 insertions(+), 125 deletions(-) diff --git a/projects/example-app/src/app/app.config.ts b/projects/example-app/src/app/app.config.ts index ea8ea569c0..092037f64c 100644 --- a/projects/example-app/src/app/app.config.ts +++ b/projects/example-app/src/app/app.config.ts @@ -2,7 +2,7 @@ import { ApplicationConfig } from '@angular/core'; import { provideHttpClient } from '@angular/common/http'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; -import { provideState, provideStore } from '@ngrx/store'; +import { provideStore } from '@ngrx/store'; import { provideEffects } from '@ngrx/effects'; import { provideRouterStore } from '@ngrx/router-store'; import { provideStoreDevtools } from '@ngrx/store-devtools'; @@ -12,8 +12,9 @@ import { rootReducers, metaReducers } from '@example-app/reducers'; import { APP_ROUTES } from '@example-app/app.routing'; import { UserEffects, RouterEffects } from '@example-app/core/effects'; import { provideRouter, withHashLocation } from '@angular/router'; -import * as fromAuth from '@example-app/auth/reducers'; import { AuthEffects } from './auth/effects'; +import { provideAuth } from '@example-app/auth/reducers'; +import { provideLayout } from '@example-app/core/reducers/layout.reducer'; export const appConfig: ApplicationConfig = { providers: [ @@ -39,6 +40,20 @@ export const appConfig: ApplicationConfig = { }, }), + /** + * The layout feature manages the visibility of the sidenav. + */ + provideLayout(), + + /** + * The Auth state is provided here to ensure that the login details + * are available as soon as the application starts. + * + * It could also be part of the `rootReducers`, but is separated + * because of demonstration purposes. + */ + provideAuth(), + /** * @ngrx/router-store keeps router state up-to-date in the store. */ @@ -65,14 +80,5 @@ export const appConfig: ApplicationConfig = { * so they are initialized when the application starts. */ provideEffects(UserEffects, RouterEffects, AuthEffects), - - /** - * The Auth state is provided here to ensure that the login details - * are available as soon as the application starts. - */ - provideState({ - name: fromAuth.authFeatureKey, - reducer: fromAuth.reducers, - }), ], }; diff --git a/projects/example-app/src/app/auth/reducers/auth.reducer.ts b/projects/example-app/src/app/auth/reducers/auth.reducer.ts index 19e3238e6d..e7af6b3e28 100644 --- a/projects/example-app/src/app/auth/reducers/auth.reducer.ts +++ b/projects/example-app/src/app/auth/reducers/auth.reducer.ts @@ -1,10 +1,8 @@ -import { createReducer, on } from '@ngrx/store'; +import { createFeature, createReducer, on } from '@ngrx/store'; import { AuthApiActions } from '@example-app/auth/actions/auth-api.actions'; import { AuthActions } from '@example-app/auth/actions/auth.actions'; import { User } from '@example-app/auth/models'; -export const statusFeatureKey = 'status'; - export interface State { user: User | null; } @@ -13,10 +11,11 @@ export const initialState: State = { user: null, }; -export const reducer = createReducer( - initialState, - on(AuthApiActions.loginSuccess, (state, { user }) => ({ ...state, user })), - on(AuthActions.logout, () => initialState) -); - -export const getUser = (state: State) => state.user; +export const statusFeature = createFeature({ + name: 'authStatus', + reducer: createReducer( + initialState, + on(AuthApiActions.loginSuccess, (state, { user }) => ({ ...state, user })), + on(AuthActions.logout, () => initialState) + ), +}); diff --git a/projects/example-app/src/app/auth/reducers/index.ts b/projects/example-app/src/app/auth/reducers/index.ts index 9350e27d2c..65b6c6cbb2 100644 --- a/projects/example-app/src/app/auth/reducers/index.ts +++ b/projects/example-app/src/app/auth/reducers/index.ts @@ -1,52 +1,19 @@ -import { - createSelector, - createFeatureSelector, - Action, - combineReducers, -} from '@ngrx/store'; -import * as fromRoot from '@example-app/reducers'; -import * as fromAuth from '@example-app/auth/reducers/auth.reducer'; -import * as fromLoginPage from '@example-app/auth/reducers/login-page.reducer'; +import { createSelector, provideState } from '@ngrx/store'; +import { statusFeature } from './auth.reducer'; +import { loginPageFeature } from './login-page.reducer'; +import { makeEnvironmentProviders } from '@angular/core'; -export const authFeatureKey = 'auth'; +export const provideAuth = () => + makeEnvironmentProviders([ + provideState(statusFeature), + provideState(loginPageFeature), + ]); -export interface AuthState { - [fromAuth.statusFeatureKey]: fromAuth.State; - [fromLoginPage.loginPageFeatureKey]: fromLoginPage.State; -} - -export interface State extends fromRoot.State { - [authFeatureKey]: AuthState; -} - -export function reducers(state: AuthState | undefined, action: Action) { - return combineReducers({ - [fromAuth.statusFeatureKey]: fromAuth.reducer, - [fromLoginPage.loginPageFeatureKey]: fromLoginPage.reducer, - })(state, action); -} - -export const selectAuthState = createFeatureSelector(authFeatureKey); - -export const selectAuthStatusState = createSelector( - selectAuthState, - (state) => state.status -); -export const selectUser = createSelector( - selectAuthStatusState, - fromAuth.getUser +export const selectLoggedIn = createSelector( + statusFeature.selectUser, + (user) => !!user ); -export const selectLoggedIn = createSelector(selectUser, (user) => !!user); -export const selectLoginPageState = createSelector( - selectAuthState, - (state) => state.loginPage -); -export const selectLoginPageError = createSelector( - selectLoginPageState, - fromLoginPage.getError -); -export const selectLoginPagePending = createSelector( - selectLoginPageState, - fromLoginPage.getPending -); +export const selectLoginPageError = loginPageFeature.selectError; + +export const selectLoginPagePending = loginPageFeature.selectPending; diff --git a/projects/example-app/src/app/auth/reducers/login-page.reducer.ts b/projects/example-app/src/app/auth/reducers/login-page.reducer.ts index d844c2052d..bb09c273ea 100644 --- a/projects/example-app/src/app/auth/reducers/login-page.reducer.ts +++ b/projects/example-app/src/app/auth/reducers/login-page.reducer.ts @@ -1,8 +1,6 @@ import { AuthApiActions } from '@example-app/auth/actions/auth-api.actions'; import { LoginPageActions } from '@example-app/auth/actions/login-page.actions'; -import { createReducer, on } from '@ngrx/store'; - -export const loginPageFeatureKey = 'loginPage'; +import { createFeature, createReducer, on } from '@ngrx/store'; export interface State { error: string | null; @@ -14,25 +12,25 @@ export const initialState: State = { pending: false, }; -export const reducer = createReducer( - initialState, - on(LoginPageActions.login, (state) => ({ - ...state, - error: null, - pending: true, - })), - - on(AuthApiActions.loginSuccess, (state) => ({ - ...state, - error: null, - pending: false, - })), - on(AuthApiActions.loginFailure, (state, { error }) => ({ - ...state, - error, - pending: false, - })) -); +export const loginPageFeature = createFeature({ + name: 'authLoginPage', + reducer: createReducer( + initialState, + on(LoginPageActions.login, (state) => ({ + ...state, + error: null, + pending: true, + })), -export const getError = (state: State) => state.error; -export const getPending = (state: State) => state.pending; + on(AuthApiActions.loginSuccess, (state) => ({ + ...state, + error: null, + pending: false, + })), + on(AuthApiActions.loginFailure, (state, { error }) => ({ + ...state, + error, + pending: false, + })) + ), +}); diff --git a/projects/example-app/src/app/core/containers/app.component.ts b/projects/example-app/src/app/core/containers/app.component.ts index ec3268f364..da3b2d9705 100644 --- a/projects/example-app/src/app/core/containers/app.component.ts +++ b/projects/example-app/src/app/core/containers/app.component.ts @@ -14,6 +14,7 @@ import { } from '../components'; import { RouterLink, RouterOutlet } from '@angular/router'; import { AsyncPipe, NgIf } from '@angular/common'; +import { selectShowSidenav } from '../reducers/layout.reducer'; @Component({ standalone: true, @@ -75,7 +76,7 @@ export class AppComponent { * Selectors can be applied with the `select` operator which passes the state * tree to the provided selector */ - this.showSidenav$ = this.store.select(fromRoot.selectShowSidenav); + this.showSidenav$ = this.store.select(selectShowSidenav); this.loggedIn$ = this.store.select(fromAuth.selectLoggedIn); } diff --git a/projects/example-app/src/app/core/reducers/layout.reducer.ts b/projects/example-app/src/app/core/reducers/layout.reducer.ts index 9b0a05ad7a..fa5a0ba5e6 100644 --- a/projects/example-app/src/app/core/reducers/layout.reducer.ts +++ b/projects/example-app/src/app/core/reducers/layout.reducer.ts @@ -1,9 +1,8 @@ -import { createReducer, on } from '@ngrx/store'; +import { createFeature, createReducer, on, provideState } from '@ngrx/store'; import { LayoutActions } from '@example-app/core/actions/layout.actions'; import { AuthActions } from '@example-app/auth/actions/auth.actions'; - -export const layoutFeatureKey = 'layout'; +import { makeEnvironmentProviders } from '@angular/core'; export interface State { showSidenav: boolean; @@ -13,11 +12,20 @@ const initialState: State = { showSidenav: false, }; -export const reducer = createReducer( - initialState, - on(LayoutActions.closeSidenav, () => ({ showSidenav: false })), - on(LayoutActions.openSidenav, () => ({ showSidenav: true })), - on(AuthActions.logoutConfirmation, () => ({ showSidenav: false })) -); +const layoutFeature = createFeature({ + name: 'layout', + reducer: createReducer( + initialState, + on(LayoutActions.closeSidenav, () => ({ showSidenav: false })), + on(LayoutActions.openSidenav, () => ({ showSidenav: true })), + on(AuthActions.logoutConfirmation, () => ({ showSidenav: false })) + ), +}); + +export const provideLayout = () => + makeEnvironmentProviders([provideState(layoutFeature)]); -export const selectShowSidenav = (state: State) => state.showSidenav; +/** + * Layout Selectors + */ +export const selectShowSidenav = layoutFeature.selectShowSidenav; diff --git a/projects/example-app/src/app/reducers/index.ts b/projects/example-app/src/app/reducers/index.ts index be4e66a253..b82bd89f28 100644 --- a/projects/example-app/src/app/reducers/index.ts +++ b/projects/example-app/src/app/reducers/index.ts @@ -18,7 +18,6 @@ import { * notation packages up all of the exports into a single object. */ -import * as fromLayout from '@example-app/core/reducers/layout.reducer'; import { isDevMode } from '@angular/core'; /** @@ -26,7 +25,6 @@ import { isDevMode } from '@angular/core'; * our top level state interface is just a map of keys to inner state types. */ export interface State { - [fromLayout.layoutFeatureKey]: fromLayout.State; router: RouterReducerState; } @@ -36,7 +34,6 @@ export interface State { * and the current or initial state and return a new immutable state. */ export const rootReducers: ActionReducerMap = { - [fromLayout.layoutFeatureKey]: fromLayout.reducer, router: routerReducer, }; @@ -61,18 +58,6 @@ export function logger(reducer: ActionReducer): ActionReducer { */ export const metaReducers: MetaReducer[] = isDevMode() ? [logger] : []; -/** - * Layout Selectors - */ -export const selectLayoutState = createFeatureSelector( - fromLayout.layoutFeatureKey -); - -export const selectShowSidenav = createSelector( - selectLayoutState, - fromLayout.selectShowSidenav -); - /** * Router Selectors */ diff --git a/projects/standalone-app/src/app/app.component.spec.ts b/projects/standalone-app/src/app/app.component.spec.ts index e636a11368..ced3e561c8 100644 --- a/projects/standalone-app/src/app/app.component.spec.ts +++ b/projects/standalone-app/src/app/app.component.spec.ts @@ -1,13 +1,18 @@ import { TestBed } from '@angular/core/testing'; import { AppComponent } from './app.component'; -import { RouterTestingModule } from '@angular/router/testing'; import { provideMockStore } from '@ngrx/store/testing'; +import { provideRouter } from '@angular/router'; +import { provideLocationMocks } from '@angular/common/testing'; describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [RouterTestingModule, AppComponent], - providers: [provideMockStore()], + imports: [AppComponent], + providers: [ + provideMockStore(), + provideRouter([]), + provideLocationMocks(), + ], }).compileComponents(); }); diff --git a/projects/standalone-app/src/app/app.effects.ts b/projects/standalone-app/src/app/app.effects.ts index db83e2bcbe..eba323c582 100644 --- a/projects/standalone-app/src/app/app.effects.ts +++ b/projects/standalone-app/src/app/app.effects.ts @@ -1,15 +1,15 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { Actions, createEffect } from '@ngrx/effects'; import { tap } from 'rxjs'; @Injectable() export class AppEffects { + private actions$ = inject(Actions); + logger$ = createEffect( () => { return this.actions$.pipe(tap((action) => console.log(action))); }, { dispatch: false } ); - - constructor(private actions$: Actions) {} } From 30b7471c2dc3a0cc94af69ea6d691de17a040fa3 Mon Sep 17 00:00:00 2001 From: Rainer Hahnekamp Date: Mon, 16 Sep 2024 00:34:21 +0200 Subject: [PATCH 04/10] docs: fix tests --- .../login-form.component.spec.ts.snap | 394 +++++++++++++++--- .../login-page.component.spec.ts.snap | 6 +- .../app/auth/reducers/auth.reducer.spec.ts | 8 +- .../auth/reducers/login-page.reducer.spec.ts | 19 +- .../auth/services/auth-guard.service.spec.ts | 7 +- .../collection-page.component.spec.ts.snap | 4 +- .../find-book-page.component.spec.ts.snap | 10 +- .../selected-book-page.component.spec.ts.snap | 6 +- 8 files changed, 366 insertions(+), 88 deletions(-) diff --git a/projects/example-app/src/app/auth/components/__snapshots__/login-form.component.spec.ts.snap b/projects/example-app/src/app/auth/components/__snapshots__/login-form.component.spec.ts.snap index 061d31df28..b6f29bfbe1 100644 --- a/projects/example-app/src/app/auth/components/__snapshots__/login-form.component.spec.ts.snap +++ b/projects/example-app/src/app/auth/components/__snapshots__/login-form.component.spec.ts.snap @@ -5,45 +5,137 @@ exports[`Login Page should compile 1`] = ` form={[Function FormGroup]} submitted={[Function EventEmitter_]} > - - + + Login - + - - + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + - Login + + + Login + + + @@ -57,47 +149,133 @@ exports[`Login Page should disable the form if pending 1`] = ` form={[Function FormGroup]} submitted={[Function EventEmitter_]} > - - + + Login - + - - + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + - Login + + + Login + + + @@ -112,35 +290,111 @@ exports[`Login Page should display an error message if provided 1`] = ` form={[Function FormGroup]} submitted={[Function EventEmitter_]} > - - + + Login - + - - + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + - Login + + + Login + + + diff --git a/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap b/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap index dcfc5b501c..b90481a4b3 100644 --- a/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap +++ b/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap @@ -2,9 +2,9 @@ exports[`Login Page should compile 1`] = ` { it('should return the default state', () => { const action = {} as any; - const result = reducer(undefined, action); + const result = statusFeature.reducer(undefined, action); /** * Snapshot tests are a quick way to validate @@ -28,7 +28,7 @@ describe('AuthReducer', () => { const user = { name: 'test' } as User; const createAction = AuthApiActions.loginSuccess({ user }); - const result = reducer(fromAuth.initialState, createAction); + const result = statusFeature.reducer(fromAuth.initialState, createAction); expect(result).toMatchSnapshot(); }); @@ -41,7 +41,7 @@ describe('AuthReducer', () => { } as fromAuth.State; const createAction = AuthActions.logout(); - const result = reducer(initialState, createAction); + const result = statusFeature.reducer(initialState, createAction); expect(result).toMatchSnapshot(); }); diff --git a/projects/example-app/src/app/auth/reducers/login-page.reducer.spec.ts b/projects/example-app/src/app/auth/reducers/login-page.reducer.spec.ts index 781008a3f1..7f856a77d8 100644 --- a/projects/example-app/src/app/auth/reducers/login-page.reducer.spec.ts +++ b/projects/example-app/src/app/auth/reducers/login-page.reducer.spec.ts @@ -1,17 +1,17 @@ -import { reducer } from '@example-app/auth/reducers/login-page.reducer'; import * as fromLoginPage from '@example-app/auth/reducers/login-page.reducer'; import { LoginPageActions } from '@example-app/auth/actions/login-page.actions'; import { AuthApiActions } from '@example-app/auth/actions/auth-api.actions'; import { Credentials, User } from '@example-app/auth/models'; +import { loginPageFeature } from '@example-app/auth/reducers/login-page.reducer'; describe('LoginPageReducer', () => { describe('undefined action', () => { it('should return the default state', () => { const action = {} as any; - const result = reducer(undefined, action); + const result = loginPageFeature.reducer(undefined, action); expect(result).toMatchSnapshot(); }); @@ -22,7 +22,10 @@ describe('LoginPageReducer', () => { const user = { username: 'test' } as Credentials; const createAction = LoginPageActions.login({ credentials: user }); - const result = reducer(fromLoginPage.initialState, createAction); + const result = loginPageFeature.reducer( + fromLoginPage.initialState, + createAction + ); expect(result).toMatchSnapshot(); }); @@ -33,7 +36,10 @@ describe('LoginPageReducer', () => { const user = { name: 'test' } as User; const createAction = AuthApiActions.loginSuccess({ user }); - const result = reducer(fromLoginPage.initialState, createAction); + const result = loginPageFeature.reducer( + fromLoginPage.initialState, + createAction + ); expect(result).toMatchSnapshot(); }); @@ -44,7 +50,10 @@ describe('LoginPageReducer', () => { const error = 'login failed'; const createAction = AuthApiActions.loginFailure({ error }); - const result = reducer(fromLoginPage.initialState, createAction); + const result = loginPageFeature.reducer( + fromLoginPage.initialState, + createAction + ); expect(result).toMatchSnapshot(); }); diff --git a/projects/example-app/src/app/auth/services/auth-guard.service.spec.ts b/projects/example-app/src/app/auth/services/auth-guard.service.spec.ts index e97451e90b..508e6c2639 100644 --- a/projects/example-app/src/app/auth/services/auth-guard.service.spec.ts +++ b/projects/example-app/src/app/auth/services/auth-guard.service.spec.ts @@ -1,15 +1,14 @@ import { TestBed } from '@angular/core/testing'; -import { MemoizedSelector } from '@ngrx/store'; import { cold } from 'jasmine-marbles'; import { authGuard } from '@example-app/auth/services'; -import * as fromAuth from '@example-app/auth/reducers'; import { provideMockStore, MockStore } from '@ngrx/store/testing'; import { Observable } from 'rxjs'; +import { selectLoggedIn } from '@example-app/auth/reducers'; describe('Auth Guard', () => { let guard: Observable; let store: MockStore; - let loggedIn: MemoizedSelector; + let loggedIn: typeof selectLoggedIn; beforeEach(() => { TestBed.configureTestingModule({ @@ -18,7 +17,7 @@ describe('Auth Guard', () => { store = TestBed.inject(MockStore); guard = TestBed.runInInjectionContext(authGuard); - loggedIn = store.overrideSelector(fromAuth.selectLoggedIn, false); + loggedIn = store.overrideSelector(selectLoggedIn, false); }); it('should return false if the user state is not logged in', () => { diff --git a/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap b/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap index dd31673d71..c675f5ac0b 100644 --- a/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap +++ b/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap @@ -2,8 +2,8 @@ exports[`Collection Page should compile 1`] = ` From 9cfc91e2a25615c41f83995b3661272455ba6adb Mon Sep 17 00:00:00 2001 From: Rainer Hahnekamp Date: Mon, 16 Sep 2024 00:46:49 +0200 Subject: [PATCH 05/10] docs: move to new control flow syntax --- .../auth/components/login-form.component.ts | 7 ++-- .../books/components/book-detail.component.ts | 32 ++++++++----------- .../components/book-preview-list.component.ts | 7 ++-- .../components/book-preview.component.ts | 26 ++++++--------- .../books/components/book-search.component.ts | 7 ++-- .../app/core/components/nav-item.component.ts | 7 ++-- .../src/app/core/containers/app.component.ts | 21 +++++------- 7 files changed, 48 insertions(+), 59 deletions(-) diff --git a/projects/example-app/src/app/auth/components/login-form.component.ts b/projects/example-app/src/app/auth/components/login-form.component.ts index 269d298e86..320eafcae4 100644 --- a/projects/example-app/src/app/auth/components/login-form.component.ts +++ b/projects/example-app/src/app/auth/components/login-form.component.ts @@ -1,4 +1,3 @@ -import { NgIf } from '@angular/common'; import { Component, Input, Output, EventEmitter } from '@angular/core'; import { FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms'; import { Credentials } from '@example-app/auth/models'; @@ -7,7 +6,7 @@ import { MaterialModule } from '@example-app/material'; @Component({ standalone: true, selector: 'bc-login-form', - imports: [MaterialModule, ReactiveFormsModule, NgIf], + imports: [MaterialModule, ReactiveFormsModule], template: ` Login @@ -35,9 +34,11 @@ import { MaterialModule } from '@example-app/material';
- - + + + + + + + + + + + + + + +
- - + + + + + + + + + + + + + +
- Login + + + Login + + +
+ @if (errorMessage) { +
{{ errorMessage }}
{{ description | bcEllipsis }}
+ @@ -184,7 +191,7 @@ exports[`Login Page should disable the form if pending 1`] = ` + @@ -229,7 +239,7 @@ exports[`Login Page should disable the form if pending 1`] = ` { }); fixture = TestBed.createComponent(LoginFormComponent); + fixture.componentRef.setInput('pending', false); instance = fixture.componentInstance; }); @@ -38,7 +39,7 @@ describe('Login Page', () => { }); it('should disable the form if pending', () => { - instance.pending = true; + fixture.componentRef.setInput('pending', true); fixture.detectChanges(); @@ -46,7 +47,7 @@ describe('Login Page', () => { }); it('should display an error message if provided', () => { - instance.errorMessage = 'Invalid credentials'; + fixture.componentRef.setInput('errorMessage', 'Invalid credentials'); fixture.detectChanges(); diff --git a/projects/example-app/src/app/auth/components/login-form.component.ts b/projects/example-app/src/app/auth/components/login-form.component.ts index 00d36064c8..13078227f9 100644 --- a/projects/example-app/src/app/auth/components/login-form.component.ts +++ b/projects/example-app/src/app/auth/components/login-form.component.ts @@ -1,4 +1,13 @@ -import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { + Component, + Input, + Output, + EventEmitter, + input, + untracked, + effect, + output, +} from '@angular/core'; import { FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms'; import { Credentials } from '@example-app/auth/models'; import { MaterialModule } from '@example-app/material'; @@ -36,9 +45,9 @@ import { MaterialModule } from '@example-app/material'; - @if (errorMessage) { + @if (errorMessage()) { - {{ errorMessage }} + {{ errorMessage() }} } @@ -87,18 +96,17 @@ import { MaterialModule } from '@example-app/material'; ], }) export class LoginFormComponent { - @Input() - set pending(isPending: boolean) { - if (isPending) { - this.form.disable(); - } else { - this.form.enable(); - } - } + readonly pending = input.required(); + + private readonly pendingEffect = effect(() => { + const pending = this.pending(); + + untracked(() => (pending ? this.form.disable() : this.form.enable())); + }); - @Input() errorMessage: string | null = null; + readonly errorMessage = input(null); - @Output() submitted = new EventEmitter(); + submitted = output(); protected readonly form: FormGroup = new FormGroup({ username: new FormControl('ngrx'), diff --git a/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap b/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap index 99553aa0f8..c659122c08 100644 --- a/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap +++ b/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap @@ -2,9 +2,9 @@ exports[`Login Page should compile 1`] = ` { }); it('should dispatch a login event on submit', () => { - const credentials: any = {}; + const credentials = { username: 'ngrx', password: 'rocks' }; const action = LoginPageActions.login({ credentials }); instance.onSubmit(credentials); diff --git a/projects/example-app/src/app/auth/containers/login-page.component.ts b/projects/example-app/src/app/auth/containers/login-page.component.ts index 6922102b5c..f1d0ff4f05 100644 --- a/projects/example-app/src/app/auth/containers/login-page.component.ts +++ b/projects/example-app/src/app/auth/containers/login-page.component.ts @@ -13,20 +13,21 @@ import { AsyncPipe } from '@angular/common'; template: ` `, - styles: [], }) export class LoginPageComponent { - private store = inject(Store); + private readonly store = inject(Store); - protected readonly pending$ = this.store.select( + protected readonly pending = this.store.selectSignal( fromAuth.selectLoginPagePending ); - protected readonly error$ = this.store.select(fromAuth.selectLoginPageError); + protected readonly error = this.store.selectSignal( + fromAuth.selectLoginPageError + ); onSubmit(credentials: Credentials) { this.store.dispatch(LoginPageActions.login({ credentials })); diff --git a/projects/example-app/src/app/books/books.routes.ts b/projects/example-app/src/app/books/books.routes.ts index ce2f4c64bc..03e99402bf 100644 --- a/projects/example-app/src/app/books/books.routes.ts +++ b/projects/example-app/src/app/books/books.routes.ts @@ -8,7 +8,7 @@ import { import { bookExistsGuard } from '@example-app/books/guards'; import { provideBooks } from '@example-app/books/reducers'; -export const BOOKS_ROUTES: Routes = [ +export default [ { path: '', providers: [provideBooks()], @@ -31,4 +31,4 @@ export const BOOKS_ROUTES: Routes = [ }, ], }, -]; +] satisfies Routes; diff --git a/projects/example-app/src/app/books/components/book-authors.component.ts b/projects/example-app/src/app/books/components/book-authors.component.ts index 363b1a5dae..f2e48981d4 100644 --- a/projects/example-app/src/app/books/components/book-authors.component.ts +++ b/projects/example-app/src/app/books/components/book-authors.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, computed, input, Input } from '@angular/core'; import { Book } from '@example-app/books/models'; import { MaterialModule } from '@example-app/material'; @@ -11,7 +11,7 @@ import { AddCommasPipe } from '@example-app/shared/pipes/add-commas.pipe'; template: ` Written By: - {{ authors | bcAddCommas }} + {{ authors() | bcAddCommas }} `, styles: [ @@ -23,9 +23,9 @@ import { AddCommasPipe } from '@example-app/shared/pipes/add-commas.pipe'; ], }) export class BookAuthorsComponent { - @Input() book: Book | undefined = undefined; + book = input(); - get authors() { - return this.book?.volumeInfo.authors || []; - } + protected readonly authors = computed( + () => this.book()?.volumeInfo.authors || [] + ); } diff --git a/projects/example-app/src/app/books/components/book-detail.component.ts b/projects/example-app/src/app/books/components/book-detail.component.ts index 3a9918c0d5..eb49b10f02 100644 --- a/projects/example-app/src/app/books/components/book-detail.component.ts +++ b/projects/example-app/src/app/books/components/book-detail.component.ts @@ -1,4 +1,12 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { + Component, + computed, + EventEmitter, + input, + Input, + output, + Output, +} from '@angular/core'; import { Book } from '@example-app/books/models'; import { MaterialModule } from '@example-app/material'; @@ -9,35 +17,34 @@ import { BookAuthorsComponent } from './book-authors.component'; selector: 'bc-book-detail', imports: [MaterialModule, BookAuthorsComponent], template: ` - @if (book) { + @let value = book(); @let volumeInfo = book().volumeInfo; - {{ title }} - @if (subtitle) { - {{ subtitle }} - } @if (thumbnail) { - + {{ volumeInfo.title }} + @if (volumeInfo.subtitle) { + {{ volumeInfo.subtitle }} + } @if (thumbnail()) { + } - + - + - @if (inCollection) { - + @if (inCollection()) { + Remove Book from Collection - } @if (!inCollection) { - + } @else { + Add Book to Collection } - } `, styles: [ ` @@ -71,38 +78,20 @@ import { BookAuthorsComponent } from './book-authors.component'; }) export class BookDetailComponent { /** - * Presentational components receive data through @Input() and communicate events - * through @Output() but generally maintain no internal state of their + * Presentational components receive data through input and communicate events + * through output but generally maintain no internal state of their * own. All decisions are delegated to 'container', or 'smart' * components before data updates flow back down. * * More on 'smart' and 'presentational' components: https://gist.github.com/btroncone/a6e4347326749f938510#utilizing-container-components */ - @Input() book: Book | undefined = undefined; - @Input() inCollection = false; - @Output() add = new EventEmitter(); - @Output() remove = new EventEmitter(); + readonly book = input.required(); + readonly inCollection = input(false); - /** - * Tip: Utilize getters to keep templates clean - */ - get id() { - return this.book?.id; - } - - get title() { - return this.book?.volumeInfo.title; - } - - get subtitle() { - return this.book?.volumeInfo.subtitle; - } - - get description() { - return this.book?.volumeInfo.description; - } + readonly add = output(); + readonly remove = output(); - get thumbnail() { - return this.book?.volumeInfo.imageLinks.smallThumbnail.replace('http:', ''); - } + protected readonly thumbnail = computed(() => + this.book().volumeInfo.imageLinks.smallThumbnail.replace('http:', '') + ); } diff --git a/projects/example-app/src/app/books/components/book-preview-list.component.ts b/projects/example-app/src/app/books/components/book-preview-list.component.ts index 5884a5c4bc..63f36c9d09 100644 --- a/projects/example-app/src/app/books/components/book-preview-list.component.ts +++ b/projects/example-app/src/app/books/components/book-preview-list.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, input, Input } from '@angular/core'; import { Book } from '@example-app/books/models'; import { BookPreviewComponent } from './book-preview.component'; @@ -8,7 +8,7 @@ import { BookPreviewComponent } from './book-preview.component'; selector: 'bc-book-preview-list', imports: [BookPreviewComponent], template: ` - @for (book of books; track book) { + @for (book of books(); track book) { } `, @@ -23,5 +23,5 @@ import { BookPreviewComponent } from './book-preview.component'; ], }) export class BookPreviewListComponent { - @Input() books = new Array(); + readonly books = input(new Array()); } diff --git a/projects/example-app/src/app/books/components/book-search.component.ts b/projects/example-app/src/app/books/components/book-search.component.ts index fe04c89b8d..662d461d2d 100644 --- a/projects/example-app/src/app/books/components/book-search.component.ts +++ b/projects/example-app/src/app/books/components/book-search.component.ts @@ -1,4 +1,11 @@ -import { Component, Output, Input, EventEmitter } from '@angular/core'; +import { + Component, + Output, + Input, + EventEmitter, + output, + input, +} from '@angular/core'; import { MaterialModule } from '@example-app/material'; @Component({ @@ -13,19 +20,19 @@ import { MaterialModule } from '@example-app/material'; - @if (error) { - {{ error }} + @if (error()) { + {{ error() }} } @@ -67,10 +74,10 @@ import { MaterialModule } from '@example-app/material'; ], }) export class BookSearchComponent { - @Input() query = ''; - @Input() searching = false; - @Input() error = ''; - @Output() search = new EventEmitter(); + readonly query = input(''); + readonly searching = input(false); + readonly error = input(''); + protected search = output(); onSearch(event: KeyboardEvent): void { this.search.emit((event.target as HTMLInputElement).value); diff --git a/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap b/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap index c675f5ac0b..a7deede9c7 100644 --- a/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap +++ b/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap @@ -2,8 +2,8 @@ exports[`Collection Page should compile 1`] = ` - - - - + `; diff --git a/projects/example-app/src/app/books/containers/collection-page.component.ts b/projects/example-app/src/app/books/containers/collection-page.component.ts index 68a2de7d60..95cf7df4e5 100644 --- a/projects/example-app/src/app/books/containers/collection-page.component.ts +++ b/projects/example-app/src/app/books/containers/collection-page.component.ts @@ -6,26 +6,23 @@ import { } from '@angular/core'; import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; import { CollectionPageActions } from '@example-app/books/actions/collection-page.actions'; -import { Book } from '@example-app/books/models'; import * as fromBooks from '@example-app/books/reducers'; import { MaterialModule } from '@example-app/material'; import { BookPreviewListComponent } from '../components'; -import { AsyncPipe } from '@angular/common'; @Component({ standalone: true, selector: 'bc-collection-page', changeDetection: ChangeDetectionStrategy.OnPush, - imports: [MaterialModule, BookPreviewListComponent, AsyncPipe], + imports: [MaterialModule, BookPreviewListComponent], template: ` My Collection - + `, /** * Container components are permitted to have just enough styles @@ -46,7 +43,9 @@ import { AsyncPipe } from '@angular/common'; export class CollectionPageComponent implements OnInit { private readonly store = inject(Store); - protected books$ = this.store.select(fromBooks.selectBookCollection); + protected readonly books = this.store.selectSignal( + fromBooks.selectBookCollection + ); ngOnInit() { this.store.dispatch(CollectionPageActions.enter()); diff --git a/projects/example-app/src/app/books/containers/find-book-page.component.ts b/projects/example-app/src/app/books/containers/find-book-page.component.ts index 2b283b9ecd..ac8420d66a 100644 --- a/projects/example-app/src/app/books/containers/find-book-page.component.ts +++ b/projects/example-app/src/app/books/containers/find-book-page.component.ts @@ -1,7 +1,6 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { Store } from '@ngrx/store'; -import { take } from 'rxjs/operators'; import { FindBookPageActions } from '@example-app/books/actions/find-book-page.actions'; import * as fromBooks from '@example-app/books/reducers'; @@ -15,26 +14,30 @@ import { AsyncPipe } from '@angular/common'; imports: [BookSearchComponent, AsyncPipe, BookPreviewListComponent], template: ` - + `, }) export class FindBookPageComponent { - private store = inject(Store); + private readonly store = inject(Store); - protected readonly searchQuery$ = this.store - .select(fromBooks.selectSearchQuery) - .pipe(take(1)); - protected readonly books$ = this.store.select(fromBooks.selectSearchResults); - protected readonly loading$ = this.store.select( + protected readonly searchQuery = this.store.selectSignal( + fromBooks.selectSearchQuery + ); + protected readonly books = this.store.selectSignal( + fromBooks.selectSearchResults + ); + protected readonly loading = this.store.selectSignal( fromBooks.selectSearchLoading ); - protected readonly error$ = this.store.select(fromBooks.selectSearchError); + protected readonly error = this.store.selectSignal( + fromBooks.selectSearchError + ); search(query: string) { this.store.dispatch(FindBookPageActions.searchBooks({ query })); diff --git a/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts b/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts index a5c93a5232..87a3be6c32 100644 --- a/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts +++ b/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts @@ -26,12 +26,6 @@ describe('Selected Book Page', () => { jest.spyOn(store, 'dispatch'); }); - it('should compile', () => { - fixture.detectChanges(); - - expect(fixture).toMatchSnapshot(); - }); - it('should dispatch a collection.AddBook action when addToCollection is called', () => { const $event: Book = generateMockBook(); const action = SelectedBookPageActions.addBook({ book: $event }); diff --git a/projects/example-app/src/app/books/containers/selected-book-page.component.ts b/projects/example-app/src/app/books/containers/selected-book-page.component.ts index 9f1fcde8ed..4ecef3e8da 100644 --- a/projects/example-app/src/app/books/containers/selected-book-page.component.ts +++ b/projects/example-app/src/app/books/containers/selected-book-page.component.ts @@ -1,36 +1,36 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; import { SelectedBookPageActions } from '@example-app/books/actions/selected-book-page.actions'; import { Book } from '@example-app/books/models'; import * as fromBooks from '@example-app/books/reducers'; import { BookDetailComponent } from '../components'; -import { AsyncPipe } from '@angular/common'; @Component({ standalone: true, selector: 'bc-selected-book-page', changeDetection: ChangeDetectionStrategy.OnPush, - imports: [BookDetailComponent, AsyncPipe], + imports: [BookDetailComponent], template: ` + @let value = book(); @if (value) { + } `, }) export class SelectedBookPageComponent { private readonly store = inject(Store); - protected readonly book$ = this.store.select( + protected readonly book = this.store.selectSignal( fromBooks.selectSelectedBook - ) as Observable; - protected readonly isSelectedBookInCollection$ = this.store.select( + ); + protected readonly isSelectedBookInCollection = this.store.selectSignal( fromBooks.isSelectedBookInCollection ); diff --git a/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts b/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts index e1ace22b50..b5e6a0a357 100644 --- a/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts +++ b/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts @@ -1,46 +1,54 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; -import { provideMockStore, MockStore } from '@ngrx/store/testing'; -import { BehaviorSubject } from 'rxjs'; - import { ViewBookPageComponent } from '@example-app/books/containers'; import { ViewBookPageActions } from '@example-app/books/actions/view-book-page.actions'; +import { Store } from '@ngrx/store'; +import { Component } from '@angular/core'; -describe('View Book Page', () => { - let fixture: ComponentFixture; - let store: MockStore; - let route: ActivatedRoute; +@Component({ + selector: 'bc-selected-book-page', + template: '', + standalone: true, +}) +class MockSelectedBookPageComponent {} - beforeEach(() => { +describe('View Book Page', () => { + const setup = () => { + const store = { + dispatch: jest.fn(), + selectSignal: jest.fn(), + }; + TestBed.overrideComponent(ViewBookPageComponent, { + set: { imports: [MockSelectedBookPageComponent] }, + }); TestBed.configureTestingModule({ imports: [ViewBookPageComponent], - providers: [ - { - provide: ActivatedRoute, - useValue: { params: new BehaviorSubject({}) }, - }, - provideMockStore(), - ], + providers: [{ provide: Store, useValue: store }], }); - fixture = TestBed.createComponent(ViewBookPageComponent); - store = TestBed.inject(MockStore); - route = TestBed.inject(ActivatedRoute); + const fixture = TestBed.createComponent(ViewBookPageComponent); - jest.spyOn(store, 'dispatch'); - }); + const dispatchSpy = store.dispatch; + const selectSpy = store.selectSignal; + + return { store, fixture }; + }; it('should compile', () => { + const { fixture } = setup(); + fixture.componentRef.setInput('id', '2'); fixture.detectChanges(); expect(fixture).toMatchSnapshot(); }); - it('should dispatch a book.Select action on init', () => { - const action = ViewBookPageActions.selectBook({ id: '2' }); + it('should dispatch a book. Select action on init', () => { + const { fixture, store } = setup(); - (route.params as BehaviorSubject).next({ id: '2' }); + const action = ViewBookPageActions.selectBook({ id: '2' }); + fixture.componentRef.setInput('id', '2'); + fixture.detectChanges(); expect(store.dispatch).toHaveBeenLastCalledWith(action); }); diff --git a/projects/example-app/src/app/books/containers/view-book-page.component.ts b/projects/example-app/src/app/books/containers/view-book-page.component.ts index df288f1917..a4c6ef514a 100644 --- a/projects/example-app/src/app/books/containers/view-book-page.component.ts +++ b/projects/example-app/src/app/books/containers/view-book-page.component.ts @@ -3,10 +3,11 @@ import { OnDestroy, ChangeDetectionStrategy, inject, + input, + effect, + untracked, } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; import { Store } from '@ngrx/store'; -import { map } from 'rxjs/operators'; import { ViewBookPageActions } from '@example-app/books/actions/view-book-page.actions'; import { SelectedBookPageComponent } from './selected-book-page.component'; @@ -28,16 +29,16 @@ import { SelectedBookPageComponent } from './selected-book-page.component'; imports: [SelectedBookPageComponent], template: ` `, }) -export class ViewBookPageComponent implements OnDestroy { +export class ViewBookPageComponent { private readonly store = inject(Store); - private readonly actionsSubscription = inject(ActivatedRoute) - .params.pipe( - map((params) => ViewBookPageActions.selectBook({ id: params.id })) - ) - .subscribe((action) => this.store.dispatch(action)); + readonly id = input.required(); - ngOnDestroy() { - this.actionsSubscription.unsubscribe(); - } + readonly selectBookEffect = effect(() => { + const id = this.id(); + + untracked(() => { + this.store.dispatch(ViewBookPageActions.selectBook({ id })); + }); + }); } From 6312a4cbf2e4dd88dfab0dce317d4b2888d57a97 Mon Sep 17 00:00:00 2001 From: Rainer Hahnekamp Date: Sun, 29 Sep 2024 22:42:19 +0200 Subject: [PATCH 10/10] feat: revert standalone-app changes --- projects/standalone-app/src/app/app.component.spec.ts | 11 +++-------- projects/standalone-app/src/app/app.effects.ts | 6 +++--- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/projects/standalone-app/src/app/app.component.spec.ts b/projects/standalone-app/src/app/app.component.spec.ts index ced3e561c8..e636a11368 100644 --- a/projects/standalone-app/src/app/app.component.spec.ts +++ b/projects/standalone-app/src/app/app.component.spec.ts @@ -1,18 +1,13 @@ import { TestBed } from '@angular/core/testing'; import { AppComponent } from './app.component'; +import { RouterTestingModule } from '@angular/router/testing'; import { provideMockStore } from '@ngrx/store/testing'; -import { provideRouter } from '@angular/router'; -import { provideLocationMocks } from '@angular/common/testing'; describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AppComponent], - providers: [ - provideMockStore(), - provideRouter([]), - provideLocationMocks(), - ], + imports: [RouterTestingModule, AppComponent], + providers: [provideMockStore()], }).compileComponents(); }); diff --git a/projects/standalone-app/src/app/app.effects.ts b/projects/standalone-app/src/app/app.effects.ts index eba323c582..db83e2bcbe 100644 --- a/projects/standalone-app/src/app/app.effects.ts +++ b/projects/standalone-app/src/app/app.effects.ts @@ -1,15 +1,15 @@ -import { inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Actions, createEffect } from '@ngrx/effects'; import { tap } from 'rxjs'; @Injectable() export class AppEffects { - private actions$ = inject(Actions); - logger$ = createEffect( () => { return this.actions$.pipe(tap((action) => console.log(action))); }, { dispatch: false } ); + + constructor(private actions$: Actions) {} }
+ @@ -229,7 +239,7 @@ exports[`Login Page should disable the form if pending 1`] = ` { }); fixture = TestBed.createComponent(LoginFormComponent); + fixture.componentRef.setInput('pending', false); instance = fixture.componentInstance; }); @@ -38,7 +39,7 @@ describe('Login Page', () => { }); it('should disable the form if pending', () => { - instance.pending = true; + fixture.componentRef.setInput('pending', true); fixture.detectChanges(); @@ -46,7 +47,7 @@ describe('Login Page', () => { }); it('should display an error message if provided', () => { - instance.errorMessage = 'Invalid credentials'; + fixture.componentRef.setInput('errorMessage', 'Invalid credentials'); fixture.detectChanges(); diff --git a/projects/example-app/src/app/auth/components/login-form.component.ts b/projects/example-app/src/app/auth/components/login-form.component.ts index 00d36064c8..13078227f9 100644 --- a/projects/example-app/src/app/auth/components/login-form.component.ts +++ b/projects/example-app/src/app/auth/components/login-form.component.ts @@ -1,4 +1,13 @@ -import { Component, Input, Output, EventEmitter } from '@angular/core'; +import { + Component, + Input, + Output, + EventEmitter, + input, + untracked, + effect, + output, +} from '@angular/core'; import { FormGroup, FormControl, ReactiveFormsModule } from '@angular/forms'; import { Credentials } from '@example-app/auth/models'; import { MaterialModule } from '@example-app/material'; @@ -36,9 +45,9 @@ import { MaterialModule } from '@example-app/material'; - @if (errorMessage) { + @if (errorMessage()) { - {{ errorMessage }} + {{ errorMessage() }} } @@ -87,18 +96,17 @@ import { MaterialModule } from '@example-app/material'; ], }) export class LoginFormComponent { - @Input() - set pending(isPending: boolean) { - if (isPending) { - this.form.disable(); - } else { - this.form.enable(); - } - } + readonly pending = input.required(); + + private readonly pendingEffect = effect(() => { + const pending = this.pending(); + + untracked(() => (pending ? this.form.disable() : this.form.enable())); + }); - @Input() errorMessage: string | null = null; + readonly errorMessage = input(null); - @Output() submitted = new EventEmitter(); + submitted = output(); protected readonly form: FormGroup = new FormGroup({ username: new FormControl('ngrx'), diff --git a/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap b/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap index 99553aa0f8..c659122c08 100644 --- a/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap +++ b/projects/example-app/src/app/auth/containers/__snapshots__/login-page.component.spec.ts.snap @@ -2,9 +2,9 @@ exports[`Login Page should compile 1`] = ` { }); it('should dispatch a login event on submit', () => { - const credentials: any = {}; + const credentials = { username: 'ngrx', password: 'rocks' }; const action = LoginPageActions.login({ credentials }); instance.onSubmit(credentials); diff --git a/projects/example-app/src/app/auth/containers/login-page.component.ts b/projects/example-app/src/app/auth/containers/login-page.component.ts index 6922102b5c..f1d0ff4f05 100644 --- a/projects/example-app/src/app/auth/containers/login-page.component.ts +++ b/projects/example-app/src/app/auth/containers/login-page.component.ts @@ -13,20 +13,21 @@ import { AsyncPipe } from '@angular/common'; template: ` `, - styles: [], }) export class LoginPageComponent { - private store = inject(Store); + private readonly store = inject(Store); - protected readonly pending$ = this.store.select( + protected readonly pending = this.store.selectSignal( fromAuth.selectLoginPagePending ); - protected readonly error$ = this.store.select(fromAuth.selectLoginPageError); + protected readonly error = this.store.selectSignal( + fromAuth.selectLoginPageError + ); onSubmit(credentials: Credentials) { this.store.dispatch(LoginPageActions.login({ credentials })); diff --git a/projects/example-app/src/app/books/books.routes.ts b/projects/example-app/src/app/books/books.routes.ts index ce2f4c64bc..03e99402bf 100644 --- a/projects/example-app/src/app/books/books.routes.ts +++ b/projects/example-app/src/app/books/books.routes.ts @@ -8,7 +8,7 @@ import { import { bookExistsGuard } from '@example-app/books/guards'; import { provideBooks } from '@example-app/books/reducers'; -export const BOOKS_ROUTES: Routes = [ +export default [ { path: '', providers: [provideBooks()], @@ -31,4 +31,4 @@ export const BOOKS_ROUTES: Routes = [ }, ], }, -]; +] satisfies Routes; diff --git a/projects/example-app/src/app/books/components/book-authors.component.ts b/projects/example-app/src/app/books/components/book-authors.component.ts index 363b1a5dae..f2e48981d4 100644 --- a/projects/example-app/src/app/books/components/book-authors.component.ts +++ b/projects/example-app/src/app/books/components/book-authors.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, computed, input, Input } from '@angular/core'; import { Book } from '@example-app/books/models'; import { MaterialModule } from '@example-app/material'; @@ -11,7 +11,7 @@ import { AddCommasPipe } from '@example-app/shared/pipes/add-commas.pipe'; template: ` Written By: - {{ authors | bcAddCommas }} + {{ authors() | bcAddCommas }} `, styles: [ @@ -23,9 +23,9 @@ import { AddCommasPipe } from '@example-app/shared/pipes/add-commas.pipe'; ], }) export class BookAuthorsComponent { - @Input() book: Book | undefined = undefined; + book = input(); - get authors() { - return this.book?.volumeInfo.authors || []; - } + protected readonly authors = computed( + () => this.book()?.volumeInfo.authors || [] + ); } diff --git a/projects/example-app/src/app/books/components/book-detail.component.ts b/projects/example-app/src/app/books/components/book-detail.component.ts index 3a9918c0d5..eb49b10f02 100644 --- a/projects/example-app/src/app/books/components/book-detail.component.ts +++ b/projects/example-app/src/app/books/components/book-detail.component.ts @@ -1,4 +1,12 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { + Component, + computed, + EventEmitter, + input, + Input, + output, + Output, +} from '@angular/core'; import { Book } from '@example-app/books/models'; import { MaterialModule } from '@example-app/material'; @@ -9,35 +17,34 @@ import { BookAuthorsComponent } from './book-authors.component'; selector: 'bc-book-detail', imports: [MaterialModule, BookAuthorsComponent], template: ` - @if (book) { + @let value = book(); @let volumeInfo = book().volumeInfo; - {{ title }} - @if (subtitle) { - {{ subtitle }} - } @if (thumbnail) { - + {{ volumeInfo.title }} + @if (volumeInfo.subtitle) { + {{ volumeInfo.subtitle }} + } @if (thumbnail()) { + } - + - + - @if (inCollection) { - + @if (inCollection()) { + Remove Book from Collection - } @if (!inCollection) { - + } @else { + Add Book to Collection } - } `, styles: [ ` @@ -71,38 +78,20 @@ import { BookAuthorsComponent } from './book-authors.component'; }) export class BookDetailComponent { /** - * Presentational components receive data through @Input() and communicate events - * through @Output() but generally maintain no internal state of their + * Presentational components receive data through input and communicate events + * through output but generally maintain no internal state of their * own. All decisions are delegated to 'container', or 'smart' * components before data updates flow back down. * * More on 'smart' and 'presentational' components: https://gist.github.com/btroncone/a6e4347326749f938510#utilizing-container-components */ - @Input() book: Book | undefined = undefined; - @Input() inCollection = false; - @Output() add = new EventEmitter(); - @Output() remove = new EventEmitter(); + readonly book = input.required(); + readonly inCollection = input(false); - /** - * Tip: Utilize getters to keep templates clean - */ - get id() { - return this.book?.id; - } - - get title() { - return this.book?.volumeInfo.title; - } - - get subtitle() { - return this.book?.volumeInfo.subtitle; - } - - get description() { - return this.book?.volumeInfo.description; - } + readonly add = output(); + readonly remove = output(); - get thumbnail() { - return this.book?.volumeInfo.imageLinks.smallThumbnail.replace('http:', ''); - } + protected readonly thumbnail = computed(() => + this.book().volumeInfo.imageLinks.smallThumbnail.replace('http:', '') + ); } diff --git a/projects/example-app/src/app/books/components/book-preview-list.component.ts b/projects/example-app/src/app/books/components/book-preview-list.component.ts index 5884a5c4bc..63f36c9d09 100644 --- a/projects/example-app/src/app/books/components/book-preview-list.component.ts +++ b/projects/example-app/src/app/books/components/book-preview-list.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, input, Input } from '@angular/core'; import { Book } from '@example-app/books/models'; import { BookPreviewComponent } from './book-preview.component'; @@ -8,7 +8,7 @@ import { BookPreviewComponent } from './book-preview.component'; selector: 'bc-book-preview-list', imports: [BookPreviewComponent], template: ` - @for (book of books; track book) { + @for (book of books(); track book) { } `, @@ -23,5 +23,5 @@ import { BookPreviewComponent } from './book-preview.component'; ], }) export class BookPreviewListComponent { - @Input() books = new Array(); + readonly books = input(new Array()); } diff --git a/projects/example-app/src/app/books/components/book-search.component.ts b/projects/example-app/src/app/books/components/book-search.component.ts index fe04c89b8d..662d461d2d 100644 --- a/projects/example-app/src/app/books/components/book-search.component.ts +++ b/projects/example-app/src/app/books/components/book-search.component.ts @@ -1,4 +1,11 @@ -import { Component, Output, Input, EventEmitter } from '@angular/core'; +import { + Component, + Output, + Input, + EventEmitter, + output, + input, +} from '@angular/core'; import { MaterialModule } from '@example-app/material'; @Component({ @@ -13,19 +20,19 @@ import { MaterialModule } from '@example-app/material'; - @if (error) { - {{ error }} + @if (error()) { + {{ error() }} } @@ -67,10 +74,10 @@ import { MaterialModule } from '@example-app/material'; ], }) export class BookSearchComponent { - @Input() query = ''; - @Input() searching = false; - @Input() error = ''; - @Output() search = new EventEmitter(); + readonly query = input(''); + readonly searching = input(false); + readonly error = input(''); + protected search = output(); onSearch(event: KeyboardEvent): void { this.search.emit((event.target as HTMLInputElement).value); diff --git a/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap b/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap index c675f5ac0b..a7deede9c7 100644 --- a/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap +++ b/projects/example-app/src/app/books/containers/__snapshots__/collection-page.component.spec.ts.snap @@ -2,8 +2,8 @@ exports[`Collection Page should compile 1`] = ` - - - - + `; diff --git a/projects/example-app/src/app/books/containers/collection-page.component.ts b/projects/example-app/src/app/books/containers/collection-page.component.ts index 68a2de7d60..95cf7df4e5 100644 --- a/projects/example-app/src/app/books/containers/collection-page.component.ts +++ b/projects/example-app/src/app/books/containers/collection-page.component.ts @@ -6,26 +6,23 @@ import { } from '@angular/core'; import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; import { CollectionPageActions } from '@example-app/books/actions/collection-page.actions'; -import { Book } from '@example-app/books/models'; import * as fromBooks from '@example-app/books/reducers'; import { MaterialModule } from '@example-app/material'; import { BookPreviewListComponent } from '../components'; -import { AsyncPipe } from '@angular/common'; @Component({ standalone: true, selector: 'bc-collection-page', changeDetection: ChangeDetectionStrategy.OnPush, - imports: [MaterialModule, BookPreviewListComponent, AsyncPipe], + imports: [MaterialModule, BookPreviewListComponent], template: ` My Collection - + `, /** * Container components are permitted to have just enough styles @@ -46,7 +43,9 @@ import { AsyncPipe } from '@angular/common'; export class CollectionPageComponent implements OnInit { private readonly store = inject(Store); - protected books$ = this.store.select(fromBooks.selectBookCollection); + protected readonly books = this.store.selectSignal( + fromBooks.selectBookCollection + ); ngOnInit() { this.store.dispatch(CollectionPageActions.enter()); diff --git a/projects/example-app/src/app/books/containers/find-book-page.component.ts b/projects/example-app/src/app/books/containers/find-book-page.component.ts index 2b283b9ecd..ac8420d66a 100644 --- a/projects/example-app/src/app/books/containers/find-book-page.component.ts +++ b/projects/example-app/src/app/books/containers/find-book-page.component.ts @@ -1,7 +1,6 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { Store } from '@ngrx/store'; -import { take } from 'rxjs/operators'; import { FindBookPageActions } from '@example-app/books/actions/find-book-page.actions'; import * as fromBooks from '@example-app/books/reducers'; @@ -15,26 +14,30 @@ import { AsyncPipe } from '@angular/common'; imports: [BookSearchComponent, AsyncPipe, BookPreviewListComponent], template: ` - + `, }) export class FindBookPageComponent { - private store = inject(Store); + private readonly store = inject(Store); - protected readonly searchQuery$ = this.store - .select(fromBooks.selectSearchQuery) - .pipe(take(1)); - protected readonly books$ = this.store.select(fromBooks.selectSearchResults); - protected readonly loading$ = this.store.select( + protected readonly searchQuery = this.store.selectSignal( + fromBooks.selectSearchQuery + ); + protected readonly books = this.store.selectSignal( + fromBooks.selectSearchResults + ); + protected readonly loading = this.store.selectSignal( fromBooks.selectSearchLoading ); - protected readonly error$ = this.store.select(fromBooks.selectSearchError); + protected readonly error = this.store.selectSignal( + fromBooks.selectSearchError + ); search(query: string) { this.store.dispatch(FindBookPageActions.searchBooks({ query })); diff --git a/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts b/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts index a5c93a5232..87a3be6c32 100644 --- a/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts +++ b/projects/example-app/src/app/books/containers/selected-book-page.component.spec.ts @@ -26,12 +26,6 @@ describe('Selected Book Page', () => { jest.spyOn(store, 'dispatch'); }); - it('should compile', () => { - fixture.detectChanges(); - - expect(fixture).toMatchSnapshot(); - }); - it('should dispatch a collection.AddBook action when addToCollection is called', () => { const $event: Book = generateMockBook(); const action = SelectedBookPageActions.addBook({ book: $event }); diff --git a/projects/example-app/src/app/books/containers/selected-book-page.component.ts b/projects/example-app/src/app/books/containers/selected-book-page.component.ts index 9f1fcde8ed..4ecef3e8da 100644 --- a/projects/example-app/src/app/books/containers/selected-book-page.component.ts +++ b/projects/example-app/src/app/books/containers/selected-book-page.component.ts @@ -1,36 +1,36 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; import { SelectedBookPageActions } from '@example-app/books/actions/selected-book-page.actions'; import { Book } from '@example-app/books/models'; import * as fromBooks from '@example-app/books/reducers'; import { BookDetailComponent } from '../components'; -import { AsyncPipe } from '@angular/common'; @Component({ standalone: true, selector: 'bc-selected-book-page', changeDetection: ChangeDetectionStrategy.OnPush, - imports: [BookDetailComponent, AsyncPipe], + imports: [BookDetailComponent], template: ` + @let value = book(); @if (value) { + } `, }) export class SelectedBookPageComponent { private readonly store = inject(Store); - protected readonly book$ = this.store.select( + protected readonly book = this.store.selectSignal( fromBooks.selectSelectedBook - ) as Observable; - protected readonly isSelectedBookInCollection$ = this.store.select( + ); + protected readonly isSelectedBookInCollection = this.store.selectSignal( fromBooks.isSelectedBookInCollection ); diff --git a/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts b/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts index e1ace22b50..b5e6a0a357 100644 --- a/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts +++ b/projects/example-app/src/app/books/containers/view-book-page.component.spec.ts @@ -1,46 +1,54 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; -import { provideMockStore, MockStore } from '@ngrx/store/testing'; -import { BehaviorSubject } from 'rxjs'; - import { ViewBookPageComponent } from '@example-app/books/containers'; import { ViewBookPageActions } from '@example-app/books/actions/view-book-page.actions'; +import { Store } from '@ngrx/store'; +import { Component } from '@angular/core'; -describe('View Book Page', () => { - let fixture: ComponentFixture; - let store: MockStore; - let route: ActivatedRoute; +@Component({ + selector: 'bc-selected-book-page', + template: '', + standalone: true, +}) +class MockSelectedBookPageComponent {} - beforeEach(() => { +describe('View Book Page', () => { + const setup = () => { + const store = { + dispatch: jest.fn(), + selectSignal: jest.fn(), + }; + TestBed.overrideComponent(ViewBookPageComponent, { + set: { imports: [MockSelectedBookPageComponent] }, + }); TestBed.configureTestingModule({ imports: [ViewBookPageComponent], - providers: [ - { - provide: ActivatedRoute, - useValue: { params: new BehaviorSubject({}) }, - }, - provideMockStore(), - ], + providers: [{ provide: Store, useValue: store }], }); - fixture = TestBed.createComponent(ViewBookPageComponent); - store = TestBed.inject(MockStore); - route = TestBed.inject(ActivatedRoute); + const fixture = TestBed.createComponent(ViewBookPageComponent); - jest.spyOn(store, 'dispatch'); - }); + const dispatchSpy = store.dispatch; + const selectSpy = store.selectSignal; + + return { store, fixture }; + }; it('should compile', () => { + const { fixture } = setup(); + fixture.componentRef.setInput('id', '2'); fixture.detectChanges(); expect(fixture).toMatchSnapshot(); }); - it('should dispatch a book.Select action on init', () => { - const action = ViewBookPageActions.selectBook({ id: '2' }); + it('should dispatch a book. Select action on init', () => { + const { fixture, store } = setup(); - (route.params as BehaviorSubject).next({ id: '2' }); + const action = ViewBookPageActions.selectBook({ id: '2' }); + fixture.componentRef.setInput('id', '2'); + fixture.detectChanges(); expect(store.dispatch).toHaveBeenLastCalledWith(action); }); diff --git a/projects/example-app/src/app/books/containers/view-book-page.component.ts b/projects/example-app/src/app/books/containers/view-book-page.component.ts index df288f1917..a4c6ef514a 100644 --- a/projects/example-app/src/app/books/containers/view-book-page.component.ts +++ b/projects/example-app/src/app/books/containers/view-book-page.component.ts @@ -3,10 +3,11 @@ import { OnDestroy, ChangeDetectionStrategy, inject, + input, + effect, + untracked, } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; import { Store } from '@ngrx/store'; -import { map } from 'rxjs/operators'; import { ViewBookPageActions } from '@example-app/books/actions/view-book-page.actions'; import { SelectedBookPageComponent } from './selected-book-page.component'; @@ -28,16 +29,16 @@ import { SelectedBookPageComponent } from './selected-book-page.component'; imports: [SelectedBookPageComponent], template: ` `, }) -export class ViewBookPageComponent implements OnDestroy { +export class ViewBookPageComponent { private readonly store = inject(Store); - private readonly actionsSubscription = inject(ActivatedRoute) - .params.pipe( - map((params) => ViewBookPageActions.selectBook({ id: params.id })) - ) - .subscribe((action) => this.store.dispatch(action)); + readonly id = input.required(); - ngOnDestroy() { - this.actionsSubscription.unsubscribe(); - } + readonly selectBookEffect = effect(() => { + const id = this.id(); + + untracked(() => { + this.store.dispatch(ViewBookPageActions.selectBook({ id })); + }); + }); } From 6312a4cbf2e4dd88dfab0dce317d4b2888d57a97 Mon Sep 17 00:00:00 2001 From: Rainer Hahnekamp Date: Sun, 29 Sep 2024 22:42:19 +0200 Subject: [PATCH 10/10] feat: revert standalone-app changes --- projects/standalone-app/src/app/app.component.spec.ts | 11 +++-------- projects/standalone-app/src/app/app.effects.ts | 6 +++--- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/projects/standalone-app/src/app/app.component.spec.ts b/projects/standalone-app/src/app/app.component.spec.ts index ced3e561c8..e636a11368 100644 --- a/projects/standalone-app/src/app/app.component.spec.ts +++ b/projects/standalone-app/src/app/app.component.spec.ts @@ -1,18 +1,13 @@ import { TestBed } from '@angular/core/testing'; import { AppComponent } from './app.component'; +import { RouterTestingModule } from '@angular/router/testing'; import { provideMockStore } from '@ngrx/store/testing'; -import { provideRouter } from '@angular/router'; -import { provideLocationMocks } from '@angular/common/testing'; describe('AppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AppComponent], - providers: [ - provideMockStore(), - provideRouter([]), - provideLocationMocks(), - ], + imports: [RouterTestingModule, AppComponent], + providers: [provideMockStore()], }).compileComponents(); }); diff --git a/projects/standalone-app/src/app/app.effects.ts b/projects/standalone-app/src/app/app.effects.ts index eba323c582..db83e2bcbe 100644 --- a/projects/standalone-app/src/app/app.effects.ts +++ b/projects/standalone-app/src/app/app.effects.ts @@ -1,15 +1,15 @@ -import { inject, Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Actions, createEffect } from '@ngrx/effects'; import { tap } from 'rxjs'; @Injectable() export class AppEffects { - private actions$ = inject(Actions); - logger$ = createEffect( () => { return this.actions$.pipe(tap((action) => console.log(action))); }, { dispatch: false } ); + + constructor(private actions$: Actions) {} }
- {{ errorMessage }} + {{ errorMessage() }}