Differenzansicht 18-modern-angular
im Vergleich zu 17-standalone

← Zurück zur Ãœbersicht | Demo | Quelltext auf GitHub
angular.json CHANGED
@@ -5,17 +5,7 @@
5
  "projects": {
6
  "book-monkey": {
7
  "projectType": "application",
8
- "schematics": {
9
- "@schematics/angular:component": {
10
- "standalone": false
11
- },
12
- "@schematics/angular:directive": {
13
- "standalone": false
14
- },
15
- "@schematics/angular:pipe": {
16
- "standalone": false
17
- }
18
- },
19
  "root": "",
20
  "sourceRoot": "src",
21
  "prefix": "bm",
@@ -39,7 +29,15 @@
39
  "styles": [
40
  "src/styles.css"
41
  ],
42
- "scripts": []
 
 
 
 
 
 
 
 
43
  },
44
  "configurations": {
45
  "production": {
5
  "projects": {
6
  "book-monkey": {
7
  "projectType": "application",
8
+ "schematics": {},
 
 
 
 
 
 
 
 
 
 
9
  "root": "",
10
  "sourceRoot": "src",
11
  "prefix": "bm",
29
  "styles": [
30
  "src/styles.css"
31
  ],
32
+ "scripts": [],
33
+ "server": "src/main.server.ts",
34
+ "prerender": {
35
+ "discoverRoutes": true,
36
+ "routesFile": "routes.txt"
37
+ },
38
+ "ssr": {
39
+ "entry": "server.ts"
40
+ }
41
  },
42
  "configurations": {
43
  "production": {
package.json CHANGED
@@ -7,7 +7,8 @@
7
  "build": "ng build",
8
  "watch": "ng build --watch --configuration development",
9
  "test": "ng test",
10
- "lint": "ng lint"
 
11
  },
12
  "private": true,
13
  "dependencies": {
@@ -19,9 +20,12 @@
19
  "@angular/forms": "^18.2.0",
20
  "@angular/platform-browser": "^18.2.0",
21
  "@angular/platform-browser-dynamic": "^18.2.0",
 
22
  "@angular/router": "^18.2.0",
 
23
  "angular-date-value-accessor": "^3.0.0",
24
  "book-monkey5-styles": "^1.0.4",
 
25
  "rxjs": "~7.8.0",
26
  "tslib": "^2.3.0",
27
  "zone.js": "~0.14.10"
@@ -30,7 +34,9 @@
30
  "@angular-devkit/build-angular": "^18.2.1",
31
  "@angular/cli": "^18.2.1",
32
  "@angular/compiler-cli": "^18.2.0",
 
33
  "@types/jasmine": "~5.1.0",
 
34
  "angular-eslint": "18.3.0",
35
  "eslint": "^9.9.0",
36
  "jasmine-core": "~5.2.0",
@@ -42,4 +48,4 @@
42
  "typescript": "~5.5.2",
43
  "typescript-eslint": "8.1.0"
44
  }
45
- }
7
  "build": "ng build",
8
  "watch": "ng build --watch --configuration development",
9
  "test": "ng test",
10
+ "lint": "ng lint",
11
+ "serve:ssr:book-monkey": "node dist/book-monkey/server/server.mjs"
12
  },
13
  "private": true,
14
  "dependencies": {
20
  "@angular/forms": "^18.2.0",
21
  "@angular/platform-browser": "^18.2.0",
22
  "@angular/platform-browser-dynamic": "^18.2.0",
23
+ "@angular/platform-server": "^18.2.0",
24
  "@angular/router": "^18.2.0",
25
+ "@angular/ssr": "^18.2.1",
26
  "angular-date-value-accessor": "^3.0.0",
27
  "book-monkey5-styles": "^1.0.4",
28
+ "express": "^4.18.2",
29
  "rxjs": "~7.8.0",
30
  "tslib": "^2.3.0",
31
  "zone.js": "~0.14.10"
34
  "@angular-devkit/build-angular": "^18.2.1",
35
  "@angular/cli": "^18.2.1",
36
  "@angular/compiler-cli": "^18.2.0",
37
+ "@types/express": "^4.17.17",
38
  "@types/jasmine": "~5.1.0",
39
+ "@types/node": "^18.18.0",
40
  "angular-eslint": "18.3.0",
41
  "eslint": "^9.9.0",
42
  "jasmine-core": "~5.2.0",
48
  "typescript": "~5.5.2",
49
  "typescript-eslint": "8.1.0"
50
  }
51
+ }
routes.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
1
+ /books/9783864909467
2
+ /books/9783864903571
3
+ /books/9783864906466
4
+ /books/9783864907791
5
+ /books/9783864904523
6
+ /books/9783960091417
7
+ /books/9783864907845
8
+ /books/9783864905520
9
+ /books/9783864902079
10
+ /books/9783864909009
server.ts ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { APP_BASE_HREF } from '@angular/common';
2
+ import { CommonEngine } from '@angular/ssr';
3
+ import express from 'express';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join, resolve } from 'node:path';
6
+ import AppServerModule from './src/main.server';
7
+
8
+ // The Express app is exported so that it can be used by serverless Functions.
9
+ export function app(): express.Express {
10
+ const server = express();
11
+ const serverDistFolder = dirname(fileURLToPath(import.meta.url));
12
+ const browserDistFolder = resolve(serverDistFolder, '../browser');
13
+ const indexHtml = join(serverDistFolder, 'index.server.html');
14
+
15
+ const commonEngine = new CommonEngine();
16
+
17
+ server.set('view engine', 'html');
18
+ server.set('views', browserDistFolder);
19
+
20
+ // Example Express Rest API endpoints
21
+ // server.get('/api/**', (req, res) => { });
22
+ // Serve static files from /browser
23
+ server.get('**', express.static(browserDistFolder, {
24
+ maxAge: '1y',
25
+ index: 'index.html',
26
+ }));
27
+
28
+ // All regular routes use the Angular engine
29
+ server.get('**', (req, res, next) => {
30
+ const { protocol, originalUrl, baseUrl, headers } = req;
31
+
32
+ commonEngine
33
+ .render({
34
+ bootstrap: AppServerModule,
35
+ documentFilePath: indexHtml,
36
+ url: `${protocol}://${headers.host}${originalUrl}`,
37
+ publicPath: browserDistFolder,
38
+ providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
39
+ })
40
+ .then((html) => res.send(html))
41
+ .catch((err) => next(err));
42
+ });
43
+
44
+ return server;
45
+ }
46
+
47
+ function run(): void {
48
+ const port = process.env['PORT'] || 4000;
49
+
50
+ // Start up the Node server
51
+ const server = app();
52
+ server.listen(port, () => {
53
+ console.log(`Node Express server listening on http://localhost:${port}`);
54
+ });
55
+ }
56
+
57
+ run();
src/app/admin/admin.module.ts DELETED
@@ -1,26 +0,0 @@
1
- import { NgModule } from '@angular/core';
2
- import { CommonModule } from '@angular/common';
3
- import { ReactiveFormsModule } from '@angular/forms';
4
- import { LocalIsoDateValueAccessor } from 'angular-date-value-accessor';
5
-
6
- import { AdminRoutingModule } from './admin-routing.module';
7
- import { BookFormComponent } from './book-form/book-form.component';
8
- import { BookCreateComponent } from './book-create/book-create.component';
9
- import { BookEditComponent } from './book-edit/book-edit.component';
10
- import { FormErrorsComponent } from './form-errors/form-errors.component';
11
-
12
- @NgModule({
13
- declarations: [
14
- BookFormComponent,
15
- BookCreateComponent,
16
- BookEditComponent,
17
- FormErrorsComponent
18
- ],
19
- imports: [
20
- CommonModule,
21
- AdminRoutingModule,
22
- ReactiveFormsModule,
23
- LocalIsoDateValueAccessor
24
- ],
25
- })
26
- export class AdminModule { }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/admin/{admin-routing.module.ts → admin.routes.ts} RENAMED
@@ -1,9 +1,8 @@
1
- import { NgModule } from '@angular/core';
2
- import { RouterModule, Routes } from '@angular/router';
3
  import { BookCreateComponent } from './book-create/book-create.component';
4
  import { BookEditComponent } from './book-edit/book-edit.component';
5
 
6
- const routes: Routes = [
7
  {
8
  path: '',
9
  redirectTo: 'create',
@@ -18,9 +17,3 @@ const routes: Routes = [
18
  component: BookEditComponent,
19
  }
20
  ];
21
-
22
- @NgModule({
23
- imports: [RouterModule.forChild(routes)],
24
- exports: [RouterModule]
25
- })
26
- export class AdminRoutingModule { }
1
+ import { Routes } from '@angular/router';
 
2
  import { BookCreateComponent } from './book-create/book-create.component';
3
  import { BookEditComponent } from './book-edit/book-edit.component';
4
 
5
+ export const ADMIN_ROUTES: Routes = [
6
  {
7
  path: '',
8
  redirectTo: 'create',
17
  component: BookEditComponent,
18
  }
19
  ];
 
 
 
 
 
 
src/app/admin/book-create/book-create.component.ts CHANGED
@@ -1,20 +1,20 @@
1
- import { Component } from '@angular/core';
2
  import { Router } from '@angular/router';
3
 
4
  import { BookStoreService } from '../../shared/book-store.service';
5
  import { Book } from '../../shared/book';
 
6
 
7
  @Component({
8
  selector: 'bm-book-create',
9
  templateUrl: './book-create.component.html',
10
- styleUrls: ['./book-create.component.css']
 
 
11
  })
12
  export class BookCreateComponent {
13
-
14
- constructor(
15
- private service: BookStoreService,
16
- private router: Router
17
- ) { }
18
 
19
  create(book: Book) {
20
  this.service.create(book).subscribe(createdBook => {
1
+ import { Component, inject } from '@angular/core';
2
  import { Router } from '@angular/router';
3
 
4
  import { BookStoreService } from '../../shared/book-store.service';
5
  import { Book } from '../../shared/book';
6
+ import { BookFormComponent } from '../book-form/book-form.component';
7
 
8
  @Component({
9
  selector: 'bm-book-create',
10
  templateUrl: './book-create.component.html',
11
+ styleUrl: './book-create.component.css',
12
+ imports: [BookFormComponent],
13
+ standalone: true
14
  })
15
  export class BookCreateComponent {
16
+ private service = inject(BookStoreService);
17
+ private router = inject(Router);
 
 
 
18
 
19
  create(book: Book) {
20
  this.service.create(book).subscribe(createdBook => {
src/app/admin/book-edit/book-edit.component.html CHANGED
@@ -1,6 +1,7 @@
1
  <h1>Edit Book</h1>
2
 
 
3
  <bm-book-form
4
- *ngIf="book$ | async as book"
5
  [book]="book"
6
  (submitBook)="update($event)"></bm-book-form>
 
1
  <h1>Edit Book</h1>
2
 
3
+ @if (book$ | async; as book) {
4
  <bm-book-form
 
5
  [book]="book"
6
  (submitBook)="update($event)"></bm-book-form>
7
+ }
src/app/admin/book-edit/book-edit.component.ts CHANGED
@@ -1,28 +1,27 @@
1
- import { Component } from '@angular/core';
2
- import { ActivatedRoute, Router } from '@angular/router';
3
- import { map, Observable, switchMap } from 'rxjs';
 
 
4
 
5
  import { Book } from '../../shared/book';
6
  import { BookStoreService } from '../../shared/book-store.service';
 
7
 
8
  @Component({
9
  selector: 'bm-book-edit',
10
  templateUrl: './book-edit.component.html',
11
- styleUrls: ['./book-edit.component.css']
 
 
12
  })
13
  export class BookEditComponent {
14
- book$: Observable<Book>;
15
-
16
- constructor(
17
- private service: BookStoreService,
18
- private route: ActivatedRoute,
19
- private router: Router
20
- ) {
21
- this.book$ = this.route.paramMap.pipe(
22
- map(params => params.get('isbn')!),
23
- switchMap(isbn => this.service.getSingle(isbn))
24
- );
25
- }
26
 
27
  update(book: Book) {
28
  this.service.update(book).subscribe(updatedBook => {
1
+ import { AsyncPipe } from '@angular/common';
2
+ import { Component, inject, input } from '@angular/core';
3
+ import { toObservable } from '@angular/core/rxjs-interop';
4
+ import { Router } from '@angular/router';
5
+ import { switchMap } from 'rxjs';
6
 
7
  import { Book } from '../../shared/book';
8
  import { BookStoreService } from '../../shared/book-store.service';
9
+ import { BookFormComponent } from '../book-form/book-form.component';
10
 
11
  @Component({
12
  selector: 'bm-book-edit',
13
  templateUrl: './book-edit.component.html',
14
+ styleUrl: './book-edit.component.css',
15
+ imports: [AsyncPipe, BookFormComponent],
16
+ standalone: true
17
  })
18
  export class BookEditComponent {
19
+ private service = inject(BookStoreService);
20
+ private router = inject(Router);
21
+ isbn = input.required<string>();
22
+ book$ = toObservable(this.isbn).pipe(
23
+ switchMap(isbn => this.service.getSingle(isbn))
24
+ );
 
 
 
 
 
 
25
 
26
  update(book: Book) {
27
  this.service.update(book).subscribe(updatedBook => {
src/app/admin/book-form/book-form.component.html CHANGED
@@ -26,10 +26,11 @@
26
  + Author
27
  </button>
28
  <fieldset formArrayName="authors">
 
29
  <input
30
- *ngFor="let a of authors.controls; index as i"
31
- [attr.aria-label]="'Author ' + i"
32
- [formControlName]="i">
33
  </fieldset>
34
  <bm-form-errors
35
  controlName="authors"
26
  + Author
27
  </button>
28
  <fieldset formArrayName="authors">
29
+ @for (a of authors.controls; track a) {
30
  <input
31
+ [attr.aria-label]="'Author ' + $index"
32
+ [formControlName]="$index">
33
+ }
34
  </fieldset>
35
  <bm-form-errors
36
  controlName="authors"
src/app/admin/book-form/book-form.component.ts CHANGED
@@ -1,18 +1,20 @@
1
- import { Component, Output, EventEmitter, Input, OnChanges, inject } from '@angular/core';
2
- import { FormArray, FormControl, FormGroup, Validators } from '@angular/forms';
3
 
4
  import { Book } from '../../shared/book';
5
- import { AsyncValidatorsService } from '../shared/async-validators.service';
6
- import { atLeastOneValue, isbnFormat } from '../shared/validators';
7
 
8
  @Component({
9
  selector: 'bm-book-form',
10
  templateUrl: './book-form.component.html',
11
- styleUrls: ['./book-form.component.css']
 
 
12
  })
13
- export class BookFormComponent implements OnChanges {
14
- @Input() book?: Book;
15
- @Output() submitBook = new EventEmitter<Book>();
16
 
17
  form = new FormGroup({
18
  title: new FormControl('', {
@@ -22,11 +24,8 @@ export class BookFormComponent implements OnChanges {
22
  subtitle: new FormControl('', { nonNullable: true }),
23
  isbn: new FormControl('', {
24
  nonNullable: true,
25
- validators: [
26
- Validators.required,
27
- isbnFormat
28
- ],
29
- asyncValidators: inject(AsyncValidatorsService).isbnExists()
30
  }),
31
  description: new FormControl('', { nonNullable: true }),
32
  published: new FormControl('', { nonNullable: true }),
@@ -34,13 +33,16 @@ export class BookFormComponent implements OnChanges {
34
  thumbnailUrl: new FormControl('', { nonNullable: true })
35
  });
36
 
37
- ngOnChanges(): void {
38
- if (this.book) {
39
- this.setFormValues(this.book);
40
- this.setEditMode(true);
41
- } else {
42
- this.setEditMode(false);
43
- }
 
 
 
44
  }
45
 
46
  private setFormValues(book: Book) {
1
+ import { Component, effect, input, output } from '@angular/core';
2
+ import { FormArray, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
3
 
4
  import { Book } from '../../shared/book';
5
+ import { FormErrorsComponent } from '../form-errors/form-errors.component';
6
+ import { atLeastOneValue, isbnExists, isbnFormat } from '../shared/validators';
7
 
8
  @Component({
9
  selector: 'bm-book-form',
10
  templateUrl: './book-form.component.html',
11
+ styleUrl: './book-form.component.css',
12
+ imports: [ReactiveFormsModule, FormErrorsComponent],
13
+ standalone: true
14
  })
15
+ export class BookFormComponent {
16
+ book = input<Book>();
17
+ submitBook = output<Book>();
18
 
19
  form = new FormGroup({
20
  title: new FormControl('', {
24
  subtitle: new FormControl('', { nonNullable: true }),
25
  isbn: new FormControl('', {
26
  nonNullable: true,
27
+ validators: [Validators.required, isbnFormat],
28
+ asyncValidators: isbnExists(),
 
 
 
29
  }),
30
  description: new FormControl('', { nonNullable: true }),
31
  published: new FormControl('', { nonNullable: true }),
33
  thumbnailUrl: new FormControl('', { nonNullable: true })
34
  });
35
 
36
+ constructor() {
37
+ effect(() => {
38
+ const book = this.book();
39
+ if (book) {
40
+ this.setFormValues(book);
41
+ this.setEditMode(true);
42
+ } else {
43
+ this.setEditMode(false);
44
+ }
45
+ });
46
  }
47
 
48
  private setFormValues(book: Book) {
src/app/admin/form-errors/form-errors.component.html CHANGED
@@ -1,3 +1,3 @@
1
- <p class="error" *ngFor="let error of errors">
2
- {{ error }}
3
- </p>
1
+ @for (error of errors; track $index) {
2
+ <p class="error">{{ error }}</p>
3
+ }
src/app/admin/form-errors/form-errors.component.ts CHANGED
@@ -1,30 +1,27 @@
1
- import { Component, Input } from '@angular/core';
2
  import { FormGroupDirective } from '@angular/forms';
3
 
4
  @Component({
5
  selector: 'bm-form-errors',
6
  templateUrl: './form-errors.component.html',
7
- styleUrls: ['./form-errors.component.css'],
 
8
  })
9
  export class FormErrorsComponent {
10
- @Input() controlName?: string;
11
- @Input() messages: { [errorCode: string]: string } = {};
12
 
13
- constructor(private form: FormGroupDirective) {}
14
 
15
  get errors(): string[] {
16
- if (!this.controlName) {
17
- return [];
18
- }
19
-
20
- const control = this.form.control.get(this.controlName);
21
 
22
  if (!control || !control.errors || !control.touched) {
23
  return [];
24
  }
25
 
26
  return Object.keys(control.errors).map(errorCode => {
27
- return this.messages[errorCode];
28
  });
29
  }
30
  }
1
+ import { Component, inject, input } from '@angular/core';
2
  import { FormGroupDirective } from '@angular/forms';
3
 
4
  @Component({
5
  selector: 'bm-form-errors',
6
  templateUrl: './form-errors.component.html',
7
+ styleUrl: './form-errors.component.css',
8
+ standalone: true
9
  })
10
  export class FormErrorsComponent {
11
+ controlName = input.required<string>();
12
+ messages = input.required<{ [errorCode: string]: string }>();
13
 
14
+ private form = inject(FormGroupDirective)
15
 
16
  get errors(): string[] {
17
+ const control = this.form.control.get(this.controlName());
 
 
 
 
18
 
19
  if (!control || !control.errors || !control.touched) {
20
  return [];
21
  }
22
 
23
  return Object.keys(control.errors).map(errorCode => {
24
+ return this.messages()[errorCode];
25
  });
26
  }
27
  }
src/app/admin/shared/async-validators.service.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Injectable } from '@angular/core';
2
  import { AsyncValidatorFn } from '@angular/forms';
3
  import { map } from 'rxjs';
4
 
@@ -8,8 +8,7 @@ import { BookStoreService } from '../../shared/book-store.service';
8
  providedIn: 'root'
9
  })
10
  export class AsyncValidatorsService {
11
-
12
- constructor(private service: BookStoreService) { }
13
 
14
  isbnExists(): AsyncValidatorFn {
15
  return (control) => {
1
+ import { Injectable, inject } from '@angular/core';
2
  import { AsyncValidatorFn } from '@angular/forms';
3
  import { map } from 'rxjs';
4
 
8
  providedIn: 'root'
9
  })
10
  export class AsyncValidatorsService {
11
+ private service = inject(BookStoreService);
 
12
 
13
  isbnExists(): AsyncValidatorFn {
14
  return (control) => {
src/app/admin/shared/validators.ts CHANGED
@@ -1,6 +1,10 @@
1
- import { isFormArray, ValidatorFn } from '@angular/forms';
 
 
2
 
3
- export const isbnFormat: ValidatorFn = function(control) {
 
 
4
  if (!control.value || typeof control.value !== 'string') {
5
  return null;
6
  }
@@ -13,9 +17,9 @@ export const isbnFormat: ValidatorFn = function(control) {
13
  } else {
14
  return { isbnformat: true };
15
  }
16
- }
17
 
18
- export const atLeastOneValue: ValidatorFn = function(control) {
19
  if (!isFormArray(control)) {
20
  return null;
21
  }
@@ -25,4 +29,13 @@ export const atLeastOneValue: ValidatorFn = function(control) {
25
  } else {
26
  return { atleastonevalue: true };
27
  }
28
- }
 
 
 
 
 
 
 
 
 
1
+ import { inject } from '@angular/core';
2
+ import { AbstractControl, AsyncValidatorFn, isFormArray, ValidatorFn } from '@angular/forms';
3
+ import { map } from 'rxjs';
4
 
5
+ import { BookStoreService } from '../../shared/book-store.service';
6
+
7
+ export const isbnFormat: ValidatorFn = function (control) {
8
  if (!control.value || typeof control.value !== 'string') {
9
  return null;
10
  }
17
  } else {
18
  return { isbnformat: true };
19
  }
20
+ };
21
 
22
+ export const atLeastOneValue: ValidatorFn = function (control) {
23
  if (!isFormArray(control)) {
24
  return null;
25
  }
29
  } else {
30
  return { atleastonevalue: true };
31
  }
32
+ };
33
+
34
+ export function isbnExists(): AsyncValidatorFn {
35
+ const service = inject(BookStoreService);
36
+ return (control: AbstractControl) => {
37
+ return service
38
+ .check(control.value)
39
+ .pipe(map((exists) => (exists ? { isbnexists: true } : null)));
40
+ };
41
+ };
src/app/app.component.html CHANGED
@@ -3,12 +3,11 @@
3
  <a routerLink="/books" routerLinkActive="active" ariaCurrentWhenActive="page">Books</a>
4
  <a routerLink="/admin" routerLinkActive="active" ariaCurrentWhenActive="page">Administration</a>
5
  <div class="actions">
6
- <button class="green"
7
- (click)="auth.login()"
8
- *ngIf="!auth.isAuthenticated">Login</button>
9
- <button class="red"
10
- (click)="auth.logout()"
11
- *ngIf="auth.isAuthenticated">Logout</button>
12
  </div>
13
  </nav>
14
 
3
  <a routerLink="/books" routerLinkActive="active" ariaCurrentWhenActive="page">Books</a>
4
  <a routerLink="/admin" routerLinkActive="active" ariaCurrentWhenActive="page">Administration</a>
5
  <div class="actions">
6
+ @if (auth.isAuthenticated()) {
7
+ <button class="red" (click)="auth.logout()">Logout</button>
8
+ } @else {
9
+ <button class="green" (click)="auth.login()">Login</button>
10
+ }
 
11
  </div>
12
  </nav>
13
 
src/app/app.component.spec.ts CHANGED
@@ -1,16 +1,10 @@
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: [
9
- RouterTestingModule
10
- ],
11
- declarations: [
12
- AppComponent
13
- ],
14
  }).compileComponents();
15
  });
16
 
@@ -20,20 +14,16 @@ describe('AppComponent', () => {
20
  expect(app).toBeTruthy();
21
  });
22
 
23
- /*
24
- it(`should have as title 'book-monkey'`, () => {
25
  const fixture = TestBed.createComponent(AppComponent);
26
  const app = fixture.componentInstance;
27
  expect(app.title).toEqual('book-monkey');
28
  });
29
- */
30
 
31
- /*
32
  it('should render title', () => {
33
  const fixture = TestBed.createComponent(AppComponent);
34
  fixture.detectChanges();
35
  const compiled = fixture.nativeElement as HTMLElement;
36
- expect(compiled.querySelector('.content span')?.textContent).toContain('book-monkey app is running!');
37
  });
38
- */
39
  });
1
  import { TestBed } from '@angular/core/testing';
 
2
  import { AppComponent } from './app.component';
3
 
4
  describe('AppComponent', () => {
5
  beforeEach(async () => {
6
  await TestBed.configureTestingModule({
7
+ imports: [AppComponent],
 
 
 
 
 
8
  }).compileComponents();
9
  });
10
 
14
  expect(app).toBeTruthy();
15
  });
16
 
17
+ it(`should have the 'book-monkey' title`, () => {
 
18
  const fixture = TestBed.createComponent(AppComponent);
19
  const app = fixture.componentInstance;
20
  expect(app.title).toEqual('book-monkey');
21
  });
 
22
 
 
23
  it('should render title', () => {
24
  const fixture = TestBed.createComponent(AppComponent);
25
  fixture.detectChanges();
26
  const compiled = fixture.nativeElement as HTMLElement;
27
+ expect(compiled.querySelector('h1')?.textContent).toContain('Hello, book-monkey');
28
  });
 
29
  });
src/app/app.component.ts CHANGED
@@ -1,12 +1,19 @@
1
- import { Component } from '@angular/core';
 
2
 
3
  import { AuthService } from './shared/auth.service';
4
 
5
  @Component({
6
  selector: 'bm-root',
 
 
 
 
 
 
7
  templateUrl: './app.component.html',
8
- styleUrls: ['./app.component.css']
9
  })
10
  export class AppComponent {
11
- constructor(public auth: AuthService) {}
12
  }
1
+ import { Component, inject } from '@angular/core';
2
+ import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
3
 
4
  import { AuthService } from './shared/auth.service';
5
 
6
  @Component({
7
  selector: 'bm-root',
8
+ standalone: true,
9
+ imports: [
10
+ RouterOutlet,
11
+ RouterLink,
12
+ RouterLinkActive
13
+ ],
14
  templateUrl: './app.component.html',
15
+ styleUrl: './app.component.css'
16
  })
17
  export class AppComponent {
18
+ auth = inject(AuthService)
19
  }
src/app/app.config.server.ts ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
2
+ import { provideServerRendering } from '@angular/platform-server';
3
+ import { appConfig } from './app.config';
4
+
5
+ const serverConfig: ApplicationConfig = {
6
+ providers: [
7
+ provideServerRendering()
8
+ ]
9
+ };
10
+
11
+ export const config = mergeApplicationConfig(appConfig, serverConfig);
src/app/app.config.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
2
+ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
3
+ import { provideClientHydration } from '@angular/platform-browser';
4
+ import { provideRouter, withComponentInputBinding } from '@angular/router';
5
+
6
+ import { routes } from './app.routes';
7
+ import { authInterceptor } from './shared/auth.interceptor';
8
+
9
+ export const appConfig: ApplicationConfig = {
10
+ providers: [
11
+ provideZoneChangeDetection({ eventCoalescing: true }),
12
+ provideRouter(routes, withComponentInputBinding()),
13
+ provideHttpClient(
14
+ withFetch(),
15
+ withInterceptors([authInterceptor])
16
+ ),
17
+ provideClientHydration()
18
+ ]
19
+ };
src/app/app.module.ts DELETED
@@ -1,31 +0,0 @@
1
- import { NgModule } from '@angular/core';
2
- import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
3
- import { BrowserModule } from '@angular/platform-browser';
4
-
5
- import { AppRoutingModule } from './app-routing.module';
6
- import { AppComponent } from './app.component';
7
- import { HomeComponent } from './home/home.component';
8
- import { SearchComponent } from './search/search.component';
9
- import { AuthInterceptor } from './shared/auth.interceptor';
10
-
11
- @NgModule({
12
- declarations: [
13
- AppComponent,
14
- HomeComponent,
15
- SearchComponent,
16
- ],
17
- imports: [
18
- BrowserModule,
19
- AppRoutingModule,
20
- HttpClientModule,
21
- ],
22
- providers: [
23
- {
24
- provide: HTTP_INTERCEPTORS,
25
- useClass: AuthInterceptor,
26
- multi: true
27
- }
28
- ],
29
- bootstrap: [AppComponent]
30
- })
31
- export class AppModule { }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/{app-routing.module.ts → app.routes.ts} RENAMED
@@ -1,10 +1,9 @@
1
- import { NgModule } from '@angular/core';
2
- import { RouterModule, Routes } from '@angular/router';
3
 
4
  import { HomeComponent } from './home/home.component';
5
  import { authGuard } from './shared/auth.guard';
6
 
7
- const routes: Routes = [
8
  {
9
  path: '',
10
  redirectTo: 'home',
@@ -20,13 +19,7 @@ const routes: Routes = [
20
  },
21
  {
22
  path: 'admin',
23
- loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
24
  canActivate: [authGuard]
25
  }
26
  ];
27
-
28
- @NgModule({
29
- imports: [RouterModule.forRoot(routes)],
30
- exports: [RouterModule]
31
- })
32
- export class AppRoutingModule { }
1
+ import { Routes } from '@angular/router';
 
2
 
3
  import { HomeComponent } from './home/home.component';
4
  import { authGuard } from './shared/auth.guard';
5
 
6
+ export const routes: Routes = [
7
  {
8
  path: '',
9
  redirectTo: 'home',
19
  },
20
  {
21
  path: 'admin',
22
+ loadChildren: () => import('./admin/admin.routes').then(m => m.ADMIN_ROUTES),
23
  canActivate: [authGuard]
24
  }
25
  ];
 
 
 
 
 
 
src/app/books/book-details/book-details.component.html CHANGED
@@ -1,27 +1,34 @@
1
- <div class="details" *ngIf="book$ | async as book">
 
2
  <h1>{{ book.title }}</h1>
3
- <p role="doc-subtitle" *ngIf="book.subtitle">{{ book.subtitle }}</p>
 
 
4
  <div class="header">
5
  <div>
6
  <h2>Authors</h2>
7
  <ul>
8
- <li *ngFor="let author of book.authors">{{ author }}</li>
 
 
9
  </ul>
10
  </div>
11
  <div>
12
  <h2>ISBN</h2>
13
  {{ book.isbn | isbn }}
14
  </div>
15
- <div *ngIf="book.published">
 
16
  <h2>Published</h2>
17
  {{ book.published | date:'longDate' }}
18
  </div>
 
19
  </div>
20
  <h2>Description</h2>
21
  <p>{{ book.description }}</p>
22
- <img *ngIf="book.thumbnailUrl"
23
- [src]="book.thumbnailUrl"
24
- alt="Cover">
25
  <a class="button arrow-left" routerLink="..">Back to list</a>
26
  <ng-container *bmLoggedinOnly>
27
  <button class="red" bmConfirm="Remove book?" (confirm)="removeBook(book.isbn)">
@@ -33,3 +40,4 @@
33
  </a>
34
  </ng-container>
35
  </div>
 
1
+ @if (book$ | async; as book) {
2
+ <div class="details">
3
  <h1>{{ book.title }}</h1>
4
+ @if (book.subtitle) {
5
+ <p role="doc-subtitle">{{ book.subtitle }}</p>
6
+ }
7
  <div class="header">
8
  <div>
9
  <h2>Authors</h2>
10
  <ul>
11
+ @for (author of book.authors; track author) {
12
+ <li>{{ author }}</li>
13
+ }
14
  </ul>
15
  </div>
16
  <div>
17
  <h2>ISBN</h2>
18
  {{ book.isbn | isbn }}
19
  </div>
20
+ @if (book.published) {
21
+ <div>
22
  <h2>Published</h2>
23
  {{ book.published | date:'longDate' }}
24
  </div>
25
+ }
26
  </div>
27
  <h2>Description</h2>
28
  <p>{{ book.description }}</p>
29
+ @if (book.thumbnailUrl) {
30
+ <img alt="Cover" [ngSrc]="book.thumbnailUrl" width="200" height="250" priority>
31
+ }
32
  <a class="button arrow-left" routerLink="..">Back to list</a>
33
  <ng-container *bmLoggedinOnly>
34
  <button class="red" bmConfirm="Remove book?" (confirm)="removeBook(book.isbn)">
40
  </a>
41
  </ng-container>
42
  </div>
43
+ }
src/app/books/book-details/book-details.component.ts CHANGED
@@ -1,35 +1,36 @@
1
- import { Component } from '@angular/core';
2
- import { AsyncPipe, DatePipe, NgFor, NgIf } from '@angular/common';
3
- import { ActivatedRoute, Router, RouterLink } from '@angular/router';
4
- import { Observable } from 'rxjs';
 
5
 
6
  import { BookStoreService } from '../../shared/book-store.service';
7
- import { Book } from '../../shared/book';
8
  import { IsbnPipe } from '../../shared/isbn.pipe';
9
  import { LoggedinOnlyDirective } from '../../shared/loggedin-only.directive';
10
- import { ConfirmDirective } from '../../shared/confirm.directive';
11
 
12
  @Component({
13
  selector: 'bm-book-details',
14
  templateUrl: './book-details.component.html',
15
- styleUrls: ['./book-details.component.css'],
16
  standalone: true,
17
  imports: [
18
- NgIf, NgFor, DatePipe, AsyncPipe, RouterLink,
19
- IsbnPipe, LoggedinOnlyDirective, ConfirmDirective
 
 
 
 
 
20
  ]
21
  })
22
  export class BookDetailsComponent {
23
- book$: Observable<Book>;
24
-
25
- constructor(
26
- private service: BookStoreService,
27
- private route: ActivatedRoute,
28
- private router: Router
29
- ) {
30
- const isbn = this.route.snapshot.paramMap.get('isbn')!;
31
- this.book$ = this.service.getSingle(isbn);
32
- }
33
 
34
  removeBook(isbn: string) {
35
  this.service.remove(isbn).subscribe(() => {
1
+ import { AsyncPipe, DatePipe, NgOptimizedImage } from '@angular/common';
2
+ import { Component, inject, input } from '@angular/core';
3
+ import { toObservable } from '@angular/core/rxjs-interop';
4
+ import { Router, RouterLink } from '@angular/router';
5
+ import { switchMap } from 'rxjs';
6
 
7
  import { BookStoreService } from '../../shared/book-store.service';
8
+ import { ConfirmDirective } from '../../shared/confirm.directive';
9
  import { IsbnPipe } from '../../shared/isbn.pipe';
10
  import { LoggedinOnlyDirective } from '../../shared/loggedin-only.directive';
 
11
 
12
  @Component({
13
  selector: 'bm-book-details',
14
  templateUrl: './book-details.component.html',
15
+ styleUrl: './book-details.component.css',
16
  standalone: true,
17
  imports: [
18
+ DatePipe,
19
+ AsyncPipe,
20
+ NgOptimizedImage,
21
+ RouterLink,
22
+ IsbnPipe,
23
+ LoggedinOnlyDirective,
24
+ ConfirmDirective
25
  ]
26
  })
27
  export class BookDetailsComponent {
28
+ private service = inject(BookStoreService);
29
+ private router = inject(Router);
30
+ isbn = input.required<string>();
31
+ book$ = toObservable(this.isbn).pipe(
32
+ switchMap(isbn => this.service.getSingle(isbn))
33
+ );
 
 
 
 
34
 
35
  removeBook(isbn: string) {
36
  this.service.remove(isbn).subscribe(() => {
src/app/books/book-list/book-list.component.html CHANGED
@@ -1,9 +1,12 @@
1
  <h1>Books</h1>
2
- <ul class="book-list" *ngIf="books$ | async as books">
3
- <li *ngFor="let book of books">
 
 
4
  <bm-book-list-item [book]="book"></bm-book-list-item>
5
  </li>
6
- <li *ngIf="!books.length">
7
- No books available.
8
- </li>
9
  </ul>
 
1
  <h1>Books</h1>
2
+ @if (books(); as books) {
3
+ <ul class="book-list">
4
+ @for (book of books; track book.isbn) {
5
+ <li>
6
  <bm-book-list-item [book]="book"></bm-book-list-item>
7
  </li>
8
+ } @empty {
9
+ <li>No books available.</li>
10
+ }
11
  </ul>
12
+ }
src/app/books/book-list/book-list.component.ts CHANGED
@@ -1,25 +1,17 @@
1
- import { AsyncPipe, NgFor, NgIf } from '@angular/common';
2
- import { Component } from '@angular/core';
3
- import { Observable } from 'rxjs';
4
 
5
- import { Book } from '../../shared/book';
6
  import { BookStoreService } from '../../shared/book-store.service';
7
  import { BookListItemComponent } from '../book-list-item/book-list-item.component';
8
 
9
  @Component({
10
  selector: 'bm-book-list',
11
  templateUrl: './book-list.component.html',
12
- styleUrls: ['./book-list.component.css'],
13
  standalone: true,
14
- imports: [
15
- NgIf, NgFor, AsyncPipe,
16
- BookListItemComponent
17
- ]
18
  })
19
  export class BookListComponent {
20
- books$: Observable<Book[]>;
21
-
22
- constructor(private service: BookStoreService) {
23
- this.books$ = this.service.getAll();
24
- }
25
  }
 
1
+ import { Component, inject } from '@angular/core';
2
+ import { toSignal } from '@angular/core/rxjs-interop';
 
3
 
 
4
  import { BookStoreService } from '../../shared/book-store.service';
5
  import { BookListItemComponent } from '../book-list-item/book-list-item.component';
6
 
7
  @Component({
8
  selector: 'bm-book-list',
9
  templateUrl: './book-list.component.html',
10
+ styleUrl: './book-list.component.css',
11
  standalone: true,
12
+ imports: [BookListItemComponent]
 
 
 
13
  })
14
  export class BookListComponent {
15
+ books = toSignal(inject(BookStoreService).getAll());
 
 
 
 
16
  }
17
+
src/app/books/book-list-item/book-list-item.component.html CHANGED
@@ -1,13 +1,21 @@
1
- <a *ngIf="book" [routerLink]="book.isbn" class="list-item">
2
- <img *ngIf="book.thumbnailUrl" [src]="book.thumbnailUrl" alt="Cover">
 
 
 
3
  <h2>{{ book.title }}</h2>
4
- <p role="doc-subtitle" *ngIf="book.subtitle">
 
5
  {{ book.subtitle }}
6
  </p>
 
7
  <ul class="comma-list">
8
- <li *ngFor="let author of book.authors">
 
9
  {{ author }}
10
  </li>
 
11
  </ul>
12
  <div>ISBN {{ book.isbn | isbn }}</div>
13
  </a>
 
1
+ @if(book(); as book) {
2
+ <a [routerLink]="book.isbn" class="list-item">
3
+ @if(book.thumbnailUrl) {
4
+ <img alt="Cover" [ngSrc]="book.thumbnailUrl" width="120" height="175">
5
+ }
6
  <h2>{{ book.title }}</h2>
7
+ @if(book.subtitle) {
8
+ <p role="doc-subtitle">
9
  {{ book.subtitle }}
10
  </p>
11
+ }
12
  <ul class="comma-list">
13
+ @for (author of book.authors; track author) {
14
+ <li>
15
  {{ author }}
16
  </li>
17
+ }
18
  </ul>
19
  <div>ISBN {{ book.isbn | isbn }}</div>
20
  </a>
21
+ }
src/app/books/book-list-item/book-list-item.component.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { NgFor, NgIf } from '@angular/common';
2
- import { Component, Input } from '@angular/core';
3
  import { RouterLink } from '@angular/router';
 
4
 
5
  import { IsbnPipe } from '../../shared/isbn.pipe';
6
  import { Book } from '../../shared/book';
@@ -8,10 +8,10 @@ import { Book } from '../../shared/book';
8
  @Component({
9
  selector: 'bm-book-list-item',
10
  templateUrl: './book-list-item.component.html',
11
- styleUrls: ['./book-list-item.component.css'],
12
  standalone: true,
13
- imports: [NgIf, NgFor, RouterLink, IsbnPipe]
14
  })
15
  export class BookListItemComponent {
16
- @Input() book?: Book;
17
  }
1
+ import { Component, input } from '@angular/core';
 
2
  import { RouterLink } from '@angular/router';
3
+ import { NgOptimizedImage } from '@angular/common';
4
 
5
  import { IsbnPipe } from '../../shared/isbn.pipe';
6
  import { Book } from '../../shared/book';
8
  @Component({
9
  selector: 'bm-book-list-item',
10
  templateUrl: './book-list-item.component.html',
11
+ styleUrl: './book-list-item.component.css',
12
  standalone: true,
13
+ imports: [RouterLink, IsbnPipe, NgOptimizedImage]
14
  })
15
  export class BookListItemComponent {
16
+ book = input.required<Book>();
17
  }
src/app/home/home.component.ts CHANGED
@@ -1,8 +1,12 @@
1
  import { Component } from '@angular/core';
 
 
2
 
3
  @Component({
4
  selector: 'bm-home',
5
  templateUrl: './home.component.html',
6
- styleUrls: ['./home.component.css']
 
 
7
  })
8
  export class HomeComponent {}
1
  import { Component } from '@angular/core';
2
+ import { RouterLink } from '@angular/router';
3
+ import { SearchComponent } from '../search/search.component';
4
 
5
  @Component({
6
  selector: 'bm-home',
7
  templateUrl: './home.component.html',
8
+ styleUrl: './home.component.css',
9
+ imports: [RouterLink, SearchComponent],
10
+ standalone: true
11
  })
12
  export class HomeComponent {}
src/app/search/search.component.html CHANGED
@@ -1,16 +1,21 @@
1
  <input type="search"
2
  autocomplete="off"
3
  aria-label="Search"
4
- [class.loading]="isLoading"
5
  #searchInput
6
  (input)="input$.next(searchInput.value)">
7
 
8
- <ul class="search-results" *ngIf="results$ | async as results">
9
- <li *ngFor="let book of results">
 
 
10
  <a [routerLink]="['/books', book.isbn]">
11
  {{ book.title }}
12
  <p role="doc-subtitle">{{ book.subtitle }}</p>
13
  </a>
14
  </li>
15
- <li *ngIf="!results.length">No results</li>
 
 
16
  </ul>
 
1
  <input type="search"
2
  autocomplete="off"
3
  aria-label="Search"
4
+ [class.loading]="isLoading()"
5
  #searchInput
6
  (input)="input$.next(searchInput.value)">
7
 
8
+ @if (results$ | async; as results) {
9
+ <ul class="search-results">
10
+ @for (book of results; track book.isbn) {
11
+ <li>
12
  <a [routerLink]="['/books', book.isbn]">
13
  {{ book.title }}
14
  <p role="doc-subtitle">{{ book.subtitle }}</p>
15
  </a>
16
  </li>
17
+ } @empty {
18
+ <li>No results</li>
19
+ }
20
  </ul>
21
+ }
src/app/search/search.component.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { Component } from '@angular/core';
2
- import { Subject, debounceTime, distinctUntilChanged, filter, switchMap, tap, Observable } from 'rxjs';
 
 
3
 
4
  import { Book } from '../shared/book';
5
  import { BookStoreService } from '../shared/book-store.service';
@@ -7,22 +9,24 @@ import { BookStoreService } from '../shared/book-store.service';
7
  @Component({
8
  selector: 'bm-search',
9
  templateUrl: './search.component.html',
10
- styleUrls: ['./search.component.css']
 
 
11
  })
12
  export class SearchComponent {
 
13
  input$ = new Subject<string>();
14
- isLoading = false;
15
-
16
  results$: Observable<Book[]>;
17
 
18
- constructor(private service: BookStoreService) {
19
  this.results$ = this.input$.pipe(
20
  filter(term => term.length >= 3),
21
  debounceTime(500),
22
  distinctUntilChanged(),
23
- tap(() => this.isLoading = true),
24
  switchMap(term => this.service.getAllSearch(term)),
25
- tap(() => this.isLoading = false)
26
  );
27
  }
28
  }
1
+ import { AsyncPipe } from '@angular/common';
2
+ import { Component, inject, signal } from '@angular/core';
3
+ import { RouterLink } from '@angular/router';
4
+ import { debounceTime, distinctUntilChanged, filter, Observable, Subject, switchMap, tap } from 'rxjs';
5
 
6
  import { Book } from '../shared/book';
7
  import { BookStoreService } from '../shared/book-store.service';
9
  @Component({
10
  selector: 'bm-search',
11
  templateUrl: './search.component.html',
12
+ styleUrl: './search.component.css',
13
+ imports: [RouterLink, AsyncPipe],
14
+ standalone: true
15
  })
16
  export class SearchComponent {
17
+ private service = inject(BookStoreService)
18
  input$ = new Subject<string>();
19
+ isLoading = signal(false);
 
20
  results$: Observable<Book[]>;
21
 
22
+ constructor() {
23
  this.results$ = this.input$.pipe(
24
  filter(term => term.length >= 3),
25
  debounceTime(500),
26
  distinctUntilChanged(),
27
+ tap(() => this.isLoading.set(true)),
28
  switchMap(term => this.service.getAllSearch(term)),
29
+ tap(() => this.isLoading.set(false))
30
  );
31
  }
32
  }
src/app/shared/auth.guard.ts CHANGED
@@ -8,7 +8,7 @@ export const authGuard: CanActivateFn = () => {
8
  const authService = inject(AuthService);
9
  const router = inject(Router);
10
 
11
- if (authService.isAuthenticated) {
12
  return true;
13
  } else {
14
  window.alert('Not logged in!');
8
  const authService = inject(AuthService);
9
  const router = inject(Router);
10
 
11
+ if (authService.isAuthenticated()) {
12
  return true;
13
  } else {
14
  window.alert('Not logged in!');
src/app/shared/auth.interceptor.ts CHANGED
@@ -1,36 +1,23 @@
1
- import { Injectable } from '@angular/core';
2
- import {
3
- HttpRequest,
4
- HttpHandler,
5
- HttpEvent,
6
- HttpInterceptor
7
- } from '@angular/common/http';
8
- import { Observable } from 'rxjs';
9
- import { AuthService } from './auth.service';
10
-
11
- @Injectable()
12
- export class AuthInterceptor implements HttpInterceptor {
13
 
14
- constructor(private authService: AuthService) {}
15
 
16
- intercept(
17
- request: HttpRequest<unknown>,
18
- next: HttpHandler
19
- ): Observable<HttpEvent<unknown>> {
20
- const token = '1234567890';
21
 
22
- if (this.authService.isAuthenticated) {
23
- // Token in Header einfügen
24
- const reqWithToken = request.clone({
25
- setHeaders: {
26
- Authorization: `Bearer ${token}`
27
- }
28
- });
29
 
30
- return next.handle(reqWithToken);
31
- } else {
32
- // Request unverändert weitergeben
33
- return next.handle(request);
34
- }
35
  }
36
  }
1
+ import { inject } from '@angular/core';
2
+ import { HttpInterceptorFn } from '@angular/common/http';
 
 
 
 
 
 
 
 
 
 
3
 
4
+ import { AuthService } from './auth.service';
5
 
6
+ export const authInterceptor: HttpInterceptorFn = (req, next) => {
7
+ const authService = inject(AuthService);
8
+ const token = '1234567890';
 
 
9
 
10
+ if (authService.isAuthenticated()) {
11
+ // Token in Header einfügen
12
+ const reqWithToken = req.clone({
13
+ setHeaders: {
14
+ Authorization: `Bearer ${token}`
15
+ }
16
+ });
17
 
18
+ return next(reqWithToken);
19
+ } else {
20
+ // Request unverändert weitergeben
21
+ return next(req);
 
22
  }
23
  }
src/app/shared/auth.service.ts CHANGED
@@ -1,22 +1,18 @@
1
- import { Injectable } from '@angular/core';
2
- import { BehaviorSubject } from 'rxjs';
3
 
4
  @Injectable({
5
  providedIn: 'root'
6
  })
7
  export class AuthService {
8
- private _isAuthenticated$ = new BehaviorSubject(true);
9
- readonly isAuthenticated$ = this._isAuthenticated$.asObservable();
10
-
11
- get isAuthenticated() {
12
- return this._isAuthenticated$.value;
13
- }
14
 
15
  login() {
16
- this._isAuthenticated$.next(true);
17
  }
18
 
19
  logout() {
20
- this._isAuthenticated$.next(false);
21
  }
22
  }
1
+ import { Injectable, signal } from '@angular/core';
2
+ import { toObservable } from '@angular/core/rxjs-interop';
3
 
4
  @Injectable({
5
  providedIn: 'root'
6
  })
7
  export class AuthService {
8
+ readonly isAuthenticated = signal(true);
9
+ readonly isAuthenticated$ = toObservable(this.isAuthenticated);
 
 
 
 
10
 
11
  login() {
12
+ this.isAuthenticated.set(true);
13
  }
14
 
15
  logout() {
16
+ this.isAuthenticated.set(false);
17
  }
18
  }
src/app/shared/book-store.service.ts CHANGED
@@ -1,5 +1,5 @@
1
  import { HttpClient } from '@angular/common/http';
2
- import { Injectable } from '@angular/core';
3
  import { Observable, catchError, of } from 'rxjs';
4
 
5
  import { Book } from './book';
@@ -9,8 +9,7 @@ import { Book } from './book';
9
  })
10
  export class BookStoreService {
11
  private apiUrl = 'https://api5.angular-buch.com';
12
-
13
- constructor(private http: HttpClient) {}
14
 
15
  getAll(): Observable<Book[]> {
16
  return this.http.get<Book[]>(`${this.apiUrl}/books`).pipe(
1
  import { HttpClient } from '@angular/common/http';
2
+ import { Injectable, inject } from '@angular/core';
3
  import { Observable, catchError, of } from 'rxjs';
4
 
5
  import { Book } from './book';
9
  })
10
  export class BookStoreService {
11
  private apiUrl = 'https://api5.angular-buch.com';
12
+ private http = inject(HttpClient);
 
13
 
14
  getAll(): Observable<Book[]> {
15
  return this.http.get<Book[]>(`${this.apiUrl}/books`).pipe(
src/app/shared/confirm.directive.ts CHANGED
@@ -1,15 +1,15 @@
1
- import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core';
2
 
3
  @Directive({
4
  selector: '[bmConfirm]',
5
  standalone: true
6
  })
7
  export class ConfirmDirective {
8
- @Input('bmConfirm') confirmText?: string;
9
- @Output() confirm = new EventEmitter<void>();
10
 
11
  @HostListener('click') onClick() {
12
- if (window.confirm(this.confirmText)) {
13
  this.confirm.emit();
14
  }
15
  }
1
+ import { Directive, HostListener, input, output } from '@angular/core';
2
 
3
  @Directive({
4
  selector: '[bmConfirm]',
5
  standalone: true
6
  })
7
  export class ConfirmDirective {
8
+ confirmText = input.required<string>({ alias: 'bmConfirm' });
9
+ confirm = output();
10
 
11
  @HostListener('click') onClick() {
12
+ if (window.confirm(this.confirmText())) {
13
  this.confirm.emit();
14
  }
15
  }
src/app/shared/loggedin-only.directive.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { Directive, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core';
2
- import { Subject, takeUntil } from 'rxjs';
3
 
4
  import { AuthService } from './auth.service';
5
 
@@ -7,26 +6,18 @@ import { AuthService } from './auth.service';
7
  selector: '[bmLoggedinOnly]',
8
  standalone: true
9
  })
10
- export class LoggedinOnlyDirective implements OnDestroy {
11
- private destroy$ = new Subject<void>();
 
 
12
 
13
- constructor(
14
- private template: TemplateRef<unknown>,
15
- private viewContainer: ViewContainerRef,
16
- private authService: AuthService
17
- ) {
18
- this.authService.isAuthenticated$.pipe(
19
- takeUntil(this.destroy$)
20
- ).subscribe(isAuthenticated => {
21
- if (isAuthenticated) {
22
  this.viewContainer.createEmbeddedView(this.template);
23
  } else {
24
  this.viewContainer.clear();
25
  }
26
  });
27
  }
28
-
29
- ngOnDestroy(): void {
30
- this.destroy$.next();
31
- }
32
  }
1
+ import { Directive, TemplateRef, ViewContainerRef, effect, inject } from '@angular/core';
 
2
 
3
  import { AuthService } from './auth.service';
4
 
6
  selector: '[bmLoggedinOnly]',
7
  standalone: true
8
  })
9
+ export class LoggedinOnlyDirective {
10
+ private template = inject(TemplateRef);
11
+ private viewContainer = inject(ViewContainerRef);
12
+ private authService = inject(AuthService);
13
 
14
+ constructor() {
15
+ effect(() => {
16
+ if (this.authService.isAuthenticated()) {
 
 
 
 
 
 
17
  this.viewContainer.createEmbeddedView(this.template);
18
  } else {
19
  this.viewContainer.clear();
20
  }
21
  });
22
  }
 
 
 
 
23
  }
src/index.html CHANGED
@@ -6,6 +6,7 @@
6
  <base href="/">
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
  <link rel="icon" type="image/x-icon" href="favicon.ico">
 
9
  </head>
10
  <body>
11
  <bm-root>
6
  <base href="/">
7
  <meta name="viewport" content="width=device-width, initial-scale=1">
8
  <link rel="icon" type="image/x-icon" href="favicon.ico">
9
+ <link rel="preconnect" href="https://cdn.ng-buch.de">
10
  </head>
11
  <body>
12
  <bm-root>
src/main.server.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
1
+ import { bootstrapApplication } from '@angular/platform-browser';
2
+ import { AppComponent } from './app/app.component';
3
+ import { config } from './app/app.config.server';
4
+
5
+ const bootstrap = () => bootstrapApplication(AppComponent, config);
6
+
7
+ export default bootstrap;
src/main.ts CHANGED
@@ -1,8 +1,6 @@
1
- import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
 
 
2
 
3
- import { AppModule } from './app/app.module';
4
-
5
- platformBrowserDynamic().bootstrapModule(AppModule, {
6
- ngZoneEventCoalescing: true
7
- })
8
- .catch(err => console.error(err));
1
+ import { bootstrapApplication } from '@angular/platform-browser';
2
+ import { appConfig } from './app/app.config';
3
+ import { AppComponent } from './app/app.component';
4
 
5
+ bootstrapApplication(AppComponent, appConfig)
6
+ .catch((err) => console.error(err));
 
 
 
 
tsconfig.app.json CHANGED
@@ -4,10 +4,14 @@
4
  "extends": "./tsconfig.json",
5
  "compilerOptions": {
6
  "outDir": "./out-tsc/app",
7
- "types": []
 
 
8
  },
9
  "files": [
10
- "src/main.ts"
 
 
11
  ],
12
  "include": [
13
  "src/**/*.d.ts"
4
  "extends": "./tsconfig.json",
5
  "compilerOptions": {
6
  "outDir": "./out-tsc/app",
7
+ "types": [
8
+ "node"
9
+ ]
10
  },
11
  "files": [
12
+ "src/main.ts",
13
+ "src/main.server.ts",
14
+ "server.ts"
15
  ],
16
  "include": [
17
  "src/**/*.d.ts"