How to update a component's property based on child content? - javascript

I am making a data table component. For accessibility, I want each table header to have a tooltip via a title attribute. The tooltip value can be explicitly set (in case it is different than the text in the header), but by default I want it to use whatever the inner text is.
I have a half-working solution, but it's not quite working and I don't know if I'm breaking some Angular rules by doing it this way.
Here's my abbreviated component html:
<div #headerContent [attr.title]="title"><ng-content></ng-content></div>
I am tagging the div with headerContent so I can reference it later.
Ok, now here's the abbreviated component class:
#Component({ ... })
export class TableHeaderComponent implements AfterContentInit {
#ViewChild('headerContent') headerContent: ElementRef;
#Input() title: string;
ngAfterContentInit() {
if (!this.title) {
this.title = this.headerContent.nativeElement.textContent.trim();
}
}
}
The idea is that if no title was specified, look at the div and grab its text content, using that for the title.
This works fine when I test it in the browser.
Example usage:
<th table-header>Contact</th>
Here, since I didn't specify a title, it should use Contact as the title.
But when I write unit tests for this component, it blows up with:
Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'Contact'
I'm not sure what I'm doing wrong here.
EDIT: Injecting the ChangeDetectorRef into my component class and calling detectChanges() after updating the title property seems to have fixed the problem. It works in the browser and also the unit tests pass.
Still wondering if this is breaking any rules in Angular to do it this way.

I was able to solve this by manually triggering change detection after updating the property.
To do this, you first inject the ChangeDetectorRef in the constructor:
constructor(private cd: ChangeDetectorRef) { }
Then after updating the property, call cd.detectChanges().

Related

Angular 10 - View doesn't update when model is modified

Angular 10.1.6, TypeScript 4.0.5
Hello
Context
In my webapp, I have 3 components.
Component A stores an Array<Image>
export class ComponentParentA implements OnInit {
images: Array <Image> ;
addImageToList(newImage: Image) {
this.images.push(newImage)
}
}
The Component C displays the list of images.
export class ComponentChildC implements OnInit {
#Input() images: Array <Image>;
}
The component C is called from the Component A html template like this :
<ComponentChildC [images]="images"></ComponentChildC>
Component B is in charge to contact my API, add an image which has been selected previously. The API return an Image object, which correspond to my model. The component emit the Image so the ComponentA add the returned image to its Array (Calling addImageToList)
addImage is an Observable return by this.http.post (HttpClient)
export class ComponentChildB implements OnInit, AfterViewInit {
#Output() eventCreateImage : EventEmitter<Image>;
addImage(data) {
this.service.addImage(data).subscribe((image) => {
this.eventCreateImage.emit(image)
})
}
}
Component A calls Component B like this :
<ComponentChildB (eventCreateImage)="addImageToList($event)"></ComponentChildB>
Problem
The Component B add an image when I click on a button. This click trigger the addImage function. All the component work correctly and the image is save on my server. The API returns the image and the latter is correctly stored in the Array in Component A. However, the view of the component C is not updated and the newly created image doesn't appear. If I click a second time on the button, a new image is stored. This time, the previous image, which I couldn't see previously, appears correctly. The new image don't.
I would like the image to appear directly.
What I already did
I saw similar issues on the web.
First, I tried to replace this.images.push(newImage) by this.images = this.images.concat(newImage) in addImageToList function. It doesn't work.
Next, I saw that, because images is an array, Angular doesn't detect any change when I add a new Image in my array. I made my Component C implements the OnChange interface. Effectively, the ngOnChange function is not called.
I saw here that I could try to reload my component manually So I made my Component C implement DoCheck. Something strange appeared. When I click on the button and I do console.log(this.images) in ngDoCheck in component C, I see that the list is correctly updated. But the image still doesn't appear. So I try to call ChangeDetectorRef methods like markForCheck and detectChanges to notify the component of the change, but it didn't work.
If you have any idea where my problem may be coming from, I would be grateful if you could help me
I didn't test but i think it's cause strategy detection. You can try 3 differents solutions (I can't test now but normaly it will be work).
PS: sorry for my english
1/ in your object #Component add changeDetection in your components
#Component({
selector: 'app-component',
template: `...`,
changeDetection: ChangeDetectionStrategy.OnPush
})
then in your function addImageToList() replace your code by
addImageToList(newImage) {
this.images = [...this.images, newImage]
}
2/ in your component C replace your #Input by
#Input() set images(value: Array <Image>) {
myImages = value
}
the properties binding in html will be myImages in this context
3/ You can change manualy the strategy detection with the service ChangeDetectorRef. Add this service in your constructor component A then in your function add :
constructor(
private cd: ChangeDetectorRef
){}
addImageToList(newImage: Image) {
this.images = [...this.images, newImages];
this.cd.markForCheck();
}
If this solutions don't work look the documentation of detection strategy in Angular or try to mix 1 and 2 together

Angular 6 error call function from expression

I'm currently working on an Angular 6 applicaiton. I try to call a method from an expression. Unfortunately it seems it doesn't work anymore.
The example is very simple and looks like that in my car.component.html:
<div>Car Name: {{ getCarName() }} </div>
In my component code car.component.ts, I implemented that function something like that:
getCarName() {
return this.carName;
}
Actually I'm changing the property carName each time I hover over another div on my component UI.
Unfortunately I'm getting the following error:
ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value...
Do you know how to solve this issue?
Thank you!!
ExpressionChangedAfterItHasBeenCheckedError means, that if you construct your logic this way, you have to detect changes manually inside your component:
import { Component, AfterViewChecked, ChangeDetectorRef } from '#angular/core';
/* Other stuff goes here */
export class PhoneVerifyComponent implements AfterViewChecked {
constructor(private _changeDetectorRef: ChangeDetectorRef) { }
ngAfterViewChecked() {
/* This way the detector will run after every change */
this._changeDetectorRef.detectChanges();
}
getCarName() {
/* This way you run detector only on this function call */
this._changeDetectorRef.detectChanges();
return this.carName;
}
}
You can choose every way you like, run detector in specific place, or after every change. If your goal is just to solve the issue only with this method, I recommend to run it on specific function call, but if you have many cases like that - better run it in ngAfterViewChecked.
BUT
The shortest way is to store the result of this function execution to class property and simply interpolate this property, instead of method:
<div>Car Name: {{ carName }}</div>
Anyway, we don't know your goals and what's going on in your real project, so you have to choose more proper way for your case.

Two way data binding in angular not working

I am trying to implement two way data binding in angular components. Currently its in a parent child mode.
parent.component.html
<child [(title)]="title"></child>
<span style="color: red">This is parent component {{title}}</span>
parent.component.ts
title = 'app';
child.component.html
<span style="color: blue">This is child component {{title}}</span>
child.component.ts
#Input() title: any;
#Output() pushTitle = new EventEmitter();
constructor() { }
ngOnInit() {
this.title = 'new title';
this.pushTitle.emit(this.title);
}
The title should implement on the parent component as well, when I change it from the child. Also, I am not sure why the parent code keeps going in a loop for no reason. I have added text in html just to test if its updated in both the components, but its updating only in the child, and not in the parent. I am coming from the angularjs background, and two way data binding worked seamlessly in it. I am just confused what I am doing wrong(I know its a noob question).
Demo here: https://stackblitz.com/edit/angular-xttmxg
There is another way that you can achive the same.
#Input() title: any;
#Output() titleChange: EventEmitter<any> = new EventEmitter<any>();
changeValue() {
this.title= !title;
this.titleChange.emit(this.title);
}
Have a look at Angular documentation about two way binding
Two way data binding only works for template - component interaction.
If you want to send title change to parent component, you should do something like this:
Parent template and component:
<child [title]="title" (pushTitle)="onTitleChange(value)"></child>
<span style="color: red">This is parent component {{title}}</span>
onTitleChange(value) {
this.title = value;
}
Followup question:
Template:
<input [(ngModel)]="inputModel">
Component:
inputModel: string;
Now, every time you type something into input field you will see changes in component model, OR when change inputModel value programmatically, you will see the change in HTML input.
You are somehow creating an infinite update cycle using the 2-way-binding. This leads to the infinite loop and eventual stack overflow you noticed.
To fix this, preferably, you want to add some logic to the titleChange event (this is the banana-part of the banana-in-a-box syntax, i.e. the part in parens in [(title)] which is getting automatically translated into an event emitter named titleChange).
For example, you might want to skip updating the title property of the parent component if its equal to the update emitted by the child component.
This means you should split up [(title)] into (titleChange)="titleChange($event)" and [title]="title". The first part lets you pass the updated title as $event and then process it in a function titleChanged (name is arbitrary in this case). The second part has the effect that the child component receives updates of the parent component's title property.
Another common pattern is to make title private (commonly with a prefixed underscore, e.g. _title) and then add a getter get title() { return this._title;} so that you can (1) encapsulate this property and (2) add some processing.
In your case this is not needed, but it doesn't hurt either. ;-)
Here's a plunkr containing these changes.

Detecting a change in model data within a component

I imagine I may be missing something really obvious, however this is my situation - I have some data that is being assigned to the ngModel input of a component, e.g:
Typescript:
SomeData = {
SomeValue: 'bar'
}
Fragment of view template:
<foo [(ngModel)]="SomeData.SomeValue"></foo>
Component:
import { Component, OnChanges, SimpleChanges } from '#angular/core';
#Component({
selector: 'foo',
template: `<input type="text" [ngModel]="value" (ngModelChange)="modelChange($event)
(change)="elementChange($event)"/>`
})
export class FooComponent {
ngOnChanges(changes: SimpleChanges) {
// Fired when #Input members change
}
modelChange(value) {
// Fired when a change in the HTML element will change the model, *not* when the model changes from elsewhere
}
elementChange(event) {
// Fired when the HTML element value changes
}
}
As per my comments in the example, I'm able to tell when Inputs change, when the value of the HTML element will change the model, and when the value of the HTML element changes.
I want to be able to know from within the component, when the property that is assigned to ngModel in the view template (i.e. SomeData.SomeValue) changes. I know that Angular does this itself, because it updates the value in the HTML, however I'm at a loss as to how to intercept this change as well, from within the component, so some other action may be taken.
SomeData.SomeValue is not controlled by angular, all you do is tell angular to bind to a property and bind to an event. Angular will then run it's own change detection mechanism which will update the view. If you are interested in how Angular does this take a look at this blog.
If you want to be notified of changes to SomeData.SomeValue you'll have to set up your own system, this can be as simple as a callback or a pub/sub. But it's really too broad to go into here.

Why does component view update when change detection is set to onPush? [duplicate]

I thought I was pretty clear on how Angular Change detection works after this discussion: Why is change detection not happening here when [value] changed?
But take a look at this plunk: https://plnkr.co/edit/jb2k7U3TfV7qX2x1fV4X?p=preview
#Component({
selector: 'simple',
template: `
<div (click)="onClick()">
{{myData[0].name}}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Simple {
public #Input() myData;
constructor() {
}
public onClick() {
}
}
Click on a, it's changed to c
I understand that the click event triggers change detection on the App level, but [myData]="testData" is still referring to the same object, and I am using On Push on Simple, why does a get changed?
That's by design.
If you have component with OnPush change detection then its detectChangesInternal function won't be triggered unless one of four things happens:
1) one of its #Inputs changes
~2.4.x
~4.x.x
Note: #Inputs should be presented in template. See issue https://github.com/angular/angular/issues/20611 and comment
2) a bound event is triggered from the component (that is your case)
Caveats: There is some difference here between 2.x.x and 4
Angular ChangeDetectionStrategy.OnPush with child component emitting an event
~2.4.x
~4.x.x
3) you manually mark the component to be checked (ChangeDetectorRef.markForCheck())
4) async pipe calls ChangeDetectorRef.markForCheck() internally
private _updateLatestValue(async: any, value: Object): void {
if (async === this._obj) {
this._latestValue = value;
this._ref.markForCheck();
}
}
https://github.com/angular/angular/blob/2.4.8/modules/%40angular/common/src/pipes/async_pipe.ts#L137
In other words if you set OnPush for component then after the first checking component's status will be changed from CheckOnce to Checked and after that it's waiting as long as we do not change status. It will happen in one of three things above.
See also:
https://github.com/angular/angular/issues/11678#issuecomment-247894782
There are also good explanations of how angular2 change detection work:
https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
https://hackernoon.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206f
Here is Live Example(Thanks to Paskal) that explains onPush change detection. (Comp16 looks like your component. You can click at this box).

Categories

Resources