├── .gitignore ├── README.md ├── angular.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── components │ │ ├── dynamic-field │ │ │ ├── dynamic-checkboxs │ │ │ │ ├── dynamic-checkboxs.component.css │ │ │ │ ├── dynamic-checkboxs.component.html │ │ │ │ ├── dynamic-checkboxs.component.spec.ts │ │ │ │ └── dynamic-checkboxs.component.ts │ │ │ ├── dynamic-field.component.css │ │ │ ├── dynamic-field.component.html │ │ │ ├── dynamic-field.component.ts │ │ │ ├── dynamic-input │ │ │ │ ├── dynamic-input.component.css │ │ │ │ ├── dynamic-input.component.html │ │ │ │ ├── dynamic-input.component.spec.ts │ │ │ │ └── dynamic-input.component.ts │ │ │ ├── dynamic-radio │ │ │ │ ├── dynamic-radio.component.css │ │ │ │ ├── dynamic-radio.component.html │ │ │ │ ├── dynamic-radio.component.spec.ts │ │ │ │ └── dynamic-radio.component.ts │ │ │ └── dynamic-select │ │ │ │ ├── dynamic-select.component.css │ │ │ │ ├── dynamic-select.component.html │ │ │ │ ├── dynamic-select.component.spec.ts │ │ │ │ └── dynamic-select.component.ts │ │ └── dynamic-form │ │ │ ├── dynamic-error │ │ │ ├── dynamic-error.component.css │ │ │ ├── dynamic-error.component.html │ │ │ ├── dynamic-error.component.spec.ts │ │ │ └── dynamic-error.component.ts │ │ │ ├── dynamic-form.component.css │ │ │ ├── dynamic-form.component.html │ │ │ ├── dynamic-form.component.spec.ts │ │ │ └── dynamic-form.component.ts │ ├── hello.component.ts │ └── services │ │ └── message.service.ts ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.css ├── tsconfig.app.json └── tsconfig.spec.json └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .angular/ 3 | .idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # How To Create Dynamic Forms in Angular 2 | 3 | This code part of the my article [Creating Dynamic Forms in Angular: A Step-by-Step Guide](https://www.danywalls.com/creating-dynamic-forms-in-angular-a-step-by-step-guide), on the guide, I explain how to create a dynamic form using angular and reactive forms, start from simple scenario to other case suggested by the community. 4 | 5 | I want to say thanks to everyone to vote a start and help to improve this article to help other developers to solve a common task for every Angular developer. 6 | 7 | Feel free to checkout the code or run in StackBlitz. 8 | 9 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/danywalls/dynamic-forms) 10 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "demo": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/demo", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css" 27 | ], 28 | "scripts": [] 29 | }, 30 | "configurations": { 31 | "production": { 32 | "fileReplacements": [ 33 | { 34 | "replace": "src/environments/environment.ts", 35 | "with": "src/environments/environment.prod.ts" 36 | } 37 | ], 38 | "optimization": true, 39 | "outputHashing": "all", 40 | "sourceMap": false, 41 | "extractCss": true, 42 | "namedChunks": false, 43 | "aot": true, 44 | "extractLicenses": true, 45 | "vendorChunk": false, 46 | "buildOptimizer": true 47 | }, 48 | "development": { 49 | "buildOptimizer": false, 50 | "optimization": false, 51 | "vendorChunk": true, 52 | "extractLicenses": false, 53 | "sourceMap": true, 54 | "namedChunks": true 55 | } 56 | }, 57 | "defaultConfiguration": "development" 58 | }, 59 | "serve": { 60 | "builder": "@angular-devkit/build-angular:dev-server", 61 | "options": { 62 | "browserTarget": "demo:build" 63 | }, 64 | "configurations": { 65 | "production": { 66 | "browserTarget": "demo:build:production" 67 | }, 68 | "development": { 69 | "browserTarget": "demo:build:development" 70 | } 71 | } 72 | }, 73 | "extract-i18n": { 74 | "builder": "@angular-devkit/build-angular:extract-i18n", 75 | "options": { 76 | "browserTarget": "demo:build" 77 | } 78 | }, 79 | "test": { 80 | "builder": "@angular-devkit/build-angular:karma", 81 | "options": { 82 | "main": "src/test.ts", 83 | "polyfills": "src/polyfills.ts", 84 | "tsConfig": "src/tsconfig.spec.json", 85 | "karmaConfig": "src/karma.conf.js", 86 | "styles": [ 87 | "styles.css" 88 | ], 89 | "scripts": [], 90 | "assets": [ 91 | "src/favicon.ico", 92 | "src/assets" 93 | ] 94 | } 95 | }, 96 | "lint": { 97 | "builder": "@angular-devkit/build-angular:tslint", 98 | "options": { 99 | "tsConfig": [ 100 | "src/tsconfig.app.json", 101 | "src/tsconfig.spec.json" 102 | ], 103 | "exclude": [ 104 | "**/node_modules/**" 105 | ] 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "dependencies": { 14 | "@angular/animations": "^16.2.12", 15 | "@angular/common": "^16.2.12", 16 | "@angular/compiler": "^16.2.12", 17 | "@angular/core": "^16.2.12", 18 | "@angular/forms": "^16.2.12", 19 | "@angular/platform-browser": "^16.2.12", 20 | "@angular/platform-browser-dynamic": "^16.2.12", 21 | "@angular/router": "^16.2.12", 22 | "rxjs": "~7.5.0", 23 | "tslib": "^2.3.0", 24 | "zone.js": "~0.13.3" 25 | }, 26 | "devDependencies": { 27 | "@angular-devkit/build-angular": "^16.2.12", 28 | "@angular/cli": "~16.2.12", 29 | "@angular/compiler-cli": "^16.2.12", 30 | "@types/jasmine": "~4.0.0", 31 | "jasmine-core": "~4.1.0", 32 | "karma": "~6.3.0", 33 | "karma-chrome-launcher": "~3.1.0", 34 | "karma-coverage": "~2.2.0", 35 | "karma-jasmine": "~5.0.0", 36 | "karma-jasmine-html-reporter": "~1.7.0", 37 | "typescript": "~4.9.5" 38 | } 39 | } -------------------------------------------------------------------------------- /src/app/app.component.css: -------------------------------------------------------------------------------- 1 | form { 2 | display: flex; 3 | flex-direction: column; 4 | gap: 1rem; 5 | } 6 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

Signup form

2 | 3 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "@angular/core"; 2 | 3 | @Component({ 4 | selector: "my-app", 5 | templateUrl: "./app.component.html", 6 | styleUrls: ["./app.component.css"], 7 | }) 8 | export class AppComponent { 9 | model = { 10 | firstname: { 11 | type: "text", 12 | value: "", 13 | label: "FirstName", 14 | rules: { 15 | required: true, 16 | } 17 | }, 18 | lastname: { 19 | type: "text", 20 | value: "", 21 | label: "LastName" 22 | }, 23 | address: { 24 | type: "text", 25 | value: "", 26 | label: "Address", 27 | }, 28 | age: { 29 | type: "number", 30 | value: "", 31 | label: "age" 32 | }, 33 | birthDay: { 34 | type: "date", 35 | value: "", 36 | label: "Birthday", 37 | }, 38 | typeBussines: { 39 | label: "Bussines Type", 40 | value: "premium", 41 | type: "radio", 42 | options: [ 43 | { 44 | label: "Enterprise", 45 | value: "1500", 46 | }, 47 | { 48 | label: "Home", 49 | value: "6", 50 | }, 51 | { 52 | label: "Personal", 53 | value: "1", 54 | }, 55 | ], 56 | 57 | }, 58 | newsletterIn: { 59 | label: "Suscribe to newsletter", 60 | value: "email", 61 | type: "checkbox", 62 | }, 63 | subscriptionType: { 64 | label: "Suscription Type", 65 | value: "premium", 66 | type: "select", 67 | options: [ 68 | { 69 | label: "Pick one", 70 | value: "", 71 | }, 72 | { 73 | label: "Premium", 74 | value: "premium", 75 | }, 76 | { 77 | label: "Basic", 78 | value: "basic", 79 | }, 80 | ], 81 | }, 82 | country: { 83 | id: 'country', 84 | label: "Country", 85 | type: "select", 86 | options: [ 87 | { 88 | label: "Spain", 89 | value: "ES" 90 | }, 91 | { 92 | label: "USA", 93 | value: "US" 94 | } 95 | ], 96 | provideData: [ 97 | { 98 | label: 'Barcelona', 99 | sourceValue: 'ES', 100 | value: 'BCN' 101 | }, 102 | { 103 | label: 'Madrid', 104 | sourceValue: 'ES', 105 | value: 'MDN' 106 | }, 107 | { 108 | label: 'New York', 109 | sourceValue: 'US', 110 | value: 'NYC' 111 | }, 112 | { 113 | label: 'Cleveland', 114 | sourceValue: 'CLV', 115 | value: 'E' 116 | } 117 | ] 118 | 119 | }, 120 | city: { 121 | label: "City", 122 | type: "select", 123 | link: 'country', 124 | value: "", 125 | options: [ 126 | { 127 | label: "Select Country First", 128 | value: "" 129 | } 130 | ] 131 | } 132 | }; 133 | } 134 | 135 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from "@angular/core"; 2 | import { BrowserModule } from "@angular/platform-browser"; 3 | import { FormsModule, ReactiveFormsModule } from "@angular/forms"; 4 | 5 | import { AppComponent } from "./app.component"; 6 | import { HelloComponent } from "./hello.component"; 7 | 8 | import { DynamicFieldComponent } from "./components/dynamic-field/dynamic-field.component"; 9 | import { DynamicFormComponent } from "./components/dynamic-form/dynamic-form.component"; 10 | import { DynamicInputComponent } from "./components/dynamic-field/dynamic-input/dynamic-input.component"; 11 | import { DynamicSelectComponent } from "./components/dynamic-field/dynamic-select/dynamic-select.component"; 12 | import { DynamicRadioComponent } from "./components/dynamic-field/dynamic-radio/dynamic-radio.component"; 13 | import { DynamicCheckboxsComponent } from "./components/dynamic-field/dynamic-checkboxs/dynamic-checkboxs.component"; 14 | import { DynamicErrorComponent } from './components/dynamic-form/dynamic-error/dynamic-error.component'; 15 | 16 | @NgModule({ 17 | imports: [BrowserModule, FormsModule, ReactiveFormsModule], 18 | declarations: [ 19 | AppComponent, 20 | HelloComponent, 21 | 22 | DynamicFieldComponent, 23 | DynamicFormComponent, 24 | DynamicInputComponent, 25 | DynamicSelectComponent, 26 | DynamicRadioComponent, 27 | DynamicCheckboxsComponent, 28 | DynamicErrorComponent, 29 | ], 30 | bootstrap: [AppComponent], 31 | }) 32 | export class AppModule {} 33 | -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-checkboxs/dynamic-checkboxs.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danywalls/dynamic-forms/3ec9242c2da25aaae1a1f44610ec1d6d5b82e6e6/src/app/components/dynamic-field/dynamic-checkboxs/dynamic-checkboxs.component.css -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-checkboxs/dynamic-checkboxs.component.html: -------------------------------------------------------------------------------- 1 |
2 | 11 |
12 | -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-checkboxs/dynamic-checkboxs.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DynamicCheckboxsComponent } from './dynamic-checkboxs.component'; 4 | 5 | describe('DynamicCheckboxsComponent', () => { 6 | let component: DynamicCheckboxsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ DynamicCheckboxsComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DynamicCheckboxsComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-checkboxs/dynamic-checkboxs.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from "@angular/core"; 2 | import { FormGroup, FormGroupDirective } from "@angular/forms"; 3 | 4 | @Component({ 5 | selector: "app-dynamic-checkboxs", 6 | templateUrl: "./dynamic-checkboxs.component.html", 7 | styleUrls: ["./dynamic-checkboxs.component.css"], 8 | }) 9 | export class DynamicCheckboxsComponent { 10 | @Input() field: any; 11 | formName: FormGroup; 12 | 13 | constructor(private formgroupDirective: FormGroupDirective) { 14 | this.formName = formgroupDirective.control; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-field.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danywalls/dynamic-forms/3ec9242c2da25aaae1a1f44610ec1d6d5b82e6e6/src/app/components/dynamic-field/dynamic-field.component.css -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-field.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-field.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AfterViewInit, 3 | ChangeDetectorRef, 4 | Component, 5 | Input, 6 | ViewChild, 7 | ViewContainerRef 8 | } from "@angular/core"; 9 | import {FormGroup} from "@angular/forms"; 10 | import {DynamicInputComponent} from "./dynamic-input/dynamic-input.component"; 11 | import {DynamicSelectComponent} from "./dynamic-select/dynamic-select.component"; 12 | import {DynamicRadioComponent} from "./dynamic-radio/dynamic-radio.component"; 13 | import {DynamicCheckboxsComponent} from "./dynamic-checkboxs/dynamic-checkboxs.component"; 14 | 15 | @Component({ 16 | selector: "app-field-input", 17 | templateUrl: "./dynamic-field.component.html", 18 | styleUrls: ["./dynamic-field.component.css"], 19 | }) 20 | export class DynamicFieldComponent implements AfterViewInit{ 21 | 22 | supportedDynamicComponents = [ 23 | { 24 | name: 'text', 25 | component: DynamicInputComponent 26 | }, 27 | { 28 | name: 'number', 29 | component: DynamicInputComponent 30 | }, 31 | { 32 | name: 'select', 33 | component: DynamicSelectComponent 34 | }, 35 | { 36 | name: 'radio', 37 | component: DynamicRadioComponent 38 | }, 39 | { 40 | name: 'date', 41 | component: DynamicInputComponent 42 | }, 43 | { 44 | name: 'checkbox', 45 | component: DynamicCheckboxsComponent 46 | } 47 | ] 48 | @ViewChild('dynamicInputContainer', { read: ViewContainerRef}) dynamicInputContainer!: ViewContainerRef; 49 | @Input() field: any; 50 | formName: FormGroup; 51 | 52 | constructor(private cd: ChangeDetectorRef) { 53 | 54 | } 55 | 56 | ngAfterViewInit(): void { 57 | this.registerDynamicField(); 58 | } 59 | 60 | private registerDynamicField() { 61 | this.dynamicInputContainer.clear(); 62 | const componentInstance = this.getComponentByType(this.field.type) 63 | const dynamicComponent = this.dynamicInputContainer.createComponent(componentInstance) 64 | dynamicComponent.setInput('field', this.field); 65 | this.cd.detectChanges(); 66 | } 67 | 68 | getComponentByType(type: string): any { 69 | let componentDynamic = this.supportedDynamicComponents.find(c => c.name === type); 70 | return componentDynamic.component || DynamicInputComponent; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-input/dynamic-input.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danywalls/dynamic-forms/3ec9242c2da25aaae1a1f44610ec1d6d5b82e6e6/src/app/components/dynamic-field/dynamic-input/dynamic-input.component.css -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-input/dynamic-input.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
-------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-input/dynamic-input.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DynamicInputComponent } from './dynamic-input.component'; 4 | 5 | describe('DynamicInputComponent', () => { 6 | let component: DynamicInputComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ DynamicInputComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DynamicInputComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-input/dynamic-input.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from "@angular/core"; 2 | import { FormGroup, FormGroupDirective } from "@angular/forms"; 3 | 4 | @Component({ 5 | selector: "app-dynamic-input", 6 | templateUrl: "./dynamic-input.component.html", 7 | styleUrls: ["./dynamic-input.component.css"], 8 | }) 9 | export class DynamicInputComponent { 10 | @Input() field: any; 11 | formName: FormGroup; 12 | 13 | constructor(private formgroupDirective: FormGroupDirective) { 14 | this.formName = formgroupDirective.control; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-radio/dynamic-radio.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danywalls/dynamic-forms/3ec9242c2da25aaae1a1f44610ec1d6d5b82e6e6/src/app/components/dynamic-field/dynamic-radio/dynamic-radio.component.css -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-radio/dynamic-radio.component.html: -------------------------------------------------------------------------------- 1 |
2 |

{{field.label}}

3 | 14 |
-------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-radio/dynamic-radio.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DynamicRadioComponent } from './dynamic-radio.component'; 4 | 5 | describe('DynamicRadioComponent', () => { 6 | let component: DynamicRadioComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ DynamicRadioComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DynamicRadioComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-radio/dynamic-radio.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from "@angular/core"; 2 | import { FormGroup, FormGroupDirective } from "@angular/forms"; 3 | 4 | @Component({ 5 | selector: "app-dynamic-radio", 6 | templateUrl: "./dynamic-radio.component.html", 7 | styleUrls: ["./dynamic-radio.component.css"], 8 | }) 9 | export class DynamicRadioComponent { 10 | @Input() field: any; 11 | formName: FormGroup; 12 | 13 | constructor(private formgroupDirective: FormGroupDirective) { 14 | this.formName = formgroupDirective.control; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-select/dynamic-select.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danywalls/dynamic-forms/3ec9242c2da25aaae1a1f44610ec1d6d5b82e6e6/src/app/components/dynamic-field/dynamic-select/dynamic-select.component.css -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-select/dynamic-select.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 13 |
14 | -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-select/dynamic-select.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DynamicSelectComponent } from './dynamic-select.component'; 4 | 5 | describe('DynamicSelectComponent', () => { 6 | let component: DynamicSelectComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ DynamicSelectComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DynamicSelectComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/components/dynamic-field/dynamic-select/dynamic-select.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, Input } from "@angular/core"; 2 | import { FormGroup, FormGroupDirective } from "@angular/forms"; 3 | import { tap, filter, iif, takeWhile } from "rxjs"; 4 | import { MessageService } from "../../../services/message.service"; 5 | 6 | 7 | @Component({ 8 | selector: "app-dynamic-select", 9 | templateUrl: "./dynamic-select.component.html", 10 | styleUrls: ["./dynamic-select.component.css"], 11 | }) 12 | export class DynamicSelectComponent implements AfterViewInit { 13 | @Input() field: any; 14 | formName: FormGroup; 15 | alive = true; 16 | 17 | constructor( 18 | private messageService: MessageService, 19 | private formGroupDirective: FormGroupDirective) { 20 | this.formName = formGroupDirective.control; 21 | } 22 | 23 | 24 | ngAfterViewInit(): void { 25 | this.listenForLinkData(); 26 | } 27 | 28 | ngOnDestroy() { 29 | this.alive = false; 30 | } 31 | 32 | listenForLinkData() { 33 | if (!this.field?.link) { 34 | return; 35 | } 36 | this.messageService.message$.pipe( 37 | filter(v => v.link === this.field.link), 38 | takeWhile(() => this.alive) 39 | ).subscribe((v) => { 40 | this.field.options = v.data 41 | }) 42 | } 43 | 44 | changedValue(value: string) { 45 | if (!this.field.provideData) { 46 | return; 47 | } 48 | this.messageService.messageSubject.next({ 49 | link: this.field.fieldName, 50 | data: this.field.provideData.filter(v => v.sourceValue === value) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/components/dynamic-form/dynamic-error/dynamic-error.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danywalls/dynamic-forms/3ec9242c2da25aaae1a1f44610ec1d6d5b82e6e6/src/app/components/dynamic-form/dynamic-error/dynamic-error.component.css -------------------------------------------------------------------------------- /src/app/components/dynamic-form/dynamic-error/dynamic-error.component.html: -------------------------------------------------------------------------------- 1 |
3 |
4 | * {{fieldName}} 5 |
6 |
-------------------------------------------------------------------------------- /src/app/components/dynamic-form/dynamic-error/dynamic-error.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DynamicErrorComponent } from './dynamic-error.component'; 4 | 5 | describe('DynamicErrorComponent', () => { 6 | let component: DynamicErrorComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ DynamicErrorComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DynamicErrorComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/components/dynamic-form/dynamic-error/dynamic-error.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, OnInit } from "@angular/core"; 2 | import { FormGroup, FormGroupDirective } from "@angular/forms"; 3 | 4 | @Component({ 5 | selector: "app-dynamic-error", 6 | templateUrl: "./dynamic-error.component.html", 7 | styleUrls: ["./dynamic-error.component.css"], 8 | }) 9 | export class DynamicErrorComponent implements OnInit { 10 | formName: FormGroup; 11 | @Input() fieldName: string; 12 | 13 | constructor(private formgroupDirective: FormGroupDirective) {} 14 | 15 | ngOnInit() { 16 | this.formName = this.formgroupDirective.control; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/components/dynamic-form/dynamic-form.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danywalls/dynamic-forms/3ec9242c2da25aaae1a1f44610ec1d6d5b82e6e6/src/app/components/dynamic-form/dynamic-form.component.css -------------------------------------------------------------------------------- /src/app/components/dynamic-form/dynamic-form.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /src/app/components/dynamic-form/dynamic-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DynamicFormComponent } from './dynamic-form.component'; 4 | 5 | describe('DynamicFormComponent', () => { 6 | let component: DynamicFormComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ DynamicFormComponent ] 12 | }) 13 | .compileComponents(); 14 | 15 | fixture = TestBed.createComponent(DynamicFormComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/components/dynamic-form/dynamic-form.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit} from "@angular/core"; 2 | import {FormControl, FormGroup, Validators} from "@angular/forms"; 3 | 4 | @Component({ 5 | selector: "app-dynamic-form", 6 | templateUrl: "./dynamic-form.component.html", 7 | styleUrls: ["./dynamic-form.component.css"], 8 | }) 9 | export class DynamicFormComponent implements OnInit { 10 | @Input() model: {}; 11 | public dynamicFormGroup: FormGroup; 12 | public fields = []; 13 | 14 | ngOnInit() { 15 | this.buildForm(); 16 | } 17 | 18 | private buildForm() { 19 | const formGroupFields = this.getFormControlsFields(); 20 | this.dynamicFormGroup = new FormGroup(formGroupFields); 21 | } 22 | 23 | private getFormControlsFields() { 24 | const formGroupFields = {}; 25 | for (const field of Object.keys(this.model)) { 26 | 27 | const fieldProps = this.model[field]; 28 | const validators = this.addValidator(fieldProps.rules); 29 | 30 | formGroupFields[field] = new FormControl(fieldProps.value, validators); 31 | this.fields.push({...fieldProps, fieldName: field}); 32 | } 33 | 34 | return formGroupFields; 35 | } 36 | 37 | private addValidator(rules) { 38 | if (!rules) { 39 | return []; 40 | } 41 | 42 | const validators = Object.keys(rules).map((rule) => { 43 | switch (rule) { 44 | case "required": 45 | return Validators.required; 46 | //add more case for future. 47 | } 48 | }); 49 | return validators; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/hello.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'hello', 5 | template: `

Hello {{name}}!

`, 6 | styles: [`h1 { font-family: Lato; }`] 7 | }) 8 | export class HelloComponent { 9 | @Input() name: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/app/services/message.service.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs'; 2 | import { Injectable } from '@angular/core'; 3 | 4 | 5 | @Injectable({ providedIn: 'root' }) 6 | export class MessageService { 7 | public messageSubject = new Subject(); 8 | public message$ = this.messageSubject.asObservable() 9 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Angular App 4 | 5 | 6 | loading 7 | 8 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/my-app'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './polyfills'; 2 | 3 | import { enableProdMode } from '@angular/core'; 4 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 5 | 6 | import { AppModule } from './app/app.module'; 7 | 8 | platformBrowserDynamic().bootstrapModule(AppModule).then(ref => { 9 | // Ensure Angular destroys itself on hot reloads. 10 | if (window['ngRef']) { 11 | window['ngRef'].destroy(); 12 | } 13 | window['ngRef'] = ref; 14 | 15 | // Otherwise, log the boot error 16 | }).catch(err => console.error(err)); -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/set'; 35 | 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | 39 | /** IE10 and IE11 requires the following to support `@angular/animation`. */ 40 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 41 | 42 | 43 | /** Evergreen browsers require these. **/ 44 | // import 'core-js/es6/reflect'; 45 | // import 'core-js/es7/reflect'; 46 | 47 | 48 | /** 49 | * Web Animations `@angular/platform-browser/animations` 50 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 51 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 52 | */ 53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | 56 | 57 | /*************************************************************************************************** 58 | * Zone JS is required by Angular itself. 59 | */ 60 | import 'zone.js/dist/zone'; // Included with Angular CLI. 61 | 62 | 63 | /*************************************************************************************************** 64 | * APPLICATION IMPORTS 65 | */ 66 | 67 | /** 68 | * Date, currency, decimal and percent pipes. 69 | * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 70 | */ 71 | // import 'intl'; // Run `npm install --save intl`. -------------------------------------------------------------------------------- /src/styles.css: -------------------------------------------------------------------------------- 1 | /* Add application styles & imports to this file! */ -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "main.ts", 9 | "polyfills.ts" 10 | ], 11 | "include": [ 12 | "**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | }, 22 | "angularCompilerOptions": { 23 | "enableIvy": true, 24 | "fullTemplateTypeCheck": true, 25 | "strictInjectionParameters": true 26 | } 27 | } --------------------------------------------------------------------------------