├── .editorconfig
├── .gitignore
├── .vscode
├── extensions.json
├── launch.json
└── tasks.json
├── README.md
├── angular.json
├── bun.lockb
├── package.json
├── public
└── favicon.ico
├── src
├── app
│ ├── app.component.html
│ ├── app.component.scss
│ ├── app.component.ts
│ ├── app.config.server.ts
│ ├── app.config.ts
│ ├── app.routes.server.ts
│ ├── app.routes.ts
│ ├── custom-input
│ │ ├── custom-input.component.html
│ │ ├── custom-input.component.scss
│ │ └── custom-input.component.ts
│ └── form-child
│ │ ├── form-child.component.html
│ │ ├── form-child.component.scss
│ │ └── form-child.component.ts
├── index.html
├── main.server.ts
├── main.ts
├── server.ts
└── styles.scss
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 | ij_typescript_use_double_quotes = false
14 |
15 | [*.md]
16 | max_line_length = off
17 | trim_trailing_whitespace = false
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
2 |
3 | # Compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /bazel-out
8 |
9 | # Node
10 | /node_modules
11 | npm-debug.log
12 | yarn-error.log
13 |
14 | # IDEs and editors
15 | .idea/
16 | .project
17 | .classpath
18 | .c9/
19 | *.launch
20 | .settings/
21 | *.sublime-workspace
22 |
23 | # Visual Studio Code
24 | .vscode/*
25 | !.vscode/settings.json
26 | !.vscode/tasks.json
27 | !.vscode/launch.json
28 | !.vscode/extensions.json
29 | .history/*
30 |
31 | # Miscellaneous
32 | /.angular/cache
33 | .sass-cache/
34 | /connect.lock
35 | /coverage
36 | /libpeerconnection.log
37 | testem.log
38 | /typings
39 |
40 | # System files
41 | .DS_Store
42 | Thumbs.db
43 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
3 | "recommendations": ["angular.ng-template"]
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
3 | "version": "0.2.0",
4 | "configurations": [
5 | {
6 | "name": "ng serve",
7 | "type": "chrome",
8 | "request": "launch",
9 | "preLaunchTask": "npm: start",
10 | "url": "http://localhost:4200/"
11 | },
12 | {
13 | "name": "ng test",
14 | "type": "chrome",
15 | "request": "launch",
16 | "preLaunchTask": "npm: test",
17 | "url": "http://localhost:9876/debug.html"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
3 | "version": "2.0.0",
4 | "tasks": [
5 | {
6 | "type": "npm",
7 | "script": "start",
8 | "isBackground": true,
9 | "problemMatcher": {
10 | "owner": "typescript",
11 | "pattern": "$tsc",
12 | "background": {
13 | "activeOnStart": true,
14 | "beginsPattern": {
15 | "regexp": "(.*?)"
16 | },
17 | "endsPattern": {
18 | "regexp": "bundle generation complete"
19 | }
20 | }
21 | }
22 | },
23 | {
24 | "type": "npm",
25 | "script": "test",
26 | "isBackground": true,
27 | "problemMatcher": {
28 | "owner": "typescript",
29 | "pattern": "$tsc",
30 | "background": {
31 | "activeOnStart": true,
32 | "beginsPattern": {
33 | "regexp": "(.*?)"
34 | },
35 | "endsPattern": {
36 | "regexp": "bundle generation complete"
37 | }
38 | }
39 | }
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Project Documentation: Dynamic Reactive Form Management in Angular
2 |
3 | ## Introduction
4 |
5 | This project implements an innovative solution for managing dynamic reactive forms
6 | in Angular 18/19, leveraging the latest features of the framework. It stands out
7 | for simplifying the handling of complex forms with multiple component layers by using
8 | **signals**, modern decorators such as `@let` and `@for`, and adopting Angular's
9 | cutting-edge patterns.
10 |
11 | Key highlights of this approach include:
12 |
13 | - **Scalability:** Dynamically add forms with individual validations.
14 | - **Optimized Reactivity:** Use **signals** for efficient value computation.
15 | - **Modularity:** Enables reuse through decoupled, customizable components.
16 |
17 | ## Project Structure
18 |
19 | ### 1. `AppComponent` (Main Component)
20 |
21 | Manages the master form containing a dynamic array of subforms.
22 |
23 | #### Key Features
24 |
25 | - **Main Reactive Form:** Defined using `FormBuilder` with a `FormArray` to store
26 | multiple individual `FormGroup` instances.
27 | - **Optimized Reactivity with Signals:** `toSignal` converts form value changes into
28 | a reactive signal, while `computed` dynamically calculates the form's total value.
29 | - **Modern Angular Decorators:** Utilizes `@let` and `@for` in the template for
30 | declarative control management.
31 |
32 | #### Relevant Code
33 |
34 | ```typescript
35 | totalValue = computed(() => {
36 | const value = this.itemChanges()?.items?.reduce((total, item) => total + (Number(item?.value) || 0), 0);
37 | return value;
38 | });
39 | ```
40 |
41 | #### Template
42 |
43 | ```html
44 |
45 | @let items = form.controls.items.controls;
46 |
47 |
48 | @for (formGroup of items; track formGroup.controls.id.value) {
49 |
50 | }
51 |
52 |
Total value: {{ totalValue() }}
53 |
54 | ```
55 |
56 | ---
57 |
58 | ### 2. `FormChildComponent` (Child Component)
59 |
60 | Represents each dynamic form element and receives a `FormGroup` as input.
61 |
62 | #### Features
63 |
64 | - **Encapsulation of Subform Logic:** Manages validations and structure of child
65 | forms.
66 | - **Reusable Input Component:** Integrates a custom component (`CustomInputComponent`)
67 | for individual inputs.
68 |
69 | #### Template
70 |
71 | ```html
72 |
76 | ```
77 |
78 | ---
79 |
80 | ### 3. `CustomInputComponent` (Custom Input Component)
81 |
82 | Simplifies the integration of reusable inputs and declaratively validates form errors.
83 |
84 | #### Features
85 |
86 | - **ControlValueAccessor Implementation:** Enables seamless integration with Angular
87 | Forms.
88 | - **Declarative Error Handling:** Uses modern directives like `@if` to reactively
89 | display errors.
90 |
91 | #### Template
92 |
93 | ```html
94 | @let localControl = control();
95 |
96 |
97 |
98 | @if (localControl.invalid && (localControl.dirty || localControl.touched)) {
99 |
100 | @if (localControl.errors?.["required"]) {
101 | This field is required
102 | }
103 |
104 | }
105 | ```
106 |
107 | ---
108 |
109 | ## Project Innovations
110 |
111 | 1. **Signals for Optimization:** Signals enable efficient value computation, avoiding
112 | unnecessary recalculations whenever the form changes.
113 |
114 | 2. **Modern Directives (`@let`, `@for`, `@if`):**
115 |
116 | - `@let`: Improves code clarity by introducing variables into the template context.
117 | - `@for`: Replaces traditional iteration logic, making the code more readable
118 | and performant.
119 | - `@if`: Simplifies conditional logic in templates.
120 |
121 | 3. **Encapsulation and Modularity:** Decoupled components such as `FormChildComponent`
122 | and `CustomInputComponent` ensure functionalities are reusable and independently
123 | testable.
124 |
125 | 4. **Simplicity in Dynamic Form Management:** Seamless integration between `FormArray`
126 | and `FormGroup` allows adding elements to the form with a single line of code,
127 | eliminating redundant logic.
128 |
129 | ---
130 |
131 | ## Benefits
132 |
133 | 1. **Less Imperative Code:** This approach minimizes the need for complex logic to
134 | synchronize data between the template and TypeScript, improving readability.
135 |
136 | 2. **High Performance:** Signals and modern decorators optimize rendering and reduce
137 | unnecessary computations.
138 |
139 | 3. **Extensibility:** The modular architecture allows adding additional validations,
140 | styles, and functionalities without impacting the project’s foundation.
141 |
142 | 4. **Maintainability:** The decoupling of the main component, child components, and
143 | custom input ensures that each part of the system is easy to modify and scale.
144 |
145 | ---
146 |
147 | ## Conclusion
148 |
149 | This project represents a revolutionary way to manage dynamic forms in Angular,
150 | combining the latest framework technologies with a focus on simplicity and efficiency.
151 | It is ideal for applications requiring highly customizable forms while maintaining
152 | a clean and modular codebase.
153 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "angular-form": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "scss"
11 | }
12 | },
13 | "root": "",
14 | "sourceRoot": "src",
15 | "prefix": "app",
16 | "architect": {
17 | "build": {
18 | "builder": "@angular-devkit/build-angular:application",
19 | "options": {
20 | "outputPath": "dist/angular-form",
21 | "index": "src/index.html",
22 | "browser": "src/main.ts",
23 | "polyfills": [
24 | "zone.js"
25 | ],
26 | "tsConfig": "tsconfig.app.json",
27 | "inlineStyleLanguage": "scss",
28 | "assets": [
29 | {
30 | "glob": "**/*",
31 | "input": "public"
32 | }
33 | ],
34 | "styles": [
35 | "src/styles.scss"
36 | ],
37 | "scripts": [],
38 | "server": "src/main.server.ts",
39 | "outputMode": "server",
40 | "ssr": {
41 | "entry": "src/server.ts"
42 | }
43 | },
44 | "configurations": {
45 | "production": {
46 | "budgets": [
47 | {
48 | "type": "initial",
49 | "maximumWarning": "500kB",
50 | "maximumError": "1MB"
51 | },
52 | {
53 | "type": "anyComponentStyle",
54 | "maximumWarning": "4kB",
55 | "maximumError": "8kB"
56 | }
57 | ],
58 | "outputHashing": "all"
59 | },
60 | "development": {
61 | "optimization": false,
62 | "extractLicenses": false,
63 | "sourceMap": true
64 | }
65 | },
66 | "defaultConfiguration": "production"
67 | },
68 | "serve": {
69 | "builder": "@angular-devkit/build-angular:dev-server",
70 | "configurations": {
71 | "production": {
72 | "buildTarget": "angular-form:build:production"
73 | },
74 | "development": {
75 | "buildTarget": "angular-form:build:development"
76 | }
77 | },
78 | "defaultConfiguration": "development"
79 | },
80 | "extract-i18n": {
81 | "builder": "@angular-devkit/build-angular:extract-i18n"
82 | },
83 | "test": {
84 | "builder": "@angular-devkit/build-angular:karma",
85 | "options": {
86 | "polyfills": [
87 | "zone.js",
88 | "zone.js/testing"
89 | ],
90 | "tsConfig": "tsconfig.spec.json",
91 | "inlineStyleLanguage": "scss",
92 | "assets": [
93 | {
94 | "glob": "**/*",
95 | "input": "public"
96 | }
97 | ],
98 | "styles": [
99 | "src/styles.scss"
100 | ],
101 | "scripts": []
102 | }
103 | }
104 | }
105 | }
106 | },
107 | "cli": {
108 | "analytics": "6e93ab4a-3366-4e4d-9252-fa33eef73648"
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gentleman-Programming/angular-signals-reactive-form/73d0ad1aa58ece2893f20961020686d40ff6daf9/bun.lockb
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-form",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "watch": "ng build --watch --configuration development",
9 | "test": "ng test",
10 | "serve:ssr:angular-form": "node dist/angular-form/server/server.mjs"
11 | },
12 | "private": true,
13 | "dependencies": {
14 | "@angular/animations": "^19.0.0",
15 | "@angular/common": "^19.0.0",
16 | "@angular/compiler": "^19.0.0",
17 | "@angular/core": "^19.0.0",
18 | "@angular/forms": "^19.0.0",
19 | "@angular/platform-browser": "^19.0.0",
20 | "@angular/platform-browser-dynamic": "^19.0.0",
21 | "@angular/platform-server": "^19.0.0",
22 | "@angular/router": "^19.0.0",
23 | "@angular/ssr": "^19.0.0",
24 | "express": "^4.18.2",
25 | "rxjs": "~7.8.0",
26 | "tslib": "^2.3.0",
27 | "zone.js": "~0.15.0"
28 | },
29 | "devDependencies": {
30 | "@angular-devkit/build-angular": "^19.0.0",
31 | "@angular/cli": "^19.0.0",
32 | "@angular/compiler-cli": "^19.0.0",
33 | "@types/express": "^4.17.17",
34 | "@types/jasmine": "~5.1.0",
35 | "@types/node": "^18.18.0",
36 | "jasmine-core": "~5.4.0",
37 | "karma": "~6.4.0",
38 | "karma-chrome-launcher": "~3.2.0",
39 | "karma-coverage": "~2.2.0",
40 | "karma-jasmine": "~5.1.0",
41 | "karma-jasmine-html-reporter": "~2.1.0",
42 | "typescript": "~5.6.2"
43 | }
44 | }
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gentleman-Programming/angular-signals-reactive-form/73d0ad1aa58ece2893f20961020686d40ff6daf9/public/favicon.ico
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 | @let items = form.controls.items.controls;
3 |
4 |
5 | @for (formGroup of items; track formGroup.controls.id.value) {
6 |
7 | }
8 |
9 |
Total value: {{ totalValue() }}
10 |
11 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gentleman-Programming/angular-signals-reactive-form/73d0ad1aa58ece2893f20961020686d40ff6daf9/src/app/app.component.scss
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, computed, inject } from '@angular/core';
2 | import {
3 | FormArray,
4 | FormControl,
5 | FormGroup,
6 | NonNullableFormBuilder,
7 | ReactiveFormsModule,
8 | Validators,
9 | } from '@angular/forms';
10 | import { toSignal } from '@angular/core/rxjs-interop';
11 | import { FormChildComponent } from './form-child/form-child.component';
12 |
13 | export interface ItemForm {
14 | id: FormControl;
15 | name: FormControl;
16 | value: FormControl;
17 | }
18 |
19 | export type CustomFormGroup = FormGroup;
20 |
21 | @Component({
22 | selector: 'app-root',
23 | imports: [ReactiveFormsModule, FormChildComponent],
24 | templateUrl: './app.component.html',
25 | styleUrls: ['./app.component.scss'],
26 | })
27 | export class AppComponent {
28 | // Injecting NonNullableFormBuilder service
29 | fb = inject(NonNullableFormBuilder);
30 |
31 | // Initializing the form with an array of custom form groups
32 | form: FormGroup<{ items: FormArray }> = this.fb.group({
33 | items: this.fb.array([]),
34 | });
35 |
36 | // Getter for the items form array
37 | get items() {
38 | return this.form.controls.items;
39 | }
40 |
41 | // Converting form value changes to a signal
42 | itemChanges = toSignal(this.form.valueChanges);
43 |
44 | // Computed property to calculate the total value of all items
45 | totalValue = computed(() => {
46 | const value = this.itemChanges()?.items?.reduce(
47 | (total, item) => total + (Number(item?.value) || 0),
48 | 0,
49 | );
50 | console.log('computing total value: ', value);
51 | return value;
52 | });
53 |
54 | // Method to add a new item to the form array
55 | addItem() {
56 | const id = this.items.length + 1;
57 | const itemForm = this.fb.group({
58 | id: this.fb.control(id),
59 | name: this.fb.control('', { validators: [Validators.required] }),
60 | value: this.fb.control(0, { validators: [Validators.required] }),
61 | });
62 |
63 | this.form.controls.items.push(itemForm);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/app/app.config.server.ts:
--------------------------------------------------------------------------------
1 | import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
2 | import { provideServerRendering } from '@angular/platform-server';
3 | import { provideServerRoutesConfig } from '@angular/ssr';
4 | import { appConfig } from './app.config';
5 | import { serverRoutes } from './app.routes.server';
6 |
7 | const serverConfig: ApplicationConfig = {
8 | providers: [
9 | provideServerRendering(),
10 | provideServerRoutesConfig(serverRoutes)
11 | ]
12 | };
13 |
14 | export const config = mergeApplicationConfig(appConfig, serverConfig);
15 |
--------------------------------------------------------------------------------
/src/app/app.config.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
2 | import { provideRouter } from '@angular/router';
3 |
4 | import { routes } from './app.routes';
5 | import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
6 |
7 | export const appConfig: ApplicationConfig = {
8 | providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideClientHydration(withEventReplay())]
9 | };
10 |
--------------------------------------------------------------------------------
/src/app/app.routes.server.ts:
--------------------------------------------------------------------------------
1 | import { RenderMode, ServerRoute } from '@angular/ssr';
2 |
3 | export const serverRoutes: ServerRoute[] = [
4 | {
5 | path: '**',
6 | renderMode: RenderMode.Prerender
7 | }
8 | ];
9 |
--------------------------------------------------------------------------------
/src/app/app.routes.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@angular/router';
2 |
3 | export const routes: Routes = [];
4 |
--------------------------------------------------------------------------------
/src/app/custom-input/custom-input.component.html:
--------------------------------------------------------------------------------
1 | @let localControl = control();
2 |
3 |
4 |
5 | @if (localControl.invalid && (localControl.dirty || localControl.touched)) {
6 |
7 | @if (localControl.errors?.["required"]) {
8 | This Field Is Required
9 | }
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/custom-input/custom-input.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gentleman-Programming/angular-signals-reactive-form/73d0ad1aa58ece2893f20961020686d40ff6daf9/src/app/custom-input/custom-input.component.scss
--------------------------------------------------------------------------------
/src/app/custom-input/custom-input.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, forwardRef, input } from '@angular/core';
2 | import {
3 | ControlValueAccessor,
4 | FormControl,
5 | NG_VALUE_ACCESSOR,
6 | ReactiveFormsModule,
7 | } from '@angular/forms';
8 |
9 | @Component({
10 | selector: 'app-custom-input',
11 | imports: [ReactiveFormsModule],
12 | templateUrl: './custom-input.component.html',
13 | styleUrl: './custom-input.component.scss',
14 | providers: [
15 | {
16 | provide: NG_VALUE_ACCESSOR,
17 | useExisting: forwardRef(() => CustomInputComponent),
18 | multi: true,
19 | },
20 | ],
21 | })
22 | export class CustomInputComponent implements ControlValueAccessor {
23 | // Form control for the custom input
24 | control = input.required>();
25 |
26 | // Placeholder for the onTouched callback
27 | onTouched = () => {};
28 | // Placeholder for the onChange callback
29 | onChange = (_value: any) => {};
30 |
31 | // Writes a new value to the element
32 | writeValue(value: any): void {
33 | if (value !== this.control().value) {
34 | this.control().setValue(value, { emitEvent: false });
35 | }
36 | }
37 |
38 | // Registers a callback function that should be called when the control's value changes in the UI
39 | registerOnChange(fn: any): void {
40 | this.onChange = fn;
41 | }
42 |
43 | // Registers a callback function that should be called when the control is touched
44 | registerOnTouched(fn: any): void {
45 | this.onTouched = fn;
46 | }
47 |
48 | // Sets the "disabled" property on the input element
49 | setDisabledState(isDisabled: boolean): void {
50 | isDisabled ? this.control().disable() : this.control().enable();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/app/form-child/form-child.component.html:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/src/app/form-child/form-child.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Gentleman-Programming/angular-signals-reactive-form/73d0ad1aa58ece2893f20961020686d40ff6daf9/src/app/form-child/form-child.component.scss
--------------------------------------------------------------------------------
/src/app/form-child/form-child.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, input } from '@angular/core';
2 | import { FormGroup, ReactiveFormsModule } from '@angular/forms';
3 | import { ItemForm } from '../app.component';
4 | import { CustomInputComponent } from '../custom-input/custom-input.component';
5 |
6 | @Component({
7 | selector: 'app-form-child',
8 | imports: [ReactiveFormsModule, CustomInputComponent],
9 | templateUrl: './form-child.component.html',
10 | styleUrl: './form-child.component.scss',
11 | })
12 | export class FormChildComponent {
13 | formGroup = input.required>();
14 | }
15 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AngularForm
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/main.server.ts:
--------------------------------------------------------------------------------
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;
8 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
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));
7 |
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AngularNodeAppEngine,
3 | createNodeRequestHandler,
4 | isMainModule,
5 | writeResponseToNodeResponse,
6 | } from '@angular/ssr/node';
7 | import express from 'express';
8 | import { dirname, resolve } from 'node:path';
9 | import { fileURLToPath } from 'node:url';
10 |
11 | const serverDistFolder = dirname(fileURLToPath(import.meta.url));
12 | const browserDistFolder = resolve(serverDistFolder, '../browser');
13 |
14 | const app = express();
15 | const angularApp = new AngularNodeAppEngine();
16 |
17 | /**
18 | * Example Express Rest API endpoints can be defined here.
19 | * Uncomment and define endpoints as necessary.
20 | *
21 | * Example:
22 | * ```ts
23 | * app.get('/api/**', (req, res) => {
24 | * // Handle API request
25 | * });
26 | * ```
27 | */
28 |
29 | /**
30 | * Serve static files from /browser
31 | */
32 | app.use(
33 | express.static(browserDistFolder, {
34 | maxAge: '1y',
35 | index: false,
36 | redirect: false,
37 | }),
38 | );
39 |
40 | /**
41 | * Handle all other requests by rendering the Angular application.
42 | */
43 | app.use('/**', (req, res, next) => {
44 | angularApp
45 | .handle(req)
46 | .then((response) =>
47 | response ? writeResponseToNodeResponse(response, res) : next(),
48 | )
49 | .catch(next);
50 | });
51 |
52 | /**
53 | * Start the server if this module is the main entry point.
54 | * The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
55 | */
56 | if (isMainModule(import.meta.url)) {
57 | const port = process.env['PORT'] || 4000;
58 | app.listen(port, () => {
59 | console.log(`Node Express server listening on http://localhost:${port}`);
60 | });
61 | }
62 |
63 | /**
64 | * The request handler used by the Angular CLI (dev-server and during build).
65 | */
66 | export const reqHandler = createNodeRequestHandler(app);
67 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
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"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "compileOnSave": false,
5 | "compilerOptions": {
6 | "outDir": "./dist/out-tsc",
7 | "strict": true,
8 | "noImplicitOverride": true,
9 | "noPropertyAccessFromIndexSignature": true,
10 | "noImplicitReturns": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "skipLibCheck": true,
13 | "isolatedModules": true,
14 | "esModuleInterop": true,
15 | "experimentalDecorators": true,
16 | "moduleResolution": "bundler",
17 | "importHelpers": true,
18 | "target": "ES2022",
19 | "module": "ES2022"
20 | },
21 | "angularCompilerOptions": {
22 | "enableI18nLegacyMessageIdFormat": false,
23 | "strictInjectionParameters": true,
24 | "strictInputAccessModifiers": true,
25 | "strictTemplates": true
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
2 | /* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
3 | {
4 | "extends": "./tsconfig.json",
5 | "compilerOptions": {
6 | "outDir": "./out-tsc/spec",
7 | "types": [
8 | "jasmine"
9 | ]
10 | },
11 | "include": [
12 | "src/**/*.spec.ts",
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------