Categories
Vue 3

Add a Swiper Carousel into a Vue 3 App with Swiper 6

Swiper for Vue.js lets us add a carousel to our Vue 3 app.

In this article, we’ll look at how to add a carousel into our Vue 3 app with Swiper.

Virtual Slides

We can add virtual slides, which are slides that are rendered completely by Vue 3.

It doesn’t require anything except the virtual prop.

For instance, we can write:

<template>
  <swiper
    :slides-per-view="3"
    :space-between="50"
    @swiper="onSwiper"
    @slideChange="onSlideChange"
    virtual
  >
    <swiper-slide v-for="n of 50" :virtualIndex="n" :key="n"
      >Slide {{ n }}</swiper-slide
    >
  </swiper>
</template>
<script>
import SwiperCore, { Virtual } from "swiper";
import { Swiper, SwiperSlide } from "swiper/vue";
import "swiper/swiper-bundle.css";

SwiperCore.use([Virtual]);

export default {
  components: {
    Swiper,
    SwiperSlide,
  },
  methods: {
    onSwiper(swiper) {
      console.log(swiper);
    },
    onSlideChange() {
      console.log("slide change");
    },
  },
};
</script>

to add the swiper component that renders virtual slides.

Also, we have to add SwiperCore.use([Virtual]) to add the Virtual plugin so that we can render virtual slides.

Controller

We can add a slider that’s controlled by another slider.

For instance, we can write:

<template>
  <swiper
    :slides-per-view="3"
    :space-between="50"
    @swiper="setControlledSwiper"
  >
    <swiper-slide v-for="n of 50" :virtualIndex="n" :key="n"
      >Slide {{ n }}</swiper-slide
    >
  </swiper>

  <swiper :controller="{ control: controlledSwiper }">
    <swiper-slide v-for="n of 50" :virtualIndex="n" :key="n"
      >Slide {{ n }}</swiper-slide
    >
  </swiper>
</template>

<script>
import SwiperCore, { Controller } from "swiper";
import { Swiper, SwiperSlide } from "swiper/vue";
import "swiper/swiper-bundle.css";

SwiperCore.use([Controller]);

export default {
  components: {
    Swiper,
    SwiperSlide,
  },
  data() {
    return {
      controlledSwiper: null,
    };
  },
  methods: {
    setControlledSwiper(swiper) {
      this.controlledSwiper = swiper;
    },
  },
};
</script>

The first swiper controls the behavior of the 2nd swiper.

We do this by calling the setControlledSwiper method when the first swiper is initialized.

In the method, we just set swiper as the value of this.controlledSwiper .

Now in the 2nd swiper , we pass in an object with the control property set to controlledSwiper into the controller prop to let the first swiper control the 2nd one.

We also have to addSwiperCore.use([Controller]) to let us add controlled swipers.

Also, we can add 2-way control to swipers.

For instance, we can write:

<template>
  <swiper
    :slides-per-view="3"
    :space-between="50"
    @swiper="setFirstSwiper"
    :controller="{ control: secondSwiper }"
  >
    <swiper-slide v-for="n of 50" :virtualIndex="n" :key="n"
      >Slide {{ n }}</swiper-slide
    >
  </swiper>

  <swiper @swiper="setSecondSwiper" :controller="{ control: firstSwiper }">
    <swiper-slide v-for="n of 50" :virtualIndex="n" :key="n"
      >Slide {{ n }}</swiper-slide
    >
  </swiper>
</template>

<script>
import SwiperCore, { Controller } from "swiper";
import { Swiper, SwiperSlide } from "swiper/vue";
import "swiper/swiper-bundle.css";

SwiperCore.use([Controller]);

export default {
  components: {
    Swiper,
    SwiperSlide,
  },
  data() {
    return {
      firstSwiper: null,
      secondSwiper: null,
    };
  },
  methods: {
    setFirstSwiper(swiper) {
      this.firstSwiper = swiper;
    },
    setSecondSwiper(swiper) {
      this.secondSwiper = swiper;
    },
  },
};
</script>

We have the setFirstSwiper and setSecondSwiper to set the firstSwiper and secondSwiper reactive properties respectively.

Then we pass them as values of the controller prop to enable control of them.

Now when we swipe one of the sliders, the other one will change.

Conclusion

We can add more complex sliders into our Vue 3 app with Swiper 6.

Categories
Vue

How to Create an Image Gallery App with Vue.js

Building an image gallery with Vue.js is easy. There are already libraries available to make a good looking image gallery app. With Vue.js, building an image gallery app is an enjoyable experience. We will use Vue Material to make the app look appealing and we use the Pixabay API located at https://pixabay.com/api/docs/

In our app, we will provide pages for image and video search, and the home page will have an infinite scrolling image gallery for letting users view the top image hits from the Pixabay API.

To get started, we install the Vue CLI by running npm i @vue/cli. Then we create the project by running vue create pixabay-app. When prompted for options, we choose the custom options and choose to include Babel, Vuex, Vue Router, and CSS preprocessor. We need all of them since we are building a single page with shared state between components, and the CSS preprocessor reduces the repetition of CSS.

Next, we have to install some libraries that we need for our app to function. Run npm i axios querystring vue-material vee-validate vue-infinite-scroll to install the packages we need. axios is our HTTP client, querystring is a package to massage objects into a query string, vue-material provides Material Design elements for our app to make it look appealing. vee-validate is a form validation library. vue-infinite-scroll is an infinite scroll library.

Now we can start writing code. We start by writing the shared code that our pages will use. In the components folder, we create a file called Results.vue and add the following:

<template>
  <div class="center">
    <div class="results">
      <md-card v-for="r in searchResults" :key="r.id">
        <md-card-media v-if="type == 'image'">
          <img :src="r.previewURL" class="image" />
        </md-card-media>
        <md-card-media v-if="type == 'video'">
          <video class="image">
            <source :src="r.videos.tiny.url" type="video/mp4" />
          </video>
        </md-card-media>
      </md-card>
    </div>
  </div>
</template>

<script>
export default {
  name: "results",
  props: {
    type: String
  },
  computed: {
    searchResults() {
      return this.$store.state.searchResults;
    }
  },
  data() {
    return {};
  }
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
.md-card {
  width: 30vw;
  margin: 4px;
  display: inline-block;
  vertical-align: top;
}
</style>

In this file, we get the data from the Vuex store and then display them to the user. this.$store is injected by Vuex and provides the state with the state property. The search results can be image or video, so we allow a type prop to let us distinguish the type of results. The state is in the computed property so that we can get it from the state.

Next, we add a mixin for getting making the requests. To do this, we add a mixins folder and add a file called photosMixin.js and fill it with the following:

const axios = require('axios');
const querystring = require('querystring');
const apiUrl = 'https://pixabay.com/api';
const apikey = 'Pixabay api key';

export const photosMixin = {
    methods: {
      getPhotos(page = 1) {
            const params = {
                page,
                key: apikey,
                per_page: 21
            }
            const queryString = querystring.stringify(params);
            return axios.get(`${apiUrl}/?${queryString}`);
        },

      searchPhoto(data) {
            let params = Object.assign({}, data);
            params['key'] = apikey;
            params['per_page'] = 21;
            Object.keys(params).forEach(key => {
                if (!params[key]) {
                    delete params[key];
                }
            })
            const queryString = querystring.stringify(params);
            return axios.get(`${apiUrl}/?${queryString}`);
        },

      searchVideo(data) {
            let params = Object.assign({}, data);
            params['key'] = apikey;
            params['per_page'] = 21;
            Object.keys(params).forEach(key => {
                if (!params[key]) {
                    delete params[key];
                }
            })
            const queryString = querystring.stringify(params);
            return axios.get(`${apiUrl}/videos/?${queryString}`);
        }
    }
}

This is where we get the images and video results from the Pixabay API. We massage the search data passed into a query string with the querystring package.

With the helper code complete, we can start building some pages. We first work on the home page, which will show a grid of image and the user can scroll down to see more. We create a file called Home.vue in the views folder which does not exist already and add the following:

<template>
  <div class="home">
    <div class="center">
      <h1>Home</h1>
      <div
        v-infinite-scroll="loadMore"
        infinite-scroll-disabled="busy"
        infinite-scroll-distance="10"
      >
        <md-card v-for="p in photos" :key="p.id">
          <md-card-media>
            <img :src="p.previewURL" class="image" />
          </md-card-media>
        </md-card>
      </div>
    </div>
  </div>
</template>

<script>
// @ is an alias to /src
import Results from "@/components/Results.vue";
import { photosMixin } from "@/mixins/photosMixin";

export default {
  name: "home",
  components: {
    Results
  },
  data() {
    return {
      photos: [],
      page: 1
    };
  },
  mixins: [photosMixin],
  beforeMount() {
    this.getAllPhotos();
  },
  methods: {
    async getAllPhotos() {
      const response = await this.getPhotos();
      this.photos = response.data.hits;
    },

    async loadMore() {
      this.page++;
      const response = await this.getPhotos(this.page);
      this.photos = this.photos.concat(response.data.hits);
    }
  }
};
</script>

<style lang="scss" scoped>
.md-card {
  width: 30vw;
  margin: 4px;
  display: inline-block;
  vertical-align: top;
}

.home {
  margin: 0 auto;
}
</style>

The v-infinite-scroll prop is the event handler provided by vue-infinite-scroll to let us do something when the user scrolls down. In this case, we call the loadMore function to load data from the next page and add it to the this.photos array. We display the images in a card view. infinite-scroll-distance=”10" means that we run the handler defined in v-infinite-scroll when the users scrolls through 90 percent of the current page.

Next we create an image search page. In the views folder, we create a file called ImageSearch.vue and add the following:

<template>
  <div class="imagesearch">
    <div class="center">
      <h1>Image Search</h1>
    </div>
    <form @submit="search" novalidate>
      <md-field :class="{ 'md-invalid': errors.has('q') }">
        <label for="q">Keyword</label>
        <md-input type="text" name="q" v-model="searchData.q" v-validate="'required'"></md-input>
        <span class="md-error" v-if="errors.has('q')">{{errors.first('q')}}</span>
      </md-field>

      <md-field :class="{ 'md-invalid': errors.has('minWidth') }">
        <label for="minWidth">Min Width</label>
        <md-input
          type="text"
          name="minWidth"
          v-model="searchData.min_width"
          v-validate="'numeric|min_value:0'"
        ></md-input>
        <span class="md-error" v-if="errors.has('minWidth')">{{errors.first('minWidth')}}</span>
      </md-field>

      <md-field :class="{ 'md-invalid': errors.has('minHeight') }">
        <label for="minHeight">Min Height</label>
        <md-input
          type="text"
          name="minHeight"
          v-model="searchData.min_height"
          v-validate="'numeric|min_value:0'"
        ></md-input>
        <span class="md-error" v-if="errors.has('minHeight')">{{errors.first('minHeight')}}</span>
      </md-field>

      <md-field>
        <label for="movie">Colors</label>
        <md-select v-model="searchData.colors" name="colors">
          <md-option :value="c" v-for="c in colorChoices" :key="c">{{c}}</md-option>
        </md-select>
      </md-field>

      <md-button class="md-raised" type="submit">Search</md-button>
    </form>
    <Results type='image' />
  </div>
</template>

<script>
// @ is an alias to /src
import Results from "@/components/Results.vue";
import { photosMixin } from "@/mixins/photosMixin";

export default {
  name: "home",
  components: {
    Results
  },
  data() {
    return {
      photos: [],
      searchData: {},
      colorChoices: [
        "grayscale",
        "transparent",
        "red",
        "orange",
        "yellow",
        "green",
        "turquoise",
        "blue",
        "lilac",
        "pink",
        "white",
        "gray",
        "black",
        "brown"
      ]
    };
  },
  mixins: [photosMixin],
  beforeMount() {
    this.$store.commit("setSearchResults", []);
  },
  computed: {
    isFormDirty() {
      return Object.keys(this.fields).some(key => this.fields[key].dirty);
    }
  },
  methods: {
    async search(evt) {
      evt.preventDefault();
      if (!this.isFormDirty || this.errors.items.length > 0) {
        return;
      }
      const response = await this.searchPhoto(this.searchData);
      this.photos = response.data.hits;
      this.$store.commit("setSearchResults", response.data.hits);
    }
  }
};
</script>

This page provides a form for the user to enter some search parameters like keyword, dimensions of the image, and colors. We check for correct data for each field with the v-validate prop provided by the vee-validate package. So for minWidth and minHeight we make sure that they are non-negative numbers. We display an error if it’s not.

We will also not allow submission if form data is invalid with the this.errors.items.length > 0 check which is also provided by vee-validate If everything is valid, we call the this.searchPhotos function provided by our photoMixin and set the result when returned in the store with the this.$store.commit function provided by the store. We pass image to the type prop of Results so that image results will be displayed.

Similarly, for the video search page, we create a new file called VideoSearch.vue in the views folder. We put the following:

<template>
  <div class="videosearch">
    <div class="center">
      <h1>Video Search</h1>
    </div>
    <form @submit="search" novalidate>
      <md-field :class="{ 'md-invalid': errors.has('q') }">
        <label for="q">Keyword</label>
        <md-input type="text" name="q" v-model="searchData.q" v-validate="'required'"></md-input>
        <span class="md-error" v-if="errors.has('q')">{{errors.first('q')}}</span>
      </md-field>

      <md-field :class="{ 'md-invalid': errors.has('minWidth') }">
        <label for="minWidth">Min Width</label>
        <md-input
          type="text"
          name="minWidth"
          v-model="searchData.min_width"
          v-validate="'numeric|min_value:0'"
        ></md-input>
        <span class="md-error" v-if="errors.has('minWidth')">{{errors.first('minWidth')}}</span>
      </md-field>

      <md-field :class="{ 'md-invalid': errors.has('minHeight') }">
        <label for="minHeight">Min Height</label>
        <md-input
          type="text"
          name="minHeight"
          v-model="searchData.min_height"
          v-validate="'numeric|min_value:0'"
        ></md-input>
        <span class="md-error" v-if="errors.has('minHeight')">{{errors.first('minHeight')}}</span>
      </md-field>

      <md-field>
        <label for="categories">Categories</label>
        <md-select v-model="searchData.category" name="categories">
          <md-option :value="c" v-for="c in categories" :key="c">{{c}}</md-option>
        </md-select>
      </md-field>

      <md-field>
        <label for="type">Type</label>
        <md-select v-model="searchData.video_type" name="type">
          <md-option :value="v" v-for="v in videoTypes" :key="v">{{v}}</md-option>
        </md-select>
      </md-field>

      <md-button class="md-raised" type="submit">Search</md-button>
    </form>
    <Results type="video" />
  </div>
</template>

<script>
import Results from "@/components/Results.vue";
import { photosMixin } from "@/mixins/photosMixin";

export default {
  name: "home",
  components: {
    Results
  },
  data() {
    return {
      photos: [],
      searchData: {},
      videoTypes: ["all", "film", "animation"],
      categories: `
       fashion, nature, backgrounds,
       science, education, people, feelings,
       religion, health, places, animals, industry,
       food, computer, sports, transportation,
       travel, buildings, business, music
      `
        .replace(/ /g, "")
        .split(",")
    };
  },
  mixins: [photosMixin],
  beforeMount() {
    this.$store.commit("setSearchResults", []);
  },
  computed: {
    isFormDirty() {
      return Object.keys(this.fields).some(key => this.fields[key].dirty);
    }
  },
  methods: {
    async search(evt) {
      evt.preventDefault();
      if (!this.isFormDirty || this.errors.items.length > 0) {
        return;
      }
      const response = await this.searchVideo(this.searchData);
      this.photos = response.data.hits;
      this.$store.commit("setSearchResults", response.data.hits);
    }
  }
};
</script>

We let users search by keywords, type, dimensions, and category. The logic for form validation and search are similar to the image search page, except that we pass video to the type prop of Results so that image results will be displayed.

In App.vue, we add the top bar and a left side menu for navigation. We replace the existing code with the following:

<template>
  <div id="app">
    <md-toolbar>
      <md-button class="md-icon-button" @click="showNavigation = true">
        <md-icon>menu</md-icon>
      </md-button>
      <h3 class="md-title">Pixabay App</h3>
    </md-toolbar>
    <md-drawer :md-active.sync="showNavigation" md-swipeable>
      <md-toolbar class="md-transparent" md-elevation="0">
        <span class="md-title">Pixabay App</span>
      </md-toolbar>

      <md-list>
        <md-list-item>
          <router-link to="/">
            <span class="md-list-item-text">Home</span>
          </router-link>
        </md-list-item>

        <md-list-item>
          <router-link to="/imagesearch">
            <span class="md-list-item-text">Image Search</span>
          </router-link>
        </md-list-item>

        <md-list-item>
          <router-link to="/videosearch">
            <span class="md-list-item-text">Video Search</span>
          </router-link>
        </md-list-item>
      </md-list>
    </md-drawer>

<router-view />
  </div>
</template>

<script>
export default {
  name: "app",
  data: () => {
    return {
      showNavigation: false
    };
  }
};
</script>

<style>
.center {
  text-align: center;
}

form {
  width: 95vw;
  margin: 0 auto;
}

.md-toolbar.md-theme-default {
  background: #009688 !important;
  height: 60px;
}

.md-title,
.md-toolbar.md-theme-default .md-icon {
  color: #fff !important;
}
</style>

The md-list contains the links to our pages, and we have a showNavigation flag to store the menu state and let us toggle the menu.

In main.js, we include the libraries that we used in this app:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import VueMaterial from 'vue-material';
import VeeValidate from 'vee-validate';
import 'vue-material/dist/vue-material.min.css'
import 'vue-material/dist/theme/default.css'
import infiniteScroll from 'vue-infinite-scroll'

Vue.use(infiniteScroll)
Vue.use(VueMaterial);
Vue.use(VeeValidate);
Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

In router.js, we add our routes so that users can see the app when they type in the URL or click on links on the menu:

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue';
import Search from './views/Search.vue';

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/search',
      name: 'search',
      component: Search
    }
  ]
})

In store.js, we put:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    searchResults: []
  },
  mutations: {
    setSearchResults(state, payload) {
      state.searchResults = payload;
    }
  },
  actions: {

  }
})

This allows the results to be set from the pages and displayed in the Results component. The this.$store.commit function sets the data and the this.$store.state property retrieves the state.

Categories
Vue 3

Create Vue 3 Apps with the Composition API — Custom Refs and Shallow Reactivity

It lets us extract logic easily an not have to worry about the value of this in our code.

It also works better with TypeScript because the value of this no longer has to be typed.

In this article, we’ll look at how to create Vue 3 apps with the Composition API.

customRef

We can use the customRef function to create a custom ref.

For instance, we can write:

<template>
  <div>
    <input v-model="text" />
    {{ text }}
  </div>
</template>

<script>
import { customRef } from "vue";

const useDebouncedRef = (value, delay = 200) => {
  let timeout;
  return customRef((track, trigger) => {
    return {
      get() {
        track();
        return value;
      },
      set(newValue) {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
          value = newValue;
          trigger();
        }, delay);
      },
    };
  });
};

export default {
  name: "App",
  setup() {
    return {
      text: useDebouncedRef("hello world"),
    };
  },
};
</script>

We create the useDebounceRef function that returns a custom ref created bu the customRef function.

It takes a callback with the track function to track the value.

And the setter calls trigger to trigger state updates.

We ser the value in the setter to newValue so that the items are updated.

markRaw

We can call the markRaw function to mark an object so that it’ll never be converted to a proxy.

For instance, we can write:

<template>
  <div></div>
</template>

<script>
import { isReactive, markRaw, reactive } from "vue";

export default {
  name: "App",
  setup() {
    const foo = markRaw({});
    console.log(isReactive(reactive(foo)));
    return { foo };
  },
};
</script>

The console log logs false since we called markRaw to disable reactivity on the object we passed into it.

But if we mark one object as raw and we reference that object in another object, then they’ll be considered not to be equal even though they have the same reference:

<template>
  <div></div>
</template>

<script>
import { isReactive, markRaw, reactive } from "vue";

export default {
  name: "App",
  setup() {
    const foo = markRaw({
      nested: {},
    });

    const bar = reactive({
      nested: foo.nested,
    });

    console.log(foo.nested === bar.nested);
    return { foo };
  },
};
</script>

We set bar.nested to foo.nested , but the console log is false .

shallowReactive

We can create a shallow reactive object with the shallowReactive function.

The reactive property is limited to the top-level properties of the object.

For instance, we can write:

<template>
  <div>
    <button @click="increment">increment</button>
    {{ state.foo }}
    {{ state.nested.bar }}
  </div>
</template>

<script>
import { isReactive, shallowReactive } from "vue";

export default {
  name: "App",
  setup() {
    const state = shallowReactive({
      foo: 1,
      nested: {
        bar: 2,
      },
    });

    const increment = () => {
      state.foo++;
      state.nested.bar++;
    };
    console.log(isReactive(state.nested.bar));
    return { state, increment };
  },
};
</script>

In the increment function, we increment both state.foo and state.nested.bar and they’ll both update in the template.

But when we log state.nested.bar with isReactive , we get false logged.

Since it’s not reactive, it won’t trigger watchers to run:

<template>
  <div>
    <button @click="increment">increment</button>
    {{ state.foo }}
    {{ state.nested.bar }}
  </div>
</template>

<script>
import { isReactive, shallowReactive, watch } from "vue";

export default {
  name: "App",
  setup() {
    const state = shallowReactive({
      foo: 1,
      nested: {
        bar: 2,
      },
    });

    const increment = () => {
      state.foo++;
      state.nested.bar++;
    };

    watch(state.nested.bar, (bar) => {
      console.log(bar);
    });

    return { state, increment };
  },
};
</script>

Then watch callback in the 2nd argument won’t run when state.nested.bar is updated.

Conclusion

We can use various functions provided Vue 3’s composition API to create various kinds of reactive and non-reactive properties.

Categories
Vue 3

Create Vue 3 Apps with the Composition API — Check Reactive Properties

It lets us extract logic easily an not have to worry about the value of this in our code.

It also works better with TypeScript because the value of this no longer has to be typed.

In this article, we’ll look at how to create Vue 3 apps with the Composition API.

toRefs

We can use the toRefs function to convert a ref toa plain object.

For instance, we can write:

<template>
  <div></div>
</template>

<script>
import { reactive, toRefs } from "vue";
export default {
  name: "App",
  setup() {
    const state = reactive({
      foo: 1,
      bar: 2,
    });

    const stateAsRefs = toRefs(state);
    console.log(stateAsRefs);

    return {
      state,
    };
  },
};
</script>

to convert the state reactive property to a plain object.

state.foo and stat.bar are reactive properties with values being the values that we set in the reactive function.

isRef

The isRef function checks if a variable is a ref.

For instance, we can write:

<template>
  <div></div>
</template>

<script>
import { isRef, reactive, ref } from "vue";
export default {
  name: "App",
  setup() {
    const state = reactive({
      foo: 1,
      bar: 2,
    });

    const r = ref(0);
    console.log(isRef(state));
    console.log(isRef(r));

    return {
      state,
    };
  },
};
</script>

We call isRef with state , which returns false .

And when we call isRef with r , it returns true .

isProxy

The isProxy function checks if an object is reactive or read-only.

For instance, we can write:

<template>
  <div></div>
</template>

<script>
import { isProxy, reactive, readonly, ref } from "vue";
export default {
  name: "App",
  setup() {
    const state = reactive({
      foo: 1,
      bar: 2,
    });
    const ro = readonly({ foo: 1 });

    const r = ref(0);
    console.log(isProxy(state));
    console.log(isProxy(ro));
    console.log(isProxy(r));

    return {
      state,
    };
  },
};
</script>

The first 2 console logs are log true since we created variables with reactive and readonly .

And the 3rd one logs false since a ref isn’t created with reactive or readonly .

isReactive

We can check if a variable is created from reactive with isReactive .

For instance, we can write:

<template>
  <div></div>
</template>

<script>
import { isReactive, reactive, readonly, ref } from "vue";
export default {
  name: "App",
  setup() {
    const state = reactive({
      foo: 1,
      bar: 2,
    });
    const ro = readonly({ foo: 1 });

    const r = ref(0);
    console.log(isReactive(state));
    console.log(isReactive(ro));
    console.log(isReactive(r));

    return {
      state,
    };
  },
};
</script>

Only state is created with the reactive function, so only the first console log logs true .

isReadonly

We can check if a variable is created with readonly is isReadonly .

For instance, we can write:

<template>
  <div></div>
</template>

<script>
import { isReadonly, reactive, readonly, ref } from "vue";
export default {
  name: "App",
  setup() {
    const state = reactive({
      foo: 1,
      bar: 2,
    });
    const ro = readonly({ foo: 1 });

    const r = ref(0);
    console.log(isReadonly(state));
    console.log(isReadonly(ro));
    console.log(isReadonly(r));

    return {
      state,
    };
  },
};
</script>

to call isReadonly .

Only the 2nd console log logs true since only ro is created with readonly .

Conclusion

We can use various functions from the Vue 3 composition API to do various checks on reactive properties.

Categories
Vue 3

Create Vue 3 Apps with the Composition API — Refs

It lets us extract logic easily an not have to worry about the value of this in our code.

It also works better with TypeScript because the value of this no longer has to be typed.

In this article, we’ll look at how to create Vue 3 apps with the Composition API.

Using Refs with v-for

We can assign refs to items rendered with v-for .

For instance, we can write:

<template>
  <div
    v-for="(item, i) in list"
    :ref="
      (el) => {
        divs[i] = el;
      }
    "
    :key="i"
  >
    {{ item }}
  </div>
</template>

<script>
import { ref, reactive, onBeforeUpdate } from "vue";

export default {
  setup() {
    const list = reactive([1, 2, 3]);
    const divs = ref([]);

    onBeforeUpdate(() => {
      divs.value = [];
    });

    return {
      list,
      divs,
    };
  },
};
</script>

We create the divs reactive property with the ref function.

Then when we render the items with v-for , we assign the el HTML element object as an entry of the divs array.

Reactivity Utilities

The Vue 3 composition API comes with some utility functions to do various things with reactive properties.

unref

The unref function lets us return the ref’s value or the argument we pass into the ref function depending on if the variable is a ref.

For instance, we can write:

<template>
  <div>
    <button @click="increment">increment</button>
    {{ count }}
  </div>
</template>

<script>
import { ref, unref, watch } from "vue";
export default {
  name: "App",
  setup() {
    const count = ref(0);
    const increment = () => {
      count.value++;
    };
    watch(count, () => {
      console.log(unref(count));
    });

    return {
      count,
      increment,
    };
  },
};
</script>

We create the count reactive property.

And in the watch callback, we call unref(count) to get the actual value of the count reactive property.

If it’s not a ref, it returns the value of the variable we pass in.

toRef

The toRef function lets us create a ref for a property on a reactive source object.

For instance, we can write:

<template>
  <div>
    <button @click="increment">increment</button>
    {{ count }}
    {{ state.count }}
  </div>
</template>

<script>
import { reactive, toRef } from "vue";
export default {
  name: "App",
  setup() {
    const state = reactive({
      count: 1,
    });

    const count = toRef(state, "count");
    const increment = () => {
      state.count++;
    };

    return {
      state,
      count,
      increment,
    };
  },
};
</script>

We call toRef to create a ref from the state.count property.

Now we can include it in the object we return and then reference it in the template.

This is useful if we want to pass it into a composition function.

For instance, we can write:

<template>
  <div>
    <button @click="increment">increment</button>
    {{ count }}
    {{ state.count }}
  </div>
</template>

<script>
import { reactive, toRef, watch } from "vue";
export default {
  name: "App",
  setup() {
    const state = reactive({
      count: 1,
    });

    const count = toRef(state, "count");
    const increment = () => {
      state.count++;
    };

    watch(count, (count) => console.log(count));

    return {
      state,
      count,
      increment,
    };
  },
};
</script>

We pass the count into watch so we can watch its value.

Conclusion

We can assign refs to HTML elements to let us access them in our component.

And we can manipulate refs with various functions that comes with Vue 3’s composition API.