Renderer2.setProperty() doesn't update the value - javascript

I am trying to update the value using the rendered.setproperty() where value is updating the second time on listen event
these are the value that I am sending for the first time as empty in some widget
<ols-giftcard-payment-widget site-id="dddd" customer-last-name="" jwt-token="abcd"></ols-giftcard-payment-widget>
here I want to update the customer-last-name as a dynamic value
import { Component, Renderer2 } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
name = 'Angular';
constructor(private renderer: Renderer2) {}
createRecaptchaContainer() {
const recaptchaContainer = this.renderer.createElement(
'ols-giftcard-payment-widget'
);
this.renderer.setProperty(recaptchaContainer, 'siteId', '56711');
this.renderer.setProperty(
recaptchaContainer,
'jwtToken',
'abcd'
);
let cardClickEvent = this.renderer.listen(
recaptchaContainer,
'addCard',
(evt) => {
this.renderer.setProperty(
recaptchaContainer,
'customerLastName',
'anonymousLastName'
);
this.renderer.appendChild(
document.getElementById('payment-widget'),
recaptchaContainer
);
return recaptchaContainer;
debugger;
}
);
this.renderer.appendChild(
document.getElementById('payment-widget'),
recaptchaContainer
);
return recaptchaContainer;
}
ngOnInit(): void {
this.createRecaptchaContainer();
}
}
<div id="payment-widget"></div>
Here I am trying to add the customerLastname in this setProperty
this.renderer.setProperty(
recaptchaContainer,
'customerLastName',
'anonymousLastName'
);
it is updating on the second click, not on the first click
please help me to find here where I am wrong in the implementation

Your error message was Please provide the last name. Apparently recaptchaContainer needs a name to work.
When you click "Add card" for the first time customerLastName is empty. Look it up on localStorage under key cardDetails. anonymousLastName is added with click listener event from the component after data has been sent, that's why it works second time around.
{
"siteId": "56711",
"customerNumber": "",
"customerFirstName": "",
"customerLastName": "anonymousLastName",
"customerSession": "",
"companyId": "",
"primaryColor": "#2196F3"
}
To fix this, we could also setProperty for customerLastName outside of the event listener, so that customerLastName would be populated on first click. Either that or put something in customer-last-name="".
<hello name="{{ name }}"></hello>
<div id="payment-widget" (click)="setUsername('123')"></div>
<br>
<input [(ngModel)]="username" (input)="onChange()" #ctrl="ngModel" required>
<p>Value: {{ username }}</p>
<p>Valid: {{ ctrl.valid }}</p>
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
show: boolean = false;
jwtToknVal = of();
title = 'angular';
username: string;
recaptchaContainer: any;
constructor(private renderer: Renderer2) {}
createRecaptchaContainer() {
this.recaptchaContainer = this.renderer.createElement(
'ols-giftcard-payment-widget'
);
this.renderer.setProperty(this.recaptchaContainer, 'siteId', '56711');
this.renderer.setProperty(
this.recaptchaContainer,
'jwtToken',
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vMXJ5ZzMwZjZnay5leGVjdXRlLWFwaS51cy13ZXN0LTIuYW1hem9uYXdzLmNvbS9hdXRoZW50aWNhdGUiLCJzY29wZSI6InNlbGYiLCJpYXQiOjE2MTkxMTIxMjcsImV4cCI6MTcxOTE5ODUyNywicm9sZXMiOlsiY29tbXVuaXR5X2FwcGxpY2F0aW9uX2FjY2Vzc3wxMTE4N3wxIl0sInVzZXIiOnsiaWQiOiIyNTc1NyIsIm5hbWUiOiJTaG9ydGN1dHMiLCJzdXJuYW1lIjoiUFBGIiwiZ2l2ZW5fbmFtZSI6IlNob3J0Y3V0cyIsImRpc3BsYXlfbmFtZSI6IlNob3J0Y3V0cyBQUEYiLCJlbWFpbCI6InNob3J0Y3V0c0BzaG9ydGN1dHNzb2Z0d2FyZWRldi5jb20ifX0.vll2mLmdtBuuz0mnlENEAs1xT0In0NmYKBWD67VfYdw'
);
this.renderer.appendChild(
document.getElementById('payment-widget'),
this.recaptchaContainer
);
this.renderer.setProperty(
this.recaptchaContainer,
'customerLastName',
this.username
);
this.renderer.listen(this.recaptchaContainer, 'addCard', (evt) => {
console.log(this.recaptchaContainer, evt);
this.renderer.setProperty(
this.recaptchaContainer,
'customerLastName',
this.username
);
return this.recaptchaContainer;
});
}
ngOnInit(): void {
this.createRecaptchaContainer();
}
onChange() {
console.log(this.username);
}
setUsername(name: string) {
this.renderer.setProperty(
this.recaptchaContainer,
'customerLastName',
name
);
}
}
Here is a working example: https://stackblitz.com/edit/angular-rkua7n?file=src%2Fapp%2Fapp.component.ts

Related

Communication between children in angular

I am making a list of students. The input field, add button, and update button are inside one child and in other child there is the list with the delete and edit buttons. Both are handled in the parent component.
When I click edit button, I would like the input filed to have a value from that list and to be able to update the list.
parent html
<ul>
<app-list-item
*ngFor="let studentName of students; let i = index"
[name]="studentName"
[index]="i"
(deleteNameByIndex)="onemitDeleteNameByIndex($event)"
(editNameById)="onemitEditNameById($event)"
>
</ul>
parent .ts
import { Component } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
students = ['huzaifa', 'hunzila', 'waqar', 'sibte', 'shahzeen'];
student = '';
onemitAddNewName(newName: string) {
this.students.push(newName);
}
onemitDeleteNameByIndex(index: number) {
this.students.splice(index, 1);
}
onemitEditNameById(student: any) {
this.student = student;
console.log('app student :>> ', this.student);
}
}
child 1 html
<input
type="text"
placeholder="enter name"
[value]="name"
(input)="oninputSetName($event)"
/>
{{ student }}
{{ name }}
<button (click)="onclickEmitNewName()">Add</button>
<button>Update</button>
child 1 .ts
import { Component, EventEmitter, Input, Output } from '#angular/core';
#Component({
selector: 'app-list-form',
templateUrl: './list-form.component.html',
styleUrls: ['./list-form.component.css'],
})
export class ListFormComponent {
name = '';
#Output() newName = new EventEmitter<string>();
#Input() student = '';
oninputSetName(event: any) {
this.name = event.target.value;
}
onclickEmitNewName() {
this.newName.emit(this.name);
}
updateInput() {
let obj = { name: this.student };
console.log('list-form student :>> ', this.student);
}
}
child 2 html
{{ name }} -- {{ index }}
<button (click)="onclickDeleteName()">delete</button>
<button (click)="onclickEditName()">edit</button>
child 2 .ts
#Component({
selector: 'app-list-item',
templateUrl: './list-item.component.html',
styleUrls: ['./list-item.component.css'],
})
export class ListItemComponent {
#Input() name = '';
#Input() index = 0;
#Output() deleteNameByIndex = new EventEmitter<number>();
#Output() editNameById = new EventEmitter<any>();
onclickDeleteName() {
this.deleteNameByIndex.emit(this.index);
}
onclickEditName() {
let obj = { index: this.index, name: this.name };
this.editNameById.emit(obj);
}
}
Or even in a more elegant way, you could use a helper service to solve your communication issue.
Below you could find a sample service:
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs';
#Injectable({
providedIn: 'root'
})
export class MessageService {
private messageSource = new Subject<string>();
currentMessage = this.messageSource.asObservable();
constructor() {}
changeMessage(message: string) {
this.messageSource.next(message);
}
}
import { Component } from '#angular/core';
import { MessageService } from './message.service';
#Component({
selector: 'app-sender',
template: `
<button (click)="sendMessage()">Send Message</button>
`
})
export class SenderComponent {
constructor(private messageService: MessageService) {}
sendMessage() {
this.messageService.changeMessage('Hello from Sender Component');
}
}
import { Component } from '#angular/core';
import { MessageService } from './message.service';
#Component({
selector: 'app-receiver',
template: `
<p>{{ message }}</p>
`
})
export class ReceiverComponent {
message: string;
constructor(private messageService: MessageService) {
this.messageService.currentMessage.subscribe(message => {
this.message = message;
});
}
}
Since angular is Pass By reference. You can take advantage of that. when you do that you don't even have to emit the changed value.
For Example, In your code at Child 1:
Rather than emitting a local variable. All you can do is assign the value to #Input.
Here is a example:
#Input existingStudentName: string;
localName: string = existingStudentName;
onUserUpdate(){
existingStudentName = localName;
//No need to emmit since existingStudentName is updated here
//It will update in parent or anyone who refer it
}
<input type="text" [(ng-Model)]="localName">
<input type="button" (click)="onUserUpdate()">
Parent Html
<section>
<!-- input field where i enter my data -->
<app-list-form
(newName)="onemitAddNewName($event)"
[student]="student"
(updatedName)="updateThisNameInList($event)"
></app-list-form>
<!-- rendering my list -->
<ul>
<app-list-item
*ngFor="let studentName of students; let i = index"
[name]="studentName"
[index]="i"
(deleteNameByIndex)="onemitDeleteNameByIndex($event)"
(editNameById)="onemitEditNameById($event)"
[
></app-list-item>
</ul>
</section>
Parent ts
import { Component } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
students = ['huzaifa', 'hunzila', 'waqar', 'sibte', 'shahzeen'];
student = null;
onemitAddNewName(newName: string) {
this.students.push(newName);
}
onemitDeleteNameByIndex(index: number) {
this.students.splice(index, 1);
}
onemitEditNameById(student: any) {
this.student = student;
}
updateThisNameInList(student: any) {
let newName = student.name;
let index = student.index;
this.students.splice(index, 1, newName);
}
}
child 1 html
<input
type="text"
placeholder="enter name"
[(ngModel)]="name"
(input)="oninputSetName($event)"
/>
<!-- {{ student?.name ?? "" }}
{{ name }} -->
<button (click)="onclickEmitNewName()">Add</button>
<button (click)="onclickEmitUpdateName()">Update</button>
child 1 ts
import {
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
} from '#angular/core';
#Component({
selector: 'app-list-form',
templateUrl: './list-form.component.html',
styleUrls: ['./list-form.component.css'],
})
export class ListFormComponent implements OnChanges {
name = '';
#Output() newName = new EventEmitter<string>();
#Input() student: any = null;
#Output() updatedName = new EventEmitter<any>();
oninputSetName(event: any) {
this.name = event.target.value;
}
ngOnChanges(changes: SimpleChanges): void {
console.log('list-form: changes happen ', changes);
this.name = changes['student']?.currentValue?.name ?? '';
}
change(event: any) {
this.name = event.target.value;
}
onclickEmitNewName() {
this.newName.emit(this.name);
this.name = '';
}
onclickEmitUpdateName() {
// if (this.name == '') return;
if (!this.name) return;
this.updatedName.emit({
name: this.name,
index: this.student.index,
});
}
}
child 2 html
<li>
{{ name }} -- {{ index }}
<button (click)="onclickDeleteName()">delete</button>
<button (click)="onclickEditName()">edit</button>
</li>
child 2 ts
import { Component, EventEmitter, Input, Output } from '#angular/core';
#Component({
selector: 'app-list-item',
templateUrl: './list-item.component.html',
styleUrls: ['./list-item.component.css'],
})
export class ListItemComponent {
#Input() name = '';
#Input() index = 0;
#Output() deleteNameByIndex = new EventEmitter<number>();
#Output() editNameById = new EventEmitter<any>();
onclickDeleteName() {
this.deleteNameByIndex.emit(this.index);
}
onclickEditName() {
let obj = {
index: this.index,
name: this.name,
};
this.editNameById.emit(obj);
}
}

Showing and Hiding a child component from parent for specific seconds

I have notification component, which is a child component and being shown in parent component after a button click. After a custom seconds of time it will be hidden. So far I have come up with this code (Stackblits).
MyCode
Parent component:
<button (click)="handleClick()">Initiate Message</button>
<app-notification [message]="message" [duration]="3000" [show]="show"></app-notification>
<app-notification [message]="message" [duration]="6000" [show]="show"></app-notification>
export class AppComponent {
message:string;
show:boolean;
handleClick(){
this.message = 'Good Morning';
this.show=true;
}
}
Child comp:
<div class="container" *ngIf="show">
{{ message }}
</div>
#Input() message: string;
#Input() duration: number;
#Input() show = false;
constructor() {}
ngOnChanges(changes: SimpleChanges) {
console.log(this.show)
setTimeout(()=>{
this.show = false;
console.log(3)
}, this.duration)
}
This works perfectly, but after 3 seconds as it is in the example, If I click the button, the child component is not being showed. What am I doing wrong. I am new to Angular, please help me.
Also I want to handle the same for both instances of the child.
You have to move the code for the timeout out of ngOnChanges.
In your component setup a setter would do the trick.
#Component({
selector: 'app-notification',
templateUrl: './notification.component.html',
styleUrls: ['./notification.component.css'],
})
export class NotificationComponent {
#Input() message: string;
#Input() duration: number;
#Input() set show(value: boolean) {
this._show = value;
// Or find another failsafe if the values are not set as expected
if (value && this.duration) {
this._show = true;
setTimeout(() => {
this._show = false;
}, this.duration);
}
}
get show(): boolean {
return this._show;
}
private _show = false;
}
As well as an ugly hack to avoid Angular not realizing that this value needs to be reset:
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
message: string;
show: boolean;
duration: number;
handleClick() {
this.message = 'Good Morning';
this.show = true;
setTimeout(() => (this.show = false), 1);
}
}
Using RxJS Observables would solve this problem in a nice way, but it would also be more complicated if you are new to Angular.

Bind different values in multiple textbox with same ngModel property in Angular 8

I am trying to create a time-picker. The picker will be opened when the user focuses on a text-box. Now, a single page may contain multiple text-boxes, for each of which the picker should be opened. The issue I am facing is, I get the values from time-picker for different text-boxes, but when binding to ngModel, any selected value gets bound to all the text-boxes.
Let me show you my approach:
component.html
<input type="text" [(ngModel)]="pickerData" (focus)="initPicker($event)" id="f-1" />
<input type="text" [(ngModel)]="pickerData" (focus)="initPicker($event)" id="f-2" />
<div #timepicks></div> <!-- Here where the picker will be dynamically Injected -->
component.ts
import { Component, OnInit, ViewChild, Output, EventEmitter, HostListener, ViewContainerRef,
ComponentFactoryResolver } from '#angular/core';
import { TimepickComponent } from './timepick/timepick.component';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
pickerData: any;
#ViewChild('timepicks', {read: ViewContainerRef, static: false}) timepicks: ViewContainerRef;
constructor(
private _componentFactoryResolver: ComponentFactoryResolver
) {}
ngOnInit() {
}
initPicker = (event) => {
this.timepicks.clear();
let pickerComponentFactory =
this._componentFactoryResolver.resolveComponentFactory(TimepickComponent);
//Dynamically creating the ' TimepickComponent ' component
let pickerComponentRef = this.timepicks.createComponent(pickerComponentFactory);
(<TimepickComponent>(pickerComponentRef.instance)).pickerId = event.target.id; // Passing id
pickerComponentRef.instance.pickData.subscribe(res => {
this.pickerData = res;
pickerComponentRef.destroy();
});
}
}
Timepickcomponent.ts
.....
.....
#Input() pickerId: any;
#Output() pickData = new EventEmitter();
.....
.....
setClose = () => {
this.pickData.emit(this.valueHolder); // Emitting as output
}
Current Output
Screenshot 1
screenshot 2
As it can be seen, screen1 is opening based on text-box id, but in screen2, when I select and set, it gets populated in another text-box. Ideally, the selected picker from a text-box should bind with that particular text-box.
Any help would be appreciated.
I will use Document Service to access the element and set the value for clicked input control:
HTML:
<input type="text" (focus)="initPicker($event)" id="f-1" />
<input type="text" (focus)="initPicker($event)" id="f-2" />
<div #timepicks></div>
Removed Two way data binding
TS:
import { Component, OnInit, ViewChild, Output, EventEmitter, HostListener, ViewContainerRef, ComponentFactoryResolver, Inject } from '#angular/core';
import { TimepickComponent } from './timepick/timepick.component';
// Added Document reference
import { DOCUMENT } from '#angular/common';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'timepicker';
isVisible: boolean = false;
pickerData: any;
#ViewChild('timepicks', { read: ViewContainerRef, static: false }) timepicks: ViewContainerRef;
constructor(
#Inject(DOCUMENT) private document: HTMLDocument,
private _componentFactoryResolver: ComponentFactoryResolver
) { }
ngOnInit() {
}
initPicker = (event) => {
this.timepicks.clear();
let pickerComponentFactory = this._componentFactoryResolver.resolveComponentFactory(TimepickComponent);
let pickerComponentRef = this.timepicks.createComponent(pickerComponentFactory);
(<TimepickComponent>(pickerComponentRef.instance)).pickerId = event.target.id;
pickerComponentRef.instance.pickData.subscribe(res => {
console.log(event.target.id);
if (res) {
(<HTMLInputElement>this.document.getElementById(event.target.id)).value = res;
}
pickerComponentRef.destroy();
});
}
}
Import Inject from #angular/core and DOCUMENT from #angular/common
Injected in constructor: #Inject(DOCUMENT) private document: HTMLDocument
Used (<HTMLInputElement>this.document.getElementById(event.target.id)).value = res; to set the value attribute of clicked input element
Working_Demo
The problem here is you were binding both the input boxes with same variable pickerData. So, even if you change any one of them, the change will be reflected for both the values.
Since you have two textboxes you need two variables to store their values, like pickerData1 and pickerData2.
While calling the initPicker() you need to pass one more parameter i,e in which textbox the datepicker is currently being opened and store the date in that respective variable.
Html code
<input type="text" [(ngModel)]="pickerData1" (focus)="initPicker($event, 1)" id="f-1" />
<input type="text" [(ngModel)]="pickerData2" (focus)="initPicker($event, 2)" id="f-2" />
TS code
initPicker = (event, index) => {
this.timepicks.clear();
let pickerComponentFactory =
this._componentFactoryResolver.resolveComponentFactory(TimepickComponent);
//Dynamically creating the ' TimepickComponent ' component
let pickerComponentRef = this.timepicks.createComponent(pickerComponentFactory);
(<TimepickComponent>(pickerComponentRef.instance)).pickerId = event.target.id; // Passing id
pickerComponentRef.instance.pickData.subscribe(res => {
if (index === 1)
this.pickerData1 = res;
elseif (index === 2)
this.pickerData2 = res;
pickerComponentRef.destroy();
});

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

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