Angular2: Bind form context to ngTemplateOutlet - javascript

I'm trying to define a Component containing a dynamic Form (using ReactiveForms) where the user should be able to add / delete Controls.
The Controls can take many forms, and has to be defined outside of the Component, so I think that TemplateRef is the most appropriate for this.
I'm struggling to find a way to bind the externally defined Control to the internal Form, through the use of formControlName
Here is a start of the implementation:
// expandable.component.ts
[...]
#Component({
selector: 'expandable',
templateUrl: 'app/component/common/expandable/expandable.component.html',
styleUrls: ['app/component/common/expandable/expandable.component.css']
})
export class ExpandableComponent {
#ContentChild('childTemplate') childTemplate: TemplateRef<any>;
#Input() children: Array<any>;
public form: FormGroup;
constructor(private _changeDetector: ChangeDetectorRef,
private _formBuilder: FormBuilder) {
this.children = [];
}
public ngOnInit(): void {
this.form = this._formBuilder.group({
children: this._formBuilder.array([])
});
const arrayControl = <FormArray>this.form.controls['children'];
this.children.forEach(child => {
const group = this.initChildForm();
arrayControl.push(group);
});
}
private initChildForm(): AbstractControl {
return this._formBuilder.group({
key: ['Initial Key', [Validators.required]],
value: ['Initial Value', [Validators.required]]
});
}
public addChild(): void {
const control = <FormArray>this.form.controls['children'];
control.push(this.initChildForm());
this._changeDetector.detectChanges();
}
}
-
<!-- expandable.component.html -->
<form [formGroup]="form">
<div class="form-group">
<div formArrayName="children">
<div *ngFor="let child of form.controls.children.controls; let i=index">
<div [formGroupName]="i">
<template
[ngTemplateOutlet]="childTemplate"
[ngOutletContext]="{ $implicit: child }"></template>
</div>
</div>
</div>
</div>
<a (click)="addChild()">Add Child</a>
</form>
Attempt to define the the template externally:
<expandable>
<template #childTemplate>
<input class="form-control"
formControlName="value" />
</template>
</expandable>
I'm naively trying to bind the formControlName to the implicitly passed context from the outter , but with no luck, as I'm getting "Cannot find control with name: 'value'". Ideally, I would like to be able to do the formControlName binding into the expandable.component.html instead, but I see no way of doing this either.
Any thoughts about this?

Yes, this is possible:
You need to implement the ControlValueAccessor interface which ngModel and formControlName inject, like so:
#Directive({
selector: 'control',
providers: [
{provide: NG_VALUE_ACCESSOR, multi: true, useExisting: SwitchControlComponent}
]
})
export class SwitchControlComponent implements ControlValueAccessor {
isOn: boolean;
_onChange: (value: any) => void;
writeValue(value: any) {
this.isOn = !!value;
}
registerOnChange(fn: (value: any) => void) {
this._onChange = fn;
}
registerOnTouched() {}
toggle(isOn: boolean) {
this.isOn = isOn;
this._onChange(isOn);
}
}
html:
<expandable>
<template #childTemplate>
<div [formGroup]="form">
<input control class="form-control"
formControlName="value" />
</template>
</div>
</expandable>
Further reading:
With new forms api, can't add inputs from child components without adding additional form tags

Related

How do I send data from a Component to another Component in Angular?

I'm very new to Angular, and I'm really struggling to find a concise answer to this problem. I have a Form Component Here:
(I'm excluding the directives and imports as they're not really relevant)
export class JournalFormComponent implements OnInit {
public entries: EntriesService;
constructor(entries: EntriesService) {
this.entries = entries;
}
ngOnInit(): void {
}
}
The EntriesService service just stores an array of entries:
export class Entry {
constructor (
public num: number,
public name: string,
public description: string,
public text: string
) { }
}
The Form Component template renders a <h2> and a <app-input> Component for each entry in the EntriesService, which works. That looks like this:
<div *ngFor="let entry of entries.entries">
<h2> {{ entry.num }}. {{ entry.name }} </h2>
<app-input id="{{entry.num}}"></app-input>
</div>
Here's the <app-input> Input Component:
#Component({
selector: 'app-input',
template: `
<textarea #box
(keyup.enter)="update(box.value)"
(blur)="update(box.value)">
</textarea>
`
})
export class InputComponent {
private value = '';
update(value: string) {
this.value = value;
}
getValue () {
return this.value;
}
}
The InputComponent stores the user's text perfectly, but I don't know how to pass that data to the Form Component's EntriesService to update the Entry in order to Export it or Save it later. How is this done?
I think I'm phrasing this question well, but I'm not sure. If you need clarification I'll provide it.
Not sure if it matters, but I'm using Angular 9.1.11
There are many ways to update the data from one component to another.
component to component using service or subjects
parent~child component data exchange using Input() and Output() decorators. Or by using #ViweChild() interactions.
and many more
But please do check the angular docs https://angular.io/guide/component-interaction .
Use the below simple code, u might need to include modules like FormsModule. and import Input(), Output etc
#Component({
selector: 'app-journal-form',
template: `
<div *ngFor="let entry of entries.entries; let i=index">
<h2> {{ entry.num }}. {{ entry.name }} </h2>
<app-input id="{{entry.num}}" [entry]="entry" [arrayIndex]="i" (updateEntry)="updateEntry($event)" ></app-input>
</div>`
})
export class JournalFormComponent implements OnInit {
constructor(private entries: EntriesService) {
this.entries = entries;
}
ngOnInit(): void {
}
updateEntry(event){
console.log(event);
this.entries[event.arrayIndex] = event.entry;
}
}
#Component({
selector: 'app-input',
template: `
<textarea [(ngModel)]="name"
(keyup.enter)="update()"
(blur)="update()">
</textarea>
`
})
export class InputComponent {
#Input() entry: any;
#Input() arrayIndex: number;
#Output() updateEntry: EventEmitter<any> = new EventEmitter();
name:string;
constructor() {
console.log(entry);
this.name = entry.name;
}
update(){
this.entry.name = this.name;
this.updateEntry.emit({entry: this.entry, arrayIndex});
}
}
Output event will help in this situation.
<div *ngFor="let entry of entries.entries">
<h2> {{ entry.num }}. {{ entry.name }} </h2>
<app-input id="{{entry.num}}" (entryChange) = "entry.text = $event"></app-input>
</div>
app-input component
export class InputComponent {
private value = '';
#Output() entryChange = new EventEmitter<string>();
update(value: string) {
this.value = value;
this.entryChange.emit(value);
}
}
Instead of entry.text = $event you can also pass it to any save function, like saveEntry($event);

How to unselect selected option-element in select-element using Angular 8

I have the following requirements:
I got two FormControl objects for select-elements mainSelect and subSelect that are required.
subSelect changes depending on the value from mainSelect.
When mainSelect changes to a value in which the value from subSelect isn't included subSelect needs to become invalid so the FormGroup both of the FormControl's are part of becomes invalid, too.
But if the value from subSelect is included subSelect needs to hold his actual value.
(A concrete example is described after the StackBlitz link.)
My problem solving this requirement:
If the value of mainSelect changes and the value of subSelect isn't included subSelect takes the first value of the list instead of becoming null/invalid.
So the SOLUTION would be if the selected value of 'subSelect' becomes null and no value is selected in the browser.
What I tried so far:
I tried to create a component and implement the ControlValueAccessor interface. Seems like here lies my problem. I think I don't really understand how that works.
I watched the following video on YouTube and read articles (1, 2) related to ControlValueAccessor, but still couldn't solve my problem.
This is part of my code:
Also you can find it on StackBlitz
Example
If in the browser MainSelect has the value thirdMainSelect and SubSelect has the value fifthSubSelect and MainSelect changes his value to firstMainSelect SubSelect should have no selected value.
select.component.ts
export class SomeObject {
value: string;
parameters: {[parameterName: string]: string} = {};
}
#Component({
selector: "app-select",
templateUrl: "./select.component.html",
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: SelectComponent,
multi: true
}]
})
export class SelectComponent implements ControlValueAccessor, OnChanges {
#ViewChild("select", {static: true}) select: ElementRef;
#Input() tableId: string;
#Input() filter: { [parameterName: string]: string};
returnedTable: SomeObject[];
onChange: (_: any) => void;
onTouched: () => void;
selected: string;
constructor(private tableService: TableService) { }
loadTable(): void {
this.tableService.getTable(this.tableId, this.filter)
.subscribe(table => {
this.returnedTable = table;
});
}
ngOnChanges(): void {
this.loadTable();
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
writeValue(value: string): void {
this.selected = value;
}
}
select.component.html
<select class="form-control" #select (change)="onChange($event.target.value)">
<option *ngFor="let item of returnedTable" [value]="item.value" [selected]="selected === item.value">{{item.value}}</option>
</select>
app.component.ts
#Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.scss"]
})
export class AppComponent implements OnInit {
form: FormGroup;
containerObject: ContainerObject;
selectedMainValue: string;
constructor(private tableService: TableService,
private formBuilder: FormBuilder) {
}
ngOnInit(): void {
this.tableService.getContainerObject()
.subscribe(containerObject => {
this.containerObject = containerObject;
this.selectedMainValue = containerObject.mainSelect;
this.initForm();
});
}
private initForm(): void {
this.form = this.formBuilder.group({
mainSelect: [this.containerObject.mainSelect, Validators.required],
subSelect: [this.containerObject.subSelect, Validators.required]
});
this.subscribeToMainSelectChanged();
this.subscribeToSubSelectChanged();
}
onSubmit(): void {
if (this.form.valid) {
this.containerObject.mainSelect = this.form.get("mainSelect").value;
this.containerObject.subSelect = this.form.get("subSelect").value;
this.tableService.saveContainerObject(this.containerObject);
}
}
private subscribeToMainSelectChanged() {
this.form.get("mainSelect").valueChanges
.subscribe(mainSelect => {
this.selectedMainValue = mainSelect;
console.log(this.form.status);
});
}
private subscribeToSubSelectChanged() {
this.form.get("subSelect").valueChanges
.subscribe(() => {
console.log(this.form.status);
});
}
}
app.component.html
<div>
<form id="wrapper" [formGroup]="form" (ngSubmit)="onSubmit()">
<div id="left" class="form-group row">
<label for="mainSelect" class="col-form-label col-sm-2">MainSelect</label>
<div class="col-sm-6">
<app-select
id="mainSelect"
formControlName="mainSelect"
[tableId]="'mainSelectTable'"
[filter]="{firstType: 'firstParameter'}"
></app-select>
</div>
</div>
<div id="right" class="form-group row">
<label for="subSelect" class="col-form-label col-sm-2">SubSelect</label>
<div class="col-sm-6">
<app-select
id="subSelect"
formControlName="subSelect"
[tableId]="'firstParameter'"
[filter]="{firstType: 'firstParameter', secondType: selectedMainValue}"></app-select>
</div>
</div>
<p></p>
<button id="button" type="submit">Submit</button>
</form>
</div>
I think that it's only use valuesChange. If you has two arrays data and subdata and a form like
form = new FormGroup({
prop1: new FormControl(),
prop2: new FormControl()
});
A simple
this.form.get("prop1").valueChanges.subscribe(res => {
this.dataService.getData(res).subscribe(data=>{
this.subdata=data;
if (!this.subdata.find(x=>x.value==this.form.get("prop2").value))
this.form.get("prop2").setValue(null);
}).unsubscribe()
});
must be enough, see stackblitz
After a while trying to solve this problem only using the SelectComponent and especially writeValue, the following code did the job:
I changed the select.component.ts as following:
export class SomeObject {
value: string;
parameters: {[parameterName: string]: string} = {};
}
#Component({
selector: "app-select",
templateUrl: "./select.component.html",
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: SelectComponent,
multi: true
}]
})
export class SelectComponent implements ControlValueAccessor, OnChanges {
#ViewChild("select", {static: true}) select: ElementRef;
#Input() tableId: string;
#Input() filter: { [parameterName: string]: string};
returnedTable: SomeObject[];
onChange: (_: any) => void;
onTouched: () => void;
selected: string;
constructor(private tableService: TableService) { }
loadTable(): void {
this.tableService.getTable(this.tableId, this.filter)
.subscribe(table => {
this.returnedTable = table;
if (!!this.select && !!this.select.nativeElement.value) {
this.writeValue(this.select.nativeElement.value);
this.onChange(this.selected);
}
});
}
ngOnChanges(): void {
this.loadTable();
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
writeValue(value: string): void {
if (!!this.returnedTable && !this.returnedTable.some(item => item.value === value)) {
this.selected = null;
} else {
this.selected = value;
}
}
}
And the select.component.html like this:
<select class="form-control" #select (change)="onChange($event.target.value)">
<option hidden *ngIf="!selected" value=""></option>
<option *ngFor="let item of returnedTable" [value]="item.value" [selected]="selected === item.value">{{item.value}}</option>
</select>
To deselect an option:— option.selected = false.
Angular library with function decompilation at its core for $scope and Nested Forms, an Angular feature that indicates that Angular team doesn't know how to effectively use HTML. HTML doesn't allow nested forms so why the hell would you try to shoehorn the language to do that? More trouble than it's worth. Of course, you can't expect much better from guys like Bradley Green, former Angular JS manager.

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>

Is it Possible to nest FormGroups inside each other in Angular2?

I want to define a Form which consists of FormControls and FormGroups.
But i receive the following Error if i add the optionalInputGroup:
TypeError: this.form._updateTreeValidity is not a function
at FormGroupDirective._updateDomValue (http://localhost:8100/build/main.js:30206:19)
at FormGroupDirective.ngOnChanges (http://localhost:8100/build/main.js:30065:18)
at Wrapper_FormGroupDirective.ngDoCheck (/ReactiveFormsModule/FormGroupDirective/wrapper.ngfactory.js:30:18)
at CompiledTemplate.proxyViewClass.View_ConfiguratorOptionalGroupInputComponent0.detectChangesInternal (/AppModule/ConfiguratorOptionalGroupInputComponent/component.ngfactory.js:397:32)
at CompiledTemplate.proxyViewClass.AppView.detectChanges (http://localhost:8100/build/main.js:134498:14)
at CompiledTemplate.proxyViewClass.DebugAppView.detectChanges (http://localhost:8100/build/main.js:134693:44)
at CompiledTemplate.proxyViewClass.AppView.internalDetectChanges (http://localhost:8100/build/main.js:134483:18)
at View_ConfiguratorPage5.detectChangesInternal (/AppModule/ConfiguratorPage/component.ngfactory.js:245:19)
at View_ConfiguratorPage5.AppView.detectChanges (http://localhost:8100/build/main.js:134498:14)
at View_ConfiguratorPage5.DebugAppView.detectChanges (http://localhost:8100/build/main.js:134693:44)
at ViewContainer.detectChangesInNestedViews (http://localhost:8100/build/main.js:134830:37)
at View_ConfiguratorPage1.detectChangesInternal (/AppModule/ConfiguratorPage/component.ngfactory.js:369:14)
at View_ConfiguratorPage1.AppView.detectChanges (http://localhost:8100/build/main.js:134498:14)
at View_ConfiguratorPage1.DebugAppView.detectChanges (http://localhost:8100/build/main.js:134693:44)
at ViewContainer.detectChangesInNestedViews (http://localhost:8100/build/main.js:134830:37)
I create my FormConfig with the following Method:
flattenConfigForForm(inputs: [AbstractConfiguratorControl], returnObject = {}): Object {
inputs.map(input => {
switch (input.type) {
case ConfigurationInputType.Group:
let groupInput: ConfiguratorInputGroup = <ConfiguratorInputGroup> input;
returnObject[groupInput.key] = groupInput;
break;
default:
let basicInput: ConfiguratorInput = <ConfiguratorInput> input;
returnObject[basicInput.key] = basicInput;
returnObject[basicInput.key + ".ion"] = basicInput.value;
}
});
return returnObject;
}
where ConfigurationInputType.Group is an instance of FormGroup with an inputs-Attribute which itself contains FormControls and default is an instance of FormControl.
This is how i build my form in html, where values is the FormGroup which was created by flattenConfigForForm.
<form [formGroup]="values" (ngSubmit)="logForm()">
<div class="requiredInputs">
<div *ngFor="let input of config.inputs">
<configurator-number-input [passedFormGroup]="values" *ngIf="isEqualType(input.type,configInputType.Number)"
formControlName="{{input.key}}"></configurator-number-input>
<configurator-toggle-input [passedFormGroup]="values" *ngIf="isEqualType(input.type,configInputType.Toggle)"
formControlName="{{input.key}}"></configurator-toggle-input>
<configurator-select-input [passedFormGroup]="values" *ngIf="isEqualType(input.type,configInputType.Select)"
formControlName="{{input.key}}"></configurator-select-input>
<configurator-optional-group-input formGroupName="{{input.key}}" [passedFormGroup]="input"
formControlName="{{input.key}}"
*ngIf="isEqualType(input.type,configInputType.Group)"></configurator-optional-group-input>
</div>
</div>
</form>
this is my formGroupInputComponent.html
<div [formGroup]="passedFormGroup">
<ion-input>
<ion-checkbox [(ngModel)]="input.enabled"></ion-checkbox>
</ion-input>
<div *ngFor="let subinput of input.inputs">
<configurator-number-input [passedFormGroup]="input" *ngIf="isEqualType(subinput.type,configInputType.Number)"
formControlName="{{subinput.key}}"></configurator-number-input>
<configurator-toggle-input [passedFormGroup]="input" *ngIf="isEqualType(subinput.type,configInputType.Toggle)"
formControlName="{{subinput.key}}"></configurator-toggle-input>
<configurator-select-input [passedFormGroup]="input" *ngIf="isEqualType(subinput.type,configInputType.Select)"
formControlName="{{subinput.key}}"></configurator-select-input>
</div>
</div>
and this is my formGroupInput.ts
#Component({
selector: 'configurator-optional-group-input',
templateUrl: 'configurator-optional-group-input.html',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ConfiguratorOptionalGroupInputComponent),
multi: true,
}
]
})
export class ConfiguratorOptionalGroupInputComponent implements ControlValueAccessor {
input: ConfiguratorInputGroup;
#Input() public passedFormGroup: FormGroup;
// the method set in registerOnChange, it is just
// a placeholder for a method that takes one parameter,
// we use it to emit changes back to the form
private propagateChange = (_: any) => {
};
constructor() {
}
//ControlValueAccessor
writeValue(obj: any): void {
if (obj) {
console.log(this.passedFormGroup);
this.input = <ConfiguratorInputGroup> obj;
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
// not used, used for touch input
registerOnTouched(fn: any): void {
}
}
Thanks in advance!

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