I'm working on a nuxt.js project, where I need to determine styles within the computed propety and apply on a div based on screen size, as in the example below:
basic example
<template>
<div :style="css"></div>
</template>
<script>
export default {
computed: {
css () {
let width = window.innerWidth
// ... mobile { ... styles }
// ... desktop { ... styles }
// ... if width is less than 700, return mobile
// ... if width greater than 700, return desktop
}
}
}
</script>
real example
<template>
<div :style="css">
<slot />
</div>
</template>
<script>
export default {
props: {
columns: String,
rows: String,
areas: String,
gap: String,
columnGap: String,
rowGap: String,
horizontalAlign: String,
verticalAlign: String,
small: Object,
medium: Object,
large: Object
},
computed: {
css () {
let small, medium, large, infinty
large = this.generateGridStyles(this.large)
medium = this.generateGridStyles(this.medium)
small = this.generateGridStyles(this.small)
infinty = this.generateGridStyles()
if (this.mq() === 'small' && this.small) return Object.assign(infinty, small)
if (this.mq() === 'medium' && this.medium) return Object.assign(infinty, medium)
if (this.mq() === 'large' && this.large) return Object.assign(infinty, large)
if (this.mq() === 'infinty') return infinty
}
},
methods: {
generateGridStyles (options) {
return {
'grid-template-columns': (options !== undefined) ? options.columns : this.columns,
'grid-template-rows': (options !== undefined) ? options.rows : this.rows,
'grid-template-areas': (options !== undefined) ? options.areas : this.areas,
'grid-gap': (options !== undefined) ? options.gap : this.gap,
'grid-column-gap': (options !== undefined) ? options.columnGap : this.columnGap,
'grid-row-gap': (options !== undefined) ? options.rowGap : this.rowGap,
'vertical-align': (options !== undefined) ? options.verticalAlign : this.verticalAlign,
'horizontal-align': (options !== undefined) ? options.horizontalAlign : this.horizontalAlign,
}
},
mq () {
let width = window.innerWidth
if (width < 600) return 'small'
if (width > 600 && width < 992) return 'medium'
if (width > 992 && width < 1200) return 'large'
if (width > 1200) return 'infinty'
}
}
}
</script>
<style lang="scss" scoped>
div {
display: grid;
}
</style>
making use of the GridLayout component on pages.vue
<template>
<GridLayout
columns="1fr 1fr 1fr 1fr"
rows="auto"
gap="10px"
verital-align="center"
:small="{
columns: '1fr',
rows: 'auto auto auto auto',
}"
>
<h1>1</h1>
<h1>2</h1>
<h1>3</h1>
<h1>3</h1>
</GridLayout>
</template>
<script>
import { GridLayout } from '#/components/bosons'
export default {
layout: 'blank',
components: {
GridLayout
},
}
</script>
<style lang="scss" scoped>
h1 {
background: #000;
color: #fff;
}
</style>
does not work, it generates a error windows is note defined in if
(this.mq() === 'small')
This works perfectly in pure Vue.js but I understand that it does not work on Nuxt.js because it is server side rendering, it makes perfect sense, but how could I make it work?
the closest I got was moving the style code into the mounted method or wrapping the style code in if (process.client) {...}, but any of the alternatives would generate a certain delay, jump in content, example:
process.client vs without the process.client
jump / delay on the layout when uses process.client condition
how could I make it work without delay? how could I have the screen width before the mounted, default behavior of Vue.js?
I suspect it's because the Nuxt framework is attempting to compute it on the server-side where there is no window object. You need to make sure that it computes it in the browser by checking process.client:
export default {
computed: {
css () {
if (process.client) {
let width = window.innerWidth
// ... mobile { ... styles }
// ... desktop { ... styles }
// ... if width is less than 700, return mobile
// ... if width greater than 700, return desktop
} else {
return { /*empty style object*/ }
}
}
}
}
Regarding the delay, it's a little bit "hacky" but you could return null if window is not available and simply display once the computed property becomes available. You would still have a delay before it becomes visible, as the root of the problem is that the style is getting applied on the next DOM update.
<template>
<div :style="css" v-show="css">
</div>
</template>
<script>
export default {
computed: {
css () {
if (process.client) {
let width = window.innerWidth
// ... mobile { ... styles }
// ... desktop { ... styles }
// ... if width is less than 700, return mobile
// ... if width greater than 700, return desktop
} else {
return null
}
}
}
}
</script>
Alternatively, as the css is applied on the next DOM update you could use a data property with Vue.$nextTick() (but it is essentially the same thing):
<template>
<div :style="css" v-show="reveal">
</div>
</template>
<script>
export default {
data() {
return {
reveal: false
}
},
computed: {
css () {
if (process.client) {
let width = window.innerWidth
// ... mobile { ... styles }
// ... desktop { ... styles }
// ... if width is less than 700, return mobile
// ... if width greater than 700, return desktop
} else {
return { /*empty style object*/ }
}
}
},
mounted() {
this.$nextTick(() => {
this.reveal = true
});
}
}
</script>
However, from your question, it appears that you want to apply a responsive layout. The best approach would be to scope this into your style tags and use css breakpoints. This would solve the delay problem and decouple your style and logic.
<template>
<div class="my-responsive-component">
</div>
</template>
<script>
export default {
computed: { /* nothing to see here! */ }
}
</script>
<style lang="css" scoped>
.my-responsive-component {
height: 100px;
width: 100px;
}
#media only screen and (max-width: 700px) {
.my-responsive-component { background: yellow; }
}
#media only screen and (min-width: 700px) {
.my-responsive-component { background: cyan; }
}
</style>
Btw, just as a side note, use the proper if/else statement in full for computed properties. Using things like if (!process.client) return { /* empty style object */} sometimes produces some unexpected behaviour in Vue computed properties.
It is an old issue which is happening due to server rendering, as stated in below issue on github.
.vue file
<script>
if (process.browser) {
require('aframe')
}
export default {
}
</script>
nuxt.config.js
build: {
vendor: ['aframe']
}
Reference:
https://github.com/nuxt/nuxt.js/issues/30#issuecomment-264348589
https://nuxtjs.org/faq/window-document-undefined/
Related
So I'm currently working on a Vue/Nuxt application and I'm trying to make it possible to switch from mobile to desktop and have certain elements appear using v-if. I've got the v-if currently in this component:
<template>
<div>
<Search />
<div v-if="device" class="header__mobile-menu-toggle">
<a #click="$emit('openMobileMenu', $event)" href="#" title="Open menu"><i class="fas fa-bars"></i></a>
</div>
<div v-if="device" class="header__mobile-menu-close">
<a #click="$emit('closeMobileMenu', $event)" href="#" title="Sluit menu"><i class="fa fa-times"></i></a>
</div>
</div>
</template>
<script>
import Search from './components/Search.vue';
export default {
props: {
data: Object
},
components: {
Search
},
computed: {
device() {
return this.$store.getters['collection/layout/getDevice'];
}
}
}
</script>
As you can see I have a computed property called "device", which is set to false or true in the default.vue layout:
mounted() {
window.addEventListener('resize', this._debounce(determineScreenSize.bind(this), 100))
function determineScreenSize() {
if(window.innerWidth < 1025) {
this.$store.commit('collection/layout/setDevice', true);
}
if(window.innerWidth < 768) {
this.$store.commit('collection/layout/setMobile', true);
}
if(window.innerWidth > 767) {
this.$store.commit('collection/layout/setMobile', false);
}
if(window.innerWidth > 1024) {
this.$store.commit('collection/layout/setDevice', false);
}
}
if(this !== undefined) {
determineScreenSize.apply(this);
}
},
And of course I have this value in the Vuex store (module) with it's appropriate mutation and getter.
export const state = () => ({
data: {},
isDevice: false,
isMobile: false
})
export default {
getters: {
getDevice: state => {
console.log(state.isDevice);
return state.isDevice
}
getMobile: state => {
return state.isMobile
}
}
, mutations : {
setDevice( state, data ) {
state.isDevice = data
}
setMobile( state, data ) {
state.isMobile = data
}
}
}
On initial pageload everything is going as planned. If the user is on a device, it displays it and if not, it doesn't. However, as you can see on resize it changes the state.isDevice and state.isMobile properties. But the template itself doesn't get repainted by Vue/Nuxt. Is there a reason for that? Am I doing something in a completely wrong way?
Thanks in advance!
EDIT: the function is actually called, here's a screenshot with the VueDevtools after resizing the window a few times:
I've tested with CSS/SCSS so far, it seems to work only on first page change :
.page-leave-active {
.news-item {
#for $i from 1 through 10 {
transition-delay: $i * 300ms;
}
transform: translateX(115%);
}
}
I was wondering is there a way to add JS transitions inside a component? Is there an equivalent to mounted, but for unmount?
if you don't use dynamic page transition name for ssr-nuxt page transition, just add the code in transition attribute like examples in nuxtjs document
if you are using a dynamic page transition name, it's a little hard if you use transition attribute that nuxt provided. I use below code for dynamic page transition name, and it works well for me(don't forget code your own <error-view/> component)
use this instead of <nuxt/>, you can add your logic easily in methods
<template>
<main>
<transition
mode="out-in"
:name="transitionName"
#beforeLeave="beforeLeave"
#enter="enter"
#afterEnter="afterEnter">
<router-view v-if="!nuxt.err"/>
<error-view :error="nuxt.err" v-else/>
</transition>
</main>
</template>
<script type="text/javascript">
import Vue from 'vue';
import ErrorView from '#/layouts/error';
export default{
components: {
ErrorView
},
data() {
return {
prevHeight: 0,
transitionName: 'fade'
};
},
beforeCreate () {
Vue.util.defineReactive(this, 'nuxt', this.$root.$options.nuxt);
},
created() {
this.$router.beforeEach((to, from, next) => {
let transitionName = to.meta.transitionName || from.meta.transitionName;
if (transitionName === 'slide') {
const toDepth = to.path.split('/').length;
const fromDepth = from.path.split('/').length;
transitionName = toDepth < fromDepth ? 'slide-right' : 'slide-left';
}
this.transitionName = transitionName || 'fade';
next();
});
},
methods: {
beforeLeave(el) {
this.prevHeight = getComputedStyle(el).height;
},
enter(el) {
const { height } = getComputedStyle(el);
el.style.height = this.prevHeight;
setTimeout(() => {
el.style.height = height;
}, 0);
},
afterEnter(el) {
el.style.height = 'auto';
}
}
}
</script>
Here is what you get
Full resource code of the demo that gif picture shows is here
I've got some questions about vuejs and router ..
window.addEventListener('scroll', ...) also is not detected in my component.
When I typed 'window.scrollY' in console.log. It will always return 0 back to me.
Right scroll(Y) are available and window.innerHeight is not equal 0
I can't detected when client move scroll to bottom
I use vuestic and vue-router
Thank you
created () {
// Not working because window.scrollY always return 0
window.addEventListener('scroll', this.handleScroll);
},
methods: {
handleScroll (event) {}
}
you can try to listen for scrolling a child element.
and using getBoundClientRect:
<template>
<div id="app">
<nav>navbar</nav>
<main id="listen">main</main>
</div>
</template>
<script>
export default {
name: "App",
created() {
document.addEventListener("scroll", this.listenScroll);
},
destroyed() { // remember to remove the listener when destroy the components
document.removeEventListener("scroll", this.listenScroll);
},
methods: {
listenScroll() {
let myScroll = document.querySelector("#listen").getBoundingClientRect()
.top;
console.log(myScroll);
},
},
};
</script>
<style>
nav {
height: 100px;
}
main {
height: 700px;
}
</style>
here there is a codesandbox https://codesandbox.io/s/great-hill-x3wb1?file=/src/App.vue:0-560
First, the terms, "link" is the area where the mouse enters. The "tooltip" is the thing that pops up and shows extra information.
--- above added 2020-04-29
I'm using Vuetify and trying to keep the v-tooltip open when mouse is hovering over the "tooltip".
The content inside the tooltip is going to be rich and don't want that to automatically hide when visitor is looking into it.
<template>
<v-tooltip
v-model="show"
max-width="600px"
content-class="link-tooltip-content"
bottom>
<template v-slot:activator="{ on }">
<div
:style="boxStyle"
#mouseover="mouseover"
#mouseleave="mouseleave"
></div>
</template>
<template v-slot:default>
<v-card
#mouseover="mouseover"
#mouseleave="mouseleave"
>
<v-row dense>
<v-col>
<v-card-title class="headline">
rich tooltip
</v-card-title>
</v-col>
</v-row>
</v-card>
</template>
</v-tooltip>
</template>
<script>
export default {
data: () => ({
show: false,
hoverTimer: null
}),
methods: {
boxStyle: function() {
return {
left: "100px",
top: "100px",
width: "100px",
height: "100px",
position: "absolute"
};
},
mouseover: function() {
console.log(`mouseover`);
this.show = true;
clearTimeout(this.hoverTimer);
},
mouseleave: function() {
console.log(`mouseleave`);
this.hoverTimer = setTimeout(() => {
this.show = false;
}, 3000);
}
}
};
</script>
But this doesn't work. The mouseover and mouseleave event handlers on the activator slot (the "link") element does fire, but the event handlers on the default slot (the "tooltip") don't fire.
I think the reason is, because the content inside the "tooltip" is moved to somewhere else under the body tag.
The questions is, how can I keep the "tooltip" open when hovering over it.
I'm moving the mouse like this:
Hover over the link (the tooltip shows up).
Move the mouse out of the link and into the tooltip. (The link and tooltip is a few pixels apart)
Now the mouseleave event for the link fires, and I want to add a mouseenter event handler on the tooltip. How do I do that ?
I'm thinking to add an mouseenter event on the tooltip, so that I can clearTimeout(hoverTimer) and keep the tooltip open.
I know there's a similar question from 9 years ago, using jQuery Keep tooltip opened when the mouse is over it , but I don't want to use jQuery if possible. I prefer a Vue way.
Here's a little reproducible example:
https://www.codeply.com/p/GuFXqAAU8Y
Instead of using v-tooltip, I suggest you use v-menu with the open-on-hover props set to true.
If you have to nudge whatever you put in the menu, make sure to set an appropriate close-delay value, so the menu doesn't close before the user reaches it.
Example:
https://codepen.io/stephane303/pen/WNwdNxY
<v-menu open-on-hover right offset-x nudge-right="20" close-delay="100">
.v-tooltip__content has pointer-events:none set in vuetify.min.css. If you set it back to auto you allow it to be hovered.
When its hovered, its parent is hovered. And when its parent is hovered, it has a tooltip. So all you need is:
.v-tooltip__content {
pointer-events: auto;
}
I made extended version of VTooltip for this purpose. Just pass interactive prop. See working example here by hovering Governance stepper: https://tzkt.io
<script>
/**
* Extends VTooltip with interactivity
* #see https://material-ui.com/components/tooltips/#interactive
*/
import { VTooltip } from 'vuetify/lib';
export default {
extends: VTooltip,
props: {
interactive: {
type: Boolean,
default: false,
},
closeDelay: {
type: [Number, String],
default: 50,
},
},
computed: {
// I'm not 100% sure in this, but it works
calculatedLeft() {
const originalValue = VTooltip.options.computed.calculatedLeft.call(this);
if (!this.interactive) return originalValue;
const { left, right } = this;
let value = parseInt(originalValue);
if (left || right) {
value += right ? -10 : 10;
}
return `${value}px`;
},
calculatedTop() {
const originalValue = VTooltip.options.computed.calculatedTop.call(this);
if (!this.interactive) return originalValue;
const { top, bottom } = this;
let value = parseInt(originalValue);
if (top || bottom) {
value += bottom ? -10 : 10;
}
return `${value}px`;
},
styles() {
const originalValue = VTooltip.options.computed.styles.call(this);
if (!this.interactive) return originalValue;
const {
top, bottom, left, right,
} = this;
let paddingDirection;
if (bottom) paddingDirection = 'top';
else if (top) paddingDirection = 'bottom';
else if (right) paddingDirection = 'left';
else if (left) paddingDirection = 'right';
return {
...originalValue,
[`padding-${paddingDirection}`]: `${10}px`,
};
},
},
methods: {
onTooltipMouseenter(e) {
if (this.interactive) {
this.clearDelay();
this.isActive = true;
}
this.$emit('tooltip:mouseenter', e);
},
onTooltipMouseleave(e) {
if (this.interactive) {
this.clearDelay();
this.runDelay('close');
}
this.$emit('tooltip:mouseleave', e);
},
genContent() {
const content = this.$createElement('div', this.setBackgroundColor(this.color, {
style: this.contentStyles,
staticClass: 'v-tooltip__content',
class: {
[this.contentClass]: true,
menuable__content__active: this.isActive,
},
}), this.getContentSlot());
return this.$createElement('div', {
style: this.styles,
attrs: this.getScopeIdAttrs(),
class: {
'v-tooltip__wrapper': true,
'v-tooltip__wrapper--fixed': this.activatorFixed,
},
directives: [{
name: 'show',
value: this.isContentActive,
}],
on: {
mouseenter: this.onTooltipMouseenter,
mouseleave: this.onTooltipMouseleave,
},
ref: 'content',
}, [content]);
},
genActivatorListeners() {
const listeners = VTooltip.options.methods.genActivatorListeners.call(this);
if (this.interactive) {
if (listeners.mouseenter) {
listeners.mouseenter = (e) => {
this.getActivator(e);
this.clearDelay();
if (!this.isActive) {
this.runDelay('open');
}
};
}
}
return listeners;
},
},
};
</script>
<style lang="scss">
.v-tooltip__wrapper {
position: absolute;
&--fixed {
position: fixed;
}
.v-tooltip__content {
position: static;
}
}
</style>
I have an nuxt app where I have two sidebars, one on the left and one on the right.
Both are fixed and body has padding from right and left.
In the middle I have <nuxt/> that loads pages.
Left sidebar can be minimized to 60px so I cannot use media queries for this and I need to watch for <nuxt/> width changes, in case that width is < 500px I would add some other classes. Something like media queries for element instead of viewport.
Is there a way to do this without additional javascript libraries, plugins etc..?
updated with two slider example:
A way to achieve the desired behavior will be to bind a dynamic width to your sliders and watch that width prop then bind the classes using the element ref upon changes :
Vue.config.devtools = false;
Vue.config.productionTip = false;
new Vue({
el: "#app",
data() {
return {
silderWidth: {
first: '100',
second: '100'
}
}
},
computed: {
first() {
return this.silderWidth.first
},
second() {
return this.silderWidth.second
}
},
methods: {
toggleWidth(ele) {
this.silderWidth[ele] === '100' ?
this.silderWidth[ele] = "200" :
this.silderWidth[ele] = "100"
}
},
watch: {
first() {
this.$nextTick(() => {
this.silderWidth.first === '200' ?
this.$refs.silderWidth1.classList.add('background') :
this.$refs.silderWidth1.classList.remove('background')
})
},
second() {
this.$nextTick(() => {
this.silderWidth.second === '200' ?
this.$refs.silderWidth2.classList.add('background') :
this.$refs.silderWidth2.classList.remove('background')
})
}
}
})
.silder {
background-color: red;
height: 200px;
}
.silder--1 {
position: fixed;
left: 0;
top: 0;
}
.silder--2 {
position: fixed;
right: 0;
top: 0;
}
.background {
background-color: yellow;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div class="silder silder--1" ref="silderWidth1" :style="{ width : first + 'px'}" #click="toggleWidth('first')"></div>
<div class="silder silder--2" ref="silderWidth2" :style="{ width : second + 'px'}" #click="toggleWidth('second')"></div>
</div>
Hope this helps:
window.onresize = () => {
let width = document.querySelector('nuxt').offsetWidth;
console.log(width);
};