I have a problem with Anglular 8 and binding input parameters from parent component to child component.
I have the following setup:
-app.component.ts
import { Component } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'parent-child-binding';
showTitle: boolean = true;
childData = [];
onBoolean(){
this.showTitle = !this.showTitle;
}
onComplexAdd(){
this.childData.push("data 4");
this.childData.push("data 5");
}
onComplexEdit(){
this.childData[0] = "data 1 edited";
this.childData[1] = "data 2 edited";
}
onComplexNew(){
this.childData = [
"data 1",
"data 2",
"data 3"
]
}
}
-app.component.html
<button (click)="onBoolean()">Boolean Bind</button>
<button (click)="onComplexNew()">Complex Object New</button>
<button (click)="onComplexEdit">Complex Object Edit</button>
<button (click)="onComplexAdd">Complex Object Add</button>
<app-child [data] = "childData" [showTitle]="showTitle"></app-child>
-child.component.ts
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '#angular/core';
#Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit, OnChanges {
#Input() showTitle : boolean = true;
#Input() data : Array<any>;
constructor() { }
ngOnChanges(changes: SimpleChanges): void {
console.log(changes);
}
ngOnInit(): void {
}
}
-child.component.html
<h3 *ngIf="showTitle">Hello from child</h3>
<p *ngFor="let item of data">{{item}}</p>
So when I start I see the following:
and the console:
When I click on the first button, as expected the title Hello from child shows and disappears.
When I click on the second button, as expected I see:
and the console:
When I click on the third and forth buttons, nothing happens, not in the UI or console (the onChanges method seems that is not firing).
An I doing something wrong, or this that I want to achieve is not possible?
Best regards,
Julian
EDIT: After a comment and an answer from #MBB and #Apoorva Chikara, I've edited the code.
<button (click)="onBoolean()">Boolean Bind</button>
<button (click)="onComplexNew()">Complex Object New</button>
<button (click)="onComplexEdit()">Complex Object Edit</button>
<button (click)="onComplexAdd()">Complex Object Add</button>
<app-child [data] = "childData" [showTitle]="showTitle"></app-child>
The edition made the buttons to act (do something), but it is not what I expect.
What I mean:
When I click on the Complex Object Edit button in the UI I see:
But in the console, there is no ngOnChanges callback firing, but the binded object has changed, as we can see on the print screen (<p *ngFor="let item of data">{{item}}</p>) fired and printed out the new values.
The same happens when I click on the Complex Object Add button. In the UI I see:
But in the console the ngOnChanges callback is not firing, but the UI is containing the new added data.
I'm confused, can anyone advice please?
You have a very simple fix, you are not calling a function instead assigning its definition :
<button (click)="onComplexEdit()">Complex Object Edit</button> // call it as a function
<button (click)="onComplexAdd()">Complex Object Add</button>// call it as a function
The issue, you are facing for NgonChanges is due to the arrays passed by reference, this has a good explanation why this happens.
Related
i have an issue while click binding on dynamic html.I tried setTimeout function but click event not binding on button.i have also tried template referance on button and get value with #ViewChildren but #ViewChildren showing null value.
Typscript
export class AddSectionComponent implements OnInit {
sectionList: any = [];
constructor(private elRef: ElementRef,private _httpService: CommonService ,private sanitized: DomSanitizer) { }
ngOnInit(): void {
this.getSectionList();
}
ngAfterViewInit() {
let element = this.elRef.nativeElement.querySelector('button');
if (element) {
element.addEventListener('click', this.bindMethod.bind(this));
}
}
bindMethod() {
console.log('clicked');
}
sanitizeHtml(value: string): SafeHtml {
return this.sanitized.bypassSecurityTrustHtml(value)
}
getSectionList() {
//API request
this._httpService.get('/Section/GetSectionList').subscribe(res => {
if (res) {
this.sectionList = res.json();
//sectionList is returning below HTML
//<div class="wrapper">
// <button type='button' class='btn btn-primary btn-sm'>Click Me</button>
//</div>
}
})
}
}
Template
<ng-container *ngFor="let item of sectionList">
<div [innerHTML]="sanitizeHtml(item?.sectionBody)">
</div>
//innerHTML after rendering showing this
//<div class="wrapper">
// <button type='button' class='btn btn-primary btn-sm'>Click Me</button>
//</div>
</ng-container>
Short Answer, you are binding functions inside your templates, which means you have a new html content every time change detection runs, and change detection runs everytime a function is called, which means your button keeps on being updated infinitely, that's why it never works, Read more here please.
Now on how to do this, I would listen to ngDoCheck, and check if my button has a listener, if not, I will append the listener. I will also make sure to use on Push change detection, because if not, this will ngDoCheck will be called a lot, and maybe the button will be replaced more often, not quite sure about it.
Here is how the code would look like.
html
<!-- no more binding to a function directly -->
<div #test [innerHTML]='sanitizedHtml'></div>
component
import { HttpClient } from '#angular/common/http';
import { AfterViewChecked, AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, DoCheck, ElementRef, OnDestroy, ViewChild } from '#angular/core';
import { DomSanitizer, SafeHtml } from '#angular/platform-browser';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent implements DoCheck {
name = 'Angular';
people: any;
//now we are getting the div itself, notice the #test in the html part
#ViewChild('test')
html!: ElementRef<HTMLDivElement>;
//a property to hold the html content
sanitizedHtml!: SafeHtml;
constructor(private _http: HttpClient, private sanitized: DomSanitizer,private change: ChangeDetectorRef ) {}
ngDoCheck(): void {
//run with every change detection, check if the div content now has a button and attach the click event
if (this.html != undefined) {
let btn = this.html.nativeElement.querySelector('button');
if (btn && btn.onclick == undefined) {
btn.onclick = this.bindMethod.bind(this);
}
}
}
ngOnInit() {
this.peoples();
}
peoples() {
this._http.get('https://swapi.dev/api/people/1').subscribe((item: any) => {
const people = `<div class="wrapper">
<p>${item['name']}</p>
<button type='button' class='btn btn-primary btn-sm'>Click Me</button>
</div>`;
//assign the html content and notify change detection
this.sanitizedHtml = this.sanitized.bypassSecurityTrustHtml(people);
this.change.markForCheck();
});
}
bindMethod() {
console.log('clicked');
}
}
I don't like the approach because of the need to listen to ngDoCheck, this can run a lot, especially if you don't use onpush change detection.
I hope this helped.
I'm trying to remove a list item with the click button, tried various options but it seems to not work. Hope you can help me out with this.
On click i want to remove a list item from my users array. I will link the Typescript code alongside with the HTML.
//Typescript code
import { UsersService } from './../users.service';
import { Component, EventEmitter, Input, OnInit, Output } from '#angular/core';
import { Iuser } from '../interfaces/iuser';
#Component({
selector: 'tr[app-table-row]',
templateUrl: './table-row.component.html',
styleUrls: ['./table-row.component.css']
})
export class TableRowComponent implements OnInit {
#Input() item!: Iuser;
#Output() userDeleted = new EventEmitter();
removeUser(item: any) {
this.userDeleted.emit(item);
}
constructor() {}
ngOnInit(): void {}
}
<th scope="row">{{item.id}}</th>
<td>{{item.name}}</td>
<td>{{item.lastname}}</td>
<td>{{item.city}}</td>
<td> <button class="btn btn-sm" (click)="removeUser(item)">remove</button></td>
As #Priscila answered, when the button is clicked, you should only emit the action and let the parent component control the respective method i.e. delete or add.
Because that way, it will be easy for the data to be manipulated and handle the component's lifecycle.
Never keep the dead ends running on the app.
Happy Coding :)
I am learing how to make binding between the parent and the child using #Input, #Output and EventEmitter decorators.
in the html section posted below
<h1 appItemDetails [item]="currentItem">{{currentItem}}</h1>
currentItem has value equal to "TV". and i pass this value to the binding variable item.
i added console.log in ngOnInit that prints the value of item to make sure that the binding from the parent to the child is working as suppose to be.
in
<button (click) = addNewItem(item.value)></button>
in this button tag i am trying to pass the value of the binding variable item to the method addNewItem() as a parameter.
For the method addNewItem() it exists in the component and it should be invoked with the right parameter which is the value of the binding variable item
when i compile the App i revceive the error posted below.
please let me know how to pass the value of the binding variable to a method on button clicked
error
TS2339: Property 'item' does not exist on type 'AppComponent'.
2 <button (click) = addNewItem(item.value)></button>
~~~~
src/app/app.component.ts:5:16
5 templateUrl: './app.component.html',
~~~~~~~~~~~~~~~~~~~~~~
Error occurs in the template of component AppComponent.
app.component.ts:
import { Component, Input, Output, EventEmitter } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'InputOutputBindings';
currentItem = 'TV';
#Output() newItemValue = new EventEmitter<string>();
addNewItem(val : string) {
this.newItemValue.emit(val);
console.log("add new item");
}
}
item-details.directive.ts:
import { Directive, Input, Output, EventEmitter } from '#angular/core';
#Directive({
selector: '[appItemDetails]'
})
export class ItemDetailsDirective {
#Input() item : string = "";
constructor() { }
ngOnInit() {
console.log("ngOnInit->:" + this.item)
}
}
app.coponent.html:
<h1 appItemDetails [item]="currentItem">{{currentItem}}</h1>
<button (click) = addNewItem(item.value)></button>
you can add exportAs like below:
#Directive({
selector: '[appItemDetails]'
exportAs: 'customdirective',
})
export class ItemDetailsDirective {
....
}
and in your html, you can add a reference to directive and get the binded value:
<h1 #test="customdirective" appItemDetails [item]="currentItem">{{currentItem}}</h1>
<button (click) = addNewItem(test.item)></button>
Here i wrote a popup function and one Boolean flag is their in my component class. My flag will return true or false based on my conditions. In template class popup function needs to fire when flag becomes true, then immediately my popup dialog box will come. But i am not aware to call right approach, If any one knows please help me the correct approach.
<ng-template #sessionSuccessModal let-c="close" let-d="dismiss">
<div class="modal-header">
<h4 class="modal-title">Include Criteria Error</h4>
<button type="button" class="close" aria-label="Close" (click)="closeModel()">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body" class="bg-light text-dark">
<p>{{ alertMessage }}!</p>
</div>
<div style="text-align: center" class="bg-light text-dark">
<button type="button" (click)="closeModel()">Ok</button>
</div>
</ng-template>
intially commaSeparation will be false, this.commaSeparation = this.genericValidator.validateMultiComma(this.orderUnitForm); this function is returning either true or false. If it is true then i need to call my displayModel() alert method. Now popup is working fine, calling in ngAfterViewInit() but getting error in console like.
ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ng-untouched: true'. Current value: 'ng-untouched: false'
ngAfterViewInit() {
let controlBlurs: Observable<any>[] = this.formControls
.map((formControl: ElementRef) => Observable.fromEvent(formControl.nativeElement, 'blur'));
// debounceTime(1000)/
Observable.merge(this.orderUnitForm.valueChanges, ...controlBlurs).subscribe(value => {
this.displayMessage = this.genericValidator.processMessages(this.orderUnitForm);
// this.valid = this.genericValidator.validateControls(this.orderUnitForm);
});
this.orderUnitForm.valueChanges.debounceTime(1000).subscribe(value => {
this.valid = this.genericValidator.validateControls(this.orderUnitForm);
this.commaSeparation = this.genericValidator.validateMultiComma(this.orderUnitForm);
if(this.commaSeparation == true){
this.displayModel();
}
});
}
This is my dispalyModel() function:
displayModel() {
this.alertMessage = 'You cannot enter more than one multiple at the same time ';
this.successErrorModalBlock = this.modalService.open(this.sessionSuccessModalref);
}
You can achieve this implementing the interface OnChanges that is a lifecycle hook of the Angular.
import { Component, OnChanges, SimpleChanges } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnChanges {
// the flag property
commaSeparation: boolean;
ngOnChanges(changes: SimpleChanges){
if (changes['commaSeparation'] && changes['commaSeparation'].currentValue){
this.showPopup();
}
}
public showPopup(){
alert('Replace this alert by the code that shows your popup');
}
}
Reference: https://angular.io/guide/lifecycle-hooks#onchanges
In general, it's a bad idea to call functions with side effects inside an angular expression. Angular is free to call those functions however often it likes to make sure the result is still the same. You should use functions in these expressions to return a result, not to cause an action.
I would suggest instead calling popupAlert from your controller, e.g.
$scope.$watch('commaSeparation', function(commaSeparation) {
// The value of commaSeparation has just changed. Alert if it changed to true!
if (commaSeparation) {
popupAlert();
}
});
If commaSeparated is an input property, you can add a property change watcher using the ngOnChanges lifecycle hook and then call popupAlert if commaSeparation was changed (link to docs).
#Component({
...
})
export class MyComponent implements OnChanges {
// ...
#Input()
commaSeparation = false;
// ...
ngOnChanges(changes: SimpleChanges) {
const commaSeparationChanges = changes.commaSeparation;
if (commaSeparationChanges && commaSeparationChanges.currentValue) {
this.popupAlert();
}
}
}
If commaSeparated is only changed inside the component, then you can just make it private and work with a getter/setter pair to trigger the popup:
#Component({
...
})
export class MyComponent implements OnChanges {
// ...
private _commaSeparation = false;
// ...
get commaSeparation() {
return this._commaSeparation;
}
set commaSeparation(value) {
this._commaSeparation = value;
if (value) {
this.popupAlert();
}
}
}
Trying to do child to parent communication with #Output event emitter but is no working
here is the child component
import { Component, OnInit, Output, Input, EventEmitter } from '#angular/core';
#Component({
selector: 'app-emiter',
templateUrl: './emiter.component.html',
styleUrls: ['./emiter.component.css']
})
export class EmiterComponent implements OnInit {
#Output() emitor: EventEmitter<any>
constructor() { this.emitor = new EventEmitter()}
touchHere(){this.emitor.emit('Should Work');
console.log('<><><><>',this.emitor) // this comes empty
}
ngOnInit() {
}
}
this is the html template
<p>
<button (click)=" touchHere()" class="btn btn-success btn-block">touch</button>
</p>
The console.log inside the touchHere it shows nothing
even if I put this inside the parent component it show nothing as well
parent component
import { Component , OnInit} from '#angular/core';
// service I use for other stuff//
import { SenderService } from './sender.service';
// I dont know if I have to import this but did it just in case
import { EmiterComponent } from './emiter/emiter.component'
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app';
user: any;
touchThis(message: string) {
console.log('Not working: ${message}');
}
constructor(private mySessionService: SenderService) { }
}
and here is the html template
<div>
<app-emiter>(touchHere)='touchThis($event)'</app-emiter>
</div>
Parent component template:
<app-emitor (emitor)='touchThis($event)'></app-emiter>
In parent template #Output should be 'called', not the child method.
Also, see: https://angular.io/guide/component-interaction#parent-listens-for-child-event
Here’s an example of how we write a component that has outputs:
#Component({
selector: 'single-component',
template: `<button (click)="liked()">Like it?</button>`
})
class SingleComponent {
#Output() putRingOnIt: EventEmitter<string>;
constructor() {
this.putRingOnIt = new EventEmitter();
}
liked(): void {
this.putRingOnIt.emit("oh oh oh");
}
}
Notice that we did all three steps: 1. specified outputs, 2. created an EventEmitter that we attached
to the output property putRingOnIt and 3. Emitted an event when liked is called.
If we wanted to use this output in a parent component we could do something like this:
#Component({
selector: 'club',
template: `
<div>
<single-component
(putRingOnIt)="ringWasPlaced($event)"
></single-component>
</div>`
})
class ClubComponent {
ringWasPlaced(message: string) { console.log(`Put your hands up: ${message}`);
} }
// logged -> "Put your hands up: oh oh oh"
Again, notice that:
putRingOnIt comes from the outputs of SingleComponent
ringWasPlaced is a function on the ClubComponent
$event contains the thing that wasemitted, in this case a string
<app-emiter (emitor)="touchThis($event)" ></app-emiter>
By using #Output() you should apply the event you need to emit in the directive of the emitter component.Adding the name of the variable to the the directive and but the emitted over function inside the quotation passing the $event.
touchHere() is the method from which you are binding some value to emit with your EventEmitter. And your EventEmitter is 'emitor'.
So your code will work if you simply do the below:
<app-emiter (emitor)='touchThis($event)'></app-emiter>