DEV Community

V
V

Posted on • Edited on

Create a universal Vue component library with Vue Demi and Tailwind CSS

Image description

Vue Demi is a developing utility that allows Vue developers to write a universal Vue library for Vue 2 and 3. Honestly though, it's just much better to migrate all your Vue libraries and applications to the latest version now that Vue 3 seems stable. But for the sake of flexibility for your existing Vue libraries, Vue Demi is the answer to your prayers.

For this tutorial, I also used Tailwind CSS to do our component design faster. Tailwind CSS is a utility-first CSS framework packed with a variety of classes that can be composed to build any design, directly in your markup. I know lots of people are hating this CSS framework (I was one of them at first), but it happened to grow on me.

Anyways, let's get started!

Create base setup for the component library

Let's create the base setup with Vue 3 via create-vue.

npm create vue@3
Enter fullscreen mode Exit fullscreen mode

I selected these configurations, but you can change it based on your preferences.

Image description

After selecting configurations, go to the directory of the created project, then install the packages via npm.

cd vue-universal-library
npm install
Enter fullscreen mode Exit fullscreen mode

Add Tailwind CSS configuration

For this example, I will be using Tailwind CSS for the component design.

Let's start by installing Tailwind CSS and its peer dependencies to the created vue project. Then, run the init command to automatically create tailwind and postcss configuration files.

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

For reference on the later steps of this tutorial, here are the configurations inside my tailwind.config.js file.

// tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
  theme: {
    extend: {
      colors: {
        primary: {
          DEFAULT: '#f25f70',
          light: '#f25f70e6',
          dark: '#c14c59'
        },
      }
    }
  },
  plugins: []
}
Enter fullscreen mode Exit fullscreen mode

Lastly, you need to add the tailwind directives to your css file.

/* tailwind.css */

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Please refer to the Tailwind CSS installation guide for more details.

Install Vue Demi

We will now install vue-demi to start with the creation of our universal component library.

npm i vue-demi
Enter fullscreen mode Exit fullscreen mode

Update package.json to add vue and @vue/composition-api as peer dependencies.

// package.son

{
  ...
  "dependencies": {
    "vue-demi": "^0.13.11"
  },
  "peerDependencies": {
    "@vue/composition-api": "^1.0.0-rc.1",
    "vue": "^2.0.0 || >=3.0.0"
  },
  "peerDependenciesMeta": {
    "@vue/composition-api": {
      "optional": true
    }
  },
  "devDependencies": {
    ...
    "vue": "^3.2.47",
    ...
  }
}
Enter fullscreen mode Exit fullscreen mode

After this, we can now create our component but instead of importing vue-related stuff from vue, we import from vue-demi.

Create your component

For this sample component library, I only created a button component based on Tailwind CSS design.

This is the snippet content of my TwButton.ts file which renders the button component based on props and checks Vue version via isVue2 API of vue-demi. When doing version-specific logic, it is best to check the migration guide for the breaking changes between Vue 2 and Vue 3.

// TwButton.ts

// import from 'vue-demi' instead of 'vue'
import { defineComponent, h, isVue2 } from 'vue-demi'

import '../assets/tailwind.css'

const TwButton = defineComponent({
  name: 'TwButton',
  props: { ... },
  emits: ['click'],
  setup(props, { emit, slots }) {
    function renderV2() { ... }

    function renderV3() {
      return () =>
        h(
          'button',
          {
            disabled: props.disabled,
            class: {
              'btn': true,
              'btn-flex': props.flex,
              'opacity-50 cursor-not-allowed': props.disabled,
              '!rounded-full': props.pill,
              'border': props.bordered,
              'border-dark': props.bordered && props.bg === 'clear',
              'border-primary-dark': props.bordered && props.bg === 'fill',
              'btn-clear': props.bg === 'clear',
              'btn-fill': props.bg === 'fill'
            },
            onClick: (event: Event) => emit('click', event)
          },
          [
            h('span', { class: 'btn-contents' }, [
              props.icon
                ? h(
                    'span',
                    {
                      class: {
                        'icon material-icons': true,
                        'ml-[-2px] mr-1.5': slots.default
                      }
                    },
                    props.icon
                  )
                : '',
              h('span', slots.default ? slots.default() : '')
            ])
          ]
        )
    }

    return isVue2 ? renderV2() : renderV3()
  }
})

export default TwButton
Enter fullscreen mode Exit fullscreen mode

For this example, I just made a separate function to render the component based on Vue version, but you can make your own wrapper to the h function just like what this vue-demi-related GitHub discussion provided.

Build library and publish

Now that the component is created, let's build the library using rollupjs.

First, we update package.json to add build:lib script that removes dist folder then builds the library via rollup.

// package.json

{
  "name": "vue-universal-library",
  ...
  "main": "dist/vue-universal-library.cjs.min.js",
  "browser": "dist/vue-universal-library.esm.min.js",
  "unpkg": "dist/vue-universal-library.global.min.js",
  "scripts": {
    ...
    "build:lib": "rimraf dist && rollup -c rollup.config.js --bundleConfigAsCjs",
    ...
  },
  ...
}

Enter fullscreen mode Exit fullscreen mode

Then, we create the rollup configuration. You can check here for the full contents of rollup.config.js.

// rollup.config.js

...
const pkg = require('./package.json')

export default {
  plugins: [...],
  external: ['vue-demi'], // add vue-demi as external
  input: ['src/index.ts'],
  output: [
    ...
    {
      format: 'umd',
      file: pkg.unpkg,
      sourcemap: true,
      name: 'VueUniversalLibrary',
      globals: {
        'vue-demi': 'VueDemi' // add vue-demi to output.globals
      }
    },
  ]
}
Enter fullscreen mode Exit fullscreen mode

After implementing the rollup configuration, let's run the added build script. This will create dist folder that contains the built library files.

npm run build:lib
Enter fullscreen mode Exit fullscreen mode

If all goes well, and assuming you already own an account, we can try publishing our library to npm.

npm publish
Enter fullscreen mode Exit fullscreen mode

Install library to Vue application

In either Vue 2 or Vue 3 application, let's install vue-universal-library via yarn or npm.

# using `yarn`
yarn add vue-universal-library

# using `npm`
npm install vue-universal-library
Enter fullscreen mode Exit fullscreen mode

Once installed, let's import vue-universal-library.min.css to our main file.

// main.ts

import 'vue-universal-library/dist/vue-universal-library.min.css'
Enter fullscreen mode Exit fullscreen mode

Then, let's try using our button component inside App.vue.

<!-- App.vue -->

<script setup lang="ts">
...
import { TwButton } from 'vue-universal-library'

function click() {
  alert('test')
}
</script>

<template>
  <div id="app">
    ...

    <main>
      ...
      <tw-button bg="fill" icon="download" @click="click" pill bordered flex>button</tw-button>
    </main>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

In order to be able to render the icon props in our TwButton component, we need to install Material Icons just by adding this line to our index.html.

<!-- index.html -->

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
Enter fullscreen mode Exit fullscreen mode

Also, if typescript returns a Cannot find module 'vue-universal-library'... error, we just need to create a declaration file for it such as below:

// vue-universal-library.d.ts

declare module 'vue-universal-library'
Enter fullscreen mode Exit fullscreen mode

After the setup, we can now run our Vue test application.

This is how our button looks like in Vue 2. (I did not change anything inside the pre-installed App.vue so all other displays are still there.)

Image description

I installed it in Vue 3 application with bg=clear and this is the result.

Image description

So that's it! You can find the sample Vue universal component library full code repo here. Feel free to comment any suggestions or comments regarding vue-demi and this blog post. Have a Vue-tiful day! 😀

Top comments (0)