Vue component method behaving weirdly - loader animation not working while data is processing - vue.js

I'm trying to figure out why this happens - loader for long processing of data does not show.. only after the processing is done. Huge (few thousands items) object of key-value items and want them to make filterable - that works - but takes few seconds. I'm using VueJS 2.
I wanted to show "please wait" message while it runs, using the isworking value. I have a span with v-if="isworking", defined with value false as initial value.
On first line I set the this.isworking prop, but instead of seeing the "please wait", the function hangs for few seconds to do a search, and THEN sets the isworking prop to true - I tried that by commenting the last isworking=false - can't figure out why it waits to change it to true for the huge processing to end.
That window.deaccent method is fn to replace all accented characters in string with basic ansii chars, nothing special.
In template, I have a simple:
<form #submit="searchmath">
<span v-if="working">please wait</span>
<input v-model=...>
<div v-for="(item,index) in searchmatchitems"> ... </div>
</form>
Method in component:
searchmatch: function($event){
this.isworking = true;
this.$forceUpdate(); // tried also this, does not help
$event.stopPropagation();$event.preventDefault();
try{
var searchid = window.deaccent(this.search_string.toLowerCase());
var searchobj = this.cdata;
let result = Object.keys(this.cdata).filter(function(el,i,c){
var elk = window.deaccent(el).toLowerCase();
var elv = window.deaccent(searchobj[el]);
return elk.indexOf(searchid) > -1 || elv.indexOf(searchid) > -1;
}, searchid);
this.searchmatchitems = result;
this.isworking = false;
} catch(e){ console.log(e); this.isworking = false; return [];}
}
I also tried moving the event.preventDefault() to bottom, just to be sure it does not affect anything, but no luck.
That cdata is a simple key-value object with many props like this, counting about 4000 items
data: {
cdata: {
"lorem": "aa",
"ipsum": "bc",
"dolor": "de",
....
},
isworking: false,
....
}

2 issues here.
First, you use working in your template, and isworking in your data and code.
Second, your method does not call any async code, so it sets isworking to true, does work, and then sets isworking to false. You template will never have a chance to refresh in this case, and the UI will freeze until the method returns.
For instance, if you made an async call to a network endpoint, and then set isworking to false in the callback, you would get the results you are expecting.
If you have long running code and wish to prevent the UI from freezing, you will need to use a web worker thread

Related

For each results in v-for loop how can I nest another v-for loop using a parameter from the results of the first loop

Using a v-for loop in Vue js. I am looping through the readingTasks data object which correctly produces two results from the data below.
readingTasks:Array[2]
0:Object
enabled:true
newunit:-1
task:"The part 3 guide"
unit:-1
unit_task_id:27
url:"#"
1:Object
enabled:true
newunit:-1
task:"The part 3 training units"
unit:-1
unit_task_id:28
url:"#"
The bit I am unsure about is how for each result, how do I run another Axios database call that shows if the reading Task is complete or not. For example for the first record, the complete status should be true (unit_task_id:27) and the second record should be false.
userTasks:Array[1]
0:Object
complete:true
enabled:true
newunit:-1
task:"The part 3 guide"
unit:-1
unit_task_id:27
unit_task_user_id:21
<ul>
<li v-for="task in readingTasks">
{{task.task}}
//trying to call a function that does an Axios call passing in parameters from readingTasks
{{getUserTaskByUnit(task.unit, task.unit_task_id)}}
<template v-for="usertask in userTasks">
{{usertask.complete}}
</template>
</li>
</ul>
//javascript if its useful
data: {
readingTasks: [],
userTasks: []
},
mounted() {
this.lastUnit();
},
methods: {
//functons
lastUnit: function() {
this.tasks();
},
tasks: function() {
var self = this;
var unit = this.unit;
axios.get("/WebService/units.asmx/GetTasks?unit=" + unit).then(function(response) {
self.readingTasks = response.data;
})
.catch(function(error) {
console.log(error);
})
.then(function() {
});
},
getUserTaskByUnit: function(unit, unitTaskId) {
var self = this;
axios.get("/WebService/units.asmx/GetUserTasks?unit=" + unit + "&unitTaskId=" + unitTaskId).then(function(response) {
self.userTasks = response.data;
})
.catch(function(error) {
console.log(error);
})
.then(function() {});
}
This code seems close to doing the correct thing, however {{usertask.complete}} flickers between true and false for both sets of results. Like it is stuck in a loop.
I would expect the first result to show True here and the second result to show False.
The part 3 guide - true
The part 3 training units - false
There are a few problems here.
The template has a dependency on userTasks, so every time userTasks changes it will cause the component to re-render, running the template again.
Every time the template runs it calls getUserTaskByUnit for both tasks. That will, asynchronously, update userTasks. When userTasks is updated it will trigger a re-render, which will call getUserTaskByUnit again, going round and round in an infinite loop.
Worse than just being an infinite loop, each time it renders it will trigger two requests, each of which will trigger another re-rendering. The number of requests will balloon exponentially.
When those requests do return you're then storing them in userTasks. But both responses are being stored in exactly the same place, so you'll only ever see the results of one request in the UI.
The first thing you'll need is a better data structure for storing the responses in getUserTaskByUnit. The simplest place to store them would be on the tasks in readingTask. That might look something like this:
// Note the whole task is now being passed to getUserTaskByUnit
getUserTaskByUnit: function(task) {
var self = this;
axios.get("/WebService/units.asmx/GetUserTasks?unit=" + task.unit + "&unitTaskId=" + task.unit_task_id).then(function(response) {
task.userTasks = response.data;
})
...
}
The call to getUserTaskByUnit needs moving out of the template. Moving it into the tasks method seems as good a place as any. There are also a few changes required to get it to work with the new version of getUserTaskByUnit:
tasks: function() {
var self = this;
var unit = this.unit;
axios.get("/WebService/units.asmx/GetTasks?unit=" + unit).then(function(response) {
var readingTasks = response.data;
// Pre-populate userTasks so it will be reactive
readingTasks.forEach(function(task) {
task.userTasks = [];
});
// This must come after userTasks is pre-populated
self.readingTasks = readingTasks;
readingTasks.forEach(function(task) {
// Passing task to getUserTaskByUnit, not unit and unit_task_id
self.getUserTaskByUnit(task);
});
})
...
Then within the template we'd need to loop over task.userTasks:
<ul>
<li v-for="task in readingTasks">
{{task.task}}
<template v-for="usertask in task.userTasks">
{{usertask.complete}}
</template>
</li>
</ul>
There are alternative data structures we could use depending on what other requirements you have. For example, you could retain a separate userTasks object to hold the userTasks but for that to work it would need to be a nested data structure rather than just an array. You'd need to key it by unit and then unitTaskId. The result in the template would be something like this:
<ul>
<li v-for="task in readingTasks">
{{task.task}}
<template v-for="usertask in userTasks[task.unit][task.unit_task_id]">
{{usertask.complete}}
</template>
</li>
</ul>
Much like with the earlier solution you would need to pre-populate the userTasks with empty values when readingTasks first loads to ensure the values are reactive and also to avoid the template blowing up at the undefined entries. Alternatively you could use $set and suitable v-if checks respectively.
This is all quite fiddly. It may be that you can simplify it a little based on your knowledge of the system. For example, it may be possible to form compound string keys for userTasks rather than using two levels of nesting. Or it might be that unit is a prop that can be considered constant and doesn't need including in that data structure.
Your userTasks is a view property and gets overwritten upon every call to getUserTaskByUnit (i.e. for each item in readingTasks). What you instead want is a nested structure. You should call getUserTaskByUnit in a loop as soon as readingTasks got loaded, i.e. after the line self.readingTasks = response.data;, and store the response as a property for every readingTask object.

How to prevent #change event when changing v-model value

I'm building an auto-complete menu in Vue.js backed by Firebase (using vue-fire). The aim is to start typing a user's display name and having match records show up in the list of divs below.
The template looks like this:
<b-form-input id="toUser"
type="text"
v-model="selectedTo"
#change="searcher">
</b-form-input>
<div v-on:click="selectToUser(user)" class="userSearchDropDownResult" v-for="user in searchResult" v-if="showSearcherDropdown">{{ user.name }}</div>
Upon clicking a potential match the intention is to set the value of the field and clear away the list of matches.
Here is the code portion of the component:
computed: {
/* method borrowed from Reddit user imGnarly: https://www.reddit.com/r/vuejs/comments/63w65c/client_side_autocomplete_search_with_vuejs/ */
searcher() {
let self = this;
let holder = [];
let rx = new RegExp(this.selectedTo, 'i');
this.users.forEach(function (val, key) {
if (rx.test(val.name) || rx.test(val.email)) {
let obj = {}
obj = val;
holder.push(obj);
} else {
self.searchResult = 'No matches found';
}
})
this.searchResult = holder;
return this.selectedTo;
},
showSearcherDropdown() {
if(this.searchResult == null) return false;
if(this.selectedTo === '') return false;
return true;
}
},
methods: {
selectToUser: function( user ) {
this.newMessage.to = user['.key'];
this.selectedTo = user.name;
this.searchResult = null;
}
}
Typeahead works well, on each change to the input field the searcher() function is called and populates the searchResult with the correct values. The v-for works and a list of divs is shown.
Upon clicking a div, I call selectToUser( user ). This correctly reports details from the user object to the console.
However, on first click I get an exception in the console and the divs don't clear away (I expect them to disappear because I'm setting searchResults to null).
[Vue warn]: Error in event handler for "change": "TypeError: fns.apply is not a function"
found in
---> <BFormInput>
<BFormGroup>
<BTab>
TypeError: fns.apply is not a function
at VueComponent.invoker (vue.esm.js?efeb:2004)
at VueComponent.Vue.$emit (vue.esm.js?efeb:2515)
at VueComponent.onChange (form-input.js?1465:138)
at boundFn (vue.esm.js?efeb:190)
at invoker (vue.esm.js?efeb:2004)
at HTMLInputElement.fn._withTask.fn._withTask (vue.esm.js?efeb:1802)
If I click the div a second time then there's no error, the input value is set and the divs disappear.
So I suspect that writing a value to this.selectedTo (which is also the v-model object for the element is triggering a #change event. On the second click the value of doesn't actually change because it's already set, so no call to searcher() and no error.
I've noticed this also happens if the element loses focus.
Question: how to prevent an #change event when changing v-model value via a method?
(other info: according to package.json I'm on vue 2.5.2)
On:
<b-form-input id="toUser"
type="text"
v-model="selectedTo"
#change="searcher">
The "searcher" should be a method. A method that will be called whenever that b-component issues a change event.
But looking at your code, it is not a method, but a computed:
computed: {
searcher() {
...
},
showSearcherDropdown() {
...
}
},
methods: {
selectToUser: function( user ) {
...
}
}
So when the change event happens, it tries to call something that is not a method (or, in other words, it tries to call a method that doesn't exist). That's why you get the error.
Now, since what you actually want is to update searcher whenever this.selectedTo changes, to get that, it is actually not needed to have that #change handler. This is due to the code of computed: { searcher() { already depending on this.selectedTo. Whenever this.selectedTo changes, Vue will calculate searcher again.
Solution: simply remove #change="searcher" from b-form. Everything else will work.
#acdcjunior, thanks for your answer.
Of course just removing the reference to searcher() just means no action is taken upon field value change so the field won’t work at all.
Moving the searcher() function into methods: {} instead of computed: {} means that it will be called on an input event and not a change even (another mystery but not one for today). A subtle difference that takes away the typeahead feature I’m aiming at.
However, it did make me remember that the result of computed: {} functions are cached and will be re-computed when any parameters change. In this case I realised that the searcher() function is dependent upon the this.selectedTo variable. So when the selectToUser() function sets this.selectedTo it triggers another call to searcher().
Fixed now. In case anyone has a similar problem in the future, I resolved this by turning to old fashioned semaphore by adding another variable.
var userMadeSelection: false
Now, searcher() begins with a check for this scenario:
computed: {
searcher() {
if(this.userMadeSelection) {
this.userMadeSelection = false;
return this.selectedTo;
}
…
and then in selectToUser():
this.userMadeSelection = true;

Setting a value in v-if block

How do I use v-if to do two things
Show The message and
2) set message.hasSublocationOutage to true.
So if there is an outage show the message and set the flag to true message.hasSublocationOutage Or pass true to a method
<div v-if="!subLocation.outageTag.length - 1">
There is a problem
</div>
There seems to be some inherent flaws in your design, but you can invoke a method that calculates whether or not to display and sets the message at the same time.
HTML
<div v-if="canShowAndCalculate()">
There is a problem
</div>
JS
export default {
methods: {
canShowAndCalculate() {
if (subLocation.outageTag.length - 1) return false;
// else
message.hasSublocationOutage = true
return true
}
}
}
As Andrey mentioned, this is highly unadvisable. Having side effects in your conditional logic hides core logic. Rather, you should update a boolean condition when your data changes, not the other way around.
As a side note, you could use a computed property like V Sambor suggested for better performance, but that hides the "wrong" implementation even further since computed properties should always be cached and flowing out, whereas you could expect a method to do both, even though in this case it is inadvisable.
You can do a computed method for this is pretty much the same as #David L answer only that this will cache your display result until some related variables changes their values.
computed: {
display() {
if (this.subLocation.outageTag.length - 1) {
return false;
}
this.message.hasSublocationOutage = true;
return true;
}
}
Then in Html you can do:
<div v-if="display()">
There is a problem
</div>

Kendo UI autocomplete dynamically loading dBdata when typing

I am writing a kendo UI autocomplete widget. The requirement is EACH TIME when I type a letter after "minLength", the dataSource need to be dynamically loaded from dB EVERYTIME. One problem is that, when the dataSource load successfully in the first time, it stops loading data.
The code snippet is:
var data;
function getDataFromDb(){
// some code to grab dummyData from dB ...
return dummyData;
}
$("#someInputText").kendoAutoComplete({
minLength: 2,
dataTextField: "someField",
dataSource: getDataFromDb(),
filter: "startswith"
});
Thanks a lot.
More details on the post. In my situation, I don't use the readOption. The data comes from another ajax call like:
var data [];
//fire this ajax call when input string length comes to 4...
$.ajax({url: "some working url", success: function(result){
var data = result;
startKendoAutoComplete();
}
});
function startKendoAutoComplete(){
if( !$.isEmptyObject(data)) // set a breakPoint, have data
{
$("#inputText").kendoAutoComplete({
minLength: 4,
dataSource : data,
...
});
}
}
Also, the ajax call will be fired when the input string length comes to 4. However, the KendoAutoComplete doesn't start working....
Thanks a lot for your sugesstion.
If you init your dataSource with an array of object, your widget will work with this array only.
The first thing you'll have to create an dataSource object and set the serverFiltering property to true. Then, if you don't specify an url where the data will be fetched, you set you own transport.read function and from there you'll be able to implement your own logic. The read function will receive the readOption which will include all the relevant information to query tour data (top / skip / filter / sort ...). The readOptions will also provide a success function that should be used to return the value:
dataSource: {
serverFiltering: true,
transport: {
read: function (readOptions) {
readOptions.success(getDataFromDb(readOptions));
}
}
},

Unbinding and rebinding DataTables search event results in null sSearch

I'm in a bit of a pickle with this one. I'm using jQuery DataTables 1.10.6, and I want to make it so that when the user stops typing (like maybe after 950 ms?), the search is performed. I kind of have it working, but since I'm using server-side processing, sSearch (the search parameter) returns null.
I'm guessing it has to do with the unbinding and re-binding, but I don't know how to make it so that sSearch is sent properly.
var delay = function () {
var timer = 0;
return function (callback, ms) {
clearTimeout(timer);
timer = setTimeout(callback, ms);
}
})();
$("div.dataTables_filter input").unbind();
$("div.dataTables_filter input").bind("keyup", function () {
alert(this.value);
delay(function () {
$("#MyTable").dataTable().fnFilter(this.value);}, 1000);
}
DataTables has an option to se delay, you can check it here
I found a deprecated plugin which is a key debounce delay: https://www.datatables.net/plug-ins/api/fnSetFilteringDelay
Even though it's stated that this won't work with 1.10+, there's a comment stating that a line change would allow it to work in 1.10+. So the following line:
anControl.unbind('keyup search input').bind('keyup search input', function() {
...would be replaced with:
anControl.off('keyup search input').on('keyup search input', function() {
I was also able to set the filtering delay timer in the calling argument. Works like a charm.