A simple show and hide on the loading element is failing during the Vue js function execution.
I have a vue js function which will do some things and update the data when it's done. I'm trying to show a loading animation during this process. I've dummied the process down to a simple loop for the purposes of this discussion to eliminate all other issues (i.e ajax, async, etc.).
My HTMl for the button looks like this:
<button type="button" v-on:click="DoStuff()">Do Stuff</button>
My vue js code looks like this:
var client = new Vue({
el: '#client',
data: {
someData: []
},
methods: {
DoStuff: function() {
//Show Loader
$(".loader").show();
//Waste 5 seconds
for (var i = 0; i < 100000000; i++) {
var x = i;
}
//Hide loader
$(".loader").hide();
// let me know it's done.
alert('cool');
}
The loader never shows. If I comment out the hide command, the loader show up AFTER the alert. Makes me think some sort of async operation is going on under the hood but I'm not doing async stuff.
You don't need to use jQuery to set and hide elements on the page based on a condition, in fact that kind of defeats the purpose of what VueJS and other front end javascript frameworks are used for.
First you should add a property called loading to your data object
data: {
someData: [],
loading: false
}
Then, in your doStuff function, remove the jquery lines and set the loading property accordingly
methods: {
doStuff: function() {
//Show Loader
this.loading = true;
//Waste 5 seconds
setTimeout(() => {
this.loading = false;
}, 5000)
}
}
Finally, in your view, add this line
<div v-if="loading">Loading some data</div>
Ill end this by saying that I also think your snippet is a little messed up. the methods property should be inside the Vue instance definition.
var client = new Vue({
el: '#client',
data: {
someData: [],
loading: false
},
methods: {
doStuff: function() {
//Show Loader
this.loading = true;
//Waste 5 seconds
setTimeout(() => {
this.loading = false;
}, 5000)
}
}
}
Don't use jquery. You can so this with vuejs.
var client = new Vue({
el: '#client',
data: {
someData: [],
loading: false,
},
methods: {
DoStuff() {
//Show Loader
this.loading = true;
//Waste 5 seconds
//Hide loader
this.loading = true;
// let me know it's done.
alert('cool');
}
And your HTML.
<div class="loading" v-if="loading">
Loading....
</div>
<div v-else>
The rest of your website
</div>
It's better to not mix with JQuery, just use Vue conditional displaying instead (v-show).
In your loader:
<div class="loader" v-show="loading"></div>
In your Vue code:
var client = new Vue({
el: '#client',
data: {
someData: [],
loading: false
},
methods: {
DoStuff: function() {
this.loading = true;
//Waste 5 seconds
for (var i = 0; i < 100000000; i++) {
var x = i;
}
this.loading = false;
// let me know it's done.
alert('cool');
}
Thanks Everyone. I have to focus on a more important feature that came up. But I just wanted to share what I've gleamed. This is not a (or the) final answer but it explains the problem and the correct approach.
The problem was correctly identified by Andrew Lohr as is further explained here:
jQuery hide() and show() not immediately run when reversed later in the function
The best (but not only) solution would be use an event bus as mentioned by perellorodrigo here: https://flaviocopes.com/vue-components-communication/
I will post my final solution when I get around to it.
Related
I would like to force the UI to update midway through an event loop cycle.
Vue.nextTick
Vue.nextTick seems to provide you with an updated version of vm.$el, but doesn't actually cause the UI to update.
CodePen: https://codepen.io/adamzerner/pen/RMexgJ?editors=1010
HTML:
<div id="example">
<p>Value: {{ message }}</p>
<button v-on:click="change()">Change</button>
</div>
JS:
var vm = new Vue({
el: '#example',
data: {
message: 'A'
},
methods: {
change: change
}
})
function change () {
vm.message = 'B';
// vm.$el.children[0].textContent === "Value: A"
Vue.nextTick(function () {
// vm.$el.children[0].textContent === "Value: B"
// but the UI hasn't actually updated
for (var i = 0; i < 10000000; i++) {}
vm.message = 'C';
});
}
vm.$forceUpdate
vm.$forceUpdate doesn't appear to do anything at all.
It doesn't appear to change the value of vm.$el.
It doesn't appear to update the UI.
CodePen: https://codepen.io/adamzerner/pen/rdqpJW?editors=1010
HTML:
<div id="example">
<p>Value: {{ message }}</p>
<button v-on:click="change()">Change</button>
</div>
JS:
var vm = new Vue({
el: '#example',
data: {
message: 'A'
},
methods: {
change: change
}
})
function change () {
vm.message = 'B';
// vm.$el.children[0].textContent === "Value: A"
vm.$forceUpdate();
// vm.$el.children[0].textContent === "Value: A" still
// and the UI hasn't actually updated
for (var i = 0; i < 10000000; i++) {}
vm.message = 'C';
}
v-bind:key
v-bind:key also doesn't appear to do anything at all:
It doesn't appear to change the value of vm.$el.
It doesn't appear to update the UI.
Codepen: https://codepen.io/adamzerner/pen/WzadKN?editors=1010
HTML:
<div id="example">
<p v-bind:key="message">Value: {{ message }}</p>
<button v-on:click="change()">Change</button>
</div>
JS:
var vm = new Vue({
el: '#example',
data: {
message: 'A'
},
methods: {
change: change
}
})
function change () {
// vm.$el.children[0].textContent === "Value: A"
vm.message = 'B';
// vm.$el.children[0].textContent === "Value: A" still
// and the UI hasn't actually updated
for (var i = 0; i < 10000000; i++) {}
vm.message = 'C';
}
computed
Using a computed property, as this popular answer recommends, also doesn't appear to do anything:
It doesn't appear to change the value of vm.$el.
It doesn't appear to update the UI.
CodePen: https://codepen.io/adamzerner/pen/EEdoeX?editors=1010
HTML:
<div id="example">
<p>Value: {{ computedMessage }}</p>
<button v-on:click="change()">Change</button>
</div>
JS:
var vm = new Vue({
el: '#example',
data: {
message: 'A'
},
computed: {
computedMessage: function () {
return this.message;
},
},
methods: {
change: change
}
})
function change () {
// vm.$el.children[0].textContent === "Value: A"
vm.message = 'B';
// vm.$el.children[0].textContent === "Value: A" still
// and the UI hasn't actually updated
for (var i = 0; i < 10000000; i++) {}
vm.message = 'C';
}
Promise (added in edit)
Using promises doesn't work either.
CodePen: https://codepen.io/adamzerner/pen/oqaEpV?editors=1010
HTML:
<div id="example">
<p>Value: {{ message }}</p>
<button v-on:click="change()">Change</button>
</div>
JS:
var vm = new Vue({
el: '#example',
data: {
message: 'A'
},
methods: {
change: change
}
})
function change () {
// vm.$el.children[0].textContent === "Value: A"
vm.message = 'B';
// vm.$el.children[0].textContent === "Value: A" still
// and the UI hasn't actually updated
var promise = new Promise(function (resolve, reject) {
for (var i = 0; i < 10000000; i++) {}
resolve();
});
promise.then(function () {
vm.message = 'C';
});
}
setTimeout
setTimeout is the only thing that seems to work. But it only works consistently when the delay is 100. When the delay is 0, it works sometimes, but doesn't work consistently.
vm.$el updates.
The UI updates.
CodePen: https://codepen.io/adamzerner/pen/PRyExg?editors=1010
HTML:
<div id="example">
<p>Value: {{ message }}</p>
<button v-on:click="change()">Change</button>
</div>
JS:
var vm = new Vue({
el: '#example',
data: {
message: 'A'
},
methods: {
change: change
}
})
function change () {
// vm.$el.children[0].textContent === "Value: A"
vm.message = 'B';
setTimeout(function () {
// vm.$el.children[0].textContent === "Value: B"
// the UI has updated
for (var i = 0; i < 10000000; i++) {}
vm.message = 'C';
}, 100);
}
Questions
Why don't Vue.nextTick, vm.$forceUpdate, v-bind:key, or computed properties work?
Why does setTimeout work inconsistently when the delay is 0?
setTimeout seems hacky. Is there a "propper" way to force a UI update?
Synopsis
The illusion of B not being updated/displayed in the UI is caused by a combination of Vue's Async Update Queue and JavaScript's Event Loop Process model. For details and proof read on.
#Summary of Findings#
These actually do what you want (but don't seem to)
Vue.nextTick
setTimeout - (but doesn't seem to with short timeout)
These work as expected (but require explanation)
v-bind:key
vm.$forceUpdate
Promise
Note: The but doesn't seem to above is an acknowledgment that Vue is doing what it is supposed to but the expected visual output does not appear. Therefore, the code doesn't produce the expected output is accurate.
Discussion
First Two Work
Proving the first two do what you want is quite easy. The idea of 'B' not being placed in the view will be disproved. But further discussion is required to address the lack of visible change.
Open each of the Pens in Chrome
In dev tools, set a break point in vue.js on line 1789
Step through the sequence
While you step through the sequence you will notice the UI is updated with the value 'B' as it should (regardless of length of timeout). Dispelled.
So what about the lack of visibility? This is caused by JavaScript's Event Loop process model and is specifically related to a principle called Run-to-Completion. The MDN Event Loop Documentation states:
A downside of this model is that if a message takes too long to
complete, the web application is unable to process user interactions
like click or scroll.
or run the render/paint browser processes. So when the stack is executed, B is rendered then C immediately thereafter, which seems like B is never rendered. One can see this exact problem when using an animated GIF with a JavaScript heavy task, such as bootstrapping a SPA. The animated GIF either will stutter or will not animate at all - the Run-to-Completion is in the way.
So Vue does what it is supposed to and JavaScript does what it is supposed to correctly. But the long running loop is troublesome. This is the reason tools like lodash _debounce or simple setTimout are helpful.
Last Three Work?
Yes. Using the same breakpoint in vue.js will show the only break happens when Vue is flushing its queue of updates. As discussed in Vue's documentation about Async Update Queue each update is queued and only the last update for each property is rendered. So although message is actually changed to B during processing, it is never rendered because of the way the Vue Async Queue works:
In case you haven’t noticed yet, Vue performs DOM updates
asynchronously. Whenever a data change is observed, it will open a
queue and buffer all the data changes that happen in the same event
loop.
I'm not sure what you want but try this...
var vm = new Vue({
el: '#example',
data: function(){
return {
message: 'A'
}
},
methods: {
change: function(){
this.message = 'B';
let el = this;
setTimeout(()=>{
el.$nextTick(function () {
el.message = 'C';
})
},2000)
}
}
})
I would look at using Vue Instance Lifecycle Hooks.
Take a look at this Vue Instance Lifecycle Diagram as well.
Tying in with a Lifecycle Hook should give you the ability to disrupt something midway if you need to update it. Maybe do some sort of check with the beforeMount hook.
Imagine that we have a Sequence widget which loads Element widgets and loads some config for each of them (through loadConfig()) .Schematic view is described on the image below:
The problem is that every approach I tried does the same "bad" thing: it freezes everything until all Elements are loaded.
array.forEach(elements, function(element) {
element.loadConfig();
});
or
var counter = 0;
var loadConfig = function(element) {
element.loadConfig()
if (++counter <= elements.length - 1) {
loadConfig(elements(counter));
}
};
loadConfig(0);
Is there any way to load and show elements one by one, instead of trying to load everything at once? JavaScript doesn't have multi-threading.. so I am really running out of ideas.
EDIT: Tried to use setTimeout() as suggested in this great answer Why is setTimeout(fn, 0) sometimes useful?
But this doesn't solve the problem. Looks like Dojo has something that prevents widgets to be rendered one by one - it loads while all data is loaded and then load them all together.
EDIT2: This is how setTimeout() was tried to be used:
array.forEach(elements, function(element) {
setTimeout(function() {
element.loadConfig();
}, 0);
});
EDIT3: Here is full code what is going on. The hierarchy is: TestSequence > Test > ElementsGroup > Element.
// Inside TestSequence widget:
...
var config = [someConfig1, someConfig2, ... someCondigN]
array.forEach(sequenceConfig, function(sequenceConfigItem, i) {
require(['some_path/' + sequenceConfig.type], function(cls) {
var test = new cls();
test.set('config', sequenceConfigItem);
});
}, this);
...
// Inside Test widget
...
_setConfigAttr: function(testConfig) {
elementsGroup.set('config', testConfig);
},
...
// Inside ElementsGroup widget
...
_setConfigAttr: function(elementsGroupConfig) {
array.forEach(config, function(elemenstGroupConfigItem, i) {
require(['some_path/' + elementsGroupConfigItem.type], function(cls) {
var element = new cls(); // <--- removing this solves the problem
element.set('config', elementsGroupConfigItem);
});
}, this);
},
...
// Inside Element widget
...
_setConfigAttr: function(testConfig) {
// apply simple DOM-stuff - set name, description, etc.
},
...
The solution was to use setTimeout along with recursive function, like this:
var counter = 0;
recursiveFunc: function(element) {
setTimeout(function() {
// Do stuff for element[counter]
if (++counter < numberOfElements) {
recursiveFunc(element);
}
}, 0);
}
recursiveFunc(element[counter])
The UI thread would still be buggy, but at least it is not frozen. This solution was picked as a fast one. For a long-one it was decided to optimize the code to get rid of sync XHR request.
I am trying to create a SoundCloud music player. It can play any track from SoundCloud, but this plugin is only working if there is only one instance of it in the page. So it wont work if two of the same plugin are in the page.
Here is an example of having two players in the page: JSFiddle
var trackURL = $(".player").text();
$(".player").empty();
$(".player").append("<div class='playBTN'>Play</div>");
$(".player").append("<div class='title'></div>");
var trackId;
SC.get('/resolve', { url: trackURL }, function (track) {
var trackId = track.id;
//var trackTitle = track.title;
$(".title").text(track.title);
$(".playBTN").on('click tap', function () {
//trackId = $(".DSPlayer").attr('id');
stream(trackId);
});
// first do async action
SC.stream("/tracks/" + trackId, {
useHTML5Audio: true,
preferFlash: false
}, function (goz) {
soundToPlay = goz;
sound = soundToPlay;
scTrack = sound;
//updater = setInterval( updatePosition, 100);
});
});
var is_playing = false,
sound;
function stream(trackId) {
scTrack = sound;
if (sound) {
if (is_playing) {
sound.pause();
is_playing = false;
$(".playBTN").text("Play");
} else {
sound.play();
is_playing = true;
$(".playBTN").text("Pause");
}
} else {
is_playing = true;
}
}
If you remove any of these div elements that hold the .player class, the other element will work. So it only doesn't work because there are two instances of the same plugin.
How can I fix it? to have multiple instances of the player in one page?
I have identified the problem. It has to do with the fact that you are trying to load multiple tracks at the same time, but have not separated the code to do so.
As #Greener mentioned you need to iterate over the .player instances separately and execute a SC.get() for each one of them.
Here is what I see happening that is causing the problem:
var trackURL = $(".player").text();
^The code above returns a string that contains both of the URLs you want to use back-to-back without spaces. This creates a problem down the road because of this code:
SC.get('/resolve', { url: trackURL }, function (track) {...
That is a function that is trying to load the relevant song from SoundCloud. You are passing it a variable "trackURL" for it to try and load a specific URL. The function gets a string that looks like "URLURL" what it needs is just "URL".
What you can do is iterate over all the different ".player" elements that exist and then call the sounds that way. I modified your script a little to make it work using a for loop. I had to move the "empty()" functions into the for loop to make it work correctly. You have to use .eq(index) when referring to JQuery array of elements.
Like this:
var trackURL
var trackId;
for(index = 0; index < $(".player").length; index++){
trackURL = $(".player").eq(index).text();
//alert(trackURL);
$(".player").eq(index).empty();
$(".player").eq(index).append("<div class='playBTN'>Play</div>");
$(".player").eq(index).append("<div class='title'></div>");
SC.get('/resolve', { url: trackURL }, function (track) {
var trackId = track.id;
alert(track.id);
//var trackTitle = track.title;
$(".title").eq(index).text(track.title);
$(".playBTN").eq(index).on('click tap', function () {
//trackId = $(".DSPlayer").attr('id');
stream(trackId);
});
// first do async action
SC.stream("/tracks/" + trackId, {
useHTML5Audio: true,
preferFlash: false
}, function (goz) {
soundToPlay = goz;
sound = soundToPlay;
scTrack = sound;
//updater = setInterval( updatePosition, 100);
});
});
}
This is not a completely finished code here, but it will initiate two separate songs "ready" for streaming. I checked using the commented out alert what IDs SoundCloud was giving us (which shows that its loaded now). You are doing some interesting stuff with your streaming function and with the play and pause. This should give you a good idea on what was happening and you can implement your custom code that way.
I've a task of building a modal prompt, that's been simple so far describing its methods like "show", "hide" when it comes down just to DOM manupulation.
Now comes the hardship for me... Imagine we have a page on which there are several immediate calls to construct and show several modals on one page
//on page load:
$("browser-deprecated-modal").modal();
$("change-your-city-modal").modal();
$("promotion-modal").modal();
By default my Modal (and other libraries i tried) construct all of these modals at once and show them overlapping each other in reverse order -
i.e $(promotion-modal) is on the top, while the
$("browser-deprecated-modal") will be below all of them. that's not what i want, let alone overlapping overlays.
I need each modal to show up only when the previous one (if there'are any) has been closed. So, first we should see "browser-deprecated-modal" (no other modals underneath), upon closing it there must pop up the second one and so on.
I've been trying to work it out with this:
$.fn.modal = function(options) {
return this.each(function() {
if (Modal.running) {
Modal.toInstantiateLater.push({this,options});
} else {
var md = new Modal(this, options);
}
});
}
destroy :function () {
....
if (Modal.toInstantiateLater.length)
new Modal (Modal.toInstantiateLater[0][0],Modal.toInstantiateLater[0][1]);
}
keeping a track of all calls to construct a Modal in a array and in the "destroy" method make a check of this array is not empty.
but it seems awkward and buggy me thinks.
i need a robust and clear solution. I've been thinking about $.Callbacks or $.Deferred,
kinda set up a Callback queue
if (Modal.running) { //if one Modal is already running
var cb = $.Callbacks();
cb.add(function(){
new Modal(this, options);
});
} else { //the road is clear
var md = new Modal(this, options);
}
and to trigger firing cb in the destroy method, but i'm new to this stuff and stuck and cannot progress, whether it's right or not, or other approach is more suitable.
Besides, I read that callbacks fire all the functions at once (if we had more than one extra modal in a queue), which is not right, because I need to fire Modal creation one by one and clear the Callback queue one by one.
Please help me in this mess.
My code jsfiddle
I got rid of the counter variable, as you can use toInstantiateLater to keep track of where you are, and only had to make a few changes. Give this a try...
Javscript
function Modal(el, opts){
this.el = $(el);
this.opts = opts;
this.overlay = $("<div class='overlay' id='overlay"+Modal.counter+"'></div>");
this.wrap = $("<div class='wrap' id='wrap"+Modal.counter+"'></div>");
this.replace = $("<div class='replace' id='replace"+Modal.counter+"'></div>");
this.close = $("<span class='close' id='close"+Modal.counter+"'></span>")
if (Modal.running) {
Modal.toInstantiateLater.push(this);
}
else {
Modal.running = true;
this.show();
}
}
Modal.destroyAll = function() {
Modal.prototype.destroyAll();
};
Modal.prototype = {
show: function() {
var s = this;
s.wrap.append(s.close);
s.el.before(s.replace).appendTo(s.wrap).show();
$('body').append(s.overlay).append(s.wrap);
s.bindEvents();
Modal.currentModal = s;
},
bindEvents: function() {
var s = this;
s.close.on("click.modal",function(e){
s.destroy.call(s,e);
});
},
destroy: function(e) {
var s = this;
s.replace.replaceWith(s.el.hide());
s.wrap.remove();
s.overlay.remove();
if (Modal.toInstantiateLater.length > 0) {
Modal.toInstantiateLater.shift().show();
}
else {
Modal.running = false;
}
},
destroyAll: function(e) {
Modal.toInstantiateLater = [];
Modal.currentModal.destroy();
}
}
Modal.running = false;
Modal.toInstantiateLater = [];
Modal.currentModal = {};
$.fn.modal = function(options) {
return this.each(function() {
var md = new Modal(this, options);
});
}
$("document").ready(function(){
$("#browser-deprecated-modal").modal();
$("#change-your-city-modal").modal();
$("#promotion-modal").modal();
$("#destroy-all").on("click", function() {
Modal.destroyAll();
});
});
jsfiddle example
http://jsfiddle.net/zz9ccbLn/4/
I've got a file which needs to run on page load (randomise_colors.js), but also needs to be called by another file as part of a callback function (in infinite_scroll.js). The randomise_colors script just loops through a list of posts on the page and assigns each one a color from an array which is used on the front-end.
Infinite Scroll loads new posts in to the DOM on a button click, but because the randomise_colors.js file has already ran on page load, new content loaded is not affected by this so I need it to run again. I'm open to other suggestions if it sounds like I could be tackling the problem in a different way, I'm no JS expert.
Currently I'm getting Uncaught ReferenceError: randomise_colours is not defined referring this line of infinite_scroll.js:
randomise_colours.init();
I'm calling all files that need be loaded on document.ready in app.js
require(['base/randomise-colours', 'base/infinite-scroll'],
function(randomise_colours, infinite_scroll) {
var $ = jQuery;
$(document).ready(function() {
infinite_scroll.init();
randomise_colours.init();
});
}
);
This is infinite_scroll.js which initialises Infinite Scroll and features the callback. The callback function runs whenever new items are loaded in via AJAX using the Infinite Scroll jQuery plugin. I've put asterix around the area where I need to run the randomise_colors.init() function from randomise_colors.js.
define(['infinitescroll'], function() {
var $ = jQuery,
$loadMore = $('.load-more-posts a');
function addClasses() {
**randomise_colours.init();**
};
return {
init: function() {
if($loadMore.length >= 1) {
this.setUp();
} else {
return false;
}
},
setUp: function() {
this.initInfiniteScroll();
},
initInfiniteScroll: function() {
$('.article-listing').infinitescroll({
navSelector : '.load-more-posts',
nextSelector : '.load-more-posts a',
itemSelector : '.standard-post'
}, function(newItems) {
addClasses();
});
//Unbind the standard scroll-load function
$(window).unbind('.infscr');
//Click handler to retrieve new posts
$loadMore.on('click', function() {
$('.article-listing').infinitescroll('retrieve');
return false;
});
}
};
});
And this is my randomise_colors.js file which runs fine on load, but needs to be re-called again after new content has loaded in.
define([], function() {
var $ = jQuery,
$colouredSlide = $('.image-overlay'),
colours = ['#e4cba3', '#867d75', '#e1ecb9', '#f5f08a'],
used = [];
function pickRandomColour() {
if(colours.length == 0) {
colours.push.apply(colours, used);
used = [];
}
var selected = colours[Math.floor(Math.random() * colours.length)];
var getSelectedIndex = colours.indexOf(selected);
colours.splice(getSelectedIndex, 1);
used.push(selected);
return selected;
};
return {
init: function() {
if($colouredSlide.length >= 1) {
this.setUp();
} else {
return false;
}
},
setUp: function() {
this.randomiseColours();
},
randomiseColours: function() {
console.log('randomise');
$colouredSlide.each(function() {
var newColour = pickRandomColour();
$(this).css('background', newColour);
});
}
};
});
You would have to reference randomiseColours inside the infiniteScroll file. So you need to change your define function to the following:
define(['infinitescroll', 'randomise-colours'], function(infiniteScroll, randomise_colours)
Remember that when using require you need to reference all variables through the define function, otherwise they will not be recognised.