Setting a value in v-if block - vue.js

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>

Related

vuelidate $touch() for same form creating/editing

I might have somewhat a tricky question and just want to be sure, I went the correct way.
To be precise, it's vuelidate 0x on vue 2 with good old boostrap-vue
Let's pressure we have a simple form, with the following validation
<b-form>
.....
<b-form-group
id="title"
label="Title"
label-for="Title"
>
<b-form-input
id="title-input"
v-model="$v.form.title.$model"
:state="validateState('title')"
type="text"
></b-form-input>
<b-form-invalid-feedback id="title-input-live-feedback">
You must enter a title.
</b-form-invalid-feedback>
</b-form-group>
....
validations: {
form: {
title: { required }
}
},
So when we create this item. I do the following:
async createCompany() {
if (!this.$v.$dirty) {
this.$v.$touch()
}
if (this.$v.$invalid) return
try {
await this.storeCompany()
...
} catch (error) {
//
}
},
Now, I come back to the form, for example, to modify it. This means, the item has once passed the validation successfully, especially for required fields.
I noticed that in that sense $touch() is not necessary, unless you add new validations, which would always result in $invalid. Still, I wanted to have a solution that will also work for this case, just so I do not stumble over it in the future.
So, to come up with a "flexible" solution, I did the following:
async updateCompany() {
if (this.$v.$invalid && !this.$v.$dirty) {
this.$v.$touch()
return
}
if (this.$v.$invalid) return
try {
let response = await CompaniesApi.update(this.id, this.form)
...
} catch (error) {
//
}
},
Basically, I first check if $invalid is true and if the form was not submitted (by checking for !this.$v.$dirty).
It works exactly as I want, no double, triple touches and no flickering in terms of validation changes due to touches.
What gives me a bit of a headache is the fact that I first check for this.$v.$invalid before I actually $touch().
Somehow, I thought it must be the other way around.
But I guess I misunderstood $touch() in a way that it's main job is really to show the $errors if needed (vuelidate only shows errors when $invalid and $dirty is set to true).
I know I could go the other way, disabling the submit button until the form is valid, but then I would need to add further elements to the form, e.g. make clear what is missing or maybe even $touch() the validation in the created() or mounted() hook.
Which however is also not what I want, because it would show errors to the user without any interaction.
So, the only solution I could find is the one described.
Did anybody else come across this? I mean, it does what I want, still would appreciate any feedback/ideas.

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

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

Comparing with previous iteration in v-for

Here's my code:
<div v-for="(message) in messages">
<div class="message-divider sticky-top pb-2" data-label="Test"> </div>
...
What I need to achieve is, if current iteration's message.createdAt.seconds differs by day from the previous one to show message-divider element.
So the time format is in seconds like this: 1621515321
How can I achieve this?
You can do variable assignment as part of the template, so you could assign the previous value and compare, but this is not a very good idea. Instead you can use a computed to prepare your array to only have the objects you want, with the data you need. So in this case, you could use a computed to create a new array with objects that have additional flags like className or showDivider etc.
example:
computed:{
messagesClean(){
let lastCreatedAt;
return this.messages.map(message => {
const ret = {
...message,
showDivider: lastCreatedAt + 3600*24 < message.createdAt.seconds // your custom logic here
}
lastCreatedAt = message.createdAt.seconds
return ret
}
}
}
the logic to determine when the divider gets shown is up to you there, I suspect it may be a bit more complicated than differing by a day.
You need something like this:
<div v-if="compEqRef">
OK
</div>
<div v-else >!!OK!!</div>
Here compEqRef could be in computed:
computed: {
compEqRef() {
//do something
}
},

how to reference a store variable inside a value.sync statement in Vue.js

Ok, so I have a Vuex store that manages dropdown content on my page. The page I'm currently working on has two sets of three identical dropdowns that should behave exactly the same. One on a modal and the other one on the main page. So what I did is this:
this.$store.commit('setModule', 'manageSchedules');
this.$store.commit('setModule', 'manageSchedulesModale');
Each store has the following:
// These are arrays with the lists' content
sites
Profiles
Employees
//These are the actual value of each of my list
CurrentSite
CurrentProfile
CurrentEmployee
My problem is that I'd like the value of my list to by synched with the corresponding "currentXXX" of my vuex store. Normally, I'd go with something like:
:value.sync="currentXXX"
However, I can't find anywhere how exactly to reference the store in a value.sync statement. Could anyone help me?
Okay, Ricky was right, using a computed property with getter and setter worked perfectly and his link provided me with all the info I needed. In essence here's what I now have:
<template lang="pug">
.modal(v-if="popupShown", style="display:block", v-on:click="hideModalSafe")
.modal-dialog.modal-lg(v-on:keydown.enter="syncSchedule()")
.modal-content
h3.modal-header {{moment(currentSchedule.start).format('YYYY-MM-DD')}}
button.close(type="button",v-on:click="popupShown=false") ×
.modal-body
.row
.col.form-group
label(for="schedule-start") {{__('Start')}}
k-input-time(:date.sync="currentSchedule.start")
.col.form-group
label(for="schedule-end") {{__('End')}}
k-input-time(:date.sync="currentSchedule.end")
.col.form-group
label(for="schedule-pause") {{__('Pause')}}
k-input-minutes(:minutes.sync="currentSchedule.pause")
.row
.col.form-group
label(for="schedule-site-id") {{__('Site')}}
k-combobox(model="modSites", context="selection",
:value.sync="currentSiteMod")
.col.form-group
label(for="schedule-profile-id") {{__('Profile')}}
k-combobox(model="modProfiles", context="selection",
:value.sync="currentProfileMod")
.col.form-group
label(for="schedule-employee-id") {{__('Employee')}}
k-combobox(model="modEmployees", context="selection",
:value.sync="currentEmployeeMod")
.modal-footer
.btn.btn-danger.mr-auto(v-on:click="deleteSchedule()")
k-icon(icon="delete")
| {{__('Remove')}}
.btn.btn-primary(v-on:click="syncSchedule()", tabindex="0") {{__('Save')}}
</template>
export default {
......
computed:{
currentSiteMod:{
get(){
return this.$store.state.manageSchedulesModale.currentSite;
},
set(value){
this.$store.dispatch("manageSchedulesModale/updateCurrentSite", value);
}
},
currentProfileMod:{
get(){
return this.$store.state.manageSchedulesModale.currentProfile;
},
set(value){
this.$store.dispatch("manageSchedulesModale/updateCurrentProfile", value);
}
},
currentEmployeeMod: {
get(){
return this.$store.state.manageSchedulesModale.currentEmployee;
},
set(value){
this.$store.dispatch("manageSchedulesModale/updateCurrentEmployee", value);
}
}
}
}
Once again, Vue.js actually impressed me with its magic. Before going with computed properties, I got tangled in a slew of ajax calls and promises that kept going back and forth, producing inconsistent results. Now, the store's value gets updated perfectly and the lists react accordingly without me having to worry about an insane amount of parameters.
Anyway, there's probably a few things I didn't do right. I know I probably should have defined getters instead of accessing the property directly, but as it stands, it works and my client's happy (and so am I).

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;