i am trying to understand the callback ngOnChanges() so i created the below posted example. but at the compile time despite the interface Post
has values for its attributes title and content respectively, however, i do not receive any logs from ngOnChanges
please let me know how to use correctly
app.component.ts:
import { Component, OnInit, OnChanges, SimpleChanges,Output, EventEmitter } from '#angular/core';
export interface Post {
title:string;
content:string;
}
#Component({
selector: 'app-post-create',
templateUrl: './post-create.component.html',
styleUrls: ['./post-create.component.css']
})
export class PostCreateComponent implements OnInit {
#Output() post : Post;
#Output() onPostSubmittedEvtEmitter: EventEmitter<Post> = new EventEmitter<Post>();
constructor() {
this.post = {} as Post;
}
ngOnInit(): void {
}
ngOnChanges(changes: SimpleChanges) {
for (let changedProperty in changes) {
console.log("ngOnChanges->: changes[changedProperty].previousValue: " + changes[changedProperty].previousValue);
console.log("ngOnChanges->: changes[changedProperty].currentValue):" + changes[changedProperty].currentValue);
}
}
onSubmitPost(post: Post) {
this.post = {
title: this.post.title,
content: this.post.content
};
this.onPostSubmittedEvtEmitter.emit(this.post);
console.log("onSubmitPost->: post.title: " + post.title);
console.log("onSubmitPost->: post.content:" + post.content);
}
}
update 05.04.2021
as recommended i have added the ngOnChanges to observe changes in a prpoperty annotated with Input decorator as follows:
#Input() postsToAddToList: Post[] = [];
now, when I compile the code i add some values, i receive the following logs from ngOnChanges :
ngOnChanges->: changes[changedProperty].previousValue: undefined
post-list.component.ts:20 ngOnChanges->: changes[changedProperty].currentValue):
but the problem is when i keep adding more values, i do not receive any logs from the ngOnChanges
please let me know why despite i keep adding more values that result in changing the contents of the object that is decorated with #Input??!
post-list.component.ts:
import { Component, Input,OnInit, OnChanges, SimpleChanges,Output, EventEmitter } from '#angular/core';
import { Post } from '../post-create/post-create.component';
#Component({
selector: 'app-post-list',
templateUrl: './post-list.component.html',
styleUrls: ['./post-list.component.css']
})
export class PostListComponent implements OnInit {
constructor() {}
#Input() postsToAddToList: Post[] = [];
ngOnInit(): void {}
ngOnChanges(changes: SimpleChanges) {
for (let changedProperty in changes) {
console.log("ngOnChanges->: changes[changedProperty].previousValue: " + changes[changedProperty].previousValue);
console.log("ngOnChanges->: changes[changedProperty].currentValue):" + changes[changedProperty].currentValue);
}
}
}
ngOnChanges() only gets called when component's inputs changed from the parent component(fields that marked with #Input decorator). But you have #Output fields. The idea of ngOnChanges() is to react to changes that were done by the parent.
Following your business logic, you can handle whatever you want straight in onSubmitPost.
Answer for the update 05.04.2021
You add values to the array itself. Since the link to the array hasn't changed, ngOnChanges() does not catch these changes. But if you put new link to the component and do the following in the parent:
component:
this.yourArrInTheParent = [...this.yourArrInTheParent];
template:
<app-post-lis [postsToAddToList]="yourArrInTheParent"></app-post-lis>
Now value that you passed to the input changed and you will see the changes in the ngOnChanges(). The same goes for objects if you change object's property, angular won't see it as a change in ngOnChanges() since it only detects changes in #Input() values.
In order to catch those changes, you can use ngDoCheck hook. But it is power consuming, bear in mind not to perform heavy calculations there.
I think you are doing in correct way. Its just you missing to implement onChanges class. In latest Angular versions it straight throws error but in older version it does not.
Try this.
export class PostListComponent implements OnInit, OnChanges{
constructor() {}
#Input() postsToAddToList: Post[] = [];
ngOnInit(): void {}
ngOnChanges(changes: SimpleChanges) {
for (let changedProperty in changes) {
console.log("ngOnChanges->: changes[changedProperty].previousValue: " +
changes[changedProperty].previousValue);
console.log("ngOnChanges->: changes[changedProperty].currentValue):" +
changes[changedProperty].currentValue);
}
}
}
As already pointed out by #Vadzim Lisakovich
ngOnChanges() only gets called when component's inputs changed from
the parent component
Now, the thing is that the input is compared using === operator i.e. shallow comparison. If you add something to the post array, the reference to the array stays the same thus no event is triggered.
To fix that you can implement ngDoCheck() or replace the reference.
Here is a very similar question to yours:
Angular2 change detection: ngOnChanges not firing for nested object
And of cause the documentation:
https://angular.io/guide/lifecycle-hooks#docheck
Related
I have component and render it in cycle (ngFor)
The component has an object
#Input() a = {
name: 'Bob'
}
After the change, it will reset its input parameters to the initial state
When the component has changed, I want to get the values that were at its initial initialization
SCENARIO:
-> #Input() a = {name: 'Bob'} (init value)
-> then do something... this.a.name = 'Alice';
-> in the loop, the object changes
-> Once again, the component matters #Input() a = {name: 'Bob'} (init value)
I want to get the previous value after the component has been updated
that is 'Alice'
From the angular documentation:
ngOnChanges(changes: SimpleChanges) {
if(changes.a) {
let chng = changes.a;
let cur = JSON.stringify(chng.currentValue);
let prev = JSON.stringify(chng.previousValue);
}
}
So, you'll need the .currentValue and .previousValue property to access current and previous values.
Edit:1
If the component gets destroyed you have to use some kind of state management - service with subject, sessionStorage, localStorage or something else
Edit: 2
You can extend the ngFor directive and add the state logic inside ngOnChanges.
#Directive({
selector: '[ngFor][ngForIn]'
})
export class NgForIn extends NgFor implements OnChanges {
#Input() ngForIn: any;
constructor(viewContainer: ViewContainerRef,
template: TemplateRef<NgForRow>,
differs: IterableDiffers,
cdr: ChangeDetectorRef) {
super(viewContainer, template, differs, cdr);
}
ngOnChanges(changes: SimpleChanges): void {
// Do something here
}
}
I've two component displayed one at a time using a ngif directive.
<app-root>
<first-Comp *ngIf="showFirst"></first-Comp>
<second-Comp *ngIf="!showFirst"></second-Comp>
</app-root>
The Points are
The showFirst variable is initialized using true.
first-comp contains a element having height 100px;
second-comp have dynamic element
Inside the second component, i'm calculating the height using document.body.scrollHeight inside ngOnInit
The problem is when the showFrist becomes false angular first renders the second-comp then removes the first-comp. As result I am getting the height 100+ instead of 0. But I need the height of the body with only second-comp on component render.
Another important things I've missed to mention as I've thought that might not hamper. That is the first and second both components are detached from angular automatic change detection for performance. I've a base component like this
export class BaseComponent {
private subscriptions: Subscription[] = [];
constructor(private childViewRef: ChangeDetectorRef) {
this.childViewRef.detach();
}
public updateUI(): void {
try {
this.childViewRef.reattach();
this.childViewRef.detectChanges();
this.childViewRef.detach();
} catch (ex) {
// ignored
}
}
protected addSubscriptions(subs: Subscription) {
this.subscriptions.push(subs);
}
protected unSubscribeSubscriptions() {
this.subscriptions.forEach(item => item.unsubscribe());
this.subscriptions = [];
}
}
All the components inherit this BaseComponent except AppComponent
So the code of SecondComp looks something like this.
#Component({
selector: 'second-comp',
templateUrl: './SecondComponent.component.html',
styleUrls: ['./SecondComponent.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SecondComponent extends BaseComponent implements OnInit, AfterViewInit{
constructor(private ref:ChangeDetectorRef){
super(ref);
}
ngAfterViewInit(): void {
this.updateUi();
this.publishHeight()
}
ngOnInit() {
this.updateUi();
this.publishHeight()
}
}
Is there anything wrong for which I'm getting this unexpected behavior.
It feels like you are doing it the wrong way. You can inject #Self in second-comp constructor, it will give you the ElementRef of itself(second-comp).
constructor( #Self() private element: ElementRef ) {}
It might not work but it wont be affected by first-comp
ngOnInit() {
this.element.nativeElement.offsetHeight //the height for whatever you need it for
}
Calculate the height in setTimeout() in second-comp inside ngOnInit
setTimeout(() => {
//calculateHeight()
}, 200);
You should calculate the height when the component's view is fully rendered. That means calculating the height inside ngAfterViewInit() hook. See https://angular.io/api/core/AfterViewInit
How do I check if Output in Component changes? Then run another method
Here is Parent component,
After it gets data from Child, want to immediately run another event.
Parent HTML:
<div>
Address Type:*
<app-address-type-dropdown (selectedItemOutput) = "test"></app-address-type-dropdown>
</div>
Parent Typescript:
Goal: When value is outputted, detect changes in this parent, and write console command.
export class AddressFormatheaderFormComponent implements OnInit {
constructor() { }
public test: any;
public sayHi(){
console.log(this.test);
}
ngOnInit() {
}
}
You can do so by creating another function, an event handler essentially. So when your child component <app-address-type-dropdown> emits a value, this event handler will take care of what to do next.
E.g. onNewItemSelect($event) is the event handler.
<div>
Address Type:*
<app-address-type-dropdown (selectedItemOutput)="onNewItemSelect($event)"></app-address-type-dropdown>
</div>
export class AddressFormatheaderFormComponent implements OnInit {
constructor() { }
public test: any;
ngOnInit() {
}
onNewItemSelect(itemSelected){
this.test = itemSelected;
console.log(this.test);
//do something else
}
}
Do have read on this section of of Angular official docs on component interaction for more information.
I think you will need EventEmitter so in child component, when value changes, it will emit event and in parent component, it will detect changes and call parent function.
Something like let's say your child component.
import { Component, EventEmitter, Output } from '#angular/core';
#Component({
selector: 'app-address-type-dropdown,
template: `<button class='btn btn-primary' (click)="valueChanged()">Click me</button> `
})
export class AppAddressTypeDropdown{
#Output() selectedItemOutput= new EventEmitter();
Counter = 0;
valueChanged() { // You can give any function name
this.counter = this.counter + 1;
this.selectedItemOutput.emit(this.counter);
}
}
And in parent html, just try update call slightly. Please call any function to know when it changes.
<app-address-type-dropdown (selectedItemOutput) = "changeDetect($event)"></app-address-type-dropdown>
export class AddressFormatheaderFormComponent implements OnInit {
constructor() { }
public test: any;
public sayHi(){
console.log(this.test);
}
ngOnInit() {
}
changeDetect(counter){
console.log(counter);
//do something here
}
I have calendar component with data property decorated as #Input():
import { Component, OnInit, Input, OnChanges, SimpleChanges } from '#angular/core';
#Component({
selector: 'app-calendar',
templateUrl: './calendar.component.html',
styleUrls: ['./calendar.component.css']
})
export class CalendarComponent implements OnInit, OnChanges {
#Input() data: CalendarDay[];
constructor() {
this.data = [];
}
ngOnInit() {
this.initDays();
}
ngOnChanges(changes: SimpleChanges) {
console.log(this.data);
console.log(changes.data);
}
}
I pass data in from another component like that:
<app-calendar [data]="this.calendarData"></app-calendar>
And passed data gets rendered by *ngFor in the calendar component (it renders perfectly and everything works just fine):
<div *ngFor="let item of data">{{item.date}}</div>
I want to parse this data first before rendering it into view and whenever i try to console.log data property within the calendar component i get strange array, its shows as empty, i can 'open' it from browser console:
.
And when i try to log value like that:
console.log(this.data[0])
or
console.log(changes.data.currentValue[0])
i get undefined value.
Delete this.data = [] from constructor, avoid change anything when you use dependecy injecton.
And use set and get for each Input() that you want to use in your template, it's a best practice.
private _data: CalendarDay[];
#Input() set data(data: CalendarDay[]) {
if(data) {
this._data = data;
}
}
get data(): CalendarDay[] {
return this._data;
}
And in your HTML you should pass it with:
<app-calendar [data]="calendarData"></app-calendar>
In calendar component you can use with
<div *ngFor="let item of data">{{item.date}}</div>
I know how to raise an event with the EventEmitter. I can also attach a method to be called if I have a component like this:
<component-with-event (myevent)="mymethod($event)" />
When I have a component like this, everything works great. I moved some logic into a service and I need to raise an event from inside the Service. What I did was this:
export class MyService {
myevent: EventEmitter = new EventEmitter();
someMethodThatWillRaiseEvent() {
this.myevent.next({data: 'fun'});
}
}
I have a component that needs to update some value based on this event but i can't seem to make it work. What I tried was this:
//Annotations...
export class MyComponent {
constructor(myService: MyService) {
//myService is injected properly and i already use methods/shared data on this.
myService.myevent.on(... // 'on' is not a method <-- not working
myService.myevent.subscribe(.. // subscribe is not a method <-- not working
}
}
How do i make MyComponent subscribe to the event when the service that raises it is not a component?
I'm on On 2.0.0-alpha.28
EDIT: Modified my "working example" to actually work, so focus can be put on the not-working part ;)
Example code:
http://plnkr.co/edit/m1x62WoCHpKtx0uLNsIv
Update: I have found a better/proper way to solve this problem using a BehaviorSubject or an Observable rather than an EventEmitter. Please see this answer: https://stackoverflow.com/a/35568924/215945
Also, the Angular docs now have a cookbook example that uses a Subject.
Original/outdated/wrong answer: again, don't use an EventEmitter in a service. That is an anti-pattern.
Using beta.1... NavService contains the EventEmiter. Component Navigation emits events via the service, and component ObservingComponent subscribes to the events.
nav.service.ts
import {EventEmitter} from 'angular2/core';
export class NavService {
navchange: EventEmitter<number> = new EventEmitter();
constructor() {}
emitNavChangeEvent(number) {
this.navchange.emit(number);
}
getNavChangeEmitter() {
return this.navchange;
}
}
components.ts
import {Component} from 'angular2/core';
import {NavService} from '../services/NavService';
#Component({
selector: 'obs-comp',
template: `obs component, item: {{item}}`
})
export class ObservingComponent {
item: number = 0;
subscription: any;
constructor(private navService:NavService) {}
ngOnInit() {
this.subscription = this.navService.getNavChangeEmitter()
.subscribe(item => this.selectedNavItem(item));
}
selectedNavItem(item: number) {
this.item = item;
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
#Component({
selector: 'my-nav',
template:`
<div class="nav-item" (click)="selectedNavItem(1)">nav 1 (click me)</div>
<div class="nav-item" (click)="selectedNavItem(2)">nav 2 (click me)</div>
`,
})
export class Navigation {
item = 1;
constructor(private navService:NavService) {}
selectedNavItem(item: number) {
console.log('selected nav item ' + item);
this.navService.emitNavChangeEvent(item);
}
}
Plunker
Using alpha 28, I accomplished programmatically subscribing to event emitters by way of the eventEmitter.toRx().subscribe(..) method. As it is not intuitive, it may perhaps change in a future release.
Sometime quick fix of library cause that added event import like
import { EventEmitter } from 'events';
You must change it with core libray using subscribe
import { EventEmitter } from '#angular/core';