How to propagate keyboard event to parent in Vue 3? - vue.js

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');

Related

how to pass an event from a child component to a parent?

I'm studying vue js3 - I ran into a misunderstanding of linking variables in components. You need to pass the #click event from the child component, so that the value in the parent has changed.
Example:
children component
<div class="btn" id="btn" #click="addToCart(item)"> Add to cart</div>
Parent component
<p >{{cardValue}}</p>
It is necessary to increase the value of {{cardValue}} by 1 when clicking in the child component
As per my understanding, You are working on an e-commerce application where you want to add the items in a cart from a child component but cart items counter is in parent component. If Yes, Then it is necessary to emit an event to parent on click of Add to cart button, So that it will increment the counter by 1.
Live Demo :
const { createApp, ref, defineComponent } = Vue;
const app = createApp({
setup() {
const cartItemsCount = ref(0)
// expose to template and other options API hooks
return {
cartItemsCount
}
}
});
app.component('item', defineComponent({
template: '<button #click="addToCart">Add to Cart</button>',
methods: {
addToCart() {
this.$emit("addToCart");
}
}
}));
app.mount('#app')
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="app">
<div>
Items added into the cart : {{ cartItemsCount }}
</div>
<item #add-to-cart="cartItemsCount += 1"></item>
</div>

Emited event does't handled by parent component in Vue 2

for some reason emited event doesn't handles by parent component
HTML:
<template id="parent-template">
<div>
<h1>Parent: {{message}}</h1>
<child-component message="Child message"></child-component>
</div>
</template>
<template id="child-template">
<div>
<h2>Child: {{message}}</h2>
<button v-on:click="changeMessage('Changed')">Change</button>
</div>
</template>
<div id="app">
<parent-component message="Parent message"></parent-component>
</div>
JS (es5):
Child:
Vue.component("child-component", {
template: "#child-template",
props:['message'],
methods:{
changeMessage: function(newMessage){
this.message = newMessage;
this.$emit("message-changed", newMessage);
}
}
});
Parent:
Vue.component("parent-component", {
template: "#parent-template",
props:['message'],
mounted: function(){
var v = this;
this.on("message-changed", function(newValue){
alert("Emit handled!");
v.message = newValue;
});
}
});
So, everythings looks fine, but nothing happens when event fires. Why?
You cannot check for the emitted event on a mounted function, since the child Vue instances are not instantiated at that point. If you want to run the code AFTER everything has been rendered which is what I am assuming you are after then you need to run the code after a tick.
this.$nextTick(function () { // Your code goes here }
Also, for clarity, I would normally do a v-on:message-changed="parentMethod()" inside of the HTML. That way the parent is not tightly coupled to the child component at the mounted.
<child-component v-on:message-changed="parentMethod()"> </child-component>
Below is the Vue Documentation regarding the mounted information I provided:
https://v2.vuejs.org/v2/api/#mounted

Custom directive v-focus is not working on vuetify component

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()
}
}
}

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.

How split vue single components template section in to smaller subtemplates

My application is being build on vuejs#2 has multiple forms most of the share same html template with add and reset button. As well as same method, resetForm nullifies the "item" property and resets the form, and create method sends the item to the backend.
<div class="row">
<div class="action">
<button class="btn btn-white" #click="create()">✎ Add</button>
<button class="btn btn-white" #click="resetForm()">❌ Reset</button>
</div>
</div>
I can share methods via mixins with each component but I can't share "template partial" same way. How to you approach such scenario?
I tried to create component create-reset-buttons, but I have no way to trigger parent method as each component encapsulates its functionality and does not allow to modify props from the child. Which need to be done in order to reset the parent form.
Components are not allowed to modify the props, but there are ways child can communicate to parent as explained here in detail.
In Vue.js, the parent-child component relationship can be summarized as props down, events up. The parent passes data down to the child via props, and the child sends messages to the parent via events. Let’s see how they work next.
How to pass props
Following is the code to pass props to chile element:
<div>
<input v-model="parentMsg">
<br>
<child v-bind:my-message="parentMsg"></child>
</div>
How to emit event
HTML:
<div id="counter-event-example">
<p>{{ total }}</p>
<button-counter v-on:increment="incrementTotal"></button-counter>
<button-counter v-on:increment="incrementTotal"></button-counter>
</div>
JS:
Vue.component('button-counter', {
template: '<button v-on:click="increment">{{ counter }}</button>',
data: function () {
return {
counter: 0
}
},
methods: {
increment: function () {
this.counter += 1
this.$emit('increment')
}
},
})
new Vue({
el: '#counter-event-example',
data: {
total: 0
},
methods: {
incrementTotal: function () {
this.total += 1
}
}
})