DEV Community

Mark
Mark

Posted on • Updated on

Building Vue3 Component Library from Scratch #4 How to Create Component

This article will introduce how to develop a component in the component library, including:

  • How to debug components locally in real-time
  • How to support global imports in the component library
  • How to name components using the setup syntax sugar
  • How to develop a component

Directory Structure

Create two packages named components and utils under the packages directory. The components package will store our components, while the utils package will store common methods and utilities. Run pnpm init in both directories and rename their package names to @stellarnovaui/components and @stellarnovaui/utils.

{
  "name": "@stellarnovaui/components",
  "version": "1.0.0",
  "description": "",
  "main": "index.ts",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
Enter fullscreen mode Exit fullscreen mode

Create src folder under the components directory to store all components. The final directory structure will be:

packages/
└── components/
    └── src/
        └── button/
            ├── button.vue
            ├── index.ts
            ├── package.json
utils/
play/
Enter fullscreen mode Exit fullscreen mode

Of course, this is just the current structure. It will be adjusted later to include directories for styles, tests, and other files.

Button Component

Create a simple button component in the button.vue file:

<template>
  <button>Test Button</button>
</template>
Enter fullscreen mode Exit fullscreen mode

Then export the Button in button/index.ts

import Button from "./button.vue"

export { Button }
export default Button
Enter fullscreen mode Exit fullscreen mode

Because we will have many components later, such as Icon, Upload, Select, etc., we need to export all components collectively in components/src/index.ts.

export * from './button'
Enter fullscreen mode Exit fullscreen mode

Finally, export all components in components/index.ts to make them available for external use.

export * from './src/index'
Enter fullscreen mode Exit fullscreen mode

Next, we'll test the component library in the play project set up in the previous article. First, install the @stellarnovaui/components package locally in the play project.

pnpm add @stellarnovaui/components
Enter fullscreen mode Exit fullscreen mode

Using the Button Component in app.vue

<script lang="ts" setup>
import { Button } from "@stellarnovaui/components";
</script>
<template>
  <div>
    <Button />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Once you start the project, you should see the Button component. Additionally, any changes you make to the Button component will be reflected immediately due to hot module replacement (HMR).

Image description

app.use Global Component Registration

Sometimes, when we use components, we want to directly use app.use() to mount the entire component library. When using app.use(), it actually calls the install method of the passed parameter. Therefore, we first add an install method to each component, and then export the entire component library. We modify button/index.ts as follows:

import _Button from "./button.vue";
import type { App, Plugin } from "vue";
type SFCWithInstall<T> = T & Plugin;
const withInstall = <T>(comp: T) => {
  (comp as SFCWithInstall<T>).install = (app: App) => {
    const name = (comp as any).name;
    // register component
    app.component(name, comp as SFCWithInstall<T>);
  };
  return comp as SFCWithInstall<T>;
};
export const Button = withInstall(_Button);
export default Button;
Enter fullscreen mode Exit fullscreen mode

Modify components/index.ts to the following:

import * as components from "./src/index";
export * from "./src/index";
import { App } from "vue";

export default {
  install: (app: App) => {
    for (let c in components) {
      app.use(components[c]);
    }
  },
};
Enter fullscreen mode Exit fullscreen mode

At this point, we need to give button.vue a name, such as ea-button, so that it can be used as the component name when globally mounted.

<template>
  <button>Test Button</button>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  name: "sn-button",
  setup() {
    return {};
  },
});
</script>
Enter fullscreen mode Exit fullscreen mode

At this point, globally mount the component library in play/main.ts.

import { createApp } from "vue";
import App from "./app.vue";
import stellarnovaui from "@stellarnovaui/components";

const app = createApp(App);
app.use(stellarnovaui);

app.mount("#app");
Enter fullscreen mode Exit fullscreen mode

Use the sn-button component in App.vue, and you will find that the component library has been successfully mounted.

<script lang="ts" setup>
</script>
<template>
  <div>
    <sn-button />
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

However, this global component does not have any property hints, so we need to use Volar in VSCode to add hinting for the global component.

Firstly, install @vue/runtime-core.

pnpm add @vue/runtime-core -D -w
Enter fullscreen mode Exit fullscreen mode

Create components.d.ts file under the src directory.

import * as components from "./index";
declare module "@vue/runtime-core" {
  export interface GlobalComponents {
    EaButton: typeof components.Button;
    EaIcon: typeof components.Icon;
  }
}
export {};
Enter fullscreen mode Exit fullscreen mode

At this point, the globally imported components also have hinting functionality.

Note: When users use the component library, they need to configure types: ["stellarnovaui/lib/src/components"] in their tsconfig.json to enable the hinting functionality.

"compilerOptions": {
    //...
    "types": ["easyest/lib/src/components"]
},
Enter fullscreen mode Exit fullscreen mode

Use Setup sugar

We all know that using the setup syntax for Vue component development is very convenient. However, there is one issue: how do we name the components when using the setup syntax?

In fact, there are two solutions. One is to add another <script> tag for naming, for example in input.vue.

<template>
  <button>Test Button</button>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  name: "sn-button"
});
</script>
<script lang="ts" setup></script>
Enter fullscreen mode Exit fullscreen mode

Obviously, this approach is not elegant.

The second solution is to use the unplugin-vue-define-options plugin. we need to configure it in the play project.

Firstly, install unplugin-vue-define-options globally, as we will also need this plugin for packaging configuration later. Installing the latest version may show errors. For now, use // @ts-ignore to ignore the errors.

pnpm add unplugin-vue-define-options  -D -w
Enter fullscreen mode Exit fullscreen mode

Then import this plugin in play/vite.config.ts

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// @ts-ignore
import DefineOptions from "unplugin-vue-define-options/vite";
export default defineConfig({
  plugins: [vue(), DefineOptions()],
});
Enter fullscreen mode Exit fullscreen mode

Now we can use defineOptions to name component directly.

<template>
  <button>Test Button</button>
</template>

<script lang="ts" setup>
defineOptions({ name: "sn-button" });
</script>
Enter fullscreen mode Exit fullscreen mode

Component Development

We all know that a component needs to accept some parameters to achieve different effects. For example, the Button component needs to accept properties like type, size, and round. Here, we will temporarily accept only the type property to develop a simple Button component.

We can assign different class names to the Button component based on the passed type.

// button.vue
<template>
  <button class="sn-button" :class="buttonStyle"><slot /></button>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
  name: "sn-button",
});
</script>

<script lang="ts" setup>
import "../style/index.less";
import { computed } from "vue";
type ButtonProps = {
  type?: string;
};
const buttonProps = defineProps<ButtonProps>();

const buttonStyle = computed(() => {
  return { [`sn-button--${buttonProps.type}`]: buttonProps.type };
});
</script>
Enter fullscreen mode Exit fullscreen mode

Here, we create a style file. Create a style folder under the button directory to store the styles for the Button component.

button/style/index.less

.sn-button {
  display: inline-block;
  line-height: 1;
  white-space: nowrap;
  cursor: pointer;
  background: #fff;
  border: 1px solid #dcdfe6;
  color: #606266;
  -webkit-appearance: none;
  text-align: center;
  box-sizing: border-box;
  outline: none;
  margin: 0;
  transition: 0.1s;
  font-weight: 500;
  padding: 12px 20px;
  font-size: 14px;
  border-radius: 4px;
}

.sn-button.sn-button--primary {
  color: #fff;
  background-color: #409eff;
  border-color: #409eff;

  &:hover {
    background: #66b1ff;
    border-color: #66b1ff;
    color: #fff;
  }
}
Enter fullscreen mode Exit fullscreen mode

now change app.vue in play project:

<script lang="ts" setup>
</script>
<template>
  <div>
    <sn-button type="primary">Test Button</sn-button>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Since component development may involve a lot of content, we won't go into detail here. Instead, we'll briefly introduce the general approach to component development. In the future, we will specifically develop some commonly used components.

The code: https://github.com/markliu2013/vue3-components

Top comments (0)