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

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

Related

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

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

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 - passing many different values from child to parent component using the same function

I have a checkbox-component which I'm using in a parent component. When the value has changed, I'm using an #Output() to let the parent component know that the value has been updated.
My code is working but I'm using the checkbox-component many times in my project and I don't want to create it every time new function to set a new value of variable.
Here is the link to my code
My question is: How can I do it better? To prevent creating a new function onRoleChangeCheckboxX() every time I have a new checkbox.
My stackblitz is just example, two extra functions doesn't bother me, but what about the case when I have to create 50 checkboxes.
its hard for me to tell exactly what you want, but if you are using eventEmitter & Output, you can Emit a complex json obj with a single emit statement and send as much data as you want to the parent:
#Output() messageEvent = new EventEmitter<any>();
var obj = { check1: true, check2: false, check3: false }
this. messageEvent.emit(obj);
May be this is not the better approach but it may work in some cases like this , there will be one emit handler in all cases.
import { Component, OnInit, Input, Output, EventEmitter } from "#angular/core";
#Component({
selector: "app-checkbox",
templateUrl: "./checkbox.component.html",
styleUrls: ["./checkbox.component.css"]
})
export class CheckboxComponent implements OnInit {
#Input() checkboxName;
#Input() checkboxData:any;
#Input() checkBoxLinkedProp:any; // added another property it is the name of the property in the parent component you are updating
#Output() toggle: EventEmitter<any> = new EventEmitter<any>(); // emit a object instead of bool
constructor() {}
ngOnInit() {}
onToggle() {
const checkedOption = this.checkboxData;
this.toggle.emit({checked:checkedOption , checkBoxLinkedProp: this.checkBoxLinkedProp });
}
}
app.component.html
[checkBoxLinkedProp] = "'checkbox1Value'" -- i am passing the prop name as string to child checkbox component. and on toggle a single method is called (toggle)="onRoleChangeCheckbox($event)"
this.toggle.emit will emit object with the back with string prop we passed
<app-checkbox [checkboxName]='checkbox1' [checkboxData]='checkbox1Value'
[checkBoxLinkedProp] = "'checkbox1Value'"
(toggle)="onRoleChangeCheckbox($event)"></app-checkbox>
<app-checkbox [checkboxName]='checkbox2' [checkboxData]='checkbox2Value' (toggle)="onRoleChangeCheckbox($event)"
[checkBoxLinkedProp] = "'checkbox2Value'"
></app-checkbox>
app.component.ts
onRoleChangeCheckbox({checkBoxLinkedProp , checked}) this method will take property name we passed as string from emit event and set that state of class.
import { Component, VERSION } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
checkbox1 = 'First checkbox';
checkbox1Value = false;
checkbox2 = 'Second checkbox';
checkbox2Value = false;
onRoleChangeCheckbox({checkBoxLinkedProp , checked}) {
this[checkBoxLinkedProp] = checked; // property name that passed to child , is received in this emit event and as this will be set
console.log(this.checkbox1Value , checkBoxLinkedProp); // tested value for 1st checkbox
}
}
Here is a stackblitz of what I think you want? You need to create an array for your checkboxes
checkboxes = [
{
name: 'First checkbox',
value: false
},
{
name: 'Second checkbox',
value: false
},
]
Use the *ngFor to have a generic html that you don't need to edit when you add another checkbox to your array
<app-checkbox *ngFor="let box of checkboxes; let i = index"
[checkboxName]="box.name"
[checkboxData]="box.value"
(toggle)="onRoleChangeCheckbox($event, i)"></app-checkbox>
Then your onRoleChangeCheckbox() function can become generic and update your array using an index
onRoleChangeCheckbox(ev, index) {
this.checkboxes[index].value = ev;
}
This method should reduce a lot of your repeated code since you only need to add new checkboxes to an array.

ionic page data does not update

So say i have page one:
This page contains multiple variables and a constructor. it could look something like this:
export class TestPage implements OnInit {
testInt: number;
testString: string;
constructor(private someService: SomeService) {
}
ngOnInit() {
this.testInt = this.someService.getInt();
this.testString = this.someService.getLongText();
}
}
Now when this page loads it correctly sets the values.
Now say that I change page and on this page, I change some of the values in the service.
When I then come pack to this TestPage it hasn't updated the values.
Does this have something to do with caching? or with push state?
How can I make sure that the page is "reloaded" ?
Try using RxJS.
#Injectable({...})
class SomeService {
private _testInt: BehaviorSubject<number> = new BehaviorSubject<number>(0); // initial value 0
setTestInt(value: number) {
this._testInt.next(value);
}
getTestInt(): Observable<number> {
return this._testInt.asObservable();
}
}
#Component({...})
class TestPage implements OnInit {
public testInt: number;
public testInt$: Observable<number>;
private subscription: Subscription;
constructor(private someService: SomeService) {}
ngOnInit() {
// one way
this.testInt$ = this.someService.getTestInt();
// or another
this.subscription = this.someService.getTestInt()
.subscribe((value: number) => {
this.testInt = value;
});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
in the HTML:
<p>{{ testInt }}</p>
<p>{{ testInt$ | async }}</p>
If you are subscribing to a Observable, make sure you unsubscribe after the usage (usually On Destroy lifecycle hook).
Async Pipe does that out of the box.
Or try the ionViewWillEnter lifecycle hook.
As you can see in the official documentation:
ngOnInit will only fire each time the page is freshly created, but not when navigated back to the page.
For instance, navigating between each page in a tabs interface will only call each page's ngOnInit method once, but not on subsequent visits.
ngOnDestroy will only fire when a page "popped". link That means that Page is cached, yes. Assigning value On Init will set the value only the first time page is visited and therefore not updated.

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

Categories