v-model inside a renderless component - vue.js

CodeSandbox: https://codesandbox.io/s/61my3w7xrw?fontsize=14
I have this renderless component that uses a scoped slot:
name: "BlockElement",
props: {
element: {
type: Object,
required: true
}
},
data() {
return {
inputValue: this.element.value
};
},
render() {
return this.$scopedSlots.default({
inputName: this.inputName,
inputValue: this.inputValue
});
}
Using it like so:
<block-element :element="element" v-slot="{ inputName, inputValue }">
<div>
<input type="text" :name="inputName" v-model="inputValue">
<p>inputValue: {{ inputValue }}</p>
</div>
</block-element>
... so the value is not updated on change. What am I doing wrong?

In the following part of the template
<input type="text" :name="inputName" v-model="inputValue">
inputValue is the variable obtained from the v-slot and not the inputValue computed property on the <block-element> component; so if you assign to it (which is what v-model does) it won't be calling the setter, it's just setting the value of a local variable in the template code.
You could "fix" it like this:
<block-element :element="element" v-slot="{ inputName }" ref="block">
<div>
<input type="text" :name="inputName" v-model="$refs.block.inputValue">
<p>inputValue: {{ $refs.block.inputValue }}</p>
</div>
</block-element>
but this is just messy and breaks the abstraction you tried to create.
Another way is to have a inputValue setter property on the scope object that will correctly delegate the update to the component:
render() {
const self = this;
return this.$scopedSlots.default({
inputName: this.inputName,
get inputValue() { return self.inputValue },
set inputValue(value) { self.inputValue = value; },
});
}
<block-element :element="element" v-slot="scope">
<div>
<input type="text" :name="scope.inputName" v-model="scope.inputValue">
<p>inputValue: {{ scope.inputValue }}</p>
</div>
</block-element>
but this isn't ideal either because the scope object isn't typically writable, and this particular implementation detail would need to be documented.
In a situation like this where you want a scoped slot to pass data back to the parent component, you would implement this by passing a callback function to the slot. You can provide a function for setting inputValue but then you can't use v-model:
render() {
return this.$scopedSlots.default({
inputName: this.inputName,
inputValue: this.inputValue,
setInputValue: value => this.inputValue = value,
});
}
<block-element :element="element" v-slot="{ inputName, inputValue, setInputValue }">
<div>
<input type="text" :name="inputName" :value="inputValue" #input="setInputValue($event.target.value)">
<p>inputValue: {{ inputValue }}</p>
</div>
</block-element>
Now there's no confusion about what to do.

Related

Problem with watching prop changes Vue.js

What's the problem
I wanted to assign a local component variable to prop. I constantly get Vue alert Invalid watch handler specified by key "undefined". Maybe the case is that the prop is passed from another component, where I use v-model, but I don't really know. I would really appreciate your help, because my small exercise project really depends on this mechanic.
Parent Component
Here I have some HTML select, this is where I actually model my state.selectedPhysicsModule
<template>
<div>
<div>
<h1>Some header</h1>
</div>
<form class="choosePhysicsModule">
<label for="selectedPhysicsModule"></label>
<select class="select_Module" id="selectedPhysicsModule" v-model="state.selectedPhysicsModule">
<option :value="option.value" v-for="(option, index) in importedListToSelect" :key="index">
{{option.name}}
</option>
</select>
</form>
<list_of_exercises v-if="state.selectedPhysicsModule" :what_exercises="state.selectedPhysicsModule"/>
</div>
</template>
<script>
export default {
name: 'ChoosePhysicsModule',
components: {list_of_exercises},
setup() {
const state = reactive({
selectedPhysicsModule: null,
})
return {
state,
importedListToSelect
}
}
}
Child Component
</script>
export default {
name: "list_of_exercises",
props: {
whatExercises: {
type: String,
required: true
}
},
data() {
return {
exercises: this.what_exercises,
}
},
watch: {
whatExercises: function () {
this.exercises = this.whatExercises
}
}
In the parent component where you are passing the prop you need to add a setter for the prop passed. Here is an example:
<template>
<div id="app">
<label>
<input name="whatExercises" v-model="whatExercises">
</label>
<ListOfExercises v-if="whatExercises" :what_exercises="whatExercises" />
</div>
</template>
<script>
export default {
data() {
return {
whatExercises: null,
}
}
}
</script>
P.S: as a side note, I recommend using camelCase for prop names. It's more in-line with the rest of the community. If you have time feel free to check out the style guide on the official website.

Can't pass checked value to parent component from custom checkbox component

In my custom checkbox component, I'm trying to pass the value of the checkbox form field to my parent component:
<template>
<div class="custom-checkbox">
<div :class="{ 'bg-white': value }">
<input
:id="checkboxId"
type="checkbox"
:checked="value"
v-model="value"
#change="$emit('input', $event.target.checked)"
/>
</div>
<label :for="checkboxId">
<slot />
</label>
</div>
</template>
<script>
export default {
props: {
value: {
type: Boolean,
default: false
},
checkboxId: {
type: String,
default: "checkbox-1"
}
}
};
</script>
Getting this error:
[Vue warn]: Avoid mutating a prop directly since the value will be
overwritten whenever the parent component re-renders. Instead, use a
data or computed property based on the prop's value. Prop being
mutated: "value"
I tried to add:
data() {
return {
checkedValue: this.value
};
}
... then replace v-model="value" with v-model="checkedValue" but the checkbox doesn't check anymore and I still don't get the value of it in parent component.
Any suggestion?
It's because you are still directly mutating value, it does not matter if you catch the #change event or not.
Try creating a computed component with a getter/setter in your child component.
computed: {
checked: {
get() {
return this.value;
},
set(value) {
this.$emit("input", value);
}
}
}
Use checked as your checkbox v-model. No need to bind anything to :checked, only v-model will suffice.
You can pass the value using v-model to this component in the parent.
For reference: https://v2.vuejs.org/v2/guide/forms.html
For the record.
CustomCheckbox.vue
<template>
<input type="checkbox" :checked="value" #change="$emit('input', $event.target.checked)">
</template>
<script>
export default {
props: ["value"]
};
</script>
Parent.vue
<template>
<div id="app">
<custom-checkbox v-model="checked"></custom-checkbox>
</div>
</template>
<script>
import CustomCheckbox from "./components/CustomCheckbox";
export default {
data: {
checked: true
},
components: {
CustomCheckbox
}
};
</script>

Avoid mutating a prop : vuejs 2

Here what appears in the console when I run my code:"Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "isChecked"".
I have seen what other posts say about it but i can't adapt it on my problem.
Could someone explain it to me please?
PARENT:
template:
<div class="checkBox-container">
<input type="checkbox"/>
<!-- <div class="check" :class="[size]" v-if="!isChecked"></div>
<div class="check" :class="[size]" v-else>
<div>v</div>
</div> -->
<div class="check" :class="[size]" #click="changeVal">
<div v-if="isChecked">v</div>
</div>
<div><label class="label">{{label}}</label></div>
<div><span class="subLabel">{{subLabel}}</span></div>
script:
export default {
name: "ax-checkbox",
props: {
label: String,
subLabel: String,
size: String,
isChecked: false,
checks: []
},
methods: {
changeVal() {
this.isChecked = !this.isChecked;
this.$emit("changeVal");
}
}
};
CHILD
<div class="filters">
<ax-checkbox label="Où :" subLabel="Ville" size="small"></ax-checkbox>
<div class="separator"></div>
<ax-checkbox label="Quoi :" subLabel="Thématique(s)" size="small"></ax-checkbox>
<div class="separator"></div>
<ax-checkbox label="Quand :" subLabel="Dans..." size="small"></ax-checkbox>
</div>
The prop you are mutating is isChecked.
So, create a local data variable (initialized it with isChecked) and mutate it instead:
export default {
name: "ax-checkbox",
props: {
label: String,
subLabel: String,
size: String,
isChecked: false,
checks: []
},
data() {
return {isCheckedInternal: this.isChecked}
},
methods: {
changeVal() {
this.isCheckedInternal = !this.isCheckedInternal;
this.$emit("changeVal");
}
}
};
And replace it in the template:
<div class="checkBox-container">
<input type="checkbox"/>
<!-- <div class="check" :class="[size]" v-if="!isCheckedInternal"></div>
<div class="check" :class="[size]" v-else>
<div>v</div>
</div> -->
<div class="check" :class="[size]" #click="changeVal">
<div v-if="isCheckedInternal">v</div>
</div>
<div><label class="label">{{label}}</label></div>
<div><span class="subLabel">{{subLabel}}</span></div>
Note: The code above will use the prop isChecked only as initializer. If the parent changes in any way the value it passed to isChecked, the child component will not pick that change up. If you want to pick it up, add a watch in addition to the proposed code above:
//...
watch: {
isChecked(newIsChecked) {
this.isCheckedInternal = newIsChecked;
}
}
};
Ideal
There are some possible improvements to your code. Here are some suggestions:
subLabel prop should be sub-label
instead of emitting a changeVal value, emit update:isChecked and then you can use :is-checked.sync="myCheckedValue" in the parent.
This way you can still bind internally to the prop isChecked and not change it, but emit events and react to when the parent changes isChecked instead.
If you wanted to go the extra mile (and think it is worth it), you could also add a model option to your component, so you can be able to use v-model instead of :is-checked.sync.
See demo below.
Vue.component("ax-checkbox", {
template: '#axCheckboxTemplate',
props: {
label: String,
subLabel: String,
size: String,
isChecked: false,
checks: []
},
model: { // <== this part to will also enable v-model besides :is-checked.async
prop: 'isChecked',
event: 'update:isChecked'
},
methods: {
updateIsChecked() {
this.$emit("update:isChecked", !this.isChecked);
}
}
})
new Vue({
el: '#app',
data: {
myCheckedValueSync: false, myCheckedValueVModel: false,
}
});
<script src="https://unpkg.com/vue#2.5.13/dist/vue.js"></script>
<template id="axCheckboxTemplate">
<div class="checkBox-container">
<input type="checkbox" :checked="isChecked" #change="updateIsChecked" />
<div class="check" :class="[size]" #click="updateIsChecked">
CLICK ME<div v-if="isChecked">v</div>
</div>
<label class="label">{{label}}</label><span class="subLabel">{{subLabel}}</span>
</div>
</template>
<div id="app">
<div class="filters">
<pre>parent's myCheckedValueSync: {{ myCheckedValueSync }}</pre>
<ax-checkbox label="Où :" sub-label="Ville" size="small" :is-checked.sync="myCheckedValueSync">
</ax-checkbox>
<pre>parent's myCheckedValueVModel: {{ myCheckedValueVModel }}</pre>
<ax-checkbox label="Quoi :" sub-label="Thématique(s)" size="small" v-model="myCheckedValueVModel">
</ax-checkbox>
</div>
</div>
As it appears you're trying to update the value of a property passed into your component, your component is in fact a custom input component. Take a look at the excellent Vue docs on this topic. I've summarised the idea below.
2-way databinding in Vue is handled by v-model. Applied to your isChecked property this comes down to v-model="isChecked", which is in fact syntactic sugar for :value="isChecked" #input="evt => isChecked = evt.target.value".
Thus for your component, you need to do the following:
Update the name of the isChecked property to value
In your changeVal method, emit an input event, like:
changeVal() { this.$emit("input", !this.value); }
If need be, you could still also emit the changeVal event.

Extend Vue function in component to display ID

I have a Vue component and I am using internalValue to access the value attribute. How would I extend this to also get the ID?
ie internalValue = value, id
I have tried but I don't know how to add this inside the internalValue function. I've even tried to only get the ID by changing all instances of value to id but it still spits out the value.
I'd be happy to have them as one ie value, id or access them like data.value and data.id
Initialise Vue
new Vue({
el: '#topic',
data: {
selectedTopic: null
}
});
Use Component
<div class="form-group" id="topic">
<topic v-model="selectedTopic"></topic>
</div>
Register Component
Vue.component('topic', require('./components/Topicselect.vue'));
Component
<template>
<div>
<label v-for="topic in topics" class="radio-inline radio-thumbnail">
<input type="radio" v-model="internalValue" name="topics_radio" :id="topic.id" :value="topic.name">
<span class="white-color lg-text font-regular text-center text-capitalize">{{ topic.name }}</span>
</label>
<ul class="hidden">
<li>{{ internalValue }}</li>
</ul>
</div>
</template>
<script>
export default {
props: ['value'],
data () {
return {
internalValue: this.value,
topics: []
}
},
mounted(){
axios.get('/vuetopics').then(response => this.topics = response.data);
},
watch: {
internalValue(v){
this.$emit('input', v);
console.log('Topicselect: the value is ' + this.internalValue);
}
}
}
</script>
Use the selected topic as your value. Basically, eliminate internalValue altogether, and just emit the topic associated with any given radio button when it's clicked. This will satisfy v-model, since it listens to input events (unless you customize it).
export default {
props: ['value'],
data () {
return {
topics: []
}
},
methods:{
selectValue(topic){
this.$emit('input', topic)
}
},
mounted(){
axios.get('/vuetopics').then(response => this.topics = response.data);
}
})
And your template
<template>
<div>
<label v-for="topic in topics" class="radio-inline radio-thumbnail">
<input type="radio" #click="selectValue(topic)" name="topics_radio" :id="topic.id" :value="topic.name" :checked="value && topic.id == value.id">
<span class="white-color lg-text font-regular text-center text-capitalize">{{ topic.name }}</span>
</label>
<ul class="hidden">
<li>{{ value }}</li>
</ul>
</div>
</template>
This will set selectedTopic in your Vue to a topic, which is something like
{
id: 2,
name: "some topic"
}
based on how you use it in your template.
Working example.

How to pass dynamic data to a component?

I'm new to Vue, so this might be dumb. In the main component I have this:
components: [FormSelect],
data() {
return {
listing: {
country: ''
}
}
}
I want to include a component:
<form-select
name="country"
model="listing"
:onchange="onCountryChange"
:options="{{ ListingStatic::getCountries()->toSelect()->toJson() }}"
label="{{ trans('listing::form.country') }}"
placeholder="{{ trans('listing::form.choose_country') }}">
</form-select>
Which is specified as:
<template>
<div class="col-md-6 form-group" :class="{'has-error': getModel.errors.has(getName)}" >
<label for="" class="control-label">{{ label }}</label>
<v-select
:value.sync="getModel.fields.getName"
:on-change="onchange"
:options="options"
placeholder="placeholder">
</v-select>
<span class="help-block" v-show="getModel.errors.has(getName)">
#{{ getModel.errors.get(getName) }}
</span>
</div>
</template>
<script type="text/ecmascript">
module.exports = {
name: 'FormSelect',
props: ['label', 'placeholder', 'options', 'model', 'onchange'],
computed: {
getModel() {
console.log(this.model)
return this.model;
},
getName() {
return this.name;
}
}
};
</script>
Basically I want to generalize select field by wrapping it into a component
Should I use computed properties to get correct model and key? How can I use getModel and getName, so it would return based on properties passed in?