Long story short, Vue requires some trickery when debugging height and #scroll issues within componenets. The best call of action is to find what element is scrolling and move the scroll to the child, in my case.
So, how can I find what element is scrolling without countless additions/removals of event listeners?
You can get the element that you're scrolling on through the scroll event that gets passed to the scroll handler with event.target. I've included a snippet below which should hopefully help your case scenario.
Vue.config.productionTip = false;
Vue.config.devtools = false;
new Vue({
el: "#app",
methods: {
scrollHandler(ev) {
console.log(ev.target.id);
}
}
});
#app > div > div {
height: 200px;
overflow-y: scroll;
max-height: 100px;
display:block;
}
h1 {
font-size: 3rem;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div>
<div :id="i" v-for="i in 5" :key="i" #scroll="scrollHandler">
<h1>{{i}}</h1>
</div>
</div>
</div>
Related
I've been trying to mock a carousel-like effect in cards rendered through v-for. I have an array of data and I have a method that left rotates that array. I'm passing that rotated array in v-for. But, the rotated arrays shift the real dom div instead of re-rendering the component in v-for (I think this is how Vue behaves for optimization). I've tried transition-group but it only applies transition to leaving and entering div. Is there any way so that I can get a carousel-like effect (divs moving upward) using Vue transition? When I was writing the code, the divs were moving upward and behaving as expected because at that time instead of divs shifting in real dom, only the data inside that div were changing but later on it started to behave like this (divs shifting in real dom)
Here is the fiddle link: https://jsfiddle.net/aanish/7pe5jq9u/4/
Please help me to achieve the expected behavior.
var Box = {
props: ['achievement'],
template: '<transition name="slide-up"><div class="box" :key="achievement">{{achievement}}</div></transition>',
};
new Vue({
el: '#app',
data: {
achievements: ['Title1', 'Title2', 'Title3', 'Title4']
},
created() {
this.autoRotateArr();
},
components: {
'box': Box
},
methods: {
leftRotateArr() {
this.achievements.push(
this.achievements.shift()
)},
autoRotateArr() {
this.interval = setInterval(() => this.leftRotateArr(), 3000);
}
}
})
.box-wrapper {
display: flex;
flex-direction: column;
margin: 0 auto;
padding: 50px;
}
.box {
height: 50px;
background-color: orange;
margin: 10px 0;
}
.slide-up-enter-active {
transition: all 666ms cubic-bezier(0, 0, 1, 1);
}
.slide-up-enter {
transform: translateY(50px);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="hero">
<box :achievement="achievements[0]"></box>
</div>
<div class="box-wrapper">
<div><button #click="leftRotateArr">Up</button></div>
<box v-for="achievement in achievements" :achievement="achievement" :key="achievement"></box>
</div>
</div>
.
From the help of Michal LevĂ˝ comment above, I get the expected behavior using
transition-group
The issue
https://streamable.com/e/9z6lev (the flickering in the video is caused by the overlay being reopened every time meal plan is selected)
It "feels" like during the initial overlay open it's not the focused element and as result is's children can be clicked through :sad:
Overlay Template
The logic for the overlay is quite simple, and allow to nest any type of content inside:
<template>
<div class='swipeableWrapper'
#click.stop.prevent // not original code, just attempt to fix the issue
#touch.stop.prevent> // not original code, just attempt to fix the issue
<slot />
</div>
</template>
.swipeableWrapper {
height: 100%;
left: 0;
min-height: 100%;
position: fixed;
top: 0;
width: 100%;
z-index: 100;
}
Items List Template
<template>
<div>
...
<ListProduct v-for='(product, index) in products'
...
:showProduct='showProduct'
:key='index' />
</div>
<template>
// List Item
<template>
<div class='listProduct'
...
#click='showProduct'>
...
</div>
</template>
Intended approaches:
The following logic added to the overlay template to prevent events from bubbling:
#click.stop.prevent
#touch.stop.prevent
Global logic that will listen to opened overlay and add the following CSS class to the body element, in order to allow click on the overlay items, but still not much luck
.overlayOpened {
& * {
pointer-events: none;
touch-action: none;
}
.swipeableWrapper {
&,
& * {
pointer-events: auto;
touch-action: auto;
}
}
}
I am a bit puzzled with this dark magic behaviour and will really appreciate your opinion on the origin of the behaviour and possible solutions :bow:
Try this
#click.self.prevent="function"
Edited:
For the list item and function as prop
:showProduct="() => showProduct(item/index)"
I'm creating a user card where there's a menu button on the top of the card when clicked, it should show a menu. Whenever I click on the menu of the first item in the loop, then it shows fine. However, there are many user cards and when I click on the menu button of the other cards, the menu still shows on the first card and not on the card that I click. What am I doing wrong here?
<template>
<div class="custom-users" v-if="users && users.length > 0">
<user-card
v-for="user in users"
:key="user.userId"
#open-menu="openMenu"
>
</user-card>
<div class="menu" v-if="showMenu">
<p>Delete</p>
</div>
</div>
</template>
<script>
export default{
data() {
return {
showMenu: false,
};
},
methods: {
openMenu(){
this.showMenu = !this.showMenu;
},
}
}
</script>
<style scoped>
.custom-users{
position: relative;
}
.menu{
position: absolute;
height: 100px;
width: 100px;
top: 60px;
right: 25px;
z-index: 9999;
}
</style>
Absolutely positioned elements are positioned absolutely relative to their most recent parent that has position: relative on it (or position: absolute as well).
You need to put both elements inside a container that has position: relative so that the dropdown will be positioned relative to the card, not to the whole container.
The menu is currently positioned relative to custom-users, which is common to all <user-card>. It simply looks like it's associated with the first <user-card> because they both are aligned to the top left (in normal flow).
Move the menu
<div class="menu" v-if="showMenu">
<p>Delete</p>
</div>
Inside the <user-card> component, apply position: relative; to it's top most element and make the below state and method part of <user-card> as well.
data() {
return {
showMenu: false,
};
},
methods: {
openMenu(){
this.showMenu = !this.showMenu;
},
}
This won't be a big problem for now since menu is only created conditionally. If menu turns out to be a large component on it's own, you can give it the ability to be displayed at coordinates passed in as props in future.
I am using VueJS and trying to fire a mouseover event on two elements, one a child element of the other.
I am unable to get the child mouseover event to fire. It appears the parent element is "covering" the child div and only the parent mouseover event is registered.
var vm = new Vue({
el: '#app',
data: {
hoverTarget: 'none'
},
methods: {
parentHover: function() {
this.hoverTarget = 'parent'
},
childHover: function() {
this.hoverTarget = 'child'
}
}
});
#parent {
width: 100px;
height: 100px;
background: #000000;
}
#child {
width: 50px;
height: 50px;
background: #FFFFFF;
}
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.13/dist/vue.js"></script>
<div id='app'>
<div id='parent' #mouseover="parentHover">
<div id='child' #mouseover="childHover">
</div>
</div>
{{ hoverTarget }}
</div>
Additionally, you could abbreviate this, using an event modifier, to #mouseover.stop="childHover".
<div id='app'>
<div id='parent' #mouseover="parentHover">
<div id='child' #mouseover="childHover">
</div>
</div>
{{ hoverTarget }}
</div>
This is happening because of the event bubbling principal
When an event happens on an element, it first runs the handlers on it,
then on its parent, then all the way up on other ancestors.
that means childHover handler will get executed and immediately after it
the parentHover will be executed making the child execution invisible.
to solve your problem you can use event.stopPropagation() method of the event to make sure no bubbling happens from child to parent.
var vm = new Vue({
el: '#app',
data: {
hoverTarget: 'none'
},
methods: {
parentHover: function() {
this.hoverTarget = 'parent'
},
childHover: function(event) {
event.stopPropagation()
this.hoverTarget = 'child'
}
}
});
I'm building a basic real-time messaging app in Meteor and currently every time a message is created it is appended to the list of messages on the page & everything is working correctly. However once the contents of the messagesList div overflow (I've set it to scroll) I can no longer see the latest messages without scrolling down manually each time.
What is the 'Meteor way' of using callbacks to skip to the bottom automatically (e.g. with Jquery scrolltop) every time the messages list rerenders? I had a go at it in the messages list template helpers but I'm not getting a value returned on scrollTop and I can't figure out if it's my selectors or something to do with Meteor, i.e. where I am putting the code. So I'd really appreciate some pointers.
Here's what I have:
messagesList.html
<template name="messagesList">
<div class="pure-g-r content" id="layout">
<div class="pure-u-1" id="list">
{{#each messages}}
{{> messageItem}}
{{/each}}
</div>
</div>
</template>
messagesList.js:
Template.messagesList.helpers({
messages: function() {
return Messages.find();
}
});
Template.messagesList.rendered = function () {
console.log("scrolltop is " + $('#list').scrollTop);
// $('#list').scrollTop( $('#list').height() )
};
messageItem.html:
<template name="messageItem">
<div class="email-item email-item pure-g">
<div class="pure-u-3-4">
<p>
<strong>{{sentBy}}</strong><br>
{{msgText}}
</p>
</div>
</div>
</template>
MessageSubmit.html:
<template name="messageSubmit">
<form class="pure-form">
<input type="text" name="msg" class="pure-input-rounded">
<button type="submit" class="pure-button">Speak!</button>
</form>
</template>
messageSubmit.js:
Template.messageSubmit.events({
'submit form': function(e) {
e.preventDefault();
console.log('new comment created')
var user = Meteor.user();
var message = {
msgText: $(e.target).find('[name=msg]').val(),
sentBy: user.profile.name,
userId: user._id
}
message._id = Messages.insert(message);
$('.pure-input-rounded').val("");
}
});
main.css:
html, body {
height: 100%;
}
#layout {
max-height: 60%;
left: 20px;
top: 60px;
position: relative;
overflow: scroll;
padding-bottom: 60px;
}
h1 {
font-size: 2em !important;
}
.pure-form {
position: absolute;
left: 10px;
bottom: 20px;
}
I don't think there is a particularly meteor way of doing this, but with this minor fix to your code it should work:
Template.messagesList.rendered = function () {
console.log("scrolltop is " + $('#list').scrollTop());
$('#list').scrollTop( $('#list').prop("scrollHeight") );
};