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
Related
I am using angular version 9. I have 2 components. 1 to search a car make, model and 2nd component to load the details of the car that is selected from component 1. I pass the image link for the car from the car that is selected from component 1 to component 2 and load that image in component 2. This works fine..
When I browse to some other page in my application and again go to the component 1 and select a different car and go to component 2, the image of the previously selected car shows for around 1-2 seconds and then my new image is shown. This looks like a browser caching problem. I have tried to load a loader image initially till my new image completes loading and then show the newly loaded image but it does not work. What I have tried is as below.
<img *ngIf="isLoading" [src]="loadingimage.png" />
<img [hidden]="isLoading" [src]="carModelImage" (load)="isLoading = false"/>
What I have noticed is, the moment component 2 is opened the (load) event on the img tag fires and changes the isLoading value to false even without waiting for the new carModelImage to load and thus the previous image shows for some time.
My component code looks like below.
#Component({
selector: 'app-request-summary',
templateUrl: './request-summary.component.html',
styleUrls: ['./request-summary.component.scss']
})
export class RequestSummaryComponent implements OnInit, OnDestroy {
public carModelImage: string = '';
public isLoading: boolean = true;
constructor() {
this.carModelImage = '';
this.isLoading = true;
}
ngOnInit() {
this.requestSummaryService.carData
.pipe(takeUntil(this.ngUnsubscribe))
.subscribe((data) => {
this.carModelImage = data[0].carModelImage ? data[0].carModelImage
:'http://nocarimage.png';
}
}
ngOnDestroy() {
this.isLoading = true;
this.carModelImage = '';
this.ngUnsubscribe.next();
this.ngUnsubscribe.complete();
}
}
I am struglling due to this issue since quite a long. Any help will be highly appreciated.
Well, I don't have much to go on here, but I'll share a simple pattern with you.
When your image is no longer being displayed null it out.
For example somewhere in your code you probably write this:
carModelImage = "your-image";
When you are sure you no longer need this var anymore, null it out.
carModelImage = null;
Post more code if you want help specific to your issue.
How do you assign carmodelImage variable . The carmodelImage is responsible for showing the previous image, if you could nullify it on destroy may be it should help
Assuming you said ngondestroy is being called
I want to load datas only if the user see the specific component which load the datas. So don't load before the component is visible.
I have this template:
<button (click)="showMyCompontente()">Click me</button>
<app-other-component *ngIf="show"></app-other-component>
With this TypeScript code:
export class MyComponent implements OnInit {
show = false;
ngOnInit() {
}
showMyCompontente() {
this.show = !this.show;
}
}
And the point is here:
#Component({
selector: 'app-other-component',
})
export class OtherComponent implements OnInit {
ngOnInit() {
this.load();
}
load() {
// needs to load datas only if the user see the component
}
}
How to achive in the OtherComponent to start the this.load() only if the component is visible to the user? I want to reload datas again if the user hide the component and show it agian.
I need a solution inside the component to detect itself is became visible or disappear. That's because I have many compontents, calling eachothers in many variations.
Which Angular lifecycle hooks fires only when the user shows the component?
I would try:
Adding an input property with type of boolean to OtherComponent component.
Passing value of show to the input property, then inside the OtherComponent component, use ngOnChanges to detect any change in the input property and call load() accordingly.
I'm trying to create a dynamic dashboard. Right now I'm using this dynamicServer to inject the component, but when I display the same component a second time, the information is shared between. I'm trying to isolate the "views" and create a new instance. Any help?
dynamicServer.ts
loadComponent(viewContainerRef: ViewContainerRef) {
const childComponent = this.factoryResolver.resolveComponentFactory(this.resolveView(this.ViewtoInjet));
const instance = viewContainerRef.createComponent(childComponent).instance;
}
ViewContainer.ts
#ViewChild('dynamic', {
read: ViewContainerRef
}) viewContainerRef: ViewContainerRef;
constructor(#Inject(DynamicServiceTestService) service, private winRef:
WindowRefService) {
this.service = service;
}
ngOnInit() {
this.service.loadComponent(this.viewContainerRef);
}
Views
As you can see, I am rendering the same component twice, but if I click to add numbers with the buttons, the value is shared between them. Any way to generate a new instance of every component to isolate them?
EDIT
I add a stackblitz https://stackblitz.com/edit/angular-tpryfl?file=src/app/view-container/view-container.component.ts
When i put my code in stackblitz i realized that my code was ok!!, but in my computer code i was using a service to refresh the counter, so it was ok so the counter in the service has to be common, but now ive got another question.
Is it possible to open 2 references of the same component, so if i add numbers in one component will i see the changes in the copy of the same component?
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().
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).