This question already has answers here:
Vuejs and Vue.set(), update array
(5 answers)
Closed 2 years ago.
I'm working on an e-commerce website, I want to have a button to add items to cart.
When I add a button, value in nbrSeats (my list of values) change (in data), but the input field shows another value.
I put a part of the code here:
https://codepen.io/martinrougeron2/pen/MWKdJBb
<!-- Use preprocessors via the lang attribute! e.g. <template lang="pug"> -->
<template>
<div id="app">
<button #click="create_list()">Create list</button>
<div v-for="(i, index) in nbrSeats" :key="index">
<input
type="number"
max="10"
min="1"
v-model="nbrSeats[index]"
style="width: 60px"
label="Nb. places"
>
</input>
<button #click="nbrSeats[index]++">Add</button>
</div>
<button #click="show()">Show me values</button>
</div>
</template>
<script>
export default {
data() {
return {
nbrSeats: [],
message: 'Welcome to Vue!'
};
},
methods: {
doSomething() {
alert('Hello!');
},
create_list() {
for (var i = 0; i < 10; i++) {
this.nbrSeats.push(1)
}
},
show() {
console.log(this.nbrSeats)
},
}
};
</script>
It is because Vue.js is not picking on changes when you mutate directly in the array.
Instead use Vue.set.
Change your callback:
<button #click="addSeat(index)">Add</button>
And then add this method:
addSeat(index) {
this.$set(this.nbrSeats, index, this.nbrSeats[index] + 1);
}
Vuejs and Vue.set(), update array
https://v2.vuejs.org/v2/guide/reactivity.html#For-Arrays
Related
I am developing a Vuejs application within which I have a field. For this field, users can provide the values and this field expands dynamically based on the user-provided values.
The field name is extensions, initially an Add Extension button will be displayed. With on click of the button, a bootstrap modal will be displayed which has 3 fields: namespace (text), localname (text), datatype(dropdown: string/complex). If the datatype is string then a simple text field will be displayed. However, if the datatype is complex then another button should be displayed and with on click of the button again the same modal is displayed with fields and the process continues. So the created JSON based on this will expand horizontally and vertically.
I am able to complete the first iteration and display the elements to users on the front end. However, for further iteration, I am not understanding how to achieve it using the recursive approach. Since I don't know how many times users may create the extensions I need to have an approach that dynamically does this.
Can someone please help me how to create and display the JSONarray using Vuejs which expands horizontally and vertically?
Following is the code I have so far:
<template>
<div class="container-fluid">
<div class="row">
<div class="col-md-3">
<span>Extensions</span>
<button class="form-control" #click="createExtensions()">
Add Another
</button>
</div>
</div>
<div v-for="extension in extensionList" :key="extension.ID" class="form-inline">
<span>{{ extension.namespace+ ":"+extension.localName }}</span>
<input v-if="extension.dataType == 'string'" type="text" #input="AddExtensionText($event,extension.ID)">
<button v-if="extension.dataType == 'complex'" #click="AddComplextExtension(extension.ID)">
Add another
</button>
</div>
<b-modal
id="Extension"
title="Add Another Element"
size="lg"
width="100%"
:visible="showModal"
>
<b-form id="AddExtension" #submit.prevent="submitExtension">
<div class="form-group">
<label for="message-text" class="col-form-label">Namespace URI:</label>
<input
v-model="extension.namespace"
type="text"
class="form-control"
required
>
</div>
<div class="form-group">
<label for="message-text" class="col-form-label">Local Name:</label>
<input
v-model="extension.localName"
type="text"
class="form-control"
required
>
</div>
<div class="form-group">
<label for="AddExtensionDataType" class="col-form-label">Data Type:</label>
<b-form-select v-model="extension.dataType" class="form-control">
<b-form-select-option value="string">
String
</b-form-select-option>
<b-form-select-option value="complex">
Complex
</b-form-select-option>
</b-form-select>
</div>
</b-form>
<template #modal-footer="{ cancel }">
<b-btn #click="cancel">
Cancel
</b-btn>
<b-btn variant="primary" type="submit" form="AddExtension">
OK
</b-btn>
</template>
</b-modal>
</div>
</template>
<script>
export default {
data () {
return {
extensionList: [],
extensionCount: 0,
extension: {
namespace: '',
localName: '',
dataType: 'string'
},
showModal: false
}
},
methods: {
// Method to create extensions and add
createExtensions () {
this.showModal = true
},
// Function to submit the each extension
submitExtension () {
this.showModal = false
const extensionObj = {}
extensionObj.ID = this.extensionCount
extensionObj.namespace = this.extension.namespace
extensionObj.localName = this.extension.localName
extensionObj.dataType = this.extension.dataType
this.extensionList.push(extensionObj)
this.extensionCount++
},
// On addition of the input field value update List
AddExtensionText (event, extensionID) {
const extension = this.extensionList.filter(ex => ex.ID === extensionID)[0]
if (typeof extension !== 'undefined') {
extension.text = (typeof event.target.value !== 'undefined') ? event.target.value : ''
}
},
// Add complex extension
AddComplextExtension (extensionID) {
this.showModal = true
}
}
}
</script>
<style>
</style>
This is the initial field I have:
This is what I want to achieve where everything is created dynamically and JSON expands both horizontally and vertically:
Can someone please let me know how to create such dynamic JSON using Vuejs and display the same in the frontend.
To display data recursively, you need to use recursive components.
Abstract your v-for code into another component file (let's call it NodeComponent.vue). Pass your extensionList to this component, then inside this component, add another NodeComponent for each extension which has type complex.
Since your extension would be another array if it is complex, you can pass it directly into this NodeComponent as a prop and let recursion work its magic.
NodeComponent.vue
<template>
<div>
<div
v-for="extension in extensionList"
:key="extension.ID"
class="form-inline"
>
<span>{{ extension.namespace + ":" + extension.localName }}</span>
<input
v-if="extension.dataType == 'string'"
type="text"
#input="$emit('AddExtensionText', {$event, id: extension.ID}"
/>
<NodeComponent v-if="extention.dataType == 'complex'" :extentionList="extension" #AddExtensionText="AddExtensionText($event)"/>
<button
v-if="extension.dataType == 'complex'"
#click="AddComplextExtension(extension.ID)"
>
Add another
</button>
</div>
</div>
</template>
<script>
export default {
props: {
extensionList: Array,
extension: Object,
},
methods: {
AddComplextExtension(extensionID) {
// Emit event on root to show modal, or use this.$bvModal.show('modal-id') or create a dynamic modal, see: https://bootstrap-vue.org/docs/components/modal#message-box-advanced-usage
}
AddExtensionText({ value, id }) {
const i = this.extensionList.findIndex((el) => el.ID === id);
this.$set(extensionList, i, value);
}
}
};
</script>
Note that I emit a custom event from child NodeComponents on changing input text so that the parent can make this change in its extensionList array, using this.$set to maintain reactivity.
EDIT: If you want to add new Node components:
You need to have a parent component that holds the first NodeComponent in it. In here you'll define the modal (if you define it inside NodeComponent, you'll have a separate modal reference for each NodeComponent. Judging from your code you're probably using Bootstrap-Vue, it injects modals lazily when shown, so I don't think this will affect your performance too much, but it still doesn't feel like good code.). You need to emit event on root to show the modal. You need to send the extensionList as payload with this event like this: this.$root.emit('showModal', extensionList). In you parent component you can listen to the event and show the modal. Now inside your submitExtension function, you can use this extensionList and push a new object to it. The corresponding NodeComponent will update itself since arrays are passed by reference.
this.$root.on('showModal`, (extensionList) => {
this.editExtensionList = extensionList;
showModal = true;
}
submitExtension() {
this.showModal = false
const extensionObj = {}
extensionObj.ID = this.extensionCount
extensionObj.namespace = this.extension.namespace
extensionObj.localName = this.extension.localName
extensionObj.dataType = this.extension.dataType
this.editExtensionList.push(extensionObj)
this.extensionCount++
}
All being said, at this point it might be worthwhile to invest in implementing a VueX store where you have a global extentionList and define mutations to it.
Background
I have a v-for loop that contains a pair of buttons and inputs. The buttons contains an #click event that is suppose to show the corresponding input.
Problem
When the buttons are clicked it shows all the input instances instead of the corresponding input to the button clicked.
<div class="buttonFor my-3" v-for="(expense,index) in expenseButton" :key="index">
<input type="submit" #click="showInput(expense)" :value="expense.expensesKey" />
<input v-show="needEdit" v-model.number="expense.subExpense" type="number" />
</div>
data() {
return {
expenseButton: [],
needEdit: false
};
}
methods: {
showInput() {
this.needEdit = !this.needEdit;
}
}
I think it's better you keep track of the chosen id. you could edit your data to receive chosen id like:
data() {
return {
expenseButton: [],
chosenExpenseId: null
};
}
your show input receives the index instead:
methods: {
showInput(index) {
this.chosenExpenseId = index;
// if want to make the button to hide input once clicked back
// this.chosenExpenseId = this.chosenExpenseId !== index ? index : null;
}
}
at your template you pass the index and validates input show condition as:
<input type="submit" #click="showInput(index)" :value="expense.expensesKey" />
<input v-show="index === chosenExpenseId" v-model.number="expense.subExpense" type="number" />
I have a form where user can fill an email address and click a plus button against it to create a new one. These input boxes are generated by iterating over an array. When user clicks on the + icon, a new entry is pushed to this array.
Now the new text box is generating fine but I want the cursor to be focused in this one.
as #ramakant-mishra told you must use this.$refs, but you need to add ref attribute dynamically on your input element also. let me show you:
new Vue({
el: '#app',
data: {
emails:[]
},
methods: {
add: function (e) {
let j = this.emails.push("")
this.$nextTick(() => {
this.$refs[`email${j - 1}`][0].focus()
})
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(email, i) in emails" :key="i">
<input v-model="email" :ref="`email${i}`" />
</div>
<button #click="add">add</button>
</div>
just don't forget to use $nextTick because the new element is not rendered yet on template
They key is to set ref on all your inputs to the same string like this:
<input type="text" ref="myInputs"/>
Then you will have access to an array called this.$refs.myInputs inside an event handler.
new Vue({
el: "#app",
data() {
return {
emails: []
};
},
methods: {
addEmail() {
this.emails.push('whatever');
this.$nextTick(() => {
const lastIdx = this.emails.length - 1;
this.$refs.myInputs[lastIdx].focus();
});
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<div id="app">
<input type="button" #click="addEmail()" value="Add Email"/>
<div v-for="(email, index) in emails" :key="index">
<input ref="myInputs" type="text" />
</div>
</div>
Note that below you must put the call to focus() inside a nextTick() in order to give Vue a chance to actually render the email you just added to the list.
I have 2 inputs and want switch focus from first to second when user press Enter.
I tried mix jQuery with Vue becouse I can't find any function to focus on something in Vue documentation:
<input v-on:keyup.enter="$(':focus').next('input').focus()"
...>
<input ...>
But on enter I see error in console:
build.js:11079 [Vue warn]: Property or method "$" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option. (found in anonymous component - use the "name" option for better debugging messages.)warn # build.js:11079has # build.js:9011keyup # build.js:15333(anonymous function) # build.js:10111
build.js:15333 Uncaught TypeError: $ is not a function
You can try something like this:
<input v-on:keyup.enter="$event.target.nextElementSibling.focus()" type="text">
JSfiddle Example
Update
In case if the target element is inside form element and next form element is not a real sibling then you can do the following:
html
<form id="app">
<p>{{ message }}</p>
<input v-on:keyup.enter="goNext($event.target)" type="text">
<div>
<input type="text">
</div>
</form>
js
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!',
focusNext(elem) {
const currentIndex = Array.from(elem.form.elements).indexOf(elem);
elem.form.elements.item(
currentIndex < elem.form.elements.length - 1 ?
currentIndex + 1 :
0
).focus();
}
}
})
JSFiddle Example
Following up from #zurzui here is in my opinion a cleaner alternative using the $refs API (https://v2.vuejs.org/v2/api/#vm-refs).
Using the $refs API, can allow you to target element in a simpler fashion without traversing the DOM.
Example: https://jsfiddle.net/xjdujke7/1/
After some tests, it's working
new Vue({
el:'#app',
methods: {
nextPlease: function (event) {
document.getElementById('input2').focus();
}
}
});
<script src="https://vuejs.org/js/vue.js"></script>
<div id='app'>
<input type="text" v-on:keyup.enter="nextPlease">
<input id="input2" type="text">
</div>
directives: {
focusNextOnEnter: {
inserted: function (el,binding,vnode) {
let length = vnode.elm.length;
vnode.elm[0].focus();
let index = 0;
el.addEventListener("keyup",(ev) => {
if (ev.keyCode === 13 && index<length-1) {
index++;
vnode.elm[index].focus();
}
});
for (let i = 0;i<length-1;i++) {
vnode.elm[i].onfocus = (function(i) {
return function () {
index = i;
}
})(i);
}
}
}
}
// use it
<el-form v-focusNextOnEnter>
...
</el-form>
Try this:
<input ref="email" />
this.$refs.email.focus()
Whilst I liked the directives answer due to it working with inputs inside other elements (style wrappers and so on), I found it was a little inflexible for elements that come and go, especially if they come and go according to other fields. It also did something more.
Instead, I've put together the following two different directives. Use them in your HTML as per:
<form v-forcusNextOnEnter v-focusFirstOnLoad>
...
</form>
Define them on your Vue object (or in a mixin) with:
directives: {
focusFirstOnLoad: {
inserted(el, binding, vnode) {
vnode.elm[0].focus();
},
},
focusNextOnEnter: {
inserted(el, binding, vnode) {
el.addEventListener('keyup', (ev) => {
let index = [...vnode.elm.elements].indexOf(ev.target);
if (ev.keyCode === 13 && index < vnode.elm.length - 1) {
vnode.elm[index + 1].focus();
}
});
},
},
},
On an enter key pressed, it looks for the index of the current input in the list of inputs, verifies it can be upped, and focuses the next element.
Key differences: length and index are calculated at the time of the click, making it more suitable for field addition/removal; no extra events are needed to change a cached variable.
Downside, this will be a little slower/more intensive to run, but given it's based off UI interaction...
Vue.js's directive is good practice for this requirement.
Define a global directive:
Vue.directive('focusNextOnEnter', {
inserted: function (el, binding, vnode) {
el.addEventListener('keyup', (ev) => {
if (ev.keyCode === 13) {
if (binding.value) {
vnode.context.$refs[binding.value].focus()
return
}
if (!el.form) {
return
}
const inputElements = Array.from(el.form.querySelectorAll('input:not([disabled]):not([readonly])'))
const currentIndex = inputElements.indexOf(el)
inputElements[currentIndex < inputElements.length - 1 ? currentIndex + 1 : 0].focus()
}
})
}
})
Note: We should exclude the disabled and readonly inputs.
Usage:
<form>
<input type="text" v-focus-next-on-enter></input>
<!-- readonly or disabled inputs would be skipped -->
<input type="text" v-focus-next-on-enter readonly></input>
<input type="text" v-focus-next-on-enter disabled></input>
<!-- skip the next and focus on the specified input -->
<input type="text" v-focus-next-on-enter='`theLastInput`'></input>
<input type="text" v-focus-next-on-enter></input>
<input type="text" v-focus-next-on-enter ref="theLastInput"></input>
</form>
<input type="text" #keyup.enter="$event.target.nextElementSibling.focus() />
I understand how to use v-bind:class if I have a computed function returning true or false.
I would like to know if it is possible to use a computed property that matches the ID of the button being clicked and the value of that button.
So clicking button 1 I could get the value of of that button and check if it matches the value of data model being bind to the input.
Currently the value of the button is sync'd to a Vue data property.
<label v-bind:class="myBtnClass">
<input type="radio" name="button1" id="button1" value="1" v-model="valueOfBtn"> One
</label>
<label v-bind:class="myBtnClass">
<input type="radio" name="button2" id="button2" value="2" v-model="valueOfBtn"> Two
</label>
new Vue({
el: '#app',
data: {
'valueOfBtn': 1
This bit would only work for one button and clearly I don't want to repeat this block of code x times.
computed: {
myBtnClass: function () {
var result = [];
if (this.valueOfBtn) == document.getElementById('button1').value.valueOf()))
{
result.push('primary');
}
return result;
Thanks in advance
use methods instead:
export default = {
methods: {
myBtnClass: function(name) {
var result = [];
if (this.valueOfBtn) === name) {
result.push('primary');
}
return result;
},
// ...
}
}
and HTML:
<label v-bind:class="myBtnClass('button1')">
....
<label v-bind:class="myBtnClass('button2')">