Im under the assumption that v-show will show/hide elements based on its passed data.
For some reason, my element will not hide dynamically when v-show is false. When I manually change the variable (showNav) to false, it will be hidden on page load so it seems to functioning properly.
My variable (showNav) depends on scroll. When scroll up, it is set to true, when scroll down it is set to false. I'd like my nav bar to hide on scroll down but that is not happening.
The scroll behavior is behaving properly. Both are properly changing my v-show variable (showNav) to either true or false but the element remains visible at all times.
HTML template
<template>
<div id="home-page">
<Navbar id="navbar" v-show="showNav" :class="{change_background: scrollPosition > 50}" />
</div>
</template>
JS
mounted() {
window.addEventListener('scroll', function(){
// detects new state and compares it with the old one
if ((document.body.getBoundingClientRect()).top > this.scrollPos) {
this.showNav = true;
console.log(this.showNav);
}
else
{
this.showNav = false;
console.log(this.showNav);
}
// saves the new position for iteration.
this.scrollPos = (document.body.getBoundingClientRect()).top;
})
},
data() {
return {
scrollPosition: null,
scrollPos: 0,
showNav: true,
}
},
You need to handle the scroll events by binding one of the methods defined within the methods block to the scroll event of the window. In your case the callback function passed to the scroll event listen will not have access to the vue instance and hence it will not update the reactive properties of vuejs. See the working sample below.
new Vue({
el: "#app",
data() {
return {
scrollPosition: null,
scrollPos: 0,
showNav: true,
}
},
mounted() {
window.addEventListener('scroll', this.handleScroll);
},
beforeDestroy() {
window.removeEventListener('scroll', this.handleScroll); // To avoid memory leakage
},
methods: {
handleScroll(event) {
// detects new state and compares it with the old one
if ((document.body.getBoundingClientRect()).top > this.scrollPos) {
this.showNav = true;
console.log(this.showNav);
} else {
this.showNav = false;
console.log(this.showNav);
}
// saves the new position for iteration.
this.scrollPos = (document.body.getBoundingClientRect()).top;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app" style="min-height: 1000px;">
<span v-show="showNav">You are trying to hide me</span>
</div>
Related
I'm a beginner trying to get my app to pass props that set CSS styles down a chain to child components. I have a listener that checks for view port size, and as the window gets resized, it checks past a certain point and then swaps the css class and passes it down the chain..
I think I may be doing something incorrectly because my child components don't seem to be receiving the new styles and aren't updating in the DOM as I drag the window.
Here is my code.. I removed irrelevant code to make it easier to read:
Page_Listings.vue
<template>
<main>
<section>
<ListingRack
:prp_classes="rackClass"
/>
</section>
</main>
</template>
<script>
import ListingRack from './Listing__Rack.vue';
export default {
name: 'Front_Page__Panel',
data() {
return {
viewportWidth: window.innerWidth
}
},
methods: {},
mounted() { window.onresize = () => this.viewportWidth = window.innerWidth },
components: {ListingRack},
},
computed: {
rackClass: function(){
let theValue;
console.log('>> viewport width is now: ',this.viewportWidth)
if(this.viewportWidth > 1200) {
theValue = "grid_view";
console.log('>> grid view')
}
else {
theValue = 'card_view';
console.log('>> card view')
}
return theValue
}
}
}
</script>
Listing__Rack.vue
<template>
<div class="listing_rack" :class="classes">
<ul>
<li v-for="item in listings" :key="item.postId">
// I removed irrelevant code for hte sake of simplicity in this example.
// listings is a GraphQL returned array of data that generates a list of "listings".
<Listing
:prp_classes=classes
/>
</li>
</ul>
</div>
</template>
<script>
import Listing from './Listing.vue'
export default {
name: 'listing__rack',
data() {
return {
posts: [], // what we get from the database.
listings: [], // what we copy from the database.
classes: this.prp_classes
}
},
props: {
prp_classes: String
},
components: {
Listing
},
watch: {
classes: function(){
//just to check if we're receiving anything...
console.log(">> [Listing_Rack](watch)(classes) there was a change to classes!");
}
}
}
</script>
Listing.vue
<template>
<div :id=id
:class=classes
class="listing"
:style="backgroundStyle"
>
</div>
</template>
<script>
export default {
name: 'Listing',
data() {
return {
classes: this.prp_classes,
backgroundStyle: String
}
},
props: {
prp_classes: String
},
methods: {
checkClasses: function(){
if(this.classes === 'grid_view') this.backgroundStyle = 'background: center / cover no-repeat url(background.jpg);';
}
},
mounted: function() {
this.checkClasses();
},
watch: {
classes: function(){
this.checkClasses();
}
}
}
</script>
My console.logs on rackClass so I know the class swapping part is working, but all my subsequent child components don't seem to be updating accordingly..
Can someone tell me what I'm doing wrong? Is there a better way to do this? How come my props aren't being passed when I drag the window, and how can I dynamically set styles in the DOM?
Your code does not work because of the one big mistake (don't worry, many people do it)
You are passing your classes using props to child components. But instead of using this prop (prp_classes) directly in the child's template, you create an absolutely unnecessary classes property in the data()
Problem with that is that data() is executed only once when the component is created. If the value of the prp_classes prop changes later, classes property from the data() just holds the old value.
To fix this, remove unnecessary classes from the data and use the prop directly in the template...
...bit more explanation by example what is going on:
let prp_classes = 'card_view'
let classes = prp_classes
prp_classes = 'grid_view'
// prp_classes === 'grid_view', classes === 'card_view', prp_classes !== classes
// strings/numbers/Date ...all work the same
let o1 = { a: 1 }
let o2 = o1
o1.a = 2
// o1.a === 2, o2.a === 2, o1 === o2
More to study
I have a component called customize-charts that includes a Vuetify drawer:
<template>
<v-col>
<v-btn style="float: right" class="mr-4 mt-2" small #click="toggleCustomize" v-if="!open">Customize Dashboard</v-btn>
<v-navigation-drawer
v-model="open"
temporary
absolute
right
style="width: 25vw"
>
<span>draw contents</span>
<v-divider />
</v-navigation-drawer>
</v-col>
</template>
<script>
export default {
props: {
data: {
type: Array,
default () { return [] }
},
open: {
type: Boolean,
default () { return false }
}
},
data () {
return {
draggingItem: null
}
},
methods: {
toggleCustomize () {
this.$emit('open')
}
}
}
</script>
As you can see, the boolean that the drawer is listening to is called "open" and it is passed from the parent:
<customize-charts v-if="chartCards.length" :data="chartCards" :open="customizePanel" #updateorder="updateOrder" #toggleshow="toggleShow" #open="customizePanel=!customizePanel"/>
The parent also has the following:
{
data () {
return {
customizePanel: false,
}
}
}
My problem is that when the custom event open is called (#open="customizePanel=!customizePanel"), the drawer opens, but when it closes (user clicks outself of drawer) it does not set customizePanel to false. How can I make this happen?
Problem is you are using prop open with v-model. Props are designed as one way data binding only (passing data from parent to child) and you should not modify it's value in child component (if you open browser Dev Tools, I'm sure you will see nice warning from Vue explaining exactly this) as the new value will be overwritten by parent value on next re-render...
just use computed property for v-model:
computed: {
isOpen: {
get() { return this.open }, // return prop value
set() { this.$emit('open') } // emit event and change prop value in parent's event handler - new value gets propagated back to child
}
}
I am looking for a way to close a component when there it a click outisde of the element.
I tried an addEventListener.
This closes the component but after being closed it will not open again.
window.addEventListener('click', function(e){
if (document.getElementById('shopcartpreview').contains(e.target)){
console.log("Clicked in Box");
} else{
console.log("Clicked outside Box");
$('#shopcartpreview').hide();
}
})
Is there a way to accomplish this?
<template>
<div id="shopcartpreview" v-if="carthover">
<div class="cartitem" v-for="item in cartitems">
<div class="cartitempic"><img class="productImg" width="80px" height="80px" v-bind:src="'assets/products/' + item.image"></div>
<div class="cartitemdetails">
<div class="cartitemname">{{item.name}}</div>
<div class="cartitemqty">1 X </div>
<div class="cartitemprice">€{{item.unit_price}}</div>
</div>
<div class="cartitemdelete">
<img src="assets/images/icon-bin.png" width="15px" height="15px">
</div>
</div>
<div class="carttotal">
<div class="carttotaltext">TOTAL:</div>
<div class="carttotalprice">€2,860.00</div>
</div>
<div class="cartcheckouttbn">PROCEED TO CHECKOUT</div>
<div class="viewcart">VIEW CART</div>
</div>
</template>
<script>
module.exports = {
data: function () {
return{
cartitems: 0,
carthover: false,
}
},
created(){
EventBus.$on('addToCart', (payload) =>{
this.cartitems = payload
}),
EventBus.$on('mouseover', (carthover) =>{
this.carthover = carthover
})
}
}
</script>
I created a div element at the end of the component like that:
<div v-if="isPopup" class="outside" v-on:click="away()"></div>
Where .outside class is specified in CSS as follows:
.outside {
width: 100vw;
height: 100vh;
position: fixed;
top: 0px;
left: 0px;
}
And away() is a method in Vue instance as:
away() {
this.isPopup = false;
}
Easy, works well.
2020.11.10 Update
I find the previous solution I answered has so many errors, so I have to update it.
There are multiple solutions to close a component by clicking outside of it.
Firstly, there are some libraries which handle this problem, for example
simplesmiler/vue-clickaway, the nuxt also use this script if you have read the source code.
Secondly, if you want to implement it manually, here is code:
onClickOutside ( event: Event ) {
const path = event.path || (event.composedPath ? event.composedPath() : undefined)
// check if the MouseClick occurs inside the component
if (path && !path.includes(this.em) && !this.em.contains(event.target as HTMLElement)) {
this.closeThisComponent() // whatever method which close your component
}
}
Then, you must bind this eventHandler onClickOutside to the document.documentElement after open your component and remove this eventHandler from document.documentElement after you close your component.
Please notice the timing and refers to the event loop of JavaScript, you must understand the difference between MicroTask and MacroTask.
For example, open the component
openThisComponent () {
this.showThisCompoennt = true // whatever codes which open your component
// You can also use Vue.$nextTick or setTimeout
requestAnimationFrame(() => {
document.documentElement.addEventListener('click', this.onClickOutside, false)
})
}
closeThisComponent () {
this.showComponent = false // whatever codes which close your component
document.documentElement.removeEventListener('click', this.onClickOutside, false)
}
Demo Fiddle : https://jsfiddle.net/bq8m4fhe/
Create a clickoutside directive ... Detect click outside element
module.exports = {
data: function() {
return {
cartitems: 0,
carthover: false
};
},
directives: {
clickoutside: {
bind: function(el, binding, vnode) {
el.clickOutsideEvent = function(event) {
// here I check that click was outside the el and his childrens
if (!(el == event.target || el.contains(event.target))) {
// and if it did, call method provided in attribute value
vnode.context[binding.expression](event);
}
};
document.body.addEventListener("click", el.clickOutsideEvent);
document.body.addEventListener("touchstart", el.clickOutsideEvent);
},
unbind: function(el) {
document.body.removeEventListener("click", el.clickOutsideEvent);
document.body.removeEventListener("touchstart", el.clickOutsideEvent);
},
stopProp(event) {
event.stopPropagation();
}
}
},
created() {
EventBus.$on("addToCart", payload => {
this.cartitems = payload;
}),
EventBus.$on("mouseover", carthover => {
this.carthover = carthover;
});
}
};
Use that directive like this.
<div id="shopcartpreview" v-if="carthover" v-clickoutside="SHOPPING_CART_HIDE_FUNCTION">
On my app, I have multiple "upload" buttons and I want to display a spinner/loader for that specific button when a user clicks on it. After the upload is complete, I want to remove that spinner/loader.
I have the buttons nested within a component so on the file for the button, I'm receiving a prop from the parent and then storing that locally so the loader doesn't show up for all upload buttons. But when the value changes in the parent, the child is not getting the correct value of the prop.
App.vue:
<template>
<upload-button
:uploadComplete="uploadCompleteBoolean"
#startUpload="upload">
</upload-button>
</template>
<script>
data(){
return {
uploadCompleteBoolean: true
}
},
methods: {
upload(){
this.uploadCompleteBoolean = false
// do stuff to upload, then when finished,
this.uploadCompleteBoolean = true
}
</script>
Button.vue:
<template>
<button
#click="onClick">
<button>
</template>
<script>
props: {
uploadComplete: {
type: Boolean
}
data(){
return {
uploadingComplete: this.uploadComplete
}
},
methods: {
onClick(){
this.uploadingComplete = false
this.$emit('startUpload')
}
</script>
Fixed event name and prop name then it should work.
As Vue Guide: Custom EventName says, Vue recommend always use kebab-case for event names.
so you should use this.$emit('start-upload'), then in the template, uses <upload-button #start-upload="upload"> </upload-button>
As Vue Guide: Props says,
HTML attribute names are case-insensitive, so browsers will interpret
any uppercase characters as lowercase. That means when you’re using
in-DOM templates, camelCased prop names need to use their kebab-cased
(hyphen-delimited) equivalents
so change :uploadComplete="uploadCompleteBoolean" to :upload-complete="uploadCompleteBoolean"
Edit: Just noticed you mentioned data property=uploadingComplete.
It is easy fix, add one watch for props=uploadComplete.
Below is one simple demo:
Vue.config.productionTip = false
Vue.component('upload-button', {
template: `<div> <button #click="onClick">Upload for Data: {{uploadingComplete}} Props: {{uploadComplete}}</button>
</div>`,
props: {
uploadComplete: {
type: Boolean
}
},
data() {
return {
uploadingComplete: this.uploadComplete
}
},
watch: { // watch prop=uploadComplete, if change, sync to data property=uploadingComplete
uploadComplete: function (newVal) {
this.uploadingComplete = newVal
}
},
methods: {
onClick() {
this.uploadingComplete = false
this.$emit('start-upload')
}
}
})
new Vue({
el: '#app',
data() {
return {
uploadCompleteBoolean: true
}
},
methods: {
upload() {
this.uploadCompleteBoolean = false
// do stuff to upload, then when finished,
this.uploadCompleteBoolean = true
},
changeStatus() {
this.uploadCompleteBoolean = !this.uploadCompleteBoolean
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<button #click="changeStatus()">Toggle Status {{uploadCompleteBoolean}}</button>
<p>Status: {{uploadCompleteBoolean}}</p>
<upload-button :upload-complete="uploadCompleteBoolean" #start-upload="upload">
</upload-button>
</div>
The UploadButton component shouldn't have uploadingComplete as local state (data); this just complicates the component since you're trying to mix the uploadComplete prop and uploadingComplete data.
The visibility of the spinner should be driven by the parent component through the prop, the button itself should not be responsible for controlling the visibility of the spinner through local state in response to clicks of the button.
Just do something like this:
Vue.component('upload-button', {
template: '#upload-button',
props: ['uploading'],
});
new Vue({
el: '#app',
data: {
uploading1: false,
uploading2: false,
},
methods: {
upload1() {
this.uploading1 = true;
setTimeout(() => this.uploading1 = false, Math.random() * 1000);
},
upload2() {
this.uploading2 = true;
setTimeout(() => this.uploading2 = false, Math.random() * 1000);
},
},
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<div id="app">
<upload-button :uploading="uploading1" #click="upload1">Upload 1</upload-button>
<upload-button :uploading="uploading2" #click="upload2">Upload 2</upload-button>
</div>
<template id="upload-button">
<button #click="$emit('click')">
<template v-if="uploading">Uploading...</template>
<slot v-else></slot>
</button>
</template>
Your question seems little bit ambiguë, You can use watch in that props object inside the child component like this:
watch:{
uploadComplete:{
handler(val){
//val gives you the updated value
}, deep:true
},
}
by adding deep to true it will watch for nested properties in that object, if one of properties changed you ll receive the new prop from val variable
for more information : https://v2.vuejs.org/v2/api/#vm-watch
if not what you wanted, i made a real quick example,
check it out hope this helps : https://jsfiddle.net/K_Younes/64d8mbs1/
I am making an app which increments a value when you click a + button.
I am following the example from the documentation on Simple State Management.
I have set up an event handling method which increments a state value. This is triggered when a button is clicked. It updates the state value, but the template doesn't update.
To prove this, I have set up console logs in my increment function that fire and reflect the state value as expected. However, the value in the DOM never changes:
I have tried referring to the counterValue in the template as state.counterValue and store.state.counterValue but I get console errors for this.
What am I doing wrong?
Here is my template:
<template>
<div>
<h1>{{store.state.counterValue}}</h1>
<button v-on:click="increment">+</button>
</div>
</template>
Here is my script:
<script>
const store = {
debug: true,
state: {
counterValue: 0
},
increment() {
console.log('updating counterValue...')
this.state.counterValue = this.state.counterValue + 1
console.log(this.state.counterValue)
}
}
export default {
data() {
return {
counterValue: store.state.counterValue
}
},
methods: {
increment: function() {
store.increment()
}
}
}
</script>
The Problem With {{store.state.counterValue}}
From the docs
The mustache tag will be replaced with the value of the msg property on the corresponding data object.
Your data object (i.e. the component/vue-instance) does not have a property named store. To access const store, you need to proxy it through the component:
data() {
return {
store: store
}
},
The Problem With counterValue: store.state.counterValue
This sets this.counterValue equal to the initial value of store.state.counterValue. But there is no code keeping them in sync. So, when store.state.counterValue changes, counterValue will remain the same.
Solution
Proxy const store through the component as explained above. Example:
const store = {
debug: true,
state: {
counterValue: 0
},
increment() {
console.log('updating counterValue...')
this.state.counterValue = this.state.counterValue + 1
console.log(this.state.counterValue)
}
}
new Vue({
el: '#app',
data() {
return {
store: store
}
},
methods: {
increment: function() {
this.store.increment();
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.4/vue.js"></script>
<div id="app">
<h1>{{store.state.counterValue}}</h1>
<button v-on:click="increment">+</button>
</div>