├── .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 |
73 | 74 | 75 |
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 |
2 | 6 | 10 |
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 | --------------------------------------------------------------------------------