src/lib/directives/form.directive.ts
FormGroupDirective
OnInit
OnChanges
OnDestroy
| Providers |
{
provide: ControlContainer, useExisting: forwardRef(() => FormDirective),
}
{
provide: RXAP_FORM_SUBMIT_METHOD, useValue: null,
}
{
provide: RXAP_FORM_LOAD_METHOD, useValue: null,
}
{
provide: RXAP_FORM_LOAD_FAILED_METHOD, useValue: null,
}
{
provide: RXAP_FORM_LOAD_SUCCESSFUL_METHOD, useValue: null,
}
{
provide: RXAP_FORM_SUBMIT_FAILED_METHOD, useValue: null,
}
{
provide: RXAP_FORM_SUBMIT_SUCCESSFUL_METHOD, useValue: null,
}
{
provide: RXAP_FORM_DEFINITION_BUILDER, useValue: null,
}
|
| Selector | form[rxapForm]:not([formGroup]):not([ngForm]),rxap-form,form[rxapForm] |
| Standalone | true |
| exportAs | rxapForm |
Properties |
|
Methods |
|
Inputs |
Outputs |
HostBindings |
Accessors |
constructor(cdr: ChangeDetectorRef, formDefinition: FormDefinition | null, submitMethod: FormSubmitMethod<any> | null, loadMethod: FormLoadMethod | null, loadFailedMethod: FormLoadFailedMethod | null, loadSuccessfulMethod: FormLoadSuccessfulMethod | null, submitFailedMethod: FormSubmitFailedMethod | null, submitSuccessfulMethod: FormSubmitSuccessfulMethod | null, formDefinitionBuilder: RxapFormBuilder | null, loadingIndicatorService: LoadingIndicatorService | null)
|
|||||||||||||||||||||||||||||||||
|
Defined in src/lib/directives/form.directive.ts:203
|
|||||||||||||||||||||||||||||||||
|
Parameters :
|
| initial | |
Type : T
|
|
|
Defined in src/lib/directives/form.directive.ts:119
|
|
| loadFailedMethod | |
Type : FormLoadFailedMethod | null
|
|
Default value : null
|
|
|
Defined in src/lib/directives/form.directive.ts:192
|
|
| loadMethod | |
Type : FormLoadMethod | null
|
|
Default value : null
|
|
|
Defined in src/lib/directives/form.directive.ts:189
|
|
| loadSuccessfulMethod | |
Type : FormLoadSuccessfulMethod | null
|
|
Default value : null
|
|
|
Defined in src/lib/directives/form.directive.ts:195
|
|
| rxapForm | |
Type : FormDefinition | string
|
|
|
Defined in src/lib/directives/form.directive.ts:166
|
|
| submitFailedMethod | |
Type : FormSubmitFailedMethod | null
|
|
Default value : null
|
|
|
Defined in src/lib/directives/form.directive.ts:198
|
|
| submitMethod | |
Type : FormSubmitMethod<any> | null
|
|
Default value : null
|
|
|
Defined in src/lib/directives/form.directive.ts:186
|
|
| submitSuccessfulMethod | |
Type : FormSubmitSuccessfulMethod | null
|
|
Default value : null
|
|
|
Defined in src/lib/directives/form.directive.ts:201
|
|
| invalidSubmit | |
Type : EventEmitter
|
|
|
Defined in src/lib/directives/form.directive.ts:134
|
|
| rxapSubmit | |
Type : EventEmitter
|
|
|
Defined in src/lib/directives/form.directive.ts:131
|
|
|
Emits when the submit method is executed without errors. The result of the submit method is passed as event object. If no submit method is defined then emit after the submit button is clicked. |
|
| submitSuccessful | |
Type : EventEmitter
|
|
|
Defined in src/lib/directives/form.directive.ts:138
|
|
| class.rxap-loaded |
Type : boolean
|
|
Defined in src/lib/directives/form.directive.ts:156
|
| class.rxap-loading |
Type : boolean
|
|
Defined in src/lib/directives/form.directive.ts:151
|
| class.rxap-loading-error |
Type : Error | null
|
|
Defined in src/lib/directives/form.directive.ts:161
|
| class.rxap-submit-error |
Type : Error | null
|
|
Defined in src/lib/directives/form.directive.ts:146
|
| class.rxap-submitting |
Type : boolean
|
|
Defined in src/lib/directives/form.directive.ts:141
|
| Protected getSubmitValue |
getSubmitValue()
|
|
Defined in src/lib/directives/form.directive.ts:474
|
|
Returns :
T
|
| Protected loadFailed | ||||||
loadFailed(error: Error)
|
||||||
|
Defined in src/lib/directives/form.directive.ts:464
|
||||||
|
Parameters :
Returns :
void
|
| Protected loadInitialState | ||||||
loadInitialState(form: RxapFormGroup)
|
||||||
|
Defined in src/lib/directives/form.directive.ts:404
|
||||||
|
Parameters :
Returns :
void
|
| Protected loadSuccessful | ||||||
loadSuccessful(value: any)
|
||||||
|
Defined in src/lib/directives/form.directive.ts:454
|
||||||
|
Parameters :
Returns :
void
|
| Public onSubmit | ||||||
onSubmit($event: Event)
|
||||||
|
Defined in src/lib/directives/form.directive.ts:322
|
||||||
|
Parameters :
Returns :
boolean
|
| Protected submit |
submit()
|
|
Defined in src/lib/directives/form.directive.ts:488
|
|
Returns :
void
|
| Protected submitFailed | ||||||
submitFailed(error: Error)
|
||||||
|
Defined in src/lib/directives/form.directive.ts:534
|
||||||
|
Parameters :
Returns :
void
|
| Protected submitSuccessful | ||||||
submitSuccessful(value: any)
|
||||||
|
Defined in src/lib/directives/form.directive.ts:547
|
||||||
|
Parameters :
Returns :
void
|
| Protected _formDefinition |
Type : FormDefinition<T>
|
|
Defined in src/lib/directives/form.directive.ts:183
|
| Public Readonly cdr |
Type : ChangeDetectorRef
|
Decorators :
@Inject(ChangeDetectorRef)
|
|
Defined in src/lib/directives/form.directive.ts:206
|
| Public Readonly context |
Default value : input<Record<string, unknown>>({})
|
|
Defined in src/lib/directives/form.directive.ts:121
|
| Public form |
Type : RxapFormGroup<T>
|
|
Defined in src/lib/directives/form.directive.ts:116
|
| Public Readonly loaded$ |
Default value : new ToggleSubject()
|
|
Defined in src/lib/directives/form.directive.ts:180
|
| Public Readonly loading$ |
Default value : new ToggleSubject()
|
|
Defined in src/lib/directives/form.directive.ts:179
|
| Public Readonly loadingError$ |
Default value : new BehaviorSubject<Error | null>(null)
|
|
Defined in src/lib/directives/form.directive.ts:181
|
| Public Readonly submitError$ |
Default value : new BehaviorSubject<Error | null>(null)
|
|
Defined in src/lib/directives/form.directive.ts:178
|
| Public Readonly submitting$ |
Default value : new ToggleSubject()
|
|
Defined in src/lib/directives/form.directive.ts:177
|
| submitting |
getsubmitting()
|
|
Defined in src/lib/directives/form.directive.ts:141
|
| submitError |
getsubmitError()
|
|
Defined in src/lib/directives/form.directive.ts:146
|
| loading |
getloading()
|
|
Defined in src/lib/directives/form.directive.ts:151
|
| loaded |
getloaded()
|
|
Defined in src/lib/directives/form.directive.ts:156
|
| loadingError |
getloadingError()
|
|
Defined in src/lib/directives/form.directive.ts:161
|
| useFormDefinition | ||||||
setuseFormDefinition(value: FormDefinition<T> | string)
|
||||||
|
Defined in src/lib/directives/form.directive.ts:166
|
||||||
|
Parameters :
Returns :
void
|
| formDefinition |
getformDefinition()
|
|
Defined in src/lib/directives/form.directive.ts:173
|
import {
ChangeDetectorRef,
Directive,
EventEmitter,
forwardRef,
HostBinding,
Inject,
input,
Input,
isDevMode,
OnChanges,
OnDestroy,
OnInit,
Optional,
Output,
SimpleChanges,
SkipSelf,
} from '@angular/core';
import {
ControlContainer,
FormGroupDirective,
} from '@angular/forms';
import { ToggleSubject } from '@rxap/rxjs';
import { LoadingIndicatorService } from '@rxap/services';
import {
clone,
isObject,
isPromise,
} from '@rxap/utilities';
import {
BehaviorSubject,
Subscription,
} from 'rxjs';
import {
debounceTime,
filter,
tap,
} from 'rxjs/operators';
import { RxapFormBuilder } from '../form-builder';
import { RxapFormGroup } from '../form-group';
import { FormDefinition } from '../model';
import {
FormLoadFailedMethod,
FormLoadMethod,
FormLoadSuccessfulMethod,
FormSubmitFailedMethod,
FormSubmitMethod,
FormSubmitSuccessfulMethod,
} from './models';
import {
RXAP_FORM_DEFINITION,
RXAP_FORM_DEFINITION_BUILDER,
RXAP_FORM_LOAD_FAILED_METHOD,
RXAP_FORM_LOAD_METHOD,
RXAP_FORM_LOAD_SUCCESSFUL_METHOD,
RXAP_FORM_SUBMIT_FAILED_METHOD,
RXAP_FORM_SUBMIT_METHOD,
RXAP_FORM_SUBMIT_SUCCESSFUL_METHOD,
} from './tokens';
@Directive({
selector: 'form[rxapForm]:not([formGroup]):not([ngForm]),rxap-form,form[rxapForm]',
providers: [
{
provide: ControlContainer,
// ignore coverage
useExisting: forwardRef(() => FormDirective),
},
// region form provider clear
// form provider that are directly associated with the current form
// are cleared to prevent that inner forms can access this providers
// Example: The parent form has a submit method provider and the inner should
// not have a submit method provider. If the parent submit method provider is
// not cleared then the inner form uses the parent submit method provider on
// submit
{
provide: RXAP_FORM_SUBMIT_METHOD,
useValue: null,
},
{
provide: RXAP_FORM_LOAD_METHOD,
useValue: null,
},
{
provide: RXAP_FORM_LOAD_FAILED_METHOD,
useValue: null,
},
{
provide: RXAP_FORM_LOAD_SUCCESSFUL_METHOD,
useValue: null,
},
{
provide: RXAP_FORM_SUBMIT_FAILED_METHOD,
useValue: null,
},
{
provide: RXAP_FORM_SUBMIT_SUCCESSFUL_METHOD,
useValue: null,
},
{
provide: RXAP_FORM_DEFINITION_BUILDER,
useValue: null,
},
// endregion
],
host: { '(reset)': 'onReset()' },
// eslint-disable-next-line @angular-eslint/no-outputs-metadata-property
outputs: [ 'ngSubmit' ],
exportAs: 'rxapForm',
standalone: true,
})
export class FormDirective<T = any>
extends FormGroupDirective
implements OnInit, OnChanges, OnDestroy {
public override form!: RxapFormGroup<T>;
@Input()
public initial?: T;
public readonly context = input<Record<string, unknown>>({});
/**
* Emits when the submit method is executed without errors. The result of the
* submit method is passed as event object.
*
* If no submit method is defined then emit after the submit button
* is clicked.
*/
@Output()
public rxapSubmit = new EventEmitter();
@Output()
public invalidSubmit = new EventEmitter<Record<string, any>>();
// eslint-disable-next-line @angular-eslint/no-output-rename
@Output('submitSuccessful')
public submitSuccessful$ = new EventEmitter();
@HostBinding('class.rxap-submitting')
public get submitting(): boolean {
return this.submitting$.value;
}
@HostBinding('class.rxap-submit-error')
public get submitError(): Error | null {
return this.submitError$.value;
}
@HostBinding('class.rxap-loading')
public get loading(): boolean {
return this.loading$.value;
}
@HostBinding('class.rxap-loaded')
public get loaded(): boolean {
return this.loaded$.value;
}
@HostBinding('class.rxap-loading-error')
public get loadingError(): Error | null {
return this.loadingError$.value;
}
@Input('rxapForm')
public set useFormDefinition(value: FormDefinition<T> | '') {
if (value) {
this._formDefinition = value as any;
this.form = value.rxapFormGroup;
}
}
public get formDefinition(): FormDefinition<T> {
return this._formDefinition;
}
public readonly submitting$ = new ToggleSubject();
public readonly submitError$ = new BehaviorSubject<Error | null>(null);
public readonly loading$ = new ToggleSubject();
public readonly loaded$ = new ToggleSubject();
public readonly loadingError$ = new BehaviorSubject<Error | null>(null);
protected _formDefinition!: FormDefinition<T>;
@Input()
public submitMethod: FormSubmitMethod<any> | null = null;
@Input()
public loadMethod: FormLoadMethod | null = null;
@Input()
public loadFailedMethod: FormLoadFailedMethod | null = null;
@Input()
public loadSuccessfulMethod: FormLoadSuccessfulMethod | null = null;
@Input()
public submitFailedMethod: FormSubmitFailedMethod | null = null;
@Input()
public submitSuccessfulMethod: FormSubmitSuccessfulMethod | null = null;
private _autoSubmitSubscription = new Subscription();
constructor(
@Inject(ChangeDetectorRef) public readonly cdr: ChangeDetectorRef,
@Optional()
@Inject(RXAP_FORM_DEFINITION)
formDefinition: FormDefinition | null = null,
// skip self, bc the token is set to null
@SkipSelf()
@Optional()
@Inject(RXAP_FORM_SUBMIT_METHOD)
submitMethod: FormSubmitMethod<any> | null = null,
// skip self, bc the token is set to null
@SkipSelf()
@Optional()
@Inject(RXAP_FORM_LOAD_METHOD)
loadMethod: FormLoadMethod | null = null,
// skip self, bc the token is set to null
@SkipSelf()
@Optional()
@Inject(RXAP_FORM_LOAD_FAILED_METHOD)
loadFailedMethod: FormLoadFailedMethod | null = null,
// skip self, bc the token is set to null
@SkipSelf()
@Optional()
@Inject(RXAP_FORM_LOAD_SUCCESSFUL_METHOD)
loadSuccessfulMethod: FormLoadSuccessfulMethod | null = null,
// skip self, bc the token is set to null
@SkipSelf()
@Optional()
@Inject(RXAP_FORM_SUBMIT_FAILED_METHOD)
submitFailedMethod: FormSubmitFailedMethod | null = null,
// skip self, bc the token is set to null
@SkipSelf()
@Optional()
@Inject(RXAP_FORM_SUBMIT_SUCCESSFUL_METHOD)
submitSuccessfulMethod: FormSubmitSuccessfulMethod | null = null,
// skip self, bc the token is set to null
@SkipSelf()
@Optional()
@Inject(RXAP_FORM_DEFINITION_BUILDER)
protected readonly formDefinitionBuilder: RxapFormBuilder | null = null,
@Optional()
@Inject(LoadingIndicatorService)
protected readonly loadingIndicatorService: LoadingIndicatorService | null = null,
) {
super([], []);
this.submitMethod = submitMethod ?? this.submitMethod;
this.loadMethod = loadMethod ?? this.loadMethod;
this.loadFailedMethod = loadFailedMethod ?? this.loadFailedMethod;
this.loadSuccessfulMethod = loadSuccessfulMethod ?? this.loadSuccessfulMethod;
this.submitFailedMethod = submitFailedMethod ?? this.submitFailedMethod;
this.submitSuccessfulMethod = submitSuccessfulMethod ?? this.submitSuccessfulMethod;
if (!formDefinition && formDefinitionBuilder) {
formDefinition = formDefinitionBuilder.build<FormDefinition>();
}
if (formDefinition) {
this._formDefinition = formDefinition;
this.form = formDefinition.rxapFormGroup;
}
this.loadingIndicatorService?.attachLoading(this.loading$);
this.loadingIndicatorService?.attachLoading(this.submitting$);
}
public override ngOnChanges(changes: SimpleChanges) {
super.ngOnChanges(changes);
const formChange = changes['form'];
if (formChange && !formChange.firstChange) {
this.loadInitialState(formChange.currentValue);
}
}
public ngOnInit() {
if (!this._formDefinition) {
// TODO : replace with rxap error
throw new Error('The form definition instance is not defined');
}
if (!this.form) {
// TODO : replace with rxap error
throw new Error('The form instance is not defined');
}
this.loadInitialState(this.form);
function HasNgOnInitMethod(obj: any): obj is OnInit {
return obj && typeof obj.ngOnInit === 'function';
}
if (HasNgOnInitMethod(this._formDefinition)) {
this._formDefinition.ngOnInit();
}
if (this._formDefinition.rxapMetadata.autoSubmit) {
const debounce =
typeof this._formDefinition.rxapMetadata.autoSubmit === 'number'
? this._formDefinition.rxapMetadata.autoSubmit
: 5000;
this._autoSubmitSubscription = this
.form
.valueChanges
.pipe(
debounceTime(debounce),
filter(() => this.form.valid),
tap((value) => {
if (isDevMode()) {
console.debug(
`Auto submit form '${ this._formDefinition.rxapMetadata.id }'`,
value,
);
}
},
),
tap(() => this.submit()),
)
.subscribe();
}
}
public override onSubmit($event: Event): boolean {
$event.preventDefault();
super.onSubmit($event);
if (this.form.valid) {
this.submit();
} else {
// eslint-disable-next-line no-inner-declarations
function reduceErrors(control: any, errors: Record<string, any> = {}): Record<string, any> {
if (control.invalid) {
if (control.controls) {
if (Array.isArray(control.controls)) {
const errorList = [];
for (const item of control.controls) {
errorList.push(reduceErrors(item));
}
errors[control.controlId] = errorList;
} else {
const innerErrors = {};
for (const child of Object.values(control.controls)) {
reduceErrors(child, innerErrors);
}
errors[control.controlId] = innerErrors;
}
} else {
if (control.errors) {
errors[control.controlId] = control.errors;
}
}
}
return errors;
}
if (isDevMode()) {
console.log(
'Form submit is not valid for: ' + this.form.controlId,
this.form.errors,
);
// eslint-disable-next-line no-inner-declarations
function printErrorControls(control: any) {
if (!control.valid) {
console.group(control.controlId);
if (control.controls) {
if (Array.isArray(control.controls)) {
for (let i = 0; i < control.controls.length; i++) {
const child = control.controls[i];
if (!child.valid) {
console.group(`index: ${ i }`);
printErrorControls(child);
console.groupEnd();
}
}
} else {
for (const child of Object.values(control.controls)) {
printErrorControls(child);
}
}
} else {
if (control.errors) {
for (const [ key, value ] of Object.entries(control.errors)) {
console.group(key);
console.log(value);
console.groupEnd();
}
}
console.log('value: ', control.value);
}
console.groupEnd();
}
}
printErrorControls(this.form);
}
this.invalidSubmit.emit(reduceErrors(this.form));
}
return false;
}
protected loadInitialState(form: RxapFormGroup): void {
if (this.initial) {
if (isDevMode()) {
console.log('use the value from input initial');
}
form.patchValue(this.initial);
} else {
if (this.loadMethod) {
this.loading$.enable();
try {
const resultOrPromise = this.loadMethod.call();
if (isPromise(resultOrPromise)) {
resultOrPromise
.then((value) => {
form.patchValue(value);
this.loaded$.enable();
this.loadSuccessful(value);
})
.catch((error) => {
this.loadingError$.next(error);
this.loadFailed(error);
})
.finally(() => {
this.loading$.disable();
this.cdr.detectChanges();
});
} else if (isObject(resultOrPromise)) {
form.patchValue(resultOrPromise);
this.loaded$.enable();
this.loadSuccessful(resultOrPromise);
this.loading$.disable();
}
} catch (error: any) {
this.loaded$.disable();
this.loadingError$.next(error);
this.loadFailed(error);
this.loading$.disable();
}
} else {
if (isDevMode()) {
console.warn(
'The form loading method is not defined for: ' + this.form.controlId,
);
}
this.loaded$.enable();
}
}
}
protected loadSuccessful(value: any) {
if (this.loadSuccessfulMethod) {
this.loadSuccessfulMethod.call(value);
} else if (isDevMode()) {
console.warn(
'The load successful is not defined for: ' + this.form.controlId,
);
}
}
protected loadFailed(error: Error) {
console.debug('Load Error:', error);
console.error('Load Error:', error.message);
if (this.loadFailedMethod) {
this.loadFailedMethod?.call(error);
} else if (isDevMode()) {
console.warn('The form loading failed for: ' + this.form.controlId);
}
}
protected getSubmitValue(): T {
let value: T = undefined as any;
if (typeof this._formDefinition['getSubmitValue'] === 'function') {
value = this._formDefinition.getSubmitValue();
} else if (typeof this._formDefinition['toJSON'] === 'function') {
value = this._formDefinition.toJSON();
}
value = value ?? this.form.value;
return clone(value);
}
protected submit() {
const value = this.getSubmitValue();
if (this.submitMethod) {
Reflect.set(this, 'submitted', false);
this.submitting$.enable();
this.submitError$.next(null);
try {
const resultOrPromise = this.submitMethod.call(value, this.context());
if (isPromise(resultOrPromise)) {
resultOrPromise
.then((result) => {
Reflect.set(this, 'submitted', true);
this.rxapSubmit.emit(result);
this.submitSuccessful(result);
})
.catch((error) => {
this.submitError$.next(error);
this.submitFailed(error);
})
.finally(() => {
this.submitting$.disable();
this.cdr.detectChanges();
});
} else {
Reflect.set(this, 'submitted', true);
this.rxapSubmit.emit(resultOrPromise);
this.submitSuccessful(resultOrPromise);
this.submitting$.disable();
}
} catch (error: any) {
this.submitting$.disable();
this.submitError$.next(error);
this.submitFailed(error);
}
} else {
if (isDevMode()) {
console.warn(
'The form submit method is not defined for: ' + this.form.controlId,
);
}
this.rxapSubmit.emit(value);
this.submitSuccessful(value);
}
}
protected submitFailed(error: Error) {
console.debug('Submit Error:', error);
console.error('Submit Error:', error.message);
if (this.submitFailedMethod) {
this.submitFailedMethod.call(error);
} else if (isDevMode()) {
console.warn(
'The form submit failed method is not defined for: ' +
this.form.controlId,
);
}
}
protected submitSuccessful(value: any) {
this.submitSuccessful$.next(value);
if (this.submitSuccessfulMethod) {
this.submitSuccessfulMethod.call(value);
} else if (isDevMode()) {
console.warn(
'The form submit successful method is not defined for: ' +
this.form.controlId,
);
}
}
public override ngOnDestroy() {
super.ngOnDestroy();
this._autoSubmitSubscription?.unsubscribe();
function HasNgOnDestroyMethod(obj: any): obj is OnDestroy {
return obj && typeof obj.ngOnDestroy === 'function';
}
if (HasNgOnDestroyMethod(this._formDefinition)) {
this._formDefinition.ngOnDestroy();
}
}
}