Custom directive v-focus is not working on vuetify component - vue.js

I'm trying to use a vuejs custom directive called focus on a component from vuetify which is v-field-text.
directives: {
focus: {
// directive definition
inserted: function(el) {
el.focus();
}
}
}
I have a todo list, and my todos are printed with v-for, I also have an option to edit todos, whenever i click on edit button todo dispears and todo edit input apears.
I am using this focus directive to auto focusing the input.
However when i use this like this is not working:
<v-field-text v-focus></v-field-text>
But it works like this:
<input v-focus />
When i console.log the el from the directive, i see that its referring to a div element created by vuetify.
How to fix this issue?

The reason you're seeing a div when using v-focus on those elements is because they are being wrapped in a div. To get around this with third party components you don't control the code to, you may use something like the following function:
import Vue from 'vue'
Vue.directive('focus', {
inserted: function(el) {
// Recursion based function for finding an input
// nested within other elements.
let findInput = (el, max_depth = 5) => {
// We found the input, so we return it, which causes
// the entire function stack to pop
if (el.nodeName === 'INPUT') {
return el
}
// Prevent infinite recursion by providing a maximum
// depth, and returning when we've reached that depth
if (max_depth === 0) {
return null
}
// Our current element is not an input, so we need to loop
// over its children and call findInput recursively
for (let child of el.children) {
let input = findInput(child, max_depth - 1)
// We've found our input, return it to unwind the stack
// otherwise, continue through the loop
if (input) {
return input
}
}
// Fallback in case for when el has no children, or we reached the end of the loop with no input
return null
}
// Start searching for the input. We can optionally
// pass a higher max_depth. Use with caution.
let input = findInput(el, 20)
if (input) {
input.focus()
}
}
})
This is using recursion to step through each elements children, searching for an element with nodeName === 'INPUT'.
As an example, the following complex structure would be parsed and the first input found would be focused:
<div v-focus>
<div>
<div>
<div>
<div>
<div>
<div>
<div>
Hello
</div>
<p>
world
</p>
<span>!</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div>
<div>
<div>
<input type="text" value="I will be focused">
</div>
</div>
</div>
</div>

Please try this solution. It's working for me:
directives: {
focus: {
// directive definition
inserted: function (el) {
let childData = el.querySelectorAll("input")[0];
childData.focus()
}
}
}

Related

How to propagate keyboard event to parent in Vue 3?

In my Vue 3 application, I have nested components five levels deep. The top-level component, TopCom and the bottom-level component MostInnerCom both have a #keydown handler.
If MostInnerCom has focus and a key is pressed that MostInnerCom cannot handle, then that event shall be handled by TopCom. How can this be achieved?
I have created a very simple demo using nested divs instead of nested components. Please see this small demo in codepen.
Here is the relevant code snippet, which doesn't work:
Vue.createApp({
setup() {
const keycode = Vue.ref('')
function onKeydownForApp(e) {
keycode.value = '***'
}
function onKeydownForButton(e) {
if (e.code === 'KeyA') {
parent.dispatchEvent(new KeyboardEvent(e.type, e))
} else {
keycode.value = e.code
}
}
return {
keycode,
onKeydownForApp,
onKeydownForButton
}
}
}).mount('#app')
<script src="https://unpkg.com/vue#next"></script>
<main id="app">
<div #keydown="onKeydownForApp" tabindex="-1">
<input type=text>
<div>
<button #keydown.stop="onKeydownForButton">- K -</button>
keycode={{keycode}}
</div>
</div>
</main>
Global variable parent refers to window.parent, i.e. the parent document, not the parent HTML element. A solution would be to add an id attribute to the div that surrounds the button, e.g. <div id="parentDiv"> and then let parent refer to this div:
var parent = document.getElementById('parentDiv');

Vue.js this.$refs empty due to v-if

I have a simple Vue component that displays an address, but converts into a form to edit the address if the user clicks a button. The address field is an autocomplete using Google Maps API. Because the field is hidden (actually nonexistent) half the time, I have to re-instantiate the autocomplete each time the field is shown.
<template>
<div>
<div v-if="editing">
<div><input ref="autocomplete" v-model="address"></div>
<button #click="save">Save</button>
</div>
<div v-else>
<p>{{ address }}</p>
<button #click="edit">Edit</button>
</div>
</div>
</template>
<script>
export default {
data() {
editing: false,
address: ""
},
methods: {
edit() {
this.editing = true;
this.initAutocomplete();
},
save() {
this.editing = false;
}
initAutocomplete() {
this.autocomplete = new google.maps.places.Autocomplete(this.$refs.autocomplete, {});
}
},
mounted() {
this.initAutocomplete();
}
}
I was getting errors that the autocomplete reference was not a valid HTMLInputElement, and when I did console.log(this.$refs) it only produced {} even though the input field was clearly present on screen. I then realized it was trying to reference a nonexistent field, so I then tried to confine the autocomplete init to only when the input field should be visible via v-if. Even with this, initAutocomplete() is still giving errors trying to reference a nonexistent field.
How can I ensure that the reference exists first?
Maybe a solution would be to use $nextTick which will wait for your DOM to rerender.
So your code would look like :
edit() {
this.editing = true;
this.$nextTick(() => { this.initAutocomplete(); });
},
Moreover if you try to use your this.initAutocomplete(); during mounting it cannot work since the $refs.autocomplete is not existing yet but I'm not sure you need it since your v-model is already empty.
I think it's because your "refs" is plural
<input refs="autocomplete" v-model="address">
It should be:
<input ref="autocomplete" v-model="address">

return elements from a vuejs method

I'm a bit new to vuejs and I'm not even sure what exactly am I looking for,
I have this template:
<template>
<md-content class="md-elevation-2">
<div class="md-layout">
<div class="md-layout-item" v-for="key in ruleData">
{{ getKeyOutput(key) }}
</div>
</div>
</md-content>
</template>
and my script is:
<script>
export default {
props: ['ruleData'],
methods: {
getKeyOutput(value) {
switch (typeof value) {
case 'string':
if (/(ban)$/g.test(value)) {
return createElement(`<h1>${ value }</h1>`) // here is the problem
} else {
return value
}
break
case 'number':
return String(value)
break
case 'boolean':
return String(value)
break
default:
return value
break
}
}
}
}
</script>
What I want to do is on some case return the string, and in some other cases like return an HTML component like h1 for example, and I can't seem to understand how I need to do this, or even if I have the correct approach for this.
You have to use v-html directive to render html tags that is stored as a string.
if you don't use v-html then vuejs by default escapes the html tags and therefore the html tags are displayed as a plain text. You don't need to use createElement() at anyplace in your code, just remove it.
Change your vue template code as below and verify if you are getting the expected result
<div
class="md-layout-item"
v-for="(value,key) in ruleData"
:key="key"
v-html="getKeyOutput(value)">
</div>
You don't need to use createElement() anymore, just return the html code as a string or template string
if (/(ban)$/g.test(value)) {
return `<h1>${ value }</h1>`; //problem solved
} else {
return value
}
Read More details about v-html in the docs https://v2.vuejs.org/v2/guide/syntax.html#Raw-HTML

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

Conditionally rendering parent element, keep inner html

Is there any built-in way to go about conditionally showing a parent element?
To illustrate:
<a v-show-but-keep-inner="someCondition">
<span>This is always rendered no matter what</span>
</a
I think it's a job for custom directive. I made this one as a quick POC:
Vue.directive('showButKeepInner', {
bind(el, bindings) {
bindings.def.wrap = function(el) {
// Find all next siblings with data-moved and move back into el
while (el.nextElementSibling && el.nextElementSibling.dataset.moved) {
el.appendChild(el.nextElementSibling).removeAttribute('data-moved')
}
el.hidden = false
}
bindings.def.unwrap = function(el) {
// Move all children of el outside and mark them with data-moved attr
Array.from(el.children).forEach(child => {
el.insertAdjacentElement('afterend', child).setAttribute('data-moved', true)
})
el.hidden = true
}
},
inserted(el, bindings) {
bindings.def[bindings.value ? 'wrap' : 'unwrap'](el)
},
update(el, bindings) {
bindings.def[bindings.value ? 'wrap' : 'unwrap'](el)
}
})
new Vue({
el: '#app',
data: {
someCondition: false
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.min.js"></script>
<div id="app">
<p>
<button v-on:click="someCondition = !someCondition">{{ someCondition }}</button>
</p>
<a v-show-but-keep-inner="someCondition" href="/">
<span>This is always rendered no matter what</span>
</a>
</div>
For Vue v3.x, the following would work:
<component
:is="condition ? 'custom-component' : 'slot'"
custom-component-prop
...
>
...
</component>
For Vue v2.x, a workaround is to do:
<component
:is="condition ? 'custom-component' : 'v-div'"
custom-component-prop
...
>
...
</component>
// VDiv.vue
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
inheritAttrs: false,
}
</script>
The tradeoff is there will be an extra element like div being rendered, since Vue v2.x doesn't support fragment.
I just ran into the same Problem.
Vue.js Core team member LinusBorg provides a great solution for this use case using a functional component with a custom render function:
Vue.component('with-root', {
functional: true,
props: ['show'],
render(h, ctx) {
const children = ctx.children.filter(vnode => vnode.tag) // remove unnecessary text nodes
// console.log(children)
if (children.length !== 1) {
console.warn('this component accepts only one root node in its slot')
}
if (ctx.props.show) {
return children[0]
} else {
return children[0].children
}
}
})
new Vue({
el: '#app',
data: {
show: true
}
})
<script src="https://unpkg.com/vue#2.6.14/dist/vue.js"></script>
<div id="app">
<with-root v-bind:show="show">
<a href="#">
<span>This is always rendered no matter what</span>
</a>
</with-root>
<br>
<button #click="show = !show">Toggle</button>
<pre>{{$data}}</pre>
</div>
His fiddle: https://jsfiddle.net/Linusborg/w9d8ujn8/
Source:
https://forum.vuejs.org/t/conditionally-render-parent-element/9324/2
If someone happens to be using the vue-fragment (https://www.npmjs.com/package/vue-fragment) library, the following works:
<component :is="someCondition ? 'a' : 'fragment'">
<span>This is always rendered no matter what</span>
</component>
That being said, I do not recommend to use one library just to do this. But if you already do, it can be useful.
Without Vuejs or other framework context, solely DOM.
You can't remove a DOM element without removing its children too.
What you could do is get the children of a DOM element and replace them with the parent or something similar like that.
With Vuejs you may be able to hide this functionality behind a directive or component, but I think this would be overcomplicating what you want to achieve.
If you want your anchor not to be clickable in certain cases, you could do something like v-on:click.prevent="yourCondition && xxx()". On top of that you could use css classes to hide the fact that it's still an anchor v-bind:class="{ fakeAnchor: yourCondition}".
Though the simplest solution may be to just duplicate your html.
<a v-show="someCondition">
<span>This is always rendered no matter what</span>
</a>
<span v-show="!someCondition">This is always rendered no matter what</span>
The best solution depends on what your case is. If the real inner content will be much larger it might not be okay to duplicate that. If that's the case you could encapsulate that in another vue component.
Maybe this approach helps you (using 'is'):
<template lang="pug">
component(is=someCondition?"v-show-but-keep-inner":"my-another-component")
v-form
v-layout
v-flex
v-btn(#click="doThat")
</template>
This way parent component changes depending on 'someCondition' and children are the same for both conditions.