know when #Input parameter is changed - javascript

I want to exec a function every time when the #Input is changed.
I tried to do it that way:
ngOnChanges(changes: { [propName: string]: SimpleChange }) {
if( changes['inputName'] && changes['inputName'].previousValue != changes['inputName'].currentValue ) {
this.func();
}
}
but my problem is:
my #Input value is'nt necessarily changed
(it can be "a" and another once "a")
but even if it's not changed I want to exec the function.
what can I do?

You need to call setter method For #Input() changed everytime..
#Input('inputName') set cmpRef (cmp : any){
// your function call here goes every time #Input() changes, this setter method is called.
this.func();
}

I solved the problem by putting object in the #Input parameter instead of string only.
and now it is changed every time I change it.

Related

does ngOnChanges actually update input properties

I'm new to Angular, just a question on Lifecycle Hook Methods. I know that:
ngOnInit() is called after Angular has set the initial value for all the input
properties that the directive has declared.
ngOnChanges() is called when the value of an input property has changed and also just before the ngOnInit method is called.
let's say we have an input attribute like:
// custom directive class
#Input("pa-attr")
numOfProduct: number;
and
//template.html
<tr [pa-attr]="getProducts().length ...
and I click add button and add a new product, so the total number of products changes, so my question is:
in my case ngOnChanges() get called, ngOnInit() won't be called because ngOnInit() only get called in the first time the input property has been initialized with a value.
But how does the input property numOfProduct get updated after ngOnChanges() get called? Is it a special lifecycle method called ngUpdateInputProperties(the name won't match, but you get the idea) that does the job as:
ngUpdateInputProperties {
latestValue = ... //retrieve latest value
numOfProduct = latestValue ;
}
or such lifecycle method doesn't exist and the numOfProduct won't have the latest value, we can only get access to the latest value in ngOnChanges() method?
If you change any values in your component, it will change detection to recheck all the data bound values, then it will pass any updates to your child component.
You can test this yourself by placing a log entry inside if your getProducts() method, then tracking its value in the child component. You'll notice it gets called when updates happen in the component and you should see the updates in the child component.
Hope this helps.

Web Components: setter not being called

Say I have a Web Component:
customElements.define("custom-list", class CustomList extends HTMLElement {
get filter() {
console.log("get filter");
return this.getAttribute("filter");
}
set filter(value) {
console.log("set filter");
this.setAttribute("filter", value);
}
});
I wanted to use the setter method to do some initial attribute validation, but the setter never gets called. I tried setting the attribute through the HTML:
<custom-list filter="some value"></custom-list>
Only when I use JavaScript to set a property programmatically does the setter get called:
var list = document.querySelector("custom-list");
list.filter = "some value";
list.setAttribute("filter", "some value"); // DOESN'T WORK EITHER
So, it seems like setting attributes through the HTML or using setAttribute doesn't trigger the setter, which I partly can understand. My questions are:
Is the setter only necessary when I want to set properties programmatically?
How could I do initial validation of an attribute? In the connectedCallback? Say I want to only accept a certain string, how would I detect that?
Since the property filter gets populated anyway, do I need the setter if I don't use JavaScript to set my attributes?
Is the setter only necessary when I want to set properties programmatically?
Yes, at least if you want/need to run some tests/filtering upon the value you want to set.
How could I do initial validation of an attribute? In the connectedCallback? Say I want to only accept a certain string, how would I detect that?
Yep, connectedCallback or even in the constructor.
Since the property filter gets populated anyway, do I need the setter if I don't use JavaScript to set my attributes ?
No, you don't
This being said if you need a clear control over your custom attributes, i would suggest creating an internal state that you populate once when your custom element is being created and then when attributeChangedCallback is being called. That would give you some advantages :
you get control over the values that value your custom attributes.
you get an internal state that you can use to re-render your component if you need to
Here is an example :
customElements.define("custom-list", class CustomList extends HTMLElement {
static get observedAttributes() { return ['filter']; }
constructor() {
super();
this.state = {
filter: null
};
this.setFilter(this.getAttribute("filter"));
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === "filter") {
this.setFilter(newValue);
}
}
getFilter() {
console.log("get filter");
return this.state.filter;
}
setFilter(value) {
// You can add some logic here to control the value
console.log("set filter");
this.state.filter=value;
}
});
Then you can call the following to change your internal state :
list.setAttribute("filter", "some value");
Would be interrested to get some feedback on this from the community. Anyway, hope this helps :)
getters and setters allow your code to receive values other than strings. Attributes are always strings and can only be called by JavaScript.
You can emulate a non-string in an attribute by parsing the value. But they are always passed in as strings.
If you want to have code run when an attribute is changed then you need to add the attributeChangedCallback function and indicate which attributes you are watching in the observedAttributes static getter. Attributes can be set in JavaScript by calling setAttribute and removeAttribute. They are also set when the browser parses your HTML due to a page load or setting innerHTML. But even then the browser eventually calls setAttribute in the background.
customElements.define("custom-list", class CustomList extends HTMLElement {
static get observedAttributes() { return ['filter']; }
constructor() {
super();
this._filter = null;
}
attributeChangedCallback(attr, oldVal, newVal) {
if (oldVal != newVal) {
// Only set this value if it is different
this.filter = newVal;
}
}
get filter() {
console.log("get filter");
return this._filter;
}
set filter(value) {
if (value !== this._filter) {
console.log(`set filter ${value}`);
this._filter=value;
this.textContent = value;
// If you want the filter property to always show
// in the attributes then do this:
if (value !== null) {
this.setAttribute('filter', value);
} else {
this.removeAttribute('filter');
}
}
}
});
const el = document.querySelector('custom-list');
setTimeout(() => {
el.filter = 'happy';
}, 2000);
<custom-list filter="10"></custom-list>
Always check to see if your oldVal and newVal are different in the function attributeChangedCallback.
It is also recommended that you check for different values in your setters.
Setters also allow you to take specific data types. For example you could check to see if the value for value was a number and, if not, throw a TypeError.
Setters also allow you to make sure a value is valid. Maybe it must be a positive number or one of three possible strings. If it isn't you can throw a RangeError.
But you have to remember that attributes are always strings. And properties can be anything.

Can't bind to property of child component as variable hasn't loaded?

I have a component with a child, and I am wanting to pass a variable to the child, but the variable takes a second or two to load properly from a subscribe.
I have given an *ngIf but I still get the error:
Can't bind to 'event' since it isn't a known property of 'app-race-card'.
Is this because of the time it takes to actually load the event from the subscribe or am I missing something bigger?
I figured that putting an *ngIf="event" in the element call for the <app-race-card> call would mean that it would wait until event was populated before attempting to load the component, and then actually pass what was now present?
event.component.ts
let event;
ngOnInit() {
this.subscribeService();
}
subscribeService() {
this.service.eventSubject
.subscribe(res => this.event = res);
}
event.component.html
<comp *ngIf="event" [event]="event"></comp>
comp.component.ts
export class Component implements OnInit {
#Input() event: IEvent;
constructor() {
}
ngOnInit() {
}
}
Most likelly (you don't show what this.eventService.eventSubject is actually doing), you are confused about how Change Detection runs. Your eventSubject(as name suggests) probably reacts to some mouse or keyboard event. Or maybe an Ajax request, XHR or even something else. All these mechanisms triggers Change Detection to run. When Change Detection is triggered it checks if properties of the class have changed, if so - it updates the view.
Now in your case when component loads, callback .subscribe(res => this.event = res); haven't yet accrued, so this.event is still undefined. But if you conditionally state in template *ngIf="event" Angular will ignore that field in template and will not throw any errors. And when .subscribe(res => this.event = res); is executed and then causes a Change Detection to be triggered, it checks all properties (including this.event) if they have changed. And because this.event was undefined before and now has some value, it rerenders the view, but this time *ngIf="event" will return true so the value will be displayed.
p.s. your solution is good. No need to worry. If you have heard about the ? (Elvis operator), it serves the exact same reason - to check if value is defined.
I think that it is possible that your variable event is not an instance of IEvent. Or that perhaps it is is not accessible to your child component. Try defining
event: Ievent;
in your parent event.component.ts. Instead of
let event;

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();
}
}

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