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)
Related
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.
EDIT 2: This appears to be my general problem, and solution (using setTimeout so Angular's lifecycle can happen). I'll either close this or post an answer to my own question when I can.
See EDIT for a simpler repro that doesn't involve Subjects/Observables but is essentially the same problem.
I have a parent component that's responsible for fetching data from a service.
export class ParentComponent implements OnInit {
public mySubject: Subject<Foo[]> = new Subject<Foo[]>();
public buttonClicked = false;
private currentValues: Foo[] = null;
constructor(private SomeService myService) { }
this.myService.get().subscribe(values => {
this.mySubject.next(values); // Does NOT work when a child component is hidden, as expected.
this.currentValues = values; // Keep value so we can manually fire later.
});
private buttonClickHandler() {
this.buttonClicked = true;
this.mySubject.next(this.currentValues);
}
}
This data is subscribed to in the HTML by a child component. This component is hidden by default via *ngIf, and only becomes visible on a button click:
<app-child-component [values]="mySubject.asObservable()" *ngif="buttonClicked" />
In the parent component above you see I'm trying to pass the current available data to the child by invoking next() when the component is made visible in some way:
this.mySubject.next(this.currentValues);
This does not work when initially un-hiding the component via *ngIf. If I click the button a second time, which then calls next() again, then it works as expected. But when Angular is in the current context of un-hiding something, observables aren't getting their data. (This also happens when things are unhidden by other means, but the result is the same: If in the same method, the subject/data passing does not work; the component has to already be visible as of the method call.)
I'm guessing the binding to the observable is not happening until after *ngIf shows the child component, after the method call resolves. Is there some place I can hook into that I can then pass child data down?
EDIT for clarification: I don't believe this is an issue of Subject vs. BehaviorSubject. I'm not having issue passing the data. The issue is that the data-passing (confirmed via console.log()) is not occurring at all in the first place. It's not that the child component is receiving a null value. The subscription just isn't firing to the child.
I found I can reproduce this in a simpler fashion too: Trying to select an element in the DOM of *ngIf HTML reveals undefined if I make *ngIf's value true within the same Angular method.
<div *ngIf="buttonClicked">
<div id="someElement">Foo</div>
</div>
public someMethod(): void {
this.buttonClicked = true;
const container = document.getElementById('someElement'); // DOES NOT WORK if this.buttonClicked was false at the start of this method!
}
You going to need to use a BehaviourSubject instead of Subject, which emits the previously set value initially.
What is the difference between Subject and BehaviorSubject?
I have two components that communicate with each other, a regular component (parent) and a bootstrap modal component (child).
Both the parent and the child have a table with records. In the parent's table each record has a checkbox. When I select one checkbox or more and click on a button, an event is triggered that tries to populate the child's table.
The child's table gets populated and I have the ability to delete the records I wish from it. If I delete some records, close the modal (close the child) and decide to send the same data from the parent to the child again, the Input event is not triggered. However, if I choose to check different records in the parent and pass them to the child, the Input event is triggered correctly.
Here's what I have:
ParentComponent.component.html
<child-component #createNewProductRequestModal [data]="newProductRequest"></m-child-component>
ParentComponent.component.ts
private onNewProductRequest(toSend){
this.newProductRequest = toSend;
$('#createNewProductRequestModal').appendTo("body").modal('toggle');
}
ChildComponent.component.ts
#Input('data')
set data(data: any) {
if (data !== undefined || data !== "") {
this.products = data;
console.log("data");
}
}
constructor() { }
ngOnInit() { }
To test if the data is changed before I render the child's table, I log the data passed by the parent to the console. With this code, every time I execute the parent's onNewProductRequest(toSend) without changing the toSend variable, the child's modal renders but doesn't execute the Input event therefore not changing the data.
When you send the same data to the child component a second time, Angular does not register this as a change to the #Input(), as you are passing the same Object reference that you passed the first time, and Angular is just comparing the references.
Try this small change:
private onNewProductRequest(toSend){
this.newProductRequest = { ...toSend };
$('#createNewProductRequestModal').appendTo("body").modal('toggle');
}
This will mean that you pass a shallow copy of the data Object to the child, rather than just a modified version of the original Object. As this shallow copy will have a different reference to the original Object, Angular will pick it up as a change and trigger your #Input setter.
When you assign the data to the input, the first time detect the changes because the object changes, but then if yo change properties inside the object the reference and the object still be the same, so the changes doesn't fire. The solution is clone the object that you're sending via input to the child. Try changing this line:
this.newProductRequest = toSend;
For this:
this.newProductRequest = {...toSend};
You can use ngOnChanges
Definition from angular docs
A callback method that is invoked immediately after the default change detector has checked data-bound properties if at least one has changed, and before the view and content children are checked.
You have some Input on child element
#Input() nameOfInput: any;
Your child component must implement OnChanges like this:
export class ChildComponent implements OnChanges {
And in next step you want do something on every change of inputs.
ngOnChanges(changes: SimpleChanges) {
console.log(changes);
if ('nameOfInput' in changes) {
console.log('Old value: ', changes.nameOfInput.previousValue);
console.log('New value: ', changes.nameOfInput.currentValue);
}
}
Look at this example.
Example
try
in child component
import {Component, OnChanges, SimpleChanges, Input} from '#angular/core';
ngOnChanges(changes: SimpleChanges) {
for (let propName in changes) {
let change = changes[propName];
let curVal = JSON.stringify(change.currentValue);
let prevVal = JSON.stringify(change.previousValue);
console.log(curVal);
console.log(prevVal);
}
}
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;
I have a global service widgetService which holds data for a number of widgets, each identified by a widgetID. Each widget's data can change at any time. I want to display a widget with a React component, say WidgetReactComponent.
The react component shall take a widget ID as property and get the information to display from the widget service. A widget's data can be queried from the widget service with the method getWidgetData(widgetID). And in order to be able to publish data changes, it also offers two methods: addListenerForWidget(widgetID, listener) and removeListenerForWidget(widgetID, listener).
When assuming that the property is set once and never changed, this can be achieved like this, following React's recommendations:
class WidgetReactComponent extends Component {
constructor() {
super();
this.state = {
data: widgetService.getWidgetData(this.props.widgetID)
};
this._onDataChange = this._onDataChange.bind(this);
}
_onDataChange(newData) {
this.setState({data: newData});
}
componentDidMount() {
// React documentation: "This method is a good place to set up any subscriptions."
widgetService.addListenerForWidget(this.props.widgetID, this._onDataChange);
}
componentWillUnmount() {
// React documentation: "Perform any necessary cleanup in this method, such as [...] cleaning up any subscriptions that were created in componentDidMount()."
widgetService.removeListenerForWidget(this.props.widgetID, this._onDataChange);
}
render() {
return <div className="Widget">{this.state.data.stuff}</div>;
}
}
The component may then be used like this:
<ReactWidgetComponent widgetID={17} />
However, the widgetID property may change at any time, and the component has to handle this in order to function properly under all circumstances. By react's recommendation, this should be handled by setting the state based on properties using the static getDerivedStateFromProps function. But since it is static, I do not have access to the component and cannot change the listeners accordingly.
One way to work around this would be to store the widgetID in the state, and then use the lifecycle method componentDidUpdate to detect the change, like this:
constructor() {
super();
this._onDataChange = this._onDataChange.bind(this);
}
static getDerivedStateFromProps(nextProps) {
return {
widgetID: nextProps.widgetID,
data: widgetService.getWidgetData(nextProps.widgetID)
};
}
componentDidUpdate(prevProps, prevState) {
if (prevState.widgetID !== this.state.widgetID) {
widgetService.removeListenerForWidget(prevState.widgetID, this._onDataChange);
widgetService.addListenerForWidget(this.state.widgetID, this._onDataChange);
}
}
However, componentDidUpdate won't be called when shouldComponentUpdate returns false. This doesn't feel like a safe way of doing this. Also I believe the listeners will be wrong for the entire timespan between the property change and the update's completion. How could I safely implement this?
You don't need to store widgetID in state, you can compare prevProps with this.props:
componentDidUpdate(prevProps, prevState) {
if (prevProps.widgetID !== this.props.widgetID) {
widgetService.removeListenerForWidget(prevProps.widgetID, this._onDataChange);
widgetService.addListenerForWidget(this.props.widgetID, this._onDataChange);
}
}
You will also need to add the listener in componentDidMount as componentDidUpdate is not called on first rendering:
componentDidMount() {
widgetService.addListenerForWidget(this.props.widgetID, this._onDataChange);
}
Regarding your concerns:
componentDidUpdate won't be called when shouldComponentUpdate returns false
From the docs:
Use shouldComponentUpdate() to let React know if a component’s output is not affected by the current change in state or props.
So if you decided to not update the component when this.props.widgetID changes, then you are violating the assumption/purpose of shouldComponentUpdate and should not expect your widget listener to be updated.
A lot of things will not work as intended if you misuse shouldComponentUpdate anyway (eg. component not updated to reflect new data), so relying on an API being used correctly as per official docs is a necessity to achieve simplicity, rather than something to be avoided.
the listeners will be wrong for the entire timespan between the property change and the update's completion
By this logic, when you update some displayed data in an event handler, you can also claim that the data displayed is wrong for the entire timespan between the event and the re-rendering. You can even claim that your text editor is displaying the wrong data between the time you press a keyboard key and rendering of the key on the screen.