├── .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 | [](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 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
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 |
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 | }
--------------------------------------------------------------------------------