Angular Template Interpolation Slow to Respond - javascript

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.

Related

Get dimensions of an image after render finishes, in Angular

I am trying to get the rendered size of an image in a component. Using the (load) event, I am getting the size of the image as it is displayed at that moment (pic1) and the "final" size after the page fully renders. I guess I can use ngAfterViewChecked but that would mean I am constantly calculating that size when the only meaningful instance for that is when the window opens or resizes
An alternate approach that you can use is, subscribe to changes in Window: resize event using the HostListener in order to calculate image dimensions, as shown in the following code snippet.
import { HostListener } from '#angular/core';
#HostListener('window:resize', ['$event'])
// Invoke function to compute the image dimensions here
}
Note: You can also invoke the function to compute the dimensions of the said image inside AfterViewInit lifecycle hook rather than on load event.
Another approach is to calculate image dimensions by listening to changes in the Window: resize event using the fromEvent, as shown in the code snippet below.
import { fromEvent } from 'rxjs';
changeSubscription$: Subscription
ngOnInit() {
this.windowResizeEventChanges$ = fromEvent(window, 'resize').subscribe(event => {
// Invoke function to compute the image dimensions here
});
}
ngOnDestroy() {
this.windowResizeEventChangesn$.unsubscribe() // Unsubscribe to stop listening the changes in Window: resize
}
Read more about fromEvent here.
I ended up using a Mutation Observer, watching for attribute changes, attached to the item that contains the image. In my case there is a need for some management because it doesn't just run once, but it solves the issue
ngAfterViewInit() {
this.refElement = this.appsContainer.nativeElement.querySelector('item') as HTMLElement;
const config = { attributes: true };
this.mutationObserver = new MutationObserver((mutation) => {
const target = mutation[0].target as HTMLElement;
//some code here to execute based on some conditions and to remove the observer
}
});
this.mutationObserver.observe(this.refElement, config);}

Angular click event handler not triggering change detection

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

Nested web-components and event handling

I'm writing a memory game in javascript. I have made a web-component for the cards, <memory-card> and a web-component to contain the cards and handle the game state <memory-game>. The <memory-card> class contains its image path for when its turned over, the default image to display as the back of the card, its turned state and an onclick function to handle switching between the states and the images.
The <memory-game> class has a setter that receives an array of images to generate <memory-cards> from. What would be the best method to handle updating the game state in the <memory-game> class? Should I attach an additional event listener to the <memory-card> elements there or is there a better way to solve it? I would like the <memory-card> elements to only handle their own functionality as they do now, ie changing images depending on state when clicked.
memory-game.js
class memoryGame extends HTMLElement {
constructor () {
super()
this.root = this.attachShadow({ mode: 'open' })
this.cards = []
this.turnedCards = 0
}
flipCard () {
if (this.turnedCards < 2) {
this.turnedCards++
} else {
this.turnedCards = 0
this.cards.forEach(card => {
card.flipCard(true)
})
}
}
set images (paths) {
paths.forEach(path => {
const card = document.createElement('memory-card')
card.image = path
this.cards.push(card)
})
}
connectedCallback () {
this.cards.forEach(card => {
this.root.append(card)
})
}
}
customElements.define('memory-game', memoryGame)
memory-card.js
class memoryCard extends HTMLElement {
constructor () {
super()
this.root = this.attachShadow({ mode: 'open' })
// set default states
this.turned = false
this.path = 'image/0.png'
this.root.innerHTML = `<img src="${this.path}"/>`
this.img = this.root.querySelector('img')
}
set image (path) {
this.path = path
}
flipCard (turnToBack = false) {
if (this.turned || turnToBack) {
this.turned = false
this.img.setAttribute('src', 'image/0.png')
} else {
this.turned = true
this.img.setAttribute('src', this.path)
}
}
connectedCallback () {
this.addEventListener('click', this.flipCard())
}
}
customElements.define('memory-card', memoryCard)
implementing the custom event after Supersharp's answer
memory-card.js (extract)
connectedCallback () {
this.addEventListener('click', (e) => {
this.flipCard()
const event = new CustomEvent('flippedCard')
this.dispatchEvent(event)
})
}
memory-game.js (extract)
set images (paths) {
paths.forEach(path => {
const card = document.createElement('memory-card')
card.addEventListener('flippedCard', this.flipCard.bind(this))
card.image = path
this.cards.push(card)
})
}
In the <memory-card>:
Create with CustomEvent() and dispatch a custom event with dispatchEvent()
In the <memory-game>:
Listen to your custom event with addEventListener()
Because the cards are nested in the game, the event will bubble naturally to the container.
This way the 2 custom elements will stay loosley coupled.
Supersharps answer is not 100% correct.
click events bubble up the DOM,
but CustomEvents (inside shadowDOM) do not
Why firing a defined event with dispatchEvent doesn't obey the bubbling behavior of events?
So you have to add the bubbles:true yourself:
[yoursender].dispatchEvent(new CustomEvent([youreventName], {
bubbles: true,
detail: [yourdata]
}));
more: https://javascript.info/dispatch-events
note: detail can be a function: How to communicate between Web Components (native UI)?
For an Eventbased programming challenge
this.cards.forEach(card => {
card.flipCard(true)
})
First of all that this.cards is not required, as all cards are available in [...this.children]
!! Remember, in JavaScript Objects are passed by reference, so your this.cards is pointing to the exact same DOM children
You have a dependency here,
the Game needs to know about the .flipCard method in Card.
► Make your Memory Game send ONE Event which is received by EVERY card
hint: every card needs to 'listen' at Game DOM level to receive a bubbling Event
in my code that whole loop is:
game.emit('allCards','close');
Cards are responsible to listen for the correct EventListener
(attached to card.parentNode)
That way it does not matter how many (or What ever) cards there are in your game
The DOM is your data-structure
If your Game no longer cares about how many or what DOM children it has,
and it doesn't do any bookkeeping of elements it already has,
shuffling becomes a piece of cake:
shuffle() {
console.log('► Shuffle DOM children');
let game = this,
cards = [...game.children],//create Array from a NodeList
idx = cards.length;
while (idx--) game.insertBefore(rand(cards), rand(cards));//swap 2 random DOM elements
}
My global rand function, producing a random value from an Array OR a number
rand = x => Array.isArray(x) ? x[rand(x.length)] : 0 | x * Math.random(),
Extra challenge
If you get your Event based programming right,
then creating a Memory Game with three matching cards is another piece of cake
.. or 4 ... or N matching cards
It would be very helpful to see some of your existing code to know what you have tried. But without it you ca do what #Supersharp has proposed, or you can have the <memory-game> class handle all events.
If you go this way then your code for <memory-card> would listen for click events on the entire field. It would check to see if you clicked on a card that is still face down and, if so, tell the card to flip. (Either through setting a property or an attribute, or through calling a function on the <memory-card> element.)
All of the rest of the logic would exist in the <memory-game> class to determine if the two selected cards are the same and assign points, etc.
If you want the cards to handle the click event then you would have that code generate a new CustomEvent to indicate that the card had flipped. Probably including the coordinates of the card within the grid and the type of card that is being flipped.
The <memory-game> class would then listen for the flipped event and act upon that information.
However you do this isn't really a problem. It is just how you want to code it and how tied together you want the code. If you never plan to use this code in any other games, then it does not matter as much.

Angular: RequestAnimationFrame and ChangeDetectorRef for updating view

In Angular, after a view has initialized, one part of it might be recalculated dynamically; in particular:
When someStream$ updates
I read the value of an already rendered element in the DOM
And set the value of another element (its height) according to the read value
an attribute listener in the template sets the height accordingly to the viewHeightvalue
So the code looks like this:
this._subs.push(this.someStream$.subscribe(() => {
this.zone.runOutsideAngular(() => {
requestAnimationFrame(() => {
const spaceTop: number = this.hostEl.nativeElement.getBoundingClientRect().top;
this.viewHeight = window.innerHeight - spaceTop;
this.cdr.detectChanges();
});
});
}));
<someElement [style.height.px]="viewHeight"></someElement>
I need ChangeDetectorRef.detectChanges(); to apply the changes instantly. My question is, why do I also need requestAnimationFrame? Why does the code not work without it? From my understanding requestAnimatoinFrame triggers re-rendering (in a browser-optimized way), but NgZone, activated by ChangeDetectorRef, should do that already.

Apply actions on Javascript events depending on current view context in AngularJS

I am building an AngularJS SPA which has multiple elements that I want to bind keypress events to. The difficulty comes in the fact that, due to the Pinterest style modal window routing, several of the elements, and thus controller scopes, can be visible at once. This means that certain keypress event will fire in more than one controller, including potential background content.
What I want is to bind the events, or at least only take action on those events depending on the current state / context of the application. Is there a good way to handle this?
What I have considered:
Using a Service that maintains a reference to the state and handles the event binding on a global level to which controllers can subscribe to events based on their context. But I do not know how to subscribe functions to an Angular service in this manor.
Unbind and bind events on the $routeChange event but feel this could get very messy.
I know this is a conceptual question but I have been stuck on this for some time now and any help would be much appreciated.
Update
I have attempted to make a Plunk here to demonstrate this. Each context (an abstract state of the application) has a directive that binds to a keypress event. I want event handler on the context in view (i.e. the active state) to be the only one that executes.
I have tried to make a simplified but relevant example. Note:
Most but not all of the contexts/states will have a route associated so I cant just rely on $stateChange events
Many states are modal windows, meaning background elements still visible may also be listening for a key press. I am not sure I can guarantee the DOM order in all cases.
I have tried using the elements focus, but this does not work (think tabbing out and back into the application, problems when those elements involve forms etc.)
Hope this makes it clearer, I am happy to update further.
After a lengthy discussion in comments (see above), it boiled down to the requirement of a service that keeps track of the current state and key-presses and accepts "subscriptions" for key-presses when in a specific state.
Below is a generic implementation of such a service, with the following attributes:
The current-state is set externally (you can implement it is any way you like: ngRoute, uiRouter, $location change events or whatever else might determine the state of your app).
It exposes two methods: One for setting the state and one for subscribing to a key-down event for a specific state and keyCode.
The subscribe function returns an unsubscribe function which can be used for...you guessed it...unsubscribing !
This demo implementation assumes that at the time of triggering the key-down event there is no $digest cycle in progress. If you have different requirements, you can check before calling $rootScope.$apply().
app.factory('StateService', function ($document, $rootScope) {
var currentState = 'init';
var stateCallbacks = {};
function fireKeydown(evt) {
$rootScope.$apply(function () {
((stateCallbacks[currentState] || {})[evt.keyCode] || []).forEach(
function (cb) {
cb(evt, currentState);
}
);
});
}
function setState(newState) {
currentState = newState;
}
function subscribeToKeydown(state, keyCode, callback) {
if (!state || !keyCode || !callback) {
return angular.noop;
}
stateCallbacks[state] = stateCallbacks[state] || {};
stateCallbacks[state][keyCode] = stateCallbacks[state][keyCode] || [];
stateCallbacks[state][keyCode].push(callback);
function unsubscribe() {
return ((stateCallbacks[state] || {})[keyCode] || []).some(
function (cb, idx, arr) {
if (cb === callback) {
arr.splice(idx, 1);
console.log('Unsubscribed from state: ' + state);
return true;
}
}
);
}
console.log('Subscribed to state: ' + state);
return unsubscribe;
}
$document.on('keydown', fireKeydown);
return {
setState: setState,
subscribeToKeydown: subscribeToKeydown
};
});
See, also, this short demo.

Categories

Resources