passing object of handlers to v-on and accessing methods - vue.js

I have a v-for loop in which I build up a row of buttons from my data. I also dynamically bind handlers to them and here is the part I am struggling with.
<component
is="button.component"
v-for="(button, index) in group"
:key="index"
v-on="button.handlers"
>
Text
</component>
As you can see, I pass button.handlers to the v-on function. My data looks like this:
data: {
group: [
{
component: BButton
value: 'foo',
handlers: { click: 'update(button.value)' }
},
{
component: CustomFilePickerButton
value: 'bar',
handlers: { change: 'uploadFile($event.files)' }
}
]
}
Vue complains that the string 'update(value)' is not a function but I dont know how I can get the equivalent to v-on:click="update(button.value)" which would work in the template.
Any ideas on how to solve this?

The use of v-on with object introduces limitations, 'update(button.value)' string cannot be compiled and make use of local button.value value because it's defined outside the template.
It would be possible to provide this functionality with custom directive similar to v-on.
Otherwise all necessary data needs to be provided to handlers with helper function:
<button
v-for="(button, index) in group"
:key="index"
v-on="mapHandlers(button, { value: button.value })"
>
...
methods: {
mapHandlers(button, data) {
let remappedHandlers = {};
for (let eventName in button.handlers) {
let handler = button.handlers[eventName];
remappedHandlers[eventName] = e => handler(e, data);
}
return remappedHandlers;
},
update(e, { value }) { ... }
},
data () {
return {
group: [
{
value: 'foo',
handlers: { click: this.update }
}
]
}
}

Related

VueJS: how to trigger 'change' on <input> changed programmatically

I'm going to build a customized virtual keyboard, so that's the first problem I've encountered.
I have an input element, whose value is changed from outside, in my case by pressing a button. The problem is that there seems to be no way to trigger the normal 'change' event.
Neither clicking outside the input, nor pressing Enter gives any result. What might be the correct way of solving this problem?
<template>
<div class="app-input">
<input #change="onChange" type="text" v-model="value" ref="input">
<button #click="onClick">A</button>
</div>
</template>
<script>
export default {
name: "AppInput",
data() {
return {
inputDiv: null,
value: ""
};
},
props: {
msg: String
},
methods: {
onClick() {
this.value = this.value + "A";
this.inputDiv.focus();
},
onChange() {
alert("changed");
}
},
mounted() {
this.$nextTick(() => {
this.inputDiv = this.$refs.input;
});
}
};
</script>
The whole pen can be found here.
v-on:change would only trigger on a direct change on the input element from a user action.
What you are looking for is a wathcer for your data property, whenever your value changes, watcher will execute your desired function or task.
watch: {
value: function() {
this.onChange();
}
}
The watch syntax is elaborated on the provided official vuejs docs link. use your data property as the key and provide a function as a value.
Check the snippet.
export default {
name: "AppInput",
data() {
return {
inputDiv: null,
value: ""
};
},
props: {
msg: String
},
methods: {
onClick() {
this.value = this.value + "A";
this.inputDiv.focus();
},
onChange() {
alert("changed");
}
},
// this one:
watch: {
value: function() {
this.onChange();
}
},
// --- rest of your code;
mounted() {
this.$nextTick(() => {
this.inputDiv = this.$refs.input;
});
}
};
When I build any new vue application, I like to use these events for a search input or for other inputs where I don't want to fire any functions on #change
<div class="class">
<input v-model="searchText" #keyup.esc="clearAll()" #keyup.enter="getData()" autofocus type="text" placeholder="Start Typing ..."/>
<button #click="getData()"><i class="fas fa-search fa-lg"></i></button>
</div>
These will provide a better user experience in my opinion.

Computed properties with v-model by index

Let's say I have the following vuex store...
state: {
someObj: {
someList: [
{ key:'a', someSubList: [] },
{ key:'b', someSubList: [] },
{ key:'c', someSubList: [] },
]
}
}
How would I bind a separate v-model to each someSubList? As an example, after I check some checkboxes, I would expect to see some Ids be populated into the someSubList like this:
someList: [
{ key:'a', someSubList: [1, 13, 17, 19] },
{ key:'b', someSubList: [1, 2, 3, 4] },
{ key:'c', someSubList: [4, 16, 20] },
]
In other words, If I check a checkbox an associated id would be added to someSubList. If I uncheck the box, the id associated with that checkbox would be removed from the someSubList. Keep in mind that each someList has a different someSubList.
I'm thinking it would be similar to below, but I'm not sure what to use for the v-model param and how to pass the index to the set method
ex.
<span v-for="(someListRow.someSubList, index2) in someList" v-bind:key="index2">
<v-checkbox v-model="myModel" />
</span>
computed: {
someList: {
get() {
return this.$store.state.someObj.someList;
},
set(value) {
this.$store.commit('someCommit', value)
}
}
}
UPDATE:
For anyone interested I got it solved using the tips provided in the posts below and ended up doing this:
<v-checkbox #change="myChangeMethod($event, myObj)" label="MyLabel"
:input-value="isMyObjSelected(myObj)" />
myChangeMethod(event, myObj) {
if (event) {
this.$store.commit('AddToMyList', {myObj});
} else {
this.$store.commit('RemoveFromMyList', {myObj});
}
}
isMyObjSelected(myObj){
this.$store.getters.isMyObjSelected(myObj});
}
I believe you want to map your inputs to some value in your store?
For this to work you cannot use v-model. Instead work with a input="updateStore($event, 'pathToStoreField')" (or #change="...") listener and a :value="..." binding. In case of a checkbox you need to use :checked="..." instead of value.
For example:
<input type="checkbox" :checked="isChecked" #input="updateField($event.target.checked, 'form.field')">
...
computed:
...
isChecked() {
return this.$store.state.form.field;
},
...
methods: {
...
updateField(value, path) {
const options = { path, value };
this.$store.commit('setFieldByPath', options);
},
...
},
Then in your store you will need a mutation setFieldByPath that resolves the string-path to a property in the state object (state.form.field) and sets this property to value.
You can also place the updateField() method as setter of a computer property.
There is library that makes this a bit more convenient: https://github.com/maoberlehner/vuex-map-fields
Just look out for checkboxes: to set them checked the checked property needs to be true not the value property.
I think, unsure but Computed values usually become like a local variable available, you need to v-model what other variable is being posted through form, so if myModel is the variable passed through then set the value of the setter once received:
<span v-for="(myList, index2) in someList" v-bind:key="index2">
<v-checkbox v-model="myModel" />
</span>
Then in script
computed: {
myList() {
return this.$store.state.someObj.someList;
}
}
I guess u ask for this:
<span v-for="(someSubList, index2) in someList" v-bind:key="index2">
<v-checkbox v-model="myModel" />
</span>
data() {
return {
myModel: ''
}
},
computed: {
someList: {
get() {
return this.$store.state.someObj.someList;
},
set(value) {
this.$store.commit('someCommit', value)
}
}
}
properties inside data() can be used to bind your v-model's on your template.
For anyone interested I got it solved using the tips provided in the posts below and ended up doing this:
<v-checkbox #change="myChangeMethod($event, myObj)" label="MyLabel"
:input-value="isMyObjSelected(myObj)" />
myChangeMethod(event, myObj) {
if (event) {
this.$store.commit('AddToMyList', {myObj});
} else {
this.$store.commit('RemoveFromMyList', {myObj});
}
}
isMyObjSelected(myObj){
this.$store.getters.isMyObjSelected(myObj});
}

Replace tag dynamically returns the object instead of the contents

I'm building an chat client and I want to scan the messages for a specific tag, in this case [item:42]
I'm passing the messages one by one to the following component:
<script>
import ChatItem from './ChatItem'
export default {
props :[
'chat'
],
name: 'chat-parser',
data() {
return {
testData: []
}
},
methods : {
parseMessage(msg, createElement){
const regex = /(?:\[\[item:([0-9]+)\]\])+/gm;
let m;
while ((m = regex.exec(msg)) !== null) {
msg = msg.replace(m[0],
createElement(ChatItem, {
props : {
"id" : m[1],
},
}))
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
}
return msg
},
},
render(createElement) {
let user = "";
let msg = this.parseMessage(this.$props.chat.Message, createElement)
return createElement(
'div',
{
},
[
// "hello",// createElement("render function")
createElement('span', '['+ this.$props.chat.Time+'] '),
user,
msg,
]
)
}
};
</script>
I thought passing createElement to the parseMessage method would be a good idea, but it itsn't working properly as it replaces the tag with [object object]
The chatItem looks like this :
<template>
<div>
<span v-model="item">chatITem : {{ id }}</span>
</div>
</template>
<script>
export default {
data: function () {
return {
item : [],
}
},
props :['id'],
created() {
// this.getItem()
},
methods: {
getItem: function(){
obj.item = ["id" : "42", "name": "some name"]
},
},
}
</script>
Example :
if the message looks like this : what about [item:42] OR [item:24] both need to be replaced with the chatItem component
While you can do it using a render function that isn't really necessary if you just parse the text into a format that can be consumed by the template.
In this case I've kept the parser very primitive. It yields an array of values. If a value is a string then the template just dumps it out. If the value is a number it's assumed to be the number pulled out of [item:24] and passed to a <chat-item>. I've used a dummy version of <chat-item> that just outputs the number in a <strong> tag.
new Vue({
el: '#app',
components: {
ChatItem: {
props: ['id'],
template: '<strong>{{ id }}</strong>'
}
},
data () {
return {
text: 'Some text with [item:24] and [item:42]'
}
},
computed: {
richText () {
const text = this.text
// The parentheses ensure that split doesn't throw anything away
const re = /(\[item:\d+\])/g
// The filter gets rid of any empty strings
const parts = text.split(re).filter(item => item)
return parts.map(part => {
if (part.match(re)) {
// This just converts '[item:24]' to the number 24
return +part.slice(6, -1)
}
return part
})
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<template v-for="part in richText">
<chat-item v-if="typeof part === 'number'" :id="part"></chat-item>
<template v-else>{{ part }}</template>
</template>
</div>
If I were going to do it with a render function I'd do it pretty much the same way, just replacing the template with a render function.
If the text parsing requirements were a little more complicated then I wouldn't just return strings and numbers. Instead I'd use objects to describe each part. The core ideas remain the same though.

How to fix Warning: `getFieldDecorator` will override `value`,so please don't set `value and v-model` directly and use `setFieldsValue` to set it.?

I'm coding a custom validation form component using ant-design-vue
I have changed my code nearly same as the example showed on the official website, but still got warning, the only difference is the example use template to define child component, but I use single vue file
//parent component
...some other code
<a-form-item
label="account"
>
<ReceiverAccount
v-decorator="[
'receiverAccount',
{
initialValue: step.receiverAccount,
rules: [
{
required: true,
message: 'need account',
}
]
}
]"
/>
</a-form-item>
...some other code
//child component
<template>
<a-input-group compact>
<a-select
:value="type"
#change="handleTypeChange"
>
<a-select-option value="alipay">alipay</a-select-option>
<a-select-option value="bank">bank</a-select-option>
</a-select>
<a-input
:value="number"
#change="handleNumberChange"
/>
</a-input-group>
</template>
<script>
export default {
props: {
value: {
type: Object,
default: () => {}
}
},
data() {
const { type, number } = this.value
return {
type: type || 'alipay',
number: number || ''
}
},
watch: {
value(val = {}) {
this.type = val.type || 'alipay'
this.number = val.number || ''
}
},
methods: {
handleTypeChange(val) {
this.triggerChange({ val })
},
handleNumberChange(e) {
const number = parseInt(e.target.value || 0, 10)
if (isNaN(number)) {
return
}
this.triggerChange({ number })
},
triggerChange(changedValue) {
this.$emit('change', Object.assign({}, this.$data, changedValue))
}
}
}
</script>
I expect everything is fine, but the actual is I got 'Warning: getFieldDecorator will override value, so please don't set value and v-model directly and use setFieldsValue to set it.'
How can I fix it? Thanks in advance
because I am new of ant-design-vue, after one day research, solution is change :value to v-model and remove value props in the child component
<template>
<a-input-group compact>
<a-select
v-model="type"
#change="handleTypeChange"
>
<a-select-option value="alipay">alipay</a-select-option>
<a-select-option value="bank">bank</a-select-option>
</a-select>
<a-input
v-model="number"
#change="handleNumberChange"
/>
</a-input-group>
</template>

Expected array got function. Passing function into component vuejs

I am trying to pass a function into my component and I keep getting this error back. "Invalid prop: type check failed for prop "form_type". Expected Array, got Function." My function returns an array so I am a little lost on how to fix this.
The function I am referencing is selectedType & the component in question is ChildTab
<template>
<div class="row">
<q-field
label="Contact Type"
:labelWidth="3"
error-label="Please select a contact type"
:error="!!failed_validations.contact_type"
>
<q-select v-model="contact_type" :options="contact_types"/>
</q-field>
</div>
<ChildTabs
:form_type="selectedType"
/>
<q-field class="float-right">
<q-btn color="faded" v-on:click="goBack()">Cancel</q-btn>
<q-btn color="green-6" v-on:click="selectedType()">Submit</q-btn>
</q-field>
</div>
</div>
</template>
<script>
'use strict';
import ChildTabs from '../tabs';
export default {
name: 'contacts-view',
data: function () {
return {
contact_type: '',
contact_types: [
{
label: 'Pregnancy',
value: 'pregnancy',
form_type: [
'BreastFeeding',
'Pregnancy'
]
},
{
label: 'Post Partum (Includes Birth)',
value: 'postpartum',
form_type: [
'Birth',
'BreastFeeding',
'PostPartum'
]
},
{
label: '1 - 2 Month',
value: '1_2_months',
form_type: [
'BreastFeeding',
'DuoMonths'
]
},
{
label: '6 Month',
value: '6_months',
form_type: [
'SixMonth'
]
}
],
}
},
props: {
},
computed: {
selectedType: function ()
{
var values = this.contact_types.map(function(o) { return o.value });
var index = values.indexOf(this.contact_type);
this.selectedForms = this.contact_types[index].form_type
// console.log(this.selectedForms);
return this.selectedForms;
}
},
methods: {
},
created: function () {
this.selectedType();
},
components: {
ChildTabs
}
}
</script>
As you try to call selectedType on click "Submit", maybe you should call it as a method.
Inside selectedType you bind a selectedForms property. Why don't you just initialize this property inside data as an empty array and pass it as a props of your ChildTabs component ?
<template>
<div class="row">
<ChildTabs :form_type="selectedForms" />
</div>
</template>
export default {
name: 'contacts-view',
data: function () {
return {
selectedForms: [],
// ...
}
},
methods: {
selectedType() {
var values = this.contact_types.map(function(o) { return o.value });
var index = values.indexOf(this.contact_type);
this.selectedForms = this.contact_types[index].form_type
}
},
//...
}
Fiddle example
What you bind as a prop in a component goes as same in the component. So as you're referencing selectedType in your ChildTabs component - the method selectedType will be received by ChildTabs as a prop. So either you edit your propType in ChildTabs component and invoke that passed method as needed or you call the selectedType method on the fly when passed in as a prop like
<ChildTabs :form_type="selectedType()" />
This will call that method then and will bind the resulting array as prop