Angular Directive doesn't receive changes from component made in ngOnDestroy method - javascript

There's an unexpected directive behavior after component was destroyed.
Directive didn't get changes made in ngOnDestroy method in component
component :
export class InfoButtonComponent implements OnDestroy {
display = false;
justMethod() {
this.display = true | false; // (whatever) works fine, directive recevied that display was changed
}
ngOnDestroy(): void {
this.display = false; // directive doesn't handle it
this.cdr.detectChanges();
}
}
<div
[show]="display"
>1</div>
directive:
export class TooltipDirective implements OnChanges {
#Input() show = false;
ngOnChanges(changes: SimpleChanges): void {
// get changes from component made in other methods
// doesn't get changes made in ngOnDestroy method
}
}

Seems to match what documentation tells for ngOnDestroy():
Called immediately before Angular destroys the directive or component.
I would put emphasis on "immediately" and wouldn't expect that another round of change detection is run afterwards.
It is not clear what you try to accomplish, but it sounds like a hack.

Related

How to update view from another component in Angular 7?

I'd like to refresh my card set from navigation bar which is part of app.component.html so I prepared refresh() function.
When it is called it does update variable Cards but doesn't render it in ngFor on html element in mainView.html.
It does render updated set if I call from html element in mainView.html (as (click)="loadCards()") but not if the same ((click)="refresh()") is done in app.component.html.
export class MainView implements OnInit {
constructor(private mMainController: MainController) {}
Cards: any = [];
ngOnInit() {
this.loadCards();
}
loadCards() {
this.mMainController.getAllCards().subscribe(
(data) => {this.Cards = data); },
(error) => {},
() => {console.log(this.Cards));
}
...
}
export class AppComponent {
...
constructor(private router: Router, private mMainView: MainView) {}
refresh(){
console.log('done');
this.mMainView.loadCards();
}
...
}
Update
Tried with #Input() but couldn't get it work. I implemented RefreshService as explained in accepted answer and now I'm able to refresh content from other components.
Thank you all for quick response.
FIST WAY: USING A SHARED SERVICE
You need to introduce a service that manage the state of your car.
In this case it may be usefull to introduce for this a BehaviorSubject like this:
Your Service:
private refresh: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public getRefresh(): Observable<boolean> {
return this.refresh.asObservable();
}
public setRefresh(value: boolean): void {
this.refresh.next(value);
}
Inside your MainView class
First: inject your service as dependency
Second: Subscribe to your observable inside OnInit hook e.g like this:
this.myService.getRefresh().subscribe((value: boolean) => {
if(value) {
this.loadCards()
}
})
Inside your AppComponent class
First: inject your service as dependency
Second: Set the value of your observable inside your refresh method.
e.g something like this:
public refresh(){
this.myService.setRefresh(true);
}
SECOND WAY: USING #Input Decorator to pass value down.
You're attempting to use MainView as a dependency but it's not an injectable dependency. Try to use inputs/outputs between app component and MainView, if possible. If MainView isn't a child of AppComponent then abstract the logic for loading cards into a service and inject it into both.
You can implement the component interaction in two ways
(i) If the components are related to each other use the common and straightforward method of sharing data. It works by using the #Input() decorator to allow data to be passed via the template.
(ii) If the components are not related to each other you can use a shared service using subject to communicate between the two components

angular 2 equivalent of vuejs v-bind

As title implies i'm looking for a way to bind an object with multiple properties to component #Inputs without having to explicitly write all of them
Let's say I have this object
let params = {
delta: 0.2,
theta: 2.3,
sigma: 'foo',
}
Instead of having to bind all of them individually like this
<my-component
[delta]="params.delta"
[theta]="params.theta"
[sigma]="params.sigma"/>
I would like bind all of them at once.
<my-component [someDirectiveIDontKnow]="params"/>
How can i do this?
Found a link to a previously asked question but couldn't get that to work properly.
Edit:
I'm not asking how to bind #Inputs. Imagine that the component I'm rendering has 40 #Inputs and I'm NOT allowed to rewrite it to just accept one #Input that could contain all the params.
So writing a template that uses this component gets really ugly and big.
....
<my-component
[param1]="params.param1"
[param2]="params.param2"
[param3]="params.param3"
[param4]="params.param4"
[param5]="params.param5"
[param6]="params.param6"
[param7]="params.param7"
[param8]="params.param8"
[param9]="params.param9"
[param10]="params.param10"
[param11]="params.param11"
[param12]="params.param12"
[param13]="params.param13"
[param14]="params.param14"
... and so on ....
/>
....
In my opinion, It would be best to define them all in a model
You would start with the following model
params.model.ts
import {SomeOtherModel} from './some-other.model'
export interface ParamsModel {
paramName1: string;
paramName2?: string;
paramName3?: number;
paramName4: SomeOtherModel;
}
Then in your component, you can force your input to take a specific model argument
my.component.ts
import {ParamsModel} from './params.model';
#Component({..})
class MyComponent {
#Input() params: ParamsModel;
}
app.component.html
<my-component params="paramsModel"></my-component>
app.component.ts
import {ParamsModel} from './params.model';
#Component({..})
class AppComponent implements OnInit {
paramsModel: ParamsModel;
ngOnInit(): void {
this.paramsModel = <ParamsModel>{someValue: someValue};
}
}
this way you have full code completion.
do note though! Angular does not deepwatch the contents, so changing the contents inside the Params object, will still have the same object ID in javascript, causing angular to not see the changes.
There are a few work-around for this
1: Bind every param (this is exactly what you do not want)
2: When changing contents of the model, destroy the instance and create a new instance everytime, you could do this by adding a constructor in the model and convert it olike this code
export class ParamsModel {
paramName1: string;
paramName2?: string;
paramName3?: number;
paramName4: SomeOtherModel;
constructor(config?: ParamsModel) {
Object.assign(this, config);
}
}
// first init
this.paramsModel = new ParamsModel(<ParamsModel>{someValue: someValue});
// updated init
this.paramsModel = new ParamsModel(this.paramsModel);
this.paramsModel.changedValue = changedValue; // (could also use an extend function on original params model)
3: Create an observer with events and trigger update events on the other side
4: use ngDoCheck to perform your own check if the contents changed
There is no generic directive to pass input properties in Angular. However, Angular supports binding any valid JavaScript type be it objects, arrays or primitives.
In the template
<my-component [params]="params"/>
In the class you have to use the #Input decorator to mark an object as an input. You can access it's value in any of the lifecycle hooks, some shown below. Note that params will not be set inside the constructor as view binding is performed after the class is instantiated.
class MyComponent {
#Input()
params: any
constructor() { } // <-- params not set
ngOnChanges() { } // <-- anytime params changes
ngOnInit() { } // <-- once when the component is mounted
}

How to call code inside ngAfterViewInit again in angular 2

In my angular component I have ngAfterViewInit method which contains some statements that I want to execute after the view is initialized cause it contains some DOM manupulations.
My question is when some parameter is changed I want to run the code inside ngAfterViewInit.
You could define an EventEmitter in the constructor, subscribe to it in the ngAfterViewInit() function to update those values, emit it in the ngAfterViewInit() function, and then emit it again every single time you want to call it in subsequent areas of the component. Here is an example:
import { EventEmitter } from '#angular/core';
export class MyComponent implements AfterViewInit {
public myEvent: EventEmitter<void>;
public constructor() {
this.myEvent = new EventEmitter<void>();
}
public ngAfterViewInit(): void {
// This is how you call the function to do what you want to do with your DOM manipulations below. You can also call this exact function even from the HTML, if you wish to do so (see HTML example).
this.myEvent.emit();
this.myEvent.subscribe(
() => {
// Do whatever actions that you need to do here to perform your DOM manipulations.
},
(err: Error) => console.error(err);
);
}
public emitMyEvent(): void {
this.myEvent.emit();
}
}
<my-component (click)="myEvent.emit()"></my-component>
<!-- or -->
<my-component (click)="emitMyEvent()"></my-component>
If you want to execute those statements on every changes than it is better to write those in ngAfterViewChecked().From the docs:
A lifecycle hook that is called after the default change detector has completed checking a component's view for changes
A callback method that is invoked immediately after the default change detector has completed one change-check cycle for a component's view.
So it will be called on every subsequent changes.
More information can also be found on the Lifecycle Hooks#AfterView docs
If your parameter is available as an Observable, you can just subscribe to it in the ngAfterViewInitmethod. If your parameter is not yet available as Observable, I suggest you take a look at the BehaviourSubject class. With this, you can control when the Observable will emit a new value + It will be triggered with the last value when you subscribe to it
You declare ordinary class methords in the class body, and later define them in the context of ngAfterviewInit.
here's a simple use case example:
export class ViewtestComponent implements OnInit, AfterViewInit{
#ViewChild('someElementMewantedToDoAction') elRef: ElementRef;
constructor() { }
ngOnInit(): void {
}
changeVal;
ngAfterViewInit() {
this.changeVal= (useME) => {
// some action
this.elRef.nativeElement.innerText++;
}
}
Later, use the method in template as
// Do Action= Button value will increment 11, 12 13 ... on each button click.
<button class="button success" (click)="changeVal($emit)>10</button>
I have solved my problem by implementing OnChanges and call ngAfterviewInit for changes other than the firstchange.This way I will make sure that the view is initiallized. By the way the variable subjected to change(changed_var) holds data used in DOM manipulation.
ngOnChanges(changes: SimpleChanges){
if(!changes.changed_var.isFirstChange()){
this.ngAfterViewInit();
}
}

Declare class properties inside typescript callback method

I have a method that is subscribing to an event from a pub sub messaging service. In the callback I am wanting to define a class property. When I try to assign the property value, it returns as undefined. I understand that the reference to 'this' changed from the class to the method, but I need it to have access to the class's 'this' property. How can I assign the value to the class property 'this.icon' inside my callback method?
import { Component, OnInit, OnDestroy } from '#angular/core';
import { Subscription } from 'rxjs/Subscription'
import { Events } from '../shared/messages/events';
import { MessageService } from '../shared/messages/message.service';
export class Component implements OnInit, OnDestroy {
public icon: string;
private subscription: Subscription;
constructor() { this.btnClickSubscribe();}
private btnClickSubscribe(): void {
this.subscription = this.messageService
.subscribe(Events.btnClick, (payload) => {
this.icon = 'fa-trash-o';
console.log(this.icon) //logs the correct value, 'fa-trash-o'
//but it's only available inside this context. I need it in the
//class context
});
}
Since this is an asynchronous event, this.icon will initially be undefined outside the callback, no matter what you do. Check this one about more info: How do I return the response from an Observable/http/async call in angular2?
You mentioned you are passing icon to a child via #Input() then make use of ngOnChanges in the child, which captures the changes happening to icon. In the ngOnChanges you can make a condition, that executes whatever logic you want to do, after the value has been set to to icon, so something like this in your child:
#Input() icon;
ngOnChanges() {
if(this.icon) {
console.log('icon is set, now do something with it!')
}
}
And if you have problems with the view, there are some possible solutions, like using the safe navigation operator, more info here: Cannot read property "totalPrice" of undefined
Here's a
Demo
Hope this helps! :)

Angular 2: ngOnChanges fires when template renders

When my template renders the function inside ngOnChanges fires one time, and then only when an #input changes. Is this the expected behaviour and how can I prevent it?
Child:
export class MobileMenuComponent implements OnInit, OnChanges {
#Input('test') dezeTest: string;
//declare menu
menu;
constructor() {
//define menu
this.menu = {state: 'inactive'};
}
togglemenu() {
var that = this;
if (that.menu.state === 'inactive'){
that.menu.state = 'active';
}
else {
that.menu.state = 'inactive';
}
}
ngOnChanges(changes: SimpleChanges) {
this.togglemenu();
}
}
This is the normal behaviour of ngOnChanges.
The ngOnChanges method will fire the first time because your properties have been checked, and subsequently fire when a property is updated. From the documentation, you can see
ngOnChanges is called right after the data-bound properties have been checked and before view and content children are checked if at least one of them has changed.
If you want to change it, you need to consider how you want to change it. That is not very clear from your question, but if you want to prevent the ngOnChanges from firing again, when a property is updated (I think that you want this because of your toggleMenu() you might consider using the ngOnInit() instead of ngOnChanges(). Alternatively, you can block the togglemenu(); after the first time.
firstrun : boolean = true; // class variable
ngOnChanges(changes : SimpleChanges){
if(firstrun){
this.togglemenu();
firstrun = false;
}
}
Alternatively, as hinted at earlier, another lifecycle hook might suit your needs better.
To expand on existing answers, and address a typing question raised in a comment at the same time:
The SimpleChange#firstChange field exists for this exact case.
Alternatively, because the value is set on your Component before ngOnChanges is called, you can also check if a field has changed, followed by if it's set:
ngOnChanges(changes: { myField: SimpleChange }) {
if(changes.myField && this.myField){
// do a thing only when 'myField' changes and is not nullish.
}
// Or, if you prefer:
if(changes.myField && changes.myField.firstChange){
// Do a thing only on the first change of 'myField'.
// WARNING! If you initialize the value within this class
// (e.g. in the constructor) you can get null values for your first change
}
}
Another little warning: If you were to use tools like WebStorm to rename 'myField' on your Component, the 'myField' of the ngOnChanges method parameters ({myField: SimpleChange }) will NOT be updated. Which can lead to some fun Component initialization errors.
As Dylan Meeus suggested, its the normal behaviour.
but i would suggest a different solution, which takes advantage of the passed SimpleChange object. it contains a previousValue and a currentValue.. initially, the previousValue is not set.
https://angular.io/docs/ts/latest/api/core/index/SimpleChange-class.html
ngOnChanges(changes : any){
if(changes.menu.previousValue){
this.togglemenu();
}
}
additionally, take care about OnChanges, since it fires on every input param... (you might add some more in the future)

Categories