'Display different Vuejs components for mobile browsers

I am developing an SPA using Vue 2.0. The components developed so far are for the "desktop" browsers, for example, I have

Main.vue, ProductList.vue, ProductDetail.vue,

I want another set of components for the mobile browsers, such as MainMobile.vue, ProductListMobile.vue, ProductDetailMobile.vue,

My question is, where and how do I make my SPA render the mobile version of components when viewing in a mobile browser?

Please note that I explicitly want to avoid making my components responsive. I want to keep two separate versions of them.

Thanks,



Solution 1:[1]

I have an idea, use a mixin which detects if the browser is opened on mobile or desktop (example for js code in this answer). then use v-if, for example:

<production-list v-if="!isMobile()"></production-list>
<production-list-mobile v-else></production-list-mobile>

so here is an example on https://jsfiddle.net/Ldku0xec/

Solution 2:[2]

I have simple solution for Vue.js:

<div v-if="!isMobile()">
  <desktop>
  </desktop>
</div>
<div v-else>
  <mobile>
  </mobile>
</div>

And methods:

methods: {
 isMobile() {
   if(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)) {
     return true
   } else {
     return false
   }
 }
}

Solution 3:[3]

I had this same problem, I solved it using a neutral and no layout vue file (Init.vue) that will be accessed by mobile and desktop, and this file redirects to the correct file.

Let's suppose that I have the Main.vue and the MainMobile.vue. I will add an Init.vue that will redirect. So my router/index.js is that:

import Router from 'vue-router'
import Vue from 'vue'
import Main from '@/components/Main'
import MainMobile from '@/components/MainMobile'
import Init from '@/components/Init'

Vue.use(Router)

export default new Router({
  routes: [
     {
        path: '/',
        name: 'Root',
        component: Init
     },
    {
      path: '/Main',
      name: 'Main',
      component: Main
    },
    {
      path: '/MainMobile',
      name: 'MainMobile',
      component: MainMobile
    },
  ]
})

At the Init.vue file, the mobile/desktop detection will happen:

<template>
</template>
<script>
    export default {
        name: 'Init',
        methods: {
            isMobile() {
                if( screen.width <= 760 ) {
                    return true;
                }
                else {
                    return false;
                }
            }
        },
        created() {
            if (this.isMobile()) {
                this.$router.push('/MainMobile');
            }
            else {
                this.$router.push('/Main');
            }
        }
    }
</script>
<style scoped>
</style>

The isMobile() function used is very simple, you can change to any other.

Solution 4:[4]

I was looking for a solution for this and came here but I couldn't find what I needed:

  1. Asynchronous imports to only load into the bundle what was needed based on the viewport.
  2. Capability to serve a different layout if the layout was resized

I mixed and matched a few things I read online including answers here so I thought I'd just come back and put all my learnings into one function for anyone else looking:

/**
 * Breakpoint configuration to be in line with element-ui's standards
 * @type {{LABELS: string[], VALUES: number[]}}
 */
const BREAKPOINTS = {
    LABELS: ['xs', 'sm', 'md', 'lg', 'xl'],
    VALUES: [0, 768, 992, 1200, 1920, Infinity]
};


/**
 * @typedef ViewFactory
 * @type function
 * A function which returns a promise which resolves to a view. Used to dynamically fetch a view file on the fly during
 * run time on a need basis
 */


/**
 * A helper to get a responsive route factory which renders different views based on the current view point
 * @param {{xs:[ViewFactory],sm:[ViewFactory],md:[ViewFactory],lg:[ViewFactory]}} map - A map of breakpoint key to a ViewFactory
 * @returns {ViewFactory} - A view factory which invokes and returns an item supplied in the map based on the current viewport size
 */
export default function responsiveRoute(map) {
    return function getResponsiveView() {
        const screenWidth = document.documentElement.clientWidth;

        // Find the matching index for the current screen width
        const matchIndex = BREAKPOINTS.VALUES.findIndex((item, idx) => {
            if (idx === 0) {
                return false;
            }
            return screenWidth >= BREAKPOINTS.VALUES[idx - 1] && screenWidth < BREAKPOINTS.VALUES[idx];
        }) - 1;


        if (map[BREAKPOINTS.LABELS[matchIndex]]) {
            // Perfect match, use it
            return map[BREAKPOINTS.LABELS[matchIndex]]();
        } else {
            // Go down the responsive break points list until a match is found
            let counter = matchIndex;
            while (counter-- > 0) {
                if (map[BREAKPOINTS.LABELS[counter]]) {
                    return map[BREAKPOINTS.LABELS[counter]]();
                }
            }
            return Promise.reject({
                code: 500,
                info: 'No component matched the breakpoint - probably a configuration error'
            });
        }
    };
} 

Usage:

const router = new Router({
    mode: 'history',
    base: process.env.BASE_URL,
    routes:[{
      path: '/login',
      name: 'login',
      component: responsiveRoute({
          // route level code-splitting
          // this generates a separate chunk (login-xs.[hash].js) for this route
          // which is lazy-loaded when the route is visited.
          xs: () => import(/* webpackChunkName: "login-xs" */ './views/Login/Login-xs.vue'),
          // sm key is missing, it falls back to xs
          md: () => import(/* webpackChunkName: "login-md" */ './views/Login/Login-md.vue')
          // lg, xl keys are missing falls back to md
      }) 
  }]
}); 

How it works:

Vue Router supports defining the component key as a function which returns a promise to support async routes. The most common way being to use the webpack import() function which returns a promise. The function which returns the promise is only invoked when the route is about to be rendered ensuring we can lazy load our components

The responsiveRoute function accepts a map of these functions with keys set for different breakpoints and returns a function which, when invoked, checks the available viewport size and returns invokes the correct promise factory and return's the promise returned by it.

Notes:

I like this method because it does not require the application architecture or route configurations to be in a certain way. It's pretty plug and play using Vue Router capabilities provided out of the box. It also does not force you to define a view for every breakpoint-route combination. You can define a route as usual without this(lazy loaded or not) along side other routes that use this without any problems.

This method does not use user agent sniffing but uses the available width of the document.documentElement instead. Other methods I saw recommended things like window.screen.width which gives the exact device screen size regardless of the window size or a more robust window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth. Mix and match as needed.

My break points are (number and their values) are based on element-ui breakpoints as I used that for normal responsive design. This can again be configured as needed by changing the constants at the top

Solution 5:[5]

A bit late for this but, in case if any of you are looking to bind watchers for the viewport size, and/or check whenever the page loads. Its a lot of boilerplate to write but can be useful for small applications.

export default {
  data: () => ({
    isMobile: false
  }),

  beforeDestroy () {
    if (typeof window !== 'undefined') {
      window.removeEventListener('resize', this.onResize, { passive: true })
    }
  },

  mounted () {
    this.onResize()
    window.addEventListener('resize', this.onResize, { passive: true })
  },

  methods: {
    onResize () {
      this.isMobile = window.innerWidth < 600
    }
  }
}

Solution 6:[6]

For vuetify users, you can use breakpoints with dynamic component loading. Suppose you have two components; DesktopLayout.vue and MobileLayout.vue. In the page where you want to load different layouts, define a dynamic component like this;

<component :is="currentComponent"></component>

Add a computed property named "currentComponent".

computed: {
    currentComponent() {
        return this.$vuetify.breakpoint.xsOnly
            ? "MobileLayout"
            : "DesktopLayout";
    }
}

this.$vuetify.breakpoint.xsOnly will make use "MobileLayout" only on extra small screens. There are many breakpoints defined in vuetify. For example you can select a range by using "mdAndDown" for medium size and down screens.

Solution 7:[7]

An extended version of Beaudinn Greves answer:

  • Use named router views
  • listen on window resize and set isMobile true if width smaller then xy
  • in router.js use "components" instead of "component" and import (you may use require) the desktop and mobile components

App.vue:

<template>
  <div id="app" class="container grid-lg">
    <router-view v-if="!store.app.isMobile"/>
    <router-view v-else name="mobile"/>
  </div>
</template>
...
name: "app",
  data: function() {
  return {
    store: this.$store.state
  };
},
mounted () {
  this.onResize()
  window.addEventListener('resize', this.onResize, { passive: true })
},
methods: {
  onResize () {
    this.store.app.isMobile = window.innerWidth < 600
  }
},
beforeDestroy () {
  if (typeof window !== 'undefined') {
    window.removeEventListener('resize', this.onResize, { passive: true })
  }
}

router.js:

routes: [
  {
    path: '/',
    name: 'home',
    components: {
      default: Home,
      mobile: HomeMobile
    }
  }
]

Solution 8:[8]

A bit late for this but, in case if any of you are looking for I handled the situation like this: I added meta to my router:

const router = new Router({
     routes: [{
      path: '/main-view
      name: 'mainView',
      component: MainView,
      meta: {
        'hasMobileView': true
      }
     },
    {
     path: '/mobile-view',
      name: 'mobileView',
      component: mobileView,
      meta: {
        'hasDesktopView': true
      }
     },
    }]
})

then on beforeeach function 

router.beforeEach((to, from, next) => {
  const hasMobileView = to.matched.some((route) => route.meta.hasMobileView)
  if (hasMobileView) {
    if (navigator.userAgent.match(/Android/i) ||
      navigator.userAgent.match(/webOS/i) ||
      navigator.userAgent.match(/iPhone/i) ||
      navigator.userAgent.match(/iPad/i) ||
      navigator.userAgent.match(/iPod/i) ||
      navigator.userAgent.match(/BlackBerry/i) ||
      navigator.userAgent.match(/Windows Phone/i)) {
      next('/mobile-view')
    } else {
      next()
    }
  }
})`

Solution 9:[9]

I have a better solution.In src/main.js:

 if (condition) {
    require('./pc/main)
 }else {
    require('./mobile/main')
}

Solution 10:[10]

you can simply get user agent and parse it and then you have a variable isMobile i assume you have it in this answer and i want to mention an important thing when you import both components (desktop and mobile) when user visit your page they both will download oh my God!!!

the solution is :

<template>
 <component :is="headerInstance" />
</template>

<script>
export default {
computed: {
    headerInstance() {
      const component = this.isMobile
        ? '/HeaderMobile'
        : '/HeaderDesktop'
      return () => import(`@/components/Layouts/Header${component}`)
    }
}
}
</script>

Solution 11:[11]

resources/js/components/Options.vue

<template>
    <div class="text-right mt-6 ml-16 fixed">
        <div v-if="isMobile()" class="px-0.5 py-0.5 hidden portrait:block">
            {/* ... */}
        </div>
        <div v-else class="px-0.5 py-0.5">
            {/* ... */}
        </div>
    </div>
</template>

<script>
    export default {
        methods: {
            isMobile() {
                if ('maxTouchPoints' in navigator) return navigator.maxTouchPoints > 0 ? true : false
                else return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)
            }
        }
    }
</script>

tailwind.config.js

module.exports = {
    purge: [],
    theme: {
        extend: {
            screens: {
                'portrait': {'raw': '(orientation: portrait)'},
            }
        }
    },
    variants: {
        extend: {}
    },
    plugins: []
}

Desktop wide

Desktop wide

Desktop small

Desktop small

Mobile portrait

Mobile portrait

Mobile landscape

Mobile landscape

Solution 12:[12]

you need to check if is mobile

const isMobile=() =>( window.innerWidth <= 760 )