I have created an example which is similar to my current work.
var App = new Vue({
el:'#app',
data:{
demoText:'text',
sections:2,
sectionData:[0,0]
},
methods:{
changeData:function(){
for(var i=0;i<this.sections;i++){
if(isNaN(this.sectionData[i]))
this.sectionData[i]=0;
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id='app'>
<div v-for="n in sections">
<input type="text" v-model=sectionData[n-1]>
</div>
<input type="button" #click="changeData" value="click">
</div>
In this example, when I click on the button I check if data is isNaN and change it to zero if it is. But this updated data doesn't update in DOM/textbox immediately. After that, if I update in another textbox then pending update action happens.
How can I resolve this issue?
There are a couple of issues in play here.
First is that Vue cannot detect the change you are making in your click handler when you change it by index. This is a common caveat when modifying an array in Vue.
Second, there is a bug in the code where if you blank out a text box, isNaN("") will resolve to false. This is documented in a number of places. I've implemented a very naive fix for this in the code below, but you should search for a stronger solution.
var App = new Vue({
el:'#app',
data:{
demoText:'text',
sections:2,
sectionData:[0,0]
},
methods:{
changeData:function(){
this.sectionData = this.sectionData.map(d => isNaN(parseInt(d,10)) ? 0 : d)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.min.js"></script>
<div id='app'>
<div v-for="n in sections">
<input type="text" v-model=sectionData[n-1]>
</div>
<input type="button" #click="changeData" value="click">
</div>
Another alternative implementation of your click handler would be
for (let i=0; i < this.sectionData.length; i++)
if (isNaN(parseInt(this.sectionData[i],10)))
this.sectionData.splice(i, 1, 0)
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.
So this is incredibly hard to explain, instead I made a video to showcase what's going on:
https://www.youtube.com/watch?v=md0FWeRhVkE
To explain in steps:
A user can create a new account.
This user will be automatically logged in (no e-mail verification step yet).
Then without refreshing the page, I'm trying to add a category.
There's a custom component that is v-modeled to a data() value (as you can see in the video this is category_name.
Whenever I fill something in in the input field, this should display above the input field (as I'm dumping the value there). You see that this doesn't happen before the page refresh.
However when I refresh the page, the v-model suddenly works.
Does anybody know what's going on and why it doesn't work when I register the user initially? It seems that the data() value category_name doesn't get created or something unless you refresh the page?
Thanks!
Your custom input (Input.vue) is not declaring and using the value prop that v-model is binding to - see the docs how v-model on custom component works
Wouldn't that only be necessary for two-way binding? As I see it, binding the value prop just allows the input value to be changed from the parent
Well, not exactly. value binding ("value from parent") is essential any time component is created and reused.
Reusing existing component instances is very common (and useful) optimization strategy Vue uses. You can play with the example below to see what is an effect of missing value biding on custom input.
And components are created more often then you can think. Switching to "Fixed" component and back demonstrates how broken the v-model without binding value is in case components are created dynamically (for example when used in Router views or some sort of custom "Tab" component)
I know this is "long shot" - I'm not sure this fixes the issue (sharing git repo doesn't fit to my definition of Minimal, Reproducible Example) BUT it is definitely a bug and I do not see anything else particularly wrong with the rest of the code...
Given how broken custom input without value is, it is reasonable to think that Vue devs never expected usage like this and that it can lead to all sorts of "strange" and unexpected behaviors ...
Vue.component('my-input-broken', {
props: ['name', 'type', 'label'],
methods: {
inputHandler(e) {
this.$emit('input', e.target.value);
},
},
template: `
<div v-if="name && type" :id="name">
<input v-if="type !== 'textarea'" #input="inputHandler" :name="name" :type="type" />
<textarea v-else-if="type === 'textarea'" #input="inputHandler" #blur="blurHandler($event)" :name="name" type="textarea" />
<label v-if="label" :for="name">{{label}}</label>
</div>
`
})
Vue.component('my-input-fixed', {
props: ['name', 'type', 'label', 'value'],
methods: {
inputHandler(e) {
this.$emit('input', e.target.value);
},
},
template: `
<div v-if="name && type" :id="name">
<input v-if="type !== 'textarea'" #input="inputHandler" :name="name" :type="type" :value='value' />
<textarea v-else-if="type === 'textarea'" #input="inputHandler" #blur="blurHandler($event)" :name="name" :value='value' type="textarea" />
<label v-if="label" :for="name">{{label}}</label>
</div>
`
})
const vm = new Vue({
el: '#app',
data: function() {
return {
values: [""],
componentToUse: 'a'
}
},
methods: {
addInput() {
this.values.unshift("")
}
},
computed: {
comp() {
return this.componentToUse === 'a' ? "my-input-broken" : "my-input-fixed"
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.js"></script>
<div id="app">
<label for="componentToUse">Component to use:</label>
<input type="radio" name="componentToUse" v-model="componentToUse" value="a"> Broken
<input type="radio" name="componentToUse" v-model="componentToUse" value="b"> Fixed
<hr>
<button #click="addInput">Add at beginning...</button>
<component :is="comp" v-for="(value, index) in values" :key="index" v-model="values[index]" :name="`value_${index}`" type="text" :label="`value_${index} ('${values[index]}')`"></component>
</div>
I think I understood the main problem of v-model now.
<input v-model="myObject[attribute]" />
is equivalent to
<input
v-bind:value="myObject[attribute]"
v-on:input="myObject[attribute] = $event"
/>
In my case the problem was that the GUI wasn't refreshing after myObject[attribute] was reassigned.
My workaround to this problem was to call $forceUpdate() after reassigning the variable which would look like following:
<input
v-bind:value="myObject[attribute]"
v-on:input="myObject[attribute] = $event; $forceUpdate()"
/>
Not sure if this solves your issue but I hope it helps somebody out there who is facing the same problem.
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'm building an autocomplete feature Basically, what I DON'T want is to bind the element like this:
v-model="input"
Binding on the element with the v-model or v-bind, gives my input element a blank value.
Instead, I'd like my element to be able to pick up an old value or a value from the database as seen in the code below. I'd only like to bind the element's value to my variable named "input" after the page has loaded with all this data from the DB. The code below works great, but I have to use document.getElementById to update my element with the new value.
<div id="spvs" class="uk-form-controls">
<input v-on:input="input = $event.target.value" v-on:keyup="getCompanies" name="company" id="company" class="uk-input {{$errors->has('company') ? ' uk-form-danger' : ''}}" placeholder="company name"
value="{{ old('company') || $errors->has('company')
? old('company')
: $property->getCompanyName()
}}">
<div v-if="spvs.length > 0" class="tm-autocomplete-box">
<ul class="uk-list uk-list-striped tm-autocomplete-list">
<li v-for="(spv, key) in spvs" #click="complete(key)"><span uk-icon="plus-circle" class="uk-margin-right"></span> #{{ spv.name }}</li>
</ul>
</div>
</div>
Ideally, I'd bind the element value to my 'input' variable when the user clicks one of the autocomplete items. Which runs a function called 'complete'.
methods:{
complete: function(key){
this.input = this.spvs[key].name;
document.getElementById('company').value = this.input;
this.spvs = '';
},
So this is the line that I would like to replace with the new binding:
document.getElementById('company').value = this.input;
So you want your input to have a old value when the component loads and then you want to update the value.Still you can use v-model as below.
When component loads the input will have the old value you want to set it, and also you can update the value:
<div id="app">
<input type="text" v-model="oldValue">
</div>
new Vue({
el: "#app",
data: {
oldValue: 'value from Database'
}
})
If you want another way there it is:
<div id="app">
<input type="text" :value="oldValue" #input="changeValue">
<hr>
The value of input is: {{oldValue}}
</div>
new Vue({
el: "#app",
data: {
oldValue: 'value from Database'
},
methods: {
changeValue(newValue) {
this.oldValue = newValue.target.value
}
}
})
See in action the first example
See in action the second example
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() />