From 905087eec4d6cfb211b82a0e02b382d935aa7325 Mon Sep 17 00:00:00 2001 From: fabioemoutinho Date: Tue, 2 Jan 2024 17:09:05 -0300 Subject: [PATCH 01/29] upgrade angular training to angular 17 --- src/angular/1-why-angular/why-angular.md | 10 ++--- .../building-first-app.md | 40 ++++++++----------- src/angular/angular.md | 8 ++-- 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/src/angular/1-why-angular/why-angular.md b/src/angular/1-why-angular/why-angular.md index 9832bcb50..312e7b6ff 100644 --- a/src/angular/1-why-angular/why-angular.md +++ b/src/angular/1-why-angular/why-angular.md @@ -7,19 +7,20 @@ ## Angular in the Modern Web Development Ecosphere -Angular was first released in 2010 as a new framework in the MV* space alongside libraries like Backbone, Knockout, and Dojo. +Angular was first released in 2010 as a new framework in the MV\* space alongside libraries like Backbone, Knockout, and Dojo. Some of the best things about Angular are that it is opinionated, has built-in testing, streamlines the Webpack build complexity with its own CLI, and is a Google-backed and supported product. + ## The Pros ### 1. An Opinionated Framework -Developers have to make a lot of decisions on a daily basis, which can often create decision fatigue. The great part about using an opinionated framework is being able to shift your focus from build configs, high level architecture decisions, to writing the actual client-specific functioning pieces of the application. +Developers have to make a lot of decisions on a daily basis, which can often create decision fatigue. The great part about using an opinionated framework is being able to shift your focus from build configs, high level architecture decisions, to writing the actual client-specific functioning pieces of the application. The use of TypeScript to force type checking improves workflow by catching errors at compile time allowing teams to catch potential errors much faster. ### 2. Testing Built In -Spinning up a new Angular Workspace automatically creates a test suite, with a working karma config, and new test spec files for any component generated. +Spinning up a new Angular Workspace automatically creates a test suite, with a working karma config, and new test spec files for any component generated. ### 3. Harnesses the Power of Webpack @@ -30,5 +31,4 @@ Angular streamlines the build process by masking Webpack config complexity with Having a heavy hitting tech titan backing a library can make it a very sustainable choice to use. - -For more information see the Angular Docs \ No newline at end of file +For more information see the Angular Docs diff --git a/src/angular/2-building-first-app/building-first-app.md b/src/angular/2-building-first-app/building-first-app.md index 744424ed9..2ec3b62b5 100644 --- a/src/angular/2-building-first-app/building-first-app.md +++ b/src/angular/2-building-first-app/building-first-app.md @@ -1,13 +1,13 @@ @page learn-angular/building-our-first-app Generate an App @parent learn-angular 2 -@description Learn how to generate an Angular 13 application with it's command line interface (CLI). +@description Learn how to generate an Angular 17 application with it's command line interface (CLI). @body ## How to Use This Guide -This guide will walk you through building an application in Angular 13. Each page of the guide is based on building a new feature, and may have multiple "problems" to solve. Each problem will be explained and include requirements and any set-up steps needed. Most problems will also include unit tests to update to verify the solution has been implemented correctly. The ✏️ icon will be used to indicate when commands need to be run or when files need to be updated. If you have any issues or suggestions as you move through this training, we'd love you to submit a GitHub issue for it! 💖 +This guide will walk you through building an application in Angular 17. Each page of the guide is based on building a new feature, and may have multiple "problems" to solve. Each problem will be explained and include requirements and any set-up steps needed. Most problems will also include unit tests to update to verify the solution has been implemented correctly. The ✏️ icon will be used to indicate when commands need to be run or when files need to be updated. If you have any issues or suggestions as you move through this training, we'd love you to submit a GitHub issue for it! 💖 ## Overview @@ -70,28 +70,28 @@ We'll start by globally installing the Angular CLI. ✏️ Run the following: ```shell -npm install -g @angular/cli@13 +npm install -g @angular/cli@17 ``` ## Generating a new app We're going to build a restaurant menu and ordering application. The final result will look like this: -![Place My Order App screenshot](../static/img/place-my-order.png "Place My Order App screenshot") +![Place My Order App screenshot](../static/img/place-my-order.png 'Place My Order App screenshot') (reminder: You can see a DoneJS implementation of this application at [www.place-my-order.com](http://www.place-my-order.com)) ✏️ To create a new Angular Workspace, run the 'ng new' command: ```shell -ng new place-my-order --prefix pmo +ng new place-my-order --prefix pmo --standalone false cd place-my-order ``` This will create a new Angular Workspace, generate an app module, needed config files, and test suite for your new Angular project. You'll be asked a series of set-up questions: -1. Would you like to add Angular routing? (**yes**) -2. Which stylesheet format would you like to use? (**Less**) +1. Which stylesheet format would you like to use? (**Less**) +2. Do you want to enable Server-Side Rendering (SSR) and Staatic Site Generation (SSG/Prerendering)? (**No**) Note that we used the prefix property to set our own default prefix. Angular's default is "app", but a good naming convention is to use a short prefix related to your company or application name to easily differentiate from 3rd party utilities. @@ -124,17 +124,11 @@ Let's walk through some of the files that were generated. | | ├── app.component.ts | | ├── app.module.ts | ├── assets/ -| ├── environments/ -| | ├── environment.prod.ts -| | ├── environment.ts | ├── index.html | ├── main.ts -| ├── polyfills.ts | ├── styles.less -| ├── test.ts -├── .browserslistrc +├── .editorconfig ├── angular.json -├── karma.conf.json ├── package-lock.json ├── package.json ├── README.md @@ -145,7 +139,7 @@ Let's walk through some of the files that were generated. ### angular.json -This file is the config schema for an Angular Workspace. By default Angular configures Webpack for it's build process, and uses the angular.json file for the build information. +This file is the config schema for an Angular Workspace. By default Angular configures Webpack for its build process, and uses the angular.json file for the build information. (Note, prior to Angular v6, this file was .angular-cli.json. When migrating versions, having the wrong workspace config file name is a cause for problems.) @@ -158,7 +152,6 @@ This file contains our TypeScript compiling options. Starting from Angular 12, " { "compileOnSave": false, "compilerOptions": { - "baseUrl": "./", "outDir": "./dist/out-tsc", "forceConsistentCasingInFileNames": true, "strict": true, @@ -166,16 +159,18 @@ This file contains our TypeScript compiling options. Starting from Angular 12, " "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, "sourceMap": true, "declaration": false, - "downlevelIteration": true, "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, - "target": "es2017", - "module": "es2020", + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, "lib": [ - "es2020", + "ES2022", "dom" ] }, @@ -186,10 +181,9 @@ This file contains our TypeScript compiling options. Starting from Angular 12, " "strictTemplates": true } } - ``` -@highlight 8 +@highlight 7 ### src/main.ts @@ -279,4 +273,4 @@ Let's change the markup to look like the home page of our place my order app. @highlight 1-2 When you save your changes, you should see the new h1 tag in your browser at localhost:4200. - \ No newline at end of file + diff --git a/src/angular/angular.md b/src/angular/angular.md index 0419f9578..6e52800ae 100644 --- a/src/angular/angular.md +++ b/src/angular/angular.md @@ -40,7 +40,7 @@ the features that are present across almost all single page apps: As for the application itself, it: -- Is written in Angular 13 +- Is written in Angular 17 - Is a single page application (SPA) that uses [pushState](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) to simulate routing between several pages. - A `home` page - A `restaurant list` page that lets the user filter restaurants by state and city @@ -75,7 +75,7 @@ retrieving a single restaurant from the service layer ([learn-angular/writing-un Now we are ready to turn our attention to learning about creating, updating, and deleting data. We will start by building an Order Form ([learn-angular/building-order-form]) and then update it to utilize directives -([learn-angular/creating-directive]), which in turn allows us to create orders on the server ([learn-angular/order-service]). We'll then create a page that lets us update an order's status or delete an order ([learn-angular/order-history-component]). +([learn-angular/creating-directive]), which in turn allows us to create orders on the server ([learn-angular/order-service]). We'll then create a page that lets us update an order's status or delete an order ([learn-angular/order-history-component]). We will utilize pipes to create an item total calculation across the application ([learn-angular/item-total-pipe]) and will even make the order page update when someone else updates an order ([learn-angular/real-time-connection]). @@ -85,8 +85,8 @@ production and deploying it for others to see ([learn-angular/deploy-app]). ## Requirements In order to complete this guide, you need to have [NodeJS](https://nodejs.org/en/) version -12 or later installed. +18.13 or later installed. ## Next Steps -✏️ Head over to the [first lesson](learn-angular/why-angular.html) and get your environment setup. \ No newline at end of file +✏️ Head over to the [first lesson](learn-angular/why-angular.html) and get your environment setup. From fa1561138554befda1fd20bc78f0362278b4a379 Mon Sep 17 00:00:00 2001 From: fabioemoutinho Date: Tue, 2 Jan 2024 17:11:51 -0300 Subject: [PATCH 02/29] fix --- src/angular/2-building-first-app/building-first-app.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/angular/2-building-first-app/building-first-app.md b/src/angular/2-building-first-app/building-first-app.md index 2ec3b62b5..10c89e8d4 100644 --- a/src/angular/2-building-first-app/building-first-app.md +++ b/src/angular/2-building-first-app/building-first-app.md @@ -1,7 +1,7 @@ @page learn-angular/building-our-first-app Generate an App @parent learn-angular 2 -@description Learn how to generate an Angular 17 application with it's command line interface (CLI). +@description Learn how to generate an Angular 17 application with its command line interface (CLI). @body @@ -91,7 +91,7 @@ cd place-my-order This will create a new Angular Workspace, generate an app module, needed config files, and test suite for your new Angular project. You'll be asked a series of set-up questions: 1. Which stylesheet format would you like to use? (**Less**) -2. Do you want to enable Server-Side Rendering (SSR) and Staatic Site Generation (SSG/Prerendering)? (**No**) +2. Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? (**No**) Note that we used the prefix property to set our own default prefix. Angular's default is "app", but a good naming convention is to use a short prefix related to your company or application name to easily differentiate from 3rd party utilities. @@ -127,7 +127,6 @@ Let's walk through some of the files that were generated. | ├── index.html | ├── main.ts | ├── styles.less -├── .editorconfig ├── angular.json ├── package-lock.json ├── package.json From e05dabec264b47a55b86969802f540c7bef5ec3b Mon Sep 17 00:00:00 2001 From: fabioemoutinho Date: Tue, 2 Jan 2024 17:47:57 -0300 Subject: [PATCH 03/29] fix --- .../3-creating-components/angular.json | 45 +++++++++---------- .../creating-components.md | 4 +- 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/angular/3-creating-components/angular.json b/src/angular/3-creating-components/angular.json index e316350ba..851052b2b 100644 --- a/src/angular/3-creating-components/angular.json +++ b/src/angular/3-creating-components/angular.json @@ -7,10 +7,14 @@ "projectType": "application", "schematics": { "@schematics/angular:component": { - "style": "less" + "style": "less", + "standalone": false }, - "@schematics/angular:application": { - "strict": true + "@schematics/angular:directive": { + "standalone": false + }, + "@schematics/angular:pipe": { + "standalone": false } }, "root": "", @@ -18,12 +22,14 @@ "prefix": "pmo", "architect": { "build": { - "builder": "@angular-devkit/build-angular:browser", + "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/place-my-order", "index": "src/index.html", - "main": "src/main.ts", - "polyfills": "src/polyfills.ts", + "browser": "src/main.ts", + "polyfills": [ + "zone.js" + ], "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "less", "assets": [ @@ -55,21 +61,12 @@ "maximumError": "4kb" } ], - "fileReplacements": [ - { - "replace": "src/environments/environment.ts", - "with": "src/environments/environment.prod.ts" - } - ], "outputHashing": "all" }, "development": { - "buildOptimizer": false, "optimization": false, - "vendorChunk": true, "extractLicenses": false, - "sourceMap": true, - "namedChunks": true + "sourceMap": true } }, "defaultConfiguration": "production" @@ -78,10 +75,10 @@ "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { - "browserTarget": "place-my-order:build:production" + "buildTarget": "place-my-order:build:production" }, "development": { - "browserTarget": "place-my-order:build:development" + "buildTarget": "place-my-order:build:development" } }, "defaultConfiguration": "development" @@ -89,16 +86,17 @@ "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { - "browserTarget": "place-my-order:build" + "buildTarget": "place-my-order:build" } }, "test": { "builder": "@angular-devkit/build-angular:karma", "options": { - "main": "src/test.ts", - "polyfills": "src/polyfills.ts", + "polyfills": [ + "zone.js", + "zone.js/testing" + ], "tsConfig": "tsconfig.spec.json", - "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "less", "assets": [ "src/favicon.ico", @@ -112,6 +110,5 @@ } } } - }, - "defaultProject": "place-my-order" + } } diff --git a/src/angular/3-creating-components/creating-components.md b/src/angular/3-creating-components/creating-components.md index fba9db8c5..c29309fcc 100644 --- a/src/angular/3-creating-components/creating-components.md +++ b/src/angular/3-creating-components/creating-components.md @@ -115,14 +115,14 @@ npm install place-my-order-assets --save Open the `angular.json` file, and make the following changes to include these files in our build process. This will copy the images into our assets directory for when we serve our application. -> Pay close attention that you're making these changes under the "build" key and not the "test" key, as the code looks very similar. The build key should be close to line 20. +> Pay close attention that you're making these changes under the "build" key and not the "test" key, as the code looks very similar. The build key should be close to line 24.
section copied - angular.json ✏️ Update __angular.json__: @sourceref ./angular.json -@highlight 29-41,only +@highlight 35-47,only
From 5bfc4b0e23a73a347fa7aea324ce4d51e265d57f Mon Sep 17 00:00:00 2001 From: fabioemoutinho Date: Thu, 4 Jan 2024 14:24:18 -0300 Subject: [PATCH 04/29] update why angular bundler text --- src/angular/1-why-angular/why-angular.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/angular/1-why-angular/why-angular.md b/src/angular/1-why-angular/why-angular.md index 312e7b6ff..3b91c6433 100644 --- a/src/angular/1-why-angular/why-angular.md +++ b/src/angular/1-why-angular/why-angular.md @@ -22,10 +22,13 @@ The use of TypeScript to force type checking improves workflow by catching error Spinning up a new Angular Workspace automatically creates a test suite, with a working karma config, and new test spec files for any component generated. -### 3. Harnesses the Power of Webpack +### 3. Harnesses the Power of Modern Bundlers -Webpack is a module bundler that also handles transforming resources, like Less or Typescript. -Angular streamlines the build process by masking Webpack config complexity with the Angular CLI. +Angular streamlines the build process by masking bundler complexity with the Angular CLI. + +Up until Angular 16, Angular's build process used Webpack. Webpack is a module bundler that also handles transforming resources, like Less or Typescript. + +Starting on Angular 17, to keep up with latest technology and for its improved build times, Angular switched to ESBuild by default. ### 4. Google-Backed and Supported Product From 266b15d555fbde505f53c11ff30de395afd0cbaf Mon Sep 17 00:00:00 2001 From: fabioemoutinho Date: Thu, 4 Jan 2024 14:25:48 -0300 Subject: [PATCH 05/29] typo fix --- src/angular/2-building-first-app/building-first-app.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/angular/2-building-first-app/building-first-app.md b/src/angular/2-building-first-app/building-first-app.md index 10c89e8d4..870091c81 100644 --- a/src/angular/2-building-first-app/building-first-app.md +++ b/src/angular/2-building-first-app/building-first-app.md @@ -16,7 +16,7 @@ In this part, we will: - Explore tools that aid Angular Development - Install Angular's CLI - Generate a new app -- Look at the files generated by the cli +- Look at the files generated by the CLI - Learn to serve our app ## Problem From c520c204b6f31131040aaad2ce3aee15c86287f0 Mon Sep 17 00:00:00 2001 From: fabioemoutinho Date: Thu, 4 Jan 2024 15:20:49 -0300 Subject: [PATCH 06/29] update why-angular and building-first-app --- src/angular/1-why-angular/why-angular.md | 2 +- src/angular/2-building-first-app/building-first-app.md | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/angular/1-why-angular/why-angular.md b/src/angular/1-why-angular/why-angular.md index 3b91c6433..6393345b6 100644 --- a/src/angular/1-why-angular/why-angular.md +++ b/src/angular/1-why-angular/why-angular.md @@ -28,7 +28,7 @@ Angular streamlines the build process by masking bundler complexity with the Ang Up until Angular 16, Angular's build process used Webpack. Webpack is a module bundler that also handles transforming resources, like Less or Typescript. -Starting on Angular 17, to keep up with latest technology and for its improved build times, Angular switched to ESBuild by default. +Starting on Angular 17, to keep up with latest technology and for its improved build times, Angular switched to esbuild by default. ### 4. Google-Backed and Supported Product diff --git a/src/angular/2-building-first-app/building-first-app.md b/src/angular/2-building-first-app/building-first-app.md index 870091c81..222290e33 100644 --- a/src/angular/2-building-first-app/building-first-app.md +++ b/src/angular/2-building-first-app/building-first-app.md @@ -107,6 +107,11 @@ Note that we used the prefix property to set our own default prefix. Angular's d ``` +We also set the standalone option to `false`. + +Standalone Angular apps drop modules in favor of Standalone Components and have a slightly simpler architecture. +While new Angular apps default to standalone `true`, for the purposes of this training we will still use Angular Modules. + There are [several more helpful properties](https://angular.io/cli/new) that customize how a project is set up. ## Looking at Our Generated Workspace @@ -138,7 +143,7 @@ Let's walk through some of the files that were generated. ### angular.json -This file is the config schema for an Angular Workspace. By default Angular configures Webpack for its build process, and uses the angular.json file for the build information. +This file is the config schema for an Angular Workspace. By default Angular configures esbuild (Webpack before v17) for its build process, and uses the angular.json file for the build information. (Note, prior to Angular v6, this file was .angular-cli.json. When migrating versions, having the wrong workspace config file name is a cause for problems.) @@ -232,7 +237,7 @@ This is our root component, you saw it called in our index.html file as `webpack-dev-server, and compiles a development version of the app. Any TypeScript errors will be caught by the compiler here, and once ready we can view our app at localhost:4200. `ng serve` also has live-reload functionality, meaning the browser will automatically reload as changes are saved and compiled. +The `start` script command value is `ng serve` which starts a development server on port 4200 by default using esbuild and Vite (webpack-dev-server before v17), to compile and serve a development version of the app. Any TypeScript errors will be caught by the compiler here, and once ready we can view our app at localhost:4200. `ng serve` also has live-reload functionality, meaning the browser will automatically reload as changes are saved and compiled. ## Running Tests From cbc68aece8ccef964b1ecc9a3c03363ac18ace14 Mon Sep 17 00:00:00 2001 From: fabioemoutinho Date: Thu, 4 Jan 2024 18:37:15 -0300 Subject: [PATCH 07/29] update adding-routing --- src/angular/4-adding-routing/adding-routing.md | 4 ++-- src/angular/4-adding-routing/app.component.spec.ts | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/angular/4-adding-routing/adding-routing.md b/src/angular/4-adding-routing/adding-routing.md index 52936efeb..d035db09d 100644 --- a/src/angular/4-adding-routing/adding-routing.md +++ b/src/angular/4-adding-routing/adding-routing.md @@ -28,7 +28,7 @@ at the end of this tutorial: ## Setup -``, which handles routing to a component based on a url, was added to our **src/app/app.component.html** file when we first generated our app and answered `yes` to the routing question. But since that time, we added components to that view. Let's remove those components because `` will handle showing +``, which handles routing to a component based on a url, was added to our **src/app/app.component.html** file when we first generated our app. But since that time, we added components to that view. Let's remove those components because `` will handle showing those components going forward. ✏️ Update **src/app/app.component.html** to: @@ -62,7 +62,7 @@ If you have completed the exercise successfully you should be able to see the ho ## Router -To be able to navigate between different views in our app, we can take advantage of Angular's built-in routing module. We already told Angular we'd like to set up routing, so it generated `src/app/app-routing.module.ts` for us and included it in our root module. `src/app/app-routing.module.ts` currently looks like: +To be able to navigate between different views in our app, we can take advantage of Angular's built-in routing module. Angular generated `src/app/app-routing.module.ts` for us and included it in our root module. `src/app/app-routing.module.ts` currently looks like: ```typescript import { NgModule } from '@angular/core'; diff --git a/src/angular/4-adding-routing/app.component.spec.ts b/src/angular/4-adding-routing/app.component.spec.ts index f1a3d4da9..b23a15a14 100644 --- a/src/angular/4-adding-routing/app.component.spec.ts +++ b/src/angular/4-adding-routing/app.component.spec.ts @@ -41,6 +41,7 @@ describe('AppComponent', () => { it('should render the HomeComponent with router navigates to "/" path', fakeAsync(() => { const location: Location = TestBed.inject(Location); const router: Router = TestBed.inject(Router); + fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; router.navigate(['']).then(() => { expect(location.path()).toBe(''); @@ -51,6 +52,7 @@ describe('AppComponent', () => { it('should render the RestaurantsComponent with router navigates to "/restaurants" path', fakeAsync(() => { const location: Location = TestBed.inject(Location); const router: Router = TestBed.inject(Router); + fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; router.navigate(['restaurants']).then(() => { expect(location.path()).toBe('/restaurants'); From d1e466b3d6361f2da549c7109aef69dc5abc3e04 Mon Sep 17 00:00:00 2001 From: fabioemoutinho Date: Thu, 4 Jan 2024 18:52:40 -0300 Subject: [PATCH 08/29] update spec file for creating navigation --- src/angular/5-creating-navigation/app.component.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/angular/5-creating-navigation/app.component.spec.ts b/src/angular/5-creating-navigation/app.component.spec.ts index 96f7a2b35..6db29102e 100644 --- a/src/angular/5-creating-navigation/app.component.spec.ts +++ b/src/angular/5-creating-navigation/app.component.spec.ts @@ -49,6 +49,7 @@ describe('AppComponent', () => { }); it('should render the HomeComponent with router navigates to "/" path', fakeAsync(() => { + fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; router.navigate(['']).then(() => { expect(location.path()).toBe(''); @@ -57,6 +58,7 @@ describe('AppComponent', () => { })); it('should render the RestaurantsComponent with router navigates to "/restaurants" path', fakeAsync(() => { + fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; router.navigate(['restaurants']).then(() => { expect(location.path()).toBe('/restaurants'); From 5870960e252007f6aad4271f50350d5a6c8601d7 Mon Sep 17 00:00:00 2001 From: fabioemoutinho Date: Sun, 7 Jan 2024 14:06:25 -0300 Subject: [PATCH 09/29] update restaurant-service.md --- .../restaurant-service.md | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/src/angular/6-restaurant-service/restaurant-service.md b/src/angular/6-restaurant-service/restaurant-service.md index 7159aa5e7..16ee060d6 100644 --- a/src/angular/6-restaurant-service/restaurant-service.md +++ b/src/angular/6-restaurant-service/restaurant-service.md @@ -80,7 +80,7 @@ heavy use of it. Checkout our [learn-rxjs] tutorial for more information. ## P1: Technical Requirements -Write a `RestaurantService` with a method `getRestaurants` that uses `httpClient` to get a list of restaurants from an environment variable\ + `/restaurants`. For example, we could get restaurants like: +Write a `RestaurantService` with a method `getRestaurants` that uses `httpClient` to get a list of restaurants from an environment variable + `/restaurants`. For example, we could get restaurants like: ```typescript const httpClient = new HttpClient(); @@ -141,42 +141,36 @@ Double check the api by navigating to { + fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; router.navigate(['']).then(() => { expect(location.path()).toBe(''); @@ -240,6 +241,7 @@ describe('AppComponent', () => { })); it('should render the RestaurantsComponent with router navigates to "/restaurants" path', fakeAsync(() => { + fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; router.navigate(['restaurants']).then(() => { expect(location.path()).toBe('/restaurants'); @@ -269,7 +271,7 @@ describe('AppComponent', () => { fixture.detectChanges(); tick(); - let homeLinkLi = compiled.querySelector('li'); + const homeLinkLi = compiled.querySelector('li'); expect(homeLinkLi?.classList).toContain('active'); expect(compiled.querySelectorAll('.active').length).toBe(1); flush(); @@ -283,7 +285,7 @@ describe('AppComponent', () => { fixture.detectChanges(); expect(location.path()).toBe('/restaurants'); - let restaurantsLinkLi = compiled.querySelector('li:nth-child(2)'); + const restaurantsLinkLi = compiled.querySelector('li:nth-child(2)'); expect(restaurantsLinkLi?.classList).toContain('active'); expect(compiled.querySelectorAll('.active').length).toBe(1); flush(); From aa7f7613b13dff9beb8e964a286c7c9de81964be Mon Sep 17 00:00:00 2001 From: fabioemoutinho Date: Sun, 7 Jan 2024 15:23:01 -0300 Subject: [PATCH 11/29] update state-city-options --- .../order-state/app.component.spec.ts | 4 ++-- .../app.component.spec.ts | 2 ++ .../restaurant.component-starter.ts | 12 +++++++--- .../restaurant.component.ts | 14 +++++++---- .../state-city-options.md | 24 ++++++++++++++++++- 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/angular/12-writing-unit-tests/order-state/app.component.spec.ts b/src/angular/12-writing-unit-tests/order-state/app.component.spec.ts index 98fcafffe..d513565c2 100644 --- a/src/angular/12-writing-unit-tests/order-state/app.component.spec.ts +++ b/src/angular/12-writing-unit-tests/order-state/app.component.spec.ts @@ -94,7 +94,7 @@ describe('AppComponent', () => { fixture.detectChanges(); tick(); fixture.detectChanges(); - let homeLinkLi = fixture.debugElement.query(By.css('li')); + const homeLinkLi = fixture.debugElement.query(By.css('li')); expect(homeLinkLi.nativeElement.classList).toContain('active'); expect(compiled.querySelectorAll('.active').length).toBe(1); fixture.destroy(); @@ -109,7 +109,7 @@ describe('AppComponent', () => { fixture.detectChanges(); tick(); fixture.detectChanges(); - let restaurantsLinkLi = fixture.debugElement.query(By.css('li:nth-child(2)')); + const restaurantsLinkLi = fixture.debugElement.query(By.css('li:nth-child(2)')); expect(restaurantsLinkLi.nativeElement.classList).toContain('active'); expect(compiled.querySelectorAll('.active').length).toBe(1); fixture.destroy(); diff --git a/src/angular/8-state-city-options/app.component.spec.ts b/src/angular/8-state-city-options/app.component.spec.ts index 3ddece118..8a7f2384f 100644 --- a/src/angular/8-state-city-options/app.component.spec.ts +++ b/src/angular/8-state-city-options/app.component.spec.ts @@ -237,6 +237,7 @@ describe('AppComponent', () => { }); it('should render the HomeComponent with router navigates to "/" path', fakeAsync(() => { + fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; router.navigate(['']).then(() => { expect(location.path()).toBe(''); @@ -245,6 +246,7 @@ describe('AppComponent', () => { })); it('should render the RestaurantsComponent with router navigates to "/restaurants" path', fakeAsync(() => { + fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; router.navigate(['restaurants']).then(() => { expect(location.path()).toBe('/restaurants'); diff --git a/src/angular/8-state-city-options/restaurant.component-starter.ts b/src/angular/8-state-city-options/restaurant.component-starter.ts index 9d6e15d93..274720e13 100644 --- a/src/angular/8-state-city-options/restaurant.component-starter.ts +++ b/src/angular/8-state-city-options/restaurant.component-starter.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { Restaurant } from './restaurant'; import { ResponseData, RestaurantService } from './restaurant.service'; @@ -14,7 +14,10 @@ export interface Data { styleUrls: ['./restaurant.component.less'], }) export class RestaurantComponent implements OnInit { - form: FormGroup = this.createForm(); + form: FormGroup<{ + state: FormControl; + city: FormControl; + }> = this.createForm(); restaurants: Data = { value: [], @@ -47,7 +50,10 @@ export class RestaurantComponent implements OnInit { }); } - createForm(): FormGroup { + createForm(): FormGroup<{ + state: FormControl; + city: FormControl; + }> { } } diff --git a/src/angular/8-state-city-options/restaurant.component.ts b/src/angular/8-state-city-options/restaurant.component.ts index ec4e098a5..5883f4fee 100644 --- a/src/angular/8-state-city-options/restaurant.component.ts +++ b/src/angular/8-state-city-options/restaurant.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { Restaurant } from './restaurant'; import { ResponseData, RestaurantService } from './restaurant.service'; @@ -14,7 +14,10 @@ export interface Data { styleUrls: ['./restaurant.component.less'], }) export class RestaurantComponent implements OnInit { - form: FormGroup = this.createForm(); + form: FormGroup<{ + state: FormControl; + city: FormControl; + }> = this.createForm(); restaurants: Data = { value: [], @@ -47,8 +50,11 @@ export class RestaurantComponent implements OnInit { }); } - createForm(): FormGroup { - return this.fb.group({ + createForm(): FormGroup<{ + state: FormControl; + city: FormControl; + }> { + return this.fb.nonNullable.group({ state: { value: '', disabled: false }, city: { value: '', disabled: false }, }); diff --git a/src/angular/8-state-city-options/state-city-options.md b/src/angular/8-state-city-options/state-city-options.md index 1a3541321..77dd97f87 100644 --- a/src/angular/8-state-city-options/state-city-options.md +++ b/src/angular/8-state-city-options/state-city-options.md @@ -130,6 +130,28 @@ This example shows the use of FormArray and using an `insert` method to dynamica @codepen @highlight 17,40,42,45-49,only +## Form Nullability + +Since Angular v14, Angular Forms are strictly typed by default. + +By default, all controls include the type `null`. The reason for the `null` type is that when calling `reset` on the form or its controls, the values are updated to `null`. + +To avoid the default behavior and having to handle possible `null` values, we can use the `NonNullableFormBuilder`, either via injecting it or accessing the FormBuilder's `nonNullable` property. + +Using `NonNullableFormBuilder` will make `reset` method use the control's initial value instead of `null`. + +```typescript +constructor(private fb: NonNullableFormBuilder) {} +``` + +```typescript +this.myQuickForm = this.fb.nonNullable.group({ + firstName: { value: '', disabled: false }, + lastName: { value: '', disabled: false }, + email: { value: '', disabled: false }, +}); +``` + ## The Solution
@@ -137,7 +159,7 @@ This example shows the use of FormArray and using an `insert` method to dynamica ✏️ Update **src/app/restaurant/restaurant.component.ts** to: @sourceref ./restaurant.component.ts -@highlight 2,17,18,23-35,49-55 +@highlight 53-62 ✏️ Update **src/app/restaurant/restaurant.component.html** to: From 39a9258c547c8b034de188547de762b49914c3b6 Mon Sep 17 00:00:00 2001 From: fabioemoutinho Date: Sun, 7 Jan 2024 17:28:33 -0300 Subject: [PATCH 12/29] update form-value-changes and updating-service-params --- .../restaurant.component-httpparams.ts | 36 ++++++++++--------- .../form-value-changes.md | 2 +- .../restaurant-generics.component.ts | 24 +++++++------ .../restaurant.component-citystate.ts | 36 ++++++++++--------- .../restaurant.component.ts | 24 +++++++------ 5 files changed, 69 insertions(+), 53 deletions(-) diff --git a/src/angular/10-updating-service-params/restaurant.component-httpparams.ts b/src/angular/10-updating-service-params/restaurant.component-httpparams.ts index 5c15369d8..e6ff5a871 100644 --- a/src/angular/10-updating-service-params/restaurant.component-httpparams.ts +++ b/src/angular/10-updating-service-params/restaurant.component-httpparams.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { Subject, takeUntil } from 'rxjs'; import { Restaurant } from './restaurant'; import { @@ -20,7 +20,10 @@ export interface Data { styleUrls: ['./restaurant.component.less'], }) export class RestaurantComponent implements OnInit, OnDestroy { - form: FormGroup = this.createForm(); + form: FormGroup<{ + state: FormControl; + city: FormControl; + }> = this.createForm(); restaurants: Data = { value: [], @@ -54,38 +57,40 @@ export class RestaurantComponent implements OnInit, OnDestroy { this.onDestroy$.complete(); } - createForm(): FormGroup { - return this.fb.group({ + createForm(): FormGroup<{ + state: FormControl; + city: FormControl; + }> { + return this.fb.nonNullable.group({ state: { value: '', disabled: true }, city: { value: '', disabled: true }, }); } onChanges(): void { - let state: string = this.form.get('state')?.value; + let state: string = this.form.controls.state.value; - this.form - .get('state') - ?.valueChanges.pipe(takeUntil(this.onDestroy$)) + this.form.controls.state.valueChanges + .pipe(takeUntil(this.onDestroy$)) .subscribe((val) => { this.restaurants.value = []; if (val) { // only enable city if state has value - this.form.get('city')?.enable({ + this.form.controls.city.enable({ onlySelf: true, emitEvent: false, }); // if state has a value and has changed, clear previous city value if (state !== val) { - this.form.get('city')?.patchValue(''); + this.form.controls.city.patchValue(''); } // fetch cities based on state val this.getCities(val); } else { // disable city if no value - this.form.get('city')?.disable({ + this.form.controls.city.disable({ onlySelf: true, emitEvent: false, }); @@ -93,9 +98,8 @@ export class RestaurantComponent implements OnInit, OnDestroy { state = val; }); - this.form - .get('city') - ?.valueChanges.pipe(takeUntil(this.onDestroy$)) + this.form.controls.city.valueChanges + .pipe(takeUntil(this.onDestroy$)) .subscribe((val) => { if (val) { this.getRestaurants(state, val); @@ -110,7 +114,7 @@ export class RestaurantComponent implements OnInit, OnDestroy { .subscribe((res: ResponseData) => { this.states.value = res.data; this.states.isPending = false; - this.form.get('state')?.enable(); + this.form.controls.state.enable(); }); } @@ -122,7 +126,7 @@ export class RestaurantComponent implements OnInit, OnDestroy { .subscribe((res: ResponseData) => { this.cities.value = res.data; this.cities.isPending = false; - this.form.get('city')?.enable({ + this.form.controls.city.enable({ onlySelf: true, emitEvent: false, }); diff --git a/src/angular/9-form-value-changes/form-value-changes.md b/src/angular/9-form-value-changes/form-value-changes.md index 3c20ddd5d..e5bdf1343 100644 --- a/src/angular/9-form-value-changes/form-value-changes.md +++ b/src/angular/9-form-value-changes/form-value-changes.md @@ -85,7 +85,7 @@ When you interact with the dropdown menus, you should see their values logged to ✏️ Update **src/app/restaurant/restaurant.component.ts** @sourceref restaurant.component.ts -@highlight 1,3,17,38,39,47-72 +@highlight 1,3,17,41,42,50-73
diff --git a/src/angular/9-form-value-changes/restaurant-generics.component.ts b/src/angular/9-form-value-changes/restaurant-generics.component.ts index 2c03fb68d..c6ad4e350 100644 --- a/src/angular/9-form-value-changes/restaurant-generics.component.ts +++ b/src/angular/9-form-value-changes/restaurant-generics.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { Subject, takeUntil } from 'rxjs'; import { Restaurant } from './restaurant'; import { ResponseData, RestaurantService } from './restaurant.service'; @@ -15,7 +15,10 @@ export interface Data { styleUrls: ['./restaurant.component.less'], }) export class RestaurantComponent implements OnInit, OnDestroy { - form: FormGroup = this.createForm(); + form: FormGroup<{ + state: FormControl; + city: FormControl; + }> = this.createForm(); restaurants: Data = { value: [], @@ -52,16 +55,14 @@ export class RestaurantComponent implements OnInit, OnDestroy { this.restaurants.isPending = false; }); - this.form - .get('state') - ?.valueChanges.pipe(takeUntil(this.onDestroy$)) + this.form.controls.state.valueChanges + .pipe(takeUntil(this.onDestroy$)) .subscribe((val) => { console.log('state', val); }); - this.form - .get('city') - ?.valueChanges.pipe(takeUntil(this.onDestroy$)) + this.form.controls.city.valueChanges + .pipe(takeUntil(this.onDestroy$)) .subscribe((val) => { console.log('city', val); }); @@ -72,8 +73,11 @@ export class RestaurantComponent implements OnInit, OnDestroy { this.onDestroy$.complete(); } - createForm(): FormGroup { - return this.fb.group({ + createForm(): FormGroup<{ + state: FormControl; + city: FormControl; + }> { + return this.fb.nonNullable.group({ state: { value: '', disabled: false }, city: { value: '', disabled: false }, }); diff --git a/src/angular/9-form-value-changes/restaurant.component-citystate.ts b/src/angular/9-form-value-changes/restaurant.component-citystate.ts index 83118c3d9..cf1791306 100644 --- a/src/angular/9-form-value-changes/restaurant.component-citystate.ts +++ b/src/angular/9-form-value-changes/restaurant.component-citystate.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { Subject, takeUntil } from 'rxjs'; import { Restaurant } from './restaurant'; import { @@ -20,7 +20,10 @@ export interface Data { styleUrls: ['./restaurant.component.less'], }) export class RestaurantComponent implements OnInit, OnDestroy { - form: FormGroup = this.createForm(); + form: FormGroup<{ + state: FormControl; + city: FormControl; + }> = this.createForm(); restaurants: Data = { value: [], @@ -54,38 +57,40 @@ export class RestaurantComponent implements OnInit, OnDestroy { this.onDestroy$.complete(); } - createForm(): FormGroup { - return this.fb.group({ + createForm(): FormGroup<{ + state: FormControl; + city: FormControl; + }> { + return this.fb.nonNullable.group({ state: { value: '', disabled: true }, city: { value: '', disabled: true }, }); } onChanges(): void { - let state: string = this.form.get('state')?.value; + let state: string = this.form.controls.state.value; - this.form - .get('state') - ?.valueChanges.pipe(takeUntil(this.onDestroy$)) + this.form.controls.state.valueChanges + .pipe(takeUntil(this.onDestroy$)) .subscribe((val) => { this.restaurants.value = []; if (val) { // only enable city if state has value - this.form.get('city')?.enable({ + this.form.controls.city.enable({ onlySelf: true, emitEvent: false, }); // if state has a value and has changed, clear previous city value if (state !== val) { - this.form.get('city')?.patchValue(''); + this.form.controls.city.patchValue(''); } // fetch cities based on state val this.getCities(val); } else { // disable city if no value - this.form.get('city')?.disable({ + this.form.controls.city.disable({ onlySelf: true, emitEvent: false, }); @@ -93,9 +98,8 @@ export class RestaurantComponent implements OnInit, OnDestroy { state = val; }); - this.form - .get('city') - ?.valueChanges.pipe(takeUntil(this.onDestroy$)) + this.form.controls.city.valueChanges + .pipe(takeUntil(this.onDestroy$)) .subscribe((val) => { if (val) { this.getRestaurants(); @@ -110,7 +114,7 @@ export class RestaurantComponent implements OnInit, OnDestroy { .subscribe((res: ResponseData) => { this.states.value = res.data; this.states.isPending = false; - this.form.get('state')?.enable(); + this.form.controls.state.enable(); }); } @@ -122,7 +126,7 @@ export class RestaurantComponent implements OnInit, OnDestroy { .subscribe((res: ResponseData) => { this.cities.value = res.data; this.cities.isPending = false; - this.form.get('city')?.enable({ + this.form.controls.city.enable({ onlySelf: true, emitEvent: false, }); diff --git a/src/angular/9-form-value-changes/restaurant.component.ts b/src/angular/9-form-value-changes/restaurant.component.ts index 2701c02fa..e5502c355 100644 --- a/src/angular/9-form-value-changes/restaurant.component.ts +++ b/src/angular/9-form-value-changes/restaurant.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { Subject, takeUntil } from 'rxjs'; import { Restaurant } from './restaurant'; import { ResponseData, RestaurantService } from './restaurant.service'; @@ -15,7 +15,10 @@ export interface Data { styleUrls: ['./restaurant.component.less'], }) export class RestaurantComponent implements OnInit, OnDestroy { - form: FormGroup = this.createForm(); + form: FormGroup<{ + state: FormControl; + city: FormControl; + }> = this.createForm(); restaurants: Data = { value: [], @@ -52,16 +55,14 @@ export class RestaurantComponent implements OnInit, OnDestroy { this.restaurants.isPending = false; }); - this.form - .get('state') - ?.valueChanges.pipe(takeUntil(this.onDestroy$)) + this.form.controls.state.valueChanges + .pipe(takeUntil(this.onDestroy$)) .subscribe((val) => { console.log('state', val); }); - this.form - .get('city') - ?.valueChanges.pipe(takeUntil(this.onDestroy$)) + this.form.controls.city.valueChanges + .pipe(takeUntil(this.onDestroy$)) .subscribe((val) => { console.log('city', val); }); @@ -72,8 +73,11 @@ export class RestaurantComponent implements OnInit, OnDestroy { this.onDestroy$.complete(); } - createForm(): FormGroup { - return this.fb.group({ + createForm(): FormGroup<{ + state: FormControl; + city: FormControl; + }> { + return this.fb.nonNullable.group({ state: { value: '', disabled: false }, city: { value: '', disabled: false }, }); From cc1f2d68cb67599dabfa3d50633f9a75cba6e93e Mon Sep 17 00:00:00 2001 From: fabioemoutinho Date: Sun, 7 Jan 2024 17:48:15 -0300 Subject: [PATCH 13/29] update declarative-state --- .../11-declarative-state/declarative-state.md | 2 +- .../restaurant.component.ts | 34 +++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/angular/11-declarative-state/declarative-state.md b/src/angular/11-declarative-state/declarative-state.md index 8d6f10e71..b56b0c5ac 100644 --- a/src/angular/11-declarative-state/declarative-state.md +++ b/src/angular/11-declarative-state/declarative-state.md @@ -226,7 +226,7 @@ You'll also add new single-responsibility streams: ✏️ Update __src/app/restaurant/restaurant.component.ts__ @sourceref ./restaurant.component.ts -@highlight 3-15, 29-34, 44-51, 59-137, 141-143 +@highlight 3-15, 29-34, 47-54, 62-140, 144-146 ✏️ Update **src/app/restaurant/restaurant.component.html** diff --git a/src/angular/11-declarative-state/restaurant.component.ts b/src/angular/11-declarative-state/restaurant.component.ts index 796673fa6..119516cf8 100644 --- a/src/angular/11-declarative-state/restaurant.component.ts +++ b/src/angular/11-declarative-state/restaurant.component.ts @@ -1,5 +1,5 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup } from '@angular/forms'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; import { combineLatest, map, @@ -39,7 +39,10 @@ const toData = map( styleUrls: ['./restaurant.component.less'], }) export class RestaurantComponent implements OnInit, OnDestroy { - form: FormGroup = this.createForm(); + form: FormGroup<{ + state: FormControl; + city: FormControl; + }> = this.createForm(); states$: Observable>; cities$: Observable>; @@ -56,13 +59,13 @@ export class RestaurantComponent implements OnInit, OnDestroy { private restaurantService: RestaurantService, private fb: FormBuilder ) { - this.selectedState$ = this.form - .get('state')! - .valueChanges.pipe(startWith('')); + this.selectedState$ = this.form.controls.state.valueChanges.pipe( + startWith('') + ); - this.selectedCity$ = this.form - .get('city')! - .valueChanges.pipe(startWith('')); + this.selectedCity$ = this.form.controls.city.valueChanges.pipe( + startWith('') + ); this.states$ = this.restaurantService .getStates() @@ -76,7 +79,7 @@ export class RestaurantComponent implements OnInit, OnDestroy { takeUntil(this.onDestroy$), tap((states) => { if (states.value.length > 0) { - this.form.get('state')!.enable(); + this.form.controls.state.enable(); } }) ); @@ -98,12 +101,12 @@ export class RestaurantComponent implements OnInit, OnDestroy { takeUntil(this.onDestroy$), tap((cities) => { if (cities.value.length === 0) { - this.form.get('city')!.disable({ + this.form.controls.city.disable({ onlySelf: true, emitEvent: false, }); } else { - this.form.get('city')!.enable({ + this.form.controls.city.enable({ onlySelf: true, emitEvent: false, }); @@ -116,7 +119,7 @@ export class RestaurantComponent implements OnInit, OnDestroy { pairwise(), tap(([previous, current]) => { if (current && current !== previous) { - this.form.get('city')!.patchValue(''); + this.form.controls.city.patchValue(''); } }) ); @@ -148,8 +151,11 @@ export class RestaurantComponent implements OnInit, OnDestroy { this.onDestroy$.complete(); } - createForm(): FormGroup { - return this.fb.group({ + createForm(): FormGroup<{ + state: FormControl; + city: FormControl; + }> { + return this.fb.nonNullable.group({ state: { value: '', disabled: true }, city: { value: '', disabled: true }, }); From c1ece8bdfefa0c3d461196340db61482ee299bb2 Mon Sep 17 00:00:00 2001 From: fabioemoutinho Date: Sun, 7 Jan 2024 18:57:35 -0300 Subject: [PATCH 14/29] update nested-routes --- src/angular/13-nested-routes/app.component.spec.ts | 3 +++ src/angular/13-nested-routes/detail.component.spec.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/angular/13-nested-routes/app.component.spec.ts b/src/angular/13-nested-routes/app.component.spec.ts index 706a8eafe..e6230a513 100644 --- a/src/angular/13-nested-routes/app.component.spec.ts +++ b/src/angular/13-nested-routes/app.component.spec.ts @@ -239,6 +239,7 @@ describe('AppComponent', () => { }); it('should render the HomeComponent with router navigates to "/" path', fakeAsync(() => { + fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; router.navigate(['']).then(() => { expect(location.path()).toBe(''); @@ -247,6 +248,7 @@ describe('AppComponent', () => { })); it('should render the RestaurantsComponent with router navigates to "/restaurants" path', fakeAsync(() => { + fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; router.navigate(['restaurants']).then(() => { expect(location.path()).toBe('/restaurants'); @@ -255,6 +257,7 @@ describe('AppComponent', () => { })); it('should render the DetailComponent with router navigates to "/restaurants/slug" path', fakeAsync(() => { + fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; router.navigate(['restaurants/crab-shack']).then(() => { expect(location.path()).toBe('/restaurants/crab-shack'); diff --git a/src/angular/13-nested-routes/detail.component.spec.ts b/src/angular/13-nested-routes/detail.component.spec.ts index af4ac69bf..f16d9da12 100644 --- a/src/angular/13-nested-routes/detail.component.spec.ts +++ b/src/angular/13-nested-routes/detail.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { of } from 'rxjs'; -import { ImageUrlPipe } from 'src/app/image-url.pipe'; +import { ImageUrlPipe } from '../../image-url.pipe'; import { RestaurantService } from '../restaurant.service'; import { DetailComponent } from './detail.component'; From f9ea1dc9e1ce2a2ec3731542732be067a3e53b72 Mon Sep 17 00:00:00 2001 From: fabioemoutinho Date: Sun, 7 Jan 2024 20:25:23 -0300 Subject: [PATCH 15/29] update building-order-form --- .../app.component.spec.ts | 4 +++ .../child-component/menu-items-1.component.ts | 8 ++--- .../child-component/menu-items-2.component.ts | 8 ++--- .../menu-items.component-props.ts | 8 ++--- .../child-component/order-2.component.html | 32 +++++++---------- .../child-component/order-2.component.ts | 35 +++++++++++++------ .../order.component-childcomponent.html | 28 ++++++--------- .../order.component-props.html | 32 +++++++---------- .../menu-items.component.ts | 8 ++--- .../order.component-final.html | 32 +++++++---------- .../order.component-solution.ts | 31 +++++++++++----- .../order.component-starter.html | 30 ++++++---------- .../order.component-starter.ts | 16 ++++++++- .../order.component-withtabs.html | 30 ++++++---------- 14 files changed, 141 insertions(+), 161 deletions(-) diff --git a/src/angular/14-building-order-form/app.component.spec.ts b/src/angular/14-building-order-form/app.component.spec.ts index b8936cc48..ac624bd33 100644 --- a/src/angular/14-building-order-form/app.component.spec.ts +++ b/src/angular/14-building-order-form/app.component.spec.ts @@ -241,6 +241,7 @@ describe('AppComponent', () => { }); it('should render the HomeComponent with router navigates to "/" path', fakeAsync(() => { + fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; router.navigate(['']).then(() => { expect(location.path()).toBe(''); @@ -249,6 +250,7 @@ describe('AppComponent', () => { })); it('should render the RestaurantsComponent with router navigates to "/restaurants" path', fakeAsync(() => { + fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; router.navigate(['restaurants']).then(() => { expect(location.path()).toBe('/restaurants'); @@ -257,6 +259,7 @@ describe('AppComponent', () => { })); it('should render the DetailComponent with router navigates to "/restaurants/slug" path', fakeAsync(() => { + fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; router.navigate(['restaurants/crab-shack']).then(() => { expect(location.path()).toBe('/restaurants/crab-shack'); @@ -265,6 +268,7 @@ describe('AppComponent', () => { })); it('should render the OrderComponent with router navigates to "/restaurants/slug/order" path', fakeAsync(() => { + fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; router.navigate(['restaurants/crab-shack/order']).then(() => { expect(location.path()).toBe('/restaurants/crab-shack/order'); diff --git a/src/angular/14-building-order-form/child-component/menu-items-1.component.ts b/src/angular/14-building-order-form/child-component/menu-items-1.component.ts index b8c46b448..98ed784e9 100644 --- a/src/angular/14-building-order-form/child-component/menu-items-1.component.ts +++ b/src/angular/14-building-order-form/child-component/menu-items-1.component.ts @@ -1,9 +1,5 @@ import { Component, Input, OnInit } from '@angular/core'; - -interface Item { - name: string; - price: number; -} +import { Item } from '../order.component'; @Component({ selector: 'pmo-menu-items', @@ -11,7 +7,7 @@ interface Item { styleUrls: ['./menu-items.component.less'], }) export class MenuItemsComponent implements OnInit { - @Input() items?: Item[]; + @Input() items: Item[] = []; selectedItems: Item[] = []; constructor() {} diff --git a/src/angular/14-building-order-form/child-component/menu-items-2.component.ts b/src/angular/14-building-order-form/child-component/menu-items-2.component.ts index 674b5300e..23bcd8ad3 100644 --- a/src/angular/14-building-order-form/child-component/menu-items-2.component.ts +++ b/src/angular/14-building-order-form/child-component/menu-items-2.component.ts @@ -1,9 +1,5 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; - -interface Item { - name: string; - price: number; -} +import { Item } from '../order.component'; @Component({ selector: 'pmo-menu-items', @@ -11,7 +7,7 @@ interface Item { styleUrls: ['./menu-items.component.less'], }) export class MenuItemsComponent implements OnInit { - @Input() items?: Item[]; + @Input() items: Item[] = []; @Output() itemsChanged: EventEmitter = new EventEmitter(); selectedItems: Item[] = []; diff --git a/src/angular/14-building-order-form/child-component/menu-items.component-props.ts b/src/angular/14-building-order-form/child-component/menu-items.component-props.ts index 9e6a87574..ad52f7387 100644 --- a/src/angular/14-building-order-form/child-component/menu-items.component-props.ts +++ b/src/angular/14-building-order-form/child-component/menu-items.component-props.ts @@ -1,9 +1,5 @@ import { Component, Input, OnInit } from '@angular/core'; - -interface Item { - name: string; - price: number; -} +import { Item } from '../order.component'; @Component({ selector: 'pmo-menu-items', @@ -11,7 +7,7 @@ interface Item { styleUrls: ['./menu-items.component.less'], }) export class MenuItemsComponent implements OnInit { - @Input() items?: Item[]; + @Input() items: Item[] = []; constructor() {} diff --git a/src/angular/14-building-order-form/child-component/order-2.component.html b/src/angular/14-building-order-form/child-component/order-2.component.html index ff62a1b2c..2db374d77 100644 --- a/src/angular/14-building-order-form/child-component/order-2.component.html +++ b/src/angular/14-building-order-form/child-component/order-2.component.html @@ -2,20 +2,24 @@

Order here

-
+ - +
- +
@@ -23,29 +27,17 @@

Order here

- +

Please enter your name.

- +

Please enter your address.

- +

Please enter your phone number.

diff --git a/src/angular/14-building-order-form/child-component/order-2.component.ts b/src/angular/14-building-order-form/child-component/order-2.component.ts index fd19ca699..a41418883 100644 --- a/src/angular/14-building-order-form/child-component/order-2.component.ts +++ b/src/angular/14-building-order-form/child-component/order-2.component.ts @@ -3,6 +3,7 @@ import { AbstractControl, FormArray, FormBuilder, + FormControl, FormGroup, ValidationErrors, ValidatorFn, @@ -14,6 +15,19 @@ import { takeUntil } from 'rxjs/operators'; import { Restaurant } from '../restaurant/restaurant'; import { RestaurantService } from '../restaurant/restaurant.service'; +export interface Item { + name: string; + price: number; +} + +interface OrderForm { + restaurant: FormControl; + name: FormControl; + address: FormControl; + phone: FormControl; + items: FormControl; +} + // CUSTOM VALIDATION FUNCTION TO ENSURE THAT THE ITEMS FORM VALUE CONTAINS AT LEAST ONE ITEM. function minLengthArray(min: number): ValidatorFn { return (c: AbstractControl): ValidationErrors | null => { @@ -30,7 +44,7 @@ function minLengthArray(min: number): ValidatorFn { styleUrls: ['./order.component.less'], }) export class OrderComponent implements OnInit, OnDestroy { - orderForm?: FormGroup; + orderForm?: FormGroup; restaurant?: Restaurant; isLoading = true; items?: FormArray; @@ -68,26 +82,25 @@ export class OrderComponent implements OnInit, OnDestroy { } createOrderForm(): void { - this.orderForm = this.formBuilder.group({ + this.orderForm = this.formBuilder.nonNullable.group({ restaurant: [this.restaurant?._id], - name: [null, Validators.required], - address: [null, Validators.required], - phone: [null, Validators.required], + name: ['', Validators.required], + address: ['', Validators.required], + phone: ['', Validators.required], // PASSING OUR CUSTOM VALIDATION FUNCTION TO THIS FORM CONTROL - items: [[], minLengthArray(1)], + items: [[] as Item[], minLengthArray(1)], }); this.onChanges(); } - getChange(newItems: []): void { - this.orderForm?.get('items')?.patchValue(newItems); + getChange(newItems: Item[]): void { + this.orderForm?.controls.items.patchValue(newItems); } onChanges(): void { // WHEN THE ITEMS CHANGE WE WANT TO CALCULATE A NEW TOTAL - this.orderForm - ?.get('items') - ?.valueChanges.pipe(takeUntil(this.onDestroy$)) + this.orderForm?.controls.items.valueChanges + .pipe(takeUntil(this.onDestroy$)) .subscribe((val) => { let total = 0.0; if (val.length) { diff --git a/src/angular/14-building-order-form/child-component/order.component-childcomponent.html b/src/angular/14-building-order-form/child-component/order.component-childcomponent.html index e1d107e0c..0b1ee26bf 100644 --- a/src/angular/14-building-order-form/child-component/order.component-childcomponent.html +++ b/src/angular/14-building-order-form/child-component/order.component-childcomponent.html @@ -2,14 +2,18 @@

Order here

- + - +
- +
@@ -17,29 +21,17 @@

Order here

- +

Please enter your name.

- +

Please enter your address.

- +

Please enter your phone number.

diff --git a/src/angular/14-building-order-form/child-component/order.component-props.html b/src/angular/14-building-order-form/child-component/order.component-props.html index c2ae72240..cf5f615d2 100644 --- a/src/angular/14-building-order-form/child-component/order.component-props.html +++ b/src/angular/14-building-order-form/child-component/order.component-props.html @@ -2,44 +2,36 @@

Order here

- + - +
    - +
- +
    - +
- +

Please enter your name.

- +

Please enter your address.

- +

Please enter your phone number.

diff --git a/src/angular/14-building-order-form/menu-items.component.ts b/src/angular/14-building-order-form/menu-items.component.ts index 374320043..eaad24e3b 100644 --- a/src/angular/14-building-order-form/menu-items.component.ts +++ b/src/angular/14-building-order-form/menu-items.component.ts @@ -1,10 +1,6 @@ import { Component, forwardRef, Input } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; - -interface Item { - name: string; - price: number; -} +import { Item } from '../order.component'; @Component({ selector: 'pmo-menu-items', @@ -19,7 +15,7 @@ interface Item { ], }) export class MenuItemsComponent implements ControlValueAccessor { - @Input() items?: Item[]; + @Input() items: Item[] = []; @Input('value') _value: Item[] = []; constructor() {} diff --git a/src/angular/14-building-order-form/order.component-final.html b/src/angular/14-building-order-form/order.component-final.html index 124043185..dbf9ec815 100644 --- a/src/angular/14-building-order-form/order.component-final.html +++ b/src/angular/14-building-order-form/order.component-final.html @@ -2,20 +2,24 @@

Order here

- + - +
- +
@@ -23,29 +27,17 @@

Order here

- +

Please enter your name.

- +

Please enter your address.

- +

Please enter your phone number.

diff --git a/src/angular/14-building-order-form/order.component-solution.ts b/src/angular/14-building-order-form/order.component-solution.ts index b24c354d3..e9a30ba20 100644 --- a/src/angular/14-building-order-form/order.component-solution.ts +++ b/src/angular/14-building-order-form/order.component-solution.ts @@ -3,6 +3,7 @@ import { AbstractControl, FormArray, FormBuilder, + FormControl, FormGroup, ValidationErrors, ValidatorFn, @@ -14,6 +15,19 @@ import { takeUntil } from 'rxjs/operators'; import { Restaurant } from '../restaurant/restaurant'; import { RestaurantService } from '../restaurant/restaurant.service'; +export interface Item { + name: string; + price: number; +} + +interface OrderForm { + restaurant: FormControl; + name: FormControl; + address: FormControl; + phone: FormControl; + items: FormControl; +} + // CUSTOM VALIDATION FUNCTION TO ENSURE THAT THE ITEMS FORM VALUE CONTAINS AT LEAST ONE ITEM. function minLengthArray(min: number): ValidatorFn { return (c: AbstractControl): ValidationErrors | null => { @@ -30,7 +44,7 @@ function minLengthArray(min: number): ValidatorFn { styleUrls: ['./order.component.less'], }) export class OrderComponent implements OnInit, OnDestroy { - orderForm?: FormGroup; + orderForm?: FormGroup; restaurant?: Restaurant; isLoading = true; items?: FormArray; @@ -68,22 +82,21 @@ export class OrderComponent implements OnInit, OnDestroy { } createOrderForm(): void { - this.orderForm = this.formBuilder.group({ + this.orderForm = this.formBuilder.nonNullable.group({ restaurant: [this.restaurant?._id], - name: [null, Validators.required], - address: [null, Validators.required], - phone: [null, Validators.required], + name: ['', Validators.required], + address: ['', Validators.required], + phone: ['', Validators.required], // PASSING OUR CUSTOM VALIDATION FUNCTION TO THIS FORM CONTROL - items: [[], minLengthArray(1)], + items: [[] as Item[], minLengthArray(1)], }); this.onChanges(); } onChanges(): void { // WHEN THE ITEMS CHANGE WE WANT TO CALCULATE A NEW TOTAL - this.orderForm - ?.get('items') - ?.valueChanges.pipe(takeUntil(this.onDestroy$)) + this.orderForm?.controls.items.valueChanges + .pipe(takeUntil(this.onDestroy$)) .subscribe((val) => { let total = 0.0; if (val.length) { diff --git a/src/angular/14-building-order-form/order.component-starter.html b/src/angular/14-building-order-form/order.component-starter.html index 7ed04c9de..3c59c6629 100644 --- a/src/angular/14-building-order-form/order.component-starter.html +++ b/src/angular/14-building-order-form/order.component-starter.html @@ -2,13 +2,17 @@

Order here

- +

Lunch Menu

- +

Dinner Menu