Global (window) variable is behaving strangely in Emberjs - javascript

I am using Ember-cli in my web app. I have a countdown component to show a countdown timer on UI. Here is my component code.
export default Ember.Component.extend({
end_time: 0,
start_time: 0,
some_id: 0,
timer: 0, // Show this in UI - {{timer}} Seconds
init: function() {
this._super();
let end_time = this.get("end_time"), // 1479476467693
start_time = this.get("start_time"), // 1479476381491
some_id = this.get("some_id");
let wait_time = Math.floor((end_time - start_time)/1000);
this.set('timer', wait_time);
let timerName = "timer_" + some_id;
let _self = this;
window.initTimer.someTimer[timerName] = setInterval(function() {
_self.set('timer', wait_time);
if(wait_time <= 0) {
clearInterval(window.initTimer.someTimer[timerName]);
}
wait_time --;
}, 1000);
}
});
This component works fine, if I add this to a single route.
Now, I have added this component to both parent route and child (/:ID) route, since I need to show the component on both templates. In the child (/:ID) template, I have a button to clear the timer. So I have added this code for that button action.
buttonAction: function(some_id) {
let timerName = "timer_" + some_id;
clearInterval(window.initTimer.someTimer[timerName]);
}
Strangely, when the buttonAction is called, the timer on the child template alone is cleared. The timer on parent template keeps running. But both the timer are assigned to a single global variable (window.initTimer.someTimer) and should be cleared when I run clearInterval().
Is there any solution for clearing the timer on both parent route and child route on click of a button, which resides on child template? Couldn't figure out what magic Ember is playing with global variables!!

Ember is doing no magic here, but your code is much to complicated!
The interesting question is from where some_id comes. If its not the same for both then with which one are you calling buttonAction?
Assume you have the ids one and two. Then you have the two intervals at window.initTimer.someTimer.timer_one and window.initTimer.someTimer.timer_two. Now if you clear window.initTimer.someTimer.timer_one why should window.initTimer.someTimer.timer_two be cleared as well? Well, its not, and thats why your code is not working.
Assume you only have one id, lets call is theOnlyOne for both timers.
Then the init hook of the second component will reassign window.initTimer.someTimer.timer_theOnlyOne, and so only this second component can be resetted when you call buttonAction. Then thats why your code is not working.
So what should you do now? First, you really should stop using the global object! There are so much better ways to do this in the ember ecosystem.
For your timers you should check out ember-concurrency.
If you want to use a global state you should use a service. However I don't recommend this for your problem, because it's against the DDAU principle. However to tell you whats the right way to do what you want to do we need to understand why you have this timers, how they are related and why you want to cancel them both with one click. Probably you would have some timer state outside of the components and pass it down to the components. Maybe a util can be helpful. But this really depends on your use-case.

Related

Insert Vue.js JSON object value directly into a Javascript setInterval function

How can I input a Vue.js object for the current item into another Javascript function on the page?
I have a slide HTML page that only displays the current HTML ID
Each slide I have has a time in seconds pulled in from my JSON, the timer object. I need to put this number in seconds into a SetInterval function to load programmatically the next slide.
var seconds = parseInt(document.querySelector("#value").innerText) * 1000;
setInterval(function () {
document.querySelector("a.o-controls2").click();
}, seconds)
You can see the example in action here.
I have tried loading the expression value but get a null result in the console. So perhaps this has something to do with the page load and how Vue renders the page?
I have also tried the following, by loading in the Vue object but this didn't work either:
var seconds = (this.timer - 1) 1000;
setInterval(function () { document.querySelector("a.o-controls2").click();
}, seconds)
the easiest way (IMHO) to access the data after some change (in this case, after fetch) is to use $emit and $on
this.items = items.reverse();
this.$nextTick(() => this.$emit('dataLoaded', this.items));
you could also use it without the nextTick, but then you'd get the data back before the dom is updated, so your js would fail.
this.items = items.reverse();
this.$emit('dataLoaded', this.items);
then listen to the change using...
app.$on('dataLoaded', items => {
console.log('items', items)
document.querySelector("a.o-controls2"); // now accessible
// do stuff
})
here is a fiddle: https://jsfiddle.net/y32hnzug/1/
it doesn't implement the click timing though since that's outside the scope of the question
The null response is the return from querySelector(); it's returning null because the external script is running before Vue has rendered the application. When the script runs, there is no element with an id of "value" in the document.
The easiest fix might be to move that code into the mounted() lifecycle hook of the app; possibly within a nextTick() to make sure that all child components have also rendered.

Vuejs watcher order

I have a Vue instance with two watchers:
watch: {
zone:function(zone) {
console.log('Zone watcher');
this.route = {};
},
route:function(route) {
console.log('Route watcher');
if(Object.getOwnPropertyNames(route).length === 0) {
var _this = this;
axios.get(route.url).then(function(response) {
_this.tracks = response.data;
});
} else this.tracks = {};
}
},
When a user selects a zone, route (and tracks) are reset. When user selects a route, tracks are loaded;
I have a component receiving zone and tracks as props, also with two internal watchers that perform some independent actions when any of this props change.
I also have a method that changes both variables:
jump:function(path) {
var parts = path.split(',');
this.zone = this.ZONES[parts[0]];
this.route = this.zone.routes[parts[1]];
},
The problem is watcher for route is fired in first place, then the watcher for zone changes route value triggering its watcher again, reseting tracks value to an empty object.
Is there a way to define the order that watchers must be triggered?
Andrey's comment shows the way. This question comes down to which tools you use for what job. Inevitably there's a bit of opinion... watch is for edge cases. You don't need it often, and if you can do without it, you probably should. watch belongs with computed and v-bind: they're reactive, use them (only) for representing state on screen, you have no (or little) control over when they run, and you shouldn't care.
A server request belongs in a method, or in a function outside of Vue (in your store perhaps) where it can be called explicitly. So create yourself a changeZone() function that clears routes and tracks, then calls the server, then updates your data (or store) with the server response. Your code will be much clearer if these little functional sequences are specified explicitly, in one place. The trigger for your sequence should likely be from an event (user-action) or lifecyle hook, not a watch.

How to fit these two classes together?

This is my first attempt at doing OOP and made a simple Pomodoro timer. https://codepen.io/hyrosian/project/editor/XpjOPR
There was an attempt to seperate the countdown timer from the controls. The countdown timer is working but i'm not sure how to get class Counter and class Controls somehow 'fit' together.
const counter = new Counter(DOMnode)
const controls = new Controls()
The plan was to set up the eventlisteners and handlers inside Controls. We get the values e.g. +5mins for changing the session/break length inside of it. But Controls needs access to the state inside Counter.
class Controls extends Time {
constructor() {
super()
this.session_btn = document.querySelectorAll('[data-session]')
this.break_btn = document.querySelectorAll('[data-break]')
}
handleBreak(e) {
console.log(e.target.dataset.break)
}
handleSession(e) {
console.log(e.target.dataset.session)
}
init() {
this.session_btn.forEach(btn => btn.addEventListener('click', this.handleSession))
this.break_btn.forEach(btn => btn.addEventListener('click', this.handleBreak))
}
}
What would be the proper way to fit them together?
Controls should have a reference to the Counter object, but not the other way around.
Controls can access the state by asking for (getTime, getState). If Controls needs to subscribe to some event (timer has run out) then Counter could emit such an event.
The underlying idea would be that Counter is a completely isolated object and just maintains its own state and timers, but Controls is the thing that handles events from the user (and potentially updates the DOM when needed).
This is a pretty common pattern. In this scenario, Counter would often be called the model, and Controls the Controller.
You should first create the Counter, and pass it as a constructor argument to Controls

Incrementing the Material Design Lite Progress bar with React

I've got MDL running with React at the moment and it seems to be working fine at the moment.
I've got the Progress Bar appearing on the page as needed and it loads up with the specified 'progress' on page load when either entering in a number directly:
document.querySelector('#questionnaireProgressBar').addEventListener('mdl-componentupgraded', function() {
this.MaterialProgress.setProgress(10);
})
or when passing in a number via a Variable:
document.querySelector('#questionnaireProgressBar').addEventListener('mdl-componentupgraded', function() {
this.MaterialProgress.setProgress(value);
})
It stops working after this though. I try to update the value via the Variable and it doesn't update. I've been advised to use this:
document.querySelector('.mdl-js-progress').MaterialProgress.setProgress(45);
to update the value but it doesn't work. Even when trying it directly in the console.
When trying via the Console I get the following Error:
Uncaught TypeError: document.querySelector(...).MaterialProgress.setProgress is not a function(…)
When I try to increment the value via the Variable I get no errors and when I console.log(value) I am presented the correct number (1,2,3,4...) after each click event that fires the function (it fires when an answer is chosen in a questionnaire)
What I want to know is if there's something obvious that I'm missing when using MTL and React to make components to work? There was an issue with scope but I seem to have it fixed with the following:
updateProgressBar: function(value) {
// fixes scope in side function below
var _this = this;
document.querySelector('#questionnaireProgressBar').addEventListener('mdl-componentupgraded', function() {
this.MaterialProgress.setProgress(value);
})
},
In React I've got the parent feeding the child with the data via props and I'm using "componentWillReceiveProps" to call the function that updates the progress bar.
I've used the "componentDidMount" function too to see if it makes a difference but it still only works on page load. From what I've read, it seems that I should be using "componentWillReceiveProps" over "componentDidMount".
It's being fed from the parent due to components sending data between each other. I've used their doc's and some internet help to correctly update the parent function to then update the progress bar in the separate component.
updateProgressBarTotal: function(questionsAnsweredTotal) {
this.props.updateProgressBarValue(questionsAnsweredTotal);
}
The parent function looks like the following (I think this may be the culprit):
// this is passed down to the Questions component
updateProgressBarTotal: function(questionsAnsweredTotal) {
this.setState({
progressBarQuestionsAnswered : questionsAnsweredTotal
})
}
I can post up some more of the code if needed.
Thank you
Looks I needed a fresh set of eyes on this.
I moved the function to the child of the parent. It seems that using document.querySelector... when in the parent doesn't find the element but when it's moved to the child where I do all the question logic it seems to be fine. It increments the progress correctly etc now :)
// goes to Questionnaire.jsx (parent) to update the props
updateProgressBarTotal: function(questionsAnsweredTotal) {
// updates the state in the parent props
this.props.updateProgressBarValue(questionsAnsweredTotal);
// update the progress bar with Value from questionsAnsweredTotal
document.querySelector('.mdl-js-progress').MaterialProgress.setProgress(questionsAnsweredTotal);
},
I had same problem in angular2 application.
You don't necessary need to move to the child component.
I found after struggling to find a reasonable fix that you simply have to be sure mdl-componentupgradedevent already occurred before being able to use MaterialProgress.setProgress(VALUE). Then it can be updated with dynamic value.
That is why moving to the child works. In the parent component mdl-componentupgraded event had time to occur before you update progress value
My solution for angular2 in this article
Adapted in a React JS application :
in componentDidMount, place a flag mdlProgressInitDone (initiated to false) in mdl-componentupgraded callback :
// this.ProgBar/nativeElement
// is angular2 = document.querySelector('.mdl-js-progress')
var self = this;
this.ProgBar.nativeElement.addEventListener('mdl-componentupgraded', function() {
this.MaterialProgress.setProgress(0);
self.mdlProgressInitDone = true; //flag to keep in state for exemple
});
Then in componentWillReceiveProps test the flag before trying to update progress value :
this.mdlProgressInitDone ? this.updateProgress() : false;
updateProgress() {
this.ProgBar.nativeElement.MaterialProgress.setProgress(this.currentProgress);
}
After attaching the progress bar to the document, execute:
function updateProgress(id) {
var e = document.querySelector(id);
componentHandler.upgradeElement(e);
e.MaterialProgress.setProgress(10);
}
updateProgress('#questionnaireProgressBar');

Test that a click event in React updates the HTML

I've set up some tests of a React component that displays a table using Mocha. I can assert on its initial state but I have a click event which sorts the data that I'd like to test.
If I use React.addons.TestUtils.Simulate.click(theComponent) to try to test the sort.
I can see that the event is handled,
that the state change is fired
the data is sorted before calling setState
but when I assert against the component nothing has changed.
it('sorts the data when the year header is clicked', function() {
var React = require('react/addons');
var TestUtils = React.addons.TestUtils;
var payTable = TestUtils.renderIntoDocument(
<PayTable payYears={data} />
);
var headers = TestUtils.scryRenderedDOMComponentsWithTag(payTable, 'th');
var yearHeader = headers[0];
TestUtils.Simulate.click(yearHeader.getDOMNode());
var columnValues = getYearColumnValues(payTable, TestUtils);
columnValues.should.match([ 'Year', '1066', '1067', '1068' ]);
});
Do I need to force an update? Re-read the component?
The code is available on Github.
I can test other aspects of the Component but not the Component values after setState
I had the same issue. The thing is, TestUtils.Simulate.click(yearHeader.getDOMNode()) is making a click on the DOM, which brings about sorting data and DOM manipulation. Even if you use jsDom and not a real dom, this manipulation itself is an async event. So, just by checking the DOM state right after the click event you are making a syncronous call, within which the DOM state has not been changed yet.
The solution is, use a setTimeout with 0 milliseconds timeout, and check the DOM state there. As for you example :
setTimeout(function() {
var columnValues = getYearColumnValues(payTable, TestUtils);
columnValues.should.match([ 'Year', '1066', '1067', '1068' ]);
},0)
This would make your DOM update its state and you will be able to test your component.
I've been spending a lot of time trying to find a clean way to work with the asynchronousity... Ended up making this for testing:
https://github.com/yormi/test-them-all
Under the hood, it uses componentDidUpdate lifecycle to listen on props/state/route changes.
Hopefully, it'll help you guys. Anyhow your opinion would be greatly appreciated :)

Categories

Resources