VueJS: Custom Directive Doesn't Work Inside v-for - vuejs2

Problem
I have a v-phone directive that formats the value of a form input. The problem is that it doesn't work inside a v-for directive. Can this be remedied? Should it be refactored as a component instead?
Code
<template v-for="(user in record.users">
<!-- ... -->
<input v-model="user.phone" v-phone="user.phone" type="text" />
<!-- ... -->
</template>
Vue.directive('phone', (el, binding, vnode) => {
let characters = _.split(binding.value, '');
let cleanCharacters = _.filter(characters, character => is.alphaNumeric(character));
let cleanValue = _.join(cleanCharacters, '');
let formattedValue = cleanValue.replace(/(\d{3})(\d{3})(\d{4})([a-z0-9])?/, '$1-$2-$3 $4');
if (_.trim(binding.oldValue) === _.trim(formattedValue)) return;
_.set(vnode.context, binding.expression, formattedValue);
});
Environment
Vue 2.3.2

I have same problem(in "vue": "^2.5.2"),but used this to fix it:
mounted(){
this.$nextTick(function () {
//to fix v-for
let _this=this;
setTimeout(function () {
_this.$forceUpdate()
},20)
})
hope it helps

Related

Input checbox v-model not checking with async data

I have a component that at the start of it, fetchs data from database. And it supose to check the input if the value is true or false. But the problem is that is not checking when its true. I tried :checked and it works, but i need the v-model because of the two way binding
code:
<input type="checkbox" v-model="data">
const data = ref(false)
onBeforeMount(async () => {
await data = fetchingData()
})
I didnt write the hole code, because the code its on another computer, this was from head. But i am having poblems with v-model not checking. If i use :checked it works like a charm
Maybe this is a rookie mistake, but without using :checked with :change i am not seing any solution.
You should use data.value instead of data for the reactivity. For the demo purpose I am directly assign the value as true. You can replace that with the API call code.
Live Demo :
<script type="module" src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0-rc.5/vue.esm-browser.js"></script>
<div id="app">
<input type="checkbox" v-model="data"/>
</div>
<script type="module">
import {ref, createApp, onBeforeMount } from 'https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.0-rc.5/vue.esm-browser.js';
const app = createApp({
setup() {
let data = ref(false)
onBeforeMount(() => {
// data = true ❌
data.value = true // ✅
})
return { data };
}
});
app.mount('#app')
</script>
I do not think it is the checkbox, but that you should use the .value when you write to a ref. Otherwise you loose the reactivity and v-model will not work.
onBeforeMount(async () => {
data.value = await fetchingData()
})
Hope this helps.

Attribute :value binding in select tag doesn't update inside Vue 3 component template (Composition API)

I have a drop down menu where options are enumerated and shuffled, so that the selected option becomes the first. This script is working as intended:
<div id="main">
<sub-select :subs="data" #comp-update="onShufflePaths($event)"></sub-select>
</div>
. .
const ui = {
setup() {
let data = ref('first_second_thrid');
const onShufflePaths = (ind) => {
let subs = data.value.match(/[^_]+/g);
const main = subs.splice(ind, 1)[0];
data.value = [main, ...subs].join('_');
}
return {
data, onShufflePaths,
};
},
};
const vueApp = createApp(ui);
vueApp.component('sub-select', {
props: ['subs'],
emits: ['comp-update'],
setup(props, { emit }) {
let subs = computed(() => props.subs.match(/[^_]+/g));
let subpath = computed(() => '0: ' + subs.value[0]);
function onChange(evt) {
emit('comp-update', evt.slice(0,1));
}
return { subs, subpath, onChange };
},
template: `
<select :value="subpath" #change="onChange($event.target.value)">
<option v-for="(v,k) in subs">{{k}}: {{v}}</option>
</select> {{subpath}}`
});
vueApp.mount('#main');
The problem is, if I delete {{subpath}} from the template, the drop down menu comes up with no options selected by default. It looks like :value="subpath" by itself is not enough to update subpath variable when props update, if it's not explicitly mentioned in the template.
How can I make it work?
Basically, I need the first option always to be selected by default.
Thank you!
https://jsfiddle.net/tfoller/uy7k1hvr/26/
So, it looks like it might be a bug in the library.
Solution 1:
wrap select tag in the template in another tag, like this (so it's not the lonely root element in the template):
template: `
<div><select :value="subpath" #change="onChange($event.target.value)">
<option v-for="(v,k) in subs">{{k}}: {{v}}</option>
</select></div>`
Solution 2:
Write a getter/setter to subpath variable, so component definition is as follows:
vueApp.component('sub-select', {
props: ['subs'],
emits: ['comp-update'],
setup(props, { emit }) {
let subs = computed(() => props.subs.match(/[^_]+/g));
let subpath = computed({
get: () => '0: ' + subs.value[0],
set (value) {
emit('comp-update', value.slice(0,1))
}
});
return { subs, subpath };
},
template: `
<select v-model="subpath">
<option v-for="(v,k) in subs">{{k}}: {{v}}</option>
</select>`
});
For having the first option selected by default you need to point to the index 0.
Here your index is the k from v-for
<option v-for="(v,k) in subs" :selected="k === 0">{{k}}: {{v}}</option>
There is no v-model in select, I think that is the main issue. Other is its not clear what you what to do.
please refer the following code and check if it satisfy your need.
// app.vue
<template>
<sub-select v-model="value" :options="options" />
{{ value }}
</template>
<script>
import { ref } from "vue";
import subSelect from "./components/subSelect.vue";
export default {
name: "App",
components: {
subSelect,
},
setup() {
const value = ref(null);
const options = ref(["one", "two", "three"]);
return { value, options };
},
};
</script>
see that I have used v-model to bind the value to sub-select component.
the sub-select component as follows
// subSelect.vue
<template>
<select v-model="compValue">
<template v-for="(option, index) in compOptions" :key="index">
<option :value="option">{{ index }}: {{ option }}</option>
</template>
</select>
</template>
<script>
import { computed } from "vue";
export default {
name: "subSelect",
props: ["modelValue", "options"],
setup(props, { emit }) {
// if value is null then update it to be first option.
if (props.modelValue === null) {
emit("update:modelValue", props.options[0]);
}
const compValue = computed({
get: () => props.modelValue,
set: (v) => emit("update:modelValue", v),
});
// return selected option first in list/Array.
const compOptions = computed(() => {
const selected = props.options.filter((o) => o === compValue.value);
const notSelected = props.options.filter((o) => o !== compValue.value);
return [...selected, ...notSelected];
});
return { compValue, compOptions };
},
};
</script>
in sub-select component i am checking first if modelValue is null and if so set value to be first option.
and also providing compOptions in such sequence that selected options will always be first in list of selection options.
so it satisfies
The first option always to be selected by default.
Selected option will always be first in list of options.
check the code working at codesandbox
edit
jsfiddle as per request
also i suspect that you need options as underscore separated string for that please refer String.prototype.split() for converting it to array and Array.prototype.join() for joining array back to string.
if this is the case please comment so I can update my answer. It should be possible by setting watcher on compOptions and emitting separate event to parent, but I don't think its a good idea!

Get reference to element in method in Vue.js

How can I get reference to the element that fired the method in Vue.js?
I have HTML like this:
<input type="text" v-model="dataField" v-bind:class="dataFieldClass" />
And in my Vue.js viewmodel I have a method:
dataFieldClass: function () {
// Here I need the element and get its ID
// Pseudo code
var elementId = $element.id;
}
I know that it's possible to get the element from event (v-on:click), but this is not an event, it's a simple method returning CSS class for the element according to few conditions of the viewmodel. It should be computable as well, but the problem is the same.
You can get the reference to your element in three ways
1. with Method Event Handlers (doc)
template:
<input type="text" v-model="dataField" v-bind:class="dataFieldClass" />
script:
dataFieldClass: function (e) {
const element = e.target;
}
2. with Inline Handlers (doc)
template:
<input type="text" v-model="dataField" v-bind:class="dataFieldClass($event, otherArgument)" />
script:
dataFieldClass: function (e, otherArgument) {
const element = e.target;
}
3. with Refs (doc)
template:
<input type="text" v-model="dataField" v-bind:class="dataFieldClass" ref="el"/>
script:
dataFieldClass: function () {
const element = this.$refs.el;
}
Maybe you could use ref?
<input type="text" v-model="dataField" v-bind:class="dataFieldClass" ref="el" />
And use it like this:
dataFieldClass: function () {
var elementId = this.$refs.el;
}
See documentation here: https://v2.vuejs.org/v2/api/#ref
What about using the ref pattern. Put ref="someName" in your DOM element, and access it in your method with this.$refs["someName"] (you can pass 'someName' as parameter to your method).
Note that's not a very good pattern except if for some reason you really need the DOM element. Otherwise just pass a relevant parameter to your method.
It's not a good method mainly because it has a major drawback: there is no $refs the first time the vue is rendered (because the element is not present yet). So you should force the vue to render twice.
If you have multiple elements inside a v-for loop, then this.$refs["someName"] becomes an array. You can get it to work with some adaptation, here is an example:
new Vue({
el: '#app',
data() {
return {
fields: [{
name: 'field1',
value: 'value1'
},
{
name: 'field2',
value: 'value2'
}
]
};
},
methods: {
dataFieldClass(index) {
if (!this.$refs.fields) {
// First render, the element is not there yet
return '';
} else {
// Here is the element
console.log(this.$refs.fields[index]);
}
}
},
mounted() {
// Force the instance to render a second time
this.$forceUpdate();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.3.4/vue.js"></script>
<div id="app">
<label v-for="(field, index) in fields">
{{ field.name }}:
<input ref="fields" :value="field.value" v-bind:class="dataFieldClass(index)">
</label>
</div>
You can get the reference from DOM event object. "event.currentTarget" is the property that references the element where the event listener(vuejs method) assigned.
This is standard DOM specification, but you can also use this property in Vuejs.
dataFieldClass: function (event) {
var elementId = event.currentTarget.id;
}
A straightforward solution is to pass a reference to the element in the method to be called.
Here's what worked for me (a pretty basic example to help understand):
new Vue({
el: '#app',
data: {
msg: '',
},
methods: {
// in order to access the HTML element,
// add an argument (namely 'event') in the method definition,
// and access the element's current value by `event.target.value`
updateValue: function(event) {
this.msg = event.target.value;
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input :value="msg" #input="updateValue" autofocus>
<br/>
<h2>
>>> {{ msg }}
</h2>
</div>
This seem to work for me, using ref (if element is nested another element)
<div ref="element">
vm.$refs.element
or $el if targeted element is the outermost
<template><div class="targeted-element">
this.$el
You can use refs as mentioned in other answers here.
Remember, refs cannot apply to computed objects. So be careful when using refs

How to reference text that's in '<slot></slot>' in Vue.js

How to reference text that's in in Vue.js?
Vue.component('component', {
template: `<button><slot></slot></button>`,
created: function() {
// i would like to access the text in slot here
}
});
Note: This answer applies to Vue v2 only.
The content inside the default slot, which is what you are describing, is exposed as this.$slots.default in the Vue. So the most naive way to get the text inside your button would be to use this.$slots.default[0].text.
Vue.component('component', {
template: `<button><slot></slot></button>`,
created: function() {
const buttonText = this.$slots.default[0].text;
}
});
The problem is that there may be more than one node inside the slot, and the nodes may not necessarily be text. Consider this button:
<button><i class="fa fa-check"></i> OK</button>
In this case, using the first solution will result in undefined because the first node in the slot is not a text node.
To fix that we can borrow a function from the Vue documentation for render functions.
var getChildrenTextContent = function (children) {
return children.map(function (node) {
return node.children
? getChildrenTextContent(node.children)
: node.text
}).join('')
}
And write
Vue.component("mybutton", {
template:"<button><slot></slot></button>",
created(){
const text = getChildrenTextContent(this.$slots.default);
console.log(text)
}
})
Which will return all the text in the slot joined together. Assuming the above example with the icon, it would return, "OK".
For Vue 3.
The answer from #bert works well on Vue 2, but Vue 3 slots have a more complex structure.
Here is one way to get the slots text contents (from default slot) on Vue 3.
const getSlotChildrenText = children => children.map(node => {
if (!node.children || typeof node.children === 'string') return node.children || ''
else if (Array.isArray(node.children)) return getSlotChildrenText(node.children)
else if (node.children.default) return getSlotChildrenText(node.children.default())
}).join('')
const slotTexts = this.$slots.default && getSlotChildrenText(this.$slots.default()) || ''
console.log(slotTexts)
Run the code snippet below that get the slot text passed by parent :
I'm using "ref" :
<span ref="mySlot">
this.$refs.mySlot.innerHTML
Careful : <slot ref="refName"></slot> don't works because <slot> are not render on html.
You have to wrap the <slot></slot> with <div></div> or <span></span>
The code :
Vue.component('component', {
template: '<button>' +
'<span ref="mySlot">' +
'Text before<br />' +
'<slot name="slot1">' +
'Text by default' +
'</slot>' +
'<br />Text after' +
'</span>' +
'</button>',
mounted: function() {
console.log( this.$refs.mySlot.innerHTML);
}
});
new Vue({
el: '#app'
});
<script src="https://vuejs.org/js/vue.min.js"></script>
<div id="app">
<component>
<span slot="slot1">I'm overriding the slot and text appear in this.$refs.mySlot.innerHTML !</span>
</component>
</div>
You can access the slot text by joining the innerText of all the children inside the slot.
getSlotText() {
return this.$slots.default.map(vnode => (vnode.text || vnode.elm.innerText)).join('');
},
My use case was pretty simple, I had a default slot with only text.
Here's how I accessed the text in vue3 with script setup:
<script setup lang="ts">
import { computed, useSlots } from "vue";
const slots = useSlots();
const slotText = computed(() => {
return slots.default()[0].children; // This is the interesting line
});
</script>

Vue.js bind to DOM custom event with dots in name (like bootstrap events)

Using Vue 2.1.10
I can bind to DOM events with v-on directive. For example:
v-on:click
To bind to DOM click.
But I can't figure how to bind to an event that has dots in the name. such as "show.bs.modal" from bootstrap.
Currently, I use a workaround binding in the created hook with Regular DOM Methods, but I really would like to use the declarative syntax for that. How can this be achieved? thanks
EDIT:
The question is about allowed syntax: how can I do something like:
Vue.component('comp',{
template:'<div v-on:show.bs.modal="sunrise"></div',
methods:{
sunrise:function(e){
}
}
})
I was facing the very same problem when working on old projects.
Luckily I found the answer here: vue2 doc
<!-- object syntax (2.4.0+) -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
This works on Bootstrap 5.1.1 with Vue 2.16.14:
<div class="modal" v-on="{ 'hide.bs.modal': handleModalClose }">
...
</div>
I think dots are not supported in v-on but you could create a custom directive to create an event listener for that event.
Not sure if there is something easier but something like in the demo below or this fiddle should work.
The demo creates a new event with dots in name but that should also work with bootstrap events (not tested). Please let me know if it's not working with bootstrap and I'll have a look.
Unbinding only works if you're using v-if. If you're removing that element with Javascript directly. The event will still be there.
var helloEvent = new Event('demo.event.hello');
document.addEventListener('demo.event.hello', function(e) {
// this is just for testing event dispatching!
console.log('main event listener');
}, false);
const bindCustomEvent = {
getName: function(binding) {
return binding.arg + '.' +
Object.keys(binding.modifiers).map(key => key).join('.');
},
bind: function(el, binding, vnode) {
const eventName = bindCustomEvent.getName(binding);
console.log(el, eventName);
document.addEventListener(eventName, binding.value);
},
unbind: function(el, binding) {
const eventName = bindCustomEvent.getName(binding);
console.log('unbinding', eventName);
document.removeEventListener(eventName, binding.value);
}
};
Vue.directive('bindCustomEvent', bindCustomEvent);
new Vue({
el: '#app',
data() {
return {
enabled: true,
eventMsg: ''
};
},
methods: {
sunrise: function(e) {
console.log('received event');
this.eventMsg = 'received event';
},
testEvent: function() {
document.dispatchEvent(helloEvent);
},
toggle: function() {
console.log('toggle', this.enabled);
this.enabled = !this.enabled;
if (!this.enabled) {
this.eventMsg = '';
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.js"></script>
<div id="app">
<div v-bind-custom-event:demo.event.hello="sunrise" v-if="enabled">
Hello, {{eventMsg}}
</div>
<!--
The following markup is not working
<div v-on="demo.event.hello:sunrise" v-if="enabled">
Hello, {{eventMsg}}
</div>-->
<button #click="testEvent()">
Change
</button>
<button #click="toggle">
<span v-if="enabled">disable custom event</span>
<span v-else>enable custom event</span>
</button>
</div>