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", diff --git a/projects/example-app/src/app/app-routing.module.ts b/projects/example-app/src/app/app-routing.module.ts deleted file mode 100644 index cba7ce2327..0000000000 --- a/projects/example-app/src/app/app-routing.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NgModule } from '@angular/core'; -import { Routes, RouterModule } from '@angular/router'; - -import { authGuard } from '@example-app/auth/services'; -import { NotFoundPageComponent } from '@example-app/core/containers'; - -export const routes: Routes = [ - { path: '', redirectTo: '/books', pathMatch: 'full' }, - { - path: 'books', - loadChildren: () => - import('@example-app/books/books.module').then((m) => m.BooksModule), - canActivate: [authGuard], - }, - { - path: '**', - component: NotFoundPageComponent, - data: { title: 'Not found' }, - }, -]; - -@NgModule({ - imports: [ - RouterModule.forRoot(routes, { - useHash: true, - }), - ], - exports: [RouterModule], -}) -export class AppRoutingModule {} 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..1c431140e8 --- /dev/null +++ b/projects/example-app/src/app/app.config.ts @@ -0,0 +1,88 @@ +import { ApplicationConfig } from '@angular/core'; +import { provideHttpClient } from '@angular/common/http'; +import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; + +import { 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, + withComponentInputBinding, + withHashLocation, +} from '@angular/router'; +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: [ + provideAnimationsAsync(), + provideHttpClient(), + provideRouter(APP_ROUTES, withHashLocation(), withComponentInputBinding()), + + /** + * 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, + }, + }), + + /** + * 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. + */ + 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), + ], +}; 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.ts b/projects/example-app/src/app/app.routing.ts new file mode 100644 index 0000000000..911c3ed7aa --- /dev/null +++ b/projects/example-app/src/app/app.routing.ts @@ -0,0 +1,22 @@ +import { Routes } from '@angular/router'; + +import { authGuard } from '@example-app/auth/services'; +import { NotFoundPageComponent } from '@example-app/core/containers'; + +export const APP_ROUTES: Routes = [ + { + path: 'login', + loadChildren: () => import('@example-app/auth/auth.routes'), + }, + { path: '', redirectTo: '/books', pathMatch: 'full' }, + { + path: 'books', + loadChildren: () => import('@example-app/books/books.routes'), + canActivate: [authGuard], + }, + { + path: '**', + component: NotFoundPageComponent, + data: { title: 'Not found' }, + }, +]; 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..4cc736f9f9 --- /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 default [ + { + path: '', + component: LoginPageComponent, + data: { title: 'Login' }, + }, +] satisfies Routes; 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..3e06030902 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 @@ -2,48 +2,145 @@ exports[`Login Page should compile 1`] = ` - - + + Login - +

- - + +

+
+
+
+ +
+
+
+
+
+
+
+
+

- - + +

+
+
+
+ +
+
+
+
+
+
+
+
+

@@ -54,50 +151,147 @@ exports[`Login Page should compile 1`] = ` exports[`Login Page should disable the form if pending 1`] = ` - - + + Login - +

- - + +

+
+
+
+ +
+
+
+
+
+
+
+
+

- - + +

+
+
+
+ +
+
+
+
+
+
+
+
+

@@ -108,39 +302,119 @@ exports[`Login Page should disable the form if pending 1`] = ` exports[`Login Page should display an error message if provided 1`] = ` - - + + 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..5931924e0b 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,8 @@ 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'; +import { By } from '@angular/platform-browser'; describe('Login Page', () => { let fixture: ComponentFixture; @@ -9,12 +10,13 @@ describe('Login Page', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [ReactiveFormsModule], - declarations: [LoginFormComponent], + imports: [LoginFormComponent], + providers: [provideNoopAnimations()], schemas: [NO_ERRORS_SCHEMA], }); fixture = TestBed.createComponent(LoginFormComponent); + fixture.componentRef.setInput('pending', false); instance = fixture.componentInstance; }); @@ -37,7 +39,7 @@ describe('Login Page', () => { }); it('should disable the form if pending', () => { - instance.pending = true; + fixture.componentRef.setInput('pending', true); fixture.detectChanges(); @@ -45,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(); @@ -57,7 +59,19 @@ describe('Login Page', () => { username: 'user', password: 'pass', }; - instance.form.setValue(credentials); + + const inpUsername: HTMLInputElement = fixture.debugElement.query( + By.css('input[data-testid=username]') + ).nativeElement; + const inpPassword: HTMLInputElement = fixture.debugElement.query( + By.css('input[data-testid=password]') + ).nativeElement; + + fixture.detectChanges(); + inpUsername.value = credentials.username; + inpUsername.dispatchEvent(new Event('input')); + inpPassword.value = credentials.password; + inpPassword.dispatchEvent(new Event('input')); jest.spyOn(instance.submitted, 'emit'); instance.submit(); 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..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,9 +1,21 @@ -import { Component, Input, Output, EventEmitter } from '@angular/core'; -import { FormGroup, FormControl } from '@angular/forms'; +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'; @Component({ + standalone: true, selector: 'bc-login-form', + imports: [MaterialModule, ReactiveFormsModule], template: ` Login @@ -16,6 +28,7 @@ import { Credentials } from '@example-app/auth/models'; matInput placeholder="Username" formControlName="username" + data-testid="username" />

@@ -27,13 +40,16 @@ import { Credentials } from '@example-app/auth/models'; matInput placeholder="Password" formControlName="password" + data-testid="password" />

- + }