Can I pass a computed property and an object inside a v-bind:class in Vuejs? - vue.js

I want to understand if I can do something like this, because I am trying but only getting erros... Forgive my bad english.
HTML file would be something like
<p :class="{mycss: isActive}, myComputedProperty" > My text </p>
and the component file would have something like
export default {
data () {
return {
isActive: true
}
},
computed: {
myComputedProperty () {
// do something
}
}
}

class value is an expression. If it doesn't make sense in raw JavaScript, it doesn't make sense there. Here comma operator is used, so the expression evaluates to myComputedProperty, and {mycss: isActive} part is discarded.
The format for combined class value is documented:
:class="[{mycss: isActive}, myComputedProperty]"
Since computed values are involved, defining the whole class object as a computed will result in cleaner template code.

I think the error is in your HTML - the comma is probably the cause. There are lots of ways to format strings, but this is one option:
<p :class="isActive ? 'mycss ' + myComputedProperty : myComputedProperty" > My text </p>

Related

vue does not recover from me specifying a non existing location for v-model

When I have a textarea like
<textarea v-model="foo.abc.text"></textarea>
and either foo or foo.abc does not exist yet then
vue removes either parts of the DOM or is giving me a blank page.
It does never recover.
That alone is annoying, regardless of if I am using a debug version of vue or not.
If I try to use an approach that I have been advised to use earlier like
<textarea v-model="foo?.abc?.text"></textarea>
then I am still out of luck, I presume that I get a "rvalue" using those question marks and what I need rather is a variable location.
How do I, with as little trickery as possible, allow v-model to exist later on even if it doesnt exist now (late binding)?
Just shape your data accordingly and initialize it with empty values:
data(){
return {
foo: {
abc: {
text: ''
}
}
}
}
You can later populate it e.g. with the result of api call, but it's still better to initialize data properly
I would suggest going the :value + #input way. It allow more control over the input model, and does not require hiding it.
<textarea :value="!!foo && foo.abc.text" #input="(val) => !!foo && (foo.abc.text = val)" />
You can even hook in a validator:
<textarea
:value="!!foo && foo.abc.text"
#input="(val) => !!foo && (foo.abc.text = val)"
:rules="v => !v && 'The object has not been initialised'"
/>
I found a solution I can live with and then I got a comment in the same direction:
Conditionally showing the textarea.
v-if seems to do it but it falls under the "trickery" category, I think (angularjs would be more relaxed).
<textarea v-if="foo!=null" v-model="foo.abc"></textarea>
The symptom to hiding components if something is not all correct is not the best part of vue.js. Better show them and color them red.

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
}
},

Nuxt.js: Can I add a class using a ternary operator based on a Boolean from a prop?

I'm trying to use a Boolean in a prop to add a class based on a ternary operator. I'm obviously not doing it right because it is always evaluating to false.
<div :class="$style.inner + ' ' + (isRight == true ? 'is-right' : 'is-false')" :style="`color: #` + fontColor"></div>
If it's true, I need is-right added to the class, otherwise, this doesn't need to be added.
props: {
isRight: {
type: Boolean,
default: false
},
}
index.vue
<Signpost isRight=true/>
.is-right { padding-left:20% }
Looking at their Docs, I'm not sure I can do this with a Boolean actually.
To pass a Boolean to a prop you need to use v-bind or : like this:
<signpost v-bind:isRight="true" />
or
<signpost :isRight="true" />
Also if the only possible values for isRight is true or false you can shorten your ternary to just be:
:class="isRight ? 'is-right' : 'is-false'
I actually had a little syntax error for my prop that needed binding:
<Signpost :is-right="true"/>
And then on the div as #kissu said:
<div :class="[isRight ? 'is-right' : '']"></div>
Try passing the prop as :is-right="false" to have it passed down as Boolean and not as a basic String (is-right="false").
For the class, it can be written like as shown in the docs
<div :class="[isRight ? 'is-right' : 'is-false']"></div>
That way, you will even be able to do a strong equality check: isRight === true (comparing boolean to boolean).
VueJS devtools can help you see the type (thanks to the color).
There are some strongly recommended prop name casing recommendation that can be found here: https://v2.vuejs.org/v2/style-guide/#Prop-name-casing-strongly-recommended
props: {
greetingText: String // camel-cased here
}
<WelcomeMessage greeting-text="hi"/> <!-- kebab-cased here -->
Also, feel free to write ES6 template litterals, it may help the lecture. A working solution would be:
<div :style="`color: #${fontColor}`"></div>
Can also be done for your class, but I didn't want to risk a bad interpolation here.

How can I implement v-model.number on my own in VueJS?

I have a text field component for numeric inputs. Basically I'm just wrapping v-text-field but in preparation for implementing it myself. It looks like this.
<template>
<v-text-field v-model.number = "content" />
</template>
<script>
export default {
name: 'NumericTextField',
props: [ 'value' ],
computed: {
content: {
get () { return this.value },
set (v) { this.$emit('input', f) },
},
}
}
</script>
This has generated user feedback that it's annoying when the text field has the string "10.2" in it and then backspace over the '2', then decimal place is automatically delete. I would like to change this behavior so that "10." remains in the text field. I'd also like to understand this from first principles since I'm relatively new to Vue.
So I tried this as a first past, and it's the most instructive of the things I've tried.
<template>
<v-text-field v-model="content" />
</template>
<script>
export default {
name: 'NumericTextField',
props: [ 'value' ],
computed: {
content: {
get () { return this.value },
set (v) {
console.log(v)
try {
const f = parseFloat(v)
console.log(f)
this.$emit('input', f)
} catch (err) {
console.log(err)
}
},
},
}
}
</script>
I read that v-model.number is based on parseFloat so I figured something like this must be happening. So it does fix the issue where the decimal place is automatically deleted. But... it doesn't even auto delete extra letters. So if I were to type "10.2A" the 'A' remains even though I see a console log with "10.2" printed out. Furthermore, there's an even worse misfeature. When I move to the start of the string and change it to "B10.2" it's immediately replaced with "NaN".
So I'd love to know a bunch of things. Why is the body of the text body immediately reactive when I change to a NaN but not immediately reactive when I type "10.2A"? Relatedly, how did I inadvertently get rid of the auto delete decimal place? I haven't even gotten to that part yet. So I'm misunderstanding data flow in Vue.
Lastly, how can I most simply provide a text box that's going to evaluate to a number for putting into my data model but not have the annoying auto delete of decimal places? The existing functionality doesn't auto delete trailing letters so I'm guessing the auto delete of decimal places was a deliberate feature that my users don't like.
I'm not 100% sure of any of this, but consider how v-model works on components. It basically is doing this:
<v-text-field
v-bind:value="content"
v-on:input="content = $event.target.value"
/>
And consider how the .number modifier works. It runs the input through parseFloat, but if parseFloat doesn't work, it leaves it as is.
So with that understanding, I would expect the following:
When you type in "10.2" and then hit backspace, "10." would be emitted via the input event, parseFloat("10.") would transform it to 10, v-on:input="content = $event.target.value" would assign it to content, and v-bind:value="content" would cause the input to display "10". So then, this is the expected behavior.
When you type in "10.2" and then hit "A", "10.2A" would be emitted via the input event, parseFloat("10.2A") would transform it to 10.2, v-on:input="content = $event.target.value" would assign it to content, and v-bind:value="content" would cause the input to display "10.2". It looks like it's failing at that very last step of causing the input to display "10.2", because the state of content is correctly being set to 10.2. If you use <input type="text" v-model.number="content" /> instead of <v-text-field v-model.number="content" />, once you blur, the text field successfully gets updated to "10.2". So it seems that the reason why <v-text-field> doesn't is due to how Vuetify is handling the v-bind:value="content" part.
When you type in "10.2" and then enter "B", in the beginning, "B10.2" would be emitted via the input event, parseFloat("B10.2") would return NaN, and thus the .number modifier would leave it as is, v-on:input="content = $event.target.value" would assign "B10.2" to content, and v-bind:value="content" would cause the input to display "B10.2". I agree that it doesn't seem right for parseFloat("10.2A") to return 10.2 but parseFloat("B10.2") to return "B10.2".
Lastly, how can I most simply provide a text box that's going to evaluate to a number for putting into my data model but not have the annoying auto delete of decimal places?
Given that the default behavior is weird, I think you're going to have to write your own custom logic for transforming the user's input. Eg. so that "10.2A" and "B10.2" both get transformed to 10.2 (or are left as is), and so that decimals are handled like you want. Something like this (CodePen):
<template>
<div id="app">
<input
v-bind:value="content"
v-on:input="handleInputEvent($event)"
/>
<p>{{ content }}</p>
</div>
</template>
<script>
export default {
data() {
return {
content: 0,
};
},
methods: {
handleInputEvent(e) {
this.content = this.transform(e.target.value);
setTimeout(() => this.$forceUpdate(), 500);
},
transform(val) {
val = this.trimLeadingChars(val);
val = this.trimTrailingChars(val);
// continue your custom logic here
return val;
},
trimLeadingChars(val) {
if (!val) {
return "";
}
for (let i = 0; i < val.length; i++) {
if (!isNaN(val[i])) {
return val.slice(i);
}
}
return val;
},
trimTrailingChars(val) {
if (!val) {
return "";
}
for (let i = val.length - 1; i >= 0; i--) {
if (!isNaN(Number(val[i]))) {
return val.slice(0,i+1);
}
}
return val;
},
},
};
</script>
The $forceUpdate seems to be necessary if you want the input field to actually change. However, it only seems to work on <input>, not <v-text-field>. Which is consistent with what we saw in the second bullet point. You can customize your <input> to make it appear and behave like <v-text-field> though.
I put it inside of a setTimeout so the user sees "I tried to type this but it got deleted" rather than "I'm typing characters but they're not appearing" because the former does a better job of indicating "What you tried to type is invalid".
Alternatively, you may want to do the transform on the blur event rather than as they type.

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).