Angular click event handler not triggering change detection - javascript

To put my problem simply, I have an element in component's template. This element has an ngIf condition and a (click) handler. It is not rendered from the very beginning, because the ngIf condition evaluates to false.
Now comes the interesting part: A code running outside the angular zone changes that condition to true, and after executing detectChanges on the change detector ref manually, this element gets rendered and the click handler ofc becomes active.
It all seems ok so far, but the problem is that when the (click) callback is run upon user's click, change detection is not triggered for the component.
Here is the reproduction https://stackblitz.com/edit/angular-kea4wi
Steps to reproduce it there:
Click at the beige area
Button appears, click it too
Nothing happens, although message should have appeared below
Description:
The beige area has a click event handler registered via addEventListener, and this event listener's callback is running outside the angular zone. Inside it a component's showButton property is set from false to true and I trigger change detection there manually by calling detectChanges(), otherwise the change in the showButton property wouldn't be registered. The code looks like this:
this.zone.runOutsideAngular(() => {
const el = this.eventTarget.nativeElement as HTMLElement;
el.addEventListener('click', e => {
this.showButton = true;
this.cd.detectChanges();
})
})
Now button appears, which thanks to *ngIf="showButton" wasn't rendered initially, and it has a click even handler declared in the template. This handler again changes component's property, this time showMessage to true.
<button *ngIf="showButton" (click)="onButtonClick()">Click me!</button>
onButtonClick() {
this.showMessage = true;
}
When I click it, the handler obviously runs and changes component's showMessage to true, but it doesn't trigger change detection and message below doesn't appear.
To make the example work, just set showButton to true from the very beginning, and the scenario above works.
The question is: How is this possible? Since I declared the (click) event handler in the template, shouldn't it always trigger change detection when called?

I created an issue in Angular's repo, and as it turns out, this behavior is logical, although perhaps unexpected. To rephrase what was written there by Angular team:
The code which causes the element with (click) handler to render is running outside the Angular zone as stated in the question. Now, although I execute detectChanges() manually there, it doesn't mean that the code magically runs in angular zone all of a sudden. It runs the change detection all right, but it "stays" in a different zone. And as a result, when the element is about to be rendered, the element's click callback is created in and bound to non-angular zone. This in turn means that when it is triggered by user clicking, it is still called, but doesn't trigger change detection.
The solution is to wrap code, which runs outside the angular zone, but which needs to perform some changes in the component, in zone.run(() => {...}).
So in my stackblitz reproduction, the code running outside the angular zone would look like this:
this.zone.runOutsideAngular(() => {
const el = this.eventTarget.nativeElement as HTMLElement;
el.addEventListener('click', e => {
this.zone.run(() => this.showButton = true);
})
})
This, unlike calling detectChanges(), makes the this.showButton = true run in the correct zone, so that also elements created as a result of running that code with their event handlers are bound to the angular zone. This way, the event handlers always trigger change detection when reacting to DOM events.
This all boils down to a following takeaway: Declaring event handlers in a template doesn't automatically guarantee change detection in all scenarios.

In case someone wants to do tasks that don't trigger change detection, here is how:
import { NgZone }from '#angular/core';
taskSelection;
constructor
// paramenters
(
private _ngZone: NgZone,
)
// code block
{}
/*
Angular Lifecycle hooks
*/
ngOnInit() {
this.processOutsideOfAngularZone();
}
processOutsideOfAngularZone () {
var _this = this;
this._ngZone.runOutsideAngular(() => {
document.onselectionchange = function() {
console.log('Outside ngZone Done!');
let selection = document.getSelection();
_this.taskSelection["anchorNode"] = selection.anchorNode.parentElement;
_this.taskSelection["anchorOffset"] = selection.anchorOffset;
_this.taskSelection["focusOffset"] = selection.focusOffset;
_this.taskSelection["focusNode"] = selection.focusNode;
_this.taskSelection["rangeObj"] = selection.getRangeAt(0);
}
});
}
https://angular.io/api/core/NgZone#runOutsideAngular

Related

Angular Template Interpolation Slow to Respond

My Angular template needs to display a rapidly changing value, driven by mousemove events, which I am retrieving from my NgRx Store. The Store appears to be keeping up with the data changes but the resulting value displayed in the template lags behind and appears to only refresh when the mouse stops moving.
A component with approximately 300 DOM elements detects mousemove events and handles them outside of ngZone as follows:
ngAfterViewInit() {
const eventElement = this.eventDiv.nativeElement;
this.move$ = fromEvent(eventElement, 'mousemove');
this.leave$ = fromEvent(eventElement, 'mouseleave');
/*
* We are going to detect mouse move events outside of
* Angular's Zone to prevent Change Detection every time
* a mouse move event is fired.
*/
this.ngZone.runOutsideAngular(() => {
// Check we have a move$ and leave$ objects.
if (this.move$ && this.leave$) {
// Configure moveSubscription.
this.moveSubscription = this.move$.pipe(
takeUntil(this.leave$),
repeat()).subscribe((e: MouseEvent) => {
e.stopPropagation();
this.mouseMove.emit(e);
});
};
});
A parent component handles the resulting mouseMove event and still outside ngZone performs some Calculations to ascertain which element the mouse is over. Once the result has been calculated a function is called, passing in the calculated result, and within this function I dispatch an NgRx Action within ngZone using this.ngZone.run(() => { dispatch Action here }.
I can see that the Store reacts quickly to the changing data. I then have a separate component responsible for displaying the result. An Observable listens to the Selector's changing values and displays the result using interpolation.
Curiously I added an RxJs tap into the Observable declaration as follows:
public mouseoverLocationName$: Observable<string | null>;
constructor(
public store: Store<fromPilecapReducers.PilecapState>
) {
this.mouseoverLocationName$ = this.store.pipe(
select(fromPilecapSelectors.PilecapMapSelectors.selectMouseoverLocationName),
tap(locationName => {
console.log(`mouseoverLocation$: ${locationName}`);
})
);
}
The console logs out the locationName value nice and quickly. However the html element displaying the string is very slow and, as I said earlier, only appears to update when the mouse stops moving. The template code is as follows:
<h2>{{mouseoverLocationName$ | async}}</h2>
I've got to the point now where I can't see the wood for the trees! any suggestions or guidance very welcome.

Is it necessary to `window.removeEventListener('unload')`?

I am storing the lastest input data values to localStorage before the user leaves the webpage. Writing to localStorage is executed on window.onunload.
useEffect(() => {
const updater = () => {
updateStorageValue('last_input', input);
};
window.addEventListener('unload', updater);
return () => window.removeEventListener('unload', updater);
}, [input]);
Let's say the component (where this useEffect was used) was mounted when the user closed/refreshed the tab/window.
I am just curious if removing the unload event has any effect while the whole webpage will stop working.
React doesn't unmount the app components when closing/refreshing the page. So the answer to your question is: No, it doesn't have any effect.
But if it's a normal component living on the page (not mounting just before exit), then the cleanup function should be there to remove the previous unload event listener before adding the next one.
Your useEffect removes the unload event listener and adds a new one when input changes. But if you remove the cleanup function, then you will have as many unload event listeners as input updates.
For example, assume that the input value changes in the following order:
'R'
'Re'
'Rea'
'Reac'
'React'
In this case, these functions will be called on the unload event:
updateStorageValue('last_input', 'R');
updateStorageValue('last_input', 'Re');
updateStorageValue('last_input', 'Rea');
updateStorageValue('last_input', 'Reac');
updateStorageValue('last_input', 'React');

Why the binding event is executed after the setstate is re-rendered

The Test component has a state of num, the component implements the function of clicking the button num+1, the button is bound to the self-increment method, the keyup is also bound to the method, the method uses the setState to re-render the value of num, but the effect of clicking the button with the mouse is not the same as the keyboard trigger event. Could you tell me why, please?
When I click the button, console logs the num first, then done. But when I press enter, console logs done, then the num.
React15.5
class Test extends React.PureComponent {
constructor(){
super();
this.state = {
num : 1
}
}
add = () => {
const {num} = this.state;
this.setState({num:num+1},()=>{
console.log("done")
})
console.log(this.state.num)
}
componentDidMount() {
document.body.addEventListener('keyup', this.add);
}
componentWillUnmount() {
document.body.removeEventListener('keyup', this.add);
}
render() {
return(
<Button onClick={this.add} >add</Button>
<span>{this.state.num}</span>
)
}
}
I think #ids-van-der-zee's answer has some important points to consider. But I think the root cause for the difference in the console output is in this answer: https://stackoverflow.com/a/33613918/4114178
React batches state updates that occur in event handlers and lifecycle methods ... To be clear, this only works in React-controlled synthetic event handlers
I don't want to quote the entire answer, please read it, but in your case <Button onClick={this.add}... is a "React-controlled synthetic event handlers" where document.body.addEventListener('keyup', this.add); adds an event listener which is not part of the React infrastructure. So the Button onClick setState call is batched until the render completes (and your callback is not called until the batch is executed, where the keyup setState call is not batched and happens immediately--before the console.log(num) statement).
I don't think in your case this is of any consequence, but I think it shows great attention to detail that you noticed. There are cases where it become important, but I don't think you will hit them in this component. I hope this helps!
Using the setState method with an object as the first parameter will execute the method asynchronously as descibed here. Therefore the order of console logs in your code may differ every time.
The reason you see a difference between the click and keyboard events is because the click event is a React.SyntheticEvent and the keyboard event is a DOM event. It seems like handling the click event takes less time so your console.log(this.state.num) executes before React is done updating your state.
If you want to see the same behaviour for both triggers I would suggest to use the componentDidUpdate lifecycle method. This lifecycle method waits until the update is done.
Edit
You can make the add method async and then await the execution of the setState method. Resulting add method:
add = async () => {
await this.setState(
{
num: state.num + 1
},
() => {
console.log("done");
}
);
console.log(this.state.num);
};
This will make sure the code after await this.setState always waits untill the state has updated.

Can I register click-listener after state transition?

I have a component that looks like this:
<div ref='carousel' onClick={this.mobileZoomOut()} className='carousel'>
The mobileZoomout is suppose to register a special condition that only applies to small screens:
mobileZoomOut () {
const elem = this.state.zoom.carousel
if (this.zoomed('carousel') && elem.scale < 1.1) {
this.setState({zoom: {}})
}
}
The regular zoom is registered like this:
this.flky = new Flickity('.carousel', flickityOptions)
this.flky.on('staticClick', (e) => {
if (this.zoomed()) {
this.setState({ zoom: {} })
} else {
this.zoomIn('carousel', 0.774, 0)
this.zoomIn('thumbs', 0.208, 0.774)
}
})
staticClick is a custom event from the image-slider flickity, it is disabled when zooming in on mobile. That is why I need another zoom-out-event on mobile.
When adding mobileZoomOut I get this error, I believe the reason is that the click event on the carousel register both events, I don't want the onClick to be registered until after the staticClick -event is done.
Warning: setState(...): Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to `componentWillMount`.
I have tried replacing onClick with onMouseDown/onMouseUp it does not help. I think I could do it with a timeout, but I would like to avoid that and I'm not really sure how.
You are not passing your function as a prop, but rather executing it and passing its returned value.
foo() executes the foo-function. You want it executed when the event is triggered though.
this should do the trick.
<div ref='carousel' onClick={this.mobileZoomOut} className='carousel'>
you should be passing reference to function as a props.
<div ref='carousel' onClick={this.mobileZoomOut.bind(this)} className='carousel'>

Angular 2 bind input to function call

Is it acceptable to bind #Input() property of child component to a function call of parent component, for example:
<navigation
[hasNextCategory]="hasNextCategory()"
[hasPreviousCategory]="hasPreviousCategory()"
(nextClicked)="nextCategory()"
(previousClicked)="previousCategory()"
(submitClicked)="submit()"
</navigation>
This seems to work, but I wonder how. Are those inputs re-evaluated when event is fired from component, or what drives the input binding?
Sure. The function is called every time change detection runs and assigns the result of the function call to the input property.
You get an exception in devMode when 2 successive calls return different values. like
hasNextValue() {
return {};
}
Exception: Expression has changed ...
It is discouraged to bind to functions. Rather assign the result to a property and bind to this property.
If you know what you are doing it's fine though.
update
so returning true / false according to some internal state is not allowed? Strange that my navigation still works
This is actually allowed. If your state changes because of some event (click, timeout, ...) then Angular change detection expect changes. If Angular change detection calls the method twice (as it does in devMode) without any event happening in between, then it doesn't expect changes and throws the exception mentioned above. What Angular doesn't like is when change detection itself causes changes.
Below example would also cause an exception because change detection itself would modify the components state (this.someState = !this.someState;)
which is not allowed.
someState:boolean = false;
hasNextValue() {
this.someState = !this.someState;
return this.someState;
}
Two successive calls would return false and true even when no event happened in between.
This example would work fine though
someState:boolean = false;
#HostListener('click') {
this.someState = !this.someState;
}
hasNextValue() {
return this.someState;
}
because two successive calls (without any event in between) would return the same value.

Categories

Resources