I have the following code snippet from my app component:
<template>
<div>
<h3>Basic</h3>
<div v-for="(field, index) in basics" :key="index">
<input v-model="basics.name" placeholder="Name" type="text">
<br>
<br>
<input v-model="basics.email" placeholder="Email" type="email">
<br>
<hr/>
<button #click.prevent="addField">Add</button>
<button #click.prevent="removeField(index)">Remove</button>
<br>
<button #click.prevent="back">Back</button>
<button #click.prevent="toNext">Next</button>
</div>
</div>
</template>
<script>
import { mapActions } from "vuex";
export default {
name: "Basics",
data() {
return {
basics: [{
name: "",
email: ""
}]
};
},
methods: {
...mapActions(["addBasicData"]),
addFied(){
this.basics.push({
name: "",
email: ""
});
},
removeField(index){
this.basics.splice(index, 1);
},
toNext() {
this.addBasicData(this.basics);
this.$router.push({ name: "Location" });
},
back() {
this.$router.back();
}
}
};
</script>
In the code above when I finish filling up the form and click next button the data is sent to the state and we are guided to another route named "Location".
When I click back button in the "Location" route I'm back to route named "Basic".
The issue here is when I'm brought back to the route named "Basic" the form fields are empty although they are binded with the data object.
How do I populate these input fields when I return back to same route ?
Here is the working replica of the app: codesandbox
<div v-for="(field, index) in basics" :key="index">
<input v-model="basic.name" placeholder="Name" type="text">
<input v-model="basic.email" placeholder="Email" type="email">
<button #click.prevent="removeField(index)">Remove</button>
</div>
<hr/>
<button #click.prevent="addField">Add</button>
<br>
<button #click.prevent="back">Back</button>
<button #click.prevent="toNext">Next</button>
methods: {
addField() {
this.$store.commit('addBasic',{name:"",email:""} )
},
removeField(index) {
this.$store.commit('removeField',index )
},
toNext() {
this.$router.push({ name: "Location" });
}
},
computed: {
basic:{
get() {
return this.$store.getters.getBasic;
}
}
}
store.js
// ...
state: {
basic:[{name:"Jonny",email:"jonny#mail.com"},
{name:"Bonny",email:"Bonny#mail.com"}]
}
mutations: {
addBasic(state,value) {
state.basic.push(value)
},
removeField(state,index ){
state.basic.splice(index,1);
}
}
Thats just one of two versions how you can do it.
Or you can map the mutatations and call them directly in the click event.
https://vuex.vuejs.org/guide/mutations.html
https://vuex.vuejs.org/guide/forms.html
The add field button makes only sense outside of the loop.
addBasicData you dont need it
This method somehow works:
mounted() {
// eslint-disable-next-line no-unused-vars
let fromState = this.$store.state.Basics.basics;
if (fromState) {
this.basics.name = fromState.name;
this.basics.email = fromState.email;
}
}
I will really appreciate if there are any other convenient method to achieve this.
Tried mapState but didn't work
Related
I have a form with two datepickers and a button to clear each one of them as follows:
<template>
<form>
<div>
<datetime id="someDate" v-model="fields.some_date"></datetime>
<button #click.prevent="clearSomeDate()">X</button>
</div>
<div>
<datetime id="anotherDate" v-model="fields.another_date"></datetime>
<button #click.prevent="clearAnotherDate()">X</button>
</div>
</form>
</template>
<script>
export default {
data() {
return {
fields: {
some_date: null,
another_date: null
},
};
},
methods: {
clearSomeDate() {
this.fields.some_date = null;
},
clearAnotherDate() {
this.fields.another_date = null;
},
},
}
</script>
And works pretty well, but it's not so much reusable.
Is there a way to achieve this with a single clearField() function and pass the model as a parameter or something? Should I do my own custom component to make it work?
You could completely get rid of the methods by just doing the assignment directly in the template:
<div>
<datetime id="someDate" v-model="fields.some_date"></datetime>
<button #click.prevent="fields.some_date = null">X</button>
</div>
That way you have the clearing logic directly next to the model.
If you want to make it reusable you could also extract it into a separate component:
<template>
<div>
<datetime v-bind="$attrs" :value="value" v-on="eventHandlers"></datetime>
<button #click.prevent="$emit('input', null)">X</button>
</div>
</template>
<script>
export default {
name: "datetime-with-x",
model: { prop: "value", event: "input" },
props: ["value"],
inheritAttrs: false,
computed: {
eventHandlers() {
return {
...this.$listeners,
input: ev => this.$emit('input', ev)
};
}
}
};
</script>
and then use it in your component like this:
<template>
<form>
<datetime-with-x id="someDate" v-model="fields.some_date" />
<datetime-with-x id="anotherDate" v-model="fields.another_date" />
</form>
</template>
<script>
import DatetimeWithX from "./datetime-with-x";
export default {
name: "your-form",
components: { DatetimeWithX },
data() {
return {
fields: {
some_date: null,
another_date: null
}
};
}
};
</script>
You could pass the field property name as parameter
clearField(name) {
this.fields[name] = null;
}
and call it with argument
<button #click.prevent="clearField('some_date')">X</button>
Though a cleaner approach would be to build another reusable component and bind it with v-model
<div>
<datetime :value="value" #change="$emit('input', $event)"></datetime>
<button #click.prevent="$emit('input', null)">X</button>
</div>
I have the following UI:
I will try to explain what is going on here. I have "Add message" button. When I click on the button I have new form with message: title, body, image, language (just multiple select via this plugin). I have clicked twice - I have 2 messages. Simple.
I don't use vue router. Implementation of my routing is with the help of backend. It means that for each route I have new vuex state.
I'm going to keep my messages in vuex, but it's impossible to use v-model for this case.
So, I will show my code.
store:
export const store = new Vuex.Store({
state: {
messages: [],
// more props are here ...
},
mutations: {
setMessages(state, messages) {
state.messages = messages;
},
// more setters are here
},
getters: {
getMessages: state => {
return state.messages;
},
// more getters are here
},
actions: {
updateMessagesAction: function({commit}, value) {
console.log(value)
},
}
});
Messages component:
<template>
<div>
<button class="btn btn-outline-info" #click.prevent="createNewMessage">
<i class="fa fa-language"/> Add message
</button>
<div>
// now it works with local state, but I need to work with vuex
<div v-for="(message, index) in messages">
<button class="btn btm-sm btn-danger" #click="deleteMessage(index, message)"><i class="fa fa-remove"/>
</button>
<b-collapse collapsed :id="`collapse-${index}`">
<form>
<div class="form-group">
<label class="typo__label">Languages</label>
<multiselect
v-model="message.languages"
:options="getLanguagesOptions"
:multiple="true"
:close-on-select="true"
:clear-on-select="false"
:preserve-search="true"
placeholder="Languages"
label="name"
track-by="id"
>
</multiselect>
</div>
<div class="form-group">
<label for="title" class="typo__label">Title</label>
<input type="text" id="title" class="form-control" autocomplete="off" ??? how to bind it to vuex ???? I dont understand :(((>
</div>
<div class="form-group">
<label for="text" class="typo__label">Body</label>
<textarea class="form-control" id="text" ??? how to bind it to vuex ???? I dont understand :(((/>
</div>
<div class="form-group">
<div id="upload-image">
<div v-if="!message.imageSrc">
<h2>Image</h2>
<input type="file" ref="file" #change="onFileChange($event, message)">
</div>
<div v-else>
<img :src="message.imageSrc"/>
<button #click.prevent="removeImage($event, message)">Remove</button>
</div>
</div>
</div>
<hr class="mb-2">
</form>
</b-collapse>
</div>
</div>
</div>
</template>
<script>
// imports
export default {
async created() {
// set languages from servert to vuex
let res = (await axios.post(this.urlForGettingLanguagesFromServerProp)).data;
this.$store.commit('setLanguagesOptions', res);
},
name: "MessagesComponent",
props: {
urlForGettingLanguagesFromServerProp: String,
uploadImageUrl: String,
deleteImageUrl: String,
selectedLanguagesIdsProp: {
type: Array,
default: () => []
},
},
methods: {
...mapMutations(['setLanguagesSelected']),
...mapActions(['updateMessagesAction']),
createNewMessage: function () {
let message = {
languages: [],
languagesIds: [],
title: "",
text: "",
imageSrc: "",
imageDbId: 0
};
this.messages.push(message);
},
deleteMessage: function (index, message) {
this.removeImage("", message);
this.messages.splice(index, 1);
},
onFileChange: async function (e, message) {
// this method add send image on server and save to state db image id ant path
},
removeImage: function (event, message) {
// remove image from server
}
},
computed: {
...mapGetters(['getLanguagesOptions', 'getMessages'])
},
watch: {
messages: {
deep: true,
immediate: true,
handler(val, oldVal) {
let message = JSON.parse(JSON.stringify(val));
this.$store.commit("setMessages", message);
}
}
},
data() {
return {
messages: [],
}
}
}
</script>
I call this component in a parent component. In the parent component I initiate vuex during update operation.
As you can see this component works with local state and synchronize local state with vuex. It's ok for Create operation. I can send messages to vuex, then I can take it in the parent component with other information and send it on the server. But what to do with Update? I have data from the server in the parent component but local state, of course, is empty for the Messages component. How to bind all messages to vuex and have ability to change each message separately? I mean, for example, to change title of the first message and to have it in vuex immediately?
Please, help me improve this component.
Background- I have created an app where the parent component can create and delete a input field (child component) on a click of a button. The input's value is recorded on screen through v-model
Problem- When a new input is created the previous value is replaced by the new input value.
Expectation- When a new input is created it adds the value of the previous input value
A visual for more clarity
https://i.stack.imgur.com/Fp8Mk.png
Parent Component
<form-input v-for="n in count" :key="n" :value="expense" #input="expense = $event"></form-input>
<button #click="addInputs">Add Expense</button>
<button #click="deleteInputs">Delete</button>
<p>Total Expense: {{ expense }}</p>
export default {
components: {
"form-input": formInput
},
name: "form",
data() {
return {
count: 0,
expense: 0
};
},
methods: {
addInputs: function() {
this.count++;
},
deleteInputs: function() {
this.count--;
}
}
};
Child Component
<input type="text" placeholder="Expense" />
<input type="number" placeholder="Amount" #input="$emit('input', $event.target.value)" />
Here, I made a sandbox for you to see my solution for this as there are a lot of changes and you can see how it performs.
https://codesandbox.io/s/confident-fire-kpwpp
The main points are:
You have to keep track of the values of each input separately. This is done using an array.
When this array is changed we recalculate the total expense
Parent
<template>
<div>
<form-input v-for="(n, idx) in count" :key="n" :id="idx" #input="getExpense"></form-input>
<button #click="addInputs">Add Expense</button>
<button #click="deleteInputs">Delete</button>
<p>Total Expense: {{ totalExpense }}</p>
</div>
</template>
<script>
import HelloWorld from "./components/HelloWorld";
export default {
components: {
"form-input": HelloWorld
},
name: "form",
data() {
return {
count: 0,
expenses: [],
totalExpense: 0
};
},
methods: {
addInputs: function() {
this.count++;
this.expenses[this.count - 1] = 0;
},
deleteInputs: function() {
this.count--;
this.expenses.pop();
this.setTotalExpense();
},
getExpense(data) {
this.expenses[data.id] = parseInt(data.value, 10) || 0;
this.setTotalExpense();
},
setTotalExpense() {
console.log(this.expenses);
this.totalExpense = this.expenses.reduce((sum, val) => {
return sum + val;
}, 0);
}
}
};
</script>
Child
<template>
<div class="hello">
<input type="text" placeholder="Expense">
<input
type="number"
placeholder="Amount"
#input="$emit('input', {
value: $event.target.value,
id
})"
>
</div>
</template>
<script>
export default {
name: "HelloWorld",
props: {
id: Number
}
};
</script>
I used the HelloWorld template so that's why there are some references to that, but I'm sure you can easily clean that up :)
And also, there may be some small edge case bugs that you can clean up. This should point you in the right direction though.
I'm trying to create a basic form that, once input is submitted, sends data to the parent and is then rendered in a list as a card. Documentation has been pointing me towards using the Event Bus, but it all seems a little too over engineered for such a simple task. Not to mention its not working xD. Am I on the right track here? or missing the whole idea?
The Data seems to be updating on submit, but I'm not seeing a card render. I'm also seeing the follow error,
Property or method "initiativeList" is not defined on the instance but referenced during render.
I do, however, notice a particularly odd change in the render. Instead of a child being rendered in EncounterList.js the child's attributes are merging into the parent .
Any help is greatly appreciated.
EncounterDashboard.js
<template>
<div>
<NewCharacterForm #add-char="addChar" />
<EncounterList v-bind="encounterList" #add-char="addChar" />
</div>
</template>
<script>
import Character from "../classes/Encounter";
import NewCharacterForm from "./NewCharacterForm/NewCharacterForm.vue";
import EncounterList from "./EncounterList/EncounterList";
import EventBus from "./EventBus.js";
export default {
name: "EncounterDashboard",
components: { NewCharacterForm, EncounterList },
data() {
return {
newChar: {},
encounterList: []
};
},
methods: {
addChar(newChar) {
this.newChar = newChar;
this.encounterList.push(newChar);
EventBus.$emit("add-to-list", this.encounterList);
}
}
};
</script>
NewCharacterForm.js
<template>
<div class="new-char-wrapper">
<form class="char-form" ref="form" v-on:submit.prevent="handleSubmit">
<NewCharInput class="name-input" label="NAME" name="name" v-model="name" />
<div class="stat-wrapper">
<NewCharInput
class="init-input"
label="INITIATIVE"
name="initiative"
v-model="initiative"
type="number"
/>
<NewCharInput class="hp-input" label="HP" name="hp" v-model="hp" type="number" />
</div>
<div class="submit-row">
<button class="submit">SUBMIT</button>
</div>
</form>
</div>
</template>
<script>
import NewCharInput from "./NewCharInput";
import Character from "../../classes/Character";
import { uuid } from "vue-uuid";
export default {
name: "NewCharacterForm",
components: { NewCharInput },
data() {
return {
name: "",
initiative: "",
hp: 0
};
},
props: ["addChar"],
methods: {
handleSubmit() {
const charName = this.$refs.form.name.value;
const charInitiative = this.$refs.form.initiative.value;
const charHp = this.$refs.form.hp.value;
const charId = this.$uuid.v4();
const newChar = new Character(charName, charInitiative, charId, charHp);
this.$emit("add-char", newChar);
}
}
};
</script>
EncounterList.js
<template>
<div class="encounter-list">
<div class="header-row">
<h2 class="header col-init">INIT</h2>
<h2 class="header col-name">NAME</h2>
<h2 class="header col-hp">HP</h2>
</div>
<EncounterCard
v-for="character in initiativeList"
v-bind:key="character.id"
v-bind:hp="character.hp"
v-bind:name="character.name"
v-bind:initiative="character.initiative"
/>
</div>
</template>
<script>
import EncounterCard from "../EncounterCard/EncounterCard";
import EventBus from "../EventBus";
export default {
name: "EncounterList",
components: { EncounterCard },
data() {
return {
data: {
initiativeList: []
}
};
},
methods: {
populateList(charList) {
this.initiativeList = charList;
}
},
mounted() {
EventBus.$on("add-to-list", charList => {
this.populateList(charList);
});
}
};
</script>
EncounterCard.js
<template>
<div class="encounter-card-wrapper">
<h1 class="char-init">{{character.initiative}}</h1>
<h1 class="char-name">{{character.name}}</h1>
<h1 class="char-hp">{{character.hp}}</h1>
</div>
</template>
<script>
export default {
name: "EncounterCard",
props: ["character"]
};
</script>
data() {
return {
data: { //Is this what you're trying to do?
initiativeList: []
}
};
},
If the data attribute is intended, "initiativeList" should be changed to "data.initiativeList".
<EncounterCard
v-for="character in data.initiativeList"
v-bind:key="character.id"
v-bind:hp="character.hp"
v-bind:name="character.name"
v-bind:initiative="character.initiative"
/>
and
populateList(charList) {
this.data.initiativeList = charList;
}
I'm a newbie of Vue, and I'm trying to simply clear the data of input component once I've submitted, but it seems I'm missing something, because since it's parent data is cleared, I still see the filled value of the input component.
Here is a living example.
I've set to the input child component v-model="title" from it's parent wrapper. Once I submit the data to the parent, I call addItem and in the end, I supposed to clear the data model just by clear it this.title = '', but I probably do something wrong on how to bind data from parent to child.
And above the code, starting from the parent component:
<template>
<form #submit="addItem" class="todo-insert">
<input-item
icon="create"
name="title"
placeholder="Add a ToVue item..."
v-model="title"
/>
<button-item tone="confirm" class="todo-insert__action">
Aggiungi
</button-item>
</form>
</template>
<script>
import ButtonItem from '#vue/Form/ButtonItem/ButtonItem.vue'
import InputItem from '#vue/Form/InputItem/InputItem.vue'
import uuid from 'uuid'
export default {
name: 'TodoInsert',
components: {
ButtonItem,
InputItem
},
data () {
return {
title: ''
}
},
methods: {
addItem (e) {
e.preventDefault()
const todo = {
id: uuid.v4(),
isComplete: false,
title: this.title
}
this.$emit('add-todo', todo)
this.title = ''
}
}
}
</script>
<style lang="scss" scoped src="./TodoList.scss"></style>
This is the child input component:
<template lang="html">
<label class="input">
<div v-if="label" class="input__label text-sans text-sans--label">
{{ label }}
</div>
<div class="input__row">
<input
:autocomplete="autocomplete"
:class="[hasPlaceholderLabel, isDirty]"
:name="name"
:placeholder="placeholder"
class="input__field"
type="text"
v-on:input="updateValue($event.target.value)"
v-on:blur="updateValue($event.target.value)"
>
<div v-if="placeholderLabel" class="input__placeholder text-sans text-sans--placeholder">
{{ placeholderLabel }}
</div>
<div v-if="icon" class="input__icon-area">
<icon-item
:name="icon"
/>
</div>
</div>
</label>
</template>
<script>
import IconItem from '../../IconItem/IconItem.vue'
export default {
name: 'InputItem',
props: {
autocomplete: {
type: String,
default: 'off'
},
icon: String,
label: String,
name: {
type: String,
default: 'input-text'
},
placeholder: String,
placeholderLabel: String
},
computed: {
hasPlaceholderLabel () {
return this.placeholderLabel ? 'input__field--placeholder-label' : ''
},
isDirty () {
// return this.$attrs.value ? 'input__field--dirty' : ''
return 'input__field--dirty'
}
},
methods: {
updateValue: function (value) {
this.$emit('input', value)
}
},
components: {
IconItem
}
}
</script>
<style lang="scss" src="./InputItem.scss"></style>
What am I missing?
Your child component is bound unidirectionally. It means that it can change the value, but does not receive any update from the parent component. To receive updates, you need to receive the property value in your child:
props: {
value: String
}
Then, you need to pass the value received to the input :
<input
:value="value"
:autocomplete="autocomplete"
:class="[hasPlaceholderLabel, isDirty]"
:name="name"
:placeholder="placeholder"
class="input__field"
type="text"
v-on:input="updateValue($event.target.value)"
v-on:blur="updateValue($event.target.value)"
>
Now the input should update when the parent component changes the value