I've started to use Vue CLI but ran into a problem of handling window scroll position.
Just copied this example from Vue docs but it doesn't work.
This is my Nav.vue component:
<template>
<nav v-scroll="handleScroll"></nav>
</template>
<script>
export default {
name: 'my-nav',
data() {
return {
scrolled: false
}
},
directives: {
scroll: {
inserted: function (el, binding) {
let f = function (evt) {
if (binding.value(evt, el)) {
window.removeEventListener('scroll', f)
}
}
}
}
},
methods: {
handleScroll: function (evt, el) {
if (window.scrollY > 50) {
el.setAttribute(
'style',
'opacity: .5; background-color: red;'
)
}
return window.scrollY > 100
}
}
}
</script>
<style lang="scss" scoped>
nav {
position: fixed;
width: 100%;
height: 68px;
background-color: white;
z-index: 100;
}
</style>
An error occurs in this case:
error in ./src/components/Nav.vue
Module Error (from ./node_modules/eslint-loader/index.js):
error: 'f' is assigned a value but never used (no-unused-vars)
Also I searched other approaches how to handle scroll event but none of the worked.
In this case handleScroll method is just ignored:
<template>
<nav v-bind:class="{ hidden: scrolled}"></nav>
</template>
<script>
export default {
name: 'my-nav',
data() {
return {
scrolled: false
}
},
methods: {
handleScroll: function () {
this.scrolled = window.scrollY > 150;
}
},
created: function () {
window.addEventListener('scroll', this.handleScroll);
},
destroyed: function () {
window.removeEventListener('scroll', this.handleScroll);
}
}
</script>
<style>
.hidden {
opacity: .3;
}
</style>
It seemed to me that such simple things are much easier to resolve with Vue but I was wrong.
How to make scroll event work properly?
Your second approach should work, with one little caveat: you are not setting scrolled in the component data properly: you should be using this.scrolled, i.e.:
handleScroll: function () {
this.scrolled = window.scrollY > 150;
}
See proof-of-concept example:
new Vue({
el: '#app',
data() {
return {
scrolled: false
}
},
methods: {
handleScroll: function() {
this.scrolled = window.scrollY > 150;
}
},
created: function() {
window.addEventListener('scroll', this.handleScroll);
},
destroyed: function() {
window.removeEventListener('scroll', this.handleScroll);
}
});
body {
min-height: 200vh;
}
nav {
position: fixed;
top: 20px;
left: 20px;
}
.hidden {
opacity: 0.3
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<nav v-bind:class="{ hidden: scrolled }">NAV</nav>
</div>
Related
I'm on progress to make a dropdown component with vue and popperjs. To be known i'm using vuejs v.2.6.12 and popperjs v.2.9.2 and here is the code
<template>
<button type="button" #click="show = true">
<slot />
<portal v-if="show" to="dropdown">
<div>
<div
style="position: fixed; top: 0; right: 0; left: 0; bottom: 0; z-index: 99998; background: black; opacity: .2"
#click="show = false"
/>
<div
ref="dropdown"
style="position: absolute; z-index: 99999;"
#click.stop="show = autoClose ? false : true"
>
<slot name="dropdown" />
</div>
</div>
</portal>
</button>
</template>
<script>
import { createPopper } from "#popperjs/core";
export default {
props: {
placement: {
type: String,
default: "bottom-end"
},
boundary: {
type: String,
default: "scrollParent"
},
autoClose: {
type: Boolean,
default: true
}
},
data() {
return {
show: false
};
},
watch: {
show(show) {
if (show) {
this.$nextTick(() => {
this.popper = createPopper(this.$el, this.$refs.dropdown, {
placement: this.placement,
modifiers: [
{
name: "preventOverflow",
options: {
boundary: this.boundary
}
}
]
});
});
} else if (this.popper) {
setTimeout(() => this.popper.destroy(), 100);
}
}
},
mounted() {
document.addEventListener("keydown", e => {
if (e.keyCode === 27) {
this.show = false;
}
});
}
};
</script>
When i'm trying to run the code, i get error message Popper: Invalid reference or popper argument provided. They must be either a DOM element or virtual element.
I don't know why i got this error, i think i've put the reference this.$el and the popper this.$refs.dropdown on the right place.
Could anybody here to help me for solving this problem?
Thank You
I created a snippet that works without the mentioned errors.
you used vue-portal: if it's not globally imported in your code, then you have to import it to the SFC
you have to create <portal-target> somewhere, otherwise this.$refs.dropdown won't exist, as it's the default content of the portal.
const createPopper = Popper.createPopper
/* import {
createPopper
} from "#popperjs/core"; */
Vue.component('DropDown', {
props: {
placement: {
type: String,
default: "bottom-end"
},
boundary: {
type: String,
default: "scrollParent"
},
autoClose: {
type: Boolean,
default: true
}
},
data() {
return {
show: false
};
},
watch: {
show(show) {
if (show) {
this.$nextTick(() => {
this.popper = createPopper(this.$el, this.$refs.dropdown, {
placement: this.placement,
modifiers: [{
name: "preventOverflow",
options: {
boundary: this.boundary
}
}]
});
});
} else if (this.popper) {
setTimeout(() => this.popper.destroy(), 100);
}
}
},
mounted() {
document.addEventListener("keydown", e => {
if (e.keyCode === 27) {
this.show = false;
}
});
},
template: `
<button type="button" #click="show = true">
<slot />
<portal
v-if="show"
to="dropdown"
>
<div>
<div
style="position: fixed; top: 0; right: 0; left: 0; bottom: 0; z-index: 99998; background: black; opacity: .2"
#click="show = false"
/>
<div
ref="dropdown"
style="position: absolute; z-index: 99999;"
#click.stop="show = autoClose ? false : true"
>
<slot name="dropdown" />
</div>
</div>
</portal>
</button>
`
})
new Vue({
el: "#app",
template: `
<div>
<drop-down>
<template
v-slot:default
>
Dropdown default slot
</template>
<template
v-slot:dropdown
>
This is the dropdown slot content
</template>
</drop-down>
<portal-target
name="dropdown"
/>
</div>
`
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.12"></script>
<script src="http://unpkg.com/portal-vue"></script>
<script src="https://unpkg.com/#popperjs/core#2"></script>
<div id="app"></div>
I have two buttons, on clicking of one button i need to make an element slide vue transition effect, on clicking of other button i need to make an element fade vue transition effect.
<template>
<transition :name="transition ? 'slide-fade' : 'fade'">
<p>Hello world</p>
</transition>
<button #click="shouldSlide">Slide</button>
<button #click="shouldFade">Fade</button>
<template>
<script>
export default {
data () {
return {
isSlide: false
}
},
computed: {
transition () {
return this.isSlide
}
},
methods: {
shouldSlide () {
this.isSlide = true
},
shouldFade () {
this.isSlide = false
}
}
}
</script>
i know it won't work, because computed happens after the template update. Is there any other way to solve this. Thanks in advance.
Your code seems to be correct. You are missing v-if or v-show to trigger the transition
new Vue({
el: "#aa",
data() {
return {
isSlide: false,
show: false,
}
},
computed: {
transition() {
return this.isSlide
}
},
methods: {
shouldSlide() {
this.isSlide = true
this.show = false;
setTimeout(() => {
this.show = true;
}, 0)
},
shouldFade() {
this.isSlide = false
this.show = false;
setTimeout(() => {
this.show = true;
}, 0)
}
}
})
.slide-leave-active,
.slide-enter-active {
transition: 1s;
}
.slide-enter {
transform: translate(100%, 0);
}
.slide-leave-to {
transform: translate(-100%, 0);
}
.fade-leave-active,
.fade-enter-active {
transition: 1s;
}
.fade-enter {
opacity: 0;
}
.fade-leave-to {
opacity: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<main id="aa">
<transition :name="transition ? 'slide' : 'fade'">
<p v-if="show">Hello world</p>
</transition>
<button #click="shouldSlide">Slide</button>
<button #click="shouldFade">Fade</button>
<main>
Some time ago I created an autocomplete component in vue for a project in which I am involved.
But today I detected a small bug.
When I select the option I want with the click of the mouse, the option does not get transmitted, as you can see in the console.log () that is in the example. If I click on another option again, what will appear in console.log () is the option previously selected.
If I put a setTimeout( () => {}, 200) it already detects and emit the option, but I think it is not the best solution for this case.
Any suggestion?
example
const Autocomplete = {
name: "autocomplete",
template: "#autocomplete",
props: {
items: {
type: Array,
required: false,
default: () => Array(150).fill().map((_, i) => `Fruit ${i+1}`)
},
isAsync: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
isOpen: false,
results: [],
search: "",
isLoading: false,
arrowCounter: 0
};
},
methods: {
onChange() {
console.log( this.search)
// Let's warn the parent that a change was made
this.$emit("input", this.search);
},
setResult(result, i) {
this.arrowCounter = i;
this.search = result;
this.isOpen = false;
},
showAll() {
this.isOpen = !this.isOpen;
(this.isOpen) ? this.results = this.items : this.results = [];
},
},
computed: {
filterResults() {
// first uncapitalize all the things
this.results = this.items.filter(item => {
return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1;
});
return this.results;
},
},
watch: {
items: function(val, oldValue) {
// actually compare them
if (val.length !== oldValue.length) {
this.results = val;
this.isLoading = false;
}
}
},
mounted() {
document.addEventListener("click", this.handleClickOutside);
},
destroyed() {
document.removeEventListener("click", this.handleClickOutside);
}
};
new Vue({
el: "#app",
name: "app",
components: {
autocomplete: Autocomplete
}
});
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
.autocomplete {
position: relative;
width: 130px;
}
.autocomplete-results {
padding: 0;
margin: 0;
border: 1px solid #eeeeee;
height: 120px;
overflow: auto;
width: 100%;
}
.autocomplete-result {
list-style: none;
text-align: left;
padding: 4px 2px;
cursor: pointer;
}
.autocomplete-result.is-active,
.autocomplete-result:hover {
background-color: #4aae9b;
color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<div id="app">
<autocomplete />
</div>
<script type="text/x-template" id="autocomplete">
<div class="autocomplete">
<input type="text" #blur="onChange" v-model="search" #click="showAll" />
<ul id="autocomplete-results" v-show="isOpen" ref="scrollContainer" class="autocomplete-results">
<li class="loading" v-if="isLoading">
Loading results...
</li>
<li ref="options" v-else v-for="(result, i) in filterResults" :key="i" #click="setResult(result, i)" class="autocomplete-result" :class="{ 'is-active': i === arrowCounter }">
{{ result }}
</li>
</ul>
</div>
</script>
You were using onblur event, but its fired when you click outside and before the onclick's item listener, so the value wasn't updated.
Use onchange event to capture data if user types anything in the input and call onChange() method inside setResult().
const Autocomplete = {
name: "autocomplete",
template: "#autocomplete",
props: {
items: {
type: Array,
required: false,
default: () => Array(150).fill().map((_, i) => `Fruit ${i+1}`)
},
isAsync: {
type: Boolean,
required: false,
default: false
}
},
data() {
return {
isOpen: false,
results: [],
search: "",
isLoading: false,
arrowCounter: 0
};
},
methods: {
onChange() {
console.log( this.search)
// Let's warn the parent that a change was made
this.$emit("input", this.search);
},
setResult(result, i) {
this.arrowCounter = i;
this.search = result;
this.isOpen = false;
// Fire onChange, because it won't do it on blur
this.onChange();
},
showAll() {
this.isOpen = !this.isOpen;
(this.isOpen) ? this.results = this.items : this.results = [];
},
},
computed: {
filterResults() {
// first uncapitalize all the things
this.results = this.items.filter(item => {
return item.toLowerCase().indexOf(this.search.toLowerCase()) > -1;
});
return this.results;
},
},
watch: {
items: function(val, oldValue) {
// actually compare them
if (val.length !== oldValue.length) {
this.results = val;
this.isLoading = false;
}
}
},
mounted() {
document.addEventListener("click", this.handleClickOutside);
},
destroyed() {
document.removeEventListener("click", this.handleClickOutside);
}
};
new Vue({
el: "#app",
name: "app",
components: {
autocomplete: Autocomplete
}
});
#app {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}
.autocomplete {
position: relative;
width: 130px;
}
.autocomplete-results {
padding: 0;
margin: 0;
border: 1px solid #eeeeee;
height: 120px;
overflow: auto;
width: 100%;
}
.autocomplete-result {
list-style: none;
text-align: left;
padding: 4px 2px;
cursor: pointer;
}
.autocomplete-result.is-active,
.autocomplete-result:hover {
background-color: #4aae9b;
color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<div id="app">
<autocomplete />
</div>
<script type="text/x-template" id="autocomplete">
<div class="autocomplete">
<input type="text" #change="onChange" v-model="search" #click="showAll" />
<ul id="autocomplete-results" v-show="isOpen" ref="scrollContainer" class="autocomplete-results">
<li class="loading" v-if="isLoading">
Loading results...
</li>
<li ref="options" v-else v-for="(result, i) in filterResults" :key="i" #click="setResult(result, i)" class="autocomplete-result" :class="{ 'is-active': i === arrowCounter }">
{{ result }}
</li>
</ul>
</div>
</script>
blur is the wrong event to use here, and I think you're over complicating it. Simply call your emit in setResult:
setResult(result, i) {
this.arrowCounter = i;
this.search = result;
this.isOpen = false;
this.$emit("input", this.search);
},
I'm working in Vue.js and would like to conditionally display a back to top button when the user scrolls past a certain point. What am I doing wrong (no JQuery)?
In my template:
<div class="scroll">
<span class="scroll_button">Top</span>
</div>
In my mounted() function
const toTop = document.getElementsByClassName('scroll').addEventListener('scroll', function() {
if (window.scrollY > 0) {
this.classList.add('shown')
}
});
toTop();
data () {
return {
scrolled: false
};
},
methods: {
handleScroll () {
this.scrolled = window.scrollY > 0;
}
},
created () {
window.addEventListener('scroll', this.handleScroll);
}
I have something similar to a notes app, and want to be able to drag and drop cards from one group to another (by using react-dnd). Naturally, after a card is dropped, I want to remove it from the source group and add it to the target group. Removing works fine, but the card is not being rendered in the target group. Here is the relevant code:
App = React.createClass({
getInitialState: function() {
...
return {
appState: appState
}
}
removeCard: function(card) {
var content = this.state.appState[card.groupId].content;
content.splice(content.indexOf(card), 1);
this.setState({ appState: this.state.appState });
},
addCard: function(card, target) {
var content = this.state.appState[target.groupId].content;
content.splice(content.indexOf(target) + 1, 0, card);
this.setState({ appState: this.state.appState });
},
onCardDrop: function(source, target) {
this.addCard(source, target); // didn't work
this.removeCard(source); // worked
},
render: function() {
var that = this;
var appState = this.state.appState;
return (
<div>
{_.map(appState, function(group) {
return (
<Group
key={group.id}
id={group.id}
group={group}
onCardDrop={that.onCardDrop} />
)
})}
</div>
)
}
});
So, the card is removed from the source group, but it never appears in the target group even though the console.log of the target group shows the card is there. Is it possible that for some reason the component is not rerendering.
The Group and Card components are rendering ul and li respectively.
I took some time to make a working example based on the code you provided... but it did work. No problems in the code you provided. This indicates that the problem lies elsewhere in your code.
I cannot give you a complete answer because the snippet you provided does not follow the Minimal, Complete, and Verifiable example rule. Though it is minimal, it's incomplete, and also not verifiable.
What I can do is paste the whole code that I made here and hope that it will be useful to you.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello React!</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.7/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
<script src="https://npmcdn.com/react-dnd-html5-backend#2.1.2/dist/ReactDnDHTML5Backend.min.js"></script>
<script src="https://npmcdn.com/react-dnd#2.1.0/dist/ReactDnD.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore.js"></script>
<style>
ul {
display: inline-block;
padding: 10px;
width: 100px;
border: 1px solid gray;
vertical-align: top;
}
li {
display: block;
padding: 0;
width: 100px;
text-align: center;
box-sizing: border-box;
position: relative;
}
li.group {
}
li.card {
height: 100px;
border: 1px solid black;
line-height: 100px;
margin-top: 5px;
font-size: 25px;
font-weight: bold;
cursor: move;
}
li > span {
vertical-align: middle;
display: inline-block;
}
</style>
</head>
<body>
<div id="example"></div>
<script type="text/babel">
window.ItemTypes = {
CARD: "card",
GROUP_TITLE: "group-title"
};
</script>
<script type="text/babel">
var cardSource = {
beginDrag: function (props) {
return { cardId: props.id, groupId: props.groupId, card: props.card };
}
};
function collect(connect, monitor) {
return {
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
}
}
var cardTarget = {
drop: function (props, monitor) {
var item = monitor.getItem();
console.log(item.card)
console.log(props.card)
props.onCardDrop(item.card, props.card);
},
canDrop: function (props, monitor) {
var item = monitor.getItem();
return item.cardId != props.id;
}
};
function collectTgt(connect, monitor) {
return {
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
};
}
window.Card = React.createClass({
propTypes: {
connectDragSource: React.PropTypes.func.isRequired,
isDragging: React.PropTypes.bool.isRequired,
isOver: React.PropTypes.bool.isRequired,
canDrop: React.PropTypes.bool.isRequired
},
renderOverlay: function (color) {
return (
<div style={{
position: 'absolute',
top: 0,
left: 0,
height: '100%',
width: '100%',
zIndex: 1,
opacity: 0.5,
backgroundColor: color,
}} />
);
},
render: function() {
var connectDragSource = this.props.connectDragSource;
var isDragging = this.props.isDragging;
var connectDropTarget = this.props.connectDropTarget;
var isOver = this.props.isOver;
var canDrop = this.props.canDrop;
return connectDropTarget(connectDragSource(
<li className="card" style={{opacity: isDragging ? 0.5 : 1}}
><span>{this.props.card.name}-{this.props.card.groupId}</span>
{isOver && !canDrop && this.renderOverlay('red')}
{!isOver && canDrop && this.renderOverlay('yellow')}
{isOver && canDrop && this.renderOverlay('green')}
</li>
));
}
});
window.Card = ReactDnD.DragSource(ItemTypes.CARD, cardSource, collect)(window.Card);
window.Card = ReactDnD.DropTarget(ItemTypes.CARD, cardTarget, collectTgt)(window.Card);
</script>
<script type="text/babel">
window.Group = React.createClass({
render: function() {
console.log(this.props.group)
var that = this;
return (
<ul>
<li className="group">Group #{this.props.group.id}</li>
{_.map(this.props.group.content, function(card) {
return (
<Card
key={card.name}
id={card.name}
groupId={card.groupId}
card={card}
onCardDrop={that.props.onCardDrop}
/>
)
})}
</ul>
);
}
});
</script>
<script type="text/babel">
window.App = React.createClass({
getInitialState: function() {
return {
appState: [
{
id: 0,
content: [
{
groupId: 0,
name: "C1"
},
{
groupId: 0,
name: "C2"
},
{
groupId: 0,
name: "C3"
},
{
groupId: 0,
name: "C4"
}
]
},
{
id: 1,
content: [
{
groupId: 1,
name: "C5"
},
{
groupId: 1,
name: "C6"
},
{
groupId: 1,
name: "C7"
},
{
groupId: 1,
name: "C8"
}
]
}
]
};
},
removeCard: function(card) {
var content = this.state.appState[card.groupId].content;
content.splice(content.indexOf(card), 1);
this.setState({ appState: this.state.appState });
},
addCard: function(card, target) {
var content = this.state.appState[target.groupId].content;
content.splice(content.indexOf(target) + 1, 0, card);
card.groupId = target.groupId;
this.setState({ appState: this.state.appState });
},
onCardDrop: function(source, target) {
this.removeCard(source); // worked
this.addCard(source, target); // worked
},
render: function() {
var that = this;
var appState = this.state.appState;
return (
<div>
{_.map(appState, function(group) {
return (
<Group
key={group.id}
id={group.id}
group={group}
onCardDrop={that.onCardDrop}
/>
)
})}
</div>
)
}
});
window.App = ReactDnD.DragDropContext(ReactDnDHTML5Backend)(window.App);
</script>
<script type="text/babel">
ReactDOM.render(
<App />,
document.getElementById('example')
);
</script>
</body>
</html>