Dynamic nested reactive form: ExpressionChangedAfterItHasBeenCheckedError - javascript

My reactive form is three component levels deep. The parent component creates a new form without any fields and passes it down to child components.
At first the outer form is valid. Later on a child component adds new form elements with validators (that fail) making the outer form invalid.
I am getting an ExpressionChangedAfterItHasBeenCheckedError error in the console. I want to fix that error.
Somehow this only happens when I add the third level of nesting. The same approach seemed to work for two levels of nesting.
Plunker: https://plnkr.co/edit/GymI5CqSACFEvhhz55l1?p=preview
Parent component
#Component({
selector: 'app-root',
template: `
myForm.valid: <b>{{myForm.valid}}</b>
<form>
<app-subform [myForm]="myForm"></app-subform>
</form>
`
})
export class AppComponent implements OnInit {
...
ngOnInit() {
this.myForm = this.formBuilder.group({});
}
}
Sub component
#Component({
selector: 'app-subform',
template: `
<app-address-form *ngFor="let addressData of addressesData;"
[addressesForm]="addressesForm">
</app-address-form>
`
})
export class SubformComponent implements OnInit {
...
addressesData = [...];
constructor() { }
ngOnInit() {
this.addressesForm = new FormArray([]);
this.myForm.addControl('addresses', this.addressesForm);
}
Child component
#Component({
selector: 'app-address-form',
template: `
<input [formControl]="addressForm.controls.addressLine1">
<input [formControl]="addressForm.controls.city">
`
})
export class AddressFormComponent implements OnInit {
...
ngOnInit() {
this.addressForm = this.formBuilder.group({
addressLine1: [
this.addressData.addressLine1,
[ Validators.required ]
],
city: [
this.addressData.city
]
});
this.addressesForm.push(this.addressForm);
}
}

To understand the problem you need to read Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error article.
For your particular case the problem is that you're creating a form in the AppComponent and use a {{myForm.valid}} interpolation in the DOM. It means that Angular will run create and run updateRenderer function for the AppComponent that updates DOM. Then you use the ngOnInit lifecycle hook of subcomponent to add subgroup with control to this form:
export class AddressFormComponent implements OnInit {
#Input() addressesForm;
#Input() addressData;
ngOnInit() {
this.addressForm = this.formBuilder.group({
addressLine1: [
this.addressData.addressLine1,
[ Validators.required ] <-----------
]
this.addressesForm.push(this.addressForm); <--------
The control becomes invalid because you don't supply initial value and you specify a required validator. Hence the entire form becomes invalid and the expression {{myForm.valid}} evaluates to false. But when Angular ran change detection for the AppComponent it evaluated to true. And that's what the error says.
One possible fix could be to mark the form as invalid in the start since you're planning to add required validator, but it seems Angular doesn't provide such method. Your best choice is probably to add controls asynchronously. In fact, this is what Angular does itself in the sources:
const resolvedPromise = Promise.resolve(null);
export class NgForm extends ControlContainer implements Form {
...
addControl(dir: NgModel): void {
// adds controls asynchronously using Promise
resolvedPromise.then(() => {
const container = this._findContainer(dir.path);
dir._control = <FormControl>container.registerControl(dir.name, dir.control);
setUpControl(dir.control, dir);
dir.control.updateValueAndValidity({emitEvent: false});
});
}
So for you case it will be:
const resolvedPromise = Promise.resolve(null);
#Component({
...
export class AddressFormComponent implements OnInit {
#Input() addressesForm;
#Input() addressData;
addressForm;
ngOnInit() {
this.addressForm = this.formBuilder.group({
addressLine1: [
this.addressData.addressLine1,
[ Validators.required ]
],
city: [
this.addressData.city
]
});
resolvedPromise.then(() => {
this.addressesForm.push(this.addressForm); <-------
})
}
}
Or use some variable in the AppComponent to hold form state and use it in the template:
{{formIsValid}}
export class AppComponent implements OnInit {
myForm: FormGroup;
formIsValid = false;
constructor(private formBuilder: FormBuilder) {}
ngOnInit() {
this.myForm = this.formBuilder.group({});
this.myForm.statusChanges((status)=>{
formIsValid = status;
})
}
}

import {ChangeDetectorRef} from '#angular/core';
....
export class SomeComponent {
form: FormGroup;
constructor(private fb: FormBuilder,
private ref: ChangeDetectorRef) {
this.form = this.fb.group({
myArray: this.fb.array([])
});
}
get myArray(): FormArray {
return this.form.controls.myArray as FormArray;
}
addGroup(): void {
const newGroup = this.fb.group({
prop1: [''],
prop2: ['']
});
this.myArray.push(newGroup);
this.ref.detectChanges();
}
}

I had the same scenario and same issue in Angular 9 and above solution works fine. I tweaked it a little bit: synchroinously adding the control without validators and adding the required validators asynchronously... Because I needed the controls immediately otherwise I got an error cannot find formControl ....
This is my solution based on the accepted answer above:
const resolvedPromise = Promise.resolve(null);
#Component({
selector: 'password-input',
templateUrl: './password-input.component.html',
styleUrls: ['./password-input.component.css']
})
export class PasswordInputComponent implements OnInit {
#Input() parentFormGroup : FormGroup;
#Input() controlName : string = 'password';
#Input() placeholder : string = 'Password';
#Input() label : string = null;
ngOnInit(): void {
this.parentFormGroup.addControl(this.controlName, new FormControl(null));
resolvedPromise.then( () => {
this.parentFormGroup.get(this.controlName).setValidators(Validators.required)
this.parentFormGroup.get(this.controlName).updateValueAndValidity();
});
}

Following the solution of Max Koretskyi, we can use async/await pattern on ngOnInit.
Here is an example:
#Component({
selector: 'my-sub-component',
templateUrl: 'my-sub-component.component.html',
})
export class MySubComponent implements OnInit {
#Input()
form: FormGroup;
async ngOnInit() {
// Avoid ExpressionChangedAfterItHasBeenCheckedError
await Promise.resolve();
this.form.removeControl('config');
this.form.addControl('config', new FormControl("", [Validator.required]));
}
}

Related

get result in parent component after selectAll and unSelectAll event emitted from child (Angular)

Hi I am now working on angular to build a multiselect dropdown using ng-multiselect-dropdown(https://www.npmjs.com/package/ng-multiselect-dropdown).
I used parent-child component communication through event emitter:
in child.component.ts:
import {Component, EventEmitter, Input, OnInit, Output} from '#angular/core';
import {IDropdownSettings} from 'ng-multiselect-dropdown';
export interface IMultiDropdownConfig {
placeholder: string;
header: string;
dropdownSettings: IDropdownSettings;
}
#Component({
selector: 'app-multi-dropdown',
templateUrl: './multi-dropdown.component.html',
styleUrls: ['./multi-dropdown.component.scss']
})
export class MultiDropdownComponent implements OnInit {
#Input() dropdownItems: any[];
#Input() selectedItems;
#Input() header: string;
#Input() placeholder: string;
#Input() dropdownSettings: IDropdownSettings;
#Input() loading;
#Output() itemSelectEvent = new EventEmitter();
#Output() itemDeselectEvent = new EventEmitter();
#Output() selectAllEvent = new EventEmitter();
#Output() deselectAllEvent = new EventEmitter();
#Output() selectedItemsChange = new EventEmitter();
constructor() { }
ngOnInit(): void {
}
onSelectItem(event) {
this.selectedItemsChange.emit(this.selectedItems);
}
onDeselectItem(event) {
this.selectedItemsChange.emit(this.selectedItems);
}
onSelectAll(event) {
this.selectedItemsChange.emit(this.selectedItems);
}
onDeselectAll(event) {
this.selectedItemsChange.emit(this.selectedItems);
}
}
in child.component.html:
<div class="multi-dropdown-item">
<div class="multi-dropdown-header">{{header}}</div>
<div *ngIf="!this.loading" class="multi-dropdown-body">
<ng-multiselect-dropdown
[placeholder]="placeholder"
[data]="dropdownItems"
[(ngModel)]="selectedItems"
[settings]="dropdownSettings"
(onSelect)="onSelectItem($event)"
(onDeSelect)="onDeselectItem($event)"
(onSelectAll)="onSelectAll($event)"
(onDeSelectAll)="onDeselectAll($event)">
</ng-multiselect-dropdown>
</div>
</div>
Then in parent.component.html:
<app-multi-dropdown
[loading]="filterPropertiesMap.get(filterEntry).loading"
[dropdownItems]="filterPropertiesMap.get(filterEntry).items"
[(selectedItems)]="filterPropertiesMap.get(filterEntry).selectedItems"
[dropdownSettings]="filterPropertiesMap.get(filterEntry).setting"
[placeholder]="filterPropertiesMap.get(filterEntry).name"
[header]="filterPropertiesMap.get(filterEntry).name"
(itemSelectEvent)="onItemSelect($event)"
(itemDeselectEvent)="onItemDeselect($event)"
(selectAllEvent)="onSelectAll($event)"
(deselectAllEvent)="onDeselectAll($event)"
></app-multi-dropdown>
in parent.component.ts I didn't do anything but log:
onItemSelect($event) {
console.log("onItemSelect");
}
onItemDeselect($event) {
console.log("onItemDeselect");
}
onSelectAll($event) {
console.log("onSelectAll");
}
onDeselectAll($event) {
console.log("onDeselectAll");
}
in above code filterPropertiesMap defines settings.
You may see that what I am doing is in child component, I created eventemitters for select, deselect, and in the function I emitt this.selectedItems.
But I don't think this is a good way to implement this, and actually, it doesn't work well.
sometimes, when I select or deselect, it doesn't changed immediately.
So how to implement this? when I select deselect, selectAll, deselectAll. my parent component can react immediately and correctly.
Also the weird thing is: when I load the page, I will have some default values chose, for example 6,7,8,9. Then I select all and it still 6,7,8,9. and then after that I deselect all agin select all, the field will change to all(for example 1,2,3,4,5,6,7,8,9). Does event emitter has delay or will ignore some choices??
Edit:
I tried to extract all the necessary snippets of code to build a project here:
https://drive.google.com/file/d/1BlV2EtdwZhqqpkdiC0_mlaw_r3w6Bder/view?usp=sharing
I hope when I all the event(select, deselect, selectAll, deselectAll) can be emitted and received by parent component
sorry one mistake: the app-multi-dropdown tag in parent component should be app-child
You can use it as part of an Angular reactive form, and just emit every time it value changes.
The HTML of the child component could be something like this:
<form [formGroup]="myForm">
<ng-multiselect-dropdown
name="dropdownItems"
[settings]="dropdownSettings"
[placeholder]="placeholder"
[data]="dropdownItems"
formControlName="dropdownItems">
</ng-multiselect-dropdown>
</form>
And the .ts file:
import { IDropdownSettings } from 'ng-multiselect-dropdown';
import { FormGroup, FormBuilder } from '#angular/forms';
import { Component, OnInit, Input, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.scss']
})
export class ChildComponent implements OnInit {
#Input() placeholder: string;
#Input() defaultValues: any[];
#Input() dropdownItems: any[];
#Input() dropdownSettings: IDropdownSettings;
#Output() onDropdownChange = new EventEmitter();
myForm: FormGroup;
constructor(
private _fb: FormBuilder
) { }
ngOnInit(): void {
this.myForm = this._fb.group({
dropdownItems: ['']
});
// Set default values as real values
this.onDropdownChange.emit(this.defaultValues);
this.myForm.valueChanges.subscribe(val => {
this.onDropdownChange.emit(val.dropdownItems)
})
}
}
EDIT: Based on your last update:
With the code you updated and my peace of code, I build this functional project running on stackblitz: https://stackblitz.com/edit/angular-ivy-g7w3dz
Hope I got the logic properly
you haven't set up your emitter properly. do this:
#Output()selectAllEvent = new EventEmitter<any>();
onSelectAll(event) {
this.selectAllEvent.emit(this.selectedItems);
}
then on the parent:
<app-multi-dropdown
(selectAllEvent)="onSelectAll($event)"
></app-multi-dropdown>

How to properly implement nested forms with Validator and Control Value Accessor?

In my application, I have a need for a reusable nested form component, such as Address. I want my AddressComponent to deal with its own FormGroup, so that I don't need to pass it from the outside.
At Angular conference (video, presentation) Kara Erikson, a member of Angular Core team recommended to implement ControlValueAccessor for the nested forms, making the nested form effectively just a FormControl.
I also figured out that I need to implement Validator, so that the validity of my nested form can be seen by the main form.
In the end, I created the SubForm class that the nested form needs to extend:
export abstract class SubForm implements ControlValueAccessor, Validator {
form: FormGroup;
public onTouched(): void {
}
public writeValue(value: any): void {
if (value) {
this.form.patchValue(value, {emitEvent: false});
this.onTouched();
}
}
public registerOnChange(fn: (x: any) => void): void {
this.form.valueChanges.subscribe(fn);
}
public registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState(isDisabled: boolean): void {
isDisabled ? this.form.disable()
: this.form.enable();
}
validate(c: AbstractControl): ValidationErrors | null {
return this.form.valid ? null : {subformerror: 'Problems in subform!'};
}
registerOnValidatorChange(fn: () => void): void {
this.form.statusChanges.subscribe(fn);
}
}
If you want your component to be used as a nested form, you need to do the following:
#Component({
selector: 'app-address',
templateUrl: './address.component.html',
styleUrls: ['./address.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AddressComponent),
multi: true
},
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => AddressComponent),
multi: true
}
],
})
export class AddressComponent extends SubForm {
constructor(private fb: FormBuilder) {
super();
this.form = this.fb.group({
street: this.fb.control('', Validators.required),
city: this.fb.control('', Validators.required)
});
}
}
Everything works well unless I check the validity status of my subform from the template of my main form. In this case ExpressionChangedAfterItHasBeenCheckedError is produced, see ngIf statement (stackblitz code) :
<form action=""
[formGroup]="form"
class="main-form">
<h4>Upper form</h4>
<label>First name</label>
<input type="text"
formControlName="firstName">
<div *ngIf="form.controls['address'].valid">Hi</div>
<app-address formControlName="address"></app-address>
<p>Form:</p>
<pre>{{form.value|json}}</pre>
<p>Validity</p>
<pre>{{form.valid|json}}</pre>
</form>
Use ChangeDetectorRef
Checks this view and its children. Use in combination with detach to
implement local change detection checks.
This is a cautionary mechanism put in place to prevent inconsistencies
between model data and UI so that erroneous or old data are not shown
to a user on the page
Ref:https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4
Ref:https://angular.io/api/core/ChangeDetectorRef
import { Component, OnInit,ChangeDetectorRef } from '#angular/core';
import { FormBuilder, FormGroup, Validators } from '#angular/forms';
#Component({
selector: 'app-upper',
templateUrl: './upper.component.html',
styleUrls: ['./upper.component.css']
})
export class UpperComponent implements OnInit {
form: FormGroup;
constructor(private fb: FormBuilder,private cdr:ChangeDetectorRef) {
this.form = this.fb.group({
firstName: this.fb.control('', Validators.required),
address: this.fb.control('')
});
}
ngOnInit() {
this.cdr.detectChanges();
}
}
Your Forked Example:https://stackblitz.com/edit/github-3q4znr
WriteValue will be triggered in the same digest cycle with the normal change detection lyfe cycle hook.
To fix that without using changeDetectionRef you can define your validity status field and change it reactively.
public firstNameValid = false;
this.form.controls.firstName.statusChanges.subscribe(
status => this.firstNameValid = status === 'VALID'
);
<div *ngIf="firstNameValid">Hi</div>
Try to use [hidden] in stand of *ngIf, it will work without ChangeDetectorRef.
Update URL : https://stackblitz.com/edit/github-3q4znr-ivtrmz?file=src/app/upper/upper.component.html
<div [hidden]="!form.controls['address'].valid">Hi</div>

Angular's `cdRef.detectChanges` doesn't prevent checked exception?

I have a situation where I have an inner component :
#Component({
selector: 'hello',
template: `<h1>Name = {{name}}!</h1> `
})
export class HelloComponent {
#Input() name: string;
#Output() ev: EventEmitter<string> = new EventEmitter<string>();
constructor(private cdRef: ChangeDetectorRef) { }
ngOnInit() {
this.ev.emit("new name");
this.cdRef.detectChanges();
}
}
Where in the parent component :
<hello name="{{ name }}" (ev)="changeName($event)"></hello>
#Component({
selector: 'my-app',
templateUrl: './app.component.html'
})
export class AppComponent {
name = 'Angular 5';
changeName(e) {
this.name = e;
console.log(e)
}
}
So basically , when the inner component loads , it emits an event which in turn received by parent and re-set the value of its inner component.
But this code ( and I do know why) - yields an exception :
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has
changed after it was checked. Previous value: 'Angular 5'. Current
value: 'new name'.
But according to other answers in SO:
This line : this.cdRef.detectChanges(); should have caused a new detection and prevent the exception.
I already know that setTimeout()... does solve it in a hacky way.
Question:
Why doesn't detectChanges prevent the exception ?
Stackblitz

Inheritance in Angular 2 components

I'm implemeting dynamic form system in my current project using ANgular 2, and so far is going good but I found the following problem:
I have two components that represent a form control like for example:
#Component({
selector: 'app-text-area',
templateUrl: './text-area.component.html',
styleUrls: ['./text-area.component.css']
})
export class TextAreaComponent implements OnInit {
label: string;
formGroup: FormGroup;
formControlName: string;
constructor(private injector: Injector) { }
ngOnInit() {
this.label = this.injector.get('label');
this.formGroup = this.injector.get('formGroup');
this.formControlName = this.injector.get('formControlName');
}
}
And:
#Component({
selector: 'app-input-text',
templateUrl: './input-text.component.html',
styleUrls: ['./input-text.component.css']
})
export class InputTextComponent implements OnInit{
label: string;
formGroup: FormGroup;
formControlName: string;
constructor(private injector: Injector) { }
ngOnInit() {
this.label = this.injector.get('label');
this.formGroup = this.injector.get('formGroup');
this.formControlName = this.injector.get('formControlName');
}
}
As you see both are identical except for the templateUrl, which is displaying different html elements.
So I would like to refactor the code and to create an abstract component to provide the common attributes and logic, and make then, the child classes to inherit the base class (as I would do when using Java). So I have created this class:
export class AbstractFormControl implements OnInit {
label: string;
formGroup: FormGroup;
formControlName: string;
constructor(private injector: Injector) { }
ngOnInit() {
this.label = this.injector.get('label');
this.formGroup = this.injector.get('formGroup');
this.formControlName = this.injector.get('formControlName');
}
}
And I have make the child classes extend the base class like this:
#Component({
selector: 'app-input-text',
templateUrl: './input-text.component.html',
styleUrls: ['./input-text.component.css']
})
export class InputTextComponent extends AbstractFormControl{
}
However now I'm getting the following error:
Uncaught Error: Can't resolve all parameters for InputTextComponent: (?).
Can someone explain me what's the right way to do this, or what I'm doing wrong?
Angular dependency injection system should know which type has been passed to constructor. When you inherit component such way typescript won't keep information about parameter private injector. You have two options:
1) Duplicate initializing
#Component({
...
})
export class InputTextComponent extends AbstractFormControl{
constructor(injector: Injector) { super(injector);}
}
But in your case you have the same number of parameters in your base and inherited classes and this solution seems redundant because we can omit constructor in our derived class.
We can omit constructor in derived class if we want to only use dependencies from parent class.
So let's say we have parent class like:
abstract class Parent {
constructor(private a: string, private b: number) {}
}
we can extend this class either
class Foo extends Parent {
constructor(a: string, b: number) {
super(a, b);
}
}
or
class Foo extends Parent {
}
because the second option will generate code like
function Foo() {
return _super !== null && _super.apply(this, arguments) || this;
}
Plunker Example
2) Use #Injectable for base class.
#Injectable()
export class AbstractFormControl {
this way typescript will translate the code above into
AbstractFormControl = __decorate([
core_1.Injectable(),
__metadata("design:paramtypes", [core_1.Injector])
], AbstractFormControl);
Plunker Example
and angular reflector can easily read this information
3) Use #Inject() for each of parameters
export class AbstractFormControl implements OnInit {
constructor(#Inject(Injector) private injector: Injector) { }

How can I pass the FormGroup of a parent component to its child component using the current Form API

I would like to pass the parent component's FormGroup to its child for the purpose of displaying an error-message using the child.
Given the following parent:
parent.component.ts
import { Component, OnInit } from '#angular/core'
import {
REACTIVE_FORM_DIRECTIVES, AbstractControl, FormBuilder, FormControl, FormGroup, Validators
} from '#angular/forms'
#Component({
moduleId: module.id,
selector: 'parent-cmp',
templateUrl: 'language.component.html',
styleUrls: ['language.component.css'],
directives: [ErrorMessagesComponent]
})
export class ParentCmp implements OnInit {
form: FormGroup;
first: AbstractControl;
second: AbstractControl;
constructor(private _fb: FormBuilder) {
this.first = new FormControl('');
this.second = new FormControl('')
}
ngOnInit() {
this.form = this._fb.group({
'first': this.first,
'second': this.second
});
}
}
I would now like to pass the form:FormGroup variable above to the child component below:
error-message.component.ts
import { Component, OnInit, Input } from '#angular/core'
import { NgIf } from '#angular/common'
import {REACTIVE_FORM_DIRECTIVES, FormGroup } from '#angular/forms'
#Component({
moduleId: module.id,
selector: 'epimss-error-messages',
template: `<span class="error" *ngIf="errorMessage !== null">{{errorMessage}}</span>`,
styles: [],
directives: [REACTIVE_FORM_DIRECTIVES, NgIf]
})
export class ErrorMessagesComponent implements OnInit {
#Input() ctrlName: string
constructor(private _form: FormGroup) { }
ngOnInit() { }
get errorMessage() {
// Find the control in the Host (Parent) form
let ctrl = this._form.find(this.ctrlName);
console.log('ctrl| ', ctrl);
// for (let propertyName of ctrl.errors) {
// // If control has a error
// if (ctrl.errors.hasOwnProperty(propertyName) && ctrl.touched) {
// // Return the appropriate error message from the Validation Service
// return CustomValidators.getValidatorErrorMessage(propertyName);
// }
// }
return null;
}
The constructor formGroup represents the FormGroup of the parent - in its present form it does not work.
I am trying to follow this obsolete example at http://iterity.io/2016/05/01/angular/angular-2-forms-and-advanced-custom-validation/
In the parent component do this:
<div [formGroup]="form">
<div>Your parent controls here</div>
<your-child-component [formGroup]="form"></your-child-component>
</div>
And then in your child component you can get hold of that reference like so:
export class YourChildComponent implements OnInit {
public form: FormGroup;
// Let Angular inject the control container
constructor(private controlContainer: ControlContainer) { }
ngOnInit() {
// Set our form property to the parent control
// (i.e. FormGroup) that was passed to us, so that our
// view can data bind to it
this.form = <FormGroup>this.controlContainer.control;
}
}
You can even ensure either formGroupName or [formGroup] is specified on your component by changing its selector like so:
selector: '[formGroup] epimss-error-messages,[formGroupName] epimss-error-messages'
This answer should be sufficient for your needs, but if you want to know more I've written a blog entry here:
https://mrpmorris.blogspot.co.uk/2017/08/angular-composite-controls-formgroup-formgroupname-reactiveforms.html
For Angular 11 I tried all the above answers, and in different combinations, but nothing quite worked for me. So I ended up with the following solution which worked for me just as I wanted.
TypeScript
#Component({
selector: 'fancy-input',
templateUrl: './fancy-input.component.html',
styleUrls: ['./fancy-input.component.scss']
})
export class FancyInputComponent implements OnInit {
valueFormGroup?: FormGroup;
valueFormControl?: FormControl;
constructor(
private formGroupDirective: FormGroupDirective,
private formControlNameDirective: FormControlName
) {}
ngOnInit() {
this.valueFormGroup = this.formGroupDirective.form;
this.valueFormControl = this.formGroupDirective.getControl(this.formControlNameDirective);
}
get controlName() {
return this.formControlNameDirective.name;
}
get enabled() {
return this.valueFormControl?.enabled
}
}
HTML
<div *ngIf="valueFormGroup && valueFormControl">
<!-- Edit -->
<div *ngIf="enabled; else notEnabled" [formGroup]="valueFormGroup">
<input class="input" type="text" [formControlName]="controlName">
</div>
<!-- View only -->
<ng-template #notEnabled>
<div>
{{valueFormControl?.value}}
</div>
</ng-template>
</div>
Usage
Note that I had to add ngDefaultControl otherwise it would give no default value accessor error in console (if somebody knows how to get rid of it without error - will be much appreciated).
<form [formGroup]="yourFormGroup" (ngSubmit)="save()">
<fancy-input formControlName="yourFormControlName" ngDefaultControl></fancy-input>
</form>
this is an example of child component used inside parent formGroup :
child component ts:
import { Component, OnInit, Input } from '#angular/core';
import { FormGroup, ControlContainer, FormControl } from '#angular/forms';
#Component({
selector: 'app-date-picker',
template: `
<mat-form-field [formGroup]="form" style="width:100%;">
<input matInput [matDatepicker]="picker" [placeholder]="placeHolder" [formControl]="control" readonly>
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
<mat-datepicker #picker></mat-datepicker>
</mat-form-field>
<mat-icon (click)="clearDate()">replay</mat-icon>`,
styleUrls: ['./date-picker.component.scss']
})
export class DatePickerComponent implements OnInit {
public form: FormGroup;
public control : FormControl;
#Input() controlName : string;
#Input() placeHolder : string;
constructor(private controlContainer: ControlContainer) {
}
clearDate(){
this.control.reset();
}
ngOnInit() {
this.form = <FormGroup>this.controlContainer.control;
this.control = <FormControl>this.form.get(this.controlName);
}
}
css date picker :
mat-icon{
position: absolute;
left: 83%;
top: 31%;
transform: scale(0.9);
cursor: pointer;
}
and used like this :
<app-date-picker class="col-md-4" [formGroup]="feuilleForm" controlName="dateCreation" placeHolder="Date de création"></app-date-picker>
Parent Component :
#Component({
selector: 'app-arent',
templateUrl: `<form [formGroup]="parentFormGroup" #formDir="ngForm">
<app-child [formGroup]="parentFormGroup"></app-child>
</form> `
})
export class ParentComponent implements {
parentFormGroup :formGroup
ngOnChanges() {
console.log(this.parentFormGroup.value['name'])
}
}
Child Component :
#Component({
selector: 'app-Child',
templateUrl: `<form [formGroup]="childFormGroup" #formDir="ngForm">
<input id="nameTxt" formControlName="name">
</form> `
})
export class ChildComponent implements OnInit {
#Input() formGroup: FormGroup
childFormGroup :FormGroup
ngOnInit() {
// Build your child from
this.childFormGroup.addControl('name', new FormControl(''))
/* Bind your child form control to parent form group
changes in 'nameTxt' directly reflect to your parent
component formGroup
*/
this.formGroup.addControl("name", this.childFormGroup.controls.name);
}
}
The ngOnInit was important - this did not work in the constructor.
And I prefer looking for the FormControlDirective - its the first one found in the child component's ancestor hierarchy
constructor(private formGroupDirective: FormGroupDirective) {}
ngOnInit() {
this.formGroupDirective.control.addControl('password', this.newPasswordControl);
this.formGroupDirective.control.addControl('confirmPassword', this.confirmPasswordControl);
this.formGroup = this.formGroupDirective.control;
}
I would do this in this way, i have passed child form data as group to parent so you can have separated form data in submit call.
Parent:
<form [formGroup]="registerStudentForm" (ngSubmit)="onSubmit()">
<app-basic-info [breakpoint]="breakpoint" [formGroup]="registerStudentForm"></app-basic-info>
<button mat-button>Submit</button>
</form>
Child:
<mat-card [formGroup]="basicInfo">
<mat-card-title>Basic Information</mat-card-title>
<mat-card-content>
<mat-grid-list
[gutterSize]="'20px'"
[cols]="breakpoint"
rowHeight="60px"
>
<mat-grid-tile>
<mat-form-field appearance="legacy" class="full-width-field">
<mat-label>Full name</mat-label>
<input matInput formControlName="full_name" />
</mat-form-field>
</mat-grid-tile>
</mat-grid-list>
</mat-card-content>
</mat-card>
Parent.ts:
export class RegisterComponent implements OnInit {
constructor() { }
registerForm = new FormGroup({});
onSubmit() {
console.warn(this.registerForm.value);
}
}
Child.ts
export class BasicInfoComponent implements OnInit {
#Input() breakpoint;
#Input() formGroup: FormGroup;
basicInfo: FormGroup;
constructor() { }
ngOnInit(): void {
this.basicInfo = new FormGroup({
full_name: new FormControl('Riki maru'),
dob: new FormControl(''),
});
this.formGroup.addControl('basicInfo', this.basicInfo);
}
}
Here in your child form components #Input() formGroup: FormGroup; part would be reference of parent component
I would pass the form as an input to the child component;
#Component(
{
moduleId: module.id,
selector: 'epimss-error-messages',
template: `
<span class="error" *ngIf="errorMessage !== null">{{errorMessage}}</span>`,
styles: [],
directives: [REACTIVE_FORM_DIRECTIVES, NgIf]
})
export class ErrorMessagesComponent implements OnInit {
#Input()
ctrlName: string
#Input('form') _form;
ngOnInit() {
this.errorMessage();
}
errorMessage() {
// Find the control in the Host (Parent) form
let ctrl = this._form.find(this.ctrlName);
console.log('ctrl| ', ctrl)
// for (let propertyName of ctrl.errors) {
// // If control has a error
// if (ctrl.errors.hasOwnProperty(propertyName) && ctrl.touched) {
// // Return the appropriate error message from the Validation Service
// return CustomValidators.getValidatorErrorMessage(propertyName);
// }
// }
return null;
}
And of course you'll need o pass the form from the parent component to the child, which you can do it in different ways , but the simplest is :
Somewhere in your parent ;
<epimss-error-messages [form]='form'></epimss-error-messages>
If you want to access the parent from the child component, you can access parent property of the FormControl instance, https://angular.io/api/forms/AbstractControl#parent
To get the parent error:
const parent = control.parent;
const errors = parent.errors;

Categories