├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── images ├── async-ngif-ngfor-ngclass.gif ├── clear-site-data-after-async-initializer.gif ├── clear-site-data-before-async-initializer.gif ├── how-angular-material-was-added.gif ├── how-create-module-was-generated.gif ├── how-demo-app-was-generated.gif ├── how-error-interceptor-was-generated.gif ├── how-http-errors-are-handled.gif ├── how-lazy-loaded-module-was-added.gif ├── how-mock-library-was-generated.gif ├── how-mock-requests-work.gif ├── how-reactive-forms-work.gif ├── how-this-workspace-was-generated.gif ├── how-to-build-an-angular-app.gif ├── how-to-delete-a-record.gif ├── how-to-refactor-routes.png ├── how-to-run-linter.gif ├── how-to-update-a-record.gif ├── how-unit-tests-are-run.gif ├── main-layout-after-theming.gif ├── main-layout-before-theming.png └── what-demo-app-initially-looked-like.gif ├── package.json ├── projects ├── demo │ ├── .browserslistrc │ ├── e2e │ │ ├── protractor.conf.js │ │ ├── src │ │ │ ├── app.e2e-spec.ts │ │ │ └── app.po.ts │ │ └── tsconfig.json │ ├── karma.conf.js │ ├── src │ │ ├── app │ │ │ ├── app-routing.module.ts │ │ │ ├── app.component.spec.ts │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── common │ │ │ │ ├── error.interceptor.spec.ts │ │ │ │ └── error.interceptor.ts │ │ │ ├── layouts │ │ │ │ └── main-layout │ │ │ │ │ ├── main-layout.component.html │ │ │ │ │ ├── main-layout.component.scss │ │ │ │ │ ├── main-layout.component.spec.ts │ │ │ │ │ ├── main-layout.component.ts │ │ │ │ │ └── main-layout.module.ts │ │ │ ├── todo-create │ │ │ │ ├── todo-create.component.html │ │ │ │ ├── todo-create.component.scss │ │ │ │ ├── todo-create.component.spec.ts │ │ │ │ ├── todo-create.component.ts │ │ │ │ └── todo-create.module.ts │ │ │ ├── todo-list │ │ │ │ ├── todo-list-routing.module.ts │ │ │ │ ├── todo-list.component.html │ │ │ │ ├── todo-list.component.scss │ │ │ │ ├── todo-list.component.spec.ts │ │ │ │ ├── todo-list.component.ts │ │ │ │ └── todo-list.module.ts │ │ │ └── todos │ │ │ │ ├── todo.service.spec.ts │ │ │ │ ├── todo.service.ts │ │ │ │ └── todos.module.ts │ │ ├── assets │ │ │ ├── .gitkeep │ │ │ └── icons │ │ │ │ ├── github.svg │ │ │ │ └── twitter.svg │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── mockServiceWorker.js │ │ ├── polyfills.ts │ │ ├── sass │ │ │ └── _variables.scss │ │ ├── styles.scss │ │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ └── tslint.json └── mock │ ├── karma.conf.js │ ├── src │ ├── index.ts │ ├── lib │ │ ├── browser.ts │ │ ├── db.ts │ │ ├── handlers.ts │ │ ├── models.ts │ │ └── validation.ts │ └── test.ts │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ ├── tsconfig.spec.json │ └── tslint.json ├── tsconfig.json ├── tslint.json └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.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 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ng101 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 11.2.7. 4 | 5 | ## Log 6 | 7 | ### How this workspace was generated 8 | 9 | ```sh 10 | npx @angular/cli new ng101 --directory=angular-101 --package-manager=yarn --strict --create-application=false 11 | ``` 12 | 13 |  14 | 15 | ### How demo app was generated 16 | 17 | ```sh 18 | yarn ng generate application demo --routing --style=scss 19 | ``` 20 | 21 |  22 | 23 | ### What demo app initially looked like 24 | 25 | ```sh 26 | yarn start --open 27 | ``` 28 | 29 |  30 | 31 | ### How Angular Material was added 32 | 33 | ```sh 34 | yarn ng add @angular/material 35 | ``` 36 | 37 |  38 | 39 | ### How unit tests are run 40 | 41 | ```sh 42 | yarn test 43 | ``` 44 | 45 |  46 | 47 | **Note:** Karma was [configured to run on headless Chrome](projects/demo/karma.conf.js#L40). The default setup would open a new Chrome instance. 48 | 49 | ### How lazy-loaded module was added 50 | 51 | ```sh 52 | yarn ng g module todo-list --module=app --route=todo-list 53 | ``` 54 | 55 |  56 | 57 | **Note:** `g` is the short alias for `generate`. 58 | 59 | ### How to generate a layout 60 | 61 | ```sh 62 | # first create the module 63 | yarn ng g module layouts/main-layout --module=todo-list/todo-list 64 | 65 | # then create the component 66 | yarn ng g component layouts/main-layout/main-layout --export --change-detection=OnPush --flat 67 | ``` 68 | 69 |  70 | 71 | **Note 1:** [Configured todo list routing](projects/demo/src/app/todo-list/todo-list-routing.module.ts) to have nested routes. 72 | 73 | **Note 2:** Obviously, [some styles](projects/demo/src/app/layouts/main-layout/main-layout.component.scss) as well as [Material components](projects/demo/src/app/layouts/main-layout/main-layout.component.html) were used to get that result. 74 | 75 | ### How to customize Material theme 76 | 77 | ```scss 78 | @import "~@angular/material/theming"; 79 | 80 | $app-primary: mat-palette($mat-blue-grey, 900); 81 | $app-accent: mat-palette($mat-yellow, 700); 82 | $app-warn: mat-palette($mat-red, 600); 83 | $app-theme: mat-light-theme( 84 | ( 85 | color: ( 86 | primary: $app-primary, 87 | accent: $app-accent, 88 | warn: $app-warn, 89 | ), 90 | ) 91 | ); 92 | ``` 93 | 94 |  95 | 96 | **Note 1:** Material has [over 900 free icons](https://fonts.google.com/icons?selected=Material+Icons) and, when required, [custom icons can be registered](projects/demo/src/app/layouts/main-layout/main-layout.component.ts) via a service. 97 | 98 | **Note 2:** Angular protects us against XSS attacks. Custom SVG source could be registered only after [explicit trust was granted](projects/demo/src/app/layouts/main-layout/main-layout.component.ts). 99 | 100 | ### How linter is run 101 | 102 | ```sh 103 | yarn lint 104 | ``` 105 | 106 |  107 | 108 | **Note:** TS Lint is deprecated and is expected to be replaced by Angular team. 109 | 110 | ### How mock library was generated 111 | 112 | **Important:** This step is not needed when developing Angular apps with a backend. 113 | 114 | ```sh 115 | yarn ng g library mock --entry-file=index --skip-package-json 116 | ``` 117 | 118 |  119 | 120 | ### How MSW and PouchDB were integrated 121 | 122 | **Important:** This step is not needed when developing Angular apps with a backend. 123 | 124 | 1. Installed [dependencies](package.json). 125 | 2. Added pouchdb to [script injected by Angular](angular.json#L39) when app is built. 126 | 3. Used MSW CLI to create [mockServiceWorker.js](projects/demo/src/mockServiceWorker.js) and [added the generated file to assets](angular.json#L32). 127 | 4. Created [models and request handlers](projects/mock/src/lib). 128 | 5. Initiated msw [only in development](projects/demo/src/environments/environment.ts). 129 | 130 |  131 | 132 | **Note:** Using MSW is a personal preference. There are other mocking options such as [Angular in-memory-web-api](https://github.com/angular/angular/tree/master/packages/misc/angular-in-memory-web-api) or providing mock services with dependency injection. 133 | 134 | ### How AsyncPipe, ngIf, nfFor, and ngClass works 135 | 136 | ```html 137 | 138 | 139 | 140 | {{ todo.title }} 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | ``` 151 | 152 |  153 | 154 | ### How to execute async operations before initialization 155 | 156 | The mock DB implementation so far has an error. When site data is cleared and the page is refreshed, the first response is empty. 157 | 158 |  159 | 160 | This is due to lack of proper asynchronous initialization. `APP_INITIALIZER` serves that purpose. 161 | 162 | ```ts 163 | { 164 | provide: APP_INITIALIZER, 165 | useFactory: () => { 166 | return async () => { 167 | await seedDb(); 168 | worker.start(); 169 | }; 170 | }, 171 | multi: true, 172 | } 173 | ``` 174 | 175 |  176 | 177 | ### How to update a record 178 | 179 | CRUD operations via AJAX are probably the most common implementations in web development. Angular has an awesome `HttpClient` to do all sorts of HTTP requests. 180 | 181 | ```ts 182 | @Injectable() 183 | export class TodoService { 184 | constructor(private http: HttpClient) {} 185 | 186 | update(id: string, input: TodoUpdate) { 187 | return this.http.put(`/api/todos/${id}`, input); 188 | } 189 | } 190 | ``` 191 | 192 | ...and in component class... 193 | 194 | ```ts 195 | @Component(/* removed for brevity */) 196 | export class TodoListComponent { 197 | private listUpdate$ = new Subject(); 198 | 199 | list$ = merge(of(0), this.listUpdate$).pipe( 200 | switchMap(() => this.todo.getList()) 201 | ); 202 | 203 | constructor(private todo: TodoService, private dialog: MatDialog) {} 204 | 205 | toggleDone(todo: Rec) { 206 | this.todo 207 | .update(todo.id, { title: todo.title, done: !todo.done }) 208 | .subscribe(() => this.listUpdate$.next()); 209 | } 210 | } 211 | ``` 212 | 213 |  214 | 215 | **Note:** Did you notice the canceled request? This is due to use of `switchMap` in `list$`. 216 | 217 | ### How to delete a record 218 | 219 | Sometimes, we need to get some confirmation before proceeding with the request. `HttpClient` uses [RxJS observables](https://rxjs.dev/), so that usually is quite easy. 220 | 221 | ```ts 222 | @Component(/* removed for brevity */) 223 | export class TodoListComponent { 224 | @ViewChild("deleteDialog") deleteDialog?: TemplateRef; 225 | 226 | /* removed for brevity */ 227 | 228 | deleteRecord(todo: Rec) { 229 | this.dialog 230 | .open(this.deleteDialog!, { data: todo.title }) 231 | .afterClosed() 232 | .pipe( 233 | concatMap((confirmed) => 234 | confirmed ? this.todo.delete(todo.id) : EMPTY 235 | ) 236 | ) 237 | .subscribe(() => this.listUpdate$.next()); 238 | } 239 | } 240 | ``` 241 | 242 |  243 | 244 | ### How to refactor routes 245 | 246 | Angular modules manage their own child routes and parent modules are unaware of grand child routes. This makes it easy to refactor routes. 247 | 248 | ```ts 249 | @NgModule({ 250 | imports: [ 251 | RouterModule.forChild([{ path: "", component: TodoListComponent }]), 252 | ], 253 | exports: [RouterModule], 254 | }) 255 | export class TodoListRoutingModule {} 256 | 257 | @NgModule({ 258 | imports: [ 259 | RouterModule.forChild([ 260 | { 261 | path: "", 262 | component: MainLayoutComponent, 263 | children: [ 264 | { path: "", pathMatch: "full", loadChildren: () => TodoListModule }, 265 | ], 266 | }, 267 | ]), 268 | ], 269 | exports: [RouterModule], 270 | }) 271 | export class TodosRoutingModule {} 272 | 273 | @NgModule({ 274 | imports: [ 275 | RouterModule.forRoot([ 276 | { 277 | path: "", 278 | loadChildren: () => 279 | import("./todos/todos.module").then((m) => m.TodosModule), 280 | }, 281 | ]), 282 | ], 283 | exports: [RouterModule], 284 | }) 285 | export class AppRoutingModule {} 286 | ``` 287 | 288 | Although there is now an additional module between, nothing changes. 289 | 290 |  291 | 292 | ### How to create a new record 293 | 294 | ```sh 295 | yarn ng g module todo-create --module=todos/todos --route=create 296 | ``` 297 | 298 |  299 | 300 | ```ts 301 | @Component(/* removed for brevity */) 302 | export class TodoCreateComponent { 303 | form!: FormGroup; 304 | 305 | constructor( 306 | private fb: FormBuilder, 307 | private router: Router, 308 | private todo: TodoService 309 | ) { 310 | this.buildForm(); 311 | } 312 | 313 | goToListView() { 314 | this.router.navigate([".."]); 315 | } 316 | 317 | submitForm() { 318 | if (!this.form.valid) return; 319 | 320 | this.todo.create(this.form.value).subscribe(() => this.goToListView()); 321 | } 322 | 323 | private buildForm(): void { 324 | this.form = this.fb.group({ 325 | title: [null, Validators.required], 326 | }); 327 | } 328 | } 329 | ``` 330 | 331 | ...and in template... 332 | 333 | ```html 334 | 335 | 336 | 337 | 338 | Todo title * 339 | 347 | {{ title.value.length }} / 256 348 | 349 | Sorry, this field is required. 350 | 351 | 352 | 353 | 354 | 355 | ``` 356 | 357 |  358 | 359 | ### How HTTP errors were handled 360 | 361 | ```sh 362 | yarn ng g interceptor common/error --flat 363 | ``` 364 | 365 |  366 | 367 | ...then... 368 | 369 | ```ts 370 | @Injectable() 371 | export class ErrorInterceptor implements HttpInterceptor { 372 | constructor(private snackBar: MatSnackBar) {} 373 | 374 | intercept( 375 | request: HttpRequest, 376 | next: HttpHandler 377 | ): Observable> { 378 | return next.handle(request).pipe( 379 | catchError(({ error, status }) => { 380 | this.snackBar.open(`${status}: ${error}`, "HTTP Error", { 381 | duration: 3000, 382 | }); 383 | return EMPTY; 384 | }) 385 | ); 386 | } 387 | } 388 | ``` 389 | 390 | ...and in root module... 391 | 392 | ```ts 393 | @NgModule({ 394 | /* removed for brevity */ 395 | 396 | providers: [ 397 | { 398 | provide: HTTP_INTERCEPTORS, 399 | useClass: ErrorInterceptor, 400 | multi: true, 401 | }, 402 | ], 403 | }) 404 | export class AppModule {} 405 | ``` 406 | 407 | **Note:** Http interceptors [can intercept outgoing requests as well](https://angular.io/guide/http#intercepting-requests-and-responses). 408 | 409 | ### How to get a production build of the app 410 | 411 | ```sh 412 | yarn build --prod 413 | ``` 414 | 415 |  416 | 417 | ### Further help 418 | 419 | 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. 420 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "cli": { 5 | "packageManager": "yarn" 6 | }, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "demo": { 10 | "projectType": "application", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | } 15 | }, 16 | "root": "projects/demo", 17 | "sourceRoot": "projects/demo/src", 18 | "prefix": "app", 19 | "architect": { 20 | "build": { 21 | "builder": "@angular-devkit/build-angular:browser", 22 | "options": { 23 | "outputPath": "dist/demo", 24 | "index": "projects/demo/src/index.html", 25 | "main": "projects/demo/src/main.ts", 26 | "polyfills": "projects/demo/src/polyfills.ts", 27 | "tsConfig": "projects/demo/tsconfig.app.json", 28 | "aot": true, 29 | "assets": [ 30 | "projects/demo/src/favicon.ico", 31 | "projects/demo/src/assets", 32 | "projects/demo/src/mockServiceWorker.js" 33 | ], 34 | "styles": [ 35 | "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", 36 | "projects/demo/src/styles.scss" 37 | ], 38 | "scripts": [ 39 | "node_modules/pouchdb/dist/pouchdb.min.js", 40 | "node_modules/pouchdb/dist/pouchdb.find.min.js" 41 | ] 42 | }, 43 | "configurations": { 44 | "production": { 45 | "fileReplacements": [ 46 | { 47 | "replace": "projects/demo/src/environments/environment.ts", 48 | "with": "projects/demo/src/environments/environment.prod.ts" 49 | } 50 | ], 51 | "optimization": true, 52 | "outputHashing": "all", 53 | "sourceMap": false, 54 | "namedChunks": false, 55 | "extractLicenses": true, 56 | "vendorChunk": false, 57 | "buildOptimizer": true, 58 | "budgets": [ 59 | { 60 | "type": "initial", 61 | "maximumWarning": "2mb", 62 | "maximumError": "5mb" 63 | }, 64 | { 65 | "type": "anyComponentStyle", 66 | "maximumWarning": "6kb", 67 | "maximumError": "10kb" 68 | } 69 | ], 70 | "scripts": [] 71 | } 72 | } 73 | }, 74 | "serve": { 75 | "builder": "@angular-devkit/build-angular:dev-server", 76 | "options": { 77 | "browserTarget": "demo:build" 78 | }, 79 | "configurations": { 80 | "production": { 81 | "browserTarget": "demo:build:production" 82 | } 83 | } 84 | }, 85 | "extract-i18n": { 86 | "builder": "@angular-devkit/build-angular:extract-i18n", 87 | "options": { 88 | "browserTarget": "demo:build" 89 | } 90 | }, 91 | "test": { 92 | "builder": "@angular-devkit/build-angular:karma", 93 | "options": { 94 | "main": "projects/demo/src/test.ts", 95 | "polyfills": "projects/demo/src/polyfills.ts", 96 | "tsConfig": "projects/demo/tsconfig.spec.json", 97 | "karmaConfig": "projects/demo/karma.conf.js", 98 | "assets": [ 99 | "projects/demo/src/favicon.ico", 100 | "projects/demo/src/assets" 101 | ], 102 | "styles": [ 103 | "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", 104 | "projects/demo/src/styles.scss" 105 | ], 106 | "scripts": [] 107 | } 108 | }, 109 | "lint": { 110 | "builder": "@angular-devkit/build-angular:tslint", 111 | "options": { 112 | "tsConfig": [ 113 | "projects/demo/tsconfig.app.json", 114 | "projects/demo/tsconfig.spec.json", 115 | "projects/demo/e2e/tsconfig.json" 116 | ], 117 | "exclude": ["**/node_modules/**"] 118 | } 119 | }, 120 | "e2e": { 121 | "builder": "@angular-devkit/build-angular:protractor", 122 | "options": { 123 | "protractorConfig": "projects/demo/e2e/protractor.conf.js", 124 | "devServerTarget": "demo:serve" 125 | }, 126 | "configurations": { 127 | "production": { 128 | "devServerTarget": "demo:serve:production" 129 | } 130 | } 131 | } 132 | } 133 | }, 134 | "mock": { 135 | "projectType": "library", 136 | "root": "projects/mock", 137 | "sourceRoot": "projects/mock/src", 138 | "prefix": "lib", 139 | "architect": { 140 | "test": { 141 | "builder": "@angular-devkit/build-angular:karma", 142 | "options": { 143 | "main": "projects/mock/src/test.ts", 144 | "tsConfig": "projects/mock/tsconfig.spec.json", 145 | "karmaConfig": "projects/mock/karma.conf.js" 146 | } 147 | }, 148 | "lint": { 149 | "builder": "@angular-devkit/build-angular:tslint", 150 | "options": { 151 | "tsConfig": [ 152 | "projects/mock/tsconfig.lib.json", 153 | "projects/mock/tsconfig.spec.json" 154 | ], 155 | "exclude": ["**/node_modules/**"] 156 | } 157 | } 158 | } 159 | } 160 | }, 161 | "defaultProject": "demo" 162 | } 163 | -------------------------------------------------------------------------------- /images/async-ngif-ngfor-ngclass.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/async-ngif-ngfor-ngclass.gif -------------------------------------------------------------------------------- /images/clear-site-data-after-async-initializer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/clear-site-data-after-async-initializer.gif -------------------------------------------------------------------------------- /images/clear-site-data-before-async-initializer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/clear-site-data-before-async-initializer.gif -------------------------------------------------------------------------------- /images/how-angular-material-was-added.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-angular-material-was-added.gif -------------------------------------------------------------------------------- /images/how-create-module-was-generated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-create-module-was-generated.gif -------------------------------------------------------------------------------- /images/how-demo-app-was-generated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-demo-app-was-generated.gif -------------------------------------------------------------------------------- /images/how-error-interceptor-was-generated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-error-interceptor-was-generated.gif -------------------------------------------------------------------------------- /images/how-http-errors-are-handled.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-http-errors-are-handled.gif -------------------------------------------------------------------------------- /images/how-lazy-loaded-module-was-added.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-lazy-loaded-module-was-added.gif -------------------------------------------------------------------------------- /images/how-mock-library-was-generated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-mock-library-was-generated.gif -------------------------------------------------------------------------------- /images/how-mock-requests-work.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-mock-requests-work.gif -------------------------------------------------------------------------------- /images/how-reactive-forms-work.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-reactive-forms-work.gif -------------------------------------------------------------------------------- /images/how-this-workspace-was-generated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-this-workspace-was-generated.gif -------------------------------------------------------------------------------- /images/how-to-build-an-angular-app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-to-build-an-angular-app.gif -------------------------------------------------------------------------------- /images/how-to-delete-a-record.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-to-delete-a-record.gif -------------------------------------------------------------------------------- /images/how-to-refactor-routes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-to-refactor-routes.png -------------------------------------------------------------------------------- /images/how-to-run-linter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-to-run-linter.gif -------------------------------------------------------------------------------- /images/how-to-update-a-record.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-to-update-a-record.gif -------------------------------------------------------------------------------- /images/how-unit-tests-are-run.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/how-unit-tests-are-run.gif -------------------------------------------------------------------------------- /images/main-layout-after-theming.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/main-layout-after-theming.gif -------------------------------------------------------------------------------- /images/main-layout-before-theming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/main-layout-before-theming.png -------------------------------------------------------------------------------- /images/what-demo-app-initially-looked-like.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/images/what-demo-app-initially-looked-like.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng101", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e", 11 | "postinstall": "npx msw init projects/demo/src" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "~11.2.8", 16 | "@angular/cdk": "11.2.7", 17 | "@angular/common": "~11.2.8", 18 | "@angular/compiler": "~11.2.8", 19 | "@angular/core": "~11.2.8", 20 | "@angular/forms": "~11.2.8", 21 | "@angular/material": "11.2.7", 22 | "@angular/platform-browser": "~11.2.8", 23 | "@angular/platform-browser-dynamic": "~11.2.8", 24 | "@angular/router": "~11.2.8", 25 | "rxjs": "~6.6.0", 26 | "tslib": "^2.0.0", 27 | "zone.js": "~0.11.3" 28 | }, 29 | "devDependencies": { 30 | "@angular-devkit/build-angular": "~0.1102.7", 31 | "@angular/cli": "~11.2.7", 32 | "@angular/compiler-cli": "~11.2.8", 33 | "@types/jasmine": "~3.6.0", 34 | "@types/node": "^12.11.1", 35 | "@types/pouchdb": "^6.4.0", 36 | "codelyzer": "^6.0.0", 37 | "jasmine-core": "~3.6.0", 38 | "jasmine-spec-reporter": "~5.0.0", 39 | "karma": "~6.1.0", 40 | "karma-chrome-launcher": "~3.1.0", 41 | "karma-coverage": "~2.0.3", 42 | "karma-jasmine": "~4.0.0", 43 | "karma-jasmine-html-reporter": "^1.5.0", 44 | "msw": "latest", 45 | "pouchdb": "^7.2.2", 46 | "protractor": "~7.0.0", 47 | "ts-node": "~8.3.0", 48 | "tslint": "~6.1.0", 49 | "typescript": "~4.1.5" 50 | }, 51 | "msw": { 52 | "workerDirectory": "projects/demo/src" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /projects/demo/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 18 | -------------------------------------------------------------------------------- /projects/demo/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | SELENIUM_PROMISE_MANAGER: false, 20 | baseUrl: 'http://localhost:4200/', 21 | framework: 'jasmine', 22 | jasmineNodeOpts: { 23 | showColors: true, 24 | defaultTimeoutInterval: 30000, 25 | print: function() {} 26 | }, 27 | onPrepare() { 28 | require('ts-node').register({ 29 | project: require('path').join(__dirname, './tsconfig.json') 30 | }); 31 | jasmine.getEnv().addReporter(new SpecReporter({ 32 | spec: { 33 | displayStacktrace: StacktraceOption.PRETTY 34 | } 35 | })); 36 | } 37 | }; -------------------------------------------------------------------------------- /projects/demo/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { browser, logging } from 'protractor'; 2 | import { AppPage } from './app.po'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', async () => { 12 | await page.navigateTo(); 13 | expect(await page.getTitleText()).toEqual('demo app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /projects/demo/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | async navigateTo(): Promise { 5 | return browser.get(browser.baseUrl); 6 | } 7 | 8 | async getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /projects/demo/e2e/tsconfig.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/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /projects/demo/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, '../../coverage/demo'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['ChromeHeadless'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /projects/demo/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | 4 | @NgModule({ 5 | imports: [ 6 | RouterModule.forRoot([ 7 | { 8 | path: '', 9 | loadChildren: () => 10 | import('./todos/todos.module').then((m) => m.TodosModule), 11 | }, 12 | ]), 13 | ], 14 | exports: [RouterModule], 15 | }) 16 | export class AppRoutingModule {} 17 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [RouterTestingModule], 9 | declarations: [AppComponent], 10 | }).compileComponents(); 11 | }); 12 | 13 | it('should create the app', () => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.componentInstance; 16 | expect(app).toBeTruthy(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | template: '', 6 | }) 7 | export class AppComponent {} 8 | -------------------------------------------------------------------------------- /projects/demo/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | import { environment } from '../environments/environment'; 7 | import { AppRoutingModule } from './app-routing.module'; 8 | import { AppComponent } from './app.component'; 9 | import { ErrorInterceptor } from './common/error.interceptor'; 10 | 11 | @NgModule({ 12 | declarations: [AppComponent], 13 | imports: [ 14 | AppRoutingModule, 15 | BrowserModule, 16 | BrowserAnimationsModule, 17 | HttpClientModule, 18 | MatSnackBarModule, 19 | ], 20 | providers: [ 21 | environment.initializers, 22 | { 23 | provide: HTTP_INTERCEPTORS, 24 | useClass: ErrorInterceptor, 25 | multi: true, 26 | }, 27 | ], 28 | bootstrap: [AppComponent], 29 | }) 30 | export class AppModule {} 31 | -------------------------------------------------------------------------------- /projects/demo/src/app/common/error.interceptor.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { MatSnackBarModule } from '@angular/material/snack-bar'; 3 | import { ErrorInterceptor } from './error.interceptor'; 4 | 5 | describe('ErrorInterceptor', () => { 6 | beforeEach(() => 7 | TestBed.configureTestingModule({ 8 | imports: [MatSnackBarModule], 9 | providers: [ErrorInterceptor], 10 | }) 11 | ); 12 | 13 | it('should be created', () => { 14 | const interceptor: ErrorInterceptor = TestBed.inject(ErrorInterceptor); 15 | expect(interceptor).toBeTruthy(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /projects/demo/src/app/common/error.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpEvent, 3 | HttpHandler, 4 | HttpInterceptor, 5 | HttpRequest, 6 | } from '@angular/common/http'; 7 | import { Injectable } from '@angular/core'; 8 | import { MatSnackBar } from '@angular/material/snack-bar'; 9 | import { EMPTY, Observable } from 'rxjs'; 10 | import { catchError } from 'rxjs/operators'; 11 | 12 | @Injectable() 13 | export class ErrorInterceptor implements HttpInterceptor { 14 | constructor(private snackBar: MatSnackBar) {} 15 | 16 | intercept( 17 | request: HttpRequest, 18 | next: HttpHandler 19 | ): Observable> { 20 | return next.handle(request).pipe( 21 | catchError(({ error, status }) => { 22 | this.snackBar.open(`${status}: ${error}`, 'HTTP Error', { 23 | duration: 3000, 24 | }); 25 | return EMPTY; 26 | }) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /projects/demo/src/app/layouts/main-layout/main-layout.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | StarTodos 4 | 5 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 30 | -------------------------------------------------------------------------------- /projects/demo/src/app/layouts/main-layout/main-layout.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../sass/variables"; 2 | 3 | :host { 4 | --container-min-height: calc(100vh - 100px); 5 | --container-padding: 30px; 6 | 7 | @media (max-width: 599px) { 8 | --container-min-height: calc(100vh - 92px); 9 | --container-padding: 20px; 10 | } 11 | } 12 | 13 | .title { 14 | display: inline-block; 15 | padding: 0 0.5em; 16 | vertical-align: middle; 17 | } 18 | 19 | .title, 20 | mat-icon { 21 | color: mat-color($app-accent, 600); 22 | } 23 | 24 | main { 25 | box-sizing: border-box; 26 | min-height: var(--container-min-height); 27 | padding: var(--container-padding); 28 | } 29 | 30 | footer { 31 | background: mat-color($mat-grey, 200); 32 | height: 36px; 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | 37 | mat-icon { 38 | color: mat-color($mat-pink, 200); 39 | width: 1rem; 40 | height: 1rem; 41 | font-size: 1rem; 42 | margin-left: 4px; 43 | } 44 | } 45 | 46 | .spacer { 47 | flex: 1 1 auto; 48 | } 49 | -------------------------------------------------------------------------------- /projects/demo/src/app/layouts/main-layout/main-layout.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { MatToolbarModule } from '@angular/material/toolbar'; 6 | import { RouterTestingModule } from '@angular/router/testing'; 7 | import { MainLayoutComponent } from './main-layout.component'; 8 | 9 | describe('MainLayoutComponent', () => { 10 | let component: MainLayoutComponent; 11 | let fixture: ComponentFixture; 12 | 13 | beforeEach(async () => { 14 | await TestBed.configureTestingModule({ 15 | imports: [ 16 | HttpClientTestingModule, 17 | RouterTestingModule, 18 | MatButtonModule, 19 | MatIconModule, 20 | MatToolbarModule, 21 | ], 22 | declarations: [MainLayoutComponent], 23 | }).compileComponents(); 24 | }); 25 | 26 | beforeEach(() => { 27 | fixture = TestBed.createComponent(MainLayoutComponent); 28 | component = fixture.componentInstance; 29 | fixture.detectChanges(); 30 | }); 31 | 32 | it('should create', () => { 33 | expect(component).toBeTruthy(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /projects/demo/src/app/layouts/main-layout/main-layout.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component } from '@angular/core'; 2 | import { MatIconRegistry } from '@angular/material/icon'; 3 | import { DomSanitizer } from '@angular/platform-browser'; 4 | 5 | @Component({ 6 | selector: 'app-main-layout', 7 | templateUrl: './main-layout.component.html', 8 | styleUrls: ['./main-layout.component.scss'], 9 | changeDetection: ChangeDetectionStrategy.OnPush, 10 | }) 11 | export class MainLayoutComponent { 12 | constructor( 13 | private iconRegistry: MatIconRegistry, 14 | private sanitizer: DomSanitizer 15 | ) { 16 | this.registerIcon('twitter'); 17 | this.registerIcon('github'); 18 | } 19 | 20 | private registerIcon(name: string): void { 21 | const source = this.sanitizer.bypassSecurityTrustResourceUrl( 22 | `/assets/icons/${name}.svg` 23 | ); 24 | this.iconRegistry.addSvgIcon(name, source); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/demo/src/app/layouts/main-layout/main-layout.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { MatToolbarModule } from '@angular/material/toolbar'; 6 | import { RouterModule } from '@angular/router'; 7 | import { MainLayoutComponent } from './main-layout.component'; 8 | 9 | @NgModule({ 10 | exports: [MainLayoutComponent], 11 | declarations: [MainLayoutComponent], 12 | imports: [ 13 | CommonModule, 14 | RouterModule, 15 | MatButtonModule, 16 | MatIconModule, 17 | MatToolbarModule, 18 | ], 19 | }) 20 | export class MainLayoutModule {} 21 | -------------------------------------------------------------------------------- /projects/demo/src/app/todo-create/todo-create.component.html: -------------------------------------------------------------------------------- 1 | 2 | New todo 3 | 4 | 5 | 6 | 7 | Todo title * 8 | 16 | {{ title.value.length }} / 256 17 | 18 | Sorry, this field is required. 19 | 20 | 21 | 22 | 23 | {{ form.value | json }} 24 | 25 | 26 | 27 | CANCEL 28 | SAVE 29 | 30 | 31 | -------------------------------------------------------------------------------- /projects/demo/src/app/todo-create/todo-create.component.scss: -------------------------------------------------------------------------------- 1 | mat-card { 2 | margin: 0 auto; 3 | max-width: 480px; 4 | padding: 40px 20px; 5 | } 6 | 7 | mat-card-title { 8 | margin-bottom: 20px; 9 | } 10 | 11 | mat-form-field { 12 | width: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /projects/demo/src/app/todo-create/todo-create.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { ReactiveFormsModule } from '@angular/forms'; 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatCardModule } from '@angular/material/card'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | import { MatInputModule } from '@angular/material/input'; 9 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 10 | import { RouterTestingModule } from '@angular/router/testing'; 11 | import { TodoCreateComponent } from './todo-create.component'; 12 | 13 | describe('TodoCreateComponent', () => { 14 | let component: TodoCreateComponent; 15 | let fixture: ComponentFixture; 16 | 17 | beforeEach(async () => { 18 | await TestBed.configureTestingModule({ 19 | imports: [ 20 | BrowserAnimationsModule, 21 | RouterTestingModule, 22 | CommonModule, 23 | HttpClientTestingModule, 24 | ReactiveFormsModule, 25 | MatButtonModule, 26 | MatCardModule, 27 | MatIconModule, 28 | MatInputModule, 29 | ], 30 | declarations: [TodoCreateComponent], 31 | }).compileComponents(); 32 | }); 33 | 34 | beforeEach(() => { 35 | fixture = TestBed.createComponent(TodoCreateComponent); 36 | component = fixture.componentInstance; 37 | fixture.detectChanges(); 38 | }); 39 | 40 | it('should create', () => { 41 | expect(component).toBeTruthy(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /projects/demo/src/app/todo-create/todo-create.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { Router } from '@angular/router'; 4 | import { TodoService } from '../todos/todo.service'; 5 | 6 | @Component({ 7 | selector: 'app-todo-create', 8 | templateUrl: './todo-create.component.html', 9 | styleUrls: ['./todo-create.component.scss'], 10 | }) 11 | export class TodoCreateComponent { 12 | form!: FormGroup; 13 | 14 | constructor( 15 | private fb: FormBuilder, 16 | private router: Router, 17 | private todo: TodoService 18 | ) { 19 | this.buildForm(); 20 | } 21 | 22 | goToListView() { 23 | this.router.navigate(['..']); 24 | } 25 | 26 | submitForm() { 27 | if (!this.form.valid) return; 28 | 29 | this.todo.create(this.form.value).subscribe(() => this.goToListView()); 30 | } 31 | 32 | private buildForm(): void { 33 | this.form = this.fb.group({ 34 | title: [null, Validators.required], 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /projects/demo/src/app/todo-create/todo-create.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatCardModule } from '@angular/material/card'; 6 | import { MatIconModule } from '@angular/material/icon'; 7 | import { MatInputModule } from '@angular/material/input'; 8 | import { RouterModule, Routes } from '@angular/router'; 9 | import { TodoCreateComponent } from './todo-create.component'; 10 | 11 | const routes: Routes = [{ path: '', component: TodoCreateComponent }]; 12 | 13 | @NgModule({ 14 | declarations: [TodoCreateComponent], 15 | imports: [ 16 | RouterModule.forChild(routes), 17 | CommonModule, 18 | ReactiveFormsModule, 19 | MatButtonModule, 20 | MatCardModule, 21 | MatIconModule, 22 | MatInputModule, 23 | ], 24 | }) 25 | export class TodoCreateModule {} 26 | -------------------------------------------------------------------------------- /projects/demo/src/app/todo-list/todo-list-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { TodoListComponent } from './todo-list.component'; 4 | 5 | @NgModule({ 6 | imports: [ 7 | RouterModule.forChild([ 8 | { 9 | path: '', 10 | component: TodoListComponent, 11 | }, 12 | ]), 13 | ], 14 | exports: [RouterModule], 15 | }) 16 | export class TodoListRoutingModule {} 17 | -------------------------------------------------------------------------------- /projects/demo/src/app/todo-list/todo-list.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 12 | 13 | 14 | {{ todo.title }} 15 | 16 | 22 | delete_outline 23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | (o^^)o 46 | Hurray! You have no task to do. 47 | 48 | 49 | 50 | 51 | Delete Confirmation 52 | 53 | "{{ title }}" will be deleted forever. 54 | Do you want to continue deleting? 55 | 56 | 57 | Cancel 58 | 59 | Delete 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /projects/demo/src/app/todo-list/todo-list.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | --todo-list-padding: 20px; 3 | } 4 | 5 | mat-card { 6 | margin: 0 auto; 7 | max-width: 720px; 8 | padding: var(--todo-list-padding); 9 | } 10 | 11 | mat-selection-list { 12 | padding: 0; 13 | } 14 | 15 | .spinner { 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | height: calc( 20 | var(--container-min-height) - 2 * var(--container-padding) - 2 * 21 | var(--todo-list-padding) 22 | ); 23 | } 24 | 25 | .done { 26 | text-decoration: line-through; 27 | } 28 | 29 | .todo-item { 30 | display: flex; 31 | align-items: center; 32 | justify-content: space-between; 33 | } 34 | 35 | .empty-state { 36 | text-align: center; 37 | font-weight: 300; 38 | } 39 | 40 | .huge-emoji { 41 | color: #dadce0; 42 | font-size: 90px; 43 | line-height: 1.5; 44 | } 45 | 46 | .footer { 47 | padding: 12px 0; 48 | text-align: center; 49 | 50 | mat-icon { 51 | display: inline-block; 52 | margin: -3px 0 0 -4px; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /projects/demo/src/app/todo-list/todo-list.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 4 | import { ReactiveFormsModule } from '@angular/forms'; 5 | import { MatButtonModule } from '@angular/material/button'; 6 | import { MatCardModule } from '@angular/material/card'; 7 | import { MatDialogModule } from '@angular/material/dialog'; 8 | import { MatIconModule } from '@angular/material/icon'; 9 | import { MatInputModule } from '@angular/material/input'; 10 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 11 | import { RouterTestingModule } from '@angular/router/testing'; 12 | import { TodoListComponent } from './todo-list.component'; 13 | 14 | describe('TodoListComponent', () => { 15 | let component: TodoListComponent; 16 | let fixture: ComponentFixture; 17 | 18 | beforeEach(async () => { 19 | await TestBed.configureTestingModule({ 20 | imports: [ 21 | BrowserAnimationsModule, 22 | RouterTestingModule, 23 | CommonModule, 24 | ReactiveFormsModule, 25 | HttpClientTestingModule, 26 | MatButtonModule, 27 | MatCardModule, 28 | MatIconModule, 29 | MatInputModule, 30 | MatDialogModule, 31 | ], 32 | declarations: [TodoListComponent], 33 | }).compileComponents(); 34 | }); 35 | 36 | beforeEach(() => { 37 | fixture = TestBed.createComponent(TodoListComponent); 38 | component = fixture.componentInstance; 39 | fixture.detectChanges(); 40 | }); 41 | 42 | it('should create', () => { 43 | expect(component).toBeTruthy(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /projects/demo/src/app/todo-list/todo-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, TemplateRef, ViewChild } from '@angular/core'; 2 | import { MatDialog } from '@angular/material/dialog'; 3 | import type { Rec, Todo } from '@ng101/mock'; 4 | import { EMPTY, merge, of, Subject } from 'rxjs'; 5 | import { concatMap, switchMap } from 'rxjs/operators'; 6 | import { TodoService } from '../todos/todo.service'; 7 | 8 | @Component({ 9 | selector: 'app-todo-list', 10 | templateUrl: './todo-list.component.html', 11 | styleUrls: ['./todo-list.component.scss'], 12 | }) 13 | export class TodoListComponent { 14 | @ViewChild('deleteDialog') 15 | private deleteDialog?: TemplateRef; 16 | 17 | private listUpdate$ = new Subject(); 18 | 19 | list$ = merge(of(0), this.listUpdate$).pipe( 20 | switchMap(() => this.todo.getList()) 21 | ); 22 | 23 | constructor(private todo: TodoService, private dialog: MatDialog) {} 24 | 25 | toggleDone(todo: Rec) { 26 | this.todo 27 | .update(todo.id, { title: todo.title, done: !todo.done }) 28 | .subscribe(() => this.listUpdate$.next()); 29 | } 30 | 31 | deleteRecord(todo: Rec) { 32 | this.dialog 33 | .open(this.deleteDialog!, { data: todo.title }) 34 | .afterClosed() 35 | .pipe( 36 | concatMap((confirmed) => 37 | confirmed ? this.todo.delete(todo.id) : EMPTY 38 | ) 39 | ) 40 | .subscribe(() => this.listUpdate$.next()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /projects/demo/src/app/todo-list/todo-list.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatCardModule } from '@angular/material/card'; 5 | import { MatDialogModule } from '@angular/material/dialog'; 6 | import { MatIconModule } from '@angular/material/icon'; 7 | import { MatListModule } from '@angular/material/list'; 8 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; 9 | import { TodoListRoutingModule } from './todo-list-routing.module'; 10 | import { TodoListComponent } from './todo-list.component'; 11 | 12 | @NgModule({ 13 | declarations: [TodoListComponent], 14 | imports: [ 15 | CommonModule, 16 | TodoListRoutingModule, 17 | MatButtonModule, 18 | MatCardModule, 19 | MatDialogModule, 20 | MatIconModule, 21 | MatListModule, 22 | MatProgressSpinnerModule, 23 | ], 24 | }) 25 | export class TodoListModule {} 26 | -------------------------------------------------------------------------------- /projects/demo/src/app/todos/todo.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { TestBed } from '@angular/core/testing'; 3 | import { TodoService } from './todo.service'; 4 | 5 | describe('TodoService', () => { 6 | let service: TodoService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({ 10 | imports: [HttpClientTestingModule], 11 | }); 12 | service = TestBed.inject(TodoService); 13 | }); 14 | 15 | it('should be created', () => { 16 | expect(service).toBeTruthy(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /projects/demo/src/app/todos/todo.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import type { List, Rec, Todo, TodoCreate, TodoUpdate } from '@ng101/mock'; 4 | 5 | @Injectable({ 6 | providedIn: 'root', 7 | }) 8 | export class TodoService { 9 | constructor(private http: HttpClient) {} 10 | 11 | getList() { 12 | return this.http.get>('/api/todos'); 13 | } 14 | 15 | create(input: TodoCreate) { 16 | return this.http.post>('/api/todos', input); 17 | } 18 | 19 | delete(id: string) { 20 | return this.http.delete(`/api/todos/${id}`); 21 | } 22 | 23 | update(id: string, input: TodoUpdate) { 24 | return this.http.put(`/api/todos/${id}`, input); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/demo/src/app/todos/todos.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule } from '@angular/router'; 3 | import { MainLayoutComponent } from '../layouts/main-layout/main-layout.component'; 4 | import { MainLayoutModule } from '../layouts/main-layout/main-layout.module'; 5 | 6 | @NgModule({ 7 | imports: [ 8 | RouterModule.forChild([ 9 | { 10 | path: '', 11 | component: MainLayoutComponent, 12 | children: [ 13 | { 14 | path: '', 15 | pathMatch: 'full', 16 | loadChildren: () => 17 | import('../todo-list/todo-list.module').then( 18 | (m) => m.TodoListModule 19 | ), 20 | }, 21 | { 22 | path: 'create', 23 | loadChildren: () => 24 | import('../todo-create/todo-create.module').then( 25 | (m) => m.TodoCreateModule 26 | ), 27 | }, 28 | ], 29 | }, 30 | ]), 31 | MainLayoutModule, 32 | ], 33 | exports: [RouterModule], 34 | }) 35 | export class TodosModule {} 36 | -------------------------------------------------------------------------------- /projects/demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/projects/demo/src/assets/.gitkeep -------------------------------------------------------------------------------- /projects/demo/src/assets/icons/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/demo/src/assets/icons/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/demo/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true, 3 | initializers: [], 4 | }; 5 | -------------------------------------------------------------------------------- /projects/demo/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | import { APP_INITIALIZER } from '@angular/core'; 2 | import { seedDb, worker } from '@ng101/mock'; 3 | 4 | export const environment = { 5 | production: false, 6 | initializers: [ 7 | { 8 | provide: APP_INITIALIZER, 9 | useFactory: () => { 10 | return async () => { 11 | await seedDb(); 12 | worker.start(); 13 | }; 14 | }, 15 | multi: true, 16 | }, 17 | ], 18 | }; 19 | 20 | /* 21 | * For easier debugging in development mode, you can import the following file 22 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 23 | * 24 | * This import should be commented out in production mode because it will have a negative impact 25 | * on performance if an error is thrown. 26 | */ 27 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 28 | -------------------------------------------------------------------------------- /projects/demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/armanozak/angular-101/7e905d97dc2efc7a36304452f6a2091c276e44c0/projects/demo/src/favicon.ico -------------------------------------------------------------------------------- /projects/demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /projects/demo/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /projects/demo/src/mockServiceWorker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock Service Worker. 3 | * @see https://github.com/mswjs/msw 4 | * - Please do NOT modify this file. 5 | * - Please do NOT serve this file on production. 6 | */ 7 | /* eslint-disable */ 8 | /* tslint:disable */ 9 | 10 | const INTEGRITY_CHECKSUM = '82ef9b96d8393b6da34527d1d6e19187' 11 | const bypassHeaderName = 'x-msw-bypass' 12 | const activeClientIds = new Set() 13 | 14 | self.addEventListener('install', function () { 15 | return self.skipWaiting() 16 | }) 17 | 18 | self.addEventListener('activate', async function (event) { 19 | return self.clients.claim() 20 | }) 21 | 22 | self.addEventListener('message', async function (event) { 23 | const clientId = event.source.id 24 | 25 | if (!clientId || !self.clients) { 26 | return 27 | } 28 | 29 | const client = await self.clients.get(clientId) 30 | 31 | if (!client) { 32 | return 33 | } 34 | 35 | const allClients = await self.clients.matchAll() 36 | 37 | switch (event.data) { 38 | case 'KEEPALIVE_REQUEST': { 39 | sendToClient(client, { 40 | type: 'KEEPALIVE_RESPONSE', 41 | }) 42 | break 43 | } 44 | 45 | case 'INTEGRITY_CHECK_REQUEST': { 46 | sendToClient(client, { 47 | type: 'INTEGRITY_CHECK_RESPONSE', 48 | payload: INTEGRITY_CHECKSUM, 49 | }) 50 | break 51 | } 52 | 53 | case 'MOCK_ACTIVATE': { 54 | activeClientIds.add(clientId) 55 | 56 | sendToClient(client, { 57 | type: 'MOCKING_ENABLED', 58 | payload: true, 59 | }) 60 | break 61 | } 62 | 63 | case 'MOCK_DEACTIVATE': { 64 | activeClientIds.delete(clientId) 65 | break 66 | } 67 | 68 | case 'CLIENT_CLOSED': { 69 | activeClientIds.delete(clientId) 70 | 71 | const remainingClients = allClients.filter((client) => { 72 | return client.id !== clientId 73 | }) 74 | 75 | // Unregister itself when there are no more clients 76 | if (remainingClients.length === 0) { 77 | self.registration.unregister() 78 | } 79 | 80 | break 81 | } 82 | } 83 | }) 84 | 85 | // Resolve the "master" client for the given event. 86 | // Client that issues a request doesn't necessarily equal the client 87 | // that registered the worker. It's with the latter the worker should 88 | // communicate with during the response resolving phase. 89 | async function resolveMasterClient(event) { 90 | const client = await self.clients.get(event.clientId) 91 | 92 | if (client.frameType === 'top-level') { 93 | return client 94 | } 95 | 96 | const allClients = await self.clients.matchAll() 97 | 98 | return allClients 99 | .filter((client) => { 100 | // Get only those clients that are currently visible. 101 | return client.visibilityState === 'visible' 102 | }) 103 | .find((client) => { 104 | // Find the client ID that's recorded in the 105 | // set of clients that have registered the worker. 106 | return activeClientIds.has(client.id) 107 | }) 108 | } 109 | 110 | async function handleRequest(event, requestId) { 111 | const client = await resolveMasterClient(event) 112 | const response = await getResponse(event, client, requestId) 113 | 114 | // Send back the response clone for the "response:*" life-cycle events. 115 | // Ensure MSW is active and ready to handle the message, otherwise 116 | // this message will pend indefinitely. 117 | if (client && activeClientIds.has(client.id)) { 118 | ;(async function () { 119 | const clonedResponse = response.clone() 120 | sendToClient(client, { 121 | type: 'RESPONSE', 122 | payload: { 123 | requestId, 124 | type: clonedResponse.type, 125 | ok: clonedResponse.ok, 126 | status: clonedResponse.status, 127 | statusText: clonedResponse.statusText, 128 | body: 129 | clonedResponse.body === null ? null : await clonedResponse.text(), 130 | headers: serializeHeaders(clonedResponse.headers), 131 | redirected: clonedResponse.redirected, 132 | }, 133 | }) 134 | })() 135 | } 136 | 137 | return response 138 | } 139 | 140 | async function getResponse(event, client, requestId) { 141 | const { request } = event 142 | const requestClone = request.clone() 143 | const getOriginalResponse = () => fetch(requestClone) 144 | 145 | // Bypass mocking when the request client is not active. 146 | if (!client) { 147 | return getOriginalResponse() 148 | } 149 | 150 | // Bypass initial page load requests (i.e. static assets). 151 | // The absence of the immediate/parent client in the map of the active clients 152 | // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet 153 | // and is not ready to handle requests. 154 | if (!activeClientIds.has(client.id)) { 155 | return await getOriginalResponse() 156 | } 157 | 158 | // Bypass requests with the explicit bypass header 159 | if (requestClone.headers.get(bypassHeaderName) === 'true') { 160 | const cleanRequestHeaders = serializeHeaders(requestClone.headers) 161 | 162 | // Remove the bypass header to comply with the CORS preflight check. 163 | delete cleanRequestHeaders[bypassHeaderName] 164 | 165 | const originalRequest = new Request(requestClone, { 166 | headers: new Headers(cleanRequestHeaders), 167 | }) 168 | 169 | return fetch(originalRequest) 170 | } 171 | 172 | // Send the request to the client-side MSW. 173 | const reqHeaders = serializeHeaders(request.headers) 174 | const body = await request.text() 175 | 176 | const clientMessage = await sendToClient(client, { 177 | type: 'REQUEST', 178 | payload: { 179 | id: requestId, 180 | url: request.url, 181 | method: request.method, 182 | headers: reqHeaders, 183 | cache: request.cache, 184 | mode: request.mode, 185 | credentials: request.credentials, 186 | destination: request.destination, 187 | integrity: request.integrity, 188 | redirect: request.redirect, 189 | referrer: request.referrer, 190 | referrerPolicy: request.referrerPolicy, 191 | body, 192 | bodyUsed: request.bodyUsed, 193 | keepalive: request.keepalive, 194 | }, 195 | }) 196 | 197 | switch (clientMessage.type) { 198 | case 'MOCK_SUCCESS': { 199 | return delayPromise( 200 | () => respondWithMock(clientMessage), 201 | clientMessage.payload.delay, 202 | ) 203 | } 204 | 205 | case 'MOCK_NOT_FOUND': { 206 | return getOriginalResponse() 207 | } 208 | 209 | case 'NETWORK_ERROR': { 210 | const { name, message } = clientMessage.payload 211 | const networkError = new Error(message) 212 | networkError.name = name 213 | 214 | // Rejecting a request Promise emulates a network error. 215 | throw networkError 216 | } 217 | 218 | case 'INTERNAL_ERROR': { 219 | const parsedBody = JSON.parse(clientMessage.payload.body) 220 | 221 | console.error( 222 | `\ 223 | [MSW] Request handler function for "%s %s" has thrown the following exception: 224 | 225 | ${parsedBody.errorType}: ${parsedBody.message} 226 | (see more detailed error stack trace in the mocked response body) 227 | 228 | This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error. 229 | If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\ 230 | `, 231 | request.method, 232 | request.url, 233 | ) 234 | 235 | return respondWithMock(clientMessage) 236 | } 237 | } 238 | 239 | return getOriginalResponse() 240 | } 241 | 242 | self.addEventListener('fetch', function (event) { 243 | const { request } = event 244 | 245 | // Bypass navigation requests. 246 | if (request.mode === 'navigate') { 247 | return 248 | } 249 | 250 | // Opening the DevTools triggers the "only-if-cached" request 251 | // that cannot be handled by the worker. Bypass such requests. 252 | if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') { 253 | return 254 | } 255 | 256 | // Bypass all requests when there are no active clients. 257 | // Prevents the self-unregistered worked from handling requests 258 | // after it's been deleted (still remains active until the next reload). 259 | if (activeClientIds.size === 0) { 260 | return 261 | } 262 | 263 | const requestId = uuidv4() 264 | 265 | return event.respondWith( 266 | handleRequest(event, requestId).catch((error) => { 267 | console.error( 268 | '[MSW] Failed to mock a "%s" request to "%s": %s', 269 | request.method, 270 | request.url, 271 | error, 272 | ) 273 | }), 274 | ) 275 | }) 276 | 277 | function serializeHeaders(headers) { 278 | const reqHeaders = {} 279 | headers.forEach((value, name) => { 280 | reqHeaders[name] = reqHeaders[name] 281 | ? [].concat(reqHeaders[name]).concat(value) 282 | : value 283 | }) 284 | return reqHeaders 285 | } 286 | 287 | function sendToClient(client, message) { 288 | return new Promise((resolve, reject) => { 289 | const channel = new MessageChannel() 290 | 291 | channel.port1.onmessage = (event) => { 292 | if (event.data && event.data.error) { 293 | return reject(event.data.error) 294 | } 295 | 296 | resolve(event.data) 297 | } 298 | 299 | client.postMessage(JSON.stringify(message), [channel.port2]) 300 | }) 301 | } 302 | 303 | function delayPromise(cb, duration) { 304 | return new Promise((resolve) => { 305 | setTimeout(() => resolve(cb()), duration) 306 | }) 307 | } 308 | 309 | function respondWithMock(clientMessage) { 310 | return new Response(clientMessage.payload.body, { 311 | ...clientMessage.payload, 312 | headers: clientMessage.payload.headers, 313 | }) 314 | } 315 | 316 | function uuidv4() { 317 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 318 | const r = (Math.random() * 16) | 0 319 | const v = c == 'x' ? r : (r & 0x3) | 0x8 320 | return v.toString(16) 321 | }) 322 | } 323 | -------------------------------------------------------------------------------- /projects/demo/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * IE11 requires the following for NgClass support on SVG elements 23 | */ 24 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 25 | 26 | /** 27 | * Web Animations `@angular/platform-browser/animations` 28 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 29 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 30 | */ 31 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 32 | 33 | /** 34 | * By default, zone.js will patch all possible macroTask and DomEvents 35 | * user can disable parts of macroTask/DomEvents patch by setting following flags 36 | * because those flags need to be set before `zone.js` being loaded, and webpack 37 | * will put import in the top of bundle, so user need to create a separate file 38 | * in this directory (for example: zone-flags.ts), and put the following flags 39 | * into that file, and then add the following code before importing zone.js. 40 | * import './zone-flags'; 41 | * 42 | * The flags allowed in zone-flags.ts are listed here. 43 | * 44 | * The following flags will work for all browsers. 45 | * 46 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 47 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 48 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 49 | * 50 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 51 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 52 | * 53 | * (window as any).__Zone_enable_cross_context_check = true; 54 | * 55 | */ 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by default for Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | /*************************************************************************************************** 64 | * APPLICATION IMPORTS 65 | */ 66 | -------------------------------------------------------------------------------- /projects/demo/src/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | @import "~@angular/material/theming"; 2 | 3 | $app-primary: mat-palette($mat-blue-grey, 900); 4 | $app-accent: mat-palette($mat-yellow, 700); 5 | $app-warn: mat-palette($mat-red, 600); 6 | $app-theme: mat-light-theme( 7 | ( 8 | color: ( 9 | primary: $app-primary, 10 | accent: $app-accent, 11 | warn: $app-warn, 12 | ), 13 | ) 14 | ); 15 | -------------------------------------------------------------------------------- /projects/demo/src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "./sass/variables"; 2 | 3 | @include mat-core(); 4 | @include angular-material-theme($app-theme); 5 | 6 | html, 7 | body { 8 | height: 100%; 9 | } 10 | body { 11 | margin: 0; 12 | font-family: Roboto, "Helvetica Neue", sans-serif; 13 | } 14 | -------------------------------------------------------------------------------- /projects/demo/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /projects/demo/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 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /projects/demo/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 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /projects/demo/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/mock/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | jasmine: { 17 | // you can add configuration options for Jasmine here 18 | // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html 19 | // for example, you can disable the random execution with `random: false` 20 | // or set a specific seed with `seed: 4321` 21 | }, 22 | clearContext: false // leave Jasmine Spec Runner output visible in browser 23 | }, 24 | jasmineHtmlReporter: { 25 | suppressAll: true // removes the duplicated traces 26 | }, 27 | coverageReporter: { 28 | dir: require('path').join(__dirname, '../../coverage/mock'), 29 | subdir: '.', 30 | reporters: [ 31 | { type: 'html' }, 32 | { type: 'text-summary' } 33 | ] 34 | }, 35 | reporters: ['progress', 'kjhtml'], 36 | port: 9876, 37 | colors: true, 38 | logLevel: config.LOG_INFO, 39 | autoWatch: true, 40 | browsers: ['ChromeHeadless'], 41 | singleRun: false, 42 | restartOnFileChange: true 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /projects/mock/src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Public API Surface of mock 3 | */ 4 | 5 | export * from './lib/browser'; 6 | export { seedDb } from './lib/db'; 7 | export * from './lib/models'; 8 | -------------------------------------------------------------------------------- /projects/mock/src/lib/browser.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from 'msw'; 2 | import { handlers } from './handlers'; 3 | 4 | export const worker = setupWorker(...handlers); 5 | -------------------------------------------------------------------------------- /projects/mock/src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import type * as pouch from 'pouchdb'; 2 | import { List, Rec, Todo, TodoCreate, TodoUpdate, User } from './models'; 3 | declare var PouchDB: typeof pouch; 4 | 5 | const dbTodos = new PouchDB('todos'); 6 | const dbUsers = new PouchDB('users'); 7 | 8 | export async function seedDb() { 9 | const skip = Promise.resolve(null); 10 | 11 | const initialTodos = insertIds([ 12 | { title: 'Meet Obi-Wan Kenobi', done: true }, 13 | { title: 'Learn the Force', done: false }, 14 | { title: 'Get a lightsaber', done: true }, 15 | ]); 16 | const dbInit1 = dbTodos 17 | .info() 18 | .then((info) => (info.doc_count ? skip : dbTodos.bulkDocs(initialTodos))); 19 | 20 | const initialUsers = insertIds([ 21 | { 22 | firstName: 'Luke', 23 | lastName: 'Skywalker', 24 | username: 'luke', 25 | }, 26 | ]); 27 | const dbInit2 = dbUsers 28 | .info() 29 | .then((info) => (info.doc_count ? skip : dbUsers.bulkDocs(initialUsers))) 30 | .then(() => dbUsers.createIndex({ index: { fields: ['username'] } })); 31 | 32 | return Promise.all([dbInit1, dbInit2]); 33 | } 34 | 35 | export async function deleteTodo(id: string) { 36 | return dbTodos 37 | .get(id) 38 | .then((doc) => dbTodos.remove(doc)) 39 | .then(mapToId); 40 | } 41 | 42 | export async function getTodos({ 43 | limit, 44 | skip, 45 | }: PouchDB.Core.AllDocsOptions): Promise> { 46 | // there are better pagination strategies, but this is a mock, no need to worry about perf 47 | return dbTodos 48 | .allDocs({ include_docs: true, limit, skip }) 49 | .then(mapToRecordList); 50 | } 51 | 52 | export async function getTodo(id: string | null): Promise | null> { 53 | return id ? dbTodos.get(id).then(mapToRecord) : null; 54 | } 55 | 56 | export async function putTodo( 57 | id: string, 58 | todo: TodoUpdate 59 | ): Promise { 60 | return dbTodos 61 | .get(id) 62 | .then((doc) => 63 | dbTodos.put({ 64 | ...doc, 65 | ...todo, 66 | }) 67 | ) 68 | .then(mapToId); 69 | } 70 | 71 | export async function postTodo(input: TodoCreate): Promise | null> { 72 | const todo = { ...input, _id: createId(), done: false }; 73 | return dbTodos.put(todo).then(mapToId).then(getTodo); 74 | } 75 | 76 | export async function getUserByUsername( 77 | username: string 78 | ): Promise | null> { 79 | return username 80 | ? dbUsers 81 | .find({ 82 | selector: { username }, 83 | fields: ['_id', 'firstName', 'lastName'], 84 | }) 85 | .then(({ docs: [user] }) => user) 86 | .then(mapToRecord) 87 | : null; 88 | } 89 | 90 | function mapToRecordList( 91 | resp: PouchDB.Core.AllDocsResponse 92 | ): List { 93 | return { 94 | rows: resp.rows.map((row) => mapToRecord(row.doc!)!), 95 | totalCount: resp.total_rows, 96 | }; 97 | } 98 | 99 | function mapToRecord( 100 | doc: PouchDB.Core.ExistingDocument 101 | ): Rec | null { 102 | if (!doc) return null; 103 | const { _id, _rev, ..._doc } = doc; 104 | return { id: _id, ...((_doc as any) as T) }; 105 | } 106 | 107 | function mapToId(resp: PouchDB.Core.Response) { 108 | return resp.ok ? resp.id : null; 109 | } 110 | 111 | function insertIds(records: T[]): PouchDB.Core.NewDocument[] { 112 | const ids = Array(records.length).fill(0).map(createId); 113 | ids.sort(); 114 | return ids.map((_id, i) => ({ _id, ...records[i] })); 115 | } 116 | 117 | function createId() { 118 | return new Date().toJSON() + Math.random(); 119 | } 120 | -------------------------------------------------------------------------------- /projects/mock/src/lib/handlers.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AsyncResponseResolverReturnType, 3 | MockedRequest, 4 | ResponseComposition, 5 | RestContext, 6 | RestRequest, 7 | } from 'msw'; 8 | import { rest } from 'msw'; 9 | import { 10 | deleteTodo, 11 | getTodo, 12 | getTodos, 13 | getUserByUsername, 14 | postTodo, 15 | putTodo, 16 | } from './db'; 17 | import { validateTodoCreate, validateTodoUpdate } from './validation'; 18 | 19 | export const handlers = [ 20 | rest.get( 21 | '/api/todos', 22 | handleErrors(async (req, res, ctx) => { 23 | const todos = await getTodos(getLimitAndSkip(req)); 24 | return res(ctx.delay(1000), ctx.json(todos)); 25 | }) 26 | ), 27 | 28 | rest.delete( 29 | '/api/todos/:id', 30 | handleErrors(async (req, res, ctx) => { 31 | await deleteTodo(req.params.id); 32 | return res(ctx.status(204)); 33 | }) 34 | ), 35 | 36 | rest.get( 37 | '/api/todos/:id', 38 | handleErrors(async (req, res, ctx) => { 39 | const todo = await getTodo(req.params.id); 40 | return res(ctx.json(todo)); 41 | }) 42 | ), 43 | 44 | rest.post( 45 | '/api/todos', 46 | handleErrors(async (req, res, ctx) => { 47 | validateTodoCreate(req.body); 48 | if (req.body.title.toLowerCase() === 'error') 49 | throw { status: 418, message: `i'm a teapot` }; 50 | const todo = await postTodo(req.body); 51 | return res(ctx.json(todo)); 52 | }) 53 | ), 54 | 55 | rest.put( 56 | '/api/todos/:id', 57 | handleErrors(async (req, res, ctx) => { 58 | validateTodoUpdate(req.body); 59 | await putTodo(req.params.id, req.body); 60 | return res(ctx.status(204)); 61 | }) 62 | ), 63 | 64 | rest.post( 65 | '/api/login', 66 | handleErrors(async (req, res, ctx) => { 67 | const { username } = req.body as any; 68 | const user = await getUserByUsername(username); 69 | return res(ctx.json(user)); 70 | }) 71 | ), 72 | ]; 73 | 74 | function handleErrors>(resolver: T): T { 75 | return (async (req: any, res: any, ctx: any) => { 76 | try { 77 | return await resolver(req, res, ctx); 78 | } catch (err) { 79 | return res(ctx.status(err.status), ctx.text(err.message)); 80 | } 81 | }) as any; 82 | } 83 | 84 | function getLimitAndSkip(req: MockedRequest) { 85 | const limit = Math.min(1000, Number(req.url.searchParams.get('limit')) || 10); 86 | const skip = ((Number(req.url.searchParams.get('page')) || 1) - 1) * limit; 87 | return { limit, skip }; 88 | } 89 | 90 | type Resolver = ( 91 | req: RestRequest, 92 | res: ResponseComposition, 93 | context: RestContext 94 | ) => AsyncResponseResolverReturnType; 95 | -------------------------------------------------------------------------------- /projects/mock/src/lib/models.ts: -------------------------------------------------------------------------------- 1 | export interface List { 2 | rows: Rec[]; 3 | totalCount: number; 4 | } 5 | 6 | export interface ListRequest { 7 | page: number; 8 | limit: number; 9 | } 10 | 11 | export type Rec = T & { 12 | id: string; 13 | }; 14 | 15 | export type Todo = { 16 | title: string; 17 | done: boolean; 18 | }; 19 | 20 | export interface TodoCreate { 21 | title: string; 22 | } 23 | 24 | export interface TodoUpdate { 25 | title: string; 26 | done: boolean; 27 | } 28 | 29 | export interface User { 30 | firstName: string; 31 | lastName: string; 32 | username: string; 33 | } 34 | -------------------------------------------------------------------------------- /projects/mock/src/lib/validation.ts: -------------------------------------------------------------------------------- 1 | import { TodoCreate, TodoUpdate } from './models'; 2 | 3 | export function validateTodoCreate(todo: any): asserts todo is TodoCreate { 4 | if (typeof todo.title === 'string') return; 5 | throw { status: 400, message: 'invalid todo create' }; 6 | } 7 | 8 | export function validateTodoUpdate(todo: any): asserts todo is TodoUpdate { 9 | if (typeof todo.title === 'string' && typeof todo.done === 'boolean') return; 10 | throw { status: 400, message: 'invalid todo update' }; 11 | } 12 | -------------------------------------------------------------------------------- /projects/mock/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone'; 4 | import 'zone.js/dist/zone-testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: { 12 | context(path: string, deep?: boolean, filter?: RegExp): { 13 | keys(): string[]; 14 | (id: string): T; 15 | }; 16 | }; 17 | 18 | // First, initialize the Angular testing environment. 19 | getTestBed().initTestEnvironment( 20 | BrowserDynamicTestingModule, 21 | platformBrowserDynamicTesting() 22 | ); 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().map(context); 27 | -------------------------------------------------------------------------------- /projects/mock/tsconfig.lib.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/lib", 6 | "target": "es2015", 7 | "declaration": true, 8 | "declarationMap": true, 9 | "inlineSources": true, 10 | "types": [], 11 | "lib": [ 12 | "dom", 13 | "es2018" 14 | ] 15 | }, 16 | "angularCompilerOptions": { 17 | "skipTemplateCodegen": true, 18 | "strictMetadataEmit": true, 19 | "enableResourceInlining": true 20 | }, 21 | "exclude": [ 22 | "src/test.ts", 23 | "**/*.spec.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /projects/mock/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.lib.json", 4 | "compilerOptions": { 5 | "declarationMap": false 6 | }, 7 | "angularCompilerOptions": { 8 | "enableIvy": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /projects/mock/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 | "files": [ 11 | "src/test.ts" 12 | ], 13 | "include": [ 14 | "**/*.spec.ts", 15 | "**/*.d.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /projects/mock/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "lib", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "lib", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "sourceMap": true, 12 | "paths": { 13 | "@ng101/mock": ["projects/mock/src/index.ts"] 14 | }, 15 | "declaration": false, 16 | "downlevelIteration": true, 17 | "experimentalDecorators": true, 18 | "moduleResolution": "node", 19 | "importHelpers": true, 20 | "target": "es2015", 21 | "module": "es2020", 22 | "lib": ["es2018", "dom"] 23 | }, 24 | "angularCompilerOptions": { 25 | "enableI18nLegacyMessageIdFormat": false, 26 | "strictInjectionParameters": true, 27 | "strictInputAccessModifiers": true, 28 | "strictTemplates": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": ["codelyzer"], 4 | "rules": { 5 | "align": { 6 | "options": ["parameters", "statements"] 7 | }, 8 | "array-type": false, 9 | "arrow-return-shorthand": true, 10 | "curly": false, 11 | "deprecation": { 12 | "severity": "warning" 13 | }, 14 | "eofline": true, 15 | "import-blacklist": [true, "rxjs/Rx"], 16 | "import-spacing": true, 17 | "indent": { 18 | "options": ["spaces"] 19 | }, 20 | "max-classes-per-file": false, 21 | "max-line-length": [true, 140], 22 | "member-ordering": [ 23 | true, 24 | { 25 | "order": [ 26 | "static-field", 27 | "instance-field", 28 | "static-method", 29 | "instance-method" 30 | ] 31 | } 32 | ], 33 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 34 | "no-empty": false, 35 | "no-inferrable-types": [true, "ignore-params"], 36 | "no-non-null-assertion": false, 37 | "no-redundant-jsdoc": true, 38 | "no-switch-case-fall-through": true, 39 | "no-var-requires": false, 40 | "object-literal-key-quotes": [true, "as-needed"], 41 | "quotemark": [true, "single"], 42 | "semicolon": { 43 | "options": ["always"] 44 | }, 45 | "space-before-function-paren": { 46 | "options": { 47 | "anonymous": "never", 48 | "asyncArrow": "always", 49 | "constructor": "never", 50 | "method": "never", 51 | "named": "never" 52 | } 53 | }, 54 | "typedef": false, 55 | "typedef-whitespace": { 56 | "options": [ 57 | { 58 | "call-signature": "nospace", 59 | "index-signature": "nospace", 60 | "parameter": "nospace", 61 | "property-declaration": "nospace", 62 | "variable-declaration": "nospace" 63 | }, 64 | { 65 | "call-signature": "onespace", 66 | "index-signature": "onespace", 67 | "parameter": "onespace", 68 | "property-declaration": "onespace", 69 | "variable-declaration": "onespace" 70 | } 71 | ] 72 | }, 73 | "variable-name": { 74 | "options": [ 75 | "ban-keywords", 76 | "check-format", 77 | "allow-pascal-case", 78 | "allow-leading-underscore" 79 | ] 80 | }, 81 | "whitespace": { 82 | "options": [ 83 | "check-branch", 84 | "check-decl", 85 | "check-operator", 86 | "check-separator", 87 | "check-type", 88 | "check-typecast" 89 | ] 90 | }, 91 | "component-class-suffix": true, 92 | "contextual-lifecycle": true, 93 | "directive-class-suffix": true, 94 | "no-conflicting-lifecycle": true, 95 | "no-host-metadata-property": true, 96 | "no-input-rename": true, 97 | "no-inputs-metadata-property": true, 98 | "no-output-native": true, 99 | "no-output-on-prefix": true, 100 | "no-output-rename": true, 101 | "no-outputs-metadata-property": true, 102 | "template-banana-in-box": true, 103 | "template-no-negated-async": true, 104 | "use-lifecycle-interface": true, 105 | "use-pipe-transform-interface": true 106 | } 107 | } 108 | --------------------------------------------------------------------------------
{{ form.value | json }}
"{{ title }}" will be deleted forever.
Do you want to continue deleting?