Building a common Vue component library using Vue Demi

Posted by rgpayne on Sun, 13 Feb 2022 14:44:07 +0100

In this article, we learn about Vue Demi by considering its function, how it works, and how to start using it.

Vue Demi is a great package with a lot of potential and practicality. I strongly recommend using it when creating the next Vue library.

According to the founder Anthony Fu, Vue Demi Is a development utility that allows users to write common Vue libraries for Vue 2 and Vue 3 without worrying about the version they install.

Previously, to create a Vue library that supports two target versions, we used different branches to separate support for each version. They are usually a more stable library for existing code.

The downside is that you need to maintain two code bases, which doubles your workload. I do not recommend this approach for new Vue libraries that want to support two target versions of Vue. Implementing two function requests and bug fixes is not ideal at all.

That's where Vue Demi comes in. Vue Demi solves this problem by providing common support for both target versions, which means that you can get all the benefits of both target versions in just one build.

In this article, we'll learn what Vue Demi does, how it works, and how to start building a common Vue component library.

Additional API s in Vue Demi

In addition to the APIs already provided by Vue, Vue Demi has introduced some additional APIs to help distinguish users' environments and execute version specific logic. Let's take a closer look at them!

Note that Vue Demi also includes standard API s that already exist in Vue, such as ref, onMounted, onUnmounted, and so on.

isVue2 and isVue3

In Vue Demi, the isvue2 and isvue3 API s allow users to apply version specific logic when creating Vue libraries.

For example:

import { isVue2, isVue3 } from 'vue-demi' 
if (isVue2) { 
  // Vue 2 only 
} else { 
  // Vue 3 only 
}

vue2

Vue Demi provides vue2 API, which allows users to access the global API of Vue 2, as shown below:

import { Vue2 } from 'vue-demi' 
// in Vue 3 `Vue2` will return undefined 
if (Vue2) { 
  Vue2.config.devtools = true 
}

install()

In Vue 2, the Composition API is provided as a plug-in. Before using it, it needs to be installed on the Vue instance:

import Vue from 'vue' 
import VueCompositionAPI from '@vue/composition-api' 

Vue.use(VueCompositionAPI)

Vue Demi will try to install it automatically, but for cases where you want to ensure that the plug-in is installed correctly, the install() API is provided to help you.

It acts as Vue The secure version of use (vuecompositionapi) is publicly available:

import { install } from 'vue-demi' 

install()

Getting started with Vue Demi

To start using Vue Demi, you need to install it into your application. In this article, we will create a Vue component library that integrates Paystack payment gateway.

You can install Vue Demi like this:

// Npm 
npm i vue-demi 

// Yarn 
yarn add vue-demi

You also need to add vue and @ vue / composition API as peer dependencies of the library to specify which version it should support.

Now we can import Vue Demi into our application:

<script lang="ts"> 
import {defineComponent, PropType, h, isVue2} from "vue-demi" 

export default defineComponent({
  // ... 
}) 
</script>

As shown here, we can use standard Vue API s that already exist, such as defineComponent, PropType, and h.

Now that we have imported Vue Demi, let's add our props. These are the attributes that users need (or don't need, depending on your taste) to pass in when using the component library.

<script lang="ts">
import {defineComponent, PropType, h, isVue2} from "vue-demi"
// Basically this tells the metadata prop what kind of data is should accept
interface MetaData {
  [key: string]: any
}

export default defineComponent({
  props: {
    paystackKey: {
      type: String as PropType<string>,
      required: true,
    },
    email: {
      type: String as PropType<string>,
      required: true,
    },
    firstname: {
      type: String as PropType<string>,
      required: true,
    },
    lastname: {
      type: String as PropType<string>,
      required: true,
    },
    amount: {
      type: Number as PropType<number>,
      required: true,
    },
    reference: {
      type: String as PropType<string>,
      required: true,
    },
    channels: {
      type: Array as PropType<string[]>,
      default: () => ["card", "bank"],
    },
    callback: {
      type: Function as PropType<(response: any) => void>,
      required: true,
    },
    close: {
      type: Function as PropType<() => void>,
      required: true,
    },
    metadata: {
      type: Object as PropType<MetaData>,
      default: () => {},
    },
    currency: {
      type: String as PropType<string>,
      default: "",
    },
    plan: {
      type: String as PropType<string>,
      default: "",
    },
    quantity: {
      type: String as PropType<string>,
      default: "",
    },
    subaccount: {
      type: String as PropType<string>,
      default: "",
    },
    splitCode: {
      type: String as PropType<string>,
      default: "",
    },
    transactionCharge: {
      type: Number as PropType<number>,
      default: 0,
    },
    bearer: {
      type: String as PropType<string>,
      default: "",
    },
  }
</script>

The properties seen above are required to use the Popup JS of Paystack.

Popup JS provides an easy way to integrate Paystack into our website and start receiving payments:

data() {
    return {
      scriptLoaded: false,
    }
  },
  created() {
    this.loadScript()
  },
  methods: {
    async loadScript(): Promise<void> {
      const scriptPromise = new Promise<boolean>((resolve) => {
        const script: any = document.createElement("script")
        script.defer = true
        script.src = "https://js.paystack.co/v1/inline.js"
        // Add script to document head
        document.getElementsByTagName("head")[0].appendChild(script)
        if (script.readyState) {
          // IE support
          script.onreadystatechange = () => {
            if (script.readyState === "complete") {
              script.onreadystatechange = null
              resolve(true)
            }
          }
        } else {
          // Others
          script.onload = () => {
            resolve(true)
          }
        }
      })
      this.scriptLoaded = await scriptPromise
    },
    payWithPaystack(): void {
      if (this.scriptLoaded) {
        const paystackOptions = {
          key: this.paystackKey,
          email: this.email,
          firstname: this.firstname,
          lastname: this.lastname,
          channels: this.channels,
          amount: this.amount,
          ref: this.reference,
          callback: (response: any) => {
            this.callback(response)
          },
          onClose: () => {
            this.close()
          },
          metadata: this.metadata,
          currency: this.currency,
          plan: this.plan,
          quantity: this.quantity,
          subaccount: this.subaccount,
          split_code: this.splitCode,
          transaction_charge: this.transactionCharge,
          bearer: this.bearer,
        }
        const windowEl: any = window
        const handler = windowEl.PaystackPop.setup(paystackOptions)
        handler.openIframe()
      }
    },
  },

The scriptLoaded status helps us know whether the payback popup JS script has been added, and the loadScript method loads the payback popup JS script and adds it to our document header.

The payWithPaystack method is used to initialize the transaction with Paystack Popup JS when calling:

render() {
    if (isVue2) {
      return h(
        "button",
        {
          staticClass: ["paystack-button"],
          style: [{display: "block"}],
          attrs: {type: "button"},
          on: {click: this.payWithPaystack},
        },
        this.$slots.default ? this.$slots.default : "PROCEED TO PAYMENT"
      )
    }
    return h(
      "button",
      {
        class: ["paystack-button"],
        style: [{display: "block"}],
        type: "button",
        onClick: this.payWithPaystack,
      },
      this.$slots.default ? this.$slots.default() : "PROCEED TO PAYMENT"
    )
}

The render function helps us create components without < template > tags and returns a virtual DOM node.

If you notice, we use an API of Vue Demi, isVue2, in the conditional statement to conditionally render our buttons. Without this, if we want to use our component library in Vue 2 applications, we may encounter errors because Vue 2 does not support some APIs of Vue 3.

Now, when we build our library, it can be accessed in Vue 2 and Vue 3.

The complete source code is available here: https://github.com/ECJ222/vue-paystack2

Topics: Javascript Front-end Vue.js