My app.component.ts
import { Component, Input, OnInit, OnChanges, SimpleChanges} from '#angular/core';
import {Counter } from './counter'
#Component({
selector: 'my-app',
template: `
<custom-counter [(counter)]="counterArray" (counterChange)="myValueChange($event);"></custom-counter>
<p><code>counterValue = {{counterValue}}</code></p>
<hr>
`
})
export class AppComponent implements OnChanges{
counterArray:Counter[]
counterValue = 5;
constructor(){
this.counterArray=[{id:0,value:0},{id:1,value:1}]
}
myValueChange(event:Counter[]) {
console.log(event);
}
}
my counter.ts
export class Counter {
id: number;
value: number;
}
and custom-counter component:
import { Component, Input, Output, EventEmitter } from '#angular/core';
import { Counter } from './counter';
#Component({
selector: 'custom-counter',
template: `
First counter
<button (click)="decrement()">-</button>
<span>{{this.counter[1].value}}</span>
<button (click)="increment()">+</button>
`
})
export class CustomCounterComponent {
#Input() counter : Counter[];
#Output() counterChange = new EventEmitter();
decrement() {
this.counter[1].value--;
this.counterChange.emit({
value: this.counter
})
}
increment() {
this.counter[1].value++;
this.counterChange.emit({
value: this.counter
})
}
}
My plan was that if user presses button on from the child component parent is informed about it and print something in console.
unfortunately when user press button error below is thrown:
"Error in ./CustomCounterComponent class CustomCounterComponent - inline template:3:10 caused by: Cannot read property 'value' of undefined"
I know that this exception is quite strait forward but I can not find why something is undefined while I pass everything.
If I comment out lines with emit no error occur but then I do not have any notifications for parent
Problem was in in-proper calling the emit method.
Calling like this:
this.counterChange.emit({
value: this.counter
})
was creating new object that was emited and that someway messed object binding. ( If anyone can explain this better then please do it).
after changing call to this:
this.counterChange.emit(this.counter)
when I emit strait the input object everything started to be working.
In your CustomCounterComponent class the template has an issue.
Try the following in your template:
template: `
First counter
<button (click)="decrement()">-</button>
<span>{{counter[1]?.value}}</span>
<button (click)="increment()">+</button>
`
? is a safety operator that would not throw an exception when counter[1] is undefined
Also note this is not required with counter in the template
The following method expects an an Array whereas it receives an object.
myValueChange(event:Counter[]) {
console.log(event);
}
Related
I am facing a situation I don't understand. I have a parent component (app.component in the example) that gets its data from an API as an observable
This data is then passed down to the child (hello.component) using the async pipe.
That child then receives the input, but when ngOnInit runs in the child, the input is null.
I don't understand why, and I don't know how to make it so that the input is the actual returned value from the API instead. The call to detectChanges() in app.component was a desperate attempt to trigger change detection in the child but that doesn't re-run ngOnInit so it's kinda moot. I left it there because that's how the actual code I'm working with looked like.
I know this code is terrible.I didn't write it. Unfortunately, the component I'm working with is like that and I can't refactor it because, you guessed it, there are no unit tests. I'm working on cleaning it all up, but for now I have to reuse that component the way it is.
Stackblitz: https://stackblitz.com/edit/angular-ivy-rw1xte?devtoolsheight=33&file=src/app/hello.component.ts
// app.component.ts
import { ChangeDetectorRef, Component, VERSION } from "#angular/core";
import { interval, Observable, of } from "rxjs";
import { mapTo, tap } from "rxjs/operators";
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
public name$: Observable<any> = of({});
constructor(private cdRef: ChangeDetectorRef) {}
public ngOnInit() {
this.name$ = interval(3000).pipe(
mapTo(() => {
first: "Eunice";
}),
tap(() => this.cdRef.detectChanges())
);
}
}
<!-- app.component.html -->
<hello [name]="name$ | async"></hello>
<p>
Start editing to see some magic happen :)
</p>
// hello.component.ts
import { Component, Input } from "#angular/core";
#Component({
selector: "hello",
template: `
<div *ngIf="name">
<h1>Hello {{ this.name.first }}!</h1>
<h1>{{ greetings }}</h1>
</div>
`,
styles: [
`
h1 {
font-family: Lato;
}
`
]
})
export class HelloComponent {
#Input() name: { first?: string }
public greetings: string = "";
public firstName: string = "";
public async ngOnInit() {
console.log('name:',this.name); // name: null
if (this.name) {
// this conditional is always false because this.name is always null
// and so this never runs.
console.log("got name");
this.firstName = this.name.first || "fallback";
this.greetings = await new Promise(resolve =>
resolve("how do you do, ${firstName}?")
);
}
}
}
this.name$ = interval(3000).pipe(
mapTo({
first: "Eunice"
}),
tap(() => this.cdRef.detectChanges())
);
Please fix it like the above. mapTo doesn't need to accept a function. It didnt make an error for you? :)
Also, the name input is null right when the hello component is mounted.
You need to check the name in ngOnChanges or you need to mount the hello component only when the name is available.
For example:
<hello *ngIf="name$ | async as name" [name]="name"></hello>
Or
public async ngOnChanges() {
console.log('name:', this.name); // name: null
if (this.name) {
...
}
}
I have an object from parent component also received in a child component similar to this:
{
attribute: 'aaaa',
attribute2: [
{
value
},
{
value
},
{
value
},
]
}
This object is an #Input from a parent component. When I make changes to the objects inside the attribute2 array, I would like the child component detect that changes were made and then gets updated. As this is an object, I could'nt make it work, so I clone the entire object (this.objet = _.cloneDeep(this.object) in the parent component so then the child component detects that changes happened.
Is there any other way of doing this that does not clone the entire object? Thanks in advance
EDIT:
Child Component
export class ChildComponent implements OnInit, OnChanges {
#Input() public object: any;
}
html
<div>
<span>{{object.attribute}}</span>
<div *ngFor="let items of object.attribute2">{{item.value}}</div>
</div>
Parent Component
export class ParentComponent implements OnInit {
public object: any;
updateObject() {
this.object.attribute2[1] = 'Changed value';
this.object = _.cloneDeep(this.object);
}
}
html
<div>
<child-component [object]="object"></child-component>
</div>
An efficient way is to use EventEmitter and service communication to
trigger changes in the child component.
On way as mentioned by #Tony is to use ngOnChanges(). It is a good shortcut for detecting bounded properties change but as you add more and more bindings, using this hook will affect you application in the long run because it will run every time any of the bound property changes whether or not you desire it all the calls.
So for Service based communication, I've created an example on
Stackblitz:
https://stackblitz.com/edit/angular-fgut7t
Gist: https://gist.github.com/stupidly-logical/a34e272156b498513505127967aec851
In this example, I am binding an Array to the child component using #Input() an on addition of new data, the array is updated by the parent and the latest value is passed on the service which then emits this value. The child component subscribes to this value and the relevant code is executed.
The Service:
import { Injectable, EventEmitter } from '#angular/core';
#Injectable({
providedIn: "root"
})
export class DataService {
dataUpdated:EventEmitter<any> = new EventEmitter();
constructor() { }
setLatestData(data) {
this.dataUpdated.emit(data);
}
}
Child Component TS
import { Component, OnInit, Input } from '#angular/core';
import { DataService } from '../data-service.service';
#Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit {
#Input() allData: [];
latestData: any;
constructor(private dataService: DataService) { }
ngOnInit() {
this.dataService.dataUpdated.subscribe((data) => {
this.latestData = data;
});
}
}
Child Component HTML
<p>
Latest Data: {{ latestData }}
</p>
<h3>List:</h3>
<li *ngFor="let data of allData">
{{ data }}
</li>
Parent Component TS
import { Component } from '#angular/core';
import { DataService } from './data-service.service'
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
name = 'Angular';
dataArr = [];
constructor(private dataService: DataService){}
onAddTimestamp() {
let timestamp = new Date();
this.dataArr.push(timestamp);
this.dataService.setLatestData(timestamp);
}
}
Parent Component HTML
<hello name="{{ name }}"></hello>
<p>
Start editing to see some magic happen :)
</p>
<button
(click)="onAddTimestamp()"
>
Add Timestamp
</button>
<app-child
[allData] = "dataArr"
></app-child>
Use the ngOnChanges() lifecycle method in your component.
ngOnChanges is called right after the data-bound properties have been
checked and before view and content children are checked if at least
one of them has changed.
Some like this
#Input() object: string;
ngOnChanges(changes: SimpleChanges) {
console.log(changes.object.currentValue);
// You can also use object.previousValue and
// object.firstChange for comparing old and new values
}
I have a child TestComponent component as follows:
import { Component, OnInit, Input } from '#angular/core';
import { ApiService } from '../../../api.service';
#Component({
selector: 'app-test',
templateUrl: './test.component.html'
})
export class TestComponent implements OnInit {
constructor(private apiService: ApiService) { }
testDisplayMessage = 'No data to show';
ngOnInit() {
}
getMessage(param: string) {
this.callingTest = true;
this.apiService.getTest( param ).subscribe( data => {
this.setTestDisplayMessage( data );
this.callingTest = false;
}, err => {
console.log( JSON.stringify( err ) );
this.setTestDisplayMessage( 'Failed to get data' );
this.callingTest = false;
} );
}
setTestDisplayMessage( message: string ) {
this.testDisplayMessage = message;
}
}
contents of test.component.html
<p style="padding: 10px;">{{ testDisplayMessage }}</p>
Use in parent componet :
Trigger JS Code in parent component on button click,
import { TestComponent } from './test/test.component';
....
.....
#Component({
providers: [ TestComponent ],
templateUrl: 'parent.component.html'
})
export class ParentComponent implements OnInit {
...
constructor(private testComponent: TestComponent) { }
...
// Button on parent template triggers this method
getMessage() {
this.testComponent.getMessage('Hello');
}
...
}
Html tag added in parent component,
<app-test></app-test>
When I debugged above code trigger point, call to setTestDisplayMessage() happens the field testDisplayMessage in TestComponent gets changed but UI shows the old message 'No data to show', why is the message on change does not reflect on UI template? Or this is not the way it is supposed to get used? Shall I use #Input
Update:
Based on the pointers given in the following answers as well as comment sections, I changed my component as #ViewChild so in above parent component instead of passing the child component as an argument to constructor I declared it as child component using #ViewChild, so code changes as follows,
Earlier wrong code
constructor(private testComponent: TestComponent) { }
Solution
#ViewChild(TestComponent)
testComponent: TestComponent;
I found this article useful.
Use #ViewChild()
In html file:
<app-test #childComp></app-test>
In parent component.ts file
import { Component, OnInit, ViewChild } from '#angular/core';
....
.....
#Component( {
templateUrl: 'parent.component.html'
} )
export class ParentComponent implements OnInit {
#viewChild('childComp') childComp: any;
constructor() { }
...
// Button on parent template triggers this method
getMessage() {
this.childComp.getMessage('Hello');
}
...
}
Update:
Based on the pointers given in the following answers as well as comment sections, I changed my component as #ViewChild so in above parent component instead of passing the child component as an argument to constructor I declared it as child component using #ViewChild, so code changes as follows,
Earlier wrong code
constructor(private testComponent: TestComponent) { }
Solution
#ViewChild(TestComponent)
testComponent: TestComponent;
I found this article useful.
definitely use #Input() but on set method
#Input()
set someProperty(value) {
// do some code
}
now every time you pass new value here, code will run
basically, your approach is wrong, please use Input() or Services to share data between components.
however, if you want to make ur code work, the below may work
import change detector
constructor(private cdRef: ChangeDetectorRef) {
}
note: import reference ->
import { ChangeDetectorRef } from '#angular/core';
execute detect change after the value is updated
setTestDisplayMessage( message: string ) {
this.testDisplayMessage = message;
this.cdRef.detectChanges();
}
I hope this helps
I get the following error when running ng test:
Can't bind to 'foobar' since it isn't a known property of 'app-my-item'.
1. If 'app-my-item' is an Angular component and it has 'foobar' input, then verify that it is part of this module.
2. If 'app-my-item' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '#NgModule.schemas' of this component to suppress this message.
Component:
import { Component, Input, OnInit, Renderer, EventEmitter, Output, OnChanges, SimpleChanges } from '#angular/core';
#Component({
selector: 'app-my-item',
templateUrl: './app-item.component.html',
styleUrls: ['./app-item.component.scss'],
})
export class MyItemComponent implements OnInit, OnChanges {
#Input() video;
#Input() foobar = <string> '';
buttonClass = 'green';
// Code....
ngOnChanges({ foobar: { currentValue } }: SimpleChanges): void {
if (currentValue) {
this.buttonClass = (
currentValue === this.video.key ? 'blue' : 'green'
);
}
}
// Code....
}
View:
<app-my-item
//Code ....
[foobar]="foobar"
//Code ....
>
</app-my-item>
I don't understand why I'm getting the error because foobar is in fact defined in the component as an input. How to fix this error?
I'm just starting with Angular2, reading the official docs. However, I have not found specific details about how and when the binding happens, and things don't seem to work as I expected.
I have a simple child component
#Component({
selector: 'dummy',
template: `
<div>{{data}}</div>
`
})
export class Dummy {
#Input() data;
}
and a root component
#Component({
selector: 'main',
template: `
<h1>hello</h1>
<dummy [data]="data"></dummy>
`
})
export class MainComponent {
data: string = "initial text";
ngOnInit() {
setTimeout(this.initData, 5000);
}
initData() {
this.data = "new text";
}
}
I would expect the text shown by the child component to change after 5 seconds, however it doesn't. What am I doing wrong? Does the documentation explain when and under what conditions bound values are initialized and updated?
you're losing context of this. at the time when setTimeout callback runs, this doesn't point to the component anymore. you might want to check a bit about javascript this problem.
try:
setTimeout(()=>{
this.data = "new text";
},5000);
You forgot to import the OnInit interface and make your component implement it.
import { Component, OnInit } from '#angular/core';
And then
export class MainComponent implements OnInit { ... }