{"id":13547496,"url":"https://github.com/mraible/ng-demo","last_synced_at":"2025-04-06T19:13:01.333Z","repository":{"id":9065883,"uuid":"60705741","full_name":"mraible/ng-demo","owner":"mraible","description":"🦴 Bare Bones Angular and Angular CLI Tutorial","archived":false,"fork":false,"pushed_at":"2024-04-08T13:14:25.000Z","size":32435,"stargazers_count":185,"open_issues_count":1,"forks_count":114,"subscribers_count":8,"default_branch":"main","last_synced_at":"2024-04-09T00:47:21.936Z","etag":null,"topics":["angular","angular-cli","angular-material","auth0","bootstrap","electron","github-actions","heroku","jenkins","okta"],"latest_commit_sha":null,"homepage":"https://github.com/mraible/ng-demo#readme","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mraible.png","metadata":{"files":{"readme":"README.adoc","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2016-06-08T14:50:02.000Z","updated_at":"2024-04-15T16:08:25.169Z","dependencies_parsed_at":"2023-02-15T12:46:35.468Z","dependency_job_id":"e56077fc-eb8c-4075-a696-48da704f41a4","html_url":"https://github.com/mraible/ng-demo","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mraible%2Fng-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mraible%2Fng-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mraible%2Fng-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mraible%2Fng-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mraible","download_url":"https://codeload.github.com/mraible/ng-demo/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247535519,"owners_count":20954576,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["angular","angular-cli","angular-material","auth0","bootstrap","electron","github-actions","heroku","jenkins","okta"],"created_at":"2024-08-01T12:00:56.984Z","updated_at":"2025-04-06T19:13:01.294Z","avatar_url":"https://github.com/mraible.png","language":"TypeScript","funding_links":[],"categories":["TypeScript"],"sub_categories":[],"readme":"= \u0026#x1F9B4; Bare Bones Angular and Angular CLI Tutorial\n\n:author: Matt Raible\n:email:  matt@raibledesigns.com\n:revnumber: 17.0.0\n:revdate: {docdate}\n:subject: Angular and Angular CLI\n:keywords: Angular, Angular CLI, TypeScript, JavaScript, Node, npm, Jasmine, Cypress\n:icons: font\n:lang: en\n:language: javadocript\n:sourcedir: .\nifndef::env-github[]\n:icons: font\nendif::[]\nifdef::env-github,env-browser[]\n:toc: preamble\n:toclevels: 2\nendif::[]\nifdef::env-github[]\n:status:\n:outfilesuffix: .adoc\n:!toc-title:\n:caution-caption: :fire:\n:important-caption: :exclamation:\n:note-caption: :paperclip:\n:tip-caption: :bulb:\n:warning-caption: :warning:\nendif::[]\n:toc: macro\n:source-highlighter: highlight.js\n\nIMPORTANT: For a book of this tutorial, please check out https://www.infoq.com/minibooks/angular-mini-book/[The Angular Mini-Book]. Its \"Build an Angular App\" chapter was inspired by this tutorial.\n\nThis tutorial shows you how to build a bare-bones search and edit application using https://angular.io[Angular] and\nhttps://github.com/angular/angular-cli[Angular CLI] version 17.\n\ntoc::[]\n\nifdef::env-github[]\nTIP: It appears you're reading this document on GitHub. If you want a prettier view, install https://chrome.google.com/webstore/detail/asciidoctorjs-live-previe/iaalpfgpbocpdfblpnhhgllgbdbchmia[Asciidoctor.js Live Preview for Chrome], then view the https://raw.githubusercontent.com/mraible/ng-demo/main/README.adoc?toc=left[raw document]. Another option is to use the https://gist.asciidoctor.org/?github-mraible%2Fng-demo%2Fmain%2F%2FREADME.adoc[DocGist view].\nendif::[]\n\n.Source Code\n****\nIf you'd like to get right to it, the https://github.com/mraible/ng-demo[source is on GitHub]. To run the app, use `ng serve`. To test it, run `ng test`. To run its integration tests, run `ng e2e`.\n****\n\nCheck out the *bonus section* at the end of this document for Angular Material, Bootstrap, Auth0, and Electron tutorials.\n\ntoc::[]\n\n== What you'll build\n\nYou'll build a simple web application with Angular CLI, a tool for Angular development. You'll create an application with search and edit features.\n\n== What you'll need\n\n* About 30 minutes.\n* A favorite text editor or IDE. I recommend https://www.jetbrains.com/idea/[IntelliJ IDEA].\n* http://nodejs.org/[Node.js] and npm installed.\n* https://github.com/angular/angular-cli[Angular CLI] installed.\n\nIf you don't have Angular CLI installed, install it:\n\n----\nnpm install -g @angular/cli@17\n----\n\nNOTE: IntelliJ IDEA Ultimate Edition has the best support for TypeScript. If you'd rather not pay for your IDE, checkout https://code.visualstudio.com/[Visual Studio Code].\n\n== Create a new Angular project\n\nCreate a new project using the `ng new` command:\n\n----\nng new ng-demo\n----\n\nWhen prompted for the stylesheet format, choose \"`CSS`\" (the default). Accept the default (No) for SSR (Server-Side Rendering) and SSG (Static Site Generation).\n\nThis will create a `ng-demo` project and run `npm install` in it. It takes about a minute to complete, but will vary based on your internet connection speed.\n\nYou can see the version of Angular CLI you're using with the `ng version` command.\n\n----\n$ ng version\n\nAngular CLI: 17.0.5\nNode: 18.18.2\nPackage Manager: npm 9.8.1\nOS: darwin arm64\n\nAngular:\n...\n\nPackage                      Version\n------------------------------------------------------\n@angular-devkit/architect    0.1700.5 (cli-only)\n@angular-devkit/core         17.0.5 (cli-only)\n@angular-devkit/schematics   17.0.5 (cli-only)\n@schematics/angular          17.0.5 (cli-only)\n----\n\nIf you run this command from the `ng-demo` directory, you'll see even more information.\n\n----\n...\n\nAngular: 17.0.5\n... animations, cli, common, compiler, compiler-cli, core, forms\n... platform-browser, platform-browser-dynamic, router\n\nPackage                         Version\n---------------------------------------------------------\n@angular-devkit/architect       0.1700.5\n@angular-devkit/build-angular   17.0.5\n@angular-devkit/core            17.0.5\n@angular-devkit/schematics      17.0.5\n@schematics/angular             17.0.5\nrxjs                            7.8.1\ntypescript                      5.2.2\nzone.js                         0.14.2\n----\n\n== Run the application\n\nThe project is configured with a simple web server for development. To start it, run:\n\n----\nng serve\n----\n\nYou should see a screen like the one below at http://localhost:4200.\n\n[[default-homepage]]\n.Default homepage\nimage::src/assets/images/default-homepage.png[Default Homepage, 800, scaledwidth=\"100%\"]\n\nYou can make sure your new project's tests pass, run `ng test`:\n\n----\n$ ng test\n...\n...: Executed 3 of 3 SUCCESS (0.048 secs / 0.044 secs)\n----\n\n== Add a search feature\n\nTo add a search feature, open the project in an IDE or your favorite text editor.\n\n=== The Basics\n\nIn a terminal window, cd into the `ng-demo` directory and run the following command to create a search component.\n\n[source]\n----\nng g component search\n----\n\nOpen `src/app/search/search.component.html` and replace its default HTML with the following:\n\n[source,html]\n.src/app/search/search.component.html\n----\n\u003ch2\u003eSearch\u003c/h2\u003e\n\u003cform\u003e\n  \u003cinput type=\"search\" name=\"query\" [(ngModel)]=\"query\" (keyup.enter)=\"search()\"\u003e\n  \u003cbutton type=\"button\" (click)=\"search()\"\u003eSearch\u003c/button\u003e\n\u003c/form\u003e\n\u003cpre\u003e{{searchResults | json}}\u003c/pre\u003e\n----\n\nAdd a `query` property to `src/app/search/search.component.ts`. While you're there, add a `searchResults` property and an empty `search()` method.\n\n[source,typescript]\n.src/app/search/search.component.ts\n----\nexport class SearchComponent implements OnInit {\n  query: string | undefined;\n  searchResults: any;\n\n  constructor() { }\n\n  ngOnInit(): void { }\n\n  search(): void { }\n\n}\n----\n\nIn `src/app/app.routes.ts`, modify the `routes` constant to add `SearchComponent` as the default:\n\n[source,typescript]\n.src/app/app.routes.ts\n----\nimport { Routes } from '@angular/router';\nimport { SearchComponent } from './search/search.component';\n\nexport const routes: Routes = [\n  { path: 'search', component: SearchComponent },\n  { path: '', redirectTo: '/search', pathMatch: 'full' }\n];\n----\n\nRun `ng serve` again you will see a compilation error.\n\n----\n⠹ Building...✘ [ERROR] NG8002: Can't bind to 'ngModel' since it\n  isn't a known property of 'input'. [plugin angular-compiler]\n----\n\nTo solve this, open `search.component.ts`. Import `FormsModule` and `JsonPipe`:\n\n[source,typescript]\n.src/app/search/search.component.ts\n----\nimport { Component, OnInit } from '@angular/core';\nimport { FormsModule } from '@angular/forms';\nimport { JsonPipe } from '@angular/common';\n\n@Component({\n  selector: 'app-search',\n  standalone: true,\n  imports: [FormsModule, JsonPipe],\n  templateUrl: './search.component.html',\n  styleUrl: './search.component.css'\n})\n----\n\nNow you should be able to see the search form.\n\n[[search-component]]\n.Search component\nimage::src/assets/images/search-without-css.png[Search component, 800, scaledwidth=\"100%\"]\n\nIf yours looks different, it's because I trimmed my `app.component.html` to the bare minimum.\n\n[source,html]\n.src/app/app.component.html\n----\n\u003ch1\u003eWelcome to {{ title }}!\u003c/h1\u003e\n\n\u003crouter-outlet\u003e\u003c/router-outlet\u003e\n----\n\nIf you want to add styling for this component, open `src/app/search/search.component.css` and add some CSS. For example:\n\n[source,css]\n.src/app/search/search.component.css\n----\n:host {\n  display: block;\n  padding: 0 20px;\n}\n----\n\nIMPORTANT: The `:host` allows you to target the container of the component. It's the only way to target the host element. You can't reach the host element from inside the component with other selectors because it's not part of the component's own template.\n\nThis section has shown you how to generate a new component and add it to a basic Angular application with Angular CLI. The next section shows you how to create and use a JSON file and `localStorage` to create a fake API.\n\n=== The Backend\n\nTo get search results, create a `SearchService` that makes HTTP requests to a JSON file. Start by generating a new service.\n\n----\nng g service shared/search/search\n----\n\nCreate `src/assets/data/people.json` to hold your data.\n\n----\nmkdir -p src/assets/data\n----\n\n[source,json]\n.src/assets/data/people.json\n----\n[\n  {\n    \"id\": 1,\n    \"name\": \"Nikola Jokić\",\n    \"phone\": \"(720) 555-1212\",\n    \"address\": {\n      \"street\": \"2000 16th Street\",\n      \"city\": \"Denver\",\n      \"state\": \"CO\",\n      \"zip\": \"80202\"\n    }\n  },\n  {\n    \"id\": 2,\n    \"name\": \"Jamal Murray\",\n    \"phone\": \"(303) 321-8765\",\n    \"address\": {\n      \"street\": \"2654 Washington Street\",\n      \"city\": \"Lakewood\",\n      \"state\": \"CO\",\n      \"zip\": \"80568\"\n    }\n  },\n  {\n    \"id\": 3,\n    \"name\": \"Aaron Gordon\",\n    \"phone\": \"(303) 323-1233\",\n    \"address\": {\n      \"street\": \"46 Creekside Way\",\n      \"city\": \"Winter Park\",\n      \"state\": \"CO\",\n      \"zip\": \"80482\"\n    }\n  }\n]\n----\n\nModify `src/app/shared/search/search.service.ts` and provide `HttpClient` as a dependency in its constructor.\n\nIn this same file, create a `getAll()` method to gather all the people. Also, define the `Address` and `Person` classes that JSON will be marshalled to.\n\n[source,typescript]\n.src/app/shared/search/search.service.ts\n----\nimport { Injectable } from '@angular/core';\nimport { HttpClient } from '@angular/common/http';\nimport { Observable } from 'rxjs';\n\n@Injectable({\n  providedIn: 'root'\n})\nexport class SearchService {\n\n  constructor(private http: HttpClient) { }\n\n  getAll(): Observable\u003cPerson[]\u003e {\n    return this.http.get\u003cPerson[]\u003e('assets/data/people.json');\n  }\n}\n\nexport class Address {\n  street: string;\n  city: string;\n  state: string;\n  zip: string;\n\n  constructor(obj?: any) {\n    this.street = obj?.street || null;\n    this.city = obj?.city || null;\n    this.state = obj?.state || null;\n    this.zip = obj?.zip || null;\n  }\n}\n\nexport class Person {\n  id: number;\n  name: string;\n  phone: string;\n  address: Address;\n\n  constructor(obj?: any) {\n    this.id = obj?.id || null;\n    this.name = obj?.name || null;\n    this.phone = obj?.phone || null;\n    this.address = obj?.address || null;\n  }\n}\n----\n\nTo make these classes easier to consume by your components, create `src/app/shared/index.ts` and add the following:\n\n[source,typescript]\n.src/app/shared/index.ts\n----\nexport * from './search/search.service';\n----\n\nThe reason for creating this file is so you can import multiple classes on a single line rather than having to import each individual class on separate lines.\n\nIn `search.component.ts`, add imports for these classes.\n\n[source,typescript]\n.src/app/search/search.component.ts\n----\nimport { Person, SearchService } from '../shared';\n----\n\nYou can now add a proper type to the `searchResults` variable. While you're there, modify the constructor to inject the `SearchService`.\n\n[source,typescript]\n.src/app/search/search.component.ts\n----\nexport class SearchComponent implements OnInit {\n  query: string | undefined;\n  searchResults: Person[] = [];\n\n  constructor(private searchService: SearchService) { }\n----\n\nThen update the `search()` method to call the service's `getAll()` method.\n\n[source,typescript]\n.src/app/search/search.component.ts\n----\nsearch(): void {\n  this.searchService.getAll().subscribe({\n    next: (data: Person[]) =\u003e {\n      this.searchResults = data;\n    },\n    error: error =\u003e console.log(error)\n  });\n}\n----\n\nAt this point, if your app is running, you'll see the following message in your browser's console.\n\n----\nNullInjectorError: No provider for _HttpClient!\n----\n\nTo fix the \"`No provider`\" error from above, update `app.config.ts` to import and use `provideHttpClient`.\n\n[source,typescript]\n.src/app/app.config.ts\n----\nimport { provideHttpClient } from '@angular/common/http';\n\nexport const appConfig: ApplicationConfig = {\n  providers: [provideRouter(routes), provideHttpClient()]\n};\n----\n\nNow clicking the search button should work. To make the results look better, remove the `\u003cpre\u003e` tag and replace it with the following in `search.component.html`.\n\n[source,xml]\n.src/app/search/search.component.html\n----\n@if (searchResults.length) {\n\u003ctable\u003e\n  \u003cthead\u003e\n  \u003ctr\u003e\n    \u003cth\u003eName\u003c/th\u003e\n    \u003cth\u003ePhone\u003c/th\u003e\n    \u003cth\u003eAddress\u003c/th\u003e\n  \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n  @for (person of searchResults; track person; let i = $index) {\n  \u003ctr\u003e\n    \u003ctd\u003e{{person.name}}\u003c/td\u003e\n    \u003ctd\u003e{{person.phone}}\u003c/td\u003e\n    \u003ctd\u003e{{person.address.street}}\u003cbr/\u003e\n      {{person.address.city}}, {{person.address.state}} {{person.address.zip}}\n    \u003c/td\u003e\n  \u003c/tr\u003e\n  }\n  \u003c/tbody\u003e\n\u003c/table\u003e\n}\n----\n\nThen add some additional CSS to `search.component.css` to improve its table layout.\n\n[source,css]\n.src/app/search/search.component.css\n----\ntable {\n  margin-top: 10px;\n  border-collapse: collapse;\n}\n\nth {\n  text-align: left;\n  border-bottom: 2px solid #ddd;\n  padding: 8px;\n}\n\ntd {\n  border-top: 1px solid #ddd;\n  padding: 8px;\n}\n----\n\nNow the search results look better.\n\n[[search-results]]\n.Search results\nimage::src/assets/images/search-results.png[Search Results, 800, scaledwidth=\"100%\"]\n\nBut wait, you still don't have search functionality! To add a search feature, add a `search()` method to `SearchService`.\n\n[source,typescript]\n.src/app/shared/search/search.service.ts\n----\nimport { map, Observable } from 'rxjs';\n...\n\n  search(q: string): Observable\u003cPerson[]\u003e {\n    if (!q || q === '*') {\n      q = '';\n    } else {\n      q = q.toLowerCase();\n    }\n    return this.getAll().pipe(\n      map((data: Person[]) =\u003e data\n        .filter((item: Person) =\u003e JSON.stringify(item).toLowerCase().includes(q)))\n    );\n  }\n----\n\nThen refactor `SearchComponent` to call this method with its `query` variable.\n\n[source,typescript]\n.src/app/search/search.component.ts\n----\nsearch(): void {\n  this.searchService.search(this.query).subscribe({\n    next: (data: Person[]) =\u003e {\n      this.searchResults = data;\n    },\n    error: error =\u003e console.log(error)\n  });\n}\n----\n\nThis won't compile right away.\n\n[source,shell]\n----\n[ERROR] TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.\n  Type 'undefined' is not assignable to type 'string'. [plugin angular-compiler]\n\n----\n\nSince `query` will always be assigned (even if it's empty), change its variable declaration to:\n\n[source,typescript]\n----\nquery!: string; // query: string = ''; will also work\n----\n\nThis is called a https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#definite-assignment-assertions[definite assignment assertion]. It's a way to tell TypeScript \"`I know what I'm doing, the variable will be assigned.`\"\n\nNow search results will be filtered by the query value you type in.\n\nThis section showed you how to fetch and display search results. The next section builds on this and shows how to edit and save a record.\n\n== Add an edit feature\n\nModify `search.component.html` to wrap the person's name with a link.\n\n[source,html]\n.src/app/search/search.component.html\n----\n\u003ctd\u003e\u003ca [routerLink]=\"['/edit', person.id]\"\u003e{{person.name}}\u003c/a\u003e\u003c/td\u003e\n----\n\nAdd `RouterLink` as an import to `search.component.ts` so everything will compile:\n\n[source,typescript]\n.src/app/search/search.component.ts\n----\nimport { RouterLink } from '@angular/router';\n\n@Component({\n  selector: 'app-search',\n  standalone: true,\n  imports: [FormsModule, JsonPipe, RouterLink],\n  ...\n})\n----\n\nRun the following command to generate an `EditComponent`.\n\n[source]\n----\nng g component edit\n----\n\nAdd a route for this component in `app.routes.ts`:\n\n[source,typescript]\n.src/app/app.routes.ts\n----\nimport { EditComponent } from './edit/edit.component';\n\nconst routes: Routes = [\n  { path: 'search', component: SearchComponent },\n  { path: 'edit/:id', component: EditComponent },\n  { path: '', redirectTo: '/search', pathMatch: 'full' }\n];\n----\n\nUpdate `src/app/edit/edit.component.html` to display an editable form. You might notice I've added `id` attributes to most elements. This is to make it easier to locate elements when writing integration tests.\n\n[source,html]\n.src/app/edit/edit.component.html\n----\n@if (person) {\n\u003ch3\u003e{{person.name}}\u003c/h3\u003e\n  \u003cdiv\u003e\n    \u003clabel\u003eId:\u003c/label\u003e\n    {{person.id}}\n  \u003c/div\u003e\n  \u003cdiv\u003e\n    \u003clabel\u003eName:\u003c/label\u003e\n    \u003cinput [(ngModel)]=\"person.name\" name=\"name\" id=\"name\" placeholder=\"Name\"/\u003e\n  \u003c/div\u003e\n  \u003cdiv\u003e\n    \u003clabel\u003ePhone:\u003c/label\u003e\n    \u003cinput [(ngModel)]=\"person.phone\" name=\"phone\" id=\"phone\" placeholder=\"Phone\"/\u003e\n  \u003c/div\u003e\n  \u003cfieldset\u003e\n    \u003clegend\u003eAddress:\u003c/legend\u003e\n    \u003caddress\u003e\n      \u003cinput [(ngModel)]=\"person.address.street\" id=\"street\"\u003e\u003cbr/\u003e\n      \u003cinput [(ngModel)]=\"person.address.city\" id=\"city\"\u003e,\n      \u003cinput [(ngModel)]=\"person.address.state\" id=\"state\" size=\"2\"\u003e\n      \u003cinput [(ngModel)]=\"person.address.zip\" id=\"zip\" size=\"5\"\u003e\n    \u003c/address\u003e\n  \u003c/fieldset\u003e\n  \u003cbutton (click)=\"save()\" id=\"save\"\u003eSave\u003c/button\u003e\n  \u003cbutton (click)=\"cancel()\" id=\"cancel\"\u003eCancel\u003c/button\u003e\n}\n----\n\nModify `EditComponent` to import model and service classes and to use the `SearchService` to get data.\n\n[source,typescript]\n.src/app/edit/edit.component.ts\n----\nimport { Component, OnDestroy, OnInit } from '@angular/core';\nimport { Person, SearchService } from '../shared';\nimport { Subscription } from 'rxjs';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { FormsModule } from '@angular/forms';\n\n@Component({\n  selector: 'app-edit',\n  standalone: true,\n  imports: [FormsModule],\n  templateUrl: './edit.component.html',\n  styleUrl: './edit.component.css'\n})\nexport class EditComponent implements OnInit, OnDestroy {\n  person!: Person;\n  sub!: Subscription;\n\n  constructor(private route: ActivatedRoute,\n              private router: Router,\n              private service: SearchService) {\n  }\n\n  async ngOnInit(): Promise\u003cvoid\u003e {\n    this.sub = this.route.params.subscribe(params =\u003e {\n      const id = +params['id']; // (+) converts string 'id' to a number\n      this.service.get(id).subscribe(person =\u003e {\n        if (person) {\n          this.person = person;\n        } else {\n          this.gotoList();\n        }\n      });\n    });\n  }\n\n  ngOnDestroy(): void {\n    if (this.sub) {\n      this.sub.unsubscribe();\n    }\n  }\n\n  async cancel() {\n    await this.router.navigate(['/search']);\n  }\n\n  async save() {\n    this.service.save(this.person);\n    await this.gotoList();\n  }\n\n  async gotoList() {\n    if (this.person) {\n      await this.router.navigate(['/search', {term: this.person.name}]);\n    } else {\n      await this.router.navigate(['/search']);\n    }\n  }\n}\n----\n\nModify `SearchService` to contain functions for finding a person by their id and saving them. While you're in there, modify the `search()` method to be aware of updated objects in `localStorage`.\n\n[source,typescript]\n.src/app/shared/search/search.service.ts\n----\nsearch(q: string): Observable\u003cPerson[]\u003e {\n  if (!q || q === '*') {\n    q = '';\n  } else {\n    q = q.toLowerCase();\n  }\n  return this.getAll().pipe(\n    map((data: Person[]) =\u003e data\n      .map((item: Person) =\u003e !!localStorage['person' + item.id] ?\n        JSON.parse(localStorage['person' + item.id]) : item)\n      .filter((item: Person) =\u003e JSON.stringify(item).toLowerCase().includes(q))\n    ));\n}\n\nget(id: number): Observable\u003cPerson\u003e {\n  return this.getAll().pipe(map((all: Person[]) =\u003e {\n    if (localStorage['person' + id]) {\n      return JSON.parse(localStorage['person' + id]);\n    }\n    return all.find((e: Person) =\u003e e.id === id);\n  }));\n}\n\nsave(person: Person) {\n  localStorage['person' + person.id] = JSON.stringify(person);\n}\n----\n\nYou can add CSS to `src/app/edit/edit.component.css` if you want to make the form look a bit better.\n\n[source,css]\n.src/app/edit/edit.component.css\n----\n:host {\n  display: block;\n  padding: 0 20px;\n}\n\nbutton {\n  margin-top: 10px;\n}\n----\n\nAt this point, you should be able to search for a person and update their information.\n\n[[edit-form]]\n.Edit component\nimage::src/assets/images/edit-form.png[Edit form, 800, scaledwidth=\"100%\"]\n\nThe \u0026lt;form\u003e in `src/app/edit/edit.component.html` calls a `save()` function to update a person's data. You already implemented this above.\nThe function calls a `gotoList()` function that appends the person's name to the URL when sending the user back to the search screen.\n\n[source,typescript]\n.src/app/edit/edit.component.ts\n----\ngotoList() {\n  if (this.person) {\n    this.router.navigate(['/search', {term: this.person.name} ]);\n  } else {\n    this.router.navigate(['/search']);\n  }\n}\n----\n\nSince the `SearchComponent` doesn't execute a search automatically when you execute this URL, add the following logic to do so in its `ngOnInit()` method.\n\n[source,typescript]\n.src/app/search/search.component.ts\n----\nimport { ActivatedRoute, RouterLink } from '@angular/router';\nimport { Subscription } from 'rxjs';\n...\n\n  sub!: Subscription;\n\n  constructor(private searchService: SearchService, private route: ActivatedRoute) { }\n\n  ngOnInit(): void {\n    this.sub = this.route.params.subscribe(params =\u003e {\n      if (params['term']) {\n        this.query = decodeURIComponent(params['term']);\n        this.search();\n      }\n    });\n  }\n----\n\nYou'll want to implement `OnDestroy` and define the `ngOnDestroy` method to clean up this subscription.\n\n[source,typescript]\n.src/app/search/search.component.ts\n----\nimport { Component, OnDestroy, OnInit } from '@angular/core';\n\nexport class SearchComponent implements OnInit, OnDestroy {\n  ...\n\n  ngOnDestroy(): void {\n    if (this.sub) {\n      this.sub.unsubscribe();\n    }\n  }\n}\n----\n\nAfter making all these changes, you should be able to search/edit/update a person's information. If it works - nice job!\n\n=== Form Validation\n\nOne thing you might notice is you can clear any input element in the form and save it. At the very least, the `name` field should be required. Otherwise, there's nothing to click on in the search results.\n\nTo make name required, modify `edit.component.html` to add a `required` attribute to the name `\u003cinput\u003e` and bind it to Angular's validation with `#name=\"ngModel\"`. Add a `\u003cdiv\u003e` next to the field to display an error message when validation fails.\n\n[source,html]\n.src/app/edit/edit.component.html\n----\n\u003cinput [(ngModel)]=\"person.name\" name=\"name\" id=\"name\" placeholder=\"Name\" required #name=\"ngModel\"/\u003e\n\u003cdiv [hidden]=\"name.valid || name.pristine\" style=\"color: red\"\u003e\n  Name is required\n\u003c/div\u003e\n----\n\nYou'll also need to wrap everything in a `\u003cform\u003e` element. Add `\u003cform\u003e` after the `\u003ch3\u003e` tag and close it before the last `\u003c/div\u003e`. You'll also need to add an `(ngSubmit)` handler to the form, give it the name of `editForm`, and change the save button to be a regular submit button that's disabled when the form is invalid.\n\n[source,html]\n.src/app/edit/edit.component.html\n----\n\u003ch3\u003e{{person.name}}\u003c/h3\u003e\n\u003cform (ngSubmit)=\"save()\" #editForm=\"ngForm\"\u003e\n  ...\n  \u003cbutton type=\"submit\" id=\"save\" [disabled]=\"!editForm.form.valid\"\u003eSave\u003c/button\u003e\n  \u003cbutton (click)=\"cancel()\" id=\"cancel\"\u003eCancel\u003c/button\u003e\n\u003c/form\u003e\n----\n\nAfter making these changes, the name field will be required.\n\n[[edit-form-required]]\n.Edit form with validation\nimage::src/assets/images/edit-form-validation.png[Edit form with validation, 800, scaledwidth=\"100%\"]\n\nIn this screenshot, you might notice the address fields are blank and the save button is enabled. This is explained by the error in your console.\n\n----\nIf ngModel is used within a form tag, either the name attribute must be set or the form control must be defined as 'standalone' in ngModelOptions.\n\nExample 1: \u003cinput [(ngModel)]=\"person.firstName\" name=\"first\"\u003e\nExample 2: \u003cinput [(ngModel)]=\"person.firstName\" [ngModelOptions]=\"{standalone: true}\"\u003e\n----\n\nTo fix this, add a `name` attribute to all the address fields. For example:\n\n[source,html]\n.src/app/edit/edit.component.html\n----\n\u003caddress\u003e\n  \u003cinput [(ngModel)]=\"person.address.street\" name=\"street\" id=\"street\"\u003e\u003cbr/\u003e\n  \u003cinput [(ngModel)]=\"person.address.city\" name=\"city\" id=\"city\"\u003e,\n  \u003cinput [(ngModel)]=\"person.address.state\" name=\"state\" id=\"state\" size=\"2\"\u003e\n  \u003cinput [(ngModel)]=\"person.address.zip\" name=\"zip\" id=\"zip\" size=\"5\"\u003e\n\u003c/address\u003e\n----\n\nNow values display in all fields, `name` is required, and save is disabled when the form is invalid.\n\n[[edit-form-names]]\n.Edit form with names and validation\nimage::src/assets/images/edit-form-names.png[Edit form with names and validation, 800, scaledwidth=\"100%\"]\n\nTo learn more about forms and validation, see https://angular.io/guide/form-validation[Angular's Validating form input documentation].\n\n== Unit and End-to-End Testing\n\nNow that you've built an application, it's important to test it to ensure it works. The best reason for writing tests is to automate your testing. Without tests, you'll likely be testing manually. This manual testing will take longer and longer as your application grows.\n\nIn this section, you'll learn to use http://jasmine.github.io/[Jasmine] for unit testing controllers and https://www.cypress.io/[Cypress] for integration testing.\n\n=== Fix the Tests\n\nIf you run `ng test`, you'll likely get failures for the components and service you created. These failures will be solved as you complete the section below. The `ng test` command will start a process that listens for changes so all you need to do is edit/save files and tests will be automatically run again.\n\nTIP: You can use `x` and `f` prefixes in front of `describe` and `it` functions to _exclude_ or _only_ run a particular test.\n\n=== Fix the `AppComponent` test\n\nIf you changed the `app.component.html` template as I did, you'll need to modify `app.component.spec.ts` to account for the change in HTML. Change its last test to look for an `\u003ch1\u003e` element and the welcome message inside it.\n\n[source,ts]\n.src/app/app.component.spec.ts\n----\nit('should render title', () =\u003e {\n  const fixture = TestBed.createComponent(AppComponent);\n  fixture.detectChanges();\n  const compiled = fixture.nativeElement as HTMLElement;\n  expect(compiled.querySelector('h1')?.textContent).toContain('Welcome to ng-demo!');\n});\n----\n\nNow this test should pass.\n\n=== Unit test the SearchService\n\nModify `src/app/shared/search/search.service.spec.ts` and set up the test's infrastructure (a.k.a. `TestBed`) using `HttpClientTestingModule` and `HttpTestingController`.\n\n[source,typescript]\n.src/app/shared/search/search.service.spec.ts\n----\nimport { TestBed } from '@angular/core/testing';\nimport { SearchService } from './search.service';\nimport { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';\n\ndescribe('SearchService', () =\u003e {\n  let service: SearchService;\n  let httpMock: HttpTestingController;\n\n  beforeEach(async () =\u003e {\n    await TestBed.configureTestingModule({\n      imports: [HttpClientTestingModule],\n      providers: [SearchService]\n    });\n\n    service = TestBed.inject(SearchService);\n    httpMock = TestBed.inject(HttpTestingController);\n  });\n\n  it('should be created', () =\u003e {\n    expect(service).toBeTruthy();\n  });\n});\n----\n\nNow, you will likely see some errors about the test stubs that Angular CLI created for you. You can ignore these for now.\n\n[%autofit]\n----\nNullInjectorError: R3InjectorError(DynamicTestModule)[SearchService -\u003e HttpClient -\u003e HttpClient]:\n  NullInjectorError: No provider for HttpClient!\n\nNullInjectorError: R3InjectorError(DynamicTestModule)[ActivatedRoute -\u003e ActivatedRoute]:\n  NullInjectorError: No provider for ActivatedRoute!\n----\n\n`HttpTestingController` allows you to mock requests and use its `flush()` method to provide response values. Since the HTTP request methods return an `Observable`, you can subscribe to it and create expectations in the callback methods. Add the first test of `getAll()` to `search.service.spec.ts`.\n\nThe test below should be on the same level as `beforeEach`.\n\n[source,typescript]\n.src/app/shared/search/search.service.spec.ts\n----\nit('should retrieve all search results', () =\u003e {\n  const mockResponse = [\n    {name: 'Nikola Jokić'},\n    {name: 'Mike Malone'}\n  ];\n\n  service.getAll().subscribe((people: any) =\u003e {\n    expect(people.length).toBe(2);\n    expect(people[0].name).toBe('Nikola Jokić');\n    expect(people).toEqual(mockResponse);\n  });\n\n  const req = httpMock.expectOne('assets/data/people.json');\n  expect(req.request.method).toBe('GET');\n  req.flush(mockResponse);\n});\n----\n\nWhile you're there, add an `afterEach()` to verify requests.\n\n[source,typescript]\n.src/app/shared/search/search.service.spec.ts\n----\nafterEach(() =\u003e {\n  httpMock.verify();\n});\n----\n\nAdd a couple more tests for filtering by search term and fetching by id.\n\n[source,typescript]\n.src/app/shared/search/search.service.spec.ts\n----\nit('should filter by search term', () =\u003e {\n  const mockResponse = [{name: 'Nikola Jokić'}];\n\n  service.search('nik').subscribe((people: any) =\u003e {\n    expect(people.length).toBe(1);\n    expect(people[0].name).toBe('Nikola Jokić');\n  });\n\n  const req = httpMock.expectOne('assets/data/people.json');\n  expect(req.request.method).toBe('GET');\n  req.flush(mockResponse);\n});\n\nit('should fetch by id', () =\u003e {\n  const mockResponse = [\n    {id: 1, name: 'Nikola Jokić'},\n    {id: 2, name: 'Mike Malone'}\n  ];\n\n  service.get(2).subscribe((person: any) =\u003e {\n    expect(person.name).toBe('Mike Malone');\n  });\n\n  const req = httpMock.expectOne('assets/data/people.json');\n  expect(req.request.method).toBe('GET');\n  req.flush(mockResponse);\n});\n----\n\n=== Unit test the SearchComponent\n\nTo unit test the `SearchComponent`, you can mock the methods in `SearchService` with http://angular-tips.com/blog/2021/07/unit-testing-spies-and-mocks/[spies]. These allow you to _spy_ on functions to check if they were called.\n\nCreate `src/app/shared/search/mocks/routes.ts` to mock Angular's `Router` and `ActivatedRoute`.\n\n[source,typescript]\n.src/app/shared/search/mocks/routes.ts\n----\nimport { ActivatedRoute } from '@angular/router';\nimport { of } from 'rxjs';\n\nexport class MockActivatedRoute extends ActivatedRoute {\n\n  constructor(parameters?: { [key: string]: any; }) {\n    super();\n    // @ts-ignore\n    this.params = of(parameters);\n  }\n}\n\nexport class MockRouter {\n  navigate = jasmine.createSpy('navigate');\n}\n----\n\nWith this mock in place, you can `TestBed.configureTestingModule()` to set up `SearchComponent` to use it as a provider. In the second `beforeEach()`, you can see that the `search()` method is spied on and its results are mocked. The response isn't important in this case because you're just unit testing the `SearchComponent`.\n\n[source,typescript]\n.src/app/search/search.component.spec.ts\n----\nimport { ComponentFixture, TestBed } from '@angular/core/testing';\nimport { SearchComponent } from './search.component';\nimport { SearchService } from '../shared';\nimport { ActivatedRoute } from '@angular/router';\nimport { RouterTestingModule } from '@angular/router/testing';\nimport { FormsModule } from '@angular/forms';\nimport { MockActivatedRoute } from '../shared/search/mocks/routes';\nimport { of } from 'rxjs';\nimport { HttpClientTestingModule } from '@angular/common/http/testing';\n\ndescribe('SearchComponent', () =\u003e {\n  let component: SearchComponent;\n  let fixture: ComponentFixture\u003cSearchComponent\u003e;\n  let mockSearchService: SearchService;\n  let mockActivatedRoute: MockActivatedRoute;\n\n  beforeEach(async () =\u003e {\n    mockActivatedRoute = new MockActivatedRoute({term: 'nikola'});\n\n    await TestBed.configureTestingModule({\n      declarations: [],\n      providers: [\n        {provide: ActivatedRoute, useValue: mockActivatedRoute}\n      ],\n      imports: [FormsModule, RouterTestingModule, HttpClientTestingModule]\n    }).compileComponents();\n  });\n\n  beforeEach(() =\u003e {\n    // mock response\n    mockSearchService = TestBed.inject(SearchService);\n    mockSearchService.search = jasmine.createSpy().and.returnValue(of([]));\n\n    // initialize component\n    fixture = TestBed.createComponent(SearchComponent);\n    component = fixture.componentInstance;\n    fixture.detectChanges();\n  });\n\n  it('should create', () =\u003e {\n    expect(component).toBeTruthy();\n  });\n});\n----\n\nAdd two tests, one to verify a search term is used when it's set on the component, and a second to verify search is called when a term is passed in as a route parameter.\n\n[source,typescript]\n.src/app/search/search.component.spec.ts\n----\nit('should search when a term is set and search() is called', () =\u003e {\n  component = fixture.componentInstance;\n  component.query = 'J';\n  component.search();\n  expect(mockSearchService.search).toHaveBeenCalledWith('J');\n});\n\nit('should search automatically when a term is on the URL', () =\u003e {\n  fixture.detectChanges();\n  expect(mockSearchService.search).toHaveBeenCalledWith('nikola');\n});\n----\n\nUpdate the test for `EditComponent`, verifying fetching a single record works. Notice how you can access the component directly with `fixture.componentInstance`, or its rendered version with `fixture.nativeElement`.\n\n[source,typescript]\n.src/app/edit/edit.component.spec.ts\n----\nimport { EditComponent } from './edit.component';\nimport { TestBed } from '@angular/core/testing';\nimport { Address, Person, SearchService } from '../shared';\nimport { MockActivatedRoute, MockRouter } from '../shared/search/mocks/routes';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { FormsModule } from '@angular/forms';\nimport { of } from 'rxjs';\nimport { HttpClientTestingModule } from '@angular/common/http/testing';\n\ndescribe('EditComponent', () =\u003e {\n  let mockSearchService: SearchService;\n  let mockActivatedRoute: MockActivatedRoute;\n  let mockRouter: MockRouter;\n\n  beforeEach(async () =\u003e {\n    mockActivatedRoute = new MockActivatedRoute({id: 1});\n    mockRouter = new MockRouter();\n\n    await TestBed.configureTestingModule({\n      declarations: [],\n      providers: [\n        {provide: ActivatedRoute, useValue: mockActivatedRoute},\n        {provide: Router, useValue: mockRouter}\n      ],\n      imports: [FormsModule, HttpClientTestingModule]\n    }).compileComponents();\n\n    mockSearchService = TestBed.inject(SearchService);\n  });\n\n  it('should fetch a single record', () =\u003e {\n    const fixture = TestBed.createComponent(EditComponent);\n\n    const person = new Person({id: 1, name: 'Michael Porter Jr.'});\n    person.address = new Address({city: 'Denver'});\n\n    // mock response\n    spyOn(mockSearchService, 'get').and.returnValue(of(person));\n\n    // initialize component\n    fixture.detectChanges();\n\n    // verify service was called\n    expect(mockSearchService.get).toHaveBeenCalledWith(1);\n\n    // verify data was set on component when initialized\n    const editComponent = fixture.componentInstance;\n    expect(editComponent.person.address.city).toBe('Denver');\n\n    // verify HTML renders as expected\n    const compiled = fixture.nativeElement;\n    expect(compiled.querySelector('h3').innerHTML).toBe('Michael Porter Jr.');\n  });\n});\n----\n\nYou should see \"`Executed 11 of 11 [green]#SUCCESS#\" in the shell window that's running `ng test`. If you don't, try canceling the command and restarting.\n\n=== Integration test the search UI\n\nTo test if the application works end-to-end, you can write tests with http://www.cypress.io/[Cypress]. These are also known as integration tests since they test the _integration_ between all layers of your application.\n\nYou can use the official https://www.npmjs.com/package/@cypress/schematic[Cypress Angular Schematic] to add Cypress to your Angular project.\n\n----\nng add @cypress/schematic\n----\n\nWhen prompted to proceed and use Cypress for `ng e2e` and component testing, answer \"`Yes`\".\n\nThis will add Cypress as a dependency and create configuration files to work with Angular and TypeScript. Rename `cypress/e2e/spec.cy.ts` to `home.cy.ts` and change it to look for the title of your app.\n\n[source,typescript]\n.cypress/e2e/home.spec.ts\n----\ndescribe('Home', () =\u003e {\n  it('Visits the initial project page', () =\u003e {\n    cy.visit('/')\n    cy.contains('Welcome to ng-demo!')\n    cy.contains('Search')\n  })\n})\n----\n\nThen, run `ng e2e`. This will compile your app, start it on `http://localhost:4200`, and launch the Cypress Electron app. When prompted to choose a browser, select *Electron*.\n\n.Cypress Electron App\nimage::ng-demo/cypress-electron-app.png[Cypress Electron App, 800, scaledwidth=\"100%\", align=center]\n\nIf you click on the file name, it'll launch a browser and run the test. You can use this feature to step through your tests, find selectors for elements, and much more. You can learn more about Cypress' features at https://armno.in.th/2020/02/26/cypress-angular-integration-testing/[Setting up Cypress for an Angular Project].\n\nPersonally, I prefer the Protractor experience where you could just run the command, it'd run all the tests, and the user doesn't need to interact. You can do this with Cypress too!\n\nThe Cypress Angular Schematic added a few scripts to your `package.json`:\n\n[source,json]\n----\n\"scripts\": {\n  ...\n  \"e2e\": \"ng e2e\",\n  \"cypress:open\": \"cypress open\",\n  \"cypress:run\": \"cypress run\"\n}\n----\n\nTo use the no-interaction approach, you'll need to start your app:\n\n----\nnpm start\n----\n\nThen, run the Cypress tests for it in another window:\n\n----\nnpm run cypress:run\n----\n\n[TIP]\n====\nYou might notice Cypress creates a video. You can disable this by adding `video: false` to your `cypress.config.ts` file.\n\n[source,typescript]\n----\nexport default defineConfig({\n  e2e: { ... },\n  video: false,\n  component: { ... }\n})\n----\n====\n\nThe `npm run cypress:run` command will run a headless browser, so you won't see anything happening on your screen.\n\nIf you want to see the tests run, append `-- --browser chrome --headed` to the command. Add this to your `package.json` if you want to make it the default. See Cypress' https://docs.cypress.io/guides/guides/launching-browsers[launching browsers] documentation to see a list of supported browsers.\n\nYou can also install https://www.npmjs.com/package/concurrently[concurrently] so you can run multiple tasks with one command.\n\n[source,shell]\n----\nnpm install -D concurrently\n----\n\nThen, add a `cy:run` script to your `package.json`:\n\n[source,json]\n----\n\"scripts\": {\n  ...\n  \"cy:run\": \"concurrently \\\"ng serve\\\" \\\"cypress run\\\"\"\n}\n----\n\nThen, you can run `npm run cy:run` to start your app and continuously run end-to-end tests on it when you change files.\n\n=== Testing the search feature\n\nCreate another end-to-end test in `cypress/e2e/search.cy.ts` to verify the search feature works. Populate it with the following code:\n\n[source,typescript]\n.cypress/e2e/search.cy.ts\n----\ndescribe('Search', () =\u003e {\n\n  beforeEach(() =\u003e {\n    cy.visit('/search')\n  });\n\n  it('should have an input and search button', () =\u003e {\n    cy.get('app-root app-search form input').should('exist');\n    cy.get('app-root app-search form button').should('exist');\n  });\n\n  it('should allow searching', () =\u003e {\n    cy.get('input').type('A');\n    cy.get('button').click();\n    const list = cy.get('app-search table tbody tr');\n    list.should('have.length', 3);\n  });\n});\n----\n\n=== Testing the edit feature\n\nCreate a `cypress/e2e/edit.cy.ts` test to verify the `EditComponent` renders a person's information and that their information can be updated.\n\n[source,typescript]\n.cypress/e2e/edit.cy.ts\n----\ndescribe('Edit', () =\u003e {\n\n  beforeEach(() =\u003e {\n    cy.visit('/edit/1')\n  });\n\n  it('should allow viewing a person',  () =\u003e {\n    cy.get('h3').should('have.text', 'Nikola Jokić');\n    cy.get('#name').should('have.value', 'Nikola Jokić');\n    cy.get('#street').should('have.value', '2000 16th Street');\n    cy.get('#city').should('have.value', 'Denver');\n  });\n\n  it('should allow updating a name', () =\u003e {\n    cy.get('#name').type(' Rocks!');\n    cy.get('#save').click();\n    // verify one element matched this change\n    const list = cy.get('app-search table tbody tr');\n    list.should('have.length', 1);\n  });\n});\n----\n\nWith your app running, execute `npm run cypress:run` to verify all your end-to-end tests pass. You should see a success message similar to the one below in your terminal window.\n\n.Cypress success\nimage::src/assets/images/cypress-success.png[Cypress success, 800, scaledwidth=\"100%\", align=center]\n\nIf you made it this far and have all your specs passing - congratulations! You're well on your way to writing quality code with Angular and verifying it works.\n\nYou can see the test coverage of your project by running `ng test --no-watch --code-coverage`.\n\nYou'll see a print out of code coverage in your terminal window.\n\n----\n=============================== Coverage summary ===============================\nStatements   : 81.96% ( 50/61 )\nBranches     : 70% ( 21/30 )\nFunctions    : 83.33% ( 25/30 )\nLines        : 81.03% ( 47/58 )\n================================================================================\n----\n\nYou can also open `coverage/ng-demo/index.html` in your browser.\n\nYou might notice that the `EditComponent` could use some additional coverage. If you feel the need to improve this coverage, please create a pull request!\n\n[[test-coverage]]\n.Test coverage\nimage::src/assets/images/test-coverage.png[Test coverage, 800, scaledwidth=\"100%\"]\n\n== Continuous Integration\n\nAt the time of this writing, Angular CLI did not have any continuous integration support. This section shows you how to set up continuous integration with https://github.com/features/actions[GitHub Actions] and https://jenkins.io/[Jenkins].\n\n=== GitHub Actions\n\nIf you've checked your project into GitHub, you can use GitHub Actions.\n\nCreate a `.github/workflows/main.yml` file. Add the following YAML to it. This will run both unit tests and integration tests with Cypress.\n\n[source,yaml]\n----\nname: Angular\n\non: [push, pull_request]\n\njobs:\n  build:\n    name: Build and Test\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: Use Node 18\n        uses: actions/setup-node@v4\n        with:\n          node-version: 18\n      - name: Install latest Chrome\n        run: |\n          sudo apt update\n          sudo apt --only-upgrade install google-chrome-stable\n          google-chrome --version\n      - name: Install dependencies\n        run: npm ci\n      - name: Run unit tests\n        run: xvfb-run npm test -- --watch=false\n      - name: Run integration tests\n        uses: cypress-io/github-action@v6\n        with:\n          browser: chrome\n          start: npm start\n          install: false\n          wait-on: http://localhost:4200\n----\n\nCheck it in on a branch, create a pull request for that branch, and you should see your tests running.\n\n=== Jenkins\n\nIf you've checked your project into source control, you can use Jenkins to automate testing.\n\n. Create a `Jenkinsfile` in the root directory and commit/push it.\n+\n[source,groovy]\n----\nnode {\n    def nodeHome = tool name: 'node-18', type: 'jenkins.plugins.nodejs.tools.NodeJSInstallation'\n    env.PATH = \"${nodeHome}/bin:${env.PATH}\"\n\n    stage('check tools') {\n        sh \"node -v\"\n        sh \"npm -v\"\n    }\n\n    stage('checkout') {\n        checkout scm\n    }\n\n    stage('npm install') {\n        sh \"npm install\"\n    }\n\n    stage('unit tests') {\n        sh \"npm test -- --watch=false\"\n    }\n\n    stage('cypress tests') {\n        sh \"npm start \u0026\"\n        sh \"npm run cypress:run\"\n    }\n}\n----\n\n[start=2]\n. Install https://jenkins.io/download/[Jenkins] on your hard drive and start it.\n. Login to Jenkins at `http://localhost:8080` and install the Node.js plugin.\n. Go to **Manage Jenkins** \u003e **Global Tool Configuration** \u003e **NodeJS**. Install and configure the name of your Node.js installation to match your build script.\n. Create a new project with **Dashboard** \u003e **New Item** \u003e **Pipeline** \u003e **Pipeline script from SCM** (near the bottom). Point it at your project's repository and specify the `main` branch.\n. Click **Save**, then **Build Now** on the following screen.\n\n== Deployment to Heroku\n\nThis section shows you how to deploy an Angular app to https://heroku.com[Heroku].\n\nhttps://signup.heroku.com/[Create a Heroku account], https://devcenter.heroku.com/articles/heroku-cli[install the heroku CLI], and run `heroku login`.\n\nRun `heroku create` to create an app on Heroku.\n\nCreate a `config/nginx.conf.erb` file with the configuration for secure headers and redirect all HTTP requests to HTTPS.\n\n[source,ruby]\n----\ndaemon off;\n# Heroku dynos have at least 4 cores.\nworker_processes \u003c%= ENV['NGINX_WORKERS'] || 4 %\u003e;\n\nevents {\n\tuse epoll;\n\taccept_mutex on;\n\tworker_connections \u003c%= ENV['NGINX_WORKER_CONNECTIONS'] || 1024 %\u003e;\n}\n\nhttp {\n\tgzip on;\n\tgzip_comp_level 2;\n\tgzip_min_length 512;\n\tgzip_proxied any; # Heroku router sends Via header\n\n\tserver_tokens off;\n\n\tlog_format l2met 'measure#nginx.service=$request_time request_id=$http_x_request_id';\n\taccess_log \u003c%= ENV['NGINX_ACCESS_LOG_PATH'] || 'logs/nginx/access.log' %\u003e l2met;\n\terror_log \u003c%= ENV['NGINX_ERROR_LOG_PATH'] || 'logs/nginx/error.log' %\u003e;\n\n\tinclude mime.types;\n\tdefault_type application/octet-stream;\n\tsendfile on;\n\n\t# Must read the body in 5 seconds.\n\tclient_body_timeout \u003c%= ENV['NGINX_CLIENT_BODY_TIMEOUT'] || 5 %\u003e;\n\n\tserver {\n\t\tlisten \u003c%= ENV[\"PORT\"] %\u003e;\n\t\tserver_name _;\n\t\tkeepalive_timeout 5;\n\t\tclient_max_body_size \u003c%= ENV['NGINX_CLIENT_MAX_BODY_SIZE'] || 1 %\u003eM;\n\n\t\troot dist/ng-demo;\n\t\tindex index.html;\n\n\t\tlocation / {\n\t\t\ttry_files $uri /index.html;\n\t\t}\n\n\t\tadd_header Content-Security-Policy \"default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; frame-ancestors 'none'; connect-src 'self' https://*.auth0.com https://*.herokuapp.com\";\n\t\tadd_header Referrer-Policy \"no-referrer, strict-origin-when-cross-origin\";\n\t\tadd_header Strict-Transport-Security \"max-age=63072000; includeSubDomains\";\n\t\tadd_header X-Content-Type-Options nosniff;\n\t\tadd_header X-Frame-Options DENY;\n\t\tadd_header X-XSS-Protection \"1; mode=block\";\n\t\tadd_header Permissions-Policy \"geolocation=(self), microphone=(), accelerometer=(), camera=()\";\n\t}\n}\n----\n\nNOTE: In this code, you might notice that some https URLs are allowed in the content security policy. Those are there so this app can make XHR requests to those domains when that functionality is added.\n\nFor `config/nginx.conf.erb` to be read, you have to use the https://elements.heroku.com/buildpacks/heroku/heroku-buildpack-nginx[Heroku NGINX buildpack].\n\nAdd a `Procfile` to the root of your project.\n\n[source,shell]\n----\nweb: bin/start-nginx-solo\n----\n\nCommit your changes to Git, add the Node.js + NGINX buildpack, and redeploy your Angular app using `git push`.\n\n[source,shell]\n----\ngit add .\ngit commit -m \"Configure secure headers and nginx buildpack\"\nheroku buildpacks:add heroku/nodejs\nheroku buildpacks:add heroku-community/nginx\ngit push heroku main\n----\n\nView the application in your browser with `heroku open`. Try your app's URL on \u003chttps://securityheaders.com\u003e to be pleasantly surprised.\n\nTIP: You can watch your app's logs using `heroku logs --tail`.\n\n== Source code\n\nA completed project with this code in it is available on GitHub at https://github.com/mraible/ng-demo.\n\n== Summary\n\nI hope you've enjoyed this in-depth tutorial on how to get started with Angular and Angular CLI. Angular CLI takes much of the pain out of setting up an Angular project and using Typescript. I expect great things from Angular CLI, mostly because the Angular setup process can be tedious and CLI greatly simplifies things.\n\n== Bonus: Angular Material, Bootstrap, Auth0, and Electron\n\nIf you'd like to see how to integrate https://material.angular.io/[Angular Material], https://getbootstrap.com[Bootstrap], https://developer.auth0.com[Auth0], or https://www.electronjs.org/[Electron] this section is for you!\n\nI've created branches to show how to integrate each of these libraries. Click on the links below to see each branch's documentation.\n\n* https://github.com/mraible/ng-demo/tree/angular-material#bonus-angular-material[Angular Material]\n* https://github.com/mraible/ng-demo/tree/bootstrap#bonus-bootstrap[Bootstrap]\n* https://github.com/mraible/ng-demo/tree/auth0#bonus-auth0[Auth0]\n* https://github.com/mraible/ng-demo/tree/electron#bonus-electron[Electron]\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmraible%2Fng-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmraible%2Fng-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmraible%2Fng-demo/lists"}