I'm trying to use Vue as a very thin layer to bind existing model objects to a view.
Below is a toy app illustrating my problem. I have a GainNode object, from the Web Audio API. I want to bind its value to a slider.
This is trivial in Angular. Two-way binding works with any object, whether part of an Angular component or not. Is there a way to do something similar in Vue?
In the real app, I have big lists of programmatically generated objects. I need to bind them to a component, e.g. <Knob v-for='channel in channels' v-model='channel.gainNode.gain.value'>.
UPDATE: I was using Workaround #2 (below), and it appeared to be working great, until I tried v-model-binding two components to the same audio parameter. Then it just didn't work in, in totally mysterious ways that I couldn't debug. I eventually gave up and am using getters/setters, which is more boilerplate but has the advantage of, ya know.. actually working.
class MyApp {
constructor() {
// core model which I'd prefer to bind to
this.audio = new AudioContext();
this.audioNode = this.audio.createGain();
this.audioNode.gain.value = .8; // want to bind a control to this
// attempts to add reactivity
this.reactiveWrapper = Vue.reactive(this.audioNode.gain);
this.refWrapper = Vue.ref(this.audioNode.gain.value);
}
get gainValue() { return this.audioNode.gain.value; }
set gainValue(value) { this.audioNode.gain.value = value; }
}
let appModel = new MyApp();
let app = Vue.createApp({
template: '#AppView',
data() { return {
// core model which I'd prefer to bind to
model: appModel,
// attempt to add reactivity
dataAliasAudioNode: appModel.audioNode }
}
});
app.mount('#mount');
<script type='text/x-template' id='AppView'>
<div>
model.audioNode.gain.value: {{model.audioNode.gain.value}}
</div>
<hr>
<div>
<div>Binding to getter/setter for <code>model.audioNode.gain.value</code> (works)</div>
<input type='range' min='0' max='1' step='.1' v-model='model.gainValue'>
</div>
<div>
<div>Binding directly to <code>model.audioNode.gain.value</code> (doesn't work)</div>
<input type='range' min='0' max='1' step='.1' v-model='model.audioNode.gain.value'>
</div>
<div>
<div>Binding through <code>model.reactiveWrapper</code> (doesn't work)</div>
<input type='range' min='0' max='1' step='.1' v-model='model.reactiveWrapper.value'>
</div>
<div>
<div>Binding through <code>model.refWrapper</code> (doesn't work)</div>
<input type='range' min='0' max='1' step='.1' v-model='model.refWrapper.value'>
</div>
<div>
<div>Binding through <code>dataAliasAudioNode.gain.value</code> (doesn't work)</div>
<input type='range' min='0' max='1' step='.1' v-model='dataAliasAudioNode.gain.value'>
</div>
</script>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id='mount'></div>
Question Addendum #1: In exploring ways to do this, I've discovered (as already noted) that if I bind to a nested portion of a foreign object (GainNode the Web Audio API) it's not reactive, but if I build a similar foreign object myself, binding to a nested parameter is reactive. Here's example code:
// my version Web Audio API's AudioContext, GainNode, and AudioParam
class AudioParamX {
constructor() {
this._value = 0;
}
get value() { return this._value; }
set value(v) { this._value = v; }
}
class ValueParamX extends AudioParamX {
}
class GainNodeX {
constructor() {
this.gain = new ValueParamX();
}
}
class AudioContextX {
createGain() {
return new GainNodeX();
}
}
//==================================================================
class MyApp {
constructor() {
this.audio = new AudioContext();
this.audioNode = this.audio.createGain();
this.xaudio = new AudioContextX();
this.xaudioNode = this.xaudio.createGain();
}
}
let appModel = new MyApp();
let app = Vue.createApp({
template: '#AppView',
data() { return { model: appModel } }
});
app.mount('#mount');
<script type='text/x-template' id='AppView'>
<div>
model.xaudioNode.gain.value: {{model.xaudioNode.gain.value}}
</div>
<div>
model.audioNode.gain.value: {{model.audioNode.gain.value}}
</div>
<hr>
<div>
<div>Binding to <code>model.xaudioNode.gain.value</code> works.</div>
<input type='range' min='0' max='1' step='.05' v-model='model.xaudioNode.gain.value'>
</div>
<div>
<div>Binding to <code>model.audioNode.gain.value</code> doesn't. Why?</div>
<input type='range' min='0' max='1' step='.05' v-model='model.audioNode.gain.value'>
</div>
</script>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id='mount'></div>
Workaround #1:
So after more exploration, I've come up with one workaround which reduces the boilerplate of a getter/setter. I either:
Create my own version of ref (no clue Vue.ref doesn't work) or
Proxy the object and call $forceUpdate when the setter is called.
Both of these work, with the disadvantage that I then must expose the proxy as a member and bind to it rather than the original object. But it's better than exposing both a getter and setter and it works with v-model.
class MyApp {
createWrapper(obj, field) {
return {
get [field]() { return obj[field]; },
set [field](v) { obj[field] = v; }
}
}
createProxy(obj) {
let update = () => this.forceUpdate();
return new Proxy(obj, {
get(target, prop) { return target[prop] },
set(target, prop, value) {
update();
return target[prop] = value
}
});
}
watch(obj, prop) {
hookSetter(obj, prop, () => this.forceUpdate());
}
constructor() {
this.audio = new AudioContext();
// core model which I'd prefer to bind to
this.audioNode = this.audio.createGain();
this.audioNode.gain.value = .1; // want to bind a control to this
this.audioNode.connect(this.audio.destination);
// attempts to add reactivity
this.wrapper = this.createWrapper(this.audioNode.gain, 'value');
this.proxy = this.createProxy(this.audioNode.gain);
}
}
let appModel = new MyApp();
let app = Vue.createApp({
template: '<AppView :model="model" />',
data() { return { model: appModel } },
});
app.component('AppView', {
template: '#AppView',
props: ['model'],
mounted() {
this.model.forceUpdate = () => this.$forceUpdate();
}
})
app.mount('#mount');
<style>body { user-select: none; }</style>
<script type='text/x-template' id='AppView'>
<div>
<div>model.audioNode.gain.value: {{model.audioNode.gain.value}}</div>
<div>model.wrapper.value: {{model.wrapper.value}}</div>
<div>model.proxy.value: {{model.wrapper.value}}</div>
</div>
<hr>
<div>
<div>Binding directly to <code>model.audioNode.gain.value</code> (doesn't work)</div>
<input type='range' min='0' max='1' step='.05' v-model='model.audioNode.gain.value'>
</div>
<div>
<div>Binding through <code>model.wrapper.value</code> (works)</div>
<input type='range' min='0' max='1' step='.05' v-model='model.wrapper.value'>
</div>
<div>
<div>Binding through <code>model.proxy.value</code> (works)</div>
<input type='range' min='0' max='1' step='.05' v-model='model.proxy.value'>
</div>
</script>
<div id='mount'></div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
Workaround #2:
Another workaround is to patch the accessor I want to watch and call $forceUpdate in it. This has the least boilerplate. I just call watch(obj, prop) and that property becomes reactive.
This is a fairly acceptable workaround, to my taste. However, I'm not sure how well these workaround schemes will work when I start moving things into child components. I'm going to try that next. I also still don't understand why Vue.reference doesn't do the same thing.
I'd like to do this in the most Vue native way possible, and it seems like it would be a pretty typical use case.
class MyApp {
watch(obj, prop) {
hookObjectSetter(obj, prop, () => this.forceUpdate());
}
constructor() {
this.audio = new AudioContext();
// core model which I'd prefer to bind to
this.audioNode = this.audio.createGain();
this.audioNode.gain.value = .1; // want to bind a control to this
this.watch(this.audioNode.gain, 'value'); // make it reactive
}
}
let appModel = new MyApp();
let app = Vue.createApp({
template: '<AppView :model="model" />',
data() { return { model: appModel } },
});
app.component('AppView', {
template: '#AppView',
props: ['model'],
mounted() {
this.model.forceUpdate = () => this.$forceUpdate();
}
})
app.mount('#mount');
function hookObjectSetter(obj, prop, callback) {
let descriptor = Object.getOwnPropertyDescriptor(obj, prop);
if (!descriptor) {
obj = Object.getPrototypeOf(obj);
descriptor = Object.getOwnPropertyDescriptor(obj, prop);
}
if (descriptor && descriptor.configurable) {
let set = descriptor.set || (v => descriptor.value = v);
let get = descriptor.get || (v => descriptor.value);
Object.defineProperty(obj, prop, {
configurable: false, // prevent double-hooking; sorry anybody else!
get,
set(v) {
callback();
return set.apply(this, arguments);
},
});
}
}
<script type='text/x-template' id='AppView'>
<div>
<div>model.audioNode.gain.value: {{model.audioNode.gain.value}}</div>
</div>
<hr>
<div>
<div>Binding directly to <code>model.audioNode.gain.value</code> with custom `watch` (works)</div>
<input type='range' min='0' max='1' step='.05' v-model='model.audioNode.gain.value'>
</div>
</script>
<div id='mount'></div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
Your question is exciting so I decided to spend hours figuring out the answer.
TLDR
Built-in objects (objects that are created by browser API) can't be converted into the reactive form so mutating its properties will not trigger re-render
Vue is not fully selective re-render so when it does re-render the template, some blocks that are even not reactive will be updated too.
Let's wrap up the problem:
The reactive does NOT work when mutating a property of a Web Audio API object (actually any built-in object will be the same)
The reactive does work when mutating that same property in a setter
Explanation
First, we need to know What happens when Vue renders a value on the template? Let's consider this template:
{{ model.audioNode.gain.value }}
If model is a reactive object (which is created by reactive, ref, or computed ...), Vue will create a getter that converts each object on the chain into reactive. So, these following objects will be converted into the reactive form using the Vue.reactive function: model.audioNode, model.audioNode.gain
But just some types that can be converted to a reactive object. Here is the code from Vue reactive package
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
As we can see, types that other than Object, Array, Map, Set, WeakMap, and WeakSet will be invalid. To know what type of your object, you can call yourObject.toString() (what Vue is actually using). Any custom class that does not modify the toString method will be Object type and can be made reactive. In your example code model is object type, model.audioNode is type GainNode. So it can NOT be converted to a reactive object and mutating its property will not trigger Vue re-render.
So Why the setter method works?
It actually does NOT work. Let's consider this snippet:
class MyApp {
constructor() {
this.audio = new AudioContext();
this.audioNode = this.audio.createGain();
this.audioNode.gain.value = .8;
}
get gainValue() { return this.audioNode.gain.value; }
set gainValue(value) { this.audioNode.gain.value = value; }
}
let appModel = new MyApp();
let app = Vue.createApp({
template: '#AppView',
data() {
return {
model: appModel,
}
}
});
app.mount('#mount');
<script type='text/x-template' id='AppView'>
<div>
model.audioNode.gain.value: {{model.audioNode.gain.value}}
</div>
<hr>
<div>
<div>Binding to getter/setter for <code>model.gainValue</code> (does NOT work)</div>
<input type='range' min='0' max='1' step='.1' :value="model.audioNode.gain.value" #input="model.gainValue=$event.target.value">
</div>
</script>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id='mount'></div>
The setter in the snippet above does NOT work. Let's consider another snippet:
class MyApp {
constructor() {
this.audio = new AudioContext();
this.audioNode = this.audio.createGain();
this.audioNode.gain.value = .8;
}
get gainValue() { return this.audioNode.gain.value; }
set gainValue(value) { this.audioNode.gain.value = value; }
}
let appModel = new MyApp();
let app = Vue.createApp({
template: '#AppView',
data() {
return {
model: appModel,
}
}
});
app.mount('#mount');
<script type='text/x-template' id='AppView'>
<div>
model.audioNode.gain.value: {{model.audioNode.gain.value}}
</div>
<hr>
<div>
<div>Binding to getter/setter for <code>model.gainValue</code> (does work)</div>
<input type='range' min='0' max='1' step='.1' :value="model.gainValue" #input="model.gainValue=$event.target.value">
</div>
</script>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id='mount'></div>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id='mount'></div>
The setter in the snippet above does work. Take a look at that line <input type='range' min='0' max='1' step='.1' :value="model.gainValue" #input="model.gainValue=$event.target.value"> It is actually what happened when you using v-model="model.gainValue". The reason it works is the line :value="model.gainValue" will trigger Vue re-render anytime model.gainValue is updated. And Vue is not fully selective re-render. So when the whole template is re-render the block {{ model.audioNode.gain.value }} will be re-rendered too.
To prove that Vue is not fully selective re-render, let's consider this snippet:
class MyApp {
constructor() {
this.audio = new AudioContext();
this.audioNode = this.audio.createGain();
this.audioNode.gain.value = .8;
}
get gainValue() { return this.audioNode.gain.value; }
set gainValue(value) { this.audioNode.gain.value = value; }
}
let appModel = new MyApp();
let app = Vue.createApp({
template: '#AppView',
data() {
return {
model: appModel,
anIndependentProperty: 1
}
},
methods: {
update(event){
this.model.audioNode.gain.value = event.target.value
this.anIndependentProperty = event.target.value
}
}
});
app.mount('#mount');
<script type='text/x-template' id='AppView'>
<div>
model.audioNode.gain.value: {{model.audioNode.gain.value}}
</div>
<div>
anIndependentProperty: {{anIndependentProperty}}
</div>
<hr>
<div>
<div>anIndependentProperty trigger re-render so the template will be updated</div>
<input type='range' min='0' max='1' step='.1' :value="model.audioNode.gain.value" #input="update">
</div>
</script>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id='mount'></div>
In the example above the anIndependentProperty is reactive and it will trigger Vue re-render anytime it is updated. When Vue re-render the template the block {{model.audioNode.gain.value}} will be update too.
Solution
This solution only works for the case using properties in the template. If you want to use the computed from your class properties, you have to roll with setter/getter method.
class MyApp {
constructor() {
this.audio = new AudioContext();
this.audioNode = this.audio.createGain();
this.audioNode.gain.value = .8;
}
}
let appModel = new MyApp();
let app = Vue.createApp({
template: '#AppView',
data() {
return {
model: appModel,
reactiveControl: 0
}
},
});
app.mount('#mount');
<script type='text/x-template' id='AppView'>
<input type="hidden" :value="reactiveControl">
<div>
<div>Binding to <code>model.audioNode.gain.value (works):</code> {{model.audioNode.gain.value}} </div>
<input type='range' min='0' max='1' step='.1' :value="model.audioNode.gain.value" #input="model.audioNode.gain.value=$event.target.value; reactiveControl++">
</div>
<div>
<div>Binding to other property <code>model.audioNode.channelCount (works):</code> {{model.audioNode.channelCount}}</div>
<input type='range' min='1' max='32' step='1' :value="model.audioNode.channelCount" #input="model.audioNode.channelCount=$event.target.value; reactiveControl++">
</div>
You can bind to any property now...
</script>
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id='mount'></div>
Please notice this line:
<input type="hidden" :value="reactiveControl">
Whenever the reactiveControl variable changes, the template will be updated and other variables will be updated too. So you just need to change the value of reactiveControl whenever you update your class properties.
Related
I worked with Vue2, but I recently try Vue 3.
I have simple problem:
<input ref="myinput" />
<button #click="submitData" />
I want to set "focus" on "myinput", inside function "submitData".
In Vue 2 it is simple (this.$refs ...), but in Vue 3, they made it complicated.
I saw example with "setup", but is no use for me + I think you can only access "value" from element.
Is there any way to execute "focus" on on element inside methods?
You are still able to do the same thing using Vue 3, but if you work with composition api there's some difference :
Options API :
const {
createApp
} = Vue;
const App = {
data() {
return {
}
},
methods: {
submitData() {
this.$refs.myinput.focus()
}
},
mounted() {
}
}
const app = createApp(App)
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app">
Vue 3 app
<input ref="myinput" />
<button #click="submitData">
Submit
</button>
</div>
composition API:
const {
createApp,
ref,
onMounted,
} = Vue;
const App = {
setup() {
const myinput = ref(null)
function submitData() {
myinput.value.focus()
}
return {
myinput,
submitData
}
}
}
const app = createApp(App)
app.mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.11/dist/vue.global.prod.js"></script>
<div id="app">
Vue 3 app
<input ref="myinput" />
<button #click="submitData">
Submit
</button>
</div>
In case someone comes to this question looking for a way to set the autofocus of a specific element in Vue3, you can achieve it using a Vue Custom Directive
const { createApp, onMounted } = Vue;
const app = createApp({})
// Register a global custom directive called `v-focus`
app.directive('focus', {
// When the bound element is mounted into the DOM...
mounted(el) {
// Focus the element
el.focus()
}
})
app.mount('#app')
<script src="https://unpkg.com/vue#next"></script>
<div id="app">
<input />
<input v-focus />
<input />
</div>
In some cases when the input is hidden under a v-show or v-if it is necessary to do a nextTick for the focus to work.
<span
v-show="!editMode"
#click="handleEditionMode"
>
{{ content }}
</span>
<input
v-show="editMode"
ref="input"
v-model="content"
aria-describedby="item-content"
name="content"
type="text"
tabindex="0"
#focusout="editMode = false"
#keydown.enter="editMode = false"
/>
const input = ref(null),
editMode = ref(false);
const handleEditionMode = () => {
editMode.value = true;
nextTick(() => {
input.value.focus();
});
};
Easiest answer I found is missing here
<input type="text" autofocus />
I was trying to select a specific input upon loading the form component.
The above examples were not useful, so I figured it out myself.
This is far simpler, IMHO. Add 1 ref tag and 1 line of code in the mounted hook.
Place a ref tag on the item you'd like to focus. Here I named it "formStart" but you can name yours whatever you like.
<form #submit.prevent="createNewRoute">
<label for="code">Code</label>
<input v-model="code" id="code" type="text" ref="formStart" /> <!-- REF TAG HERE -->
<label for="type">Type</label>
<input v-model="type" id="type" type="text" />
/* Other inputs hidden for simplicity */
<button type="submit">Add Route</button>
</form>
Reference that ref tag in the mounted hook and focus() it.
<script>
export default {
/* Other options hidden for simplicity */
mounted() {
this.$refs.formStart.focus(); // FOCUS ELEMENT HERE
},
};
</script>
Another vanilla solution is:
document.getElementById("code")?.focus()
to be called on onMounted
<div
v-if="openmodal"
tabindex="0"
v-focus
#keydown.right="() => nextimage(1)"
>
</div>
i used methods of #webcu and added tabindex="0" it's work!
I am building a form in Vue
I have a component that looks as follow:
<template>
<transition name="preview-pane">
<label>{{ option.group }}</label>
<input type="text" class="form-control"
:name="`group_name[${index}]`"
v-on:input="option.group = $event.target.value"
:value="option.group">
<a ref="#" class="btn btn-primary float-right" #click="$emit('copy')" role="button">{{ __('Copy') }} </a>
</transition>
</template>
<script>
export default {
props: {
option: {
group: ''
},
index: {}
}
}
</script>
My Vue instance is as follow:
var products = new Vue({
el: '#products',
data: {
options: []
},
methods: {
add() {
this.options.push({
group: ''
})
},
copy(index) {
this.options.push(this.options[index])
}
}
})
And last my html looks as follow
<product-option
v-for="(option, index) in options"
:key="index"
:option="option"
:index="index"
#copy="copy(index)">
</product-option>
I have one button that basically takes one of the options and push it once again (copy method on the vue instance). When I run everything seems fine but then when I change the input it update the props of all the components that have been copied.
What can I do to make vue understand that each component should work separately?
Well in case someone have the same issue, I sort it out like this:
copy(index) {
var object = this.options[index]
var newObject = {}
for (const property in object) {
newObject[property] = object[property]
}
this.options.push(newObject)
}
Consider the following Vue component
<template>
<div class="myclass">
<div class="myotherclass">I am in {{getcurrentClass()}}</div>
</div>
</template>
<script>
export default {
data() {
return {
currentClass: null
}
},
methods: {
getCurrentClass() {
// code to get the class in the context of the place
// it is called in the template
}
}
}
</script>
What I am trying to understand is how to account for the "template context" in a method. In the code above getCurrentClass() would return the class of the element (this is an example, it can be explained to "name of the element", or "id of the element", ...).
Is this at all possible? If so I would appreciate even a general pointer (and I can post the response once I have a solution) - I am not sure which direction I should be looking at to start with.
You can use basic javascript dom referencing techiniques inside the mounted lifecycle hooks.
<template>
<div class="myclass">
<div id="elem" :class="getcurrentClass('myotherclass')">I am in {{ currentClass }}</div>
</div>
</template>
<script>
export default {
data() {
return {
currentClass: null
}
},
methods: {
getCurrentClass(className) {
this.currentClass = className;
return className;
}
},
mounted() {
// here you can get reference to a dom element by using basic javascript dom referencing techniques
const elem = document.querySelector("#elem");
element.classList ....
}
}
</script>
You can also use the Vue $refs object. If used on a child component, it will give you the reference to that child component instance. If used on a plain DOM element, it will give a reference to that element. Read more from here
<template>
<div class="myclass">
<div ref="myElem" :class="getcurrentClass('myotherclass')">I am in {{ currentClass }}</div>
</div>
</template>
<script>
export default {
data() {
return {
currentClass: null
}
},
methods: {
getCurrentClass(className) {
this.currentClass = className;
return className;
}
},
mounted() {
// here you can get reference to a dom element or child component context by using $refs object
const elem = this.$refs.myElem;
myElem.classList ....
}
}
</script>
You could set the currentClass = <className> in state then render it in template:
<template>
<div class="myclass">
<div :class="getcurrentClass('myotherclass')">I am in {{ currentClass }}</div>
</div>
</template>
<script>
export default {
data() {
return {
currentClass: null
}
},
methods: {
getCurrentClass(className) {
this.currentClass = className;
return className;
}
}
}
</script>
I'm writing a re-usable component. It's basically a section with a header and body, where if you click the header, the body will expand/collapse.
I want to allow the consumer of the component to use v-model to bind a boolean to it so that it can expand/collapse under any condition it wants, but within my component, the user can click to expand/collapse.
I've got it working, but it requires the user of the component to use v-model, if they don't, then the component doesn't work.
I essentially want the consumer to decide if they care about being able to see/change the state of the component or not. If they don't, they shouldn't have to supply a v-model attribute to the component.
Here's a simplified version of my component:
<template>
<div>
<div #click="$emit('input', !value)">
<div>
<slot name="header">Header</slot>
</div>
</div>
<div :class="{ collapse: !value }">
<div class="row">
<div class="col-xs-12">
<div>
<slot></slot>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from "vue-property-decorator";
#Component
export default class CollapsibleSection extends Vue {
#Prop({ default: true }) public value: boolean;
}
</script>
Update:
I've come up with a solution that meets my requirements functionally. It's a little more verbose than I would like, so if anyone has a more terse solution, I would love to read about it, and I will gladly mark it as the accepted answer if it meets my requirements with less code/markup.
<template>
<div>
<div #click="toggle">
<div>
<slot name="header">Header</slot>
</div>
</div>
<div :class="{ collapse: !currentValue }">
<div class="row">
<div class="col-xs-12">
<div>
<slot></slot>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop, Watch } from "vue-property-decorator";
#Component
export default class CollapsibleSection extends Vue {
#Prop({ default: true }) public value: boolean;
public currentValue = true;
public toggle() {
this.currentValue = !this.currentValue;
this.$emit('input', this.currentValue);
}
public mounted() {
this.currentValue = this.value;
}
#Watch('value')
public valueChanged() {
this.currentValue = this.value;
}
}
</script>
Your update works and has the right gist in general, but instead of a watcher it would be better to use a computed property. See the docs for computed properties and watchers for more info.
I've excluded the class notation in the below snippet to have it runnable on-site.
Vue.component('expandable', {
props: {
value: {
// Just to be explicit, not required
default: undefined,
validator(value) {
return typeof value === 'boolean' || typeof value === 'undefined';
},
},
},
template: `
<div class="expandable">
<p #click="toggle()">toggle</p>
<slot v-if="isOpen" />
</div>
`,
data() {
return {
internalValue: true,
};
},
computed: {
isOpen() {
return (typeof this.value !== 'undefined') ? this.value : this.internalValue;
},
},
methods: {
toggle() {
this.internalValue = !this.internalValue;
this.$emit('input', !this.isOpen);
}
}
});
new Vue({
el: '#app',
data() {
return {
isOpen: false,
}
}
})
.expandable {
border: 2px solid blue;
margin-bottom: 1rem;
}
<script src="https://unpkg.com/vue"></script>
<div id="app">
<expandable>
<p>no model</p>
</expandable>
<expandable v-model="isOpen">
<p>has model</p>
</expandable>
</div>
I am using Vue2 for buidling a tab-based form. I am using in my main.js
import VueFormWizard from 'vue-form-wizard'
import 'vue-form-wizard/dist/vue-form-wizard.min.css'
Vue.use(VueFormWizard)
import VeeValidate from 'vee-validate'
Vue.use(VeeValidate)
The file AddUser.vue comprises of the following code:
<script>
import swal from 'sweetalert'
export default {
methods: {
validateFirstTab: function () {
this.$validator.validateAll().then((result) => {
if (result) {
return
}
swal('Input Field(s) Validation', 'Please correct the errors!', 'error')
})
}
}
}
</script>
<template>
<div class="wrapper" id="add-user-wrapper">
<section class="content">
<form-wizard #on-complete="onComplete"
shape="tab"
color="#3498db"
error-color="#a94442">
<h2 slot="title">Add a New User</h2>
<tab-content title="User Details" :before-change="validateFirstTab">
<div class="row">
<div class="col-md-12">
<div class="col-md-4">
<div class="form-group">
<label class="control-label">Name</label>
<input v-model="user.name" v-validate data-vv-rules="required|alpha_spaces" data-vv-delay="500" data-vv-as="Name" class="form-control" :class="{'input': true, 'is-danger': errors.has('name') }" type="text" placeholder="Enter Name" name="name" autofocus="true" />
<i v-show="errors.has('name')" class="fa fa-warning"></i>
<span v-show="errors.has('name')" class="help is-danger">{{ errors.first('name') }}</span>
</div>
</div>
</form-wizard>
</section>
</div>
<template>
The problem that I am facing is whenever I am trying to validate the input field it is getting validated correctly but throwing an error on console: "cannot read property then of undefined" while switching to the new tab. I searched through the communities only to get back a solution of returning a Promise with resolve(true) always but still unfortunately, even with a valid input in the first tab, I am unable to switch to the next tab(code not given in the html below).
Can someone help me out in this regard as to what or how should be the approach? As I am quite new to Vue, please let me know if you need any further details
A return value is missing in the validator promise. vue-form-wizard beforeChange function is expecting a boolean.
Here's the TabContent component's beforeChange prop.
/***
* Function to execute before tab switch. Return value must be boolean
* If the return result is false, tab switch is restricted
*/
beforeChange: {
type: Function
},
Here's what actually happens when the promise is resolved.
validateBeforeChange (promiseFn, callback) {
this.setValidationError(null)
// we have a promise
if (isPromise(promiseFn)) {
this.setLoading(true)
promiseFn.then((res) => {
// ^^^
// res is undefined because there is no return value in your case,
// error is catched later on.
//
this.setLoading(false)
let validationResult = res === true
this.executeBeforeChange(validationResult, callback)
}).catch((error) => {
this.setLoading(false)
this.setValidationError(error)
})
// we have a simple function
} else {
let validationResult = promiseFn === true
this.executeBeforeChange(validationResult, callback)
}
},
Try the following:
<script>
import swal from 'sweetalert'
export default {
methods: {
validateFirstTab: function () {
this.$validator.validateAll().then((result) => {
if (result) {
return true
}
swal('Input Field(s) Validation', 'Please correct the errors!', 'error')
return false
})
}
}
}
</script>