Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,42 +1,60 @@
<div [formGroup]="form">
<label [attr.for]="question.key">{{ question.label }}</label>

<div [ngSwitch]="question.controlType">

<input *ngSwitchCase="'textbox'"
[class]="isValid ? config.validClass : config.invalidClass"
[formControlName]="question.key"
[placeholder]="question.placeholder"
[id]="question.key"
[type]="question['type']">

<select [id]="question.key"
*ngSwitchCase="'dropdown'"
[class]="isValid ? config.validClass : config.invalidClass"
[formControlName]="question.key">
<option value=""
disabled
*ngIf="!!question.placeholder"
selected>{{ question.placeholder }}</option>
<option *ngFor="let opt of question['options']"
[value]="opt.key">{{ opt.value }}</option>
</select>

<textarea *ngSwitchCase="'textarea'"
[formControlName]="question.key"
[id]="question.key"
[class]="isValid ? config.validClass : config.invalidClass"
[cols]="question['cols']"
[rows]="question['rows']"
[maxlength]="question['maxlength']"
[minlength]="question['minlength']"
[placeholder]="question.placeholder"></textarea>

<button *ngIf="question.iterable"
type="button"
(click)="addItem(question)">+</button>

<ng-template #formTmpl
let-index="index">
<label [attr.for]="questionId(index)">{{ questionLabel(index) }}</label>

<div [ngSwitch]="question.controlType">

<input *ngSwitchCase="'textbox'"
[formControl]="questionControl(index)"
[placeholder]="question.placeholder"
[attr.min]="question['min']"
[attr.max]="question['max']"
[attr.pattern]="question['pattern']"
[id]="questionId(index)"
[type]="question['type']">

<select [id]="question.key"
*ngSwitchCase="'dropdown'"
[formControl]="questionControl(index)">
<option value=""
disabled
*ngIf="!!question.placeholder"
selected>{{ question.placeholder }}</option>
<option *ngFor="let opt of question['options']"
[value]="opt.key">{{ opt.value }}</option>
</select>

<textarea *ngSwitchCase="'textarea'"
[formControl]="questionControl(index)"
[id]="question.key"
[cols]="question['cols']"
[rows]="question['rows']"
[maxlength]="question['maxlength']"
[minlength]="question['minlength']"
[placeholder]="question.placeholder"></textarea>
</div>

<div class="errorMessage"
*ngIf="!isValid">{{ question.label }} is required</div>

</ng-template>

<div *ngIf="question.iterable; else formTmpl">
<div *ngFor="let field of questionArray.controls; let i=index; first as isFirst last as isLast">
<ng-container [ngTemplateOutlet]="formTmpl"
[ngTemplateOutletContext]="{index: i}"></ng-container>

<button *ngIf="question.iterable && questionArray.controls.length > 1"
(click)="removeQuestion(i)"
type="button">-</button>

<button *ngIf="question.iterable && isLast"
(click)="addQuestion()"
type="button">+</button>

</div>
</div>

<div class="errorMessage"
*ngIf="!isValid">{{ question.label }} is required</div>
</div>
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, OnInit, Input } from '@angular/core';
import { FormGroup, FormArray, FormControl } from '@angular/forms';
import { FormGroup, FormArray, FormBuilder, AbstractControl } from '@angular/forms';

import { QuestionBase, QuestionConfig } from '@app/models';
import { QuestionBase } from '@app/models';

@Component({
selector: 'app-question',
Expand All @@ -12,22 +12,37 @@ export class DynamicFormQuestionComponent implements OnInit {

@Input() question: QuestionBase<any>;
@Input() form: FormGroup;
@Input() config: QuestionConfig;
get isValid() { return this.form.controls[this.question.key].valid; }

constructor() {
constructor(private fb: FormBuilder) { }

ngOnInit() { }

private asFormArray(ctrl: AbstractControl): FormArray {
return ctrl as FormArray;
}

public addQuestion(): void {
this.questionArray.push(this.fb.control(''));
}

ngOnInit() {
public removeQuestion(index: number): void {
this.questionArray.removeAt(index);
}

public get questionArray(): FormArray {
return this.form.get(this.question.key) as FormArray;
}

if (this.question.iterable) {
console.log(this.question);
}
public questionControl(index?: number): AbstractControl {
return this.question.iterable ? this.asFormArray(this.form.get(this.question.key)).controls[index] : this.form.get(this.question.key);
}

public addItem(question: QuestionBase<any>): void {
(<FormArray> this.form.get(question.key)).push(new FormControl(''));
console.log(question);
public questionId(index?: number): string {
return this.question.iterable ? `${this.question.key}-${index}` : this.question.key;
}

public questionLabel(index?: number): string {
return this.question.iterable ? `${this.question.label} n°${index + 1}` : this.question.label;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
<div *ngFor="let question of questions"
class="form-row">
<app-question [question]="question"
[config]="config"
[form]="form"></app-question>
</div>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Component, OnInit, Input, Inject } from '@angular/core';
import { Component, OnInit, Input } from '@angular/core';
import { FormGroup } from '@angular/forms';

import { QuestionBase, QuestionConfig } from '@app/models';
import { QuestionBase } from '@app/models';
import { QuestionControlService } from '@app/services';

@Component({
Expand All @@ -15,10 +15,7 @@ export class DynamicFormComponent implements OnInit {
form: FormGroup;
payLoad = '';

constructor(
@Inject('questionConfig') public config: QuestionConfig,
private qcs: QuestionControlService
) { }
constructor(private qcs: QuestionControlService) { }

ngOnInit() {
this.form = this.qcs.toFormGroup(this.questions);
Expand Down
28 changes: 1 addition & 27 deletions src/app/_components/common/dynamic-form/dynamic-form.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { NgModule, ModuleWithProviders } from '@angular/core';
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormsModule } from '@angular/forms';

import { DynamicFormComponent } from './dynamic-form.component';
import { DynamicFormQuestionModule } from '../dynamic-form-question/dynamic-form-question.module';

import { QuestionConfig } from '@app/models';
import { QuestionControlService } from '@app/services';

@NgModule({
Expand All @@ -20,29 +19,4 @@ import { QuestionControlService } from '@app/services';
exports: [DynamicFormComponent]
})
export class DynamicFormModule {

/**
* Use in AppModule: new instance of NgxSmartModal.
*/
public static forRoot(config?: QuestionConfig): ModuleWithProviders {
if (!config) {
config = { validClass: 'valid', invalidClass: 'invalid' };
}

return {
ngModule: DynamicFormModule,
providers: [QuestionControlService, { provide: 'questionConfig', useValue: config }]
};
}

/**
* Use in features modules with lazy loading: new instance of NgxSmartModal.
*/
public static forChild(): ModuleWithProviders {
return {
ngModule: DynamicFormModule,
providers: [QuestionControlService]
};
}

}
1 change: 0 additions & 1 deletion src/app/_models/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './question-config';
export * from './question-base';
export * from './question-dropdown';
export * from './question-textbox';
Expand Down
4 changes: 0 additions & 4 deletions src/app/_models/question-config.ts

This file was deleted.

6 changes: 6 additions & 0 deletions src/app/_models/question-textbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@ import { QuestionBase } from './question-base';
export class TextboxQuestion extends QuestionBase<string> {
controlType = 'textbox';
type: string;
min: number | string;
max: number | string;
pattern: string;

constructor(options: {} = {}) {
super(options);
this.type = options['type'] || 'text';
this.min = options['min'];
this.max = options['max'];
this.pattern = options['pattern'];
}
}
21 changes: 16 additions & 5 deletions src/app/_services/question-control.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { FormGroup, FormControl, Validators, FormArray } from '@angular/forms';

import { QuestionBase } from '@app/models';

@Injectable({
providedIn: 'root'
})
@Injectable()
export class QuestionControlService {

constructor() { }
Expand All @@ -17,8 +15,21 @@ export class QuestionControlService {

if (question.iterable) {

group[question.key] = question.required ? new FormArray([new FormControl(question.value || '')
]) : new FormArray([new FormControl(question.value || '')], Validators.required);
if (!Array.isArray(question.value)) {
question.value = !!question.value ? [question.value] : [''];
}

const tmpArray: FormArray = question.required ? new FormArray([]) : new FormArray([], Validators.required);

if (!question.value || !question.value.length) {
tmpArray.push(new FormControl(''));
} else {
question.value.forEach(val => {
tmpArray.push(new FormControl(val));
});
}

group[question.key] = tmpArray;

} else {

Expand Down
16 changes: 12 additions & 4 deletions src/app/_services/question.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ import {
TextareaQuestion
} from '@app/models';

@Injectable({
providedIn: 'root'
})
@Injectable()
export class QuestionService {

// TODO: get from a remote source of question metadata
Expand Down Expand Up @@ -42,11 +40,21 @@ export class QuestionService {
new TextboxQuestion({
key: 'jobs',
label: 'Jobs',
value: '',
value: ['toto'],
iterable: true,
order: 5
}),

new TextboxQuestion({
key: 'level',
label: 'Level',
type: 'range',
value: 70,
min: 20,
max: 200,
order: 6
}),

new TextboxQuestion({
key: 'emailAddress',
label: 'Email',
Expand Down
58 changes: 56 additions & 2 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,64 @@
<!-- Fork me SVG corner image -->
<a href="https://github.com/maximelafarie/angular-dynamic-forms"
target="_blank"
class="github-corner"
aria-label="View source on Github"
title="View source on Github">
<svg width="80"
height="80"
viewBox="0 0 250 250"
style="fill:#9b4dca; color:#fff; position: absolute; top: 0; border: 0; right: 0;"
aria-hidden="true">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
fill="currentColor"
style="transform-origin: 130px 106px;"
class="octo-arm"></path>
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
fill="currentColor"
class="octo-body"></path>
</svg>
</a>

<!-- How it's implemented... -->
<div class="container">

<div class="row">
<div class="column">
<h2>Job Application for Heroes</h2>
<app-dynamic-form [questions]="questions"></app-dynamic-form>
<h1>Job Application for Heroes</h1>
<p>Here is an example of how you can build on-the-fly forms with Angular. <code>The form</code> is the form as defined in <code>The data object</code>. You also can see the changes while you're modifying the form's values in <code>The result</code> section.</p>
</div>
</div>

<div class="row">
<div class="column">
<h2>The form</h2>
<app-dynamic-form #dynamicForm
[questions]="questions"></app-dynamic-form>
</div>
<div class="column">

<div class="row">
<div class="column">
<h2>The result</h2>
<pre><code>{{ dynamicForm.form.value | json }}</code></pre>
</div>
</div>

<div class="row">
<div class="column">
<h2>The data object</h2>
<pre><code>{{ questions | json }}</code></pre>
</div>
</div>

</div>
</div>

<!-- A little credit ;) -->
<footer class="footer">
Demo made by <a target="_blank"
href="https://github.com/maximelafarie">Maxime Lafarie</a> using Angular {{ version }}
</footer>

</div>
Loading