From abad9ab0d6154fa619bfd78ec7a0af98c86809cf Mon Sep 17 00:00:00 2001 From: cbourget Date: Wed, 13 Feb 2019 15:23:56 -0500 Subject: [PATCH] feat(form): configurable forms --- demo/src/app/app.component.html | 1 + demo/src/app/app.module.ts | 2 + .../app/common/form/form-routing.module.ts | 15 +++ demo/src/app/common/form/form.component.html | 33 ++++++ demo/src/app/common/form/form.component.scss | 12 ++ demo/src/app/common/form/form.component.ts | 83 ++++++++++++++ demo/src/app/common/form/form.module.ts | 21 ++++ .../form-field-select.component.html | 10 ++ .../form-field/form-field-select.component.ts | 56 +++++++++ .../form-field/form-field-text.component.html | 9 ++ .../form-field/form-field-text.component.ts | 39 +++++++ .../form/form-field/form-field.component.html | 9 ++ .../form/form-field/form-field.component.scss | 3 + .../form/form-field/form-field.component.ts | 44 +++++++ .../lib/form/form-field/form-field.module.ts | 42 +++++++ .../form/form-group/form-group.component.html | 17 +++ .../form/form-group/form-group.component.scss | 24 ++++ .../form/form-group/form-group.component.ts | 65 +++++++++++ .../lib/form/form-group/form-group.module.ts | 24 ++++ projects/common/src/lib/form/form.module.ts | 36 ++++++ .../src/lib/form/form/form.component.html | 13 +++ .../src/lib/form/form/form.component.scss | 21 ++++ .../src/lib/form/form/form.component.ts | 107 ++++++++++++++++++ .../common/src/lib/form/form/form.module.ts | 25 ++++ projects/common/src/lib/form/index.ts | 1 + .../lib/form/shared/form-field-component.ts | 7 ++ .../src/lib/form/shared/form-field.service.ts | 23 ++++ .../src/lib/form/shared/form.interfaces.ts | 49 ++++++++ .../src/lib/form/shared/form.service.ts | 58 ++++++++++ .../common/src/lib/form/shared/form.utils.ts | 18 +++ projects/common/src/lib/form/shared/index.ts | 5 + projects/common/src/public_api.ts | 5 + 32 files changed, 877 insertions(+) create mode 100644 demo/src/app/common/form/form-routing.module.ts create mode 100644 demo/src/app/common/form/form.component.html create mode 100644 demo/src/app/common/form/form.component.scss create mode 100644 demo/src/app/common/form/form.component.ts create mode 100644 demo/src/app/common/form/form.module.ts create mode 100644 projects/common/src/lib/form/form-field/form-field-select.component.html create mode 100644 projects/common/src/lib/form/form-field/form-field-select.component.ts create mode 100644 projects/common/src/lib/form/form-field/form-field-text.component.html create mode 100644 projects/common/src/lib/form/form-field/form-field-text.component.ts create mode 100644 projects/common/src/lib/form/form-field/form-field.component.html create mode 100644 projects/common/src/lib/form/form-field/form-field.component.scss create mode 100644 projects/common/src/lib/form/form-field/form-field.component.ts create mode 100644 projects/common/src/lib/form/form-field/form-field.module.ts create mode 100644 projects/common/src/lib/form/form-group/form-group.component.html create mode 100644 projects/common/src/lib/form/form-group/form-group.component.scss create mode 100644 projects/common/src/lib/form/form-group/form-group.component.ts create mode 100644 projects/common/src/lib/form/form-group/form-group.module.ts create mode 100644 projects/common/src/lib/form/form.module.ts create mode 100644 projects/common/src/lib/form/form/form.component.html create mode 100644 projects/common/src/lib/form/form/form.component.scss create mode 100644 projects/common/src/lib/form/form/form.component.ts create mode 100644 projects/common/src/lib/form/form/form.module.ts create mode 100644 projects/common/src/lib/form/index.ts create mode 100644 projects/common/src/lib/form/shared/form-field-component.ts create mode 100644 projects/common/src/lib/form/shared/form-field.service.ts create mode 100644 projects/common/src/lib/form/shared/form.interfaces.ts create mode 100644 projects/common/src/lib/form/shared/form.service.ts create mode 100644 projects/common/src/lib/form/shared/form.utils.ts create mode 100644 projects/common/src/lib/form/shared/index.ts diff --git a/demo/src/app/app.component.html b/demo/src/app/app.component.html index cc12144c3d..8e675bc43e 100644 --- a/demo/src/app/app.component.html +++ b/demo/src/app/app.component.html @@ -26,6 +26,7 @@

{{title}}

Dynamic Component Entity Table + Form
diff --git a/demo/src/app/app.module.ts b/demo/src/app/app.module.ts index 8883188b0a..64395cd8e5 100644 --- a/demo/src/app/app.module.ts +++ b/demo/src/app/app.module.ts @@ -19,6 +19,7 @@ import { AppRequestModule } from './core/request/request.module'; import { AppDynamicComponentModule } from './common/dynamic-component/dynamic-component.module'; import { AppEntityTableModule } from './common/entity-table/entity-table.module'; +import { AppFormModule } from './common/form/form.module'; import { AppAuthFormModule } from './auth/auth-form/auth-form.module'; @@ -60,6 +61,7 @@ import { AppComponent } from './app.component'; AppDynamicComponentModule, AppEntityTableModule, + AppFormModule, AppAuthFormModule, diff --git a/demo/src/app/common/form/form-routing.module.ts b/demo/src/app/common/form/form-routing.module.ts new file mode 100644 index 0000000000..1f75319748 --- /dev/null +++ b/demo/src/app/common/form/form-routing.module.ts @@ -0,0 +1,15 @@ +import { Routes, RouterModule } from '@angular/router'; +import { ModuleWithProviders } from '@angular/core'; + +import { AppFormComponent } from './form.component'; + +const routes: Routes = [ + { + path: 'form', + component: AppFormComponent + } +]; + +export const AppFormRoutingModule: ModuleWithProviders = RouterModule.forChild( + routes +); diff --git a/demo/src/app/common/form/form.component.html b/demo/src/app/common/form/form.component.html new file mode 100644 index 0000000000..a01b090a41 --- /dev/null +++ b/demo/src/app/common/form/form.component.html @@ -0,0 +1,33 @@ + + Common + Form + + See the code of this example + + + + + + + +
+ + +
+
diff --git a/demo/src/app/common/form/form.component.scss b/demo/src/app/common/form/form.component.scss new file mode 100644 index 0000000000..f21bfc1bb3 --- /dev/null +++ b/demo/src/app/common/form/form.component.scss @@ -0,0 +1,12 @@ +pre, +code { + font-family: monospace, monospace; +} +pre { + overflow: auto; +} +pre > code { + display: block; + padding: 1rem; + word-wrap: normal; +} diff --git a/demo/src/app/common/form/form.component.ts b/demo/src/app/common/form/form.component.ts new file mode 100644 index 0000000000..10834ff31f --- /dev/null +++ b/demo/src/app/common/form/form.component.ts @@ -0,0 +1,83 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Validators } from '@angular/forms'; +import { BehaviorSubject, Subscription } from 'rxjs'; + +import { Form, FormService } from '@igo2/common'; + +@Component({ + selector: 'app-form', + templateUrl: './form.component.html', + styleUrls: ['./form.component.scss'] +}) +export class AppFormComponent implements OnInit, OnDestroy { + + form$ = new BehaviorSubject
(undefined); + + data$ = new BehaviorSubject<{[key: string]: any}>(undefined); + + submitDisabled: boolean = true; + + private valueChanges$$: Subscription; + + constructor(private formService: FormService) {} + + ngOnInit() { + const fieldConfigs = [ + { + name: 'id', + title: 'ID', + options:  { + cols: 1, + validator: Validators.required + } + }, + { + name: 'name', + title: 'Name', + options:  { + cols: 1, + validator: Validators.required + } + }, + { + name: 'status', + title: 'Status', + type: 'select', + options:  { + cols: 2 + }, + inputs: { + choices: [ + {value: 1, title: 'Single'}, + {value: 2, title: 'Married'} + ] + } + } + ]; + + const fields = fieldConfigs.map((config) => this.formService.field(config)); + const form = this.formService.form([], [this.formService.group({name: 'info'}, fields)]); + + this.valueChanges$$ = form.control.valueChanges.subscribe(() => { + this.submitDisabled = !form.control.valid; + }); + + this.form$.next(form); + } + + ngOnDestroy() { + this.valueChanges$$.unsubscribe(); + } + + fillForm() { + this.data$.next({ + id: 1, + name: 'Bob', + status: 2 + }); + } + + onSubmit(data: {[key: string]: any}) { + alert(JSON.stringify(data)); + } +} diff --git a/demo/src/app/common/form/form.module.ts b/demo/src/app/common/form/form.module.ts new file mode 100644 index 0000000000..14fa769383 --- /dev/null +++ b/demo/src/app/common/form/form.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatButtonModule, MatCardModule } from '@angular/material'; + +import { IgoLibFormModule } from '@igo2/common'; + +import { AppFormComponent } from './form.component'; +import { AppFormRoutingModule } from './form-routing.module'; + +@NgModule({ + declarations: [AppFormComponent], + imports: [ + CommonModule, + AppFormRoutingModule, + MatButtonModule, + MatCardModule, + IgoLibFormModule + ], + exports: [AppFormComponent] +}) +export class AppFormModule {} diff --git a/projects/common/src/lib/form/form-field/form-field-select.component.html b/projects/common/src/lib/form/form-field/form-field-select.component.html new file mode 100644 index 0000000000..4a676c3014 --- /dev/null +++ b/projects/common/src/lib/form/form-field/form-field-select.component.html @@ -0,0 +1,10 @@ + + + + {{choice.title}} + + + diff --git a/projects/common/src/lib/form/form-field/form-field-select.component.ts b/projects/common/src/lib/form/form-field/form-field-select.component.ts new file mode 100644 index 0000000000..f43a25f398 --- /dev/null +++ b/projects/common/src/lib/form/form-field/form-field-select.component.ts @@ -0,0 +1,56 @@ +import { + Input, + Component, + ChangeDetectionStrategy, +} from '@angular/core'; +import { FormControl } from '@angular/forms'; + +import { Observable, of } from 'rxjs'; + +import { formControlIsRequired } from '../shared/form.utils'; +import { FormFieldSelectChoice } from '../shared/form.interfaces'; +import { FormFieldComponent } from '../shared/form-field-component'; + +/** + * This component renders a select field + */ +@FormFieldComponent('select') +@Component({ + selector: 'igo-form-field-select', + templateUrl: './form-field-select.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FormFieldSelectComponent { + + public choices$: Observable; + + /** + * The field's form control + */ + @Input() formControl: FormControl; + + /** + * Field placeholder + */ + @Input() placeholder: string; + + /** + * Select input choices + */ + @Input() + set choices(value: Observable | FormFieldSelectChoice[]) { + if (value instanceof Observable) { + this.choices$ = value; + } else { + this.choices$ = of(value); + } + } + + /** + * Whether the field is required + */ + get required(): boolean { + return formControlIsRequired(this.formControl); + } + +} diff --git a/projects/common/src/lib/form/form-field/form-field-text.component.html b/projects/common/src/lib/form/form-field/form-field-text.component.html new file mode 100644 index 0000000000..67c34e695b --- /dev/null +++ b/projects/common/src/lib/form/form-field/form-field-text.component.html @@ -0,0 +1,9 @@ + + + + + diff --git a/projects/common/src/lib/form/form-field/form-field-text.component.ts b/projects/common/src/lib/form/form-field/form-field-text.component.ts new file mode 100644 index 0000000000..2129ac2c05 --- /dev/null +++ b/projects/common/src/lib/form/form-field/form-field-text.component.ts @@ -0,0 +1,39 @@ +import { + Input, + Component, + ChangeDetectionStrategy, +} from '@angular/core'; +import { FormControl } from '@angular/forms'; + +import { formControlIsRequired } from '../shared/form.utils'; +import { FormFieldComponent } from '../shared/form-field-component'; + +/** + * This component renders a text field + */ +@FormFieldComponent('text') +@Component({ + selector: 'igo-form-field-text', + templateUrl: './form-field-text.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FormFieldTextComponent { + + /** + * The field's form control + */ + @Input() formControl: FormControl; + + /** + * Field placeholder + */ + @Input() placeholder: string; + + /** + * Whether the field is required + */ + get required(): boolean { + return formControlIsRequired(this.formControl); + } + +} diff --git a/projects/common/src/lib/form/form-field/form-field.component.html b/projects/common/src/lib/form/form-field/form-field.component.html new file mode 100644 index 0000000000..e0feb14a16 --- /dev/null +++ b/projects/common/src/lib/form/form-field/form-field.component.html @@ -0,0 +1,9 @@ + + + + + + diff --git a/projects/common/src/lib/form/form-field/form-field.component.scss b/projects/common/src/lib/form/form-field/form-field.component.scss new file mode 100644 index 0000000000..5d4e87f30f --- /dev/null +++ b/projects/common/src/lib/form/form-field/form-field.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/projects/common/src/lib/form/form-field/form-field.component.ts b/projects/common/src/lib/form/form-field/form-field.component.ts new file mode 100644 index 0000000000..ad228f9778 --- /dev/null +++ b/projects/common/src/lib/form/form-field/form-field.component.ts @@ -0,0 +1,44 @@ +import { + Component, + Input, + ChangeDetectionStrategy +} from '@angular/core'; + +import { FormField, FormFieldInputs } from '../shared/form.interfaces'; +import { FormFieldService } from '../shared/form-field.service'; + +/** + * This component renders the proper form input based on + * the field configuration it receives. + */ +@Component({ + selector: 'igo-form-field', + templateUrl: './form-field.component.html', + styleUrls: ['./form-field.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FormFieldComponent { + + /** + * Field configuration + */ + @Input() field: FormField; + + constructor(private formFieldService: FormFieldService) {} + + getFieldComponent(): any { + return this.formFieldService.getFieldByType(this.field.type || 'text'); + } + + getFieldInputs(): FormFieldInputs { + return Object.assign( + {placeholder: this.field.title}, + Object.assign({}, this.field.inputs || {}), + {formControl: this.field.control} + ); + } + + getFieldSubscribers(): {[key: string]: ({field: FormField, control: FormControl}) => void } { + return Object.assign({}, this.field.subscribers || {}); + } +} diff --git a/projects/common/src/lib/form/form-field/form-field.module.ts b/projects/common/src/lib/form/form-field/form-field.module.ts new file mode 100644 index 0000000000..405661a9f8 --- /dev/null +++ b/projects/common/src/lib/form/form-field/form-field.module.ts @@ -0,0 +1,42 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { + MatIconModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule +} from '@angular/material'; + +import { IgoLibDynamicOutletModule } from '../../dynamic-component/dynamic-outlet/dynamic-outlet.module'; + +import { FormFieldComponent } from './form-field.component'; +import { FormFieldSelectComponent } from './form-field-select.component'; +import { FormFieldTextComponent } from './form-field-text.component'; + +/** + * @ignore + */ +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + MatIconModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + IgoLibDynamicOutletModule + ], + exports: [ + FormFieldComponent, + FormFieldSelectComponent, + FormFieldTextComponent + ], + declarations: [ + FormFieldComponent, + FormFieldSelectComponent, + FormFieldTextComponent + ] +}) +export class IgoLibFormFieldModule {} diff --git a/projects/common/src/lib/form/form-group/form-group.component.html b/projects/common/src/lib/form/form-group/form-group.component.html new file mode 100644 index 0000000000..cb7911163c --- /dev/null +++ b/projects/common/src/lib/form/form-group/form-group.component.html @@ -0,0 +1,17 @@ + +
+
+
+ +
+
+ +
+ +
+
diff --git a/projects/common/src/lib/form/form-group/form-group.component.scss b/projects/common/src/lib/form/form-group/form-group.component.scss new file mode 100644 index 0000000000..5ce6776db0 --- /dev/null +++ b/projects/common/src/lib/form/form-group/form-group.component.scss @@ -0,0 +1,24 @@ +.igo-form-group { + width: 100%; + height: 100%; + display: block; + overflow: auto; + padding: 10px; +} + +.igo-form-field-wrapper { + display: inline-block; +} + +.igo-form-field-colspan-2 { + width: 100%; +} + +.igo-form-field-colspan-1 { + width: 50%; +} + +igo-form-field, +igo-form-field ::ng-deep mat-form-field { + width: 100%; +} diff --git a/projects/common/src/lib/form/form-group/form-group.component.ts b/projects/common/src/lib/form/form-group/form-group.component.ts new file mode 100644 index 0000000000..380d378953 --- /dev/null +++ b/projects/common/src/lib/form/form-group/form-group.component.ts @@ -0,0 +1,65 @@ +import { + Component, + Input, + Output, + EventEmitter, + ChangeDetectionStrategy +} from '@angular/core'; + +import { FormField, FormFieldGroup } from '../shared/form.interfaces'; + +/** + * A configurable form, optionnally bound to an entity + * (for example in case of un update). Submitting that form + * emits an event with the form data but no other operation is performed. + */ +@Component({ + selector: 'igo-form-group', + templateUrl: './form-group.component.html', + styleUrls: ['./form-group.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FormGroupComponent { + + /** + * Form field group + */ + @Input() group: FormFieldGroup; + + /** + * Event emitted when the form control changes + */ + @Output() formControlChanges = new EventEmitter(); + + constructor() {} + + /** + * Return the number of columns a field should occupy. + * The maximum allowed is 2, even if the field config says more. + * @param field Field + * @returns Number of columns + * @internal + */ + getFieldColSpan(field: FormField): number { + let colSpan = 2; + const options = field.options || {}; + if (options.cols && options.cols > 0) { + colSpan = Math.min(options.cols, 2); + } + + return colSpan; + } + + /** + * Return the number of columns a field should occupy. + * The maximum allowed is 2, even if the field config says more. + * @param field Field + * @returns Number of columns + * @internal + */ + getFieldNgClass(field: FormField): {[key: string]: boolean} { + const colspan = this.getFieldColSpan(field); + return {[`igo-form-field-colspan-${colspan}`]: true}; + } + +} diff --git a/projects/common/src/lib/form/form-group/form-group.module.ts b/projects/common/src/lib/form/form-group/form-group.module.ts new file mode 100644 index 0000000000..87a845a9ad --- /dev/null +++ b/projects/common/src/lib/form/form-group/form-group.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatFormFieldModule } from '@angular/material'; + +import { IgoLibFormFieldModule } from '../form-field/form-field.module'; +import { FormGroupComponent } from './form-group.component'; + +/** + * @ignore + */ +@NgModule({ + imports: [ + CommonModule, + MatFormFieldModule, + IgoLibFormFieldModule + ], + exports: [ + FormGroupComponent + ], + declarations: [ + FormGroupComponent + ] +}) +export class IgoLibFormGroupModule {} diff --git a/projects/common/src/lib/form/form.module.ts b/projects/common/src/lib/form/form.module.ts new file mode 100644 index 0000000000..349b16eb71 --- /dev/null +++ b/projects/common/src/lib/form/form.module.ts @@ -0,0 +1,36 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { IgoLibFormFormModule } from './form/form.module'; +import { IgoLibFormGroupModule } from './form-group/form-group.module'; +import { IgoLibFormFieldModule } from './form-field/form-field.module'; +import { FormFieldSelectComponent } from './form-field/form-field-select.component'; +import { FormFieldTextComponent } from './form-field/form-field-text.component'; +import { FormService } from './shared/form.service'; +import { FormFieldService } from './shared/form-field.service'; + +/** + * @ignore + */ +@NgModule({ + imports: [ + CommonModule, + IgoLibFormGroupModule, + IgoLibFormFieldModule + ], + exports: [ + IgoLibFormFormModule, + IgoLibFormGroupModule, + IgoLibFormFieldModule + ], + declarations: [], + providers: [ + FormService, + FormFieldService + ], + entryComponents: [ + FormFieldSelectComponent, + FormFieldTextComponent + ] +}) +export class IgoLibFormModule {} diff --git a/projects/common/src/lib/form/form/form.component.html b/projects/common/src/lib/form/form/form.component.html new file mode 100644 index 0000000000..10dcab580e --- /dev/null +++ b/projects/common/src/lib/form/form/form.component.html @@ -0,0 +1,13 @@ + + +
+
+ +
+
+ +
+
+ diff --git a/projects/common/src/lib/form/form/form.component.scss b/projects/common/src/lib/form/form/form.component.scss new file mode 100644 index 0000000000..a45d67baf4 --- /dev/null +++ b/projects/common/src/lib/form/form/form.component.scss @@ -0,0 +1,21 @@ +:host { + display: block; +} + +form { + width: 100%; + height: 100%; +} + +.igo-form-body, +.igo-form-content { + height: 100%; +} + +.igo-form-body-with-buttons .igo-form-content { + height: calc(100% - 56px); +} + +.igo-form-content { + display: flex; +} diff --git a/projects/common/src/lib/form/form/form.component.ts b/projects/common/src/lib/form/form/form.component.ts new file mode 100644 index 0000000000..ffc5c8ae26 --- /dev/null +++ b/projects/common/src/lib/form/form/form.component.ts @@ -0,0 +1,107 @@ +import { + Component, + Input, + Output, + EventEmitter, + OnChanges, + SimpleChanges, + ChangeDetectionStrategy, + ViewChild, + ElementRef +} from '@angular/core'; + +import t from 'typy'; + +import { Form, FormField, FormFieldGroup } from '../shared/form.interfaces'; + +/** + * A configurable form + */ +@Component({ + selector: 'igo-form', + templateUrl: './form.component.html', + styleUrls: ['./form.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class FormComponent implements OnChanges { + + /** + * Form + */ + @Input() form: Form; + + /** + * Input data + */ + @Input() formData: { [key: string]: any}; + + /** + * Event emitted when the form is submitted + */ + @Output() submitForm = new EventEmitter<{[key: string]: any}>(); + + @ViewChild('buttons') buttons: ElementRef; + + get hasButtons(): boolean { + return this.buttons.nativeElement.children.length !== 0; + } + + constructor() {} + + /** + * Is the entity or the template change, recreate the form or repopulate it. + * @param changes + * @internal + */ + ngOnChanges(changes: SimpleChanges) { + const formData = changes.formData; + if (formData && formData.currentValue !== formData.previousValue) { + if (formData.currentValue === undefined) { + this.clear(); + } else { + this.setData(formData.currentValue); + } + } + } + + /** + * Transform the form data to a feature and emit an event + * @param event Form submit event + * @internal + */ + onSubmit() { + this.submitForm.emit(this.getData()); + } + + private setData(data: {[key: string]: any}) { + this.form.fields.forEach((field: FormField) => { + field.control.setValue(t(data, field.name).safeObject); + }); + + this.form.groups.forEach((group: FormFieldGroup) => { + group.fields.forEach((field: FormField) => { + field.control.setValue(t(data, field.name).safeObject); + }); + }); + } + + private getData(): { [key: string]: any} { + const data = {}; + this.form.fields.forEach((field: FormField) => { + data[field.name] = field.control.value; + }); + + this.form.groups.forEach((group: FormFieldGroup) => { + Object.assign(data, group.control.value); + }); + return data; + } + + /** + * Clear form + */ + private clear() { + this.form.control.reset(); + } + +} diff --git a/projects/common/src/lib/form/form/form.module.ts b/projects/common/src/lib/form/form/form.module.ts new file mode 100644 index 0000000000..85728fb91e --- /dev/null +++ b/projects/common/src/lib/form/form/form.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; + +import { FormComponent } from './form.component'; + +/** + * @ignore + */ +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule + ], + exports: [ + FormComponent, + FormsModule, + ReactiveFormsModule + ], + declarations: [ + FormComponent + ] +}) +export class IgoLibFormFormModule {} diff --git a/projects/common/src/lib/form/index.ts b/projects/common/src/lib/form/index.ts new file mode 100644 index 0000000000..c3da79f741 --- /dev/null +++ b/projects/common/src/lib/form/index.ts @@ -0,0 +1 @@ +export * from './shared'; diff --git a/projects/common/src/lib/form/shared/form-field-component.ts b/projects/common/src/lib/form/shared/form-field-component.ts new file mode 100644 index 0000000000..e8d7ff4561 --- /dev/null +++ b/projects/common/src/lib/form/shared/form-field-component.ts @@ -0,0 +1,7 @@ +import { FormFieldService } from './form-field.service'; + +export function FormFieldComponent(type: string): (cls: any) => any { + return function (compType: any) { + FormFieldService.register(type, compType); + }; +} diff --git a/projects/common/src/lib/form/shared/form-field.service.ts b/projects/common/src/lib/form/shared/form-field.service.ts new file mode 100644 index 0000000000..860a0f2f78 --- /dev/null +++ b/projects/common/src/lib/form/shared/form-field.service.ts @@ -0,0 +1,23 @@ +/** + * Service where all available form fields are registered. + */ +export class FormFieldService { + + static fields: {[key: string]: any} = {}; + + static register(type: string, component: any) { + FormFieldService.fields[type] = component; + } + + constructor() {} + + /** + * Return field component by type + * @param type Field type + * @returns Field component + */ + getFieldByType(type: string): any { + return FormFieldService.fields[type]; + } + +} diff --git a/projects/common/src/lib/form/shared/form.interfaces.ts b/projects/common/src/lib/form/shared/form.interfaces.ts new file mode 100644 index 0000000000..72841e27ef --- /dev/null +++ b/projects/common/src/lib/form/shared/form.interfaces.ts @@ -0,0 +1,49 @@ +import { FormControl, FormGroup, ValidatorFn } from '@angular/forms'; +import { Observable } from 'rxjs'; + +export interface Form { + fields: FormField[]; + groups: FormFieldGroup[]; + control: FormGroup; +} + +export interface FormFieldGroupConfig { + name: string; +} + +export interface FormFieldGroup extends FormFieldGroupConfig { + fields: FormField[]; + control: FormGroup; +} + +export interface FormFieldConfig { + name: string; + title: string; + + type?: string; + options?: FormFieldOptions; + inputs?: T; + subscribers?: {[key: string]: ({field: FormField, control: FormControl}) => void}; +} + +export interface FormField extends FormFieldConfig { + control: FormControl; +} + +export interface FormFieldOptions { + validator?: ValidatorFn; + disabled?: boolean; + visible?: boolean; + cols?: number; +} + +export interface FormFieldInputs {} + +export interface FormFieldSelectInputs extends FormFieldInputs { + choices: Observable | FormFieldSelectChoice[]; +} + +export interface FormFieldSelectChoice { + value: any; + title: string; +} diff --git a/projects/common/src/lib/form/shared/form.service.ts b/projects/common/src/lib/form/shared/form.service.ts new file mode 100644 index 0000000000..9c652ab970 --- /dev/null +++ b/projects/common/src/lib/form/shared/form.service.ts @@ -0,0 +1,58 @@ +import { Injectable } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; + +import { + Form, + FormField, + FormFieldConfig, + FormFieldGroup, + FormFieldGroupConfig +} from './form.interfaces'; + +@Injectable({ + providedIn: 'root' +}) +export class FormService { + + constructor(private formBuilder: FormBuilder) {} + + form(fields: FormField[], groups: FormFieldGroup[]): Form { + const control = this.formBuilder.group({}); + fields.forEach((field: FormField) => { + control.addControl(field.name, field.control); + }); + groups.forEach((group: FormFieldGroup) => { + control.addControl(group.name, group.control); + }); + + return {fields, groups, control}; + } + + group(config: FormFieldGroupConfig, fields: FormField[]): FormFieldGroup { + const control = this.formBuilder.group({}); + fields.forEach((field: FormField) => { + control.addControl(field.name, field.control); + }); + + return Object.assign({}, config, {fields, control}) as FormFieldGroup; + } + + field(config: FormFieldConfig): FormField { + const options = config.options || {}; + const state = Object.assign({value: ''}, { + disabled: options.disabled + }); + const control = this.formBuilder.control(state); + control.setValidators(options.validator); + + return Object.assign({type: 'text'}, config, {control}) as FormField; + } + + extendFieldConfig(config: FormFieldConfig, partial: Partial): FormFieldConfig { + const options = Object.assign({}, config.options || {}, partial.options || {}); + const inputs = Object.assign({}, config.inputs || {}, partial.inputs || {}); + const subscribers = Object.assign({}, config.subscribers || {}, partial.subscribers || {}); + return Object.assign({}, config, {options, inputs, subscribers}); + } + +} diff --git a/projects/common/src/lib/form/shared/form.utils.ts b/projects/common/src/lib/form/shared/form.utils.ts new file mode 100644 index 0000000000..6f3f88de54 --- /dev/null +++ b/projects/common/src/lib/form/shared/form.utils.ts @@ -0,0 +1,18 @@ +import { AbstractControl } from '@angular/forms'; + +export function formControlIsRequired(control: AbstractControl): boolean { + if (control.validator) { + const validator = control.validator({} as AbstractControl); + if (validator && validator.required) { + return true; + } + } + + if (control['controls']) { + Object.keys(control['controls']).find((key: string) => { + return formControlIsRequired(control['controls'][key]); + }); + } + + return false; +} diff --git a/projects/common/src/lib/form/shared/index.ts b/projects/common/src/lib/form/shared/index.ts new file mode 100644 index 0000000000..d5d40aabdf --- /dev/null +++ b/projects/common/src/lib/form/shared/index.ts @@ -0,0 +1,5 @@ +export * from './form.interfaces'; +export * from './form.utils'; +export * from './form.service'; +export * from './form-field.service'; +export * from './form-field-component'; diff --git a/projects/common/src/public_api.ts b/projects/common/src/public_api.ts index 3a7052b829..3ed2f87aa1 100644 --- a/projects/common/src/public_api.ts +++ b/projects/common/src/public_api.ts @@ -12,6 +12,10 @@ export * from './lib/drag-drop/drag-drop.module'; export * from './lib/dynamic-component/dynamic-component.module'; export * from './lib/dynamic-component/dynamic-outlet/dynamic-outlet.module'; export * from './lib/flexible/flexible.module'; +export * from './lib/form/form.module'; +export * from './lib/form/form/form.module'; +export * from './lib/form/form-field/form-field.module'; +export * from './lib/form/form-group/form-group.module'; export * from './lib/entity/entity.module'; export * from './lib/entity/entity-table/entity-table.module'; export * from './lib/image/image.module'; @@ -32,6 +36,7 @@ export * from './lib/confirm-dialog'; export * from './lib/custom-html'; export * from './lib/drag-drop'; export * from './lib/dynamic-component'; +export * from './lib/form'; export * from './lib/entity'; export * from './lib/flexible'; export * from './lib/image';