Angular Mouseover Events Preventing Change Detection Updating The Template - javascript

Angular v12.2,
NgRx v12.4,
RxJs v6.6.0
I am developing an Angular web app and one part of it displays a data bound grid of approximately 300 squares. A little like a chess board. As the user moves the mouse over the grid I display some data relating to the individual square the mouse is hovering over. However, no matter what I try the displayed data lags behind the mouse and only updates when the mouse slows or stops. Curiously I am following a method which I successfully adopted for another project which is really performant but was using Angular v10, NgRx v10.
The grid of components is a simple *ngFor="let location of locations; let i = index; trackBy:locationTrackByFunction".
I have chosen to create a mouse-tracker component which sits above my grid and its only responsibility is to get the mouse offsetX and offsetY and emit events outside of the Angular Zone for the mouse-tracker-container parent component to receive. I did this to minimise the change detection being triggered for every change in the mousemove event. My mouse-tracker template is as follows:
<div #eventDiv id="mouse-tracker"></div>
The code is as follows:
#ViewChild('eventDiv', { static: true }) eventDiv!: ElementRef;
#Output() mouseMove = new EventEmitter();
private move$: Observable<MouseEvent> | undefined;
private leave$: Observable<MouseEvent> | undefined;
private moveSubscription: Subscription = new Subscription;
constructor(private ngZone: NgZone) {}
ngOnInit() {}
ngOnDestroy(): void {
this.moveSubscription.unsubscribe();
}
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();
e.preventDefault();
this.mouseMove.emit(e);
});
};
});
};
The mouseMove event is received by the hosting grid component and it simply emits its own event passing the data back to its own grid-container parent as follows:
public onMouseMove(e: MouseEvent) {
this.mouseMove.emit(e);
};
The grid-container component, on receiving the mouseMove event simply updates an RxJs Subject as follows:
public onMouseMove(e: MouseEvent) {
this.mouseOffsetsSubject$.next({ x: e.offsetX, y: e.offsetY });
};
The mouseoverLocationSubject$ along with two other Observables are brought together in an RxJs CombineLatest and ultimately after performing some calculations Dispatch an NgRx Action every time the mouse is over a different location (using distinctUntilChanged()) in the Angular Zone (to implement change detection) as follows:
this.ngZone.run(() => {
if (location !== null) {
// Dispatch an NgRx Action to save the Location in the Store.
this.store.dispatch(fromCoreActions.LocationActions.setMouseoverLocationId(
{ mouseoverLocationId: location.id }
));
}
else {
// Dispatch an NgRx Action to save the Location in the Store.
this.store.dispatch(fromCoreActions.LocationActions.setMouseoverLocationId(
{ mouseoverLocationId: null }
));
};
});
If I console log out the code in the reducer, or use the Redux dev tools in Chrome, I can see the NgRx Store being rapidly updated. No problem.
A location-display component is responsible for displaying the name of the mouse-over location. Its parent location-display-container component has an Observable being updated by the changing NgRx Selector as follows:
public mouseoverLocation$: Observable<Location | null>;
...
this.mouseoverLocation$ = this.store.pipe(
select(fromCoreSelectors.locationSelectors.selectMouseoverLocation),
tap(location => {
//console.log(`mouseoverLocation$: ${location?.name}`);
})
);
If I un-comment the console.log() the changing stream of data being received is rapid and closely matches the moving mouse. No delays up to here.
A simple async pipe passes the mouseoverLocation object to the child component [mouseoverLocation]="mouseoverLocation$ | async" which receives the object #Input() mouseoverLocation: Location | null = null; and actually displays the data in the template using interpolation as follows:
<div fxFlex="{{lineHeight}}px">
<div fxLayout="row" fxLayoutGap="8px">
<div fxFlex="200px;" class="mouse-over-sub-hdg">Location: </div>
<div fxFlex>{{mouseoverLocation?.name}}</div>
</div>
</div>
When run, both in developer and production builds, the displayed value lags badly behind the mouse and does not at all match the stream of data being received in the Observable. In fact if I circle the mouse over the grid so it never stops moving the template does not update at all. Only when I slow the mouse or stop does it jump to the value of the square the mouse is over.
I have tried refactoring this in many different ways including forcing change detection as each new value is received by the Observable cdRef.markForCheck() and cdRef.detectChanges(). I tried creating a #ViewChild of the location-display component and directly updating the mouseoverLocation value. I tried using the ngrxPush pipe. Every time the same result. All components implement OnPush.
I removed all the Zone related code thinking that this may be the root of the problem, but it lagged just the same, though I could see the considerable increase in change detection for every mousemove event.
Perhaps Chrome's DevTools Performance tab may provide a clue but honestly I can't get my head around how to use it.
Any advise, thoughts or suggestions very welcome. Thank you.

Related

Unable to listen to emitted events with Ionic 5

I created a simple ionic app that allows users to book services. The user would select a service category, choose a service(s) then navigate to a form to complete the booking.
I've setup an event bus using tiny emitter since the project uses vue 3 with the composition api. The data emits as expected however when navigating to the booking form the listener is not triggered.
The expected behaviour is to get the selected service(s) and send it along with the rest of the booking info to a REST api.
eventBus.js
import { TinyEmitter } from 'tiny-emitter';
const emitter = new TinyEmitter();
const eventBus = () => {
return { emitter };
};
export default eventBus;
Service.vue
// template
<ion-button routerDirection="forward" routerLink="/booking" #click="sendEvent">Book Now</ion-button>
// script
import eventBus from './eventBus';
export default {
...
setup() {
...
const sendEvent = () => {
eventBus().emitter.emit('selected-service', 100) // the real code emits an array
}
return { sendEvent }
}
}
Booking.vue - Nothing happens in the console log
<script>
...
onMounted(() => {
eventBus().emitter.on('selected-service', (payload) => {
console.log('listener', payload);
})
})
</script>
I know this works in a regular Vue 3 project but I'm not sure why it's not working with ionic.
Things I've tried
Using the native emitter via context as a setup param. https://v3.vuejs.org/guide/composition-api-setup.html#accessing-component-properties
Using the mitt package as described here: Vue 3 Event Bus with Composition API
Emitting the event when the user chooses a service rather than when they click "Book Now"
Calling the listener in setup() directly rather than onMounted.
UPDATE
I noticed the listener gets called when I navigate off the booking page then back to it. So if I go from service details -> booking -> back to service details -> booking it triggers the bus and the payload is captured.
This may be a framework level bug. I've spoken to the Ionic team via twitter and was advised to use query params instead so that's the route I took.
may be rxjs helps you
import { Subject } from 'rxjs';
private newProduct = new Subject<any>();
publishNewProduct() {
this.newProduct.next();
}
subscribeNewProduct(): Subject<any> {
return this.newProduct;
}
Just had the same problem, to make it worse, in my case the bug was intermittent and only present if the dev tools console was closed. Opening the dev tools or using alerts would result in the component being rendered in time to receive the event.. I almost lost my sanity over it.
In my case using a watcher on the a prop using immediate: true was the cleanest solution.
I think this bug is really nasty since global events support have been removed from Vue2 and the Vue3 upgrade docs explicitly suggest to use tiny emitter to achieve it.
This leads to weird behaviors and bugs that are almost impossible to trace back. For this reason, I think the global event pattern should be avoided as much as possible.
As a final note if it can help someone, this is how I ended up being able to use console logs to trace the problem back to the global event bus:
const logs = []
app.config.globalProperties.$log = function () { logs.push(arguments) }
window.viewlogs = () => {
for (let i = 0; i < logs.length; i++) {
console.log.apply(window, logs[i])
}
}
Once the bug occured I could open the dev tools and view logs using window.viewlogs()

Why ReplaySubject is getting called on ngOnInit?

I am trying to interact between 2 components i.e. the header component and the router-outlet component which contains a reactive form. Some days ago after great research, I got an answer which told me to use shared service and there takes a ReplaySubject variable. This will act as an Observable which will be called in ngOnInit of the component in router-outlet. So the problem arises here. Everything is working fine but just now while testing noticed a small bug. After clicking the button in header the event is firing in the main component but it is staying active until I hard reload the page.
Below is my code:
shared.service.ts
public updateSrc = new ReplaySubject<any>(1);
public updateClick = this.updateSrc.asObservable();
header.component.ts (A button is clicked here)
update() {
return this.dataService.updateSrc.next(true);
}
main.component.ts (Here the main operation is happening on click of header button)
updateClicked = new Subject<any>();
ngOnInit() {
this.dataService.updateClick.pipe(takeUntil(this.updateClicked)).subscribe({
next: value => console.log(value); // At first click of button this happening perfectly but if coming from different route & had clicked the header button previously this thing is getting triggered
});
}
ngOnDestroy() {
this.updateClicked.next(); // Closing subscription here
}
Can anyone suggest to me how to reset this?
The ReplaySubject (and the BehaviorSubject) stores previously emitted values and emit those whenever a new subscriber is added.
If you don't want new subscribers to receive previously emitted value(s), you can always use Subject which doesn't store emitted values.
You may want to read more about the available variations in the docs.

Angular 5+: Child Component Subscribed to Parent Observable Does Not Get Value On Unhide

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?

React DND - get coordinates of dragged element as the mouse moves

I have an image that when I drag I want to implement a rotation too. The solution I had in mind was to use React DnD to get the xy coordinates of the dragged image position and calculate the difference between the original image location. The value from this difference would form the basis to make the rotation.
I've looked at examples on the ReactDnD library and see that the DragSource Specification can access the Monitor variable. This monitor variable has access to methods like getInitialClientOffset(). When I implement a console.log() of this value it shows me the last last coordinate set when I release the mouse.
Using this library, is there an easy way to get the current position of the dragged element as I move the mouse?
import React from 'react'
import { DragSource } from 'react-dnd'
// Drag sources and drop targets only interact
// if they have the same string type.
// You want to keep types in a separate file with
// the rest of your app's constants.
const Types = {
CARD: 'card',
}
/**
* Specifies the drag source contract.
* Only `beginDrag` function is required.
*/
const cardSource = {
beginDrag(props,monitor,component) {
// Return the data describing the dragged item
const clientOffset = monitor.getSourceClientOffset();
const item = { id: props.id }
console.log(clientOffset);
return item
},
isDragging(props, monitor){
console.log(monitor.getClientOffset())
},
endDrag(props, monitor, component) {
if (!monitor.didDrop()) {
return
}
// When dropped on a compatible target, do something
const item = monitor.getItem()
const dropResult = monitor.getDropResult()
console.log(item,dropResult)
//CardActions.moveCardToList(item.id, dropResult.listId)
},
}
/**
* Specifies which props to inject into your component.
*/
function collect(connect, monitor) {
return {
// Call this function inside render()
// to let React DnD handle the drag events:
connectDragSource: connect.dragSource(),
// You can ask the monitor about the current drag state:
isDragging: monitor.isDragging(),
}
}
function Card(props) {
// Your component receives its own props as usual
const { id } = props
// These two props are injected by React DnD,
// as defined by your `collect` function above:
const { isDragging, connectDragSource } = props
return connectDragSource(
<div>
I am a draggable card number {id}
{isDragging && ' (and I am being dragged now)'}
</div>,
)
}
// Export the wrapped version
export default DragSource(Types.CARD, cardSource, collect)(Card)
From what I remember, custom drag layer had serious performance issues. It's better to directly subscribe to the underlying API (the official documentation is severely lacking, but you can get this information from reading drag layer source):
const dragDropManager = useDragDropManager();
const monitor = dragDropManager.getMonitor();
React.useEffect(() => monitor.subscribeToOffsetChange(() => {
const offset = monitor.getClientOffset();
// do stuff like setState, though consider directly updating style through refs for performance
}), [monitor]);
You can also use some flag based on drag state to only subscribe when needed (simple isDragging && monitor.subscribe... plus dependencies array does the trick).
I'm posting this way after the fact, but in case anyone is looking for this answer, the react-dnd way of doing this is by using what they call a Drag Layer - it gives you a way to use a custom component to be displayed when dragging.
They have a full example here:
https://codesandbox.io/s/github/react-dnd/react-dnd/tree/gh-pages/examples_hooks_js/02-drag-around/custom-drag-layer?from-embed=&file=/src/CustomDragLayer.jsx
Also in the docs you want to look at useDragLayer and DragLayerMonitor
The useDrop hook has a hover(item, monitor) method which does what you are looking for. It triggers repeatedly while you are hovering over the drop target.
You can use
ReactDOM.findDOMNode(component).getBoundingClientRect();
Ref: https://reactjs.org/docs/react-dom.html#finddomnode
Or just make a ref to your component instance and get getBoundingClientRect() on the instance within a onmousemove event.
As far as I understand all you need offset of currently dragging element, via HTML5Backend you can't get, but if you use MouseBackend you can easily get the coordinates
https://github.com/zyzo/react-dnd-mouse-backend
see example of with preview

Angular 4 - "Expression has changed after it was checked" error while using NG-IF

I setup a service to keep track of logged in users. That service returns an Observable and all components that subscribe to it are notified (so far only a single component subscribe to it).
Service:
private subject = new Subject<any>();
sendMessage(message: boolean) {
this.subject.next( message );
}
getMessage(): Observable<any> {
return this.subject.asObservable();
}
Root App Component: (this component subscribes to the observable)
ngAfterViewInit(){
this.subscription = this._authService.getMessage().subscribe(message => { this.user = message; });
}
Welcome Component:
ngOnInit() {
const checkStatus = this._authService.checkUserStatus();
this._authService.sendMessage(checkStatus);
}
App Component Html: (this is where the error occurs)
<div *ngIf="user"><div>
What I'm trying to do:
I want every component (except the Root App Component) to send the users logged-in state to the Root App Component so I can manipulate the UI within the Root App Component Html.
The issue:
I get the following error when the Welcome Component is initialised.
Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'true'.
Please note this error occurs on this *ngIf="user" expression which is located within Root App Components HTML file.
Can someone explain the reason for this error and how I can fix this?
On a side note: If you think theres a better way to achieve what I'm trying to do then please let me know.
Update 1:
Putting the following in the constructor solves the issue but don't want to use the constructor for this purpose so it seems it's not a good solution.
Welcome Component:
constructor(private _authService: AuthenticationService) {
const checkStatus = this._authService.checkUserStatus();
this._authService.sendMessage(checkStatus);
}
Root App Component:
constructor(private _authService: AuthenticationService){
this.subscription = this._authService.getMessage().subscribe(message => { this.usr = message; });
}
Update 2:
Here's the plunkr. To see the error check the browser console. When the app loads a boolean value of true should be displayed but I get the error in the console.
Please note that this plunkr is a very basic version of my main app. As the app is bit large I couldn't upload all the code. But the plunkr demonstrates the error perfectly.
What this means is that the change detection cycle itself seems to have caused a change, which may have been accidental (ie the change detection cycle caused it somehow) or intentional. If you do change something in a change detection cycle on purpose, then this should retrigger a new round of change detection, which is not happening here. This error will be suppressed in prod mode, but means you have issues in your code and cause mysterious issues.
In this case, the specific issue is that you're changing something in a child's change detection cycle which affects the parent, and this will not retrigger the parent's change detection even though asynchronous triggers like observables usually do. The reason it doesn't retrigger the parent's cycle is becasue this violates unidirectional data flow, and could create a situation where a child retriggers a parent change detection cycle, which then retriggers the child, and then the parent again and so on, and causes an infinite change detection loop in your app.
It might sound like I'm saying that a child can't send messages to a parent component, but this is not the case, the issue is that a child can't send a message to a parent during a change detection cycle (such as life cycle hooks), it needs to happen outside, as in in response to a user event.
The best solution here is to stop violating unidirectional data flow by creating a new component that is not a parent of the component causing the update so that an infinite change detection loop cannot be created. This is demonstrated in the plunkr below.
New app.component with child added:
<div class="col-sm-8 col-sm-offset-2">
<app-message></app-message>
<router-outlet></router-outlet>
</div>
message component:
#Component({
moduleId: module.id,
selector: 'app-message',
templateUrl: 'message.component.html'
})
export class MessageComponent implements OnInit {
message$: Observable<any>;
constructor(private messageService: MessageService) {
}
ngOnInit(){
this.message$ = this.messageService.message$;
}
}
template:
<div *ngIf="message$ | async as message" class="alert alert-success">{{message}}</div>
slightly modified message service (just a slightly cleaner structure):
#Injectable()
export class MessageService {
private subject = new Subject<any>();
message$: Observable<any> = this.subject.asObservable();
sendMessage(message: string) {
console.log('send message');
this.subject.next(message);
}
clearMessage() {
this.subject.next();
}
}
This has more benefits than just letting change detection work properly with no risk of creating infinite loops. It also makes your code more modular and isolates responsibility better.
https://plnkr.co/edit/4Th7m0Liovfgd1Z3ECWh?p=preview
Declare this line in constructor
private cd: ChangeDetectorRef
after that in ngAfterviewInit call like this
ngAfterViewInit() {
// it must be last line
this.cd.detectChanges();
}
it will resolve your issue because DOM element boolean value doesnt get change. so its throw exception
Your Plunkr Answer Here Please check with AppComponent
import { AfterViewInit, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild } from '#angular/core';
import { Subscription } from 'rxjs/Subscription';
import { MessageService } from './_services/index';
#Component({
moduleId: module.id,
selector: 'app',
templateUrl: 'app.component.html'
})
export class AppComponent implements OnDestroy, OnInit {
message: any = false;
subscription: Subscription;
constructor(private messageService: MessageService,private cd: ChangeDetectorRef) {
// subscribe to home component messages
//this.subscription = this.messageService.getMessage().subscribe(message => { this.message = message; });
}
ngOnInit(){
this.subscription = this.messageService.getMessage().subscribe(message =>{
this.message = message
console.log(this.message);
this.cd.detectChanges();
});
}
ngOnDestroy() {
// unsubscribe to ensure no memory leaks
this.subscription.unsubscribe();
}
}
Nice question, so, what causes the problem? What's the reason for this error? We need to understand how Angular change detection works, I'm gonna explain briefly:
You bind a property to a component
You run an application
An event occurs (timeouts, ajax calls, DOM events, ...)
The bound property is changed as an effect of the event
Angular also listens to the event and runs a Change Detection Round
Angular updates the view
Angular calls the lifecycle hooks ngOnInit, ngOnChanges and ngDoCheck
Angular run a Change Detection Round in all the children components
Angular calls the lifecycle hooks ngAfterViewInit
But what if a lifecycle hook contains a code that changes the property again, and a Change Detection Round isn't run? Or what if a lifecycle hook contains a code that causes another Change Detection Round and the code enters into a loop? This is a dangerous eventuality and Angular prevents it paying attention to the property to don't change in the while or immediately after. This is achieved performing a second Change Detection Round after the first, to be sure that nothing is changed. Pay attention: this happens only in development mode.
If you trigger two events at the same time (or in a very small time frame), Angular will fire two Change Detection Cycles at the same time and there are no problems in this case, because Angular since both the events trigger a Change Detection Round and Angular is intelligent enough to understand what's happening.
But not all the events cause a Change Detection Round, and yours is an example: an Observable does not trigger the change detection strategy.
What you have to do is to awake Angular triggering a round of change detection. You can use an EventEmitter, a timeout, whatever causes an event.
My favorite solution is using window.setTimeout:
this.subscription = this._authService.getMessage().subscribe(message => window.setTimeout(() => this.usr = message, 0));
This solves the problem.
To understand the error, read:
Everything you need to know about the ExpressionChangedAfterItHasBeenCheckedError error
You case falls under the Synchronous event broadcasting category:
This pattern is illustrated by this plunker. The application is
designed to have a child component emitting an event and a parent
component listening to this event. The event causes some of the parent
properties to be updated. And these properties are used as input
binding for the child component. This is also an indirect parent
property update.
In your case the parent component property that is updated is user and this property is used as input binding to *ngIf="user". The problem is that you're triggering an event this._authService.sendMessage(checkStatus) as part of change detection cycle because you're doing it from lifecycle hook.
As explained in the article you have two general approaches to working around this error:
Asynchronous update - this allows triggering an event outside of change detection process
Forcing change detection - this adds additional change detection run between the current run and the verification stage
First you have to answer the question if there's any need to trigger the even from the lifecycle hook. If you have all the information you need for the even in the component constructor I don't think that's the bad option. See The essential difference between Constructor and ngOnInit in Angular for more details.
In your case I would probably go with either asynchronous event triggering instead of manual change detection to avoid redundant change detection cycles:
ngOnInit() {
const checkStatus = this._authService.checkUserStatus();
Promise.resolve(null).then(() => this._authService.sendMessage(checkStatus););
}
or with asynchronous event processing inside the AppComponent:
ngAfterViewInit(){
this.subscription = this._authService.getMessage().subscribe(Promise.resolve(null).then((value) => this.user = message));
The approach I've shown above is used by ngModel in the implementation.
But I'm also wondering how come this._authService.checkUserStatus() is synchronous?
I recently encountered the same issue after migration to Angular 4.x, a quick solution is to wrap each part of the code which causes the ChangeDetection in setTimeout(() => {}, 0) // notice the 0 it's intentional.
This way it will push the emit AFTER the life-cycle hook therefore not cause change detection error.
While I am aware this is a pretty dirty solution it's a viable quickfix.
Don't change the var in ngOnInit, change it in constructor
constructor(private apiService: ApiService) {
this.apiService.navbarVisible(false);
}

Categories