preface

Recently, I learned Vue 3 systematically, and wanted to make some wheels to improve the efficiency of my own development, so I came up with the idea of making an official website of UI component library. Currently, there are Input, Switch, Dialog, Button, and Tabs components

Preview link (requires scientific Access) Source code link (requires scientific access)

Environment construction and preparation

The installation

  • create-vite-app, can be used directlyyarn devOpen the page for debugging;
  • vue-router, which is used to configure routes for different pages.
  • There are othergithub-markdown-css,marked,sassprismjs.

The specific version number can be found in package.json:

{
  "name": "sky-ui-dev"."version": "0.0.0"."scripts": {
    "dev": "vite"."build": "vite build"
  },
  "dependencies": {
    "github-markdown-css": "4.0.0"."prismjs": "^ 1.21.0"."vue": "^ 3.1.3"."vue-router": "Three 4.0.0 - beta."
  },
  "devDependencies": {
    "@vue/compiler-sfc": "^ 3.1.3"."marked": "^ 2.1.1"."sass": "^ 1.32.11"."vite": 1 "" ^ 1.0.0 - rc.}}Copy the code

CSS minimum impact principles for UI libraries

(1) Scoped is not recommended

XXX in the HTML tag data-v-xxx is a random number every time we run, we need to output stable class selector, convenient for users to cover.

(2) The class prefix needs personalization

Sky-theme-button, for example, is not easily overridden by the user. Prefixes such as.button and.theme-link are more generic and can easily be overwritten by users.

(3) Store the common style of UI library

We can create a new sky.scss to hold the common styles of the UI library, and use the write name prefix only for that UI library.

[class^="sky-"].[class*=" sky-"] {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
Copy the code

[class^=”sky-“] =

[class*=” sky-“] means a class starting with space +tree-, equivalent to a tag like

.

Project directory structure

Plugins: The plugins that are stored in the markdown documentation that describes the project;

Assets: official website logo;

Components: Vue files for different pages;

Lib: component file;

Markdown: website introduction and start documentation;

Views: page files on the official website.

Vue Router configuration

Create a router-. ts file in the project root directory. We use hash mode to configure routes here, and do not need to set up a background server.

/ / hash pattern
const history = createWebHashHistory();
export const router = createRouter({
    history: history,
  	// Link to different files
    routes: [{path: '/'.component: Home},
        {
            path: '/doc'.component: Doc,
            children: [{path: ' '.redirect: '/doc/intro'},
                {path: 'intro'.component: md(intro)},
                {path: 'get-started'.component: md(getStarted)},
                {path: 'switch'.component: SwitchDemo},
                {path: 'button'.component: ButtonDemo},
                {path: 'dialog'.component: DialogDemo},
                {path: 'tabs'.component: TabsDemo},
                {path: 'radio'.component: RadioDemo},
                {path: 'input'.component: InputDemo}
            ]
        }
    ]
});
router.afterEach(() = > {});
Copy the code

Add

to app.vue

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

<script lang="ts">
import {router} from './router';

export default {
  name: 'App'.setup(){}}</script>
Copy the code

In main.ts add:

app.use(router)
Copy the code

At this point, the preliminary work of a project has been basically prepared, and then we begin to develop our UI library.

Component development

More details in the development process, here to pick a few representative out to talk about.

How do I get a part of a component to fire an event?

inButtonComponent, external onedivRed box, click on thisdivIt triggers an event, but all we have to do is clickbuttonTriggers an event. How can this be done?

Step 1: Make divs in components do not inherit properties

Set inheritAttrs: false and clicking on div will not trigger an event. Inherit means inheritance, attrs is short for attributes, which means attributes. However, clicking on a button at this point does not trigger an event, because this statement masks all inherited events by default. Next we need to do another setup.

<script lang="ts">
export default {
  inheritAttrs: false
}
</script>
Copy the code

Step 2: Bind $attrs to buttons in the div

$attrs allows an element of a button to inherit all attributes bound to a button. Add v-bind=”$attrs” to the button.

<button v-bind="$attrs">
Copy the code

Click button at this point to see the @click/@focus/@mouseover event results appear in the console.

How do I load events for different components on demand?

Let’s say I have a component that wants the click and Focus events, and another component that only needs the click event. How do I implement this? The main idea is to declare setup inside the component, pass in parameters, and return the obtained parameters.

<template>
  <div :size="size"> <! -- Step 3: Define the event or attribute on the tag, where the size is specified -->
    <button v-bind="$attrs">
      <slot/>
    </button>
  </div>
</template>

<script lang="ts">
export default {
  inheritAttrs: false.setup(props, context){
    // Step 1: Destructor syntax to get an external event or attribute
    const {size, onClick, onMouseOver} = context.attrs;
    // Step 2: Return the size event or property
    return {size}
  }
}
</script>
Copy the code

You can also simplify by using ES6’s latest remaining operators. To get to… After REST, use it in the template.

<script lang="ts">
export default {
  inheritAttrs: false.setup(props, context){
    / / use... Rest extension operator that gets other properties or events other than size
    const{size, ... rest} = context.attrs;return {size, rest}
  }
}
</script>
Copy the code

Named slot in the popover

We created a Dialog that lets the user customize its title and content, using a named slot called V-slot. Vue 3 uses a named slot differently from Vue 2.

Step 1: Define the slot name using v-slot: XXX:

<template v-slot:content>
   <div>hello</div>
</template>
Copy the code

Step 2: Introduce slots where needed, using

<main>
    <slot name="content"/>
</main>
Copy the code

The results are as follows:

Use Teleport to “deliver” components

So how do we prevent the Dialog from being blocked? This is where teleport comes in. Teleport in Chinese it means “send”, we can use it to put the Dialog “send” to go out, it will not be affected by other elements.

Use

to wrap the component that needs to be “sent” and add to=”body” to specify the destination to be sent, moving directly under the body tag.

Upon success, we can see that the Dialog has been successfully “delivered” in the developer tools.

Dynamically mount components

If we don’t want to declare a variable in the component and want to change the value of the variable, we can use the h of createApp to do so. Let’s take the one click to open the Dialog component as an example.

Step 1: Create a Button, add @click=”showDialog” event, and click to open the Dialog;

Step 2: Call openDialog from the showDialog function. The user can pass title, content, OK, cancel from the contents of the defined Dialog component.

<script>
    const showDialog = () = >{
      openDialog({
        title:'title'.content:'hello'.ok(){console.log('ok'); },cancel(){console.log('cancel'); }})}return {showDialog}
  }
}
</script>
Copy the code

Step 3: Create an OpenDialog. ts component that retrieves external properties and creates a div directly in the body. Here you need to use h() to construct the new Dialog.

import Dialog from './Dialog.vue';
import {createApp, h} from 'vue';

export const openDialog = (options) = > {
    const {title, content, ok, cancel} = options;
    const div = document.createElement('div');
    document.body.appendChild(div);
    const close = () = > {
        app.unmount(div);
        div.remove();
    };
    const app = createApp({
        render() {
          // Use h to construct the Dialog
            return h(Dialog, {
                visible: true.'onUpdate:visible': (newVisible) = > {
                    if (newVisible === false) { close(); } }, ok, cancel }, { title, content }); }});// Mount the new div
    app.mount(div);
};
Copy the code

useTemplate RefsDynamic settingdivThe width of the

After making the Tab bottom navigation alert, I set the width to 100px at first, but the width is more than the width of the text. For component libraries, how do you dynamically set the width of the navigation bar based on the user’s text? We need to use Template Refs in Vue3.

As described in Vue 3, we bind a ref to the navigation text div and use the value in the ref to get the width of the div when mounting the component.

<template>
  <div :ref="element => { if(element) navItems[index] = element }"></div><! If element exists, then make the element in navItems equal to element--><div class="tree-tabs-nav-indicator"></div></template> <script lang="ts"> import {onMounted, ref} from 'vue'; export default { setup(props, context) { const navItems = ref<HTMLDivElement[]>([]); const indicator = ref<HTMLDivElement>(null); onMounted(() => { const divs = navItems.value; const result = divs.filter(div=>div.classList.contains('selected'))[0] const {width} = result.getBoundingClientRect() indicator.value.style.width = width + 'px' }); return {navItems, indicator}; }}; </script>Copy the code

After running the code successfully, we can get indicator width in real time from the console.

Component source code beautification

usecustom blockShow source code

The Custom Block plug-in is used when you want to display the source code from a component on the page.

Step 1: Add the following code to vite. Config. ts, which is used to parse the code in the component.

import {md} from './plugins/md'
import fs from 'fs'
import {baseParse} from '@vue/compiler-core'

export default {
    vueCustomBlockTransforms: {
        demo: (options) = > {
            const { code, path } = options
            const file = fs.readFileSync(path).toString()
            const parsed = baseParse(file).children.find(n= > n.tag === 'demo')
            const title = parsed.children[0].content
            const main = file.split(parsed.loc.source).join(' ').trim()
            return `export default function (Component) {
        Component.__sourceCode = The ${JSON.stringify(main)
            }
        Component.__sourceCodeTitle = The ${JSON.stringify(title)}} `.trim()
        }
    }
}
Copy the code

Step 2: In the component that you want to show the code to, add the

tag:

</demo><template>
  <Switch v-model:value="bool"/>
</template>
Copy the code

Step 3: Display code in the parent component:

<div class="demo-code">
  <pre>{{Switch1Demo.__sourceCode}}</pre>
</div>
Copy the code

useprismjsv-htmlHighlight source code

Prismjs is a library for highlighting code that needs to be introduced and is suitable for displaying code in our UI library.

To install prismjs, run yarn add prismjs

Introduced in a component and exported using setup() for ease of use:

import 'prismjs';
import 'prismjs/themes/prism-okaidia.css';
// In the themes file, you can try a few more and use the one you like better

const Prism = (window as any).Prism;
export default {
  setup() {
    return{Prism}; }};Copy the code

This is shown in the template:

<template>
   <div class="demo-code">
     <pre class="language-html" v-html="Prism.highlight(Switch1Demo.__sourceCode, Prism.languages.html,'html')"/>
   </div>
</template>
Copy the code

Highlighting the code looks like this:

Compatible with TypeScript and MarkDown

Sometimes libraries do not have TypeScript declarations. You can run the following code after installing the library, with XXX as the library name:

yarn add --dev @types/xxx
Copy the code

Ides do not recognize markdown files, so you can declare this in shims.d.ts to avoid errors:

declare module '*.md'{
    const str: string
    export default str
}
Copy the code

Automate project packaging and deployment

Run the following command:

yarn build
Copy the code

This will generate a dist folder in the project root directory. You just need to upload this folder to Github.

Typically, it takes a lot of command lines to go from packaging to deploying github, so we can automate the deployment using a single deploy.sh.

Create a deploy.sh file in the project root directory and run the following command (add && to confirm the running status of the command. For example, if a command fails to run, the system stops the operation) :

rm -rf dist &&
yarn build &&
cd dist &&
git init &&
git add . &&
git commit -m "update"&& git branch -m main && // Enter your github repository address git remote add origin [email protected]: XXXXXX && git push -f -u origin main &&cd.Copy the code

After that, you only need to enter sh deploy.sh in the terminal to implement one-line code deployment.

If this post has helped you, please follow, like, or comment