Vue: #mouseover.stop still bubbling - vue.js

I need to trigger a mouseover in Vue, but only on an element and its children, but not its parent. According to documentation this should work with the .stop modifier, but for some reason it still bubbles. .self wont work on child elements.
Any ideas what I might be doing wrong?
The code is simple:
<div v-for="(element) in elements" :key="element.id" :class="element.states"
#mouseover.stop="element.states.hover = true"
#mouseleave.stop="element.states.hover = false"></div>
Or on a component:
<my-component v-for="(element) in elements" :key="element.id" :class="element.states"
#mouseover.native.stop="element.states.hover = true"
#mouseleave.native.stop="element.states.hover = false"></my-component>

I've created a simple snippet to show how you can use #mouseover and #mouseleave with the stop event modifier and it seems to work, i.e. you only see BodyOver and BodyLeave in the console when entering and leaving the outer element.
new Vue({
el: "#app",
data: () => {
return {
parents: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}
},
methods: {
over(ev) {
console.log(`Over ${ev.target.classList[0]}`);
},
leave(ev) {
console.log(`Leave ${ev.target.classList[0]}`);
},
bodyOver(ev) {
console.log(`Body Over ${ev.target.classList[0]}`);
},
bodyLeave(ev) {
console.log(`Body Leave ${ev.target.classList[0]}`);
}
}
});
.body {
padding: 20px;
background: red;
}
.parent {
padding: 20px;
background: green;
}
.child {
padding: 20px;
background: orange;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="body" #mouseover="bodyOver" #mouseleave="bodyLeave">
Body
<div class="parent" #mouseover.stop="over" #mouseleave.stop="leave" v-for="parent in parents">
Parent
<div class="child">
Child
</div>
</div>
</div>
</div>

After some extensive testing and try&error I found the reason why it was not working:
Due to the DOM nesting the mouseleave event is not fired, when hovering over a child. Also thanks #Shoejep for the test with logging, where you can see this behaviour.
To fix it, I had to use mouseout event on the children which gets fired on the parent when entering a child.

Related

Inline style in vue

I try:
<a v-for='(item, index) in categories' :key='index'>
<div class='slider-categories__slide' :style='{ background: item.background}'>
</div>
</a>
Didn't work. Is it possible? If not, how i can add background for elements? (different background for items)
you can have an object named for example style in each object in the array, in each style object you can have specific style for that object and bind that to the style attribute on the element like :style="item.style".
also if you can't have a dedicated object for the styles in you array's objects you can use the data that you have to construct the appropriate object binding in the v-for, just pay attention to the correct formatting.
check the demo below: (here I used destructuring in v-for but its not necessary)
Vue.config.productionTip = false;
new Vue({
el: '#app',
data: {
items: [{
id: 1,
style: {
background: 'blue'
}
},
{
id: 2,
style: {
background: "url('https://picsum.photos/id/1025/200')",
backgroundSize: 'contain'
}
},
{
id: 3,
style: {
background: "linear-gradient(#e66465, #9198e5)"
},
},
]
},
})
.items {
height: 100px;
width: 100px;
display: inline-block;
border: 2px solid red;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.x/dist/vue.js"></script>
<div id="app">
<div class="items" v-for="{id, style} in items" :key="id" :style="style"></div>
</div>

List Transitions work only for "enter" not for "leave"

Following the example in the docs, I'm using transition-group for a list of items. Strangely it works when items appear (enter), not when they disappear (leave), meaning they slide down in an animated fashion when appearing, but disappear instantly without animation: the leave animation failed. Why?
<template>
<div v-if="notifications.length">
<transition-group name="notifications">
<span
v-for="notification in notifications"
:key="notification.id"
>
<!-- content -->
</span>
</transition-group>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapState({
notifications: state => state.notifications.notifications
})
}
}
</script>
<style lang="scss" scoped>
.notifications-enter-active,
.notifications-leave-active {
transition: all 0.5s;
}
.notifications-enter {
transform: translateY(-100%);
}
.notifications-leave-to {
opacity: 0;
}
</style>
Store
export const mutations = {
DELETE_NOTIFICATION (state, id) {
state.notifications.splice(
state.notifications.findIndex(item => item.id === id),
1
)
}
}
I couldn't reproduce the exact symptom with that code (demo 1), which only transitions on leave instead of enter in your scenario. The reason for that is because the span is display: inline, which prevents the transition.
The Vue docs provide a tip for this:
One important note is that these FLIP transitions do not work with elements set to display: inline. As an alternative, you can use display: inline-block or place elements in a flex context.
So, you can apply display: flex on the transition-group:
<template>
<transition-group class="container">
...
</transition-group>
</template>
<style>
.container {
display: flex;
}
</style>
demo 2
Or display: inline-block on the span to be transitioned:
<template>
<span class="notification-item">
...
</span>
</template>
<style>
.notification-item {
display: inline-block;
}
</style>
demo 3
Turns out by replacing <div v-if="notifications.length"> with <div v-if="notifications"> transitions now work. Even though this doesn't make any sense to me.
If anyone can explain in a comment that'd be nice :)

How to design a reusable dialog box within children components in Vue?

I've been struggling with implementing a dialog box / modal design and behavior from inside of children components in Vue.
So here's the set up, I have a Vue component called "WorkersComponent". This component is just a list of workers assigned to some case fetched from the backend (Laravel). This component is reusable an can be in any place/case/ticket/lookup where a user would want to add workers to.
The component has an "add" button in it. Once clicked, I want a new component to appear at that location (at the click location), which could be a dropdown, modal, dialogue - doesn't really mater. This subcomponent has a search bar and some controls to fetch workers info and add them to the parent component.
My problem is that I can't figure out how to get the nesting / positioning to work. Because it is a child component, its position is always against the parent component, so I can only control it's position within that parent component, but I want it to be displaying on top of other DOM elements and components if necessary - whatever makes sense. Worst case scenario - I want it to be in the middle of the page at least.
Now how do I implement this? I probably want it to be a unique subcomponent, not a global generic modal. On top of it, if it were a global generic, then I have an idea of how to populate the modal with relevant options but how to pass them back to the component that called the modal - no idea. So I'm struggling with the approach. It seems like such a simple thing and yet, I can't find a viable solution.
<workers-component name="Assigned Workers">
<button <!-- Vue controls in here to invoke a modal/dialogue/dropdown --> >Add Worker</button>
<!-- The subcomponent itself -->
<workers-select-component />
</workers-component>
Here's an example from Gmail: wherever this search bar is (let's say it's a parent component), if I click on a triangle, it will expand this other pane, which will (1) appear wherever the search bar is and (2) cover other elements to display it and (3) not dismiss the pane until manually dismissed (which is easy but normal Bootstrap dropdowns don't support this).
Here's a solution:
Vue.component('ToggleDialog', {
props: ['state'],
template: `
<button
#click="$emit('toggle', state)"
class="dialog-button"
>
TOGGLE MODAL
</button>
`
})
Vue.component('DialogModal', {
props: ['state'],
template: `
<div
class="dialog-backdrop"
>
<div
class="dialog-button"
>
<toggle-dialog
:state="state"
#toggle="toggleModal"
/>
</div>
</div>
`,
methods: {
toggleModal(state) {
this.$emit('toggle', state)
}
}
})
new Vue({
el: "#app",
data() {
return {
isModalOpen: false
}
},
methods: {
toggleModal(state) {
this.isModalOpen = !state
}
}
})
.dialog-backdrop {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
height: 100%;
width: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
}
.dialog-button {
padding: 10px 15px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<toggle-dialog :state="isModalOpen" #toggle="toggleModal">
OPEN MODAL
</toggle-dialog>
<dialog-modal v-if="isModalOpen" :state="isModalOpen" #toggle="toggleModal" />
</div>
As you can see the modal is not the child of the button, but the child of the main app. toggle events are emitted (and the modal re-emits it) to the app that controls the state of the modal dialog.
For more complex apps it might not be the best. You could use an event bus (deprecated in Vue3) or Vuex (state management) to overcome this multiple emit-re-emit stuff.
EDIT: NEW SOLUTION
Vue.component('ToggleDialog', {
data() {
return {
isModalOpen: false
}
},
template: `
<div
class="toggle-modal-wrapper"
>
<button
#click="isModalOpen = !isModalOpen"
class="dialog-button"
>
TOGGLE MODAL
</button>
<dialog-modal
v-if="isModalOpen"
#toggle="isModalOpen = !isModalOpen"
>
<slot></slot>
</dialog-modal>
</div>
`
})
Vue.component('DialogModal', {
props: {
innerComponent: {
type: String
}
},
template: `
<div
class="dialog-backdrop"
>
<div>
<slot></slot>
<br />
<button
#click="$emit('toggle')"
class="dialog-button"
>
TOGGLE MODAL
</button>
</div>
</div>
`
})
new Vue({
el: "#app",
})
.toggle-modal-wrapper {
z-index: 10000;
}
.dialog-backdrop {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
height: 100%;
width: 100%;
background: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
}
.dialog-button {
padding: 10px 15px;
}
.other-part {
z-index: 1000;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<toggle-dialog>
<template>
This is the first.
</template>
</toggle-dialog>
<toggle-dialog>
<template>
This is the other.
</template>
</toggle-dialog>
<div class="other-part">
OTHER PART OF THE UI
</div>
</div>
You could try playing with slots if you want a reusable component - or even better: the render function.

Why does this event actually trigger?

Im in a vue project using routing, its a tutorial: https://www.youtube.com/watch?v=Wy9q22isx3U
The repo with the full code is here:
https://github.com/bradtraversy/vue_crash_todolist
My Home.vue looks like this:
<template>
<div id="app">
<AddTodo v-on:add-todo="addTodo"/>
<Todos v-bind:todos="todos" v-on:del-todo="deleteTodo"/>
</div>
</template>
<script>
import AddTodo from '../components/AddTodo'
import Todos from '../components/Todos'
export default {
name: 'home',
components: {
Todos,
AddTodo
},
data() {
return {
todos: [
{
id: 1,
title: "Todo one",
completed: false
},
{
id: 2,
title: "Todo two",
completed: true
},
{
id: 3,
title: "Todo three",
completed: false
}
]
}
},
methods: {
deleteTodo(id){
this.todos = this.todos.filter(todo => todo.id != id)
},
addTodo(newTodo){
this.todos = [...this.todos, newTodo]
}
}
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
Whats important here is the Todos element in the markup.
Its a complex component which imported TodoItem itself.
Now Todos looks like this:
<template>
<div>
<div v-bind:key="todo.id" v-for="todo in todos">
<TodoItem v-bind:todo="todo" v-on:del-todo="$emit('del-todo', todo.id)"/>
</div>
</div>
</template>
<script>
import TodoItem from './TodoItem.vue'
export default {
name: "Todos",
components: {
TodoItem
},
props: ["todos"]
}
</script>
<style scoped>
</style>
And it imported TodoItem, which is here:
<template>
<div class="todo-item" v-bind:class="{'is-complete':todo.completed}">
<p>
<input type="checkbox" v-on:change="markComplete">
{{todo.title}}
<button #click="$emit('del-todo', todo.id)"class="del">x</button>
</p>
</div>
</template>
<script>
export default {
name: "TodoItem",
props:["todo"],
methods: {
markComplete(){
this.todo.completed = !this.todo.completed
}
}
}
</script>
<style scoped>
.todo-item {
background: #f4f4f4;
padding: 10px;
border-bottom: 1px #ccc dotted;
}
.is-complete {
text-decoration: line-through;
}
.del {
background: #ff0000;
color: #fff;
border: none;
padding: 5px 9px;
border-radius: 50%;
cursor: pointer;
float: right;
}
</style>
Now what confuses me is the syntax surrounding the emitted events.
In TodoItem, I have this emitted event from the button:
<button #click="$emit('del-todo', todo.id)"class="del">x</button>
Now this is completely understandable for me because we have the event trigger specified with "#click".
This is then exported to parent, Todos.vue, and there we can see this:
<TodoItem v-bind:todo="todo" v-on:del-todo="$emit('del-todo', todo.id)"/>
Here Im starting to get confused.
Again, in the long syntax, a event trigger is defined:
v-on:del-todo
But del-todo is not an event trigger. Its neither click, nor change, nor input.
So how can this code even work? What does vue.js imply when it encounters code like above?
My confusion then gets even worse in Home.vue
<Todos v-bind:todos="todos" v-on:del-todo="deleteTodo"/>
For the third time, an event trigger is specified.
And for the second time, this event trigger doesn't specify a "native" trigger like click.
Now I already wrapped my head around this and I could at least beat SOME sense into it.
In Todos.vue and Home.vue, the specified events seem to execute when del-todo has fired. So they are like callbacks, they take the return value of del-todo.
In Todos.vue, triggering del-todo emits del-todo to its parent, Home.vue.
Is that correct?
Home.vue then triggers deleteTodo when del-todo is fired.
However, deleteTodo requires an id to be handed over through the parameter, but interestingly, <Todos v-bind:todos="todos" v-on:del-todo="deleteTodo"/> doesnt.
Still, the function works. So how does id ever arrive in deleteTodo?
A similar problem arises in TodoItem.vue. Here, del-todo is called, but actually we haven't any sort of declaration of this function anywhere in the script inside TodoItem.vue. So again, what does vueJS imply when it encounters a situation where a function is emitted/called which wasn't defined anywhere?
In Vue.js there are not only the events click change and input, it also allows you to define custom events. All you have to do ist throw an event in the child-component with $emit('my-custom-event', param1, param2) and catch it in the direct parent-component with v-on:my-custom-event="handler" (you can also write #my-custom-event, thats synonym). The handler is a function that takes the parameters passed when emitting the event. Your handler in the Todos-component catches the event del-todo and throws a new event with the same name. The Home-component catches that event and has its function deleteTodo defined as its handler, so this function is being called (the id that was passed with the event is the parameter for deleteTodo).
What is happening is that each time you click there is an event AND value emitted to the parent component, the parent component has a listener v-on:del-todo means it is listening on the del-todo event, once it is triggered/handled it emits it again one level up until it reaches the component where you want to actually manipulate the data (delete the item based on id).
Note: the value is implicitly passed into the handler function deleteTodo so even though it is not explicitly there (i.e. deleteTodo($event) it is there.

Referring to properties passes as props in Vue.js components

I've started working on a board game prototype and decided to go with Vue.js. I have some experience with JavaScript and everything was going fine ... until I tried to access a property passed with 'props' in a component.
Here's the whole code:
<!DOCTYPE HTML>
<html>
<head>
<title>Board</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<style type="text/css">
#board {
width: 600px;
}
.square { width: 100px; height: 100px; margin: 1px; border: 1px solid grey; display: inline-block; float: left; }
</style>
</head>
<body>
<div id="board">
<square v-for="square in squares"></square>
</div>
<script>
var app = new Vue({
el: '#board',
data: {
squares: []
}
})
const rows = 5
const cols = 5
const reservedLocation = { row: 2, col: 2 }
Vue.component('square', {
props: [
'row',
'col',
'type',
],
template: '<div class="square" v-on:click="logLocation"></div>',
methods: {
logLocation: function() {
console.log(this)
console.log("Location: " + this.col + "x" + this.row )
},
},
})
for (var row=0; row<rows; row++) {
for (var col=0; col<cols; col++) {
const type = (row == reservedLocation.row && col == reservedLocation.col) ? 'reserved' : 'empty'
app.squares.push({ row: row, col: col, type: type })
}
}
</script>
</body>
</html>
What's happening there is the "board" div is filled with the "square" components. Each square component has the 'row', 'col' and 'type' properties, passed to it as 'props'. When the user click on a square, the 'logLocation' function of the corresponding component is called and all that function does is, it logs the 'row' and 'col' properties.
Everything works fine except the message logged is: "Location: undefinedxundefined", in other words, both this.col and this.row seems to be undefined. I've checked 'this', and it seems to be the correct component.
I'm sure it's something obvious but I couldn't find an answer in either the official documentation, in tutorials or even here, on Stack Overflow itself – perhaps I'm not using the correct terms.
A bit of new info: the 'row' and 'col' properties are set on the component object and in the '$props' property but the value they return in 'undefined'. Am I, somehow, passing the parameters incorrectly?
Solution
Turns out, there is a section in the Vue.js documentation dedicated specifically to using 'v-for' with components: "v-for with a Component" and here's the relevant portion of the code:
<div id="board">
<square
v-for="square in squares"
:key="square.id"
:row="square.row"
:col="square.col"
:type="square.type"
></square>
</div>
Huge thanks to Stephen Thomas for pointing me in the right direction!
You've defined the props correctly, and you're accessing the props correctly, but you haven't actually set them to any value. The markup:
<square v-for="square in squares"></square>
doesn't pass the props to the component. Perhaps you want something like
<div v-for="row in rows" :key="row">
<div v-for="col in cols" :key="col">
<square :row="row" :col="col"></square>
</div>
</div>
Try to use
console.log("Location: " + this.$props.col + " x " + this.$props.row )