ngOnChanges not always firing - javascript

As the title suggests I am having a problem with ngOnChanges activating correctly but the two listings below do something I find quite odd. The first listing never gets into the ngOnChanges method and the second one always does. It is essentially the same method with a different body for the first if statement.
never hits:
async ngOnChanges(changes: SimpleChanges): Promise<void>
{
if(changes.selectedImage)//{console.log("Changing ...")}
{
this.tileSource = await this.fetchCurrentTileSource();
if(this.tileSource)
{
this.refreshImageLayer(this.tileSource);
}
}
}
always hits
async ngOnChanges(changes: SimpleChanges): Promise<void>
{
if(changes.selectedImage){console.log("Changing ...")}
/* {
this.tileSource = await this.fetchCurrentTileSource();
if(this.tileSource)
{
this.refreshImageLayer(this.tileSource);
}
} */
}
Component (contains method above):
#Component({
selector: 'camera-check-viewer',
template: ''
})
export class CameraCheckViewerComponent implements OnInit, AfterViewInit, OnChanges, OnDestroy {
...
#Input() public selectedImage: CaptureQueryResponseItem;
Containing Component:
export class CameraCheckPhotoViewerComponent implements AfterViewInit, OnChanges, OnDestroy {
...
#ViewChild(CameraCheckViewerComponent, {static: true}) cameraCheckViewer: CameraCheckViewerComponent
...
public selectedCapture: CaptureQueryResponseItem;
...
ngOnChanges(changes: SimpleChanges) {
if (changes['selectedBoothCameraId']) {
this.hideCameraFailedNotesModal();
this.loadImages();
}
}
Within loadImages:
this.selectedCapture = Object.assign({}, capture); //Force change detection, not sure required!
and the html:
<div class="check-camera-wrapper">
<camera-check-viewer class="check-camera-view"
[selectedImage]="selectedCapture"
(click)="$event.stopPropagation()">
</camera-check-viewer>
</div>
Place a breakpoint on the first if statement with the console.log body and it executes, change the commenting so the other body is active and it never executes. I don't understand why this happens so does anyone have any idea as to what is happening here?

Related

Angular: ngOnInit hook does not work in dynamically created component

I'm having the following directive that adds dynamic component to ng-container
#Directive({
selector: '[appAddingDirective]'
})
export class AddingDirective {
constructor(protected vc: ViewContainerRef) { }
public addComponent(factory: ComponentFactory<any>, inputs: any): void {
this.vc.clear();
const ref: ComponentRef<any> = this.vc.createComponent(factory);
Object.assign(ref.instance, inputs); // can't find more elegant way to assign inputs((
ref.instance.ngOnInit(); // IMPORTANT: if I remove this call ngOnInit will not be called
}
}
The directive is used in an obvious way.
#Component({
selector: 'app-wrapper',
template: `<ng-container appAddingDirective></ng-container>`
})
export class WrapperComponent implements AfterViewInit{
#ViewChild(DynamicItemDirective)
private dynamicItem: DynamicItemDirective;
constructor() { }
ngAfterViewInit(): void {
// hope it doesn't matter how we get componentFactory
this.dynamicItem.addComponent(componentFactory, {a: '123'});
}
}
Finally in a component that is loaded dynamically I have
#Component({
selector: 'app-dynamic',
template: '<p>Dynamic load works {{ a }}</p>'
})
export class DynamicComponent implements OnInit {
#Input() a: string;
constructor() {}
ngOnInit(): void {
console.log(this.a);
debugger;
}
}
Here are my questions.
If I remove ref.instance.ngOnInit() call in AddingDirective, I do not get in ngOnInit of DynamicComponent (debugger and console.log do not fire up). Do component lifecycle hooks work in a component that is created and attached dynamically? What is the best way to make these hooks work?
I don't see rendered string Dynamic load works 123 still if I remove {{ a }} in template (template: '<p>Dynamic load works</p>'), Dynamic load works is rendered as it should. What is the reason and how can I fix that?
Is there a better way to assing inputs than doing Object.assign(ref.instance, inputs) as above?
PS. I'm using Angular 11

Observable input is null in child component. Why?

I am facing a situation I don't understand. I have a parent component (app.component in the example) that gets its data from an API as an observable
This data is then passed down to the child (hello.component) using the async pipe.
That child then receives the input, but when ngOnInit runs in the child, the input is null.
I don't understand why, and I don't know how to make it so that the input is the actual returned value from the API instead. The call to detectChanges() in app.component was a desperate attempt to trigger change detection in the child but that doesn't re-run ngOnInit so it's kinda moot. I left it there because that's how the actual code I'm working with looked like.
I know this code is terrible.I didn't write it. Unfortunately, the component I'm working with is like that and I can't refactor it because, you guessed it, there are no unit tests. I'm working on cleaning it all up, but for now I have to reuse that component the way it is.
Stackblitz: https://stackblitz.com/edit/angular-ivy-rw1xte?devtoolsheight=33&file=src/app/hello.component.ts
// app.component.ts
import { ChangeDetectorRef, Component, VERSION } from "#angular/core";
import { interval, Observable, of } from "rxjs";
import { mapTo, tap } from "rxjs/operators";
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
public name$: Observable<any> = of({});
constructor(private cdRef: ChangeDetectorRef) {}
public ngOnInit() {
this.name$ = interval(3000).pipe(
mapTo(() => {
first: "Eunice";
}),
tap(() => this.cdRef.detectChanges())
);
}
}
<!-- app.component.html -->
<hello [name]="name$ | async"></hello>
<p>
Start editing to see some magic happen :)
</p>
// hello.component.ts
import { Component, Input } from "#angular/core";
#Component({
selector: "hello",
template: `
<div *ngIf="name">
<h1>Hello {{ this.name.first }}!</h1>
<h1>{{ greetings }}</h1>
</div>
`,
styles: [
`
h1 {
font-family: Lato;
}
`
]
})
export class HelloComponent {
#Input() name: { first?: string }
public greetings: string = "";
public firstName: string = "";
public async ngOnInit() {
console.log('name:',this.name); // name: null
if (this.name) {
// this conditional is always false because this.name is always null
// and so this never runs.
console.log("got name");
this.firstName = this.name.first || "fallback";
this.greetings = await new Promise(resolve =>
resolve("how do you do, ${firstName}?")
);
}
}
}
this.name$ = interval(3000).pipe(
mapTo({
first: "Eunice"
}),
tap(() => this.cdRef.detectChanges())
);
Please fix it like the above. mapTo doesn't need to accept a function. It didnt make an error for you? :)
Also, the name input is null right when the hello component is mounted.
You need to check the name in ngOnChanges or you need to mount the hello component only when the name is available.
For example:
<hello *ngIf="name$ | async as name" [name]="name"></hello>
Or
public async ngOnChanges() {
console.log('name:', this.name); // name: null
if (this.name) {
...
}
}

Angular - NgForm valueChanges called multiple times on page load

I have a problem using the valueChanges function of ngForm. When binding an Input variable to the form with [(ngModel)], the form gets called multiple times on page load.
Is there a good way to only detect user changes?
export class ContainerComponent implements OnInit, AfterViewInit {
#Input() formData: Object;
#ViewChild('form') form: NgForm;
constructor() { }
ngOnInit(): void {
}
ngAfterViewInit(): void {
this.form.form.valueChanges.subscribe((value) => {
//Gets called multiple times on page load
});
}
}
Perhaps it will be sufficient to just check for dirty/touched state
From: https://angular.io/guide/form-validation
To prevent the validator from displaying errors before the user has a chance to edit the form, you should check for either the dirty or touched states in a control.
When the user changes the value in the watched field, the control is marked as "dirty".
When the user blurs the form control element, the control is marked as "touched".
I solved the problem:
export class ContainerComponent implements OnInit, AfterViewInit {
#Input() formData: Object;
#ViewChild('form') form: NgForm;
constructor() { }
ngOnInit(): void {
}
ngAfterViewInit(): void {
this.form.form.valueChanges.subscribe((value) => {
if(this.form.form.dirty) {
//DO STUFF
}
});
}
}
Try this :
export class ContainerComponent implements OnInit, AfterViewInit {
#Input() formData: Object;
#ViewChild('form') form: NgForm;
constructor() { }
ngOnInit(): void {
this.form.form.valueChanges.subscribe((value) => {
//Gets called multiple times on page load
});
}
ngAfterViewInit(): void {
}
}

Angular 2+: Child components ts variable changes but UI does not show changed value?

I have a child TestComponent component as follows:
import { Component, OnInit, Input } from '#angular/core';
import { ApiService } from '../../../api.service';
#Component({
selector: 'app-test',
templateUrl: './test.component.html'
})
export class TestComponent implements OnInit {
constructor(private apiService: ApiService) { }
testDisplayMessage = 'No data to show';
ngOnInit() {
}
getMessage(param: string) {
this.callingTest = true;
this.apiService.getTest( param ).subscribe( data => {
this.setTestDisplayMessage( data );
this.callingTest = false;
}, err => {
console.log( JSON.stringify( err ) );
this.setTestDisplayMessage( 'Failed to get data' );
this.callingTest = false;
} );
}
setTestDisplayMessage( message: string ) {
this.testDisplayMessage = message;
}
}
contents of test.component.html
<p style="padding: 10px;">{{ testDisplayMessage }}</p>
Use in parent componet :
Trigger JS Code in parent component on button click,
import { TestComponent } from './test/test.component';
....
.....
#Component({
providers: [ TestComponent ],
templateUrl: 'parent.component.html'
})
export class ParentComponent implements OnInit {
...
constructor(private testComponent: TestComponent) { }
...
// Button on parent template triggers this method
getMessage() {
this.testComponent.getMessage('Hello');
}
...
}
Html tag added in parent component,
<app-test></app-test>
When I debugged above code trigger point, call to setTestDisplayMessage() happens the field testDisplayMessage in TestComponent gets changed but UI shows the old message 'No data to show', why is the message on change does not reflect on UI template? Or this is not the way it is supposed to get used? Shall I use #Input
Update:
Based on the pointers given in the following answers as well as comment sections, I changed my component as #ViewChild so in above parent component instead of passing the child component as an argument to constructor I declared it as child component using #ViewChild, so code changes as follows,
Earlier wrong code
constructor(private testComponent: TestComponent) { }
Solution
#ViewChild(TestComponent)
testComponent: TestComponent;
I found this article useful.
Use #ViewChild()
In html file:
<app-test #childComp></app-test>
In parent component.ts file
import { Component, OnInit, ViewChild } from '#angular/core';
....
.....
#Component( {
templateUrl: 'parent.component.html'
} )
export class ParentComponent implements OnInit {
#viewChild('childComp') childComp: any;
constructor() { }
...
// Button on parent template triggers this method
getMessage() {
this.childComp.getMessage('Hello');
}
...
}
Update:
Based on the pointers given in the following answers as well as comment sections, I changed my component as #ViewChild so in above parent component instead of passing the child component as an argument to constructor I declared it as child component using #ViewChild, so code changes as follows,
Earlier wrong code
constructor(private testComponent: TestComponent) { }
Solution
#ViewChild(TestComponent)
testComponent: TestComponent;
I found this article useful.
definitely use #Input() but on set method
#Input()
set someProperty(value) {
// do some code
}
now every time you pass new value here, code will run
basically, your approach is wrong, please use Input() or Services to share data between components.
however, if you want to make ur code work, the below may work
import change detector
constructor(private cdRef: ChangeDetectorRef) {
}
note: import reference ->
import { ChangeDetectorRef } from '#angular/core';
execute detect change after the value is updated
setTestDisplayMessage( message: string ) {
this.testDisplayMessage = message;
this.cdRef.detectChanges();
}
I hope this helps

TypeScript | JavaScript | Angular 2: Dynamically Set #HostListener Argument

Dynamically Setting the #HostListener's Arguments
I have a directive which needs to listen for any event provided declaratively by the engineer. Here's an example:
import { Directive, Input, ElementRef, HostListener, OnInit } from '#angular/core';
//--
import { Sandbox } from '../../../sandbox';
#Directive({ selector: '[addItem]' })
class AddNewItemDirective implements OnInit {
#Input('addItem') data;
#Input() on: string = 'click';
constructor(private $: Sandbox, private element: ElementRef) { }
ngOnInit() { console.log('#INIT', this); }
#HostListener('click', ['$event']) handleEvent(e) {
console.log('add-item', e);
}
}
export { AddNewItemDirective };
Here's its usage:
<button class="btn btn-primary" [addItem]="{ name: 'Jeffrey' }" on="focus">Add New Item</button>
This works fine. However, my intuition told me I should be able to dynamically set the HostListener's arguments at render time based upon an input parameter:
#Directive({ selector: '[addItem]' })
class AddNewItemDirective implements OnInit {
#Input('addItem') data;
#Input() on: string = 'click';
constructor(private $: Sandbox, private element: ElementRef) { }
ngOnInit() { console.log('#INIT', this); }
#HostListener(this.on, ['$event']) handleEvent(e) {
console.log('add-item', e);
}
}
Of course, this.on would not be overwritten with 'focus' until the time ngOnInit is invoked. To my surprise, this.on throws an error because undefined has no property 'on'. So when my directive class is instantiated, for whatever reason, this === undefined.
I found one similar question here, though, its looking to dynamically modify HostListener at runtime while I just need it modified at compile/render/instantiation time.
Can someone please shed light on how I can accomplish this?
Thx
HostListener is not dynamic, it can not be changed at runtime. You should use Renderer class, which provides listen method:
#Input()
public on:string;
private dispose:Function;
constructor(private renderer:Renderer, private elementRef:ElementRef){}
ngOnInit(){
this.dispose = this.renderer.listen(this.elementRef.nativeElement, this.on, e => console.log(e));
}
ngOnDestroy(){
this.dispose();
}

Categories

Resources