Building Vue3 Component Library from Scratch #4 How to Create Component
Tags: vue
Published: 2024-05-21 13:40:20

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`.

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

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/
```

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:

```vue
<template>
  <button>Test Button</button>
</template>
```

Then export the Button in `button/index.ts`

```javascript
import Button from "./button.vue"

export { Button }
export default Button
```

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`.

```javascript
export * from './button'
```

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

```javascript
export * from './src/index'
```

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.

```bash
pnpm add @stellarnovaui/components
```

Using the Button Component in `app.vue`

```vue
<script lang="ts" setup>
import { Button } from "@stellarnovaui/components";
</script>
<template>
  <div>
    <Button />
  </div>
</template>
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0i26jxu0dekjkzlm1pn3.png)

## `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:

```typescript
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;
```

Modify `components/index.ts` to the following:

```typescript
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]);
    }
  },
};
```

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.

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

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

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

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

app.mount("#app");
```

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

```vue
<script lang="ts" setup>
</script>
<template>
  <div>
    <sn-button />
  </div>
</template>
```

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`.

```bash
pnpm add @vue/runtime-core -D -w
```

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

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

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.


```json
"compilerOptions": {
    //...
    "types": ["easyest/lib/src/components"]
},
```

## 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`.

```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>
```

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.

```bash
pnpm add unplugin-vue-define-options  -D -w
```

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

```typescript
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()],
});
```

Now we can use defineOptions to name component directly.

```vue
<template>
  <button>Test Button</button>
</template>

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

## 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`.

```vue
// 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>
```

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

```css
.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;
  }
}
```

now change app.vue in play project:

```vue
<script lang="ts" setup>
</script>
<template>
  <div>
    <sn-button type="primary">Test Button</sn-button>
  </div>
</template>
```

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