-
Notifications
You must be signed in to change notification settings - Fork 125
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(platform): FormGroup layout support (#2139)
* feat(formgroup): FormGroup layout support #2131 Also disabling one lint rule due the problem which is still present in the tslint mgechev/codelyzer#64 * feat(formgroup): FormGroup layout support refactored. * feat(formgroup): Fix for status * feat(formgroup): Adding missing package-lock.json
- Loading branch information
Showing
20 changed files
with
1,480 additions
and
151 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,265 @@ | ||
import { | ||
AfterViewInit, | ||
ChangeDetectorRef, | ||
DoCheck, | ||
ElementRef, | ||
Input, | ||
OnChanges, | ||
OnDestroy, | ||
OnInit, | ||
Optional, | ||
Self, | ||
SimpleChanges, | ||
ViewChild | ||
} from '@angular/core'; | ||
import { FormFieldControl, InputSize, Status } from './form-control'; | ||
import { ControlValueAccessor, FormControl, NgControl, NgForm } from '@angular/forms'; | ||
import { coerceBooleanProperty } from '@angular/cdk/coercion'; | ||
import { Subject } from 'rxjs'; | ||
|
||
let randomId = 0; | ||
|
||
/** | ||
* All form components share the same information (value, name, placeholder,.. ) as well as | ||
* the same behavior given by ControlValueAccessor. | ||
* | ||
* Even this is not ideal solution there is no other way then use inheritance to reuse some of the | ||
* common logic. It should be possible to use some kind of compositions with Proxies but something | ||
* similar that exists in Aspect Oriented Programing. | ||
* | ||
* Usually try to fire stateChange only for things that can change dynamically in runtime. We don't expect | ||
* that e.g. placeholder will change after component is created | ||
*/ | ||
export abstract class BaseInput implements FormFieldControl<any>, ControlValueAccessor, | ||
OnInit, OnChanges, DoCheck, AfterViewInit, OnDestroy { | ||
|
||
protected defaultId: string = `fdp-input-id-${randomId++}`; | ||
protected _disabled: boolean; | ||
protected _value: any; | ||
protected _editable: boolean = true; | ||
protected _destroyed = new Subject<void>(); | ||
|
||
@Input() | ||
id: string = this.defaultId; | ||
|
||
@Input() | ||
name: string; | ||
|
||
@Input() | ||
placeholder: string; | ||
|
||
@Input() | ||
size: InputSize = 'cozy'; | ||
|
||
@Input() | ||
get disabled(): boolean { | ||
if (this.ngControl && this.ngControl.disabled !== null) { | ||
return this.ngControl.disabled; | ||
} | ||
return this._disabled; | ||
} | ||
|
||
set disabled(value: boolean) { | ||
this.setDisabledState(value); | ||
} | ||
|
||
/** | ||
* Tell the component if we are in editing mode. | ||
* | ||
*/ | ||
@Input() | ||
get editable(): boolean { | ||
return this._editable; | ||
} | ||
|
||
/** | ||
* Firing CD, as we can keep switching between editable and non-editable mode | ||
* | ||
*/ | ||
set editable(value: boolean) { | ||
const newVal = coerceBooleanProperty(value); | ||
if (this._editable !== newVal) { | ||
this._editable = newVal; | ||
this._cd.markForCheck(); | ||
this.stateChanges.next('editable'); | ||
} | ||
} | ||
|
||
|
||
/** | ||
* need to make these value accessor as abstract to be implemented by subclasses. Having them | ||
* in superclass have issue getting reference to them with Object.getOwnPropertyDescripton | ||
* which we need to programmatically wraps components set/get value | ||
* | ||
*/ | ||
abstract get value(): any; | ||
|
||
abstract set value(value: any); | ||
|
||
|
||
/** | ||
* Reference to internal Input element | ||
*/ | ||
@ViewChild('elemRef', { static: true }) | ||
protected _elementRef: ElementRef; | ||
|
||
|
||
/** | ||
* See @FormFieldControl | ||
*/ | ||
focused: boolean = false; | ||
|
||
/** | ||
* See @FormFieldControl | ||
*/ | ||
_status: Status; | ||
|
||
/** | ||
* See @FormFieldControl | ||
*/ | ||
readonly stateChanges: Subject<any> = new Subject<any>(); | ||
|
||
|
||
// @formatter:off | ||
onChange = (_: any) => {}; | ||
onTouched = () => {}; | ||
|
||
// @formatter:on | ||
|
||
constructor(protected _cd: ChangeDetectorRef, | ||
@Optional() @Self() public ngControl: NgControl, | ||
@Optional() @Self() public ngForm: NgForm) { | ||
|
||
if (this.ngControl) { | ||
this.ngControl.valueAccessor = this; | ||
} | ||
} | ||
|
||
ngOnInit(): void { | ||
if (!this.id || !this.name) { | ||
throw new Error('form input must have [id] and [name] attribute.'); | ||
} | ||
} | ||
|
||
ngOnChanges(changes: SimpleChanges): void { | ||
this.stateChanges.next('input: ngOnChanges'); | ||
} | ||
|
||
|
||
/** | ||
* Re-validate and emit event to parent container on every CD cycle as they are some errors | ||
* that we can't subscribe to. | ||
*/ | ||
ngDoCheck(): void { | ||
if (this.ngControl) { | ||
this.updateErrorState(); | ||
} | ||
} | ||
|
||
ngAfterViewInit(): void { | ||
this._cd.detectChanges(); | ||
} | ||
|
||
registerOnChange(fn: (_: any) => void): void { | ||
this.onChange = fn; | ||
} | ||
|
||
registerOnTouched(fn: () => void): void { | ||
this.onTouched = fn; | ||
} | ||
|
||
ngOnDestroy(): void { | ||
this.stateChanges.complete(); | ||
this._destroyed.next(); | ||
this._destroyed.complete(); | ||
} | ||
|
||
setDisabledState(isDisabled: boolean): void { | ||
const newState = coerceBooleanProperty(isDisabled); | ||
if (newState !== this._disabled) { | ||
this._disabled = isDisabled; | ||
this.stateChanges.next('setDisabledState'); | ||
} | ||
} | ||
|
||
/** | ||
* Each sub class must override this method as inheritance does not work | ||
*/ | ||
writeValue(value: any): void { | ||
this._value = value; | ||
this.onChange(value); | ||
this.stateChanges.next('writeValue'); | ||
} | ||
|
||
get status(): Status { | ||
return this._status; | ||
} | ||
|
||
|
||
/** | ||
* | ||
* Keeps track of element focus | ||
*/ | ||
_onFocusChanged(isFocused: boolean) { | ||
if (isFocused !== this.focused && (!this.disabled || !isFocused)) { | ||
this.focused = isFocused; | ||
this.stateChanges.next('_onFocusChanged'); | ||
} | ||
this.onTouched(); | ||
} | ||
|
||
/** | ||
* Handles even when we click on parent container which is the FormField Wrapping this | ||
* control | ||
*/ | ||
onContainerClick(event: MouseEvent): void { | ||
this.focus(event); | ||
} | ||
|
||
/** | ||
* In most of the cases when working with input element directly you should be just find to assing | ||
* variable to this element | ||
* | ||
* ``` | ||
* <input #elemRef fd-form-control ...> | ||
* ``` | ||
* | ||
* and this default behavior used. For other cases implement focus. | ||
* | ||
*/ | ||
focus(event?: MouseEvent): void { | ||
if (this._elementRef && !this.focused) { | ||
this._elementRef.nativeElement.focus(event); | ||
} | ||
} | ||
|
||
|
||
/** | ||
* Need re-validates errors on every CD iteration to make sure we are also | ||
* covering non-control errors, errors that happens outside of this control | ||
*/ | ||
protected updateErrorState() { | ||
const oldState = this.status === 'error'; | ||
const parent = this.ngForm; | ||
const control = this.ngControl ? this.ngControl.control as FormControl : null; | ||
const newState = !!(control && control.invalid && (control.touched || (parent && parent.submitted))); | ||
|
||
if (newState !== oldState) { | ||
this._status = newState ? 'error' : undefined; | ||
this.stateChanges.next('updateErrorState'); | ||
} | ||
} | ||
|
||
|
||
protected setValue(value: any) { | ||
if (value !== this._value) { | ||
this.writeValue(value); | ||
this._cd.markForCheck(); | ||
} | ||
} | ||
|
||
protected getValue(): any { | ||
return this._value; | ||
} | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { Observable } from 'rxjs'; | ||
import { NgControl } from '@angular/forms'; | ||
|
||
export type InputSize = 'compact' | 'cozy'; | ||
export type Status = 'error' | 'warning' | void; | ||
|
||
export abstract class FormFieldControl<T> { | ||
|
||
/** | ||
* Each input control has always a value. Need to make sure we keep a convention for | ||
* input fields | ||
*/ | ||
value: T | null; | ||
|
||
/** | ||
* Need to have a way to set placeholder to the input | ||
*/ | ||
placeholder: string; | ||
|
||
/** | ||
* Sets id from FF to Input | ||
*/ | ||
id: string; | ||
|
||
/** | ||
* This should be coming from Parent. | ||
*/ | ||
editable: boolean; | ||
|
||
/** | ||
* Components works in two sizes compact or cozy | ||
*/ | ||
size: InputSize; | ||
/** | ||
* | ||
* Form Field listen for all the changes happening inside the input | ||
*/ | ||
readonly stateChanges: Observable<void>; | ||
|
||
|
||
/** | ||
* Each input should inject its own ngControl and we should retrieve it | ||
*/ | ||
readonly ngControl: NgControl | null; | ||
|
||
/** Whether the control is disabled. */ | ||
readonly disabled: boolean; | ||
|
||
/** | ||
* Keeps track if the form element is in focus | ||
*/ | ||
readonly focused: boolean; | ||
|
||
/** | ||
* Currently used only to identify if we are in error status | ||
*/ | ||
readonly status: Status; | ||
|
||
abstract focus(event?: MouseEvent): void; | ||
|
||
/** | ||
* Handles even when we click on parent container which is the FormField Wrapping this | ||
* control | ||
*/ | ||
abstract onContainerClick(event: MouseEvent): void; | ||
|
||
} | ||
|
||
|
33 changes: 33 additions & 0 deletions
33
libs/platform/src/lib/components/form/form-group/fdp-form.module.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { NgModule } from '@angular/core'; | ||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; | ||
import { CommonModule } from '@angular/common'; | ||
import { FormModule as FdFormModule, InlineHelpModule, PopoverModule } from '@fundamental-ngx/core'; | ||
import { FormGroupComponent } from './form-group.component'; | ||
import { FormFieldComponent } from './form-field/form-field.component'; | ||
import { | ||
InputMessageGroupWithTemplate | ||
} from '../input-message-group-with-template/input-message-group-with-template.component'; | ||
|
||
@NgModule({ | ||
declarations: [ | ||
FormGroupComponent, | ||
FormFieldComponent, | ||
InputMessageGroupWithTemplate | ||
], | ||
imports: [ | ||
CommonModule, | ||
FormsModule, | ||
ReactiveFormsModule, | ||
FdFormModule, | ||
InlineHelpModule, | ||
PopoverModule | ||
], | ||
exports: [ | ||
FormGroupComponent, | ||
FormFieldComponent | ||
] | ||
}) | ||
export class FdpFormGroupModule { | ||
} | ||
|
||
|
Oops, something went wrong.