AlexandreBonaventure / vue-mq

📱 💻 Define your breakpoints and build responsive design semantically and declaratively in a mobile-first way with Vue.
https://alexandrebonaventure.github.io/vue-mq
MIT License
537 stars 59 forks source link

Consider allowing the user to pass a string media query rather than just a max width value #35

Open stephantabor opened 5 years ago

stephantabor commented 5 years ago

would solve issues like #18, #34

What i'm looking to do personally is just use min-width media queries so i can easily match them up with our predefined in our css

VinnyFonseca commented 5 years ago

Same here. It states it's a mobile first approach but uses max-width for breakpoint recognition. An option for min-width would be great.

flyingL123 commented 5 years ago

Yes, please add this. I just spent a while trying to figure out why things weren't working correctly. I was just trying to set the breakpoints to be the same as my tailwind configuration:

Vue.use(VueMq, {
    breakpoints: {
        sm: 640,
        md: 768,
        lg: 1024,
        xl: 1280,
    }
});

It took me longer than I would like to admit to realize that this library was using max width while tailwind and most other CSS frameworks use min width. It will be easier to take the configuration directly from tailwind and use it within vue if they are all min width.

stephantabor commented 5 years ago

What I ended up doing is just using window.matchMedia and setting some state inside of Vuex (or just a Vue component instance when I’m not using vuex) for whatever breakpoints match the framework I’m using, or some other special cases. It’s not as nice as using this lib would be, but I don’t need the breakpoints too often in templates / js so it’ll do for now.

flyingL123 commented 5 years ago

I just adjusted the settings to be max-width but still correspond to the css min-width breakpoints

Vue.use(VueMq, {
    // These are max-width settings
    breakpoints: {
        sm: 767,
        md: 1023,
        lg: 1279,
        xl: Infinity,
    }
});

Not as convenient, but it seems to work fine, unless I'm missing something?

AndrewBogdanovTSS commented 4 years ago

I would really like to switch to vue-mq, but this specific issue is keeping me from doing so. As of now approach that I'm using is pretty similar to what @stephantabor has described. I created a store module and a mixin called media:

media store

import breakpoints from '@/config/tailwind/breakpoints'

export const state = () => ({
  windowWidth: 0
})

export const getters = {
  is: state => breakpoint => {
    return state.windowWidth >= breakpoints[breakpoint]
  }
}

export const mutations = {
  setWindowWidth(state, payload) {
    if (payload !== state.windowWidth) {
      state.windowWidth = payload
    }
  }
}

media mixin

export default {
  methods: {
    media(brakepoint) {
      return this.$store.getters['media/is'](brakepoint)
    }
  }
}

that way I can get exactly the behavior I want. The only caveat to this solution is that I have to subscribe to resize event when app starts:

window.addEventListener('resize', debounce(this.setWindowWidth, 300))
nathanchase commented 4 years ago

Yes, please! I use min-width media queries and this doesn't support that:

I have the following breakpoints:

@custom-media --larger-than-skyscraper (min-width: 160px);
@custom-media --larger-than-iphone-se (min-width: 374px);
@custom-media --larger-than-mobile (min-width: 414px);
@custom-media --larger-than-phablet (min-width: 550px);
@custom-media --larger-than-leaderboard (min-width: 728px);
@custom-media --larger-than-tablet (min-width: 750px);
@custom-media --larger-than-desktop (min-width: 1000px);
@custom-media --larger-than-ipad (min-width: 1024px);
@custom-media --larger-than-desktop-hd (min-width: 1200px);
@custom-media --full-size (min-width: 1440px);

but there's no way to match those with vue-mq.

flyingL123 commented 4 years ago

@nathanchase can't you just adjust the mq settings to use max-width values, like I mentioned above?

https://github.com/AlexandreBonaventure/vue-mq/issues/35#issuecomment-552491390

stephantabor commented 4 years ago

@flyingL123 it can work for some simple cases, but the main reason that I made this issue was because using matchMedia and allowing any media queries has a few other advantages:

These are just some things i've personally done with my home rolled solution, i'm sure there are a bunch of others I haven't considered

nathanchase commented 4 years ago

@flyingL123 No, because my design is built around the breakpoints being "larger than" a certain size, not "less than" a certain size.

stephantabor commented 4 years ago

@nathanchase I think the person meant you could convert your min-width to max-width, e.g.

@custom-media --larger-than-desktop-hd (min-width: 1200px);
@custom-media --full-size (min-width: 1440px);

becomes

ue.use(VueMq, {
    // These are max-width settings
    breakpoints: {
        largerHd: 1339,
        fullSize: Infinity,
    }
});

conversion might be off, just an example

But it's not perfect and annoying that you have to then mentally convert your css mq to the js one, being able to have the same exact media queries would be waaaaaay better

nathanchase commented 4 years ago

That doesn't work because I need the change (show/hide of a component) to happen exactly at the minimum width, not at the last pixel before the next breakpoint.

nathanchase commented 4 years ago

Decided to switch libraries to the much more full-featured https://github.com/reegodev/vue-screen instead. Cheers.

flyingL123 commented 4 years ago

@nathanchase good find, thanks!

AndrewBogdanovTSS commented 4 years ago

@nathanchase wow, that lib looks really awesome! Thanks for the link!

nathanchase commented 4 years ago

Well, vue-screen also causes SSR/client hydration errors (Using Nuxt), so now I don't know what to do. Stuck trying to come up with an implementation that works, but nothing exists.

flyingL123 commented 4 years ago

@nathanchase I am still confused why you can't adjust your values so that mq will work. For example, my css breakpoints are:

breakpoints: {
  sm: 640,
  md: 768,
  lg: 1024,
  xl: 1280,
}

Using that as an example, md is from 768-1023, and lg is from 1024-1279.

Then, for mq, they are defined like this:

breakpoints: {
  sm: 767,
  md: 1023,
  lg: 1279,
  xl: Infinity,
}

Going through the same example, now these are max-width settings, which means mq treats 768-1023 as medium, and 1024-1279 as large. The ranges are exactly the same, just like you want, aren't they?

nathanchase commented 4 years ago

Either way, I can't actually use them in a computed property the way I need to.

I need to be able to do v-if='biggerThanMobile' on an element - which is unsupported. To do that, I'd have to check against EVERY breakpoint in a computed property, like:

computed() {
    return biggerThanMobile = this.$mq === 'largerThanMobile' ||  this.$mq === 'largerThanPhablet' || this.$mq === 'largerThanLeaderboard' || this.$mq === 'largerThanTablet' || this.$mq === 'largerThanDesktop' || this.$mq === 'largerThaniPad' || this.$mq === 'largerThanDesktopHD' || this.$mq === 'fullSize';
}

Instead of something that figures out anything bigger than the named breakpoint.

Not ideal.

flyingL123 commented 4 years ago

Not ideal sure but it’s a one time inconvenience to write a small object that includes a method to check those ugly conditionals for each breakpoint and return true or false. Then use it in whatever components need it. Either through the provide inject api or directly as a separate import.

AndrewBogdanovTSS commented 4 years ago

@nathanchase you will get hydration errors anyway, there's no cure from that since it's how Nuxt works, it's not related to one lib or another. The actual problem is that in SSR phase you don't have a window object thus no media queries so Nuxt will always render "mobile" version of your app first, but once client phase kicks in - the lib changes your DOM structure and you get a hydration error. The rules here are simple - try to use vue-screen in v-if as less as possible. The better way is to apply some specific class that will alter the look. I think it will also work good if you will provide the same structural element in v-else, you can just put a display:none on it with a class, but the goal here is to always try to keep identical DOM structure on all breakpoints.

nathanchase commented 4 years ago

I think it will also work good if you will provide the same structural element in v-else, you can just put a display:none on it with a class, but the goal here is to always try to keep identical DOM structure on all breakpoints.

That's the problem though, is that I want to be able to lazy load Vue components as needed. There are components specific to a mobile view that don't need to be loaded for a desktop user, and vice-versa. Yes, I can (and have) been using CSS to show/hide content, but it still loads both sets of content and just hides some of it - wasting time downloading content that will likely not be shown UNLESS the user changes their viewport.

In addition, I do need SSR to fetch and render out information for SEO purposes, and by default, Googlebot uses a mobile user agent as its primary crawler. This is problematic, as the content will not be the same (and shouldn't be) for mobile as it would be on desktop - so I do still need what amounts to two customized versions of the site depending on a "Smartphone" crawler or a "Desktop" crawler.

My hope is that there's some way to be able to use v-if on elements accordingly. I could use user agent sniffing, but that seems a lot less resilient than viewport widths.

It's a difficult problem to solve!

AndrewBogdanovTSS commented 4 years ago

@nathanchase I understand your pain but can't give you a straight solution to the problem. If you will find something that can mitigate this issue - let me know, please. I'm also curious about how can it be fixed in the right way :)

SergeyDarnopykh commented 3 years ago

@nathanchase @AndrewBogdanovTSS Hey guys, I think I've solved a problem with vue-mq and SSR. Think you might be interested.

So I was searching for a solution, and as you found none. So I've spent the whole day on this and finally I've got a working solution (it seems so).

My idea: we render component with ssr, but don't hydrate it on the client immediately. We check if the breakpoint on the client matches the defaultBreakpoint / breakpoint on a server (I use device-detector-js on a server to determine user's device from user agent), and if the two breakpoints match - we do hydration, if they don't - we render the component from scratch. I created a wrapper component for this purpose, to wrap mq-related code with it. Here's the code, though it uses some vue-property-decorator and some additional utils (let me know if you want to see them)

<template>
    <div>
        <LazyHydrate never :trigger-hydrate="doHydration" v-if="doHydration">
            <slot></slot>
        </LazyHydrate>

        <NoSsr v-else>
            <slot></slot>
        </NoSsr>
    </div>
</template>

<script lang="ts">
    import { Vue, Component } from 'vue-property-decorator';
    import LazyHydrate from 'vue-lazy-hydration';
    import NoSsr from 'vue-no-ssr';
    import { isServer } from '@vue-storefront/core/helpers';
    import detectBreakpoint from 'theme/utils/detect-breakpoint';

    @Component({
        components: {
            LazyHydrate,
            NoSsr
        }
    })
    export default class ResponsiveHydrate extends Vue {
        public name: string = 'ResponsiveHydrate';

        public doHydration: boolean = isServer;

        mounted() {
            this.doHydration = this.$mq === detectBreakpoint();
        }
    }
</script>

Ps: Device detector would've been enough by itself for solving hydration issue (cause it's good at detecting mobile / tablet / desktop), but we can't 100% guarantee that the user of tablet will have the screen resolution we will set it to (768-1024 by default) or that the user on desktop won't use dev tools to make screen smaller. Thus this solution above.

Let me know if this was any help to you!