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.
Related
I am creating a few input fields of type datetime-local in Vuejs/Nuxtjs dynamically. I would like to set the default value for all these input fields as 2022-04-21T14:27:27.000. How to do it?
For the direct field, we can assign the value using the v-model but I am a bit unsure about how to set a default value for all the fields of datetime-local. If I am not wrong there is an option in Vanilla JavaScript to get all fields of a certain type and change the value, is there a way to achieve the same using Vuejs/Nuxtjs.
Following is the code I have so far:
<template>
<div>
<button type="button" #click="addField" class="btn btn-info">
Add Field
</button>
<div v-for="field in fieldArray" :key="field.ID" class="form-group">
<input
v-model="field.time"
type="datetime-local"
class="form-control"
step="1"
value="2022-04-21T14:27:27.000"
#change="timeChange(field.ID)"
/>
</div>
</div>
</template>
<script>
export default {
data() {
return {
fieldCount: 0,
fieldArray: [],
};
},
mounted() {
let now = new Date();
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
now.setSeconds(now.getSeconds(), 0);
now = now.toISOString().slice(0, -1);
//I would like to set this "now" time value for all the created fields of input type datetime-local
console.log("Current Time : " + now);
},
methods: {
//Add field
addField() {
const fieldObj = { ID: this.fieldCount, time: "" };
this.fieldArray.push(fieldObj);
},
//Time change
timeChange(ID) {
console.log("ID : " + ID);
console.log(JSON.stringify(this.fieldArray, null, 4));
},
},
};
</script>
<style>
</style>
All I would like to do is set the default time for all the datetime-local fields within my component. Is there any way to do it? I am able to achieve this for single field but not for the fields which is created dynamically.
Something like this?
<template>
<div>
<button type="button" #click="addField" class="btn btn-info">
Add Field
</button>
<div v-for="field in fieldArray" :key="field.ID" class="form-group">
<input
v-model="field.time"
type="datetime-local"
class="form-control"
step="1"
value="2022-04-21T14:27:27.000"
#change="timeChange(field.ID)"
/>
</div>
</div>
</template>
<script>
export default {
data() {
return {
fieldCount: 0,
fieldArray: [],
now: null,
};
},
mounted() {
this.now = new Date();
this.now.setMinutes(this.now.getMinutes() - this.now.getTimezoneOffset());
this.now.setSeconds(this.now.getSeconds(), 0);
this.now = this.now.toISOString().slice(0, -1);
//I would like to set this "now" time value for all the created fields of input type datetime-local
console.log("Current Time : " + this.now);
},
methods: {
//Add field
addField() {
const fieldObj = { ID: this.fieldCount, time: this.now };
this.fieldArray.push(fieldObj);
},
//Time change
timeChange(ID) {
console.log("ID : " + ID);
console.log(JSON.stringify(this.fieldArray, null, 4));
},
},
};
</script>
<style>
</style>
Dates are always hard.
The HTML input element with type datetime-local is odd. While it accepts a JavsScript date as input, it outputs a string... I would create a proxy component to handle this.
Take a look at this playground
// DateInput.vue
<template>
<input :value="modelValue"
#input="$emit('update:modelValue', new Date($event.target.value))"
type="datetime-local" />
</template>
<script>
export default {
props: ["modelValue"],
emits: ["update:modelValue"],
}
</script>
// Parent.vue
<template>
<div>
<button type="button" #click="addField" class="btn btn-info">
Add Field
</button>
<div v-for="field in fieldArray" :key="field.ID" class="form-group">
{{field.ID}}
<DateInput v-model="field.time"></DateInput> {{field.time}}
</div>
<hr />
{{fieldCount}}
</div>
</template>
<script>
import DateInput from './DateInput.vue'
export default {
components: { DateInput },
data() {
return {
fieldArray: [],
};
},
computed: {
fieldCount () {
return this.fieldArray.length
}
},
methods: {
addField() {
this.fieldArray.push({ ID: this.fieldCount, time: new Date() });
},
},
};
</script>
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
So I create this cart page in Vue. I just don't understand how to update the cart page total price when the quantity of the item child component increase or decreases. If the item component quantity increase, of course, the total price must increase too.
Here's my cart parent component :
<template>
<div class="cart-container">
<h1 class="cart-title-page">Keranjang Anda</h1>
<div class="cart-item-container">
<cart-item v-for="(data, i) in cartData" :item="data" :key="i" />
</div>
<div class="cart-total-wrapper">
<div class="total-text-wrapper">
<p>Total</p>
</div>
<div class="total-amount-wrapper">
<p>Rp. 150.000.000</p>
</div>
</div>
</div>
</template>
<script>
import CartItem from '#/components/cart-item'
export default {
data() {
return {
cartData: [
{
product_name: 'vario ZS1',
price: 1000000,
url_thumbnail: 'https://cdn3.imggmi.com/uploads/2019/10/8/9e27ca9046031f6f21850be39b379075-full.png',
color: '#fff'
},
{
product_name: 'vario ZS1',
price: 1000000,
url_thumbnail: 'https://cdn3.imggmi.com/uploads/2019/10/8/9e27ca9046031f6f21850be39b379075-full.png',
color: '#fff'
},
{
product_name: 'vario ZS1',
price: 1000000,
url_thumbnail: 'https://cdn3.imggmi.com/uploads/2019/10/8/9e27ca9046031f6f21850be39b379075-full.png',
color: '#fff'
}
]
}
},
methods: {
getAllCartItem () {
this.$store.dispatch('cart/checkCartItem')
this.cartData = this.$store.state.cart.cartItem
}
},
created () {
this.getAllCartItem ()
},
components: {
'cart-item': CartItem
}
}
</script>
this is my cart item child component:
<template>
<div class="root-cart-item">
<div class="container-cart-left">
<div class="cart-img-wrapper">
<img :src="item.url_thumbnail" />
</div>
<div class="cart-title-wrapper">
<div class="title-wrapper">
<h3>{{ getProductbrand }}</h3>
<p>{{ item.product_name }}</p>
</div>
</div>
</div>
<div class="container-cart-right">
<div class="cart-amount-wrapper">
<number-input v-model="singleCart.amount" :min="1" :max="singleCart.stok" inline center controls></number-input>
</div>
<div class="cart-price-wrapper">
<p>{{ getProductTotalPrice }}</p>
</div>
<div class="cart-delete-wrapper">
<img src="../assets/delete.svg"/>
</div>
</div>
</div>
</template>
<script>
import ProductImage from './product-image'
import VueNumberInput from '#chenfengyuan/vue-number-input';
export default {
props: {
item: {
type: Object,
required: true
}
},
data () {
return {
singleCart: {
stok: 15,
amount: 1,
totalPrice: 0
}
}
},
computed: {
getProductbrand: function () {
let splittedName = this.item.product_name.split(' ')
return splittedName[0]
},
getProductTotalPrice: function () {
var x = this.singleCart.totalPrice.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".")
var totalPrice = `Rp. ${x}`
return totalPrice
}
},
watch: {
'singleCart.amount': {
handler: function () {
this.singleCart.totalPrice = this.singleCart.price * this.singleCart.amount
},
deep: true
}
},
components: {
'product-image': ProductImage,
'number-input': VueNumberInput
}
}
</script>>
and if anyone wondering, this is my cart store:
const state = {
cartItem: []
}
const getters = {
getAllCartItem: (state) => {
return state.cartItem
}
}
const mutations = {
updateCartItem: (state, cart) => {
state.cartItems = cart
}
}
const actions = {
checkCartItem: ({ commit }) => {
let item = JSON.parse(localStorage.getItem('cart'))
if (cart) {
commit('updateCartItem', item)
}
}
}
export default {
namespaced: true,
state,
getters,
actions,
mutations
}
like I said, the problem should be quite simple, I just have to update the CSS class .total-amount-wrapper in the parent component, when the quantity in the child component increase or decreases. The total price in the child cart-item component is working, I just have to find a way to count every total price in the child cart-item component, and show it in the parent component.
For update the parent you must use the v-model approach or use the $emit.
In your code you must update the input to use the v-model or you must $emit an event when price change.
The first is simple and you must follow the tutorial you find in the link above, the second is below.
Child Component
watch: {
'singleCart.amount': {
handler: function () {
this.singleCart.totalPrice = this.singleCart.price * this.singleCart.amount
this.$emit("priceChanged", this.singleCart.totalPrice);
},
deep: true
}
}
Parent
<template>
..
<div class="cart-item-container">
<cart-item v-for="(data, i) in cartData" :item="data" :key="i"
#priceChanged="onPriceChanged" />
</div>
</template>
<script>
methods: {
..
onPriceChanged(value) {
this.total += value;
}
}
</scritp>
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