this.myService.myEvent.toRx().subscribe() called but no DOM refresh (Zone trigger) - javascript

I'm playing with angular2 alpha 40 with ng2-play starter from pawel.
Examples are in typescript.
I have a service MovieList like this:
export class Movie {
selected: boolean = false
constructor(public name:string, public year:number, public score:number) {}
}
export class MovieListService {
list: Array<Movie>
selectMovie = new EventEmitter();
constructor() {
this.list = [new Movie('Star Wars', 1977, 4.4)];
}
add(m:Movie) {
this.list.push(m);
}
remove(m:Movie) {
for(var i = this.list.length - 1; i >= 0; i--) {
if(this.list[i] === m) {
if(m.selected) this.selectMovie.next();
this.list.splice(i, 1);
}
}
}
select(m:Movie) {
this.list.map((m) => m.selected = false);
m.selected = true;
this.selectMovie.next(m);
}
}
I have a component showing the movies list and make possible to select one by clicking on it, which call select() in the service above.
And I have another component (on the same level, I don't want to use (selectmovie)="select($event)") which subscribe to the movie selection event like this:
#Component({
selector: 'movie-edit',
})
#View({
directives: [NgIf],
template: `
<div class="bloc">
<p *ng-if="currentMovie == null">No movie selected</p>
<p *ng-if="currentMovie != null">Movie edition in progress !</p>
</div>
`
})
export class MovieEditComponent {
currentMovie:Movie
constructor(public movieList: MovieListService) {
this.movieList.selectMovie.toRx().subscribe(this.movieChanged);
setTimeout(() => { this.movieChanged('foo'); }, 4000);
}
movieChanged(f:Movie = null) {
this.currentMovie = f;
console.log(this.currentMovie);
}
}
The event is subscribed using .toRx().subscribe() on the eventEmitter.
movieChanged() is called but nothing happen in the template..
I tried using a timeout() calling the same function and changes are refleted in the template.

The problem seems to be the fact that subscribe expects an Observer or three functions that work as an observer while you are passing a normal function. So in your code I just changed movieChanged to be an Observer instead of a callback function.
movieChanged: Observer = Observer.create(
(f) => { this.currentMovie = f; }, // onNext
(err) => {}, // onError
() => {} // onCompleted
);
See this plnkr for an example. It would have been nice to see a minimal working example of your requirement so my solution would be closer to what you are looking for. But if I understood correctly this should work for you. Instead of a select I just used a button to trigger the change.
Update
You can avoid creating the Òbserver just by passing a function to the subscriber method (clearly there's a difference between passing directly a function and using a class method, don't know really why is different)
this.movieList.selectMovie.toRx().subscribe((m: Movie = null) => {
this.currentMovie = m;
});
Note
EventEmitter is being refactored, so in future releases next will be renamed to emit.
Note 2
Angular2 moved to #reactivex/rxjs but in the plnkr I'm not able to use directly those libs (didn't find any cdn). But you can try in your own project using these libs.
I hope it helps.

The movieChanged function expects the movie object and not the String. Try changing below code
setTimeout(() => { this.movieChanged('foo'); }, 4000);
to
setTimeout(() => { this.movieChanged(new Movie('Troy', 2000 , 8)); }, 4000);

Related

Vitest -Unable to access the DOM inside a test

I am new to front-end testing, and I am trying to get a good grasp on it.
In the process, inside one project, I am creating a test for a Vue component. I want to test that the behaviour of the code is correct (The component has code inside the mounted() hook that has to perform basically some checks and an API call).
I want to check that the code reaches one method. Previously to that, the code creates a click event listener to one element in the DOM.
My test emulates a click event (triggers it), but it cannot assert that the proper method has been called after the click event.
This is due to it not finding the element in the DOM to which it has to add the event listener. It seems that the code cannot find anything inside the document (using .getElementById()).
I wonder why, and how I would resolve this, since I have been stuck here for hours and I haven't found any solution that could work here, even when I have learned some interesting things in the process. I will leave a code example with the code structure I have built:
Inside the component:
<template>
// ...
<button id = "myButton">Add</button>
</template>
<script>
import { classInExternalScriptsFile } from "#/scripts/externalScriptsFile ";
let classIESF = new classInExternalScriptsFile();
export default {
methods: {
setup: function () {
classIESF.setupMethod();
},
},
mounted() {
this.setupMethod();
},
};
</script>
Inside the scriptsFile
export class classInExternalScriptsFile {
setupMethod() {
let myButton = document.getElementById("myButton") // <-- getElementById() returns a null here
if (typeof myButton !== "undefined" && myButton !== null) {
myButton.onclick = () => { // <-- The test code complains because it cannot enter here
// Some lines...
this.mySuperMethod()
}
}
}
mySuperMethod() {
// API call etc.
}
}
Inside the .spec.js test file:
// imports...
import { classInExternalScriptsFile } from "#/scripts/externalScriptsFile.js";
describe("description...", () => {
const mySuperMethodMock = vi
.spyOn(classInExternalScriptsFile.prototype, "mySuperMethod")
.mockImplementation(() => {});
test("That the button performs x when clicked", () => {
let wrapper = mount(myComponent, {
props: ...,
});
let myButton = wrapper.find('[test-id="my-button"]');
myButton.trigger("click");
expect(mySuperMethodMock).toHaveBeenCalled(); // <-- The test fails here
}
}

Calling the vue method from inside an event from VueDraggable

I am trying to get drag and drop function working in the vue.js app using vue-draggable https://vuejsexamples.com/vuejs-drag-and-drop-library-without-any-dependency/
The library has few events you can listen to and I would like to execute some logic once the item is dropped. However, I am not able to access vue component 'this' along with the data and methods. I've tried to use this.$dispatch('symDragged', event); but it is not working for the same reason. 'this' is not a vue instance but rather instance of draggable element.
Here is the code:
export default {
components: {
ICol,
SymptomsChooser, MultiSelectEditor, TempPressureChooser, BodyPartsEditor, MandatorySymptomsChooser},
data() {
return {
// data ommited...
options: {
dropzoneSelector: 'ul',
draggableSelector: 'li',
excludeOlderBrowsers: true,
showDropzoneAreas: true,
multipleDropzonesItemsDraggingEnabled: true,
onDrop(event) {
// delete symptom from old basket and add it to new one
let oldBasket = event.owner.accessKey;
let newBasket = event.droptarget.accessKey;
//this is not working
//this.symDragged(this.draggedSymId, oldBasket, newBasket);
},
onDragstart(event) {
this.draggedSymId = event.items[0].accessKey;
}
}
}
},
methods: {
symDragged(symId, oldBasketId, newBasketId) {
console.log("symDragged!");
let draggedSym = this.getSymById(symId);
let basketOld = this.getBasketById(oldBasketId);
this.delSym(basketOld, draggedSym);
this.addSym({baskedId: newBaskedId, sym: draggedSym});
}
//other methods ommited
}
}
So, what is the correct way to call the vue component method from callback event? Or maybe I need to create another event so that vue instance could listen to it?
Thanks for you help!
The problem you are facing is that with this you are referencing to the returned data object scope and not component scope. The best way to solve this is to make reference to the component instance, so later on you can call anything attached to that instance. You can also take a look at codesandbox example https://codesandbox.io/embed/7kykmmmznq
data() {
const componentInstance = this;
return {
onDrop() {
let oldBasket = event.owner.accessKey;
let newBasket = event.droptarget.accessKey;
let draggedItemsAccessKeys = event.items.map(element => element.accessKey);
componentInstance.symDragged(
draggedItemsAccessKeys,
oldBasket,
newBasket
);
}
}
}

Angular 4+ Function reloading each time the component loads

So here is the function
greenToAmber() {
let x = 0;
setInterval(function () {
x++;
..... Rest of code
}, 500);
}
}
I've set this component up using the routes as you would expect, I've tried calling the function in OnInit as well, but every time I go this component then off it and back again the counter will launch a second instance of the counter & then a third ect for each time I leave & come back to the page.
From my understanding I thought ngOnDestroy was meant to prevent this, I'm assuming that I'll need to subscribe and then unsubscribe to the function maybe on destroy?
But I'm relatively new to angular 4 so pretty inexperienced.
setInterval is not destroyed on component destroy, you have to save the interval id in your class and use clearInterval javascript native function to clean it on your component destroy hook ngOnDestroy:
import {Component, OnDestroy} from '#angular/core';
#Component({ ... })
export class YourClass implements OnDestroy {
public intervalId: any;
public greenToAmber() {
let x = 0;
// registering interval
this.intervalId = setInterval(function () {
// ..... Rest of code
}, 500);
}
}
public ngOnDestroy () {
if (this.intervalId !== undefined) {
clearInterval(this.intervalId); // cleaning interval
}
}
}
Hopes it helps.
You're setting a demon process with setInterval. The behavior you mention is expected. That is how JavaScript works. Its not Angular specific.
SetInterval always returns a ID which you might want to track in your controller. When you want to destroy it, make sure you do it specifically.
eg:
greenToAmber() {
let x = 0;
$scope.myDemon = setInterval(function () {
x++;
..... Rest of code
}, 500);
}
}
//Somewhere else; where you want to destroy the interval/stop the interval:
If($scope.myDemon) {
clearInterval($scope.myDemon);
}

Flux/Alt setTimeout not updating store

I'm trying to create a basic "Toast" like service in my React app using Alt.
I've got most of the logic working, I can add new items to the array which appear on my view when triggering the add(options) action, however I'm trying to also allow a timeout to be sent and remove a toast item after it's up:
onAdd(options) {
this.toasts.push(options);
const key = this.toasts.length - 1;
if (options.timeout) {
options.timeout = window.setTimeout(() => {
this.toasts.splice(key, 1);
}, options.timeout);
}
}
On add, the toast appears on my page, and the timeout also gets triggered (say after a couple of seconds), however manipulating this.toasts inside of this setTimeout does not seem to have any effect.
Obviously this is missing the core functionality, but everything works apart from the setTimeout section.
It seems that the timeout is setting the state internally and is not broadcasting a change event. It might be as simple as calling forceUpdate(). But the pattern I use is to call setState() which is what I think you might want in this case.
Here is an example updating state and broadcasting the change event.
import alt from '../alt'
import React from 'react/addons'
import ToastActions from '../actions/ToastActions'
class ToastStore {
constructor() {
this.toasts = [];
this.bindAction(ToastActions.ADD, this.add);
this.bindAction(ToastActions.REMOVE, this.remove);
}
add(options) {
this.toasts.push(options);
this.setState({toasts: this.toasts});
if (options.timeout) {
// queue the removal of this options
ToastActions.remove.defer(options);
}
}
remove(options) {
const removeOptions = () => {
const toasts = this.toasts.filter(t => t !== options);
this.setState({toasts: toasts});
};
if (options.timeout) {
setTimeout(removeOptions, options.timeout);
} else {
removeOptions();
}
}
}
module.exports = alt.createStore(ToastStore, 'ToastStore');

Typescript derived class as callback signature

I'm trying to make a base class that issues a method for throtteling highly frequented event calls like the document.onscroll event. Here is my base class:
class ThrottledRunner {
private timerId: number;
lastTimeRun: number;
runAtMostEvery = 100;
// Here is the Method
runThrottled(action: (e: ThrottledRunner) => void) {
var now: number = new Date().getTime();
if (this.timerId == null) {
if (now - this.lastTimeRun > (3 * this.runAtMostEvery)) {
action(this);
this.lastTimeRun = now;
}
this.timerId = setTimeout(function (e: ThrottledRunner) {
e.timerId = null;
e.lastTimeRun = new Date().getTime();
action(e);
}, this.runAtMostEvery);
}
}
}
My derived class:
class MyTest extends ThrottledRunner {
myProp: string = "works";
constructor() {
super();
window.addEventListener("scroll", () => this.runThrottled(this.onScroll(this)));
// Supplied parameters do not match any signature of call target.
// Could not select overload for 'call' expression.
}
onScroll(self: MyTest): void {
alert(self.myProp);
}
}
Since MyTest derives from ThrottledRunner, runThrottled() should accept it as a parmeter but it seems i am wrong. I moved completely to Typescript + vanillajs, so no jQuery answers please.
Have you had a look at using underscorejs throttle() function ?
_.throttle(function, wait, [options])
Creates and returns a new, throttled version of the passed function, that, when invoked repeatedly, will only actually call the original function at most once per every wait milliseconds. Useful for rate-limiting events that occur faster than you can keep up with.
Underscore has a number of extremely useful functions, and has full TypeScript and nuGet support : underscore.TypeScript.DefinitelyTyped
You can't call onScroll the way you are as it's calling it immediately upon executing, when it really needs to wait until your runThrottled application is ready. I've changed the onScroll method to not need a parameter as the this context is set correctly.
If you change your class to:
class MyTest extends ThrottledRunner {
myProp: string = "works";
constructor() {
super();
window.addEventListener("scroll",
() => this.runThrottled(() => this.onScroll()));
}
onScroll(): void {
console.log(this.myProp);
}
}
The this will be correct within the context of the runThrottled.

Categories