Home > Blockchain >  How to get a reactive matchMedia in Vue 2?
How to get a reactive matchMedia in Vue 2?

Time:09-23

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>

CodePudding user response:

Although there's quite a bit of overlap,

  • detecting mobile devices is mostly about detecting touch devices
  • matching media queries is about testing if a CSS media query currently matches

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/[email protected]/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/[email protected]/dist/vue.min.js"></script>
<script src="https://unpkg.com/[email protected]/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 }")

CodePudding user response:

You are not declaring a reactive property.

let a = true
Vue.prototype.$test = a
a = false

This is how you expect things to work based on the OP. Vue has no way to tell if a value changed.

Declare it in the data property. Upon initializing a Vue instance, Vue has added its reactivity magic and can now tell if the value changed

Docs: https://v2.vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties

Since Vue doesn’t allow dynamically adding root-level reactive properties, you have to initialize Vue instances by declaring all root-level reactive data properties upfront, even with an empty value:

var vm = new Vue({
  data: {
    // declare message with an empty value
    message: ''
  },
  template: '<div>{{ message }}</div>'
})
// set `message` later
vm.message = 'Hello!'

If you don’t declare message in the data option, Vue will warn you that the render function is trying to access a property that doesn’t exist.

There are technical reasons behind this restriction - it eliminates a class of edge cases in the dependency tracking system, and also makes Vue instances play nicer with type checking systems. But there is also an important consideration in terms of code maintainability: the data object is like the schema for your component’s state. Declaring all reactive properties upfront makes the component code easier to understand when revisited later or read by another developer.

  • Related