How to get previous value when component has been destroyed in Angular - javascript

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
}
}

Related

ngOnChanges do not display changes expected in a variable

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

Angular #Input() value is not changed when the value is same

When I run this program, initially I get the output as false, 5, 500 as I have initialized them in the child component like that, but when I try to click on update button, I am not able to revert to the previous values. I am expecting the output to be true, 10, 1000, but I am getting it as false, 5, 1000. Only the number which was different got changed.
How can I solve this issue so that I can get the values whatever I set in the parent component?
Link to stackblitz.
app-component.html
<span (click)="clickme()">Update data</span>
app-component.ts
export class AppComponent {
public parentBool: boolean;
public parentNum: number;
public p2: number;
public ngOnInit(): void {
this.parentBool = true;
this.parentNum = 10;
this.p2 = 100;
}
public clickme(): void {
this.parentBool = true;
this.parentNum = 10;
this.p2 = 1000;
}
}
hello.component.ts
#Component({
selector: 'hello-comp',
template: `
<div>
Boolean value is - {{boolChild}} <br/>
Number value is - {{numChild}} <br />
Second number is - {{numChild2}}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyComponent {
#Input() boolChild: boolean;
#Input() numChild: number;
#Input() numChild2: number;
public ngOnInit(): void {
this.boolChild = false;
this.numChild = 5;
this.numChild2 = 500;
}
constructor(private _cd: ChangeDetectorRef){}
public ngOnChanges(changes: {}): void {
// this._cd.detectChanges();
console.log(changes);
}
}
Input bindings occur right before ngOnInit, you’re overriding the values in ngOnInit.
Put your defaults in the declaration:
#Input() numChild: number = 5;
Like that
Well your problem is when everything is initialized
Parent value is True and Child Value is False
You are clicking on parent span. This sets the parentBool value to True. This is not a change when it comes to Parent Component. Hence the Change detection does not fire.
I forked this https://stackblitz.com/edit/angular-rgdgd6
from your link below
https://stackblitz.com/edit/angular-jv3sjz
You can try 2 different approachs
Capture ViewChildren and update within Parent https://stackblitz.com/edit/angular-4fqlqg
Have a service to maintain intermediate state and children will listen to that state. (if required use ngrx or else a simple service will suffice) (Prefer this a bit better as this is more scaleable)
have you tried calling detectChanges before anything else?
ngOnInit() {
this._cd.detectChanges();
....rest of your code...
}
Btw, I don't understand why you are setting the same values in the ngOnInit() as on line 14 & 15. Actually you don't need to...
This is how the input variables in the child component is initialized at the moment.
Initialize AppComponent, trigger ngOnInit and render the template.
Initialize MyComponent, trigger ngOnChanges. This will show in the console log as follows
{
"boolChild": {
"currentValue": true,
"firstChange": true
},
"numChild": {
"currentValue": 10,
"firstChange": true
},
"numChild2": {
"currentValue": 100,
"firstChange": true
}
}
Trigger ngOnInit (note: this is triggered after ngOnChanges) in MyComponent assign values false, 5 and 500. This is the reason why these values are shown in the template and not the one sent initially from the AppComponent.
Click Update data button in the AppComponent which will push values true, 10 and 1000. It will show the following output
{
"numChild2": {
"previousValue": 100,
"currentValue": 1000,
"firstChange": false
}
}
Explanation
According to ngOnChanges in the MyComponent, the previous values were true, 10 and 100 (from the ngOnInit of the AppComponent). The values false, 5 and 500 were not registered by the ngOnChanges because they weren't changes through the Input(). And so changes shows the only object that has been changed from it's previous state, which is numChild2. The other 2 values stay the same and they aren't reflected in the ngOnChanges.
One solution would be to avoid assigning values in the ngOnInit() hook in the child and assigning the values during definition.
_boolChild: boolean = false;
_numChild: number = 5;
_numChild2: number = 500;
Even with these changes, you will still observe only numChild2 object in the changes variable when you click the button because that is how SimpleChanges works. It will only show the value that has been changed from it's previous state, it won't reflect the values that stay the same.
Here is the hook calling order for reference
OnChanges
OnInit
DoCheck
AfterContentInit
AfterContentChecked
AfterViewInit
AfterViewChecked
OnDestroy

NgOnChanges overrides form control value when user types

I have autocomplete form control:
#Component({
selector: 'app-autocomplete',
templateUrl: './app-autocomplete.view.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AutoCompleteFilterComponent implements OnInit, OnDestroy, OnChanges {
#Input() value: any;
#Output() onChanged = new EventEmitter();
autoCompleteControl: FormControl = new FormControl();
private autoCompleteControlSubscription: Subscription;
constructor() { }
ngOnInit(): void {
this.autoCompleteControl.setValue(this.value, { emitEvent: false });
this.autoCompleteControlSubscription = this.autoCompleteControl.valueChanges
.pipe(
skipUndefined(),
filter(value => value.length >= 3),
distinctUntilChanged(),
debounceTime(350),
map(value => {
this.onChanged.emit(value.trim());
})
).subscribe();
}
ngOnChanges(changes: SimpleChanges): void {
if (!changes.value.firstChange) {
this.autoCompleteControl.setValue(changes.value.currentValue);
}
}
ngOnDestroy(): void {
if (this.autoCompleteControlSubscription) {
this.autoCompleteControlSubscription.unsubscribe();
}
}
I get initial value from store and pass it as #Input variable:
this.value$ = this._store$.select(s=>s.value);
<app-autocomplete [value]="value$ | async"></app-autocomplete>
The problem that I ran into is:
Component loads and I pass initial value from the store.
User types something in input text field.
User stops typing for 350ms (debounce time).
I emit value to the parent and use an Action + Reducer to keep the value in the store.
this.value$ Observable reacts on store change and triggers ngOnChange method.
User continue typing.
Value from the store overwrites what user has already typed.
For example user typed "stri", then made short pause, then typed "string", but "store" value overwrites his "string" value and he got "stri" value that I put into the "store" before.
Has anyone come across this before? The only solution we come up with is to check the focus and don't set new value.
You're subscribing to changes in ngOnInit:
this.autoCompleteControlSubscription = this.autoCompleteControl.valueChanges
And ngOnChanges too.
this.autoCompleteControl.setValue(changes.value.currentValue);
I'm going to take a shot at what you're trying to do:
On init you may want to patchValue and then setup the subscription so they do not interfere with each other.
If you want to patch value without triggering the form's valueChanges then patch without event:
this.form.controls[control].patchValue(value, { emitEvent: false });
Take a look at how I'm doing this with my own ControlValueAccessor control component on StackBlitz. I set the initial control value (if there is any) with writeValue

Observable of Component Attribute Changes in Angular2

When creating a component in angular 2 that has inputs attributes via #Input, how can I get an observable from the changes made to that attribute #Input (not to be confused with user form input).
export class ExampleComponent implement OnChanges{
#Input() userObject: User;
ngOnChanges(changes: any): void{
// Validate that its the 'userObject' property first
this.doStuff()
}
}
In practice, I would like to merge the Observable changes of the userObject with the Observable changes of other things to have a fluent change reaction pattern.
export class ExampleComponent implement OnChanges{
#Input() userObject: User;
constructor():{
userObject.valueChanges.subscribe(x=>{ this.doStuff() });
}
}
I found out the that BehaviorSubject class enables this scenario the best. Instead of creating a separate backend field, you can use the BehaviorSubject's getValue function to peak at the current value. Then use the backing BehaviorSubject to view as an observable for changes.
export class ExampleComponent{
private _userObject: BehaviorSubject<User> = new BehaviorSubject<User>(null);
#Input()
set userObject(value: User): { this._userObject.next(value); }
get userObject(): User { return this._userObject.getValue(); }
}
Try usings a get and a set, valueChanges() below will fire on being set.
private _userObject: User;
#Input()
set userObject(userObject: User) {
this._userObject = userObject;
this.valueChanges();
}
get userObject(): User {
return this._userObject;
}
With an Observable:
private userObjectChange = new Subject<User>();
userObjectChange$ = this.userObjectChange.asObservable();
private _userObject: User;
#Input()
set userObject(userObject: User) {
this.userObjectChange.next(userObject);
this._userObject = userObject;
}
get userObject(): User {
return this._userObject;
}
To subscribe:
this.newQuote.subscribe(user => {...})
You can use subject for this:
export class ExampleComponent {
#Input() set userObject(userObject: User) {
this.userObject$.next(userObject);
}
private userObject$ = new Subject<User>();
constructor():{
this.userObject$.subscribe(x=>{ this.doStuff() });
}
}
The best way to check the change of an input is actually by using the ngOnChanges life cycle.
ngOnChanges(changes: { [propertyName: string]: SimpleChange }) {
const changedInputs = Object.keys(changes);
// Only update the userObject if the inputs changed, to avoid unnecessary DOM operations.
if (changedInputs.indexOf('userObject') != -1) {
// do something
}
}
Reference: https://github.com/angular/material2/blob/master/src/lib/icon/icon.ts#L143

Handle #Input and #Output for dynamically created Component in Angular 2

How to handle/provide #Input and #Output properties for dynamically created Components in Angular 2?
The idea is to dynamically create (in this case) the SubComponent when the createSub method is called. Forks fine, but how do I provide data for the #Input properties in the SubComponent. Also, how to handle/subscribe to the #Output events the SubComponent provides?
Example:
(Both components are in the same NgModule)
AppComponent
#Component({
selector: 'app-root'
})
export class AppComponent {
someData: 'asdfasf'
constructor(private resolver: ComponentFactoryResolver, private location: ViewContainerRef) { }
createSub() {
const factory = this.resolver.resolveComponentFactory(SubComponent);
const ref = this.location.createComponent(factory, this.location.length, this.location.parentInjector, []);
ref.changeDetectorRef.detectChanges();
return ref;
}
onClick() {
// do something
}
}
SubComponent
#Component({
selector: 'app-sub'
})
export class SubComponent {
#Input('data') someData: string;
#Output('onClick') onClick = new EventEmitter();
}
You can easily bind it when you create the component:
createSub() {
const factory = this.resolver.resolveComponentFactory(SubComponent);
const ref = this.location.createComponent(factory, this.location.length, this.location.parentInjector, []);
ref.someData = { data: '123' }; // send data to input
ref.onClick.subscribe( // subscribe to event emitter
(event: any) => {
console.log('click');
}
)
ref.changeDetectorRef.detectChanges();
return ref;
}
Sending data is really straigthforward, just do ref.someData = data where data is the data you wish to send.
Getting data from output is also very easy, since it's an EventEmitter you can simply subscribe to it and the clojure you pass in will execute whenever you emit() a value from the component.
I found the following code to generate components on the fly from a string (angular2 generate component from just a string) and created a compileBoundHtml directive from it that passes along input data (doesn't handle outputs but I think the same strategy would apply so you could modify this):
#Directive({selector: '[compileBoundHtml]', exportAs: 'compileBoundHtmlDirective'})
export class CompileBoundHtmlDirective {
// input must be same as selector so it can be named as property on the DOM element it's on
#Input() compileBoundHtml: string;
#Input() inputs?: {[x: string]: any};
// keep reference to temp component (created below) so it can be garbage collected
protected cmpRef: ComponentRef<any>;
constructor( private vc: ViewContainerRef,
private compiler: Compiler,
private injector: Injector,
private m: NgModuleRef<any>) {
this.cmpRef = undefined;
}
/**
* Compile new temporary component using input string as template,
* and then insert adjacently into directive's viewContainerRef
*/
ngOnChanges() {
class TmpClass {
[x: string]: any;
}
// create component and module temps
const tmpCmp = Component({template: this.compileBoundHtml})(TmpClass);
// note: switch to using annotations here so coverage sees this function
#NgModule({imports: [/*your modules that have directives/components on them need to be passed here, potential for circular references unfortunately*/], declarations: [tmpCmp]})
class TmpModule {};
this.compiler.compileModuleAndAllComponentsAsync(TmpModule)
.then((factories) => {
// create and insert component (from the only compiled component factory) into the container view
const f = factories.componentFactories[0];
this.cmpRef = f.create(this.injector, [], null, this.m);
Object.assign(this.cmpRef.instance, this.inputs);
this.vc.insert(this.cmpRef.hostView);
});
}
/**
* Destroy temporary component when directive is destroyed
*/
ngOnDestroy() {
if (this.cmpRef) {
this.cmpRef.destroy();
}
}
}
The important modification is in the addition of:
Object.assign(this.cmpRef.instance, this.inputs);
Basically, it copies the values you want to be on the new component into the tmp component class so that they can be used in the generated components.
It would be used like:
<div [compileBoundHtml]="someContentThatHasComponentHtmlInIt" [inputs]="{anInput: anInputValue}"></div>
Hopefully this saves someone the massive amount of Googling I had to do.
createSub() {
const factory = this.resolver.resolveComponentFactory(SubComponent);
const ref = this.location.createComponent(factory, this.location.length,
ref.instance.model = {Which you like to send}
ref.instance.outPut = (data) =>{ //will get called from from SubComponent}
this.location.parentInjector, []);
ref.changeDetectorRef.detectChanges();
return ref;
}
SubComponent{
public model;
public outPut = <any>{};
constructor(){ console.log("Your input will be seen here",this.model) }
sendDataOnClick(){
this.outPut(inputData)
}
}
If you know the type of the component you want to add i think you can use another approach.
In your app root component html:
<div *ngIf="functionHasCalled">
<app-sub [data]="dataInput" (onClick)="onSubComponentClick()"></app-sub>
</div>
In your app root component typescript:
private functionHasCalled:boolean = false;
private dataInput:string;
onClick(){
//And you can initialize the input property also if you need
this.dataInput = 'asfsdfasdf';
this.functionHasCalled = true;
}
onSubComponentClick(){
}
Providing data for #Input is very easy. You have named your component app-sub and it has a #Input property named data. Providing this data can be done by doing this:
<app-sub [data]="whateverdatayouwant"></app-sub>

Categories