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": "src/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": "^19.1.0",
20
  "@angular/platform-browser": "^19.1.0",
21
  "@angular/platform-browser-dynamic": "^19.1.0",
 
22
  "@angular/router": "^19.1.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.15.0"
@@ -30,7 +34,9 @@
30
  "@angular-devkit/build-angular": "^19.1.1",
31
  "@angular/cli": "^19.1.1",
32
  "@angular/compiler-cli": "^19.1.0",
 
33
  "@types/jasmine": "~5.1.0",
 
34
  "angular-eslint": "19.0.2",
35
  "eslint": "^9.16.0",
36
  "jasmine-core": "~5.5.0",
@@ -42,4 +48,4 @@
42
  "typescript": "~5.7.2",
43
  "typescript-eslint": "8.18.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": "^19.1.0",
21
  "@angular/platform-browser": "^19.1.0",
22
  "@angular/platform-browser-dynamic": "^19.1.0",
23
+ "@angular/platform-server": "^19.1.0",
24
  "@angular/router": "^19.1.0",
25
+ "@angular/ssr": "^19.1.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.15.0"
34
  "@angular-devkit/build-angular": "^19.1.1",
35
  "@angular/cli": "^19.1.1",
36
  "@angular/compiler-cli": "^19.1.0",
37
+ "@types/express": "^4.17.17",
38
  "@types/jasmine": "~5.1.0",
39
+ "@types/node": "^18.18.0",
40
  "angular-eslint": "19.0.2",
41
  "eslint": "^9.16.0",
42
  "jasmine-core": "~5.5.0",
48
  "typescript": "~5.7.2",
49
  "typescript-eslint": "8.18.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
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,21 +1,19 @@
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
- standalone: false,
11
- styleUrl: './book-create.component.css'
12
  })
13
  export class BookCreateComponent {
14
-
15
- constructor(
16
- private service: BookStoreService,
17
- private router: Router
18
- ) { }
19
 
20
  create(book: Book) {
21
  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
  })
14
  export class BookCreateComponent {
15
+ private service = inject(BookStoreService);
16
+ private router = inject(Router);
 
 
 
17
 
18
  create(book: Book) {
19
  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,29 +1,26 @@
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
- standalone: false,
12
- styleUrl: './book-edit.component.css'
13
  })
14
  export class BookEditComponent {
15
- book$: Observable<Book>;
16
-
17
- constructor(
18
- private service: BookStoreService,
19
- private route: ActivatedRoute,
20
- private router: Router
21
- ) {
22
- this.book$ = this.route.paramMap.pipe(
23
- map(params => params.get('isbn')!),
24
- switchMap(isbn => this.service.getSingle(isbn))
25
- );
26
- }
27
 
28
  update(book: Book) {
29
  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
  })
17
  export class BookEditComponent {
18
+ private service = inject(BookStoreService);
19
+ private router = inject(Router);
20
+ isbn = input.required<string>();
21
+ book$ = toObservable(this.isbn).pipe(
22
+ switchMap(isbn => this.service.getSingle(isbn))
23
+ );
 
 
 
 
 
 
24
 
25
  update(book: Book) {
26
  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,19 +1,19 @@
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
- standalone: false,
12
- styleUrl: './book-form.component.css'
13
  })
14
- export class BookFormComponent implements OnChanges {
15
- @Input() book?: Book;
16
- @Output() submitBook = new EventEmitter<Book>();
17
 
18
  form = new FormGroup({
19
  title: new FormControl('', {
@@ -23,11 +23,8 @@ export class BookFormComponent implements OnChanges {
23
  subtitle: new FormControl('', { nonNullable: true }),
24
  isbn: new FormControl('', {
25
  nonNullable: true,
26
- validators: [
27
- Validators.required,
28
- isbnFormat
29
- ],
30
- asyncValidators: inject(AsyncValidatorsService).isbnExists()
31
  }),
32
  description: new FormControl('', { nonNullable: true }),
33
  published: new FormControl('', { nonNullable: true }),
@@ -35,13 +32,16 @@ export class BookFormComponent implements OnChanges {
35
  thumbnailUrl: new FormControl('', { nonNullable: true })
36
  });
37
 
38
- ngOnChanges(): void {
39
- if (this.book) {
40
- this.setFormValues(this.book);
41
- this.setEditMode(true);
42
- } else {
43
- this.setEditMode(false);
44
- }
 
 
 
45
  }
46
 
47
  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
  })
14
+ export class BookFormComponent {
15
+ book = input<Book>();
16
+ submitBook = output<Book>();
17
 
18
  form = new FormGroup({
19
  title: new FormControl('', {
23
  subtitle: new FormControl('', { nonNullable: true }),
24
  isbn: new FormControl('', {
25
  nonNullable: true,
26
+ validators: [Validators.required, isbnFormat],
27
+ asyncValidators: isbnExists(),
 
 
 
28
  }),
29
  description: new FormControl('', { nonNullable: true }),
30
  published: new FormControl('', { nonNullable: true }),
32
  thumbnailUrl: new FormControl('', { nonNullable: true })
33
  });
34
 
35
+ constructor() {
36
+ effect(() => {
37
+ const book = this.book();
38
+ if (book) {
39
+ this.setFormValues(book);
40
+ this.setEditMode(true);
41
+ } else {
42
+ this.setEditMode(false);
43
+ }
44
+ });
45
  }
46
 
47
  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,31 +1,26 @@
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
- standalone: false,
8
  styleUrl: './form-errors.component.css',
9
  })
10
  export class FormErrorsComponent {
11
- @Input() controlName?: string;
12
- @Input() messages: { [errorCode: string]: string } = {};
13
 
14
- constructor(private form: FormGroupDirective) {}
15
 
16
  get errors(): string[] {
17
- if (!this.controlName) {
18
- return [];
19
- }
20
-
21
- const control = this.form.control.get(this.controlName);
22
 
23
  if (!control || !control.errors || !control.touched) {
24
  return [];
25
  }
26
 
27
  return Object.keys(control.errors).map(errorCode => {
28
- return this.messages[errorCode];
29
  });
30
  }
31
  }
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
  })
9
  export class FormErrorsComponent {
10
+ controlName = input.required<string>();
11
+ messages = input.required<{ [errorCode: string]: string }>();
12
 
13
+ private form = inject(FormGroupDirective)
14
 
15
  get errors(): string[] {
16
+ const control = this.form.control.get(this.controlName());
 
 
 
 
17
 
18
  if (!control || !control.errors || !control.touched) {
19
  return [];
20
  }
21
 
22
  return Object.keys(control.errors).map(errorCode => {
23
+ return this.messages()[errorCode];
24
  });
25
  }
26
  }
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.ts CHANGED
@@ -1,13 +1,18 @@
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
- standalone: false,
9
  styleUrl: './app.component.css'
10
  })
11
  export class AppComponent {
12
- constructor(public auth: AuthService) {}
13
  }
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
+ imports: [
9
+ RouterOutlet,
10
+ RouterLink,
11
+ RouterLinkActive
12
+ ],
13
  templateUrl: './app.component.html',
 
14
  styleUrl: './app.component.css'
15
  })
16
  export class AppComponent {
17
+ auth = inject(AuthService)
18
  }
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 { provideHttpClient, withInterceptorsFromDi, 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
- ],
21
- providers: [
22
- provideHttpClient(withInterceptorsFromDi()),
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,34 +1,35 @@
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
  styleUrl: './book-details.component.css',
16
  imports: [
17
- NgIf, NgFor, DatePipe, AsyncPipe, RouterLink,
18
- IsbnPipe, LoggedinOnlyDirective, ConfirmDirective
 
 
 
 
 
19
  ]
20
  })
21
  export class BookDetailsComponent {
22
- book$: Observable<Book>;
23
-
24
- constructor(
25
- private service: BookStoreService,
26
- private route: ActivatedRoute,
27
- private router: Router
28
- ) {
29
- const isbn = this.route.snapshot.paramMap.get('isbn')!;
30
- this.book$ = this.service.getSingle(isbn);
31
- }
32
 
33
  removeBook(isbn: string) {
34
  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
  imports: [
17
+ DatePipe,
18
+ AsyncPipe,
19
+ NgOptimizedImage,
20
+ RouterLink,
21
+ IsbnPipe,
22
+ LoggedinOnlyDirective,
23
+ ConfirmDirective
24
  ]
25
  })
26
  export class BookDetailsComponent {
27
+ private service = inject(BookStoreService);
28
+ private router = inject(Router);
29
+ isbn = input.required<string>();
30
+ book$ = toObservable(this.isbn).pipe(
31
+ switchMap(isbn => this.service.getSingle(isbn))
32
+ );
 
 
 
 
33
 
34
  removeBook(isbn: string) {
35
  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,8 +1,6 @@
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
 
@@ -10,15 +8,9 @@ import { BookListItemComponent } from '../book-list-item/book-list-item.componen
10
  selector: 'bm-book-list',
11
  templateUrl: './book-list.component.html',
12
  styleUrl: './book-list.component.css',
13
- imports: [
14
- NgIf, NgFor, AsyncPipe,
15
- BookListItemComponent
16
- ]
17
  })
18
  export class BookListComponent {
19
- books$: Observable<Book[]>;
20
-
21
- constructor(private service: BookStoreService) {
22
- this.books$ = this.service.getAll();
23
- }
24
  }
 
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
 
8
  selector: 'bm-book-list',
9
  templateUrl: './book-list.component.html',
10
  styleUrl: './book-list.component.css',
11
+ imports: [BookListItemComponent]
 
 
 
12
  })
13
  export class BookListComponent {
14
+ books = toSignal(inject(BookStoreService).getAll());
 
 
 
 
15
  }
16
+
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';
@@ -9,8 +9,8 @@ import { Book } from '../../shared/book';
9
  selector: 'bm-book-list-item',
10
  templateUrl: './book-list-item.component.html',
11
  styleUrl: './book-list-item.component.css',
12
- imports: [NgIf, NgFor, RouterLink, IsbnPipe]
13
  })
14
  export class BookListItemComponent {
15
- @Input() book?: Book;
16
  }
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';
9
  selector: 'bm-book-list-item',
10
  templateUrl: './book-list-item.component.html',
11
  styleUrl: './book-list-item.component.css',
12
+ imports: [RouterLink, IsbnPipe, NgOptimizedImage]
13
  })
14
  export class BookListItemComponent {
15
+ book = input.required<Book>();
16
  }
src/app/home/home.component.ts CHANGED
@@ -1,9 +1,11 @@
1
  import { Component } from '@angular/core';
 
 
2
 
3
  @Component({
4
  selector: 'bm-home',
5
  templateUrl: './home.component.html',
6
- standalone: false,
7
- styleUrl: './home.component.css'
8
  })
9
  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
  })
11
  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,23 +9,23 @@ import { BookStoreService } from '../shared/book-store.service';
7
  @Component({
8
  selector: 'bm-search',
9
  templateUrl: './search.component.html',
10
- standalone: false,
11
- styleUrl: './search.component.css'
12
  })
13
  export class SearchComponent {
 
14
  input$ = new Subject<string>();
15
- isLoading = false;
16
-
17
  results$: Observable<Book[]>;
18
 
19
- constructor(private service: BookStoreService) {
20
  this.results$ = this.input$.pipe(
21
  filter(term => term.length >= 3),
22
  debounceTime(500),
23
  distinctUntilChanged(),
24
- tap(() => this.isLoading = true),
25
  switchMap(term => this.service.getAllSearch(term)),
26
- tap(() => this.isLoading = false)
27
  );
28
  }
29
  }
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
  })
15
  export class SearchComponent {
16
+ private service = inject(BookStoreService)
17
  input$ = new Subject<string>();
18
+ isLoading = signal(false);
 
19
  results$: Observable<Book[]>;
20
 
21
+ constructor() {
22
  this.results$ = this.input$.pipe(
23
  filter(term => term.length >= 3),
24
  debounceTime(500),
25
  distinctUntilChanged(),
26
+ tap(() => this.isLoading.set(true)),
27
  switchMap(term => this.service.getAllSearch(term)),
28
+ tap(() => this.isLoading.set(false))
29
  );
30
  }
31
  }
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,14 +1,14 @@
1
- import { Directive, EventEmitter, HostListener, Input, Output } from '@angular/core';
2
 
3
  @Directive({
4
  selector: '[bmConfirm]'
5
  })
6
  export class ConfirmDirective {
7
- @Input('bmConfirm') confirmText?: string;
8
- @Output() confirm = new EventEmitter<void>();
9
 
10
  @HostListener('click') onClick() {
11
- if (window.confirm(this.confirmText)) {
12
  this.confirm.emit();
13
  }
14
  }
1
+ import { Directive, HostListener, input, output } from '@angular/core';
2
 
3
  @Directive({
4
  selector: '[bmConfirm]'
5
  })
6
  export class ConfirmDirective {
7
+ confirmText = input.required<string>({ alias: 'bmConfirm' });
8
+ confirm = output();
9
 
10
  @HostListener('click') onClick() {
11
+ if (window.confirm(this.confirmText())) {
12
  this.confirm.emit();
13
  }
14
  }
src/app/shared/loggedin-only.directive.ts CHANGED
@@ -1,31 +1,22 @@
1
- import { Directive, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core';
2
- import { Subject, takeUntil } from 'rxjs';
3
 
4
  import { AuthService } from './auth.service';
5
 
6
  @Directive({
7
  selector: '[bmLoggedinOnly]'
8
  })
9
- export class LoggedinOnlyDirective implements OnDestroy {
10
- private destroy$ = new Subject<void>();
 
 
11
 
12
- constructor(
13
- private template: TemplateRef<unknown>,
14
- private viewContainer: ViewContainerRef,
15
- private authService: AuthService
16
- ) {
17
- this.authService.isAuthenticated$.pipe(
18
- takeUntil(this.destroy$)
19
- ).subscribe(isAuthenticated => {
20
- if (isAuthenticated) {
21
  this.viewContainer.createEmbeddedView(this.template);
22
  } else {
23
  this.viewContainer.clear();
24
  }
25
  });
26
  }
27
-
28
- ngOnDestroy(): void {
29
- this.destroy$.next();
30
- }
31
  }
1
+ import { Directive, TemplateRef, ViewContainerRef, effect, inject } from '@angular/core';
 
2
 
3
  import { AuthService } from './auth.service';
4
 
5
  @Directive({
6
  selector: '[bmLoggedinOnly]'
7
  })
8
+ export class LoggedinOnlyDirective {
9
+ private template = inject(TemplateRef);
10
+ private viewContainer = inject(ViewContainerRef);
11
+ private authService = inject(AuthService);
12
 
13
+ constructor() {
14
+ effect(() => {
15
+ if (this.authService.isAuthenticated()) {
 
 
 
 
 
 
16
  this.viewContainer.createEmbeddedView(this.template);
17
  } else {
18
  this.viewContainer.clear();
19
  }
20
  });
21
  }
 
 
 
 
22
  }
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,7 +1,6 @@
1
- import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
2
- import { AppModule } from './app/app.module';
 
3
 
4
- platformBrowserDynamic().bootstrapModule(AppModule, {
5
- ngZoneEventCoalescing: true,
6
- })
7
- .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));
 
 
src/server.ts ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { APP_BASE_HREF } from '@angular/common';
2
+ import { CommonEngine, isMainModule } from '@angular/ssr/node';
3
+ import express from 'express';
4
+ import { dirname, join, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import AppServerModule from './main.server';
7
+
8
+ const serverDistFolder = dirname(fileURLToPath(import.meta.url));
9
+ const browserDistFolder = resolve(serverDistFolder, '../browser');
10
+ const indexHtml = join(serverDistFolder, 'index.server.html');
11
+
12
+ const app = express();
13
+ const commonEngine = new CommonEngine();
14
+
15
+ /**
16
+ * Example Express Rest API endpoints can be defined here.
17
+ * Uncomment and define endpoints as necessary.
18
+ *
19
+ * Example:
20
+ * ```ts
21
+ * app.get('/api/**', (req, res) => {
22
+ * // Handle API request
23
+ * });
24
+ * ```
25
+ */
26
+
27
+ /**
28
+ * Serve static files from /browser
29
+ */
30
+ app.get(
31
+ '**',
32
+ express.static(browserDistFolder, {
33
+ maxAge: '1y',
34
+ index: 'index.html'
35
+ }),
36
+ );
37
+
38
+ /**
39
+ * Handle all other requests by rendering the Angular application.
40
+ */
41
+ app.get('**', (req, res, next) => {
42
+ const { protocol, originalUrl, baseUrl, headers } = req;
43
+
44
+ commonEngine
45
+ .render({
46
+ bootstrap: AppServerModule,
47
+ documentFilePath: indexHtml,
48
+ url: `${protocol}://${headers.host}${originalUrl}`,
49
+ publicPath: browserDistFolder,
50
+ providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
51
+ })
52
+ .then((html) => res.send(html))
53
+ .catch((err) => next(err));
54
+ });
55
+
56
+ /**
57
+ * Start the server if this module is the main entry point.
58
+ * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
59
+ */
60
+ if (isMainModule(import.meta.url)) {
61
+ const port = process.env['PORT'] || 4000;
62
+ app.listen(port, () => {
63
+ console.log(`Node Express server listening on http://localhost:${port}`);
64
+ });
65
+ }
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
+ "src/server.ts"
15
  ],
16
  "include": [
17
  "src/**/*.d.ts"