I'd like to have a list which maps the modified values back to the description in the container. In other way, I want the container to have a prop shortcuts like:
[{action: "foo"}]
with text being editable in the component created from the list.
I've got the container:
Vue.component("shortcuts", {
data: function () {
return {
shortcuts: []
}
},
methods: {
add: function() {
this.shortcuts.push({
action: "",
})
},
},
template: '<div>\
<shortcut-entry v-for="(shortcut, index) in shortcuts" is="shortcut-entry" v-bind:key="index" v-bind="shortcut" #remove="shortcuts.splice(index, 1)"></shortcut-entry>\
<br>\
<button v-on:click=add>Add</button>\
</div>'
})
And the list element:
Vue.component("shortcut-entry", {
methods: {
clearParams: function() {
this.params = {}
},
remove: function() {
this.$emit("remove")
}
},
props: {
action: String,
},
template: '<div>\
<input type="text" v-model="action"></input>\
<button v-on:click="remove">Remove</button>\
</div>'
})
However this results in a warning:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders.
But if I use data instead of props in the shortcut-entry, the changes do not propagate into the objects in the list either.
What am I missing here? How can I bind the text entry in the item back to the object in the list in the container?
Use v-model to update the state:
Vue.component('shortcuts', {
props: {
value: Array // Of shortcuts
},
methods: {
add: function () {
const newValue = [...this.value, { action: '' }]
this.$emit('input', newValue)
},
remove: function (index) {
const newValue = [...this.value]
newValue.splice(index, 1)
this.$emit('input', newValue)
}
},
template: '<div>\
<shortcut-entry v-for="(shortcut, index) in value" is="shortcut-entry" v-bind:key="index" v-bind="shortcut" #remove="remove(index)"></shortcut-entry>\
<br>\
<button v-on:click=add>Add</button>\
</div>'
And the parent component is something like this:
Vue.component('parent-comp', {
data () {
return {
shortcuts: []
}
},
template: '<shortcut v-model="shortcuts"></shortcut>'
})
*** Use .sync ***
Vue.component('shortcuts', {
props: {
shortcuts: Array // Of shortcuts
},
methods: {
add: function () {
const newValue = [...this.shortcuts, { action: '' }]
this.$emit('update:shortcuts', newValue)
},
remove: function (index) {
const newValue = [...this.shortcuts]
newValue.splice(index, 1)
this.$emit('update:shortcuts', newValue)
}
},
template: '<div>\
<shortcut-entry v-for="(shortcut, index) in shortcuts" is="shortcut-entry" v-bind:key="index" v-bind="shortcut" #remove="remove(index)"></shortcut-entry>\
<br>\
<button v-on:click=add>Add</button>\
</div>'
})
So, the caller component:
Vue.component('parent-comp', {
data () {
return {
shortcuts: []
}
},
template: '<shortcut :shortcuts.sync="shortcuts"></shortcut>'
})
Related
I have an input in a child component and when the user starts typing in the input it updates the data in the parent component, or should. This is my code, I can post other parts if useful.
Child
<input
:keyup="updateFilters(filters[data.field.key])"
:placeholder="data.label"
/>
methods: {
updateFilters(value) {
this.$emit("input", value);
}
}
Parent
data() {
return {
filters: {
name: "",
age: "",
address: "",
},
};
},
You can change the parent from child component the same way you do emiting other events like onchange, onkeyup, onkeydown etc.
Vue.component('parent', {
data() {
return {
parentValue: ''
};
},
template: `
<div>
<label>Parent state value:</label>
{{parentValue}}
<br/><br/>
<label>input is the child component:</label>
<br/>
<child #fromChild="fromChild"></child>
</div>
`,
methods: {
fromChild(value) {
this.parentValue = value
console.log(value) // someValue
}
}
})
Vue.component('child', {
template: `
<input
v-on:keyup="updateParent($event.target.value)"
placeholder="type something"
/>
`,
methods: {
updateParent(value) {
console.log(value)
this.$emit("fromChild", value);
}
},
})
new Vue({
el: "#app",
data: {
label: 'in Vue'
},
methods: {
toggle: function(todo) {
todo.done = !todo.done
}
}
})
I've prepared a working example here.
Vue component won't re-render array items after its value was set externally. State chenges but v-for element is not showing the changes.
I have a component that renders items from array. I also have buttons to change the array length and it works well: '+' adds one line and '-' removes the last line. The problem starts when I set the array data from a fetch method. Data is displayed but '+' and '-' buttons don't work.
Here's a link to codesanbox https://codesandbox.io/s/q9jv524kvw
/App.vue
<template>
<div id="app">
<button #click="downloadTemplate">Load data</button>
<HelloWorld :formData="formData" />
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
export default {
name: "App",
components: {
HelloWorld
},
data() {
return {
fakeData: {
unloadingContactPersons: [
{
id: this.idGen("unloadingContactPersons"),
value: "123"
},
{
id: this.idGen("unloadingContactPersons"),
value: "1234"
},
{
id: this.idGen("unloadingContactPersons"),
value: "12345"
}
]
},
lengthDependentLoadings: [
"loadingDates",
"loadingAddresses",
"loadingContactPersons"
],
lengthDependentUnloadings: [
"unloadingDates",
"unloadingAddresses",
"unloadingContactPersons"
],
formData: {
unloadingContactPersons: [
{
id: this.idGen("unloadingContactPersons"),
value: ""
}
]
}
};
},
methods: {
idGen(string = "") {
// Math.random should be unique because of its seeding algorithm.
// Convert it to base 36 (numbers + letters), and grab the first 9 characters
// after the decimal.
return (
string +
"_" +
Math.random()
.toString(36)
.substr(2, 9)
);
},
addLine(id) {
console.log("id", id);
const parentName = id.split("_")[0];
const dependentArray = this.lengthDependentLoadings.includes(parentName)
? this.lengthDependentLoadings
: this.lengthDependentUnloadings;
dependentArray.forEach(objName => {
this.formData[objName]
? this.formData[objName].push({
id: this.idGen(objName),
value: ""
})
: null;
});
console.log("--length", this.formData.unloadingContactPersons.length);
},
removeLine(id) {
const parentName = id.split("_")[0];
const dependentArray = this.lengthDependentLoadings.includes(parentName)
? this.lengthDependentLoadings
: this.lengthDependentUnloadings;
dependentArray.forEach(objName => {
this.formData[objName] ? this.formData[objName].pop() : null;
});
console.log("--length", this.formData.unloadingContactPersons.length);
},
downloadTemplate(link) {
// fake fetch request
const getFunctionDummy = data =>
new Promise(resolve => setTimeout(resolve.bind(null, data), 1500));
// data setter
getFunctionDummy(this.fakeData).then(result => {
// set our data according to the template data
const templateKeys = Object.keys(result);
const templateData = result;
this.formData = {};
templateKeys.forEach((key, index) => {
let value = templateData[key];
console.log(value);
if (Array.isArray(value)) {
console.log("array", value);
this.formData[key] = value.map((item, id) => {
console.log("---from-template", item);
return {
id: this.idGen(key),
value: item.value
};
});
} else {
this.formData[key] = {
id: this.idGen(key),
value
};
}
});
});
}
},
mounted() {
// takes id number of item to be added
this.$root.$on("addLine", ({ value }) => {
console.log("---from-mounted", value);
this.addLine(value);
});
// takes id number of item to be removed
this.$root.$on("removeLine", ({ value }) => {
this.removeLine(value);
});
},
beforeDestroy() {
this.$root.$off("addLine");
this.$root.$off("removeLine");
}
};
</script>
/HelloWorld.vue
<template>
<div class="hello">
<div class="form-item">
<div class="form-item__label">
<label :for="formData.unloadingContactPersons"
>Contact person on unload:</label
>
</div>
<div class="form-item__input multiline__wrapper">
<div
class="multiline__container"
#mouseover="handleMouseOver(unloadingContactPerson.id);"
v-for="unloadingContactPerson in formData.unloadingContactPersons"
:key="unloadingContactPerson.id"
>
<span
class="hover-button hover-button__remove"
#click="removeLine(unloadingContactPerson.id);"
><i class="fas fa-minus-circle fa-lg"></i>-</span
>
<input
class="multiline__input"
:id="unloadingContactPerson.id"
type="text"
v-model="unloadingContactPerson.value"
#input="emitFormData"
/>
<span
class="hover-button hover-button__add"
#click="addLine(unloadingContactPerson.id);"
><i class="fas fa-plus-circle fa-lg"></i>+</span
>
</div>
</div>
</div>
</div>
</template>
<script>
import Datepicker from "vuejs-datepicker";
import { uk } from "vuejs-datepicker/dist/locale";
export default {
name: "SubmitForm",
components: {
Datepicker
},
props: {
formData: Object
},
data: () => {
return {
uk,
hoveredItemId: null
};
},
methods: {
emitFormData() {
this.$root.$emit("submitFormData", { value: this.formData });
},
handleMouseOver(id) {
this.hoveredItemId = id;
},
addLine(id) {
// console.log("---add", id);
this.$root.$emit("addLine", {
value: id
});
},
removeLine(id) {
// console.log("---remove", id);
this.$root.$emit("removeLine", {
value: id
});
}
}
};
</script>
Just comment line no 111 of App.vue and it will work.
// this.formData = {}
The problem is that you directly mutating formData object which Vue.js cannot detect. Read more about Array Change detection [List Rendering - Vue.js]
How to binding parent's model to child in Vue.js?
These codes below is works fine. if i fill the input manually, then child's model return it's value to the parent's model.
But the issue is, if the data set from AJAX request in a parent, the input doesn't automatically filled.
Can anyone help me on this?
Form.vue
<template>
<form-input v-model="o.name" :fieldModel="o.name" #listenChanges="o.name = $event"/>
<form-input v-model="o.address" :fieldModel="o.address" #listenChanges="o.address = $event"/>
</template>
<script>
import FormInput from '../share/FormInput.vue'
export default {
data () {
return {
o: {
name: '',
address: ''
}
}
},
components: { 'form-input': FormInput },
created: function() {
axios.get('http://api.example.com')
.then(response => {
this.o.name = response.data.name
this.o.address = response.data.address
})
.catch(e => { console.log(e) })
}
}
</script>
FormInput.vue
<template>
<input type="text" v-model='fieldModelValue' #input="forceUpper($event, fieldModel)">
</template>
<script>
export default {
props: ['fieldModel'],
data() {
return {
fieldModelValue: ''
}
},
mounted: function() {
this.fieldModelValue = this.fieldModel;
},
methods: {
forceUpper(e, m) {
const start = e.target.selectionStart;
e.target.value = e.target.value.toUpperCase();
this.fieldModelValue = e.target.value.toUpperCase();
this.$emit('listenChanges', this.fieldModelValue)
}
}
}
</script>
Things are more straightforward if you take advantage of v-model in components.
If you put v-model on a component, the component should take a prop named value, and should emit input events to trigger it to update.
I like to make a computed to hide the event emitting, and allow me to just v-model the computed inside my component.
new Vue({
el: '#app',
data: {
o: {
name: '',
address: ''
}
},
components: {
'form-input': {
template: '#form-input',
props: ['value'],
computed: {
fieldModelValue: {
get() {
return this.value;
},
set(newValue) {
this.$emit('input', newValue.toUpperCase());
}
}
}
}
},
// Simulate axios call
created: function() {
setTimeout(() => {
this.o.name = 'the name';
this.o.address = 'and address';
}, 500);
}
});
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
Name ({{o.name}})
<form-input v-model="o.name"></form-input>
Address ({{o.address}})
<form-input v-model="o.address"></form-input>
</div>
<template id="form-input">
<input type="text" v-model='fieldModelValue'>
</template>
The mounted() hook is blocking subsequent updates from the parent.
Remove mounted and change v-model to 'fieldModel'
<template>
<input type="text" :value='fieldModel' #input="forceUpper($event, fieldModel)">
</template>
<script>
export default {
props: ['fieldModel'],
data() {
return {
fieldModelValue: ''
}
},
// mounted: function() {
// this.fieldModelValue = this.fieldModel;
// },
methods: {
forceUpper(e, m) {
const start = e.target.selectionStart;
e.target.value = e.target.value.toUpperCase();
this.fieldModelValue = e.target.value.toUpperCase();
this.$emit('listenChanges', this.fieldModelValue)
}
}
}
</script>
Demo CodeSandbox
I'm currently doing the below:
<script>
export default {
computed: {
editingItem: {
get() {
return this.$store.getters['editing/editingItem'];
},
set(newValue) {
this.$store.commit('editing/UPDATE_EDITING', newValue);
}
},
editingItemName: {
get() {
return this.editingItem.name;
},
set(newValue) {
this.editingItem.name = newValue;
this.editingItem = this.editingItem;
}
}
},
}
</script>
Am I over complicating it? The second line on the editingItemName set(), is a workaround to make the editingItem set() function trigger.
Check this article. it's about forms, but it shows the way to achieve to 2-way binding with vuex.
regarding your special case, see the code. telephone is a nested property inside an object.
myModule.js
const myModule = {
state: {
customerInfo: {
name: '',
telephone: ''
}
},
getters: {
getTelephone(state) {
return state.customerInfo.telephone
}
},
mutations: {
setTelephone(state, payload) {
state.customerInfo.telephone += payload
},
}
}
export default myModule;
form.vue
<template>
<div>
<input v-model="telephone"></input>
</div>
</template>
<script>
export default {
computed: {
telephone: {
get() {
return this.$store.getters['getTelephone']
},
set(value) {
this.$store.commit('setTelephone', value)
}
},
}
}
</script>
I am trying to test if my $on function is working. I can clearly see that the the Vue console is receiving the event emit, but the pre defined call back function in $on is not being called.
Here is the code:
<template lang="html">
<div class="test">
<Filters></Filters>
<div>
<ul class="o-list c-stores">
<Result v-bind:total="filteredRestuarants.length" v-bind:open="isOpen" v-on:toggle="toggleRestaurantList"></Result>
<li v-for="(restaurant, index) in filteredRestuarants" class="c-stores__location" :class="{'first': isFirst(index), 'last': isLast(index, restaurants)}">
<Location :index="index" :store="restaurant" :link="() => setCurrentRestaurant(restaurant)"></Location>
</li>
</ul>
</div>
</div>
</template>
<script>
import eventHub from './../../event-hubs/storefinder'
import Location from './Location'
import Filters from './Filters'
import Result from './Result'
export default {
props: ["restaurants", "isOpen", "currentSearch"],
data() {
return {
attributes : [],
}
},
head: {
title: function () {
return {
inner: this.$t('storefinder.overview')
}
},
meta: function functionName() {
return [{
name: 'og:title',
content: this.$t('storefinder.overview') + ' - ' + this.$t('storefinder.name'),
id: "og-title"
},
{
name: 'description',
content: this.$t('storefinder.description'),
id: "meta-description"
},
{
name: 'og:description',
content: this.$t('storefinder.description'),
id: "og-description"
},
]
}
},
components: {
Location,
Filters,
Result
},
computed: {
filteredRestuarants(rest) {
let restaur = rest || this.restaurants;
return this.restaurants;
}
},
methods: {
startEvents(){
eventHub.$on('addFilterTheRestaurants', (attribute) => {console.log("test")});
eventHub.$on('removeFilterTheRestaurants', (attribute) => {console.log("test")});
},
toggleRestaurantList() {
eventHub.$emit('showRestaurantList');
},
setCurrentRestaurant(restaurant) {
this.trackRestaurantSelect(restaurant.publicNameSlug);
this.$router.push({
name: "store",
params: {
restaurant: restaurant.publicNameSlug
}
});
},
trackRestaurantSelect(restaurantName) {
dataLayer.push({
'event': 'GAEvent',
'eventCategory': 'restaurants',
'eventAction': 'clickResult',
'eventLabel': restaurantName,
'eventValue': undefined,
'searchTerm': this.currentSearch && this.currentSearch.toLowerCase(),
'amountSearchResults': 1
});
},
created() {
this.startEvents()
// eventHub.$on('addFilterTheRestaurants', (attribute) => this.filteredRestuarants.value = this.restaurants.forEach(rest => {console.log(rest)}));
// eventHub.$on('addFilterTheRestaurants', (attribute) => {console.log("test")});
// eventHub.$on('removeFilterTheRestaurants', (attribute) => {console.log("test")});
},
beforeDestroy () {
bus.$off('addFilterTheRestaurants')
bus.$off('removeFilterTheRestaurants')
},
isLast: function (idx, list) {
return idx === list.length - 1;
},
isFirst: function (idx) {
return idx === 0;
},
}
}
</script>
Adn its being emited here:
<template lang="html">
<div class="c-filters">
<div class="c-filters-list">
// Here I call the function to $emit my event
<div class="c-filters__item" v-for="item in filters" #click="(e) => {toggleClass(e); addFilter(item)}">
{{$t(`storefinder.store.attributes.${item}`)}}
</div>
</div>
</div>
</template>
<script>
import {getKeys} from './../../i18n/'
import eventHub from './../../event-hubs/storefinder'
import notificationHub from './../../event-hubs/notification'
import GLSApi from './../../api/gls'
export default {
data() {
return {
attributes: null,
filters : [
"WIFI",
"TABLESERVICE",
"MCCAFE",
"INDOORDINING",
"DRIVETHRU",
"BREAKFAST",
"MALEFEMALEHANDICAPACCESSIBLE",
"BABYCHANGING",
"BIRTHDAYPARTIES",
"SELFORDERKIOSK",
"OUTDOORPLAYGROUND",
"INDOORPLAYGROUND",
"JUNIORCLUB"
]
}
},
computed: {
getAttributes() {
let arr = this.attributes.map(elem => elem.storeAttributes.attribute)
return arr.map((el,index, array) => array[index].map(obj => obj.type))
}
},
methods: {
toggleClass(e){
e.target.classList.contains("add-filter") ? e.target.classList.remove("add-filter") : e.target.classList.add("add-filter") ;
},
// here it the $emit happening
addFilter (item) {
eventHub.$emit('addFilterTheRestaurants', item);
},
deleteFilter (item) {
eventHub.$emit('removeFilterTheRestaurants', item);
}
},
beforeCreate() {
eventHub.$on('attributesLoaded', (params) => this.attributes = params);
}
}
</script>
You were registering your addFilterTheRestaurants event in a computed property. However, computed properties' functions aren't called unless the computed property is referenced. Since you never reference this.startEvents, the function is never executed.
You can emit the addFilterTheRestaurants event all day, but nothing will happen because you haven't registered that event.
Just register the $on handler in the created hook. Or make startEvents a method instead of a computed property and call it in the created hook. But, make sure you are specifying the created lifecycle hook, and not a method named created.