├── LICENSE ├── README.md ├── part-1 ├── .editorconfig ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── app.component.html │ │ ├── app.component.ts │ │ └── app.module.ts │ ├── assets │ │ └── .gitkeep │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json ├── part-2 ├── .editorconfig ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── account │ │ │ ├── index.ts │ │ │ ├── login.component.html │ │ │ ├── login.component.ts │ │ │ ├── register.component.html │ │ │ └── register.component.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ └── home │ │ │ ├── home.component.html │ │ │ ├── home.component.ts │ │ │ └── index.ts │ ├── assets │ │ └── .gitkeep │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json ├── part-3 ├── .editorconfig ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── _helpers │ │ │ ├── auth.guard.ts │ │ │ ├── error.interceptor.ts │ │ │ ├── fake-backend.ts │ │ │ ├── index.ts │ │ │ └── jwt.interceptor.ts │ │ ├── _models │ │ │ ├── index.ts │ │ │ └── user.ts │ │ ├── _services │ │ │ ├── account.service.ts │ │ │ └── index.ts │ │ ├── account │ │ │ ├── index.ts │ │ │ ├── login.component.html │ │ │ ├── login.component.ts │ │ │ ├── register.component.html │ │ │ └── register.component.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ └── home │ │ │ ├── home.component.html │ │ │ ├── home.component.ts │ │ │ └── index.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json ├── part-4 ├── .editorconfig ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── _helpers │ │ │ ├── auth.guard.ts │ │ │ ├── error.interceptor.ts │ │ │ ├── fake-backend.ts │ │ │ ├── index.ts │ │ │ └── jwt.interceptor.ts │ │ ├── _models │ │ │ ├── index.ts │ │ │ └── user.ts │ │ ├── _services │ │ │ ├── account.service.ts │ │ │ └── index.ts │ │ ├── account │ │ │ ├── index.ts │ │ │ ├── login.component.html │ │ │ ├── login.component.ts │ │ │ ├── register.component.html │ │ │ └── register.component.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ └── home │ │ │ ├── home.component.html │ │ │ ├── home.component.ts │ │ │ └── index.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json ├── part-5 ├── .editorconfig ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── _components │ │ │ ├── alert.component.html │ │ │ ├── alert.component.ts │ │ │ └── index.ts │ │ ├── _helpers │ │ │ ├── auth.guard.ts │ │ │ ├── error.interceptor.ts │ │ │ ├── fake-backend.ts │ │ │ ├── index.ts │ │ │ └── jwt.interceptor.ts │ │ ├── _models │ │ │ ├── index.ts │ │ │ └── user.ts │ │ ├── _services │ │ │ ├── account.service.ts │ │ │ ├── alert.service.ts │ │ │ └── index.ts │ │ ├── account │ │ │ ├── index.ts │ │ │ ├── login.component.html │ │ │ ├── login.component.ts │ │ │ ├── register.component.html │ │ │ └── register.component.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ └── home │ │ │ ├── home.component.html │ │ │ ├── home.component.ts │ │ │ └── index.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json ├── part-6 ├── .editorconfig ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── _components │ │ │ ├── alert.component.html │ │ │ ├── alert.component.ts │ │ │ └── index.ts │ │ ├── _helpers │ │ │ ├── auth.guard.ts │ │ │ ├── error.interceptor.ts │ │ │ ├── fake-backend.ts │ │ │ ├── index.ts │ │ │ └── jwt.interceptor.ts │ │ ├── _models │ │ │ ├── index.ts │ │ │ └── user.ts │ │ ├── _services │ │ │ ├── account.service.ts │ │ │ ├── alert.service.ts │ │ │ └── index.ts │ │ ├── account │ │ │ ├── index.ts │ │ │ ├── login.component.html │ │ │ ├── login.component.ts │ │ │ ├── register.component.html │ │ │ └── register.component.ts │ │ ├── app-routing.module.ts │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── home │ │ │ ├── home.component.html │ │ │ ├── home.component.ts │ │ │ └── index.ts │ │ └── users │ │ │ ├── add-edit.component.html │ │ │ ├── add-edit.component.ts │ │ │ ├── list.component.html │ │ │ ├── list.component.ts │ │ │ ├── users-routing.module.ts │ │ │ └── users.module.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json ├── part-7 ├── .editorconfig ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ └── tasks.json ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── src │ ├── app │ │ ├── _components │ │ │ ├── alert.component.html │ │ │ ├── alert.component.ts │ │ │ └── index.ts │ │ ├── _helpers │ │ │ ├── auth.guard.ts │ │ │ ├── error.interceptor.ts │ │ │ ├── fake-backend.ts │ │ │ ├── index.ts │ │ │ └── jwt.interceptor.ts │ │ ├── _models │ │ │ ├── index.ts │ │ │ └── user.ts │ │ ├── _services │ │ │ ├── account.service.ts │ │ │ ├── alert.service.ts │ │ │ └── index.ts │ │ ├── account │ │ │ ├── index.ts │ │ │ ├── login.component.html │ │ │ ├── login.component.ts │ │ │ ├── register.component.html │ │ │ └── register.component.ts │ │ ├── app.component.html │ │ ├── app.component.ts │ │ ├── app.routes.ts │ │ ├── home │ │ │ ├── home.component.html │ │ │ ├── home.component.ts │ │ │ └── index.ts │ │ └── users │ │ │ ├── add-edit.component.html │ │ │ ├── add-edit.component.ts │ │ │ ├── list.component.html │ │ │ ├── list.component.ts │ │ │ └── users.routes.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json └── part-8 ├── .dockerignore ├── .editorconfig ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── tasks.json ├── Dockerfile ├── README.md ├── angular.json ├── nginx.conf ├── package-lock.json ├── package.json ├── src ├── app │ ├── _components │ │ ├── alert.component.html │ │ ├── alert.component.ts │ │ └── index.ts │ ├── _helpers │ │ ├── auth.guard.ts │ │ ├── error.interceptor.ts │ │ ├── fake-backend.ts │ │ ├── index.ts │ │ └── jwt.interceptor.ts │ ├── _models │ │ ├── index.ts │ │ └── user.ts │ ├── _services │ │ ├── account.service.ts │ │ ├── alert.service.ts │ │ └── index.ts │ ├── account │ │ ├── index.ts │ │ ├── login.component.html │ │ ├── login.component.ts │ │ ├── register.component.html │ │ └── register.component.ts │ ├── app.component.html │ ├── app.component.ts │ ├── app.routes.ts │ ├── home │ │ ├── home.component.html │ │ ├── home.component.ts │ │ └── index.ts │ └── users │ │ ├── add-edit.component.html │ │ ├── add-edit.component.ts │ │ ├── list.component.html │ │ ├── list.component.ts │ │ └── users.routes.ts ├── assets │ └── .gitkeep ├── environments │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts └── styles.css ├── tsconfig.app.json ├── tsconfig.json └── tsconfig.spec.json /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Jason Watmore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular 15/16 Free Course 2 | 3 | In this free step by step Angular course we'll be covering how to implement routing, authentication, registration and CRUD functionality in Angular. 4 | 5 | * [Part 1 - Create Base Project Structure](https://jasonwatmore.com/angular-15-free-course-1-create-base-project-structure) 6 | * [Part 2 - Add Routing & Multiple Pages](https://jasonwatmore.com/angular-15-free-course-2-add-routing-multiple-pages) 7 | * [Part 3 - Login Form, Authentication & Route Guard](https://jasonwatmore.com/angular-15-free-course-2-add-routing-multiple-pages) 8 | * [Part 4 - Registration Form & Service Methods](https://jasonwatmore.com/angular-15-free-course-4-registration-form-service-methods) 9 | * [Part 5 - Alerts & Home Page](https://jasonwatmore.com/angular-15-free-course-5-alerts-home-page) 10 | * [Part 6 - User Management (CRUD) Section](https://jasonwatmore.com/angular-15-free-course-5-alerts-home-page) 11 | * [Part 7 - Migrate to Standalone Components and Functional Interceptors](https://jasonwatmore.com/angular-15-16-free-course-7-migrate-to-standalone-components-and-functional-interceptors) 12 | * [Part 8 - Dockerize App with Nginx](https://jasonwatmore.com/angular-15-16-free-course-8-dockerize-app-with-nginx) 13 | -------------------------------------------------------------------------------- /part-1/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /part-1/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /part-1/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /part-1/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /part-1/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /part-1/README.md: -------------------------------------------------------------------------------- 1 | # AngularTutorial 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.0.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /part-1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-tutorial", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^16.0.0", 14 | "@angular/common": "^16.0.0", 15 | "@angular/compiler": "^16.0.0", 16 | "@angular/core": "^16.0.0", 17 | "@angular/forms": "^16.0.0", 18 | "@angular/platform-browser": "^16.0.0", 19 | "@angular/platform-browser-dynamic": "^16.0.0", 20 | "@angular/router": "^16.0.0", 21 | "rxjs": "~7.8.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.13.0" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "^16.0.0", 27 | "@angular/cli": "~16.0.0", 28 | "@angular/compiler-cli": "^16.0.0", 29 | "@types/jasmine": "~4.3.0", 30 | "jasmine-core": "~4.6.0", 31 | "karma": "~6.4.0", 32 | "karma-chrome-launcher": "~3.2.0", 33 | "karma-coverage": "~2.2.0", 34 | "karma-jasmine": "~5.1.0", 35 | "karma-jasmine-html-reporter": "~2.0.0", 36 | "typescript": "~5.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /part-1/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

Hello Angular!

-------------------------------------------------------------------------------- /part-1/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ selector: 'app-root', templateUrl: 'app.component.html' }) 4 | export class AppComponent { } -------------------------------------------------------------------------------- /part-1/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AppComponent } from './app.component'; 5 | 6 | @NgModule({ 7 | imports: [BrowserModule], 8 | declarations: [AppComponent], 9 | bootstrap: [AppComponent] 10 | }) 11 | export class AppModule { } -------------------------------------------------------------------------------- /part-1/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cornflourblue/angular-16-tutorial/a2d771bd813e937d38b1d47ea93bfc270905ee2c/part-1/src/assets/.gitkeep -------------------------------------------------------------------------------- /part-1/src/favicon.ico: -------------------------------------------------------------------------------- 1 | �PNG 2 |  3 | IHDR?�~� pHYs  ��~�fIDATH��WKLQ���̔�uG�� e�n�. 6qcb�l?���D`�F#� Ku�F 1Qc� 4 | ��!���� ��C�P�|B?$���ܱ3����I&}��}�̽s�[*�ɀU�A��K��yx�gY�Ajq��3L Š���˫�OD�4��3Ϗ:X�3��o�P J�ğo#IH�a����,{>1/�2$�R AR]�)w��?�sZw^��q�Y�m_��e���r[8�^� 5 | �&p��-���A}c��- ������!����2_) E�)㊪j���v�m��ZOi� g�nW�{�n.|�e2�a&�0aŸ����be�̀��C�fˤE%-{�ֺ��׮C��N��jXi�~c�C,t��T�����r�{� �L)s��V��6%�(�#ᤙ!�]��H�ҐH$R���^R�A�61(?Y舚�>���(Z����Qm�L2�K�ZIc�� 6 | ���̧�C��2!⅄�(����"�Go��>�q��=��$%�z`ѯ��T�&����PHh�Z!=���z��O��������,*VVV�1�f*CJ�]EE��K�k��d�#5���`2yT!�}7���߈~�,���zs�����y�T��V������D��C2�G��@%̑72Y�޾{oJ�"@��^h�~ ��fĬ�!a�D��6���Ha|�3��� [>�����]7U2п���]�ė 7 | ��PU� �.Wejq�in�g��+p<ߺQH����總j[������.��� Q���p _�K�� 1(��+��bB8�\ra 8 | �́�v.l���(���ǽ�w���L��w�8�C��IEND�B`� -------------------------------------------------------------------------------- /part-1/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Tutorial 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /part-1/src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | 6 | platformBrowserDynamic().bootstrapModule(AppModule) 7 | .catch(err => console.error(err)); 8 | -------------------------------------------------------------------------------- /part-1/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /part-1/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /part-1/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /part-1/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /part-2/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /part-2/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /part-2/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /part-2/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /part-2/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /part-2/README.md: -------------------------------------------------------------------------------- 1 | # AngularTutorial 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.0.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /part-2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-tutorial", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^16.0.0", 14 | "@angular/common": "^16.0.0", 15 | "@angular/compiler": "^16.0.0", 16 | "@angular/core": "^16.0.0", 17 | "@angular/forms": "^16.0.0", 18 | "@angular/platform-browser": "^16.0.0", 19 | "@angular/platform-browser-dynamic": "^16.0.0", 20 | "@angular/router": "^16.0.0", 21 | "rxjs": "~7.8.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.13.0" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "^16.0.0", 27 | "@angular/cli": "~16.0.0", 28 | "@angular/compiler-cli": "^16.0.0", 29 | "@types/jasmine": "~4.3.0", 30 | "jasmine-core": "~4.6.0", 31 | "karma": "~6.4.0", 32 | "karma-chrome-launcher": "~3.2.0", 33 | "karma-coverage": "~2.2.0", 34 | "karma-jasmine": "~5.1.0", 35 | "karma-jasmine-html-reporter": "~2.0.0", 36 | "typescript": "~5.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /part-2/src/app/account/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.component'; 2 | export * from './register.component'; -------------------------------------------------------------------------------- /part-2/src/app/account/login.component.html: -------------------------------------------------------------------------------- 1 |

Login Page

-------------------------------------------------------------------------------- /part-2/src/app/account/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ templateUrl: 'login.component.html' }) 4 | export class LoginComponent { } -------------------------------------------------------------------------------- /part-2/src/app/account/register.component.html: -------------------------------------------------------------------------------- 1 |

Register Page

-------------------------------------------------------------------------------- /part-2/src/app/account/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ templateUrl: 'register.component.html' }) 4 | export class RegisterComponent { } -------------------------------------------------------------------------------- /part-2/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { HomeComponent } from './home'; 5 | import { LoginComponent, RegisterComponent } from './account'; 6 | 7 | const routes: Routes = [ 8 | { path: '', component: HomeComponent }, 9 | { path: 'account/login', component: LoginComponent }, 10 | { path: 'account/register', component: RegisterComponent }, 11 | 12 | // otherwise redirect to home 13 | { path: '**', redirectTo: '' } 14 | ]; 15 | 16 | @NgModule({ 17 | imports: [RouterModule.forRoot(routes)], 18 | exports: [RouterModule] 19 | }) 20 | export class AppRoutingModule { } -------------------------------------------------------------------------------- /part-2/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 |
12 | 13 |
-------------------------------------------------------------------------------- /part-2/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ selector: 'app-root', templateUrl: 'app.component.html' }) 4 | export class AppComponent { } -------------------------------------------------------------------------------- /part-2/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | import { HomeComponent } from './home'; 7 | import { LoginComponent, RegisterComponent } from './account'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | BrowserModule, 12 | AppRoutingModule 13 | ], 14 | declarations: [ 15 | AppComponent, 16 | HomeComponent, 17 | LoginComponent, 18 | RegisterComponent 19 | ], 20 | bootstrap: [AppComponent] 21 | }) 22 | export class AppModule { } -------------------------------------------------------------------------------- /part-2/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |

Home Page

2 | -------------------------------------------------------------------------------- /part-2/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ templateUrl: 'home.component.html' }) 4 | export class HomeComponent { } -------------------------------------------------------------------------------- /part-2/src/app/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home.component'; -------------------------------------------------------------------------------- /part-2/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cornflourblue/angular-16-tutorial/a2d771bd813e937d38b1d47ea93bfc270905ee2c/part-2/src/assets/.gitkeep -------------------------------------------------------------------------------- /part-2/src/favicon.ico: -------------------------------------------------------------------------------- 1 | �PNG 2 |  3 | IHDR?�~� pHYs  ��~�fIDATH��WKLQ���̔�uG�� e�n�. 6qcb�l?���D`�F#� Ku�F 1Qc� 4 | ��!���� ��C�P�|B?$���ܱ3����I&}��}�̽s�[*�ɀU�A��K��yx�gY�Ajq��3L Š���˫�OD�4��3Ϗ:X�3��o�P J�ğo#IH�a����,{>1/�2$�R AR]�)w��?�sZw^��q�Y�m_��e���r[8�^� 5 | �&p��-���A}c��- ������!����2_) E�)㊪j���v�m��ZOi� g�nW�{�n.|�e2�a&�0aŸ����be�̀��C�fˤE%-{�ֺ��׮C��N��jXi�~c�C,t��T�����r�{� �L)s��V��6%�(�#ᤙ!�]��H�ҐH$R���^R�A�61(?Y舚�>���(Z����Qm�L2�K�ZIc�� 6 | ���̧�C��2!⅄�(����"�Go��>�q��=��$%�z`ѯ��T�&����PHh�Z!=���z��O��������,*VVV�1�f*CJ�]EE��K�k��d�#5���`2yT!�}7���߈~�,���zs�����y�T��V������D��C2�G��@%̑72Y�޾{oJ�"@��^h�~ ��fĬ�!a�D��6���Ha|�3��� [>�����]7U2п���]�ė 7 | ��PU� �.Wejq�in�g��+p<ߺQH����總j[������.��� Q���p _�K�� 1(��+��bB8�\ra 8 | �́�v.l���(���ǽ�w���L��w�8�C��IEND�B`� -------------------------------------------------------------------------------- /part-2/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Tutorial 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /part-2/src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | 6 | platformBrowserDynamic().bootstrapModule(AppModule) 7 | .catch(err => console.error(err)); 8 | -------------------------------------------------------------------------------- /part-2/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /part-2/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /part-2/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ] 26 | }, 27 | "angularCompilerOptions": { 28 | "enableI18nLegacyMessageIdFormat": false, 29 | "strictInjectionParameters": true, 30 | "strictInputAccessModifiers": true, 31 | "strictTemplates": true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /part-2/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /part-3/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /part-3/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /part-3/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /part-3/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /part-3/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /part-3/README.md: -------------------------------------------------------------------------------- 1 | # AngularTutorial 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.0.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /part-3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-tutorial", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^16.0.0", 14 | "@angular/common": "^16.0.0", 15 | "@angular/compiler": "^16.0.0", 16 | "@angular/core": "^16.0.0", 17 | "@angular/forms": "^16.0.0", 18 | "@angular/platform-browser": "^16.0.0", 19 | "@angular/platform-browser-dynamic": "^16.0.0", 20 | "@angular/router": "^16.0.0", 21 | "rxjs": "~7.8.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.13.0" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "^16.0.0", 27 | "@angular/cli": "~16.0.0", 28 | "@angular/compiler-cli": "^16.0.0", 29 | "@types/jasmine": "~4.3.0", 30 | "jasmine-core": "~4.6.0", 31 | "karma": "~6.4.0", 32 | "karma-chrome-launcher": "~3.2.0", 33 | "karma-coverage": "~2.2.0", 34 | "karma-jasmine": "~5.1.0", 35 | "karma-jasmine-html-reporter": "~2.0.0", 36 | "typescript": "~5.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /part-3/src/app/_helpers/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | 4 | import { AccountService } from '@app/_services'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class AuthGuard implements CanActivate { 8 | constructor( 9 | private router: Router, 10 | private accountService: AccountService 11 | ) { } 12 | 13 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 14 | const user = this.accountService.userValue; 15 | if (user) { 16 | // authorised so return true 17 | return true; 18 | } 19 | 20 | // not logged in so redirect to login page with the return url 21 | this.router.navigate(['/account/login'], { queryParams: { returnUrl: state.url } }); 22 | return false; 23 | } 24 | } -------------------------------------------------------------------------------- /part-3/src/app/_helpers/error.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; 3 | import { Observable, throwError } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | 6 | import { AccountService } from '@app/_services'; 7 | 8 | @Injectable() 9 | export class ErrorInterceptor implements HttpInterceptor { 10 | constructor(private accountService: AccountService) { } 11 | 12 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 13 | return next.handle(request).pipe(catchError(err => { 14 | if ([401, 403].includes(err.status) && this.accountService.userValue) { 15 | // auto logout if 401 or 403 response returned from api 16 | this.accountService.logout(); 17 | } 18 | 19 | const error = err.error?.message || err.statusText; 20 | console.error(err); 21 | return throwError(() => error); 22 | })) 23 | } 24 | } -------------------------------------------------------------------------------- /part-3/src/app/_helpers/fake-backend.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http'; 3 | import { Observable, of, throwError } from 'rxjs'; 4 | import { delay, materialize, dematerialize } from 'rxjs/operators'; 5 | 6 | let users = [{ id: 1, firstName: 'Jason', lastName: 'Watmore', username: 'test', password: 'test' }]; 7 | 8 | @Injectable() 9 | export class FakeBackendInterceptor implements HttpInterceptor { 10 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 11 | const { url, method, headers, body } = request; 12 | 13 | return handleRoute(); 14 | 15 | function handleRoute() { 16 | switch (true) { 17 | case url.endsWith('/users/authenticate') && method === 'POST': 18 | return authenticate(); 19 | default: 20 | // pass through any requests not handled above 21 | return next.handle(request); 22 | } 23 | } 24 | 25 | // route functions 26 | 27 | function authenticate() { 28 | const { username, password } = body; 29 | const user = users.find(x => x.username === username && x.password === password); 30 | if (!user) return error('Username or password is incorrect'); 31 | return ok({ 32 | ...basicDetails(user), 33 | token: 'fake-jwt-token' 34 | }) 35 | } 36 | 37 | // helper functions 38 | 39 | function ok(body?: any) { 40 | return of(new HttpResponse({ status: 200, body })) 41 | .pipe(delay(500)); // delay observable to simulate server api call 42 | } 43 | 44 | function error(message: string) { 45 | return throwError(() => ({ error: { message } })) 46 | .pipe(materialize(), delay(500), dematerialize()); // call materialize and dematerialize to ensure delay even if an error is thrown (https://github.com/Reactive-Extensions/RxJS/issues/648); 47 | } 48 | 49 | function basicDetails(user: any) { 50 | const { id, username, firstName, lastName } = user; 51 | return { id, username, firstName, lastName }; 52 | } 53 | } 54 | } 55 | 56 | export const fakeBackendProvider = { 57 | // use fake backend in place of Http service for backend-less development 58 | provide: HTTP_INTERCEPTORS, 59 | useClass: FakeBackendInterceptor, 60 | multi: true 61 | }; -------------------------------------------------------------------------------- /part-3/src/app/_helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.guard'; 2 | export * from './error.interceptor'; 3 | export * from './fake-backend'; 4 | export * from './jwt.interceptor'; -------------------------------------------------------------------------------- /part-3/src/app/_helpers/jwt.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { environment } from '@environments/environment'; 6 | import { AccountService } from '@app/_services'; 7 | 8 | @Injectable() 9 | export class JwtInterceptor implements HttpInterceptor { 10 | constructor(private accountService: AccountService) { } 11 | 12 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 13 | // add auth header with jwt if user is logged in and request is to the api url 14 | const user = this.accountService.userValue; 15 | const isLoggedIn = user?.token; 16 | const isApiUrl = request.url.startsWith(environment.apiUrl); 17 | if (isLoggedIn && isApiUrl) { 18 | request = request.clone({ 19 | setHeaders: { Authorization: `Bearer ${user.token}` } 20 | }); 21 | } 22 | 23 | return next.handle(request); 24 | } 25 | } -------------------------------------------------------------------------------- /part-3/src/app/_models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user'; -------------------------------------------------------------------------------- /part-3/src/app/_models/user.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | id?: string; 3 | username?: string; 4 | password?: string; 5 | firstName?: string; 6 | lastName?: string; 7 | token?: string; 8 | } -------------------------------------------------------------------------------- /part-3/src/app/_services/account.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { HttpClient } from '@angular/common/http'; 4 | import { BehaviorSubject, Observable } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | import { environment } from '@environments/environment'; 8 | import { User } from '@app/_models'; 9 | 10 | @Injectable({ providedIn: 'root' }) 11 | export class AccountService { 12 | private userSubject: BehaviorSubject; 13 | public user: Observable; 14 | 15 | constructor( 16 | private router: Router, 17 | private http: HttpClient 18 | ) { 19 | this.userSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('user')!)); 20 | this.user = this.userSubject.asObservable(); 21 | } 22 | 23 | public get userValue() { 24 | return this.userSubject.value; 25 | } 26 | 27 | login(username: string, password: string) { 28 | return this.http.post(`${environment.apiUrl}/users/authenticate`, { username, password }) 29 | .pipe(map(user => { 30 | // store user details and jwt token in local storage to keep user logged in between page refreshes 31 | localStorage.setItem('user', JSON.stringify(user)); 32 | this.userSubject.next(user); 33 | return user; 34 | })); 35 | } 36 | 37 | logout() { 38 | // remove user from local storage and set current user to null 39 | localStorage.removeItem('user'); 40 | this.userSubject.next(null); 41 | this.router.navigate(['/account/login']); 42 | } 43 | } -------------------------------------------------------------------------------- /part-3/src/app/_services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account.service'; -------------------------------------------------------------------------------- /part-3/src/app/account/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.component'; 2 | export * from './register.component'; -------------------------------------------------------------------------------- /part-3/src/app/account/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
{{error}}
3 |
4 |

Login

5 |
6 |
7 |
8 | 9 | 10 |
11 |
Username is required
12 |
13 |
14 |
15 | 16 | 17 |
18 |
Password is required
19 |
20 |
21 |
22 | 26 | Register 27 |
28 |
29 |
30 |
31 |
-------------------------------------------------------------------------------- /part-3/src/app/account/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 4 | import { first } from 'rxjs/operators'; 5 | 6 | import { AccountService } from '@app/_services' 7 | 8 | @Component({ templateUrl: 'login.component.html' }) 9 | export class LoginComponent implements OnInit { 10 | form!: FormGroup; 11 | loading = false; 12 | submitted = false; 13 | error?: string; 14 | 15 | constructor( 16 | private formBuilder: FormBuilder, 17 | private route: ActivatedRoute, 18 | private router: Router, 19 | private accountService: AccountService 20 | ) { 21 | // redirect to home if already logged in 22 | if (this.accountService.userValue) { 23 | this.router.navigate(['/']); 24 | } 25 | } 26 | 27 | ngOnInit() { 28 | this.form = this.formBuilder.group({ 29 | username: ['', Validators.required], 30 | password: ['', Validators.required] 31 | }); 32 | } 33 | 34 | // convenience getter for easy access to form fields 35 | get f() { return this.form.controls; } 36 | 37 | onSubmit() { 38 | this.submitted = true; 39 | 40 | // reset alert on submit 41 | this.error = ''; 42 | 43 | // stop here if form is invalid 44 | if (this.form.invalid) { 45 | return; 46 | } 47 | 48 | this.loading = true; 49 | this.accountService.login(this.f.username.value, this.f.password.value) 50 | .pipe(first()) 51 | .subscribe({ 52 | next: () => { 53 | // get return url from query parameters or default to home page 54 | const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; 55 | this.router.navigateByUrl(returnUrl); 56 | }, 57 | error: error => { 58 | this.error = error; 59 | this.loading = false; 60 | } 61 | }); 62 | } 63 | } -------------------------------------------------------------------------------- /part-3/src/app/account/register.component.html: -------------------------------------------------------------------------------- 1 |

Register Page

-------------------------------------------------------------------------------- /part-3/src/app/account/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ templateUrl: 'register.component.html' }) 4 | export class RegisterComponent { } -------------------------------------------------------------------------------- /part-3/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { HomeComponent } from './home'; 5 | import { LoginComponent, RegisterComponent } from './account'; 6 | import { AuthGuard } from './_helpers'; 7 | 8 | const routes: Routes = [ 9 | { path: '', component: HomeComponent, canActivate: [AuthGuard] }, 10 | { path: 'account/login', component: LoginComponent }, 11 | { path: 'account/register', component: RegisterComponent }, 12 | 13 | // otherwise redirect to home 14 | { path: '**', redirectTo: '' } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forRoot(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class AppRoutingModule { } -------------------------------------------------------------------------------- /part-3/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 |
11 | 12 |
-------------------------------------------------------------------------------- /part-3/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { AccountService } from './_services'; 4 | import { User } from './_models'; 5 | 6 | @Component({ selector: 'app-root', templateUrl: 'app.component.html' }) 7 | export class AppComponent { 8 | user?: User | null; 9 | 10 | constructor(private accountService: AccountService) { 11 | this.accountService.user.subscribe(x => this.user = x); 12 | } 13 | 14 | logout() { 15 | this.accountService.logout(); 16 | } 17 | } -------------------------------------------------------------------------------- /part-3/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 5 | 6 | // used to create fake backend 7 | import { fakeBackendProvider } from './_helpers'; 8 | 9 | import { AppRoutingModule } from './app-routing.module'; 10 | import { JwtInterceptor, ErrorInterceptor } from './_helpers'; 11 | import { AppComponent } from './app.component'; 12 | import { HomeComponent } from './home'; 13 | import { LoginComponent, RegisterComponent } from './account'; 14 | 15 | @NgModule({ 16 | imports: [ 17 | BrowserModule, 18 | ReactiveFormsModule, 19 | HttpClientModule, 20 | AppRoutingModule 21 | ], 22 | declarations: [ 23 | AppComponent, 24 | HomeComponent, 25 | LoginComponent, 26 | RegisterComponent 27 | ], 28 | providers: [ 29 | { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, 30 | { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }, 31 | 32 | // provider used to create fake backend 33 | fakeBackendProvider 34 | ], 35 | bootstrap: [AppComponent] 36 | }) 37 | export class AppModule { } -------------------------------------------------------------------------------- /part-3/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |

Home Page

2 | -------------------------------------------------------------------------------- /part-3/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ templateUrl: 'home.component.html' }) 4 | export class HomeComponent { } -------------------------------------------------------------------------------- /part-3/src/app/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home.component'; -------------------------------------------------------------------------------- /part-3/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cornflourblue/angular-16-tutorial/a2d771bd813e937d38b1d47ea93bfc270905ee2c/part-3/src/assets/.gitkeep -------------------------------------------------------------------------------- /part-3/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | apiUrl: 'http://localhost:4000' 3 | }; -------------------------------------------------------------------------------- /part-3/src/favicon.ico: -------------------------------------------------------------------------------- 1 | �PNG 2 |  3 | IHDR?�~� pHYs  ��~�fIDATH��WKLQ���̔�uG�� e�n�. 6qcb�l?���D`�F#� Ku�F 1Qc� 4 | ��!���� ��C�P�|B?$���ܱ3����I&}��}�̽s�[*�ɀU�A��K��yx�gY�Ajq��3L Š���˫�OD�4��3Ϗ:X�3��o�P J�ğo#IH�a����,{>1/�2$�R AR]�)w��?�sZw^��q�Y�m_��e���r[8�^� 5 | �&p��-���A}c��- ������!����2_) E�)㊪j���v�m��ZOi� g�nW�{�n.|�e2�a&�0aŸ����be�̀��C�fˤE%-{�ֺ��׮C��N��jXi�~c�C,t��T�����r�{� �L)s��V��6%�(�#ᤙ!�]��H�ҐH$R���^R�A�61(?Y舚�>���(Z����Qm�L2�K�ZIc�� 6 | ���̧�C��2!⅄�(����"�Go��>�q��=��$%�z`ѯ��T�&����PHh�Z!=���z��O��������,*VVV�1�f*CJ�]EE��K�k��d�#5���`2yT!�}7���߈~�,���zs�����y�T��V������D��C2�G��@%̑72Y�޾{oJ�"@��^h�~ ��fĬ�!a�D��6���Ha|�3��� [>�����]7U2п���]�ė 7 | ��PU� �.Wejq�in�g��+p<ߺQH����總j[������.��� Q���p _�K�� 1(��+��bB8�\ra 8 | �́�v.l���(���ǽ�w���L��w�8�C��IEND�B`� -------------------------------------------------------------------------------- /part-3/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Tutorial 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /part-3/src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | 6 | platformBrowserDynamic().bootstrapModule(AppModule) 7 | .catch(err => console.error(err)); 8 | -------------------------------------------------------------------------------- /part-3/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /part-3/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /part-3/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": false, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ], 26 | "paths": { 27 | "@app/*": ["src/app/*"], 28 | "@environments/*": ["src/environments/*"] 29 | } 30 | }, 31 | "angularCompilerOptions": { 32 | "enableI18nLegacyMessageIdFormat": false, 33 | "strictInjectionParameters": true, 34 | "strictInputAccessModifiers": true, 35 | "strictTemplates": true 36 | } 37 | } -------------------------------------------------------------------------------- /part-3/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /part-4/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /part-4/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /part-4/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /part-4/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /part-4/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /part-4/README.md: -------------------------------------------------------------------------------- 1 | # AngularTutorial 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.0.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /part-4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-tutorial", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^16.0.0", 14 | "@angular/common": "^16.0.0", 15 | "@angular/compiler": "^16.0.0", 16 | "@angular/core": "^16.0.0", 17 | "@angular/forms": "^16.0.0", 18 | "@angular/platform-browser": "^16.0.0", 19 | "@angular/platform-browser-dynamic": "^16.0.0", 20 | "@angular/router": "^16.0.0", 21 | "rxjs": "~7.8.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.13.0" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "^16.0.0", 27 | "@angular/cli": "~16.0.0", 28 | "@angular/compiler-cli": "^16.0.0", 29 | "@types/jasmine": "~4.3.0", 30 | "jasmine-core": "~4.6.0", 31 | "karma": "~6.4.0", 32 | "karma-chrome-launcher": "~3.2.0", 33 | "karma-coverage": "~2.2.0", 34 | "karma-jasmine": "~5.1.0", 35 | "karma-jasmine-html-reporter": "~2.0.0", 36 | "typescript": "~5.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /part-4/src/app/_helpers/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | 4 | import { AccountService } from '@app/_services'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class AuthGuard implements CanActivate { 8 | constructor( 9 | private router: Router, 10 | private accountService: AccountService 11 | ) { } 12 | 13 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 14 | const user = this.accountService.userValue; 15 | if (user) { 16 | // authorised so return true 17 | return true; 18 | } 19 | 20 | // not logged in so redirect to login page with the return url 21 | this.router.navigate(['/account/login'], { queryParams: { returnUrl: state.url } }); 22 | return false; 23 | } 24 | } -------------------------------------------------------------------------------- /part-4/src/app/_helpers/error.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; 3 | import { Observable, throwError } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | 6 | import { AccountService } from '@app/_services'; 7 | 8 | @Injectable() 9 | export class ErrorInterceptor implements HttpInterceptor { 10 | constructor(private accountService: AccountService) { } 11 | 12 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 13 | return next.handle(request).pipe(catchError(err => { 14 | if ([401, 403].includes(err.status) && this.accountService.userValue) { 15 | // auto logout if 401 or 403 response returned from api 16 | this.accountService.logout(); 17 | } 18 | 19 | const error = err.error?.message || err.statusText; 20 | console.error(err); 21 | return throwError(() => error); 22 | })) 23 | } 24 | } -------------------------------------------------------------------------------- /part-4/src/app/_helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.guard'; 2 | export * from './error.interceptor'; 3 | export * from './fake-backend'; 4 | export * from './jwt.interceptor'; -------------------------------------------------------------------------------- /part-4/src/app/_helpers/jwt.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { environment } from '@environments/environment'; 6 | import { AccountService } from '@app/_services'; 7 | 8 | @Injectable() 9 | export class JwtInterceptor implements HttpInterceptor { 10 | constructor(private accountService: AccountService) { } 11 | 12 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 13 | // add auth header with jwt if user is logged in and request is to the api url 14 | const user = this.accountService.userValue; 15 | const isLoggedIn = user?.token; 16 | const isApiUrl = request.url.startsWith(environment.apiUrl); 17 | if (isLoggedIn && isApiUrl) { 18 | request = request.clone({ 19 | setHeaders: { Authorization: `Bearer ${user.token}` } 20 | }); 21 | } 22 | 23 | return next.handle(request); 24 | } 25 | } -------------------------------------------------------------------------------- /part-4/src/app/_models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user'; -------------------------------------------------------------------------------- /part-4/src/app/_models/user.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | id?: string; 3 | username?: string; 4 | password?: string; 5 | firstName?: string; 6 | lastName?: string; 7 | token?: string; 8 | } -------------------------------------------------------------------------------- /part-4/src/app/_services/account.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { HttpClient } from '@angular/common/http'; 4 | import { BehaviorSubject, Observable } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | import { environment } from '@environments/environment'; 8 | import { User } from '@app/_models'; 9 | 10 | @Injectable({ providedIn: 'root' }) 11 | export class AccountService { 12 | private userSubject: BehaviorSubject; 13 | public user: Observable; 14 | 15 | constructor( 16 | private router: Router, 17 | private http: HttpClient 18 | ) { 19 | this.userSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('user')!)); 20 | this.user = this.userSubject.asObservable(); 21 | } 22 | 23 | public get userValue() { 24 | return this.userSubject.value; 25 | } 26 | 27 | login(username: string, password: string) { 28 | return this.http.post(`${environment.apiUrl}/users/authenticate`, { username, password }) 29 | .pipe(map(user => { 30 | // store user details and jwt token in local storage to keep user logged in between page refreshes 31 | localStorage.setItem('user', JSON.stringify(user)); 32 | this.userSubject.next(user); 33 | return user; 34 | })); 35 | } 36 | 37 | logout() { 38 | // remove user from local storage and set current user to null 39 | localStorage.removeItem('user'); 40 | this.userSubject.next(null); 41 | this.router.navigate(['/account/login']); 42 | } 43 | 44 | register(user: User) { 45 | return this.http.post(`${environment.apiUrl}/users/register`, user); 46 | } 47 | } -------------------------------------------------------------------------------- /part-4/src/app/_services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account.service'; -------------------------------------------------------------------------------- /part-4/src/app/account/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.component'; 2 | export * from './register.component'; -------------------------------------------------------------------------------- /part-4/src/app/account/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
{{error}}
3 |
{{success}}
4 |
5 |

Login

6 |
7 |
8 |
9 | 10 | 11 |
12 |
Username is required
13 |
14 |
15 |
16 | 17 | 18 |
19 |
Password is required
20 |
21 |
22 |
23 | 27 | Register 28 |
29 |
30 |
31 |
32 |
-------------------------------------------------------------------------------- /part-4/src/app/account/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 4 | import { first } from 'rxjs/operators'; 5 | 6 | import { AccountService } from '@app/_services' 7 | 8 | @Component({ templateUrl: 'login.component.html' }) 9 | export class LoginComponent implements OnInit { 10 | form!: FormGroup; 11 | loading = false; 12 | submitted = false; 13 | error?: string; 14 | success?: string; 15 | 16 | constructor( 17 | private formBuilder: FormBuilder, 18 | private route: ActivatedRoute, 19 | private router: Router, 20 | private accountService: AccountService 21 | ) { 22 | // redirect to home if already logged in 23 | if (this.accountService.userValue) { 24 | this.router.navigate(['/']); 25 | } 26 | } 27 | 28 | ngOnInit() { 29 | this.form = this.formBuilder.group({ 30 | username: ['', Validators.required], 31 | password: ['', Validators.required] 32 | }); 33 | 34 | // show success message after registration 35 | if (this.route.snapshot.queryParams.registered) { 36 | this.success = 'Registration successful'; 37 | } 38 | } 39 | 40 | // convenience getter for easy access to form fields 41 | get f() { return this.form.controls; } 42 | 43 | onSubmit() { 44 | this.submitted = true; 45 | 46 | // reset alerts on submit 47 | this.error = ''; 48 | this.success = ''; 49 | 50 | // stop here if form is invalid 51 | if (this.form.invalid) { 52 | return; 53 | } 54 | 55 | this.loading = true; 56 | this.accountService.login(this.f.username.value, this.f.password.value) 57 | .pipe(first()) 58 | .subscribe({ 59 | next: () => { 60 | // get return url from query parameters or default to home page 61 | const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; 62 | this.router.navigateByUrl(returnUrl); 63 | }, 64 | error: error => { 65 | this.error = error; 66 | this.loading = false; 67 | } 68 | }); 69 | } 70 | } -------------------------------------------------------------------------------- /part-4/src/app/account/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 4 | import { first } from 'rxjs/operators'; 5 | 6 | import { AccountService } from '@app/_services'; 7 | 8 | @Component({ templateUrl: 'register.component.html' }) 9 | export class RegisterComponent implements OnInit { 10 | form!: FormGroup; 11 | loading = false; 12 | submitted = false; 13 | error?: string; 14 | 15 | constructor( 16 | private formBuilder: FormBuilder, 17 | private route: ActivatedRoute, 18 | private router: Router, 19 | private accountService: AccountService 20 | ) { 21 | // redirect to home if already logged in 22 | if (this.accountService.userValue) { 23 | this.router.navigate(['/']); 24 | } 25 | } 26 | 27 | ngOnInit() { 28 | this.form = this.formBuilder.group({ 29 | firstName: ['', Validators.required], 30 | lastName: ['', Validators.required], 31 | username: ['', Validators.required], 32 | password: ['', [Validators.required, Validators.minLength(6)]] 33 | }); 34 | } 35 | 36 | // convenience getter for easy access to form fields 37 | get f() { return this.form.controls; } 38 | 39 | onSubmit() { 40 | this.submitted = true; 41 | 42 | // reset alert on submit 43 | this.error = ''; 44 | 45 | // stop here if form is invalid 46 | if (this.form.invalid) { 47 | return; 48 | } 49 | 50 | this.loading = true; 51 | this.accountService.register(this.form.value) 52 | .pipe(first()) 53 | .subscribe({ 54 | next: () => { 55 | this.router.navigate(['/account/login'], { queryParams: { registered: true }}); 56 | }, 57 | error: error => { 58 | this.error = error; 59 | this.loading = false; 60 | } 61 | }); 62 | } 63 | } -------------------------------------------------------------------------------- /part-4/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { HomeComponent } from './home'; 5 | import { LoginComponent, RegisterComponent } from './account'; 6 | import { AuthGuard } from './_helpers'; 7 | 8 | const routes: Routes = [ 9 | { path: '', component: HomeComponent, canActivate: [AuthGuard] }, 10 | { path: 'account/login', component: LoginComponent }, 11 | { path: 'account/register', component: RegisterComponent }, 12 | 13 | // otherwise redirect to home 14 | { path: '**', redirectTo: '' } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forRoot(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class AppRoutingModule { } -------------------------------------------------------------------------------- /part-4/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 |
11 | 12 |
-------------------------------------------------------------------------------- /part-4/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { AccountService } from './_services'; 4 | import { User } from './_models'; 5 | 6 | @Component({ selector: 'app-root', templateUrl: 'app.component.html' }) 7 | export class AppComponent { 8 | user?: User | null; 9 | 10 | constructor(private accountService: AccountService) { 11 | this.accountService.user.subscribe(x => this.user = x); 12 | } 13 | 14 | logout() { 15 | this.accountService.logout(); 16 | } 17 | } -------------------------------------------------------------------------------- /part-4/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 5 | 6 | // used to create fake backend 7 | import { fakeBackendProvider } from './_helpers'; 8 | 9 | import { AppRoutingModule } from './app-routing.module'; 10 | import { JwtInterceptor, ErrorInterceptor } from './_helpers'; 11 | import { AppComponent } from './app.component'; 12 | import { HomeComponent } from './home'; 13 | import { LoginComponent, RegisterComponent } from './account'; 14 | 15 | @NgModule({ 16 | imports: [ 17 | BrowserModule, 18 | ReactiveFormsModule, 19 | HttpClientModule, 20 | AppRoutingModule 21 | ], 22 | declarations: [ 23 | AppComponent, 24 | HomeComponent, 25 | LoginComponent, 26 | RegisterComponent 27 | ], 28 | providers: [ 29 | { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, 30 | { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }, 31 | 32 | // provider used to create fake backend 33 | fakeBackendProvider 34 | ], 35 | bootstrap: [AppComponent] 36 | }) 37 | export class AppModule { } -------------------------------------------------------------------------------- /part-4/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |

Home Page

2 | -------------------------------------------------------------------------------- /part-4/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ templateUrl: 'home.component.html' }) 4 | export class HomeComponent { } -------------------------------------------------------------------------------- /part-4/src/app/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home.component'; -------------------------------------------------------------------------------- /part-4/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cornflourblue/angular-16-tutorial/a2d771bd813e937d38b1d47ea93bfc270905ee2c/part-4/src/assets/.gitkeep -------------------------------------------------------------------------------- /part-4/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | apiUrl: 'http://localhost:4000' 3 | }; -------------------------------------------------------------------------------- /part-4/src/favicon.ico: -------------------------------------------------------------------------------- 1 | �PNG 2 |  3 | IHDR?�~� pHYs  ��~�fIDATH��WKLQ���̔�uG�� e�n�. 6qcb�l?���D`�F#� Ku�F 1Qc� 4 | ��!���� ��C�P�|B?$���ܱ3����I&}��}�̽s�[*�ɀU�A��K��yx�gY�Ajq��3L Š���˫�OD�4��3Ϗ:X�3��o�P J�ğo#IH�a����,{>1/�2$�R AR]�)w��?�sZw^��q�Y�m_��e���r[8�^� 5 | �&p��-���A}c��- ������!����2_) E�)㊪j���v�m��ZOi� g�nW�{�n.|�e2�a&�0aŸ����be�̀��C�fˤE%-{�ֺ��׮C��N��jXi�~c�C,t��T�����r�{� �L)s��V��6%�(�#ᤙ!�]��H�ҐH$R���^R�A�61(?Y舚�>���(Z����Qm�L2�K�ZIc�� 6 | ���̧�C��2!⅄�(����"�Go��>�q��=��$%�z`ѯ��T�&����PHh�Z!=���z��O��������,*VVV�1�f*CJ�]EE��K�k��d�#5���`2yT!�}7���߈~�,���zs�����y�T��V������D��C2�G��@%̑72Y�޾{oJ�"@��^h�~ ��fĬ�!a�D��6���Ha|�3��� [>�����]7U2п���]�ė 7 | ��PU� �.Wejq�in�g��+p<ߺQH����總j[������.��� Q���p _�K�� 1(��+��bB8�\ra 8 | �́�v.l���(���ǽ�w���L��w�8�C��IEND�B`� -------------------------------------------------------------------------------- /part-4/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Tutorial 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /part-4/src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | 6 | platformBrowserDynamic().bootstrapModule(AppModule) 7 | .catch(err => console.error(err)); 8 | -------------------------------------------------------------------------------- /part-4/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /part-4/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /part-4/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": false, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ], 26 | "paths": { 27 | "@app/*": ["src/app/*"], 28 | "@environments/*": ["src/environments/*"] 29 | } 30 | }, 31 | "angularCompilerOptions": { 32 | "enableI18nLegacyMessageIdFormat": false, 33 | "strictInjectionParameters": true, 34 | "strictInputAccessModifiers": true, 35 | "strictTemplates": true 36 | } 37 | } -------------------------------------------------------------------------------- /part-4/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /part-5/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /part-5/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /part-5/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /part-5/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /part-5/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /part-5/README.md: -------------------------------------------------------------------------------- 1 | # AngularTutorial 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.0.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /part-5/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-tutorial", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^16.0.0", 14 | "@angular/common": "^16.0.0", 15 | "@angular/compiler": "^16.0.0", 16 | "@angular/core": "^16.0.0", 17 | "@angular/forms": "^16.0.0", 18 | "@angular/platform-browser": "^16.0.0", 19 | "@angular/platform-browser-dynamic": "^16.0.0", 20 | "@angular/router": "^16.0.0", 21 | "rxjs": "~7.8.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.13.0" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "^16.0.0", 27 | "@angular/cli": "~16.0.0", 28 | "@angular/compiler-cli": "^16.0.0", 29 | "@types/jasmine": "~4.3.0", 30 | "jasmine-core": "~4.6.0", 31 | "karma": "~6.4.0", 32 | "karma-chrome-launcher": "~3.2.0", 33 | "karma-coverage": "~2.2.0", 34 | "karma-jasmine": "~5.1.0", 35 | "karma-jasmine-html-reporter": "~2.0.0", 36 | "typescript": "~5.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /part-5/src/app/_components/alert.component.html: -------------------------------------------------------------------------------- 1 |
2 |
{{alert.message}}
3 |
-------------------------------------------------------------------------------- /part-5/src/app/_components/alert.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Subscription } from 'rxjs'; 3 | 4 | import { AlertService } from '@app/_services'; 5 | 6 | @Component({ selector: 'alert', templateUrl: 'alert.component.html' }) 7 | export class AlertComponent implements OnInit, OnDestroy { 8 | private subscription!: Subscription; 9 | alert: any; 10 | 11 | constructor(private alertService: AlertService) { } 12 | 13 | ngOnInit() { 14 | this.subscription = this.alertService.onAlert() 15 | .subscribe(alert => { 16 | switch (alert?.type) { 17 | case 'success': 18 | alert.cssClass = 'alert alert-success'; 19 | break; 20 | case 'error': 21 | alert.cssClass = 'alert alert-danger'; 22 | break; 23 | } 24 | 25 | this.alert = alert; 26 | }); 27 | } 28 | 29 | ngOnDestroy() { 30 | this.subscription.unsubscribe(); 31 | } 32 | } -------------------------------------------------------------------------------- /part-5/src/app/_components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './alert.component'; -------------------------------------------------------------------------------- /part-5/src/app/_helpers/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | 4 | import { AccountService } from '@app/_services'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class AuthGuard implements CanActivate { 8 | constructor( 9 | private router: Router, 10 | private accountService: AccountService 11 | ) { } 12 | 13 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 14 | const user = this.accountService.userValue; 15 | if (user) { 16 | // authorised so return true 17 | return true; 18 | } 19 | 20 | // not logged in so redirect to login page with the return url 21 | this.router.navigate(['/account/login'], { queryParams: { returnUrl: state.url } }); 22 | return false; 23 | } 24 | } -------------------------------------------------------------------------------- /part-5/src/app/_helpers/error.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; 3 | import { Observable, throwError } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | 6 | import { AccountService } from '@app/_services'; 7 | 8 | @Injectable() 9 | export class ErrorInterceptor implements HttpInterceptor { 10 | constructor(private accountService: AccountService) { } 11 | 12 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 13 | return next.handle(request).pipe(catchError(err => { 14 | if ([401, 403].includes(err.status) && this.accountService.userValue) { 15 | // auto logout if 401 or 403 response returned from api 16 | this.accountService.logout(); 17 | } 18 | 19 | const error = err.error?.message || err.statusText; 20 | console.error(err); 21 | return throwError(() => error); 22 | })) 23 | } 24 | } -------------------------------------------------------------------------------- /part-5/src/app/_helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.guard'; 2 | export * from './error.interceptor'; 3 | export * from './fake-backend'; 4 | export * from './jwt.interceptor'; -------------------------------------------------------------------------------- /part-5/src/app/_helpers/jwt.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { environment } from '@environments/environment'; 6 | import { AccountService } from '@app/_services'; 7 | 8 | @Injectable() 9 | export class JwtInterceptor implements HttpInterceptor { 10 | constructor(private accountService: AccountService) { } 11 | 12 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 13 | // add auth header with jwt if user is logged in and request is to the api url 14 | const user = this.accountService.userValue; 15 | const isLoggedIn = user?.token; 16 | const isApiUrl = request.url.startsWith(environment.apiUrl); 17 | if (isLoggedIn && isApiUrl) { 18 | request = request.clone({ 19 | setHeaders: { Authorization: `Bearer ${user.token}` } 20 | }); 21 | } 22 | 23 | return next.handle(request); 24 | } 25 | } -------------------------------------------------------------------------------- /part-5/src/app/_models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user'; -------------------------------------------------------------------------------- /part-5/src/app/_models/user.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | id?: string; 3 | username?: string; 4 | password?: string; 5 | firstName?: string; 6 | lastName?: string; 7 | token?: string; 8 | } -------------------------------------------------------------------------------- /part-5/src/app/_services/account.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router } from '@angular/router'; 3 | import { HttpClient } from '@angular/common/http'; 4 | import { BehaviorSubject, Observable } from 'rxjs'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | import { environment } from '@environments/environment'; 8 | import { User } from '@app/_models'; 9 | 10 | @Injectable({ providedIn: 'root' }) 11 | export class AccountService { 12 | private userSubject: BehaviorSubject; 13 | public user: Observable; 14 | 15 | constructor( 16 | private router: Router, 17 | private http: HttpClient 18 | ) { 19 | this.userSubject = new BehaviorSubject(JSON.parse(localStorage.getItem('user')!)); 20 | this.user = this.userSubject.asObservable(); 21 | } 22 | 23 | public get userValue() { 24 | return this.userSubject.value; 25 | } 26 | 27 | login(username: string, password: string) { 28 | return this.http.post(`${environment.apiUrl}/users/authenticate`, { username, password }) 29 | .pipe(map(user => { 30 | // store user details and jwt token in local storage to keep user logged in between page refreshes 31 | localStorage.setItem('user', JSON.stringify(user)); 32 | this.userSubject.next(user); 33 | return user; 34 | })); 35 | } 36 | 37 | logout() { 38 | // remove user from local storage and set current user to null 39 | localStorage.removeItem('user'); 40 | this.userSubject.next(null); 41 | this.router.navigate(['/account/login']); 42 | } 43 | 44 | register(user: User) { 45 | return this.http.post(`${environment.apiUrl}/users/register`, user); 46 | } 47 | } -------------------------------------------------------------------------------- /part-5/src/app/_services/alert.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, NavigationStart } from '@angular/router'; 3 | import { Observable, Subject } from 'rxjs'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class AlertService { 7 | private subject = new Subject(); 8 | private showAfterRedirect = false; 9 | 10 | constructor(private router: Router) { 11 | // clear alert messages on route change unless 'showAfterRedirect' flag is true 12 | this.router.events.subscribe(event => { 13 | if (event instanceof NavigationStart) { 14 | if (this.showAfterRedirect) { 15 | // only keep for a single route change 16 | this.showAfterRedirect = false; 17 | } else { 18 | // clear alert message 19 | this.clear(); 20 | } 21 | } 22 | }); 23 | } 24 | 25 | onAlert(): Observable { 26 | return this.subject.asObservable(); 27 | } 28 | 29 | success(message: string, showAfterRedirect = false) { 30 | this.showAfterRedirect = showAfterRedirect; 31 | this.subject.next({ type: 'success', message }); 32 | } 33 | 34 | error(message: string, showAfterRedirect = false) { 35 | this.showAfterRedirect = showAfterRedirect; 36 | this.subject.next({ type: 'error', message }); 37 | } 38 | 39 | clear() { 40 | // clear by calling subject.next() with null 41 | this.subject.next(null); 42 | } 43 | } -------------------------------------------------------------------------------- /part-5/src/app/_services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account.service'; 2 | export * from './alert.service'; -------------------------------------------------------------------------------- /part-5/src/app/account/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.component'; 2 | export * from './register.component'; -------------------------------------------------------------------------------- /part-5/src/app/account/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Login

4 |
5 |
6 |
7 | 8 | 9 |
10 |
Username is required
11 |
12 |
13 |
14 | 15 | 16 |
17 |
Password is required
18 |
19 |
20 |
21 | 25 | Register 26 |
27 |
28 |
29 |
30 |
-------------------------------------------------------------------------------- /part-5/src/app/account/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 4 | import { first } from 'rxjs/operators'; 5 | 6 | import { AccountService, AlertService } from '@app/_services' 7 | 8 | @Component({ templateUrl: 'login.component.html' }) 9 | export class LoginComponent implements OnInit { 10 | form!: FormGroup; 11 | loading = false; 12 | submitted = false; 13 | 14 | constructor( 15 | private formBuilder: FormBuilder, 16 | private route: ActivatedRoute, 17 | private router: Router, 18 | private accountService: AccountService, 19 | private alertService: AlertService 20 | ) { 21 | // redirect to home if already logged in 22 | if (this.accountService.userValue) { 23 | this.router.navigate(['/']); 24 | } 25 | } 26 | 27 | ngOnInit() { 28 | this.form = this.formBuilder.group({ 29 | username: ['', Validators.required], 30 | password: ['', Validators.required] 31 | }); 32 | } 33 | 34 | // convenience getter for easy access to form fields 35 | get f() { return this.form.controls; } 36 | 37 | onSubmit() { 38 | this.submitted = true; 39 | 40 | // reset alerts on submit 41 | this.alertService.clear(); 42 | 43 | // stop here if form is invalid 44 | if (this.form.invalid) { 45 | return; 46 | } 47 | 48 | this.loading = true; 49 | this.accountService.login(this.f.username.value, this.f.password.value) 50 | .pipe(first()) 51 | .subscribe({ 52 | next: () => { 53 | // get return url from query parameters or default to home page 54 | const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; 55 | this.router.navigateByUrl(returnUrl); 56 | }, 57 | error: error => { 58 | this.alertService.error(error); 59 | this.loading = false; 60 | } 61 | }); 62 | } 63 | } -------------------------------------------------------------------------------- /part-5/src/app/account/register.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Register

4 |
5 |
6 |
7 | 8 | 9 |
10 |
First Name is required
11 |
12 |
13 |
14 | 15 | 16 |
17 |
Last Name is required
18 |
19 |
20 |
21 | 22 | 23 |
24 |
Username is required
25 |
26 |
27 |
28 | 29 | 30 |
31 |
Password is required
32 |
Password must be at least 6 characters
33 |
34 |
35 |
36 | 40 | Cancel 41 |
42 |
43 |
44 |
45 |
-------------------------------------------------------------------------------- /part-5/src/app/account/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 4 | import { first } from 'rxjs/operators'; 5 | 6 | import { AccountService, AlertService } from '@app/_services'; 7 | 8 | @Component({ templateUrl: 'register.component.html' }) 9 | export class RegisterComponent implements OnInit { 10 | form!: FormGroup; 11 | loading = false; 12 | submitted = false; 13 | 14 | constructor( 15 | private formBuilder: FormBuilder, 16 | private route: ActivatedRoute, 17 | private router: Router, 18 | private accountService: AccountService, 19 | private alertService: AlertService 20 | ) { 21 | // redirect to home if already logged in 22 | if (this.accountService.userValue) { 23 | this.router.navigate(['/']); 24 | } 25 | } 26 | 27 | ngOnInit() { 28 | this.form = this.formBuilder.group({ 29 | firstName: ['', Validators.required], 30 | lastName: ['', Validators.required], 31 | username: ['', Validators.required], 32 | password: ['', [Validators.required, Validators.minLength(6)]] 33 | }); 34 | } 35 | 36 | // convenience getter for easy access to form fields 37 | get f() { return this.form.controls; } 38 | 39 | onSubmit() { 40 | this.submitted = true; 41 | 42 | // reset alert on submit 43 | this.alertService.clear(); 44 | 45 | // stop here if form is invalid 46 | if (this.form.invalid) { 47 | return; 48 | } 49 | 50 | this.loading = true; 51 | this.accountService.register(this.form.value) 52 | .pipe(first()) 53 | .subscribe({ 54 | next: () => { 55 | this.alertService.success('Registration successful', true); 56 | this.router.navigate(['/account/login'], { queryParams: { registered: true }}); 57 | }, 58 | error: error => { 59 | this.alertService.error(error); 60 | this.loading = false; 61 | } 62 | }); 63 | } 64 | } -------------------------------------------------------------------------------- /part-5/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { HomeComponent } from './home'; 5 | import { LoginComponent, RegisterComponent } from './account'; 6 | import { AuthGuard } from './_helpers'; 7 | 8 | const routes: Routes = [ 9 | { path: '', component: HomeComponent, canActivate: [AuthGuard] }, 10 | { path: 'account/login', component: LoginComponent }, 11 | { path: 'account/register', component: RegisterComponent }, 12 | 13 | // otherwise redirect to home 14 | { path: '**', redirectTo: '' } 15 | ]; 16 | 17 | @NgModule({ 18 | imports: [RouterModule.forRoot(routes)], 19 | exports: [RouterModule] 20 | }) 21 | export class AppRoutingModule { } -------------------------------------------------------------------------------- /part-5/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 |
11 | 12 | 13 |
-------------------------------------------------------------------------------- /part-5/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { AccountService } from './_services'; 4 | import { User } from './_models'; 5 | 6 | @Component({ selector: 'app-root', templateUrl: 'app.component.html' }) 7 | export class AppComponent { 8 | user?: User | null; 9 | 10 | constructor(private accountService: AccountService) { 11 | this.accountService.user.subscribe(x => this.user = x); 12 | } 13 | 14 | logout() { 15 | this.accountService.logout(); 16 | } 17 | } -------------------------------------------------------------------------------- /part-5/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 5 | 6 | // used to create fake backend 7 | import { fakeBackendProvider } from './_helpers'; 8 | 9 | import { AppRoutingModule } from './app-routing.module'; 10 | import { JwtInterceptor, ErrorInterceptor } from './_helpers'; 11 | import { AppComponent } from './app.component'; 12 | import { HomeComponent } from './home'; 13 | import { LoginComponent, RegisterComponent } from './account'; 14 | import { AlertComponent } from './_components'; 15 | 16 | @NgModule({ 17 | imports: [ 18 | BrowserModule, 19 | ReactiveFormsModule, 20 | HttpClientModule, 21 | AppRoutingModule 22 | ], 23 | declarations: [ 24 | AppComponent, 25 | HomeComponent, 26 | LoginComponent, 27 | RegisterComponent, 28 | AlertComponent 29 | ], 30 | providers: [ 31 | { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, 32 | { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }, 33 | 34 | // provider used to create fake backend 35 | fakeBackendProvider 36 | ], 37 | bootstrap: [AppComponent] 38 | }) 39 | export class AppModule { } -------------------------------------------------------------------------------- /part-5/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Hi {{user?.firstName}}!

4 |

You're logged in with Angular!!

5 |
6 |
-------------------------------------------------------------------------------- /part-5/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { User } from '@app/_models'; 4 | import { AccountService } from '@app/_services'; 5 | 6 | @Component({ templateUrl: 'home.component.html' }) 7 | export class HomeComponent { 8 | user: User | null; 9 | 10 | constructor(private accountService: AccountService) { 11 | this.user = this.accountService.userValue; 12 | } 13 | } -------------------------------------------------------------------------------- /part-5/src/app/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home.component'; -------------------------------------------------------------------------------- /part-5/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cornflourblue/angular-16-tutorial/a2d771bd813e937d38b1d47ea93bfc270905ee2c/part-5/src/assets/.gitkeep -------------------------------------------------------------------------------- /part-5/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | apiUrl: 'http://localhost:4000' 3 | }; -------------------------------------------------------------------------------- /part-5/src/favicon.ico: -------------------------------------------------------------------------------- 1 | �PNG 2 |  3 | IHDR?�~� pHYs  ��~�fIDATH��WKLQ���̔�uG�� e�n�. 6qcb�l?���D`�F#� Ku�F 1Qc� 4 | ��!���� ��C�P�|B?$���ܱ3����I&}��}�̽s�[*�ɀU�A��K��yx�gY�Ajq��3L Š���˫�OD�4��3Ϗ:X�3��o�P J�ğo#IH�a����,{>1/�2$�R AR]�)w��?�sZw^��q�Y�m_��e���r[8�^� 5 | �&p��-���A}c��- ������!����2_) E�)㊪j���v�m��ZOi� g�nW�{�n.|�e2�a&�0aŸ����be�̀��C�fˤE%-{�ֺ��׮C��N��jXi�~c�C,t��T�����r�{� �L)s��V��6%�(�#ᤙ!�]��H�ҐH$R���^R�A�61(?Y舚�>���(Z����Qm�L2�K�ZIc�� 6 | ���̧�C��2!⅄�(����"�Go��>�q��=��$%�z`ѯ��T�&����PHh�Z!=���z��O��������,*VVV�1�f*CJ�]EE��K�k��d�#5���`2yT!�}7���߈~�,���zs�����y�T��V������D��C2�G��@%̑72Y�޾{oJ�"@��^h�~ ��fĬ�!a�D��6���Ha|�3��� [>�����]7U2п���]�ė 7 | ��PU� �.Wejq�in�g��+p<ߺQH����總j[������.��� Q���p _�K�� 1(��+��bB8�\ra 8 | �́�v.l���(���ǽ�w���L��w�8�C��IEND�B`� -------------------------------------------------------------------------------- /part-5/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Tutorial 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /part-5/src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | 6 | platformBrowserDynamic().bootstrapModule(AppModule) 7 | .catch(err => console.error(err)); 8 | -------------------------------------------------------------------------------- /part-5/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /part-5/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /part-5/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": false, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ], 26 | "paths": { 27 | "@app/*": ["src/app/*"], 28 | "@environments/*": ["src/environments/*"] 29 | } 30 | }, 31 | "angularCompilerOptions": { 32 | "enableI18nLegacyMessageIdFormat": false, 33 | "strictInjectionParameters": true, 34 | "strictInputAccessModifiers": true, 35 | "strictTemplates": true 36 | } 37 | } -------------------------------------------------------------------------------- /part-5/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /part-6/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /part-6/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /part-6/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /part-6/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /part-6/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /part-6/README.md: -------------------------------------------------------------------------------- 1 | # AngularTutorial 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.0.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /part-6/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-tutorial", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^16.0.0", 14 | "@angular/common": "^16.0.0", 15 | "@angular/compiler": "^16.0.0", 16 | "@angular/core": "^16.0.0", 17 | "@angular/forms": "^16.0.0", 18 | "@angular/platform-browser": "^16.0.0", 19 | "@angular/platform-browser-dynamic": "^16.0.0", 20 | "@angular/router": "^16.0.0", 21 | "rxjs": "~7.8.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.13.0" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "^16.0.0", 27 | "@angular/cli": "~16.0.0", 28 | "@angular/compiler-cli": "^16.0.0", 29 | "@types/jasmine": "~4.3.0", 30 | "jasmine-core": "~4.6.0", 31 | "karma": "~6.4.0", 32 | "karma-chrome-launcher": "~3.2.0", 33 | "karma-coverage": "~2.2.0", 34 | "karma-jasmine": "~5.1.0", 35 | "karma-jasmine-html-reporter": "~2.0.0", 36 | "typescript": "~5.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /part-6/src/app/_components/alert.component.html: -------------------------------------------------------------------------------- 1 |
2 |
{{alert.message}}
3 |
-------------------------------------------------------------------------------- /part-6/src/app/_components/alert.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { Subscription } from 'rxjs'; 3 | 4 | import { AlertService } from '@app/_services'; 5 | 6 | @Component({ selector: 'alert', templateUrl: 'alert.component.html' }) 7 | export class AlertComponent implements OnInit, OnDestroy { 8 | private subscription!: Subscription; 9 | alert: any; 10 | 11 | constructor(private alertService: AlertService) { } 12 | 13 | ngOnInit() { 14 | this.subscription = this.alertService.onAlert() 15 | .subscribe(alert => { 16 | switch (alert?.type) { 17 | case 'success': 18 | alert.cssClass = 'alert alert-success'; 19 | break; 20 | case 'error': 21 | alert.cssClass = 'alert alert-danger'; 22 | break; 23 | } 24 | 25 | this.alert = alert; 26 | }); 27 | } 28 | 29 | ngOnDestroy() { 30 | this.subscription.unsubscribe(); 31 | } 32 | } -------------------------------------------------------------------------------- /part-6/src/app/_components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './alert.component'; -------------------------------------------------------------------------------- /part-6/src/app/_helpers/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | 4 | import { AccountService } from '@app/_services'; 5 | 6 | @Injectable({ providedIn: 'root' }) 7 | export class AuthGuard implements CanActivate { 8 | constructor( 9 | private router: Router, 10 | private accountService: AccountService 11 | ) { } 12 | 13 | canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 14 | const user = this.accountService.userValue; 15 | if (user) { 16 | // authorised so return true 17 | return true; 18 | } 19 | 20 | // not logged in so redirect to login page with the return url 21 | this.router.navigate(['/account/login'], { queryParams: { returnUrl: state.url } }); 22 | return false; 23 | } 24 | } -------------------------------------------------------------------------------- /part-6/src/app/_helpers/error.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; 3 | import { Observable, throwError } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | 6 | import { AccountService } from '@app/_services'; 7 | 8 | @Injectable() 9 | export class ErrorInterceptor implements HttpInterceptor { 10 | constructor(private accountService: AccountService) { } 11 | 12 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 13 | return next.handle(request).pipe(catchError(err => { 14 | if ([401, 403].includes(err.status) && this.accountService.userValue) { 15 | // auto logout if 401 or 403 response returned from api 16 | this.accountService.logout(); 17 | } 18 | 19 | const error = err.error?.message || err.statusText; 20 | console.error(err); 21 | return throwError(() => error); 22 | })) 23 | } 24 | } -------------------------------------------------------------------------------- /part-6/src/app/_helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.guard'; 2 | export * from './error.interceptor'; 3 | export * from './fake-backend'; 4 | export * from './jwt.interceptor'; -------------------------------------------------------------------------------- /part-6/src/app/_helpers/jwt.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http'; 3 | import { Observable } from 'rxjs'; 4 | 5 | import { environment } from '@environments/environment'; 6 | import { AccountService } from '@app/_services'; 7 | 8 | @Injectable() 9 | export class JwtInterceptor implements HttpInterceptor { 10 | constructor(private accountService: AccountService) { } 11 | 12 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 13 | // add auth header with jwt if user is logged in and request is to the api url 14 | const user = this.accountService.userValue; 15 | const isLoggedIn = user?.token; 16 | const isApiUrl = request.url.startsWith(environment.apiUrl); 17 | if (isLoggedIn && isApiUrl) { 18 | request = request.clone({ 19 | setHeaders: { Authorization: `Bearer ${user.token}` } 20 | }); 21 | } 22 | 23 | return next.handle(request); 24 | } 25 | } -------------------------------------------------------------------------------- /part-6/src/app/_models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user'; -------------------------------------------------------------------------------- /part-6/src/app/_models/user.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | id?: string; 3 | username?: string; 4 | password?: string; 5 | firstName?: string; 6 | lastName?: string; 7 | token?: string; 8 | } -------------------------------------------------------------------------------- /part-6/src/app/_services/alert.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, NavigationStart } from '@angular/router'; 3 | import { Observable, Subject } from 'rxjs'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class AlertService { 7 | private subject = new Subject(); 8 | private showAfterRedirect = false; 9 | 10 | constructor(private router: Router) { 11 | // clear alert messages on route change unless 'showAfterRedirect' flag is true 12 | this.router.events.subscribe(event => { 13 | if (event instanceof NavigationStart) { 14 | if (this.showAfterRedirect) { 15 | // only keep for a single route change 16 | this.showAfterRedirect = false; 17 | } else { 18 | // clear alert message 19 | this.clear(); 20 | } 21 | } 22 | }); 23 | } 24 | 25 | onAlert(): Observable { 26 | return this.subject.asObservable(); 27 | } 28 | 29 | success(message: string, showAfterRedirect = false) { 30 | this.showAfterRedirect = showAfterRedirect; 31 | this.subject.next({ type: 'success', message }); 32 | } 33 | 34 | error(message: string, showAfterRedirect = false) { 35 | this.showAfterRedirect = showAfterRedirect; 36 | this.subject.next({ type: 'error', message }); 37 | } 38 | 39 | clear() { 40 | // clear by calling subject.next() with null 41 | this.subject.next(null); 42 | } 43 | } -------------------------------------------------------------------------------- /part-6/src/app/_services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account.service'; 2 | export * from './alert.service'; -------------------------------------------------------------------------------- /part-6/src/app/account/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.component'; 2 | export * from './register.component'; -------------------------------------------------------------------------------- /part-6/src/app/account/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Login

4 |
5 |
6 |
7 | 8 | 9 |
10 |
Username is required
11 |
12 |
13 |
14 | 15 | 16 |
17 |
Password is required
18 |
19 |
20 |
21 | 25 | Register 26 |
27 |
28 |
29 |
30 |
-------------------------------------------------------------------------------- /part-6/src/app/account/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 4 | import { first } from 'rxjs/operators'; 5 | 6 | import { AccountService, AlertService } from '@app/_services' 7 | 8 | @Component({ templateUrl: 'login.component.html' }) 9 | export class LoginComponent implements OnInit { 10 | form!: FormGroup; 11 | loading = false; 12 | submitted = false; 13 | 14 | constructor( 15 | private formBuilder: FormBuilder, 16 | private route: ActivatedRoute, 17 | private router: Router, 18 | private accountService: AccountService, 19 | private alertService: AlertService 20 | ) { 21 | // redirect to home if already logged in 22 | if (this.accountService.userValue) { 23 | this.router.navigate(['/']); 24 | } 25 | } 26 | 27 | ngOnInit() { 28 | this.form = this.formBuilder.group({ 29 | username: ['', Validators.required], 30 | password: ['', Validators.required] 31 | }); 32 | } 33 | 34 | // convenience getter for easy access to form fields 35 | get f() { return this.form.controls; } 36 | 37 | onSubmit() { 38 | this.submitted = true; 39 | 40 | // reset alerts on submit 41 | this.alertService.clear(); 42 | 43 | // stop here if form is invalid 44 | if (this.form.invalid) { 45 | return; 46 | } 47 | 48 | this.loading = true; 49 | this.accountService.login(this.f.username.value, this.f.password.value) 50 | .pipe(first()) 51 | .subscribe({ 52 | next: () => { 53 | // get return url from query parameters or default to home page 54 | const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; 55 | this.router.navigateByUrl(returnUrl); 56 | }, 57 | error: error => { 58 | this.alertService.error(error); 59 | this.loading = false; 60 | } 61 | }); 62 | } 63 | } -------------------------------------------------------------------------------- /part-6/src/app/account/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Router, ActivatedRoute } from '@angular/router'; 3 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 4 | import { first } from 'rxjs/operators'; 5 | 6 | import { AccountService, AlertService } from '@app/_services'; 7 | 8 | @Component({ templateUrl: 'register.component.html' }) 9 | export class RegisterComponent implements OnInit { 10 | form!: FormGroup; 11 | loading = false; 12 | submitted = false; 13 | 14 | constructor( 15 | private formBuilder: FormBuilder, 16 | private route: ActivatedRoute, 17 | private router: Router, 18 | private accountService: AccountService, 19 | private alertService: AlertService 20 | ) { 21 | // redirect to home if already logged in 22 | if (this.accountService.userValue) { 23 | this.router.navigate(['/']); 24 | } 25 | } 26 | 27 | ngOnInit() { 28 | this.form = this.formBuilder.group({ 29 | firstName: ['', Validators.required], 30 | lastName: ['', Validators.required], 31 | username: ['', Validators.required], 32 | password: ['', [Validators.required, Validators.minLength(6)]] 33 | }); 34 | } 35 | 36 | // convenience getter for easy access to form fields 37 | get f() { return this.form.controls; } 38 | 39 | onSubmit() { 40 | this.submitted = true; 41 | 42 | // reset alert on submit 43 | this.alertService.clear(); 44 | 45 | // stop here if form is invalid 46 | if (this.form.invalid) { 47 | return; 48 | } 49 | 50 | this.loading = true; 51 | this.accountService.register(this.form.value) 52 | .pipe(first()) 53 | .subscribe({ 54 | next: () => { 55 | this.alertService.success('Registration successful', true); 56 | this.router.navigate(['/account/login'], { queryParams: { registered: true }}); 57 | }, 58 | error: error => { 59 | this.alertService.error(error); 60 | this.loading = false; 61 | } 62 | }); 63 | } 64 | } -------------------------------------------------------------------------------- /part-6/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { HomeComponent } from './home'; 5 | import { LoginComponent, RegisterComponent } from './account'; 6 | import { AuthGuard } from './_helpers'; 7 | 8 | const usersModule = () => import('./users/users.module').then(x => x.UsersModule); 9 | 10 | const routes: Routes = [ 11 | { path: '', component: HomeComponent, canActivate: [AuthGuard] }, 12 | { path: 'users', loadChildren: usersModule, canActivate: [AuthGuard] }, 13 | { path: 'account/login', component: LoginComponent }, 14 | { path: 'account/register', component: RegisterComponent }, 15 | 16 | // otherwise redirect to home 17 | { path: '**', redirectTo: '' } 18 | ]; 19 | 20 | @NgModule({ 21 | imports: [RouterModule.forRoot(routes)], 22 | exports: [RouterModule] 23 | }) 24 | export class AppRoutingModule { } -------------------------------------------------------------------------------- /part-6/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 |
12 | 13 | 14 |
-------------------------------------------------------------------------------- /part-6/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { AccountService } from './_services'; 4 | import { User } from './_models'; 5 | 6 | @Component({ selector: 'app-root', templateUrl: 'app.component.html' }) 7 | export class AppComponent { 8 | user?: User | null; 9 | 10 | constructor(private accountService: AccountService) { 11 | this.accountService.user.subscribe(x => this.user = x); 12 | } 13 | 14 | logout() { 15 | this.accountService.logout(); 16 | } 17 | } -------------------------------------------------------------------------------- /part-6/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 5 | 6 | // used to create fake backend 7 | import { fakeBackendProvider } from './_helpers'; 8 | 9 | import { AppRoutingModule } from './app-routing.module'; 10 | import { JwtInterceptor, ErrorInterceptor } from './_helpers'; 11 | import { AppComponent } from './app.component'; 12 | import { HomeComponent } from './home'; 13 | import { LoginComponent, RegisterComponent } from './account'; 14 | import { AlertComponent } from './_components'; 15 | 16 | @NgModule({ 17 | imports: [ 18 | BrowserModule, 19 | ReactiveFormsModule, 20 | HttpClientModule, 21 | AppRoutingModule 22 | ], 23 | declarations: [ 24 | AppComponent, 25 | HomeComponent, 26 | LoginComponent, 27 | RegisterComponent, 28 | AlertComponent 29 | ], 30 | providers: [ 31 | { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, 32 | { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }, 33 | 34 | // provider used to create fake backend 35 | fakeBackendProvider 36 | ], 37 | bootstrap: [AppComponent] 38 | }) 39 | export class AppModule { } -------------------------------------------------------------------------------- /part-6/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Hi {{user?.firstName}}!

4 |

You're logged in with Angular!!

5 |
6 |
-------------------------------------------------------------------------------- /part-6/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { User } from '@app/_models'; 4 | import { AccountService } from '@app/_services'; 5 | 6 | @Component({ templateUrl: 'home.component.html' }) 7 | export class HomeComponent { 8 | user: User | null; 9 | 10 | constructor(private accountService: AccountService) { 11 | this.user = this.accountService.userValue; 12 | } 13 | } -------------------------------------------------------------------------------- /part-6/src/app/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home.component'; -------------------------------------------------------------------------------- /part-6/src/app/users/list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Users

4 | Add User 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 26 | 27 | 28 | 31 | 32 | 33 |
First NameLast NameUsername
{{user.firstName}}{{user.lastName}}{{user.username}} 20 | Edit 21 | 25 |
29 | 30 |
34 |
35 |
-------------------------------------------------------------------------------- /part-6/src/app/users/list.component.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, OnInit } from '@angular/core'; 3 | import { first } from 'rxjs/operators'; 4 | 5 | import { AccountService } from '@app/_services'; 6 | 7 | @Component({ templateUrl: 'list.component.html' }) 8 | export class ListComponent implements OnInit { 9 | users?: any[]; 10 | 11 | constructor(private accountService: AccountService) {} 12 | 13 | ngOnInit() { 14 | this.accountService.getAll() 15 | .pipe(first()) 16 | .subscribe(users => this.users = users); 17 | } 18 | 19 | deleteUser(id: string) { 20 | const user = this.users!.find(x => x.id === id); 21 | user.isDeleting = true; 22 | this.accountService.delete(id) 23 | .pipe(first()) 24 | .subscribe(() => this.users = this.users!.filter(x => x.id !== id)); 25 | } 26 | } -------------------------------------------------------------------------------- /part-6/src/app/users/users-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | 4 | import { ListComponent } from './list.component'; 5 | import { AddEditComponent } from './add-edit.component'; 6 | 7 | const routes: Routes = [ 8 | { path: '', component: ListComponent }, 9 | { path: 'add', component: AddEditComponent }, 10 | { path: 'edit/:id', component: AddEditComponent } 11 | ]; 12 | 13 | @NgModule({ 14 | imports: [RouterModule.forChild(routes)], 15 | exports: [RouterModule] 16 | }) 17 | export class UsersRoutingModule { } -------------------------------------------------------------------------------- /part-6/src/app/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { ReactiveFormsModule } from '@angular/forms'; 3 | import { CommonModule } from '@angular/common'; 4 | 5 | import { UsersRoutingModule } from './users-routing.module'; 6 | import { ListComponent } from './list.component'; 7 | import { AddEditComponent } from './add-edit.component'; 8 | 9 | @NgModule({ 10 | imports: [ 11 | CommonModule, 12 | ReactiveFormsModule, 13 | UsersRoutingModule 14 | ], 15 | declarations: [ 16 | ListComponent, 17 | AddEditComponent 18 | ] 19 | }) 20 | export class UsersModule { } -------------------------------------------------------------------------------- /part-6/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cornflourblue/angular-16-tutorial/a2d771bd813e937d38b1d47ea93bfc270905ee2c/part-6/src/assets/.gitkeep -------------------------------------------------------------------------------- /part-6/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | apiUrl: 'http://localhost:4000' 3 | }; -------------------------------------------------------------------------------- /part-6/src/favicon.ico: -------------------------------------------------------------------------------- 1 | �PNG 2 |  3 | IHDR?�~� pHYs  ��~�fIDATH��WKLQ���̔�uG�� e�n�. 6qcb�l?���D`�F#� Ku�F 1Qc� 4 | ��!���� ��C�P�|B?$���ܱ3����I&}��}�̽s�[*�ɀU�A��K��yx�gY�Ajq��3L Š���˫�OD�4��3Ϗ:X�3��o�P J�ğo#IH�a����,{>1/�2$�R AR]�)w��?�sZw^��q�Y�m_��e���r[8�^� 5 | �&p��-���A}c��- ������!����2_) E�)㊪j���v�m��ZOi� g�nW�{�n.|�e2�a&�0aŸ����be�̀��C�fˤE%-{�ֺ��׮C��N��jXi�~c�C,t��T�����r�{� �L)s��V��6%�(�#ᤙ!�]��H�ҐH$R���^R�A�61(?Y舚�>���(Z����Qm�L2�K�ZIc�� 6 | ���̧�C��2!⅄�(����"�Go��>�q��=��$%�z`ѯ��T�&����PHh�Z!=���z��O��������,*VVV�1�f*CJ�]EE��K�k��d�#5���`2yT!�}7���߈~�,���zs�����y�T��V������D��C2�G��@%̑72Y�޾{oJ�"@��^h�~ ��fĬ�!a�D��6���Ha|�3��� [>�����]7U2п���]�ė 7 | ��PU� �.Wejq�in�g��+p<ߺQH����總j[������.��� Q���p _�K�� 1(��+��bB8�\ra 8 | �́�v.l���(���ǽ�w���L��w�8�C��IEND�B`� -------------------------------------------------------------------------------- /part-6/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Tutorial 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /part-6/src/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app/app.module'; 4 | 5 | 6 | platformBrowserDynamic().bootstrapModule(AppModule) 7 | .catch(err => console.error(err)); 8 | -------------------------------------------------------------------------------- /part-6/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /part-6/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /part-6/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": false, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ], 26 | "paths": { 27 | "@app/*": ["src/app/*"], 28 | "@environments/*": ["src/environments/*"] 29 | } 30 | }, 31 | "angularCompilerOptions": { 32 | "enableI18nLegacyMessageIdFormat": false, 33 | "strictInjectionParameters": true, 34 | "strictInputAccessModifiers": true, 35 | "strictTemplates": true 36 | } 37 | } -------------------------------------------------------------------------------- /part-6/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /part-7/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /part-7/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /part-7/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /part-7/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /part-7/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /part-7/README.md: -------------------------------------------------------------------------------- 1 | # AngularTutorial 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.0.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /part-7/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-tutorial", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^16.0.0", 14 | "@angular/common": "^16.0.0", 15 | "@angular/compiler": "^16.0.0", 16 | "@angular/core": "^16.0.0", 17 | "@angular/forms": "^16.0.0", 18 | "@angular/platform-browser": "^16.0.0", 19 | "@angular/platform-browser-dynamic": "^16.0.0", 20 | "@angular/router": "^16.0.0", 21 | "rxjs": "~7.8.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.13.0" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "^16.0.0", 27 | "@angular/cli": "~16.0.0", 28 | "@angular/compiler-cli": "^16.0.0", 29 | "@types/jasmine": "~4.3.0", 30 | "jasmine-core": "~4.6.0", 31 | "karma": "~6.4.0", 32 | "karma-chrome-launcher": "~3.2.0", 33 | "karma-coverage": "~2.2.0", 34 | "karma-jasmine": "~5.1.0", 35 | "karma-jasmine-html-reporter": "~2.0.0", 36 | "typescript": "~5.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /part-7/src/app/_components/alert.component.html: -------------------------------------------------------------------------------- 1 |
2 |
{{alert.message}}
3 |
-------------------------------------------------------------------------------- /part-7/src/app/_components/alert.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { NgIf, NgClass } from '@angular/common'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { AlertService } from '@app/_services'; 6 | 7 | @Component({ 8 | selector: 'alert', templateUrl: 'alert.component.html', 9 | standalone: true, 10 | imports: [NgIf, NgClass] 11 | }) 12 | export class AlertComponent implements OnInit, OnDestroy { 13 | private subscription!: Subscription; 14 | alert: any; 15 | 16 | constructor(private alertService: AlertService) { } 17 | 18 | ngOnInit() { 19 | this.subscription = this.alertService.onAlert() 20 | .subscribe(alert => { 21 | switch (alert?.type) { 22 | case 'success': 23 | alert.cssClass = 'alert alert-success'; 24 | break; 25 | case 'error': 26 | alert.cssClass = 'alert alert-danger'; 27 | break; 28 | } 29 | 30 | this.alert = alert; 31 | }); 32 | } 33 | 34 | ngOnDestroy() { 35 | this.subscription.unsubscribe(); 36 | } 37 | } -------------------------------------------------------------------------------- /part-7/src/app/_components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './alert.component'; -------------------------------------------------------------------------------- /part-7/src/app/_helpers/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | 4 | import { AccountService } from '@app/_services'; 5 | 6 | export function authGuard(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 7 | const router = inject(Router); 8 | const accountService = inject(AccountService); 9 | const user = accountService.userValue; 10 | if (user) { 11 | // authorised so return true 12 | return true; 13 | } 14 | 15 | // not logged in so redirect to login page with the return url 16 | router.navigate(['/account/login'], { queryParams: { returnUrl: state.url } }); 17 | return false; 18 | } 19 | -------------------------------------------------------------------------------- /part-7/src/app/_helpers/error.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { HttpRequest, HttpHandlerFn } from '@angular/common/http'; 3 | import { throwError } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | 6 | import { AccountService } from '@app/_services'; 7 | 8 | export function errorInterceptor(request: HttpRequest, next: HttpHandlerFn) { 9 | const accountService = inject(AccountService); 10 | return next(request).pipe(catchError(err => { 11 | if ([401, 403].includes(err.status) && accountService.userValue) { 12 | // auto logout if 401 or 403 response returned from api 13 | accountService.logout(); 14 | } 15 | 16 | const error = err.error?.message || err.statusText; 17 | console.error(err); 18 | return throwError(() => error); 19 | })) 20 | } 21 | -------------------------------------------------------------------------------- /part-7/src/app/_helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.guard'; 2 | export * from './error.interceptor'; 3 | export * from './fake-backend'; 4 | export * from './jwt.interceptor'; -------------------------------------------------------------------------------- /part-7/src/app/_helpers/jwt.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { HttpRequest, HttpHandlerFn } from '@angular/common/http'; 3 | 4 | import { environment } from '@environments/environment'; 5 | import { AccountService } from '@app/_services'; 6 | 7 | export function jwtInterceptor(request: HttpRequest, next: HttpHandlerFn) { 8 | // add auth header with jwt if user is logged in and request is to the api url 9 | const accountService = inject(AccountService); 10 | const user = accountService.userValue; 11 | const isLoggedIn = user?.token; 12 | const isApiUrl = request.url.startsWith(environment.apiUrl); 13 | if (isLoggedIn && isApiUrl) { 14 | request = request.clone({ 15 | setHeaders: { Authorization: `Bearer ${user.token}` } 16 | }); 17 | } 18 | 19 | return next(request); 20 | } -------------------------------------------------------------------------------- /part-7/src/app/_models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user'; -------------------------------------------------------------------------------- /part-7/src/app/_models/user.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | id?: string; 3 | username?: string; 4 | password?: string; 5 | firstName?: string; 6 | lastName?: string; 7 | token?: string; 8 | } -------------------------------------------------------------------------------- /part-7/src/app/_services/alert.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, NavigationStart } from '@angular/router'; 3 | import { Observable, Subject } from 'rxjs'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class AlertService { 7 | private subject = new Subject(); 8 | private showAfterRedirect = false; 9 | 10 | constructor(private router: Router) { 11 | // clear alert messages on route change unless 'showAfterRedirect' flag is true 12 | this.router.events.subscribe(event => { 13 | if (event instanceof NavigationStart) { 14 | if (this.showAfterRedirect) { 15 | // only keep for a single route change 16 | this.showAfterRedirect = false; 17 | } else { 18 | // clear alert message 19 | this.clear(); 20 | } 21 | } 22 | }); 23 | } 24 | 25 | onAlert(): Observable { 26 | return this.subject.asObservable(); 27 | } 28 | 29 | success(message: string, showAfterRedirect = false) { 30 | this.showAfterRedirect = showAfterRedirect; 31 | this.subject.next({ type: 'success', message }); 32 | } 33 | 34 | error(message: string, showAfterRedirect = false) { 35 | this.showAfterRedirect = showAfterRedirect; 36 | this.subject.next({ type: 'error', message }); 37 | } 38 | 39 | clear() { 40 | // clear by calling subject.next() with null 41 | this.subject.next(null); 42 | } 43 | } -------------------------------------------------------------------------------- /part-7/src/app/_services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account.service'; 2 | export * from './alert.service'; -------------------------------------------------------------------------------- /part-7/src/app/account/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.component'; 2 | export * from './register.component'; -------------------------------------------------------------------------------- /part-7/src/app/account/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Login

4 |
5 |
6 |
7 | 8 | 9 |
10 |
Username is required
11 |
12 |
13 |
14 | 15 | 16 |
17 |
Password is required
18 |
19 |
20 |
21 | 25 | Register 26 |
27 |
28 |
29 |
30 |
-------------------------------------------------------------------------------- /part-7/src/app/account/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NgClass, NgIf } from '@angular/common'; 3 | import { Router, ActivatedRoute, RouterLink } from '@angular/router'; 4 | import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; 5 | import { first } from 'rxjs/operators'; 6 | 7 | import { AccountService, AlertService } from '@app/_services' 8 | 9 | @Component({ 10 | templateUrl: 'login.component.html', 11 | standalone: true, 12 | imports: [ReactiveFormsModule, NgClass, NgIf, RouterLink] 13 | }) 14 | export class LoginComponent implements OnInit { 15 | form!: FormGroup; 16 | loading = false; 17 | submitted = false; 18 | 19 | constructor( 20 | private formBuilder: FormBuilder, 21 | private route: ActivatedRoute, 22 | private router: Router, 23 | private accountService: AccountService, 24 | private alertService: AlertService 25 | ) { 26 | // redirect to home if already logged in 27 | if (this.accountService.userValue) { 28 | this.router.navigate(['/']); 29 | } 30 | } 31 | 32 | ngOnInit() { 33 | this.form = this.formBuilder.group({ 34 | username: ['', Validators.required], 35 | password: ['', Validators.required] 36 | }); 37 | } 38 | 39 | // convenience getter for easy access to form fields 40 | get f() { return this.form.controls; } 41 | 42 | onSubmit() { 43 | this.submitted = true; 44 | 45 | // reset alerts on submit 46 | this.alertService.clear(); 47 | 48 | // stop here if form is invalid 49 | if (this.form.invalid) { 50 | return; 51 | } 52 | 53 | this.loading = true; 54 | this.accountService.login(this.f.username.value, this.f.password.value) 55 | .pipe(first()) 56 | .subscribe({ 57 | next: () => { 58 | // get return url from query parameters or default to home page 59 | const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; 60 | this.router.navigateByUrl(returnUrl); 61 | }, 62 | error: error => { 63 | this.alertService.error(error); 64 | this.loading = false; 65 | } 66 | }); 67 | } 68 | } -------------------------------------------------------------------------------- /part-7/src/app/account/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NgClass, NgIf } from '@angular/common'; 3 | import { Router, ActivatedRoute, RouterLink } from '@angular/router'; 4 | import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; 5 | import { first } from 'rxjs/operators'; 6 | 7 | import { AccountService, AlertService } from '@app/_services'; 8 | 9 | @Component({ 10 | templateUrl: 'register.component.html', 11 | standalone: true, 12 | imports: [ReactiveFormsModule, NgClass, NgIf, RouterLink] 13 | }) 14 | export class RegisterComponent implements OnInit { 15 | form!: FormGroup; 16 | loading = false; 17 | submitted = false; 18 | 19 | constructor( 20 | private formBuilder: FormBuilder, 21 | private route: ActivatedRoute, 22 | private router: Router, 23 | private accountService: AccountService, 24 | private alertService: AlertService 25 | ) { 26 | // redirect to home if already logged in 27 | if (this.accountService.userValue) { 28 | this.router.navigate(['/']); 29 | } 30 | } 31 | 32 | ngOnInit() { 33 | this.form = this.formBuilder.group({ 34 | firstName: ['', Validators.required], 35 | lastName: ['', Validators.required], 36 | username: ['', Validators.required], 37 | password: ['', [Validators.required, Validators.minLength(6)]] 38 | }); 39 | } 40 | 41 | // convenience getter for easy access to form fields 42 | get f() { return this.form.controls; } 43 | 44 | onSubmit() { 45 | this.submitted = true; 46 | 47 | // reset alert on submit 48 | this.alertService.clear(); 49 | 50 | // stop here if form is invalid 51 | if (this.form.invalid) { 52 | return; 53 | } 54 | 55 | this.loading = true; 56 | this.accountService.register(this.form.value) 57 | .pipe(first()) 58 | .subscribe({ 59 | next: () => { 60 | this.alertService.success('Registration successful', true); 61 | this.router.navigate(['/account/login'], { queryParams: { registered: true }}); 62 | }, 63 | error: error => { 64 | this.alertService.error(error); 65 | this.loading = false; 66 | } 67 | }); 68 | } 69 | } -------------------------------------------------------------------------------- /part-7/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 |
12 | 13 | 14 |
-------------------------------------------------------------------------------- /part-7/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NgIf } from '@angular/common'; 3 | import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; 4 | 5 | import { AccountService } from './_services'; 6 | import { User } from './_models'; 7 | import { AlertComponent } from './_components/alert.component'; 8 | 9 | @Component({ 10 | selector: 'app-root', templateUrl: 'app.component.html', 11 | standalone: true, 12 | imports: [NgIf, RouterOutlet, RouterLink, RouterLinkActive, AlertComponent] 13 | }) 14 | export class AppComponent { 15 | user?: User | null; 16 | 17 | constructor(private accountService: AccountService) { 18 | this.accountService.user.subscribe(x => this.user = x); 19 | } 20 | 21 | logout() { 22 | this.accountService.logout(); 23 | } 24 | } -------------------------------------------------------------------------------- /part-7/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from "@angular/router"; 2 | 3 | import { HomeComponent } from './home'; 4 | import { LoginComponent, RegisterComponent } from './account'; 5 | import { authGuard } from './_helpers'; 6 | 7 | const usersRoutes = () => import('./users/users.routes').then(x => x.USERS_ROUTES); 8 | 9 | export const APP_ROUTES: Routes = [ 10 | { path: '', component: HomeComponent, canActivate: [authGuard] }, 11 | { path: 'users', loadChildren: usersRoutes, canActivate: [authGuard] }, 12 | { path: 'account/login', component: LoginComponent }, 13 | { path: 'account/register', component: RegisterComponent }, 14 | 15 | // otherwise redirect to home 16 | { path: '**', redirectTo: '' } 17 | ]; 18 | -------------------------------------------------------------------------------- /part-7/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Hi {{user?.firstName}}!

4 |

You're logged in with Angular!!

5 |
6 |
-------------------------------------------------------------------------------- /part-7/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { User } from '@app/_models'; 4 | import { AccountService } from '@app/_services'; 5 | 6 | @Component({ 7 | templateUrl: 'home.component.html', 8 | standalone: true 9 | }) 10 | export class HomeComponent { 11 | user: User | null; 12 | 13 | constructor(private accountService: AccountService) { 14 | this.user = this.accountService.userValue; 15 | } 16 | } -------------------------------------------------------------------------------- /part-7/src/app/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home.component'; -------------------------------------------------------------------------------- /part-7/src/app/users/list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Users

4 | Add User 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 26 | 27 | 28 | 31 | 32 | 33 |
First NameLast NameUsername
{{user.firstName}}{{user.lastName}}{{user.username}} 20 | Edit 21 | 25 |
29 | 30 |
34 |
35 |
-------------------------------------------------------------------------------- /part-7/src/app/users/list.component.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, OnInit } from '@angular/core'; 3 | import { NgFor, NgIf } from '@angular/common'; 4 | import { RouterLink } from '@angular/router'; 5 | import { first } from 'rxjs/operators'; 6 | 7 | import { AccountService } from '@app/_services'; 8 | 9 | @Component({ 10 | templateUrl: 'list.component.html', 11 | standalone: true, 12 | imports: [RouterLink, NgFor, NgIf] 13 | }) 14 | export class ListComponent implements OnInit { 15 | users?: any[]; 16 | 17 | constructor(private accountService: AccountService) {} 18 | 19 | ngOnInit() { 20 | this.accountService.getAll() 21 | .pipe(first()) 22 | .subscribe(users => this.users = users); 23 | } 24 | 25 | deleteUser(id: string) { 26 | const user = this.users!.find(x => x.id === id); 27 | user.isDeleting = true; 28 | this.accountService.delete(id) 29 | .pipe(first()) 30 | .subscribe(() => this.users = this.users!.filter(x => x.id !== id)); 31 | } 32 | } -------------------------------------------------------------------------------- /part-7/src/app/users/users.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | import { ListComponent } from './list.component'; 4 | import { AddEditComponent } from './add-edit.component'; 5 | 6 | export const USERS_ROUTES: Routes = [ 7 | { path: '', component: ListComponent }, 8 | { path: 'add', component: AddEditComponent }, 9 | { path: 'edit/:id', component: AddEditComponent } 10 | ]; -------------------------------------------------------------------------------- /part-7/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cornflourblue/angular-16-tutorial/a2d771bd813e937d38b1d47ea93bfc270905ee2c/part-7/src/assets/.gitkeep -------------------------------------------------------------------------------- /part-7/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | apiUrl: 'http://localhost:4000' 3 | }; -------------------------------------------------------------------------------- /part-7/src/favicon.ico: -------------------------------------------------------------------------------- 1 | �PNG 2 |  3 | IHDR?�~� pHYs  ��~�fIDATH��WKLQ���̔�uG�� e�n�. 6qcb�l?���D`�F#� Ku�F 1Qc� 4 | ��!���� ��C�P�|B?$���ܱ3����I&}��}�̽s�[*�ɀU�A��K��yx�gY�Ajq��3L Š���˫�OD�4��3Ϗ:X�3��o�P J�ğo#IH�a����,{>1/�2$�R AR]�)w��?�sZw^��q�Y�m_��e���r[8�^� 5 | �&p��-���A}c��- ������!����2_) E�)㊪j���v�m��ZOi� g�nW�{�n.|�e2�a&�0aŸ����be�̀��C�fˤE%-{�ֺ��׮C��N��jXi�~c�C,t��T�����r�{� �L)s��V��6%�(�#ᤙ!�]��H�ҐH$R���^R�A�61(?Y舚�>���(Z����Qm�L2�K�ZIc�� 6 | ���̧�C��2!⅄�(����"�Go��>�q��=��$%�z`ѯ��T�&����PHh�Z!=���z��O��������,*VVV�1�f*CJ�]EE��K�k��d�#5���`2yT!�}7���߈~�,���zs�����y�T��V������D��C2�G��@%̑72Y�޾{oJ�"@��^h�~ ��fĬ�!a�D��6���Ha|�3��� [>�����]7U2п���]�ė 7 | ��PU� �.Wejq�in�g��+p<ߺQH����總j[������.��� Q���p _�K�� 1(��+��bB8�\ra 8 | �́�v.l���(���ǽ�w���L��w�8�C��IEND�B`� -------------------------------------------------------------------------------- /part-7/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Tutorial 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /part-7/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { provideRouter } from '@angular/router'; 3 | import { provideHttpClient, withInterceptors } from '@angular/common/http'; 4 | 5 | // fake backend 6 | import { fakeBackendInterceptor } from '@app/_helpers'; 7 | 8 | import { AppComponent } from '@app/app.component'; 9 | import { jwtInterceptor, errorInterceptor } from '@app/_helpers'; 10 | import { APP_ROUTES } from '@app/app.routes'; 11 | 12 | bootstrapApplication(AppComponent, { 13 | providers: [ 14 | provideRouter(APP_ROUTES), 15 | provideHttpClient( 16 | withInterceptors([ 17 | jwtInterceptor, 18 | errorInterceptor, 19 | 20 | // fake backend 21 | fakeBackendInterceptor 22 | ]) 23 | ) 24 | ] 25 | }); 26 | -------------------------------------------------------------------------------- /part-7/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /part-7/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /part-7/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": false, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ], 26 | "paths": { 27 | "@app/*": ["src/app/*"], 28 | "@environments/*": ["src/environments/*"] 29 | } 30 | }, 31 | "angularCompilerOptions": { 32 | "enableI18nLegacyMessageIdFormat": false, 33 | "strictInjectionParameters": true, 34 | "strictInputAccessModifiers": true, 35 | "strictTemplates": true 36 | } 37 | } -------------------------------------------------------------------------------- /part-7/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /part-8/.dockerignore: -------------------------------------------------------------------------------- 1 | # copied from .gitignore 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /part-8/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /part-8/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /bazel-out 8 | 9 | # Node 10 | /node_modules 11 | npm-debug.log 12 | yarn-error.log 13 | 14 | # IDEs and editors 15 | .idea/ 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # Visual Studio Code 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | .history/* 30 | 31 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /part-8/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /part-8/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "ng serve", 7 | "type": "chrome", 8 | "request": "launch", 9 | "preLaunchTask": "npm: start", 10 | "url": "http://localhost:4200/" 11 | }, 12 | { 13 | "name": "ng test", 14 | "type": "chrome", 15 | "request": "launch", 16 | "preLaunchTask": "npm: test", 17 | "url": "http://localhost:9876/debug.html" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /part-8/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 3 | "version": "2.0.0", 4 | "tasks": [ 5 | { 6 | "type": "npm", 7 | "script": "start", 8 | "isBackground": true, 9 | "problemMatcher": { 10 | "owner": "typescript", 11 | "pattern": "$tsc", 12 | "background": { 13 | "activeOnStart": true, 14 | "beginsPattern": { 15 | "regexp": "(.*?)" 16 | }, 17 | "endsPattern": { 18 | "regexp": "bundle generation complete" 19 | } 20 | } 21 | } 22 | }, 23 | { 24 | "type": "npm", 25 | "script": "test", 26 | "isBackground": true, 27 | "problemMatcher": { 28 | "owner": "typescript", 29 | "pattern": "$tsc", 30 | "background": { 31 | "activeOnStart": true, 32 | "beginsPattern": { 33 | "regexp": "(.*?)" 34 | }, 35 | "endsPattern": { 36 | "regexp": "bundle generation complete" 37 | } 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /part-8/Dockerfile: -------------------------------------------------------------------------------- 1 | # ---------------------------- 2 | # build from source 3 | # ---------------------------- 4 | FROM node:18 AS build 5 | 6 | WORKDIR /app 7 | 8 | COPY package*.json . 9 | RUN npm install 10 | 11 | COPY . . 12 | RUN npm run build 13 | 14 | # ---------------------------- 15 | # run with nginx 16 | # ---------------------------- 17 | FROM nginx 18 | 19 | RUN rm /etc/nginx/conf.d/default.conf 20 | COPY nginx.conf /etc/nginx/conf.d 21 | COPY --from=build /app/dist/angular-tutorial /usr/share/nginx/html 22 | 23 | EXPOSE 80 24 | -------------------------------------------------------------------------------- /part-8/README.md: -------------------------------------------------------------------------------- 1 | # AngularTutorial 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.0.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /part-8/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | location / { 3 | root /usr/share/nginx/html; 4 | try_files $uri $uri/ /index.html; 5 | } 6 | } -------------------------------------------------------------------------------- /part-8/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-tutorial", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "watch": "ng build --watch --configuration development", 9 | "test": "ng test" 10 | }, 11 | "private": true, 12 | "dependencies": { 13 | "@angular/animations": "^16.0.0", 14 | "@angular/common": "^16.0.0", 15 | "@angular/compiler": "^16.0.0", 16 | "@angular/core": "^16.0.0", 17 | "@angular/forms": "^16.0.0", 18 | "@angular/platform-browser": "^16.0.0", 19 | "@angular/platform-browser-dynamic": "^16.0.0", 20 | "@angular/router": "^16.0.0", 21 | "rxjs": "~7.8.0", 22 | "tslib": "^2.3.0", 23 | "zone.js": "~0.13.0" 24 | }, 25 | "devDependencies": { 26 | "@angular-devkit/build-angular": "^16.0.0", 27 | "@angular/cli": "~16.0.0", 28 | "@angular/compiler-cli": "^16.0.0", 29 | "@types/jasmine": "~4.3.0", 30 | "jasmine-core": "~4.6.0", 31 | "karma": "~6.4.0", 32 | "karma-chrome-launcher": "~3.2.0", 33 | "karma-coverage": "~2.2.0", 34 | "karma-jasmine": "~5.1.0", 35 | "karma-jasmine-html-reporter": "~2.0.0", 36 | "typescript": "~5.0.2" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /part-8/src/app/_components/alert.component.html: -------------------------------------------------------------------------------- 1 |
2 |
{{alert.message}}
3 |
-------------------------------------------------------------------------------- /part-8/src/app/_components/alert.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { NgIf, NgClass } from '@angular/common'; 3 | import { Subscription } from 'rxjs'; 4 | 5 | import { AlertService } from '@app/_services'; 6 | 7 | @Component({ 8 | selector: 'alert', templateUrl: 'alert.component.html', 9 | standalone: true, 10 | imports: [NgIf, NgClass] 11 | }) 12 | export class AlertComponent implements OnInit, OnDestroy { 13 | private subscription!: Subscription; 14 | alert: any; 15 | 16 | constructor(private alertService: AlertService) { } 17 | 18 | ngOnInit() { 19 | this.subscription = this.alertService.onAlert() 20 | .subscribe(alert => { 21 | switch (alert?.type) { 22 | case 'success': 23 | alert.cssClass = 'alert alert-success'; 24 | break; 25 | case 'error': 26 | alert.cssClass = 'alert alert-danger'; 27 | break; 28 | } 29 | 30 | this.alert = alert; 31 | }); 32 | } 33 | 34 | ngOnDestroy() { 35 | this.subscription.unsubscribe(); 36 | } 37 | } -------------------------------------------------------------------------------- /part-8/src/app/_components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './alert.component'; -------------------------------------------------------------------------------- /part-8/src/app/_helpers/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | 4 | import { AccountService } from '@app/_services'; 5 | 6 | export function authGuard(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { 7 | const router = inject(Router); 8 | const accountService = inject(AccountService); 9 | const user = accountService.userValue; 10 | if (user) { 11 | // authorised so return true 12 | return true; 13 | } 14 | 15 | // not logged in so redirect to login page with the return url 16 | router.navigate(['/account/login'], { queryParams: { returnUrl: state.url } }); 17 | return false; 18 | } 19 | -------------------------------------------------------------------------------- /part-8/src/app/_helpers/error.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { HttpRequest, HttpHandlerFn } from '@angular/common/http'; 3 | import { throwError } from 'rxjs'; 4 | import { catchError } from 'rxjs/operators'; 5 | 6 | import { AccountService } from '@app/_services'; 7 | 8 | export function errorInterceptor(request: HttpRequest, next: HttpHandlerFn) { 9 | const accountService = inject(AccountService); 10 | return next(request).pipe(catchError(err => { 11 | if ([401, 403].includes(err.status) && accountService.userValue) { 12 | // auto logout if 401 or 403 response returned from api 13 | accountService.logout(); 14 | } 15 | 16 | const error = err.error?.message || err.statusText; 17 | console.error(err); 18 | return throwError(() => error); 19 | })) 20 | } 21 | -------------------------------------------------------------------------------- /part-8/src/app/_helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.guard'; 2 | export * from './error.interceptor'; 3 | export * from './fake-backend'; 4 | export * from './jwt.interceptor'; -------------------------------------------------------------------------------- /part-8/src/app/_helpers/jwt.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { inject } from '@angular/core'; 2 | import { HttpRequest, HttpHandlerFn } from '@angular/common/http'; 3 | 4 | import { environment } from '@environments/environment'; 5 | import { AccountService } from '@app/_services'; 6 | 7 | export function jwtInterceptor(request: HttpRequest, next: HttpHandlerFn) { 8 | // add auth header with jwt if user is logged in and request is to the api url 9 | const accountService = inject(AccountService); 10 | const user = accountService.userValue; 11 | const isLoggedIn = user?.token; 12 | const isApiUrl = request.url.startsWith(environment.apiUrl); 13 | if (isLoggedIn && isApiUrl) { 14 | request = request.clone({ 15 | setHeaders: { Authorization: `Bearer ${user.token}` } 16 | }); 17 | } 18 | 19 | return next(request); 20 | } -------------------------------------------------------------------------------- /part-8/src/app/_models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user'; -------------------------------------------------------------------------------- /part-8/src/app/_models/user.ts: -------------------------------------------------------------------------------- 1 | export class User { 2 | id?: string; 3 | username?: string; 4 | password?: string; 5 | firstName?: string; 6 | lastName?: string; 7 | token?: string; 8 | } -------------------------------------------------------------------------------- /part-8/src/app/_services/alert.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Router, NavigationStart } from '@angular/router'; 3 | import { Observable, Subject } from 'rxjs'; 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class AlertService { 7 | private subject = new Subject(); 8 | private showAfterRedirect = false; 9 | 10 | constructor(private router: Router) { 11 | // clear alert messages on route change unless 'showAfterRedirect' flag is true 12 | this.router.events.subscribe(event => { 13 | if (event instanceof NavigationStart) { 14 | if (this.showAfterRedirect) { 15 | // only keep for a single route change 16 | this.showAfterRedirect = false; 17 | } else { 18 | // clear alert message 19 | this.clear(); 20 | } 21 | } 22 | }); 23 | } 24 | 25 | onAlert(): Observable { 26 | return this.subject.asObservable(); 27 | } 28 | 29 | success(message: string, showAfterRedirect = false) { 30 | this.showAfterRedirect = showAfterRedirect; 31 | this.subject.next({ type: 'success', message }); 32 | } 33 | 34 | error(message: string, showAfterRedirect = false) { 35 | this.showAfterRedirect = showAfterRedirect; 36 | this.subject.next({ type: 'error', message }); 37 | } 38 | 39 | clear() { 40 | // clear by calling subject.next() with null 41 | this.subject.next(null); 42 | } 43 | } -------------------------------------------------------------------------------- /part-8/src/app/_services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account.service'; 2 | export * from './alert.service'; -------------------------------------------------------------------------------- /part-8/src/app/account/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login.component'; 2 | export * from './register.component'; -------------------------------------------------------------------------------- /part-8/src/app/account/login.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Login

4 |
5 |
6 |
7 | 8 | 9 |
10 |
Username is required
11 |
12 |
13 |
14 | 15 | 16 |
17 |
Password is required
18 |
19 |
20 |
21 | 25 | Register 26 |
27 |
28 |
29 |
30 |
-------------------------------------------------------------------------------- /part-8/src/app/account/login.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NgClass, NgIf } from '@angular/common'; 3 | import { Router, ActivatedRoute, RouterLink } from '@angular/router'; 4 | import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; 5 | import { first } from 'rxjs/operators'; 6 | 7 | import { AccountService, AlertService } from '@app/_services' 8 | 9 | @Component({ 10 | templateUrl: 'login.component.html', 11 | standalone: true, 12 | imports: [ReactiveFormsModule, NgClass, NgIf, RouterLink] 13 | }) 14 | export class LoginComponent implements OnInit { 15 | form!: FormGroup; 16 | loading = false; 17 | submitted = false; 18 | 19 | constructor( 20 | private formBuilder: FormBuilder, 21 | private route: ActivatedRoute, 22 | private router: Router, 23 | private accountService: AccountService, 24 | private alertService: AlertService 25 | ) { 26 | // redirect to home if already logged in 27 | if (this.accountService.userValue) { 28 | this.router.navigate(['/']); 29 | } 30 | } 31 | 32 | ngOnInit() { 33 | this.form = this.formBuilder.group({ 34 | username: ['', Validators.required], 35 | password: ['', Validators.required] 36 | }); 37 | } 38 | 39 | // convenience getter for easy access to form fields 40 | get f() { return this.form.controls; } 41 | 42 | onSubmit() { 43 | this.submitted = true; 44 | 45 | // reset alerts on submit 46 | this.alertService.clear(); 47 | 48 | // stop here if form is invalid 49 | if (this.form.invalid) { 50 | return; 51 | } 52 | 53 | this.loading = true; 54 | this.accountService.login(this.f.username.value, this.f.password.value) 55 | .pipe(first()) 56 | .subscribe({ 57 | next: () => { 58 | // get return url from query parameters or default to home page 59 | const returnUrl = this.route.snapshot.queryParams['returnUrl'] || '/'; 60 | this.router.navigateByUrl(returnUrl); 61 | }, 62 | error: error => { 63 | this.alertService.error(error); 64 | this.loading = false; 65 | } 66 | }); 67 | } 68 | } -------------------------------------------------------------------------------- /part-8/src/app/account/register.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NgClass, NgIf } from '@angular/common'; 3 | import { Router, ActivatedRoute, RouterLink } from '@angular/router'; 4 | import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; 5 | import { first } from 'rxjs/operators'; 6 | 7 | import { AccountService, AlertService } from '@app/_services'; 8 | 9 | @Component({ 10 | templateUrl: 'register.component.html', 11 | standalone: true, 12 | imports: [ReactiveFormsModule, NgClass, NgIf, RouterLink] 13 | }) 14 | export class RegisterComponent implements OnInit { 15 | form!: FormGroup; 16 | loading = false; 17 | submitted = false; 18 | 19 | constructor( 20 | private formBuilder: FormBuilder, 21 | private route: ActivatedRoute, 22 | private router: Router, 23 | private accountService: AccountService, 24 | private alertService: AlertService 25 | ) { 26 | // redirect to home if already logged in 27 | if (this.accountService.userValue) { 28 | this.router.navigate(['/']); 29 | } 30 | } 31 | 32 | ngOnInit() { 33 | this.form = this.formBuilder.group({ 34 | firstName: ['', Validators.required], 35 | lastName: ['', Validators.required], 36 | username: ['', Validators.required], 37 | password: ['', [Validators.required, Validators.minLength(6)]] 38 | }); 39 | } 40 | 41 | // convenience getter for easy access to form fields 42 | get f() { return this.form.controls; } 43 | 44 | onSubmit() { 45 | this.submitted = true; 46 | 47 | // reset alert on submit 48 | this.alertService.clear(); 49 | 50 | // stop here if form is invalid 51 | if (this.form.invalid) { 52 | return; 53 | } 54 | 55 | this.loading = true; 56 | this.accountService.register(this.form.value) 57 | .pipe(first()) 58 | .subscribe({ 59 | next: () => { 60 | this.alertService.success('Registration successful', true); 61 | this.router.navigate(['/account/login'], { queryParams: { registered: true }}); 62 | }, 63 | error: error => { 64 | this.alertService.error(error); 65 | this.loading = false; 66 | } 67 | }); 68 | } 69 | } -------------------------------------------------------------------------------- /part-8/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 |
12 | 13 | 14 |
-------------------------------------------------------------------------------- /part-8/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NgIf } from '@angular/common'; 3 | import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; 4 | 5 | import { AccountService } from './_services'; 6 | import { User } from './_models'; 7 | import { AlertComponent } from './_components/alert.component'; 8 | 9 | @Component({ 10 | selector: 'app-root', templateUrl: 'app.component.html', 11 | standalone: true, 12 | imports: [NgIf, RouterOutlet, RouterLink, RouterLinkActive, AlertComponent] 13 | }) 14 | export class AppComponent { 15 | user?: User | null; 16 | 17 | constructor(private accountService: AccountService) { 18 | this.accountService.user.subscribe(x => this.user = x); 19 | } 20 | 21 | logout() { 22 | this.accountService.logout(); 23 | } 24 | } -------------------------------------------------------------------------------- /part-8/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from "@angular/router"; 2 | 3 | import { HomeComponent } from './home'; 4 | import { LoginComponent, RegisterComponent } from './account'; 5 | import { authGuard } from './_helpers'; 6 | 7 | const usersRoutes = () => import('./users/users.routes').then(x => x.USERS_ROUTES); 8 | 9 | export const APP_ROUTES: Routes = [ 10 | { path: '', component: HomeComponent, canActivate: [authGuard] }, 11 | { path: 'users', loadChildren: usersRoutes, canActivate: [authGuard] }, 12 | { path: 'account/login', component: LoginComponent }, 13 | { path: 'account/register', component: RegisterComponent }, 14 | 15 | // otherwise redirect to home 16 | { path: '**', redirectTo: '' } 17 | ]; 18 | -------------------------------------------------------------------------------- /part-8/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Hi {{user?.firstName}}!

4 |

You're logged in with Angular!!

5 |
6 |
-------------------------------------------------------------------------------- /part-8/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { User } from '@app/_models'; 4 | import { AccountService } from '@app/_services'; 5 | 6 | @Component({ 7 | templateUrl: 'home.component.html', 8 | standalone: true 9 | }) 10 | export class HomeComponent { 11 | user: User | null; 12 | 13 | constructor(private accountService: AccountService) { 14 | this.user = this.accountService.userValue; 15 | } 16 | } -------------------------------------------------------------------------------- /part-8/src/app/home/index.ts: -------------------------------------------------------------------------------- 1 | export * from './home.component'; -------------------------------------------------------------------------------- /part-8/src/app/users/list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Users

4 | Add User 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 26 | 27 | 28 | 31 | 32 | 33 |
First NameLast NameUsername
{{user.firstName}}{{user.lastName}}{{user.username}} 20 | Edit 21 | 25 |
29 | 30 |
34 |
35 |
-------------------------------------------------------------------------------- /part-8/src/app/users/list.component.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Component, OnInit } from '@angular/core'; 3 | import { NgFor, NgIf } from '@angular/common'; 4 | import { RouterLink } from '@angular/router'; 5 | import { first } from 'rxjs/operators'; 6 | 7 | import { AccountService } from '@app/_services'; 8 | 9 | @Component({ 10 | templateUrl: 'list.component.html', 11 | standalone: true, 12 | imports: [RouterLink, NgFor, NgIf] 13 | }) 14 | export class ListComponent implements OnInit { 15 | users?: any[]; 16 | 17 | constructor(private accountService: AccountService) {} 18 | 19 | ngOnInit() { 20 | this.accountService.getAll() 21 | .pipe(first()) 22 | .subscribe(users => this.users = users); 23 | } 24 | 25 | deleteUser(id: string) { 26 | const user = this.users!.find(x => x.id === id); 27 | user.isDeleting = true; 28 | this.accountService.delete(id) 29 | .pipe(first()) 30 | .subscribe(() => this.users = this.users!.filter(x => x.id !== id)); 31 | } 32 | } -------------------------------------------------------------------------------- /part-8/src/app/users/users.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | 3 | import { ListComponent } from './list.component'; 4 | import { AddEditComponent } from './add-edit.component'; 5 | 6 | export const USERS_ROUTES: Routes = [ 7 | { path: '', component: ListComponent }, 8 | { path: 'add', component: AddEditComponent }, 9 | { path: 'edit/:id', component: AddEditComponent } 10 | ]; -------------------------------------------------------------------------------- /part-8/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cornflourblue/angular-16-tutorial/a2d771bd813e937d38b1d47ea93bfc270905ee2c/part-8/src/assets/.gitkeep -------------------------------------------------------------------------------- /part-8/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | apiUrl: 'http://localhost:4000' 3 | }; -------------------------------------------------------------------------------- /part-8/src/favicon.ico: -------------------------------------------------------------------------------- 1 | �PNG 2 |  3 | IHDR?�~� pHYs  ��~�fIDATH��WKLQ���̔�uG�� e�n�. 6qcb�l?���D`�F#� Ku�F 1Qc� 4 | ��!���� ��C�P�|B?$���ܱ3����I&}��}�̽s�[*�ɀU�A��K��yx�gY�Ajq��3L Š���˫�OD�4��3Ϗ:X�3��o�P J�ğo#IH�a����,{>1/�2$�R AR]�)w��?�sZw^��q�Y�m_��e���r[8�^� 5 | �&p��-���A}c��- ������!����2_) E�)㊪j���v�m��ZOi� g�nW�{�n.|�e2�a&�0aŸ����be�̀��C�fˤE%-{�ֺ��׮C��N��jXi�~c�C,t��T�����r�{� �L)s��V��6%�(�#ᤙ!�]��H�ҐH$R���^R�A�61(?Y舚�>���(Z����Qm�L2�K�ZIc�� 6 | ���̧�C��2!⅄�(����"�Go��>�q��=��$%�z`ѯ��T�&����PHh�Z!=���z��O��������,*VVV�1�f*CJ�]EE��K�k��d�#5���`2yT!�}7���߈~�,���zs�����y�T��V������D��C2�G��@%̑72Y�޾{oJ�"@��^h�~ ��fĬ�!a�D��6���Ha|�3��� [>�����]7U2п���]�ė 7 | ��PU� �.Wejq�in�g��+p<ߺQH����總j[������.��� Q���p _�K�� 1(��+��bB8�\ra 8 | �́�v.l���(���ǽ�w���L��w�8�C��IEND�B`� -------------------------------------------------------------------------------- /part-8/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Angular Tutorial 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /part-8/src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { provideRouter } from '@angular/router'; 3 | import { provideHttpClient, withInterceptors } from '@angular/common/http'; 4 | 5 | // fake backend 6 | import { fakeBackendInterceptor } from '@app/_helpers'; 7 | 8 | import { AppComponent } from '@app/app.component'; 9 | import { jwtInterceptor, errorInterceptor } from '@app/_helpers'; 10 | import { APP_ROUTES } from '@app/app.routes'; 11 | 12 | bootstrapApplication(AppComponent, { 13 | providers: [ 14 | provideRouter(APP_ROUTES), 15 | provideHttpClient( 16 | withInterceptors([ 17 | jwtInterceptor, 18 | errorInterceptor, 19 | 20 | // fake backend 21 | fakeBackendInterceptor 22 | ]) 23 | ) 24 | ] 25 | }); 26 | -------------------------------------------------------------------------------- /part-8/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | -------------------------------------------------------------------------------- /part-8/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /part-8/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "forceConsistentCasingInFileNames": true, 8 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": false, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 16 | "experimentalDecorators": true, 17 | "moduleResolution": "node", 18 | "importHelpers": true, 19 | "target": "ES2022", 20 | "module": "ES2022", 21 | "useDefineForClassFields": false, 22 | "lib": [ 23 | "ES2022", 24 | "dom" 25 | ], 26 | "paths": { 27 | "@app/*": ["src/app/*"], 28 | "@environments/*": ["src/environments/*"] 29 | } 30 | }, 31 | "angularCompilerOptions": { 32 | "enableI18nLegacyMessageIdFormat": false, 33 | "strictInjectionParameters": true, 34 | "strictInputAccessModifiers": true, 35 | "strictTemplates": true 36 | } 37 | } -------------------------------------------------------------------------------- /part-8/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "include": [ 11 | "src/**/*.spec.ts", 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | --------------------------------------------------------------------------------