how to use window.innerWidth within computed property - NuxtJS

11,430

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.

Share:
11,430
Yung Silva
Author by

Yung Silva

Updated on June 04, 2022

Comments

  • Yung Silva
    Yung Silva almost 2 years

    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?

  • Yung Silva
    Yung Silva almost 5 years
    I tested it and it works, but it applies the style with a delay because of if (process.client), same when the style logic is inside the mountd .. I need to have the styles ready before reaching the mounted. I tried this option but the code seems incomplete, I could not get it to work, can you help me? I'm not using store, I'd like to store in a global function, for example this.$mq
  • GoldenLab88
    GoldenLab88 almost 5 years
    From your question, it appears you just want to have a responsive layout. Your best option is to write the breakpoints into your css directly and not have the style dependent on a computed property... is this not an option for you? There are other/better options but I'm not sure how your project is set up or what your aim is.
  • Yung Silva
    Yung Silva almost 5 years
    in this case no, because it is a reusable component, soon the stylization will be dynamic and will have a certain level of complexity to the point that only assigning a CSS class would not solve the problem, need to be done with scripting.
  • GoldenLab88
    GoldenLab88 almost 5 years
    My gut says that this is not the right approach, but I don't know what you're building so I trust you! :) Regarding the delay, I suppose you could just display once the computed property is set on the next tick... it's a little bit "hacky" but it might work. I'll edit my answer
  • Yung Silva
    Yung Silva almost 5 years
    I'm really happy to be helping me :) I tested, it works again, but still with delay, only different. If you only use if (process.client) it displays unstressed content, and applies the style with delay. If use if (process.client) along with v-show =" css " it will show the div only when the style is ready, but still with delay compared to other div/DOM. in short, the delay problem continues
  • Yung Silva
    Yung Silva almost 5 years
    I believe the best option is to check innerWidth inside the computed with the help of a plugin injected into and this, the context, then simply do if (this.$ mq < 600) return 'mobile'
  • GoldenLab88
    GoldenLab88 almost 5 years
    Ah I see, I thought that the div was a container of some sort. I believe the delay is because the css doesn't get applied until the next DOM update cycle, so I don't think injecting a global function will solve the delay problem. Ultimately, I think you'll have to do this via css (I'll edit my answer to include this)... maybe if you can show us your mobile/desktop css objects we can help you with that?
  • Yung Silva
    Yung Silva almost 5 years
    I updated my question, I'd love for you to take a look, can you imagine some way to solve it without having to delete the GridLayout component?
  • GoldenLab88
    GoldenLab88 almost 5 years
    Yes, that is a little more complex! haha. Well, I quickly copied and pasted that into a new nuxt project and there is no issue with delay. Everything renders on time. I assume your real code is lot more complex and it doesn't get computed until the next tick. As a quick fix, I suppose you could; 1) Add an inline placeholder until v-show is ready; or 2) Don't show any components on the page until the computed innerWidth is ready... maybe pass innerWidth as a prop to GridLayout? Sorry it's a bit hacky and I can't be of more help here... but that's my 2 cents given I can't reproduce the delay.
  • Yung Silva
    Yung Silva almost 5 years
    When i updated my question i put if (this.mq === 'small') but the correct one is if (this.mq() === 'small'), I updated my question again! if you copy and run in a nuxt project, you will see that the window is not defined error. if you wrap everything inside an if (process.client) you will see the delay regardless of the complexity or size of the page.
  • GoldenLab88
    GoldenLab88 almost 5 years
    Ok, I wrapped everything in process.client. I'm resizing my window width small, medium etc. but I still don't get a delay.
  • Yung Silva
    Yung Silva almost 5 years
    the delay is only on the first load of the component, do a simple test with process.client and another test without process.client, will observe that when it has the condition (process.cliente) it takes a while to apply the styles
  • Yung Silva
    Yung Silva almost 5 years
    to observe more clearly, keep pressing F5 or command + R if it is on the mac
  • GoldenLab88
    GoldenLab88 almost 5 years
    I'm doing exactly that... honestly I have no delay. I'm using Nuxt v2.8.1. I've disabled cache.
  • Yung Silva
    Yung Silva almost 5 years
    look this, can you upload a video too? I'd like to see the behavior you're getting, I know it sounds like little, but this little delay in big things can make the site very ugly.
  • Yung Silva
    Yung Silva almost 5 years
    (process.client) being applied to GridLayout.vue see the delay
  • GoldenLab88
    GoldenLab88 almost 5 years
    Ah yes, I can reproduce it now! The problem was I wasn't doing it in 'infinty', apologies. Yes, this is definitely because it doesn't update until the next DOM update cycle. So with this implementation a quick fix would be to apply v-show on Vue.$nextTick or by waiting until non-falsy value for css like in my answer. Both will produce a tiny delay but at least the user won't see the layout jump.
  • Yung Silva
    Yung Silva almost 5 years
    could you show me the nextTick sweat in this situation?
  • GoldenLab88
    GoldenLab88 almost 5 years
    sure, I've updated my answer. But it essentially the same as css != null solution before. I'm just writing it directly on my phone so there might be mistakes.