Checkbox renders false but the data and DOM attribute is true - vue.js

I want to create a checkbox group component. Currently there is no native solution so I tried to create one on my own
<template>
<v-container fluid>
<v-checkbox
v-for="(groupItem, index) in groupItems"
:key="index"
:label="groupItem.display"
:value="groupItem.value"
#change="onCheckboxUpdated(index)"
></v-checkbox>
</v-container>
</template>
<script>
export default {
props: {
groupItems: {
type: Array,
required: true
}
},
methods: {
onCheckboxUpdated: function(index) {
this.groupItems[index].value = !this.groupItems[index].value;
this.$emit("checkboxGroupUpdated", this.groupItems);
}
}
};
</script>
This component should render a specific amount of checkboxes and fire an event with all the updated values.
When I pass in these values
values: [
{
display: "Read permissions",
value: true
},
{
display: "Write permissions",
value: false
},
{
display: "Delete permissions",
value: false
}
]
the first generated checkbox renders a false state although the DOM element is set to true
When toggling the checkbox it will work fine the next time.
I created an example to reproduce the problem
https://codesandbox.io/s/checkboxgroup-l8gcg

If you want to expand the v-model of a v-checkbox you'll need to use input-value as the prop rather than value.
:input-value="groupItem.value"
See:
https://github.com/vuetifyjs/vuetify/blob/27d5fdd32dc7c8a9af38f823d1574d92b211d405/packages/vuetify/src/mixins/selectable.js#L14
That's a mixin that's included in v-checkbox.
While input-value is documented, https://vuetifyjs.com/en/components/selection-controls#api, you do have to dig a bit to find it.
Unrelated to your problem, your current implementation of a checkbox group violates one-way data flow. You are mutating an object that has been passed via a prop.
https://v2.vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow
I'll leave it to you to decide whether you care.

It is because Vue.js ignores the value of inputs. You need to use v-model instead.
<v-checkbox
v-for="(groupItem, index) in groupItems"
:key="index"
:label="groupItem.display"
v-model="groupItem.value"
#change="onCheckboxUpdated(index)"
></v-checkbox>
From the documentation https://v2.vuejs.org/v2/guide/forms.html
v-model will ignore the initial value, checked or selected attributes found on any form elements. It will always treat the Vue instance data as the source of truth. You should declare the initial value on the JavaScript side, inside the data option of your component.

Related

Vue- best practice for loops and event handlers

I am curious if it is better to include methods within loops instead of using v-if. Assume the following codes work (they are incomplete and do not)
EX: Method
<template >
<div>
<div v-for="(d, i) in data" v-bind:key="i">
<span v-on:click="insertPrompt($event)">
{{ d }}
</span>
</div>
</div>
</template>
<script>
export default {
data() {
data:[
.....
]
},
methods:{
insertPrompt(e){
body.insertBefore(PROMPT)
}
}
}
</script>
The DOM would be updated via the insertPrompt() function which is just for display
EX: V-IF
//Parent
<template >
<div>
<div v-for="(d, i) in data" v-bind:key="i">
<child v-bind:data="d"/>
</div>
</div>
</template>
<script>
import child from './child'
export default {
components:{
child
},
data() {
data:[
.....
]
},
}
</script>
//Child
<template>
<div>
<span v-on:click="display != display">
{{ d }}
</span>
<PROMPT v-if="display"/>
</div>
</template>
<script>
import child from './child'
export default {
components:{
child
},
data(){
return {
display:false
}
},
props: {
data:{
.....
}
},
}
</script>
The PROMPT is a basic template that is rendered with the data from the loop data click.
Both methods can accomplish the same end result. My initial thought is having additional conditions within a loop would negatively impact performance?!?!
Any guidance is greatly appreciated
Unless you are rendering really huge amounts of items in your loops (and most of the times you don't), you don't need to worry about performance at all. Any differences will be so small nobody will ever notice / benefit from having it a tiny touch faster.
The second point I want to make is that doing your own DOM manipulations is often not the best idea: Why do modern JavaScript Frameworks discourage direct interaction with the DOM
So I would in any case stick with the v-if for conditionally rendering things. If you want to care about performance / speed here, you might consider what exactly is the way your app will be used and decide between v-if and v-show. Citing the official documentation:
v-if is “real” conditional rendering because it ensures that event
listeners and child components inside the conditional block are
properly destroyed and re-created during toggles.
v-if is also lazy: if the condition is false on initial render, it
will not do anything - the conditional block won’t be rendered until
the condition becomes true for the first time.
In comparison, v-show is much simpler - the element is always rendered
regardless of initial condition, with CSS-based toggling.
Generally speaking, v-if has higher toggle costs while v-show has
higher initial render costs. So prefer v-show if you need to toggle
something very often, and prefer v-if if the condition is unlikely to
change at runtime.
https://v2.vuejs.org/v2/guide/conditional.html#v-if-vs-v-show
There are numerous solutions to solving this issue, but let's stick to 3. Options 2 and 3 are better practices, but option 1 works and Vue was designed for this approach even if hardcore developers might frown, but stick yoru comfort level.
Option 1: DOM Manipulation
Your data from a click, async, prop sets a condition for v-if or v-show and your component is shown. Note v-if removes the DOM element where v-show hides the visibility but the element is still in the flow. If you remove the element and add its a complete new init, which sometimes works in your favor when it come to reactivity, but in practice try not to manipulate the DOM as that will always be more expensive then loops, filters, maps, etc.
<template >
<div>
<div v-for="(d, i) in getData"
:key="i">
<div v-if="d.active">
<child-one></child-one>
</div>
<div v-else-if="d.active">
<child-two></child-two>
</div>
</div>
</div>
</template>
<script>
import ChildOne from "./ChildOne";
import ChildTwo from "./ChildTwo";
export default {
components: {
ChildOne,
ChildTwo
},
data() {
return {
data: [],
}
},
computed: {
getData() {
return this.data;
},
},
mounted() {
// assume thsi woudl come from async but for now ..
this.data = [
{
id: 1,
comp: 'ChildOne',
active: false
},
{
id: 2,
comp: 'ChildTwo',
active: true
},
];
}
}
</script>
Option 2: Vue's <component> component
Always best to use Vue built in component Vue’s element with the is special attribute: <component v-bind:is="currentTabComponent"></component>
In this example we pass a slug or some data attribute to activate the component. Note we have to load the components ahead of time with the components: {}, property for this to work i.e. it has to be ChildOne or ChildTwo as slug string. This is often used with tabs and views to manage and maintain states.
The advantage of this approach is if you have 3 form tabs and you enter data on one and jump to the next and then back the state / data is maintained, unlike v-if where everything will be rerendered / lost.
Vue
<template >
<div>
<component :is="comp"/>
</div>
</template>
<script>
import ChildOne from "./ChildOne";
import ChildTwo from "./ChildTwo";
export default {
components: {
ChildOne,
ChildTwo
},
props: ['slug'],
data() {
return {
comp: 'ChildOne',
}
},
methods: {
setComponent () {
// assume prop slug passed from component or router is one of the components e.g. 'ChildOne'
this.comp = this.slug;
}
},
mounted() {
this.nextTick(this.setModule())
}
}
</script>
Option 3: Vue & Webpack Async and Dynamic components.
When it comes to larger applications or if you use Vuex and Vue Route where you have dynamic and large number of components then there are a number of approaches, but I'll stick to one. Similar to option 2, we are using the component element, but we are using WebPack to find all Vue files recursively with the keyword 'module'. We then load these dynamically / asynchronous --- meaning they will only be loaded when needed and you can see this in action in network console of browser. This means I can build components dynamically (factory pattern) and render them as needed. Example, of this might be if a user adds projects and you have to build and config views dynamically for projects created e.g. using vue router you passed it a ID for a new project, then you would need to dynamically load an existing component or build and load a factory built one.
Note: I'll use v-if on a component element if I have many components and I'm unsure the user will need them. I don't want to maintain state on large collections of components because I will end up memory and with loads of observers / watches / animations will most likely end up with CPU issues
<template >
<div>
<component :is="module" v-if="module"/>
</div>
</template>
<script>
const requireContext = require.context('./', true, /\.module\.vue$/);
const modules = requireContext.keys()
.map(file =>
[file.replace(/(.*\/(.+?)\/)|(\.module.vue$)/g, ''), requireContext(file)]
)
.reduce((components, [name, component]) => {
// console.error("components", components)
components[name] = component.default || component
return components
}, {});
export default {
data() {
return {
module: [],
}
},
props: {
slug: {
type: String,
required: true
}
},
computed: {
getData() {
return this.data;
},
},
methods: {
setModule () {
let module = this.slug;
if (!module || !modules[module]) {
module = this.defaultLayout
}
this.module = modules[module]
}
},
mounted() {
this.nextTick(this.setModule())
}
}
</script>
My initial thought is having additional conditions within a loop would negatively impact performance?
I think you might be confused by this rule in the style guide that says:
Never use v-if on the same element as v-for.
It's only a style issue if you use v-if and v-for on the same element. For example:
<div v-for="user in users" v-if="user.isActive">
But it's not a problem if you use v-if in a "child" element of a v-for. For example:
<div v-for="user in users">
<div v-if="user.isActive">
Using v-if wouldn't have a more negative performance impact than a method. And I'm assuming you would have to do some conditional checks inside your method as well. Remember that even calling a method has some (very small) performance impact.
Once you use Vue, I think it's a good idea not to mix it up with JavaScript DOM methods (like insertBefore). Vue maintains a virtual DOM which helps it to figure out how best to update the DOM when your component data changes. By using JavaScript DOM methods, you won't be taking advantage of Vue's virtual DOM anymore.
By sticking to Vue syntax you also make your code more understandable and probably more maintainable other developers who might read or contribute to your code later on.

Cleanest way to re-render component in Vue

I have a Keyboard.vue component containing many Key.vue child component instances (one for each key).
In Key.vue, the key is actually a html <button> element that can get disabled.
By clicking a certain button in my app, I want to reset keyboard and make all keys enabled again. I thought that setting a v-if to false then to true again (<keyboard v-if="BooleanValue" />) would re-render Keyboard.vue and all its Key.vue child component instances.
It doesn't. Why not?
App.vue
<template>
<div class="app">
...
<keyboard v-if="!gameIsOver && showKeyboard" />
...
</div>
</template>
<script>
export default {
components: {
Keyboard
},
computed: {
gameIsOver () {
return this.$store.state.gameIsOver
},
showKeyboard () {
return this.$store.state.showKeyboard
}
}
Keyboard.vue
<template>
<section>
<key class="letter" v-for="(letter) in letters" :key="letter" :letter="letter" />
</section>
</template>
Key.vue
<template>
<button :disabled="disabled" #click="checkLetter(letter)">
{{ letter }}
</button>
</template>
<script>
export default {
...
data () {
return {
disabled: false
}
}
My button resetting keyboard triggers:
this.$store.commit('SET_KEYBOARD_VISIBILITY', false)
this.$store.commit('SET_KEYBOARD_VISIBILITY', true)
To answer your question first, the cleanest way to re-render a Vue component or any element is to bind it's key attribute to something reactive that will control the re-renders, whenever the key value changes it will trigger a re-render.
To make such a unique key per render, I would probably use an incremented number and whenever I would like to re-render I would increment it.
<template>
<div>
<div :key="renderKey">
</div>
</div>
</template.
<script>
export default {
data: () => ({
renderKey: 0
}),
methods: {
reRender() {
this.renderKey++;
}
}
};
</script>
Now as for why toggling v-if didn't work: Toggling a reactive property between true and false doesn't necessarily trigger 2 re-renders because Vue has an async update queue which applies DOM changes in patches in certain time frames, not per individual update. This why Vue is so fast and efficient.
So you trigger disabled to false, then to true. The renderer will decide not to update the DOM because the final value has not changed from the last time, the timing is about 16ms If I recall correctly. So you could make that work by waiting more than 16ms between toggling your prop between true and false, I say "could" but not "should".
The cleanest way is to have the disabled state somewhere where you can reset it, because re-rendering your component to reset it is making use of a side-effect of destroying and re-creating your components. It makes it hard for someone to figure out why the buttons are enabled again, because there is no code changing the disabled variable to false anywhere that is being called when you rerender.
That said, you see your current behaviour because Vue aggregates all changes of the current "tick", and only rerenders at the end of that tick. That means if you set your variable to false, then to true, it will only use the last value.
// Nothing happens
this.showSomething = false
this.showSomething = true
To force it to re-render, you can use the trick Amitha shows, using key. Since Vue will use an instance per key value, changing the key will destroy the old one and create a new one. Alternatively, you can use this.$nextTick(() => { ... }) to force some of your code to run on the next tick.
// Destroy all the things
this.showSomething = false
this.$nextTick(() => {
// Okay, now that everything is destroyed, lets build it up again
this.showSomething = true
});
Could you please give a try to set a :key for keyboard like below
<keyboard v-if="!gameIsOver && showKeyboard" :key="increment" />
"increment": local component property
and increase the "increment" property value by one when you need to re-render view. use the vuex store property to sync with local "increment" property.
how to sync vuex value changes with local property: add a "watcher" to watch the vuex store property changes and assigned that change to the local "increment" property which we setted as the "key" for keyboard

Vue.JS radio input without v-model

I have a component that takes an array of options objects, a name, and a v-model, and puts out a radio input with matching label for each of the options in that array of options.
options = [
{text: "Option 1", value="option-1" },
{text: "Option 2", value="option-2" },
]
"updateValue" : function(){
this.$emit('input', this.$refs.input.value);
}
<template v-for="option in options" >
<input
:value="option.value"
:id="name + option.value"
:name="name"
#input="updateValue()"
ref="input" />
<label :for="name + option.value">{{option.text}}</label>
</template>
I've found a similar pattern in this guide where a v-model is taken and then value emitted by watching the #input event.
I cannot wrap my head around how to do this with a radio-type input. Setting :value="value" as with say a text input would make all the radio values identical, which really doesn't work.
Here's an example of a really primitive radio button component that uses v-model to update data when an entry is clicked:
const radio = {
props: ['option', 'value'],
template: `<div #click="onClick">{{ option === value ? 'o' : 'x' }}<slot /></div>`,
methods: {
onClick () {
this.$emit('input', this.option)
}
}
}
new Vue({
el: '#app',
components: {
radio
},
data () {
return {
options: ['red', 'green', 'blue'],
selectedOption: 'red'
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<radio v-for="option in options"
v-model="selectedOption"
:option="option"
>
{{ option }}
</radio>
</div>
With this implementation each radio button is passed both the currently selected value (via v-model) as well as the value for that particular option (via option). It uses === inside each radio button to determine whether it is the currently selected option.
Vue's support for native radio button's is described here:
https://v2.vuejs.org/v2/guide/forms.html#Radio
It is effectively the same as my example except that what I've called option it calls value. Don't be confused by the way it uses both v-model and value. While v-model binds to value and input by default it can actually be bound to any prop and event (see https://v2.vuejs.org/v2/api/#model). In the case of native checkboxes and radio buttons it binds to the checked property and the change event (mentioned in https://v2.vuejs.org/v2/guide/forms.html#Basic-Usage).
Update:
I think I may have misunderstood your question. My answer provides relevant background but it may be backwards for what you want. v-model just expands to a prop and an event, so if you want to replace v-model with something else you just need to find out which prop and event it's using and use those directly instead.

Determining if slot content is null or empty

I have a little Loading component, whose default text I want to be 'Loading...'. Good candidate for slots, so I have something like this as my template:
<p class="loading"><i class="fa fa-spinner fa-spin"></i><slot>Loading...</slot></p>
That allows me to change the loading message with e.g. <loading>Searching...</loading>. The behaviour I would like, though, is not just to display the default message if no slot content is supplied, but also if the slot content is null or blank. At the moment if I do e.g.<loading>{{loadingMessage}}</loading> and loadingMessage is null, no text is displayed (where I want the default text to be displayed). So ideally I need to test this.$slots.default. This tells me whether content was passed in, but how do I find whether or not it was empty? this.$slots.default.text returns undefined.
You'd need a computed property which checks for this.$slots. With a default slot you'd check this.$slots.default, and with a named slot just replace default with the slot name.
computed: {
slotPassed() {
return !!this.$slots.default[0].text.length
}
}
And then use it in your template:
<template>
<div>
<slot v-if="slotPassed">Loading...</slot>
<p v-else>Searching...</p>
</div>
</template>
You can see a small example here. Notice how fallback content is displayed and not "default content", which is inside the slot.
Edit:
My wording could've been better. What you need to do is check for $slots.X value, but computed property is a way to check that. You could also just write the slot check in your template:
<template>
<div>
<slot v-if="!!$slots.default[0].text">Loading...</slot>
<p v-else>Searching...</p>
</div>
</template>
Edit 2: As pointed out by #GoogleMac in the comments, checking for a slot's text property fails for renderless components (e.g. <transition>, <keep-alive>, ...), so the check they suggested is:
!!this.$slots.default && !!this.$slots.default[0]
// or..
!!(this.$slots.default || [])[0]
#kano's answer works well, but there's a gotcha: this.$slots isn't reactive, so if it starts out being false, and then becomes true, any computed property won't update.
The solution is to not rely on a computed value but instead on created and beforeUpdated (as #MathewSonke points out):
export default {
name: "YourComponentWithDynamicSlot",
data() {
return {
showFooter: false,
showHeader: false,
};
},
created() {
this.setShowSlots();
},
beforeUpdate() {
this.setShowSlots();
},
methods: {
setShowSlots() {
this.showFooter = this.$slots.footer?.[0];
this.showHeader = this.$slots.header?.[0];
},
},
};
UPDATE: Vue 3 (Composition API)
For Vue 3, it seems that the way to check whether a slot has content has changed (using the new composition API):
import { computed, defineComponent } from "vue";
export default defineComponent({
setup(_, { slots }) {
const showHeader = computed(() => !!slots.header);
return {
showHeader,
};
},
});
note: I can't find any documentation on this, so take it with a pinch of salt, but seems to work in my very limited testing.
this.$slots can be checked to see if a slot has been used.
It is important to note that this.$slots is not reactive. This could cause problems when using this.$slots in a computed value.
https://v2.vuejs.org/v2/api/?redirect=true#:~:text=Please%20note%20that%20slots%20are%20not%20reactive.
This means we need to ensure that this.slots is checked whenever the component re-renders. We can do this simply by using a method instead of a computed property.
https://v2.vuejs.org/v2/guide/computed.html?redirect=true#:~:text=In%20comparison%2C%20a%20method%20invocation%20will%20always%20run%20the%20function%20whenever%20a%20re%2Drender%20happens
<template>
<div>
<slot v-if="hasHeading" name="heading"/>
</div>
</template>
<script>
export default{
name: "some component",
methods: {
hasHeading(){ return !!this.slots.heading}
}
}
</script>

Vue.js Input value gets deleted on first char only

Based on the official example Custom Input Component example:
HTML:
<div id="app" >
<div :class="{'has-value' : hasValue}">
<input type="text"
ref="input"
v-bind:value="value"
#input="onInput" >
</div>
</div>
JS:
new Vue({
el: '#app',
data: function() {
return {
hasValue: false
}
},
props: ['value'],
methods:{
onInput: function(){
val= this.$refs.input.value
if(val && val.length > 0){
this.hasValue = true
} else {
this.hasValue = false
}
}
}
})
JS Fiddle to play
Expected:
Dynamically set hasValue data for input and bind a Css Class to this data
Unexpected:
Only after initialization and typing the first Character, the typed character gets deleted from the input. Press JS-Fiddle->Run to see it again.
After first character everything works as expected.
If I remove v-bind:value="value" or comment \\this.hasValue = true it works as expected. Binding value prop is recommended for custom Input component.
Is this intended and why? Or is this a bug I should report?
This is expected behaviour:
When the first character is inserted, hasValue is changed from false to true, causing the component to re-render
During re-render, Vue sees that the input has a value (the character you just typed in), but the property you have bound to it (the value prop) is empty.
Therefore, Vue updates the input to match the bound prop - and thus, it empties the input.
After that, hasValue doesn't change anymore, so there's no re-rendering happening, and thus, Vue doesn't reset the input field's value anymore.
So how to fix this?
First you have to understand that Vue is data-driven - the data determines the HTML, not the other way around. So if you want your input to have a value, you have to reflect that value in the property bound to it.
This would be trivial if you worked with a local data property:
https://jsfiddle.net/Linusborg/jwok2jsx/2/
But since you used a prop, and I assume you want to continue using it, the sittuation is a bit different - Vue follows a pattern of "data down - events up". You can't change props from within a child, you have to tell the parent from which you got it that you want to change it with an event. The responsibility to change it would be the parent's. Here's your example with a proper parent-child relationship:
https://jsfiddle.net/Linusborg/jwok2jsx/4/
I'm guessing that when you modified hasValue, Vue re-renders the component (to apply the has-value class to the div) which causes the v-bind:value to be re-evaluated. value is a prop which is unassigned and never changes, so the value of the input element gets cleared.
You really should use v-model on the input element and control has-value based on the value in the model, rather than interacting directly with the DOM. (You won't be able to use v-model with value though since value is a prop and cannot be assigned from within the component.)
I'm also guessing you want to make a custom input component which applies a certain style when it has a value. Try this:
Vue.component('my-input', {
props: ['value'],
template: `<input class="my-input" :class="{ 'has-value': value }" type="text" :value="value" #input="$emit('input', $event.target.value)">`,
});
new Vue({
el: '#app',
data: {
value: '',
},
});
.my-input.has-value {
background-color: yellow;
}
<script src="https://unpkg.com/vue#2.5.6/dist/vue.js"></script>
<div id="app">
<my-input v-model="value"></my-input>
</div>
Are you trying to achieve something like this?
new Vue({
el: '#app',
data: {
input: ''
},
computed: {
hasValue () {
return this.input.length ? true : false
}
}
})
.has-value {
background-color: red;
}
<div id="app">
<div :class="{'has-value' : hasValue}">
<input type="text" v-model="input">
</div>
</div>
<script src="https://unpkg.com/vue"></script>