javascriptvue.jsvuejs2matchmedia

How to get a reactive matchMedia in Vue 2?


I need to have a reactive global variable in Vue. The variable is simply a boolean that tells me whether the user is on a mobile device. I have tried so many things but this is the last thing I tried:

  Vue.prototype.$testIsMobile = false
  const mobileMediaMatch = window.matchMedia('(max-width: 768px)')
  Vue.prototype.$testIsMobile = mobileMediaMatch.matches
  window.addEventListener('resize', function () {
    console.log("resizeeeee: " + mobileMediaMatch.matches)
    Vue.prototype.$testIsMobile = mobileMediaMatch.matches
  }, true)

Now this will get triggered when I reszie my screen because I can see the text resizeeeee getting logged repeatedly to the console. The problem is that when I use the variable $testIsMobile in other components, the variable is not reactive. It does not re-render the page accordingly until I refresh the page manually. How can I make this variable fully reactive so that any component can use it and it contains the correct value?

Here is an example of how I use it in a component:

<div>--{{$testIsMobile}}--</div>

Solution

  • Although there's quite a bit of overlap,

    If you want to detect touch devices, I suggest isMobile (or similar). It's not reactive, because (normally) the device does not change. (e.g: if emulating - therefore changing device on the fly - you need to reload the page to reapply detection.

    Vue 2 implementation:

    Vue.use({
      install(v) {
        v.prototype.$isMobile = isMobile
      }
    })
    
    new Vue({ el: '#app' })
    <script src="https://unpkg.com/vue@2.7.10/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/ismobilejs@1/dist/isMobile.min.js"></script>
    <div id="app">
      <pre v-text="JSON.stringify($isMobile, null, 2)"></pre>
    </div>


    To detect if a media matches in real time, you could roll your own, but it's already done: vue-component-media-queries (for Vue 2):

    const { MatchMedia, MediaQueryProvider } = VueComponentMediaQueries;
    
    const baseQueries = [
      { xs: { max: 539 } },
      { sm: { min: 540, max: 767 } },
      { md: { min: 768, max: 991 } },
      { lg: { min: 992, max: 1199 } },
      { xl: { min: 1200, max: 1499 } },
      { xxl: { min: 1500 } },
    ];
    
    new Vue({
      el: "#app",
      components: {
        MatchMedia,
        MediaQueryProvider,
      },
      data: () => ({
        queries: Object.assign(
          {
            portrait: "(orientation: portrait)",
            landscape: "(orientation: landscape)",
          },
          ...Object.entries(Object.assign({}, ...baseQueries)).map(
            ([key, val]) => ({
              [key]: [
                val.min ? `(min-width: ${val.min}px)` : "",
                val.max ? `(max-width: ${val.max}.99px)` : "",
              ]
                .filter((o) => o)
                .join(" and "),
              ...(val.min && val.max
                ? {
                    ["min-" + key]: `(min-width: ${val.min}px)`,
                    ["max-" + key]: `(max-width: ${val.max}.99px)`,
                  }
                : {}),
            })
          )
        ),
      }),
    });
    th {
      text-align: left;
    }
    td:last-child {
      text-align: center;
    }
    table {
      width: 100%;
      max-width: 800px;
      margin: 0 auto;
    }
    <script src="https://unpkg.com/vue@2.7.10/dist/vue.min.js"></script>
    <script src="https://unpkg.com/vue-component-media-queries@1.0.0/dist/index.js"></script>
    <div id="app">
      <media-query-provider :queries="queries">
        <match-media #default="matches">
          <table>
            <thead>
              <tr>
                <th>Key</th>
                <th>Query</th>
                <th>Matches?</th>
              </tr>
            </thead>
            <tbody>
              <tr
                v-for="(value, key) in matches"
                :key="key"
                :style="{color: value ? '#275': '#930'}"
              >
                <td v-text="key"></td>
                <td>
                  <code v-text="queries[key]"></code>
                </td>
                <td>{{ value ? '✅' : '❌' }}</td>
              </tr>
            </tbody>
          </table>
        </match-media>
        <hr />
    
        <!-- Simpler syntax (but the same thing, in essence): -->
        <match-media v-slot="{ md }">
          <div v-if="md">md</div>
          <div v-else>not md</div>
        </match-media>
      </media-query-provider>
    </div>

    Look at the simpler syntax at the bottom.

    Define as many queries as you want. They're all be available on <MediaQuery />'s v-slot (e.g: #default="{ someQuery }")