UI components encapsulate a set of related interactions and styles, providing simple invocation methods and interfaces that make it easy for developers to use the functionality provided by the components to implement business requirements.

I am in a Vue UI component library named Admin UI (GitHub address: github.com/BboyAwey/ad… Vue component library. But even if you’re a React user, you can use this lesson, because if you’re going to write a React UI component library, you’ll have to deal with almost exactly the same problem.

This article is also the content of one of my technical sharing in the company. I’m going to focus on ideas here and try not to get into the implementation. And my solution to these problems is not entirely reasonable, if there are mistakes, please correct the reader.

Organize your project

When you start component library development, the first thing you will probably do is create a project. Since it is the Vue component library, you will probably use the official recommended VUE-CLI tool to generate a project.

1.1 Appropriate file structure

When the project is generated, you quickly discover that the file structure of the project template is great for business development, but not so great for component library development. In this case, you will most likely create a folder with the name of your component library in its SRC folder to store your component library code. But at this point you don’t know everything that needs to be done, and you haven’t continued to tweak the file structure.

We’ll just name your component library admin-ui for the sake of subsequent articles

When you actually start writing your first component, you will certainly start by writing a page to show the component you are developing and testing it on it. So you create a new examples folder in the SRC folder to hold your sample code.

Your file structure should look something like this:

1.2 Write or generate usage documentation for component libraries

Chances are, after a while, you’ll write a sample page for each component, even if some of the sample pages themselves are pretty good. If one day the main component library development is complete, the stack of instance pages will be of little use. You might want to refine these sample pages, put a list of features and interfaces for each component on their sample pages, or even use sample code, and then deploy them on a server for your users to view at any time. Fortunately, they are not wasted and are used in component libraries!

1.3 Component library itself development document management

Then you might realize the weakness of one person and invite other developers to contribute to your project, but even though you’ve already turned ESlint on when you used vue-CLI to build your project, it’s not enough for multiple people to develop a complete UI library based on the same code style. You may need to create development documentation that publishes conventions and designs, as well as other information that needs to be shared. So you create a new documentation directory in the root directory of your project and use GitBook to generate a document there and synchronize it to the GitBook server so that your friends can view it online even if they haven’t been synchronized.

1.4 Support for various installation modes

You are now ready to start developing your components, and as you write your first component, you realize that the project you are writing is essentially a use document for your component libraries, if it is a business project, and it is a direct use of your component library source code in your own source code. But if other business teams use your library, they currently have to copy the source code into their projects as you do now, and every time your library is updated, they have to make another copy to upgrade it, which is obviously unacceptable. You start thinking about how your users will install your component library.

The first thing that comes to mind is the most popular installation: NPM. If you publish your component library directly to NPM, your users can easily install and upgrade through it or YARN. But right now your component library is just getting started, and you don’t want to open source right away. So you apply for a gitLab repository that your company has built, and then initialize a Git project in the same folder as your component library and synchronize it to the repository. By this time, the company’s colleagues can already pass

npm install admin-ui git+ssh://admin-ui-git-location.git --save
Copy the code

Time to install your component library.

And then the second way you can think of installing it is CDN. Your users do this by inlining their pages

<script src="admin/ui/cdn/location"></script>
Copy the code

To use your component library, this is how to package your component library. In this scenario, you need to package your component library as a whole ADMIN-ui.js file to use. We’ll talk more about packaging in the next section.

Of course, the final installation method is to use the source code directly, putting your component libraries directly into the project source code for reference.

1.5 Package and publish your component libraries

Having identified the installation methods that need to be supported, you are probably aware that you now have two parts of your project that need to be packaged and released:

  • Your component library (this is the point)
  • Usage documentation for your component library (i.e. the project itself)

The first thing you’ll think about is how easy it is to use document packaging, because the project’s current packaging configuration packages all the sample pages you’ve written, and all you have to do is run them

npm run build
Copy the code

But the key problem is your component library admin-UI. It currently exists as part of the source code for your project. So you have to start thinking about how to package this part of the code separately.

Of course, you could not package your component library and distribute its source code directly as an NPM package, but then users would have to rely on the packaging tool to package your code when they use it. Projects generated by tools such as VUe-CLI do not package code from the node_modules folder by default, and users must modify the build configuration to manually specify where to package code, which is inconvenient

Publish. Js, webpack.publish. Conf. js, and publish Env.js file and looked through the Webpack documentation to get rid of unwanted feature configurations and set up configurations to package your component libraries.

You want the packaged code to be placed in your component library folder named dist, and your component library source files will need to be moved to the SRC directory.

Webpack needs to specify an exit file when packaging code, and you find that your component library doesn’t have an exit file itself.

1.6 Full load and load on demand

You create a new index.js file in the SRC folder of the component library, which imports and outputs all the components.

import Button from './components/button'
import Icon from './components/icon'/ /... Omitted code...export{ Button, Icon // ... Omitted code... }Copy the code

At this point, you might as well plan out the file structure of the component library itself:

In this output format, your users can pass

import { Button } from 'admin-ui'
Copy the code

To get the Button component from the component library. While this is just support for the format (it’s not load on demand), users also need to be able to load in full, which means all the components are imported at once and all automatically registered. So you mount all the components in index.js to the adminUi object, then mount the install() method on that object to support vue.use (), and print the object directly. Now your index.js looks like this:

import Button from './components/button'
import Icon from './components/icon'/ /... Omitted code...export{ Button, Icon // ... Omitted code... } const adminUi = { Button, Icon, // ... Omitted code... } adminUi.install =function (Vue, options = {}) {
  Vue.component('cu-button', Button)
  Vue.component('cu-icon', Icon) // ... Omit code, you can also use a loop to write... }export default adminUi
Copy the code

There are many things you can do in the install() method, besides registering components, and it is likely that you will also mount some instance methods in it

That’s when your users can pass through

import adminUi from 'adminUi'
Vue.use(adminUi)
Copy the code

For full load.

The next step is loading on demand. You find that if you simply load a component through your index.js entry file, other components will be compiled into the user’s code without being imported by the user. So you have to think about new ways. Since you can’t load from a single portal, can you specify a load point for each component? You want your users to be able to pass similar

import Button from 'admin-ui/button'
Copy the code

Load individual components this way so that there are no redundant components. So you realize that each component needs to be packaged separately. Package each component as a separate module in the Lib folder in Dist, using the exit file for each component (which may also be an index.js file, where you should be aware of the benefits of having a consistent file structure for each component) as the package entry point. At this point, loading on demand is supported.

I didn’t discuss the webPack configuration because this article focuses on ideas rather than implementation, because this topic would require more space to go into depth, and because webPack configuration itself is complex and I’m not familiar with it.

Then you happily try packaging, but are frustrated to find that you end up with a.css file in addition to a.js file, either as an entry point from the component library itself or as a separate package for each component. Whether your users load it full or load it on demand, they also need to import the corresponding.css file when they import the.js file. In full load, this doesn’t seem to be a big deal since it’s only loaded once. But if you’re loading on demand, because you’re importing it multiple times, it’s a little tricky.

Separate CSS files are for performance reasons, CSS files can be cached by the browser, and components don’t need to regenerate CSS when rendering themselves

There are two solutions. One is to recommend using the babel-plugin-Component, and the other is for the packaged component to no longer provide the CSS file itself, but to import the fully loaded CSS file globally. You can do either, but I’m using the latter.

There are two reasons. First, the styles of the components are small in size, compressed and packaged in 60KB or less (most of which is font-awesome style code; all of the styles of the components are no larger than 5KB). Second, because of the use of font-awesome, if each component introduces its own style separately, components that rely on font-awesome will have duplicate styles.

Design a theme system

When your component library is used in different projects, or when a project needs skin functionality, it is inevitable that you will need to design a theme system in your component library.

2.1 Determine the functional boundaries of the subject system

First, you need to define the functional boundaries of your subject system. In my opinion, there are three main factors affecting the style of a management background system:

  • Color (this is the main thing)
  • shadow
  • Rounded corners

So start by setting the boundaries of your theme system as these three factors.

2.2 Select an appropriate implementation scheme

Then you start thinking about possible thematic systems to implement:

  • Special format string substitution
  • Theme files are precompiled
  • Style class

Special format string substitution is undoubtedly the easiest, when the development needs to be controlled by the theme system style, in THE CSS directly use special format string, at run time to replace. Such as:

div {
  color:? primary? ; }Copy the code

The runtime is replaced by your script with:

div {
  color: #00f;
}
Copy the code

The advantage of this solution is that it is very convenient to develop, basically does not affect the development experience, and even improves. This was not a problem in the traditional jquery era, but in the case of Vue projects, there was a “replacement timing” issue. You can replace all the special characters in the

Theme file precompilation is the mainstream theme implementation scheme in the market. That is, the UI library itself provides tools to generate CSS files for different themes, with several sets of theme style files compiled in advance. Advantages are simple and direct, convenient and easy to use. But the drawback is also obvious: the theme substitution at runtime becomes very rough (coarse-grained) — you can only do a whole set of substitution.

Style classes are designed to apply style rules to desired elements:

.au-theme-font-color--primary {
  color: #00f;
}
Copy the code
<p class="au-theme-font-color--primary">The main color</p>
Copy the code

Style classes also have their obvious drawbacks: first, you need to have very clear style class rule design, and then the development impact is very significant: all styles covered by the theme system can only be written in style classes, not directly in CSS. These two points bring certain cognitive and use burden to the user. But the advantages are equally obvious: the granularity of control can be very fine, there is no replacement timing problem, and you can control not only the theme of the component library itself, but also directly for the entire project.

With an experimental texture, I chose the style class, so assume you made the same choice.

2.3 Use style classes to design and implement your theme system

If you don’t know where to start, try looking at the user of your theme system. You expect your users to be able to control the theme with a simple function:

adminUi.theme(config)
Copy the code

It is natural to define the structure of config. Based on the theme system functions defined above, you can define them as follows:

const config = {
  colors: {
    'base-0': '# 000'.'base-1': '#232a29'.'base-2': '#2f3938'.'base-3': '#44514f '.'base-4': '#616e6c'.'base-5': '#7c8886'.'base-6': '#b1bbba'.'base-7': '#d9dedd'.'base-8': '#eaf0ef'.'base-9': '#f3f7f6'.'base-10': '#f7fcfb'.'base-11': '#fdfdfd'.'base-12': '#fff'.'primary-top': '#01241d'.'primary-up': '#3fd5b8'.'primary': '#19bc9d'.'primary-down': '#169f85'.'primary-bottom': '#e7f7f4'.'info-top': '# 011725'.'info-up': '#f0faf8'.'info': '#3498db'.'info-down': '#2d82ba'.'info-bottom': '#e6f3fc'.'warning-top': '# 251800'.'warning-up': '#fec564'.'warning': '#ffb433'.'warning-down': '#e99b14'.'warning-bottom': '#fbf3e5'.'danger-top': '# 220401'.'danger-up': '#f56354'.'danger': '#e74c3c'.'danger-down': '#c33a2c'.'danger-bottom': '#fae7e5'.'success-top': '# 012401'.'success-up': '#7fcb7f'.'success': '#5cb95c'.'success-down': '#3da63d'.'success-bottom': '#e7fae7'
  },
  shadows: {
    'base': '0 1px 4px rgba(0, 0, 0, .2)'.'primary': '0 0 4px rgba(25, 188, 157, .6)'.'info': '0 0 4px rgba(52, 152, 219, .6)'.'warning': '0 0 4px rgba(255, 180, 51, .6)'.'danger': '0 0 4px rgba(231, 76, 60, .6)'.'success': '0 0 4px rgba(92, 185, 92, .6)'
  },
  radiuses: {
    'small': '2px'.'large': '5px'}}Copy the code
  • primary,warning,danger,info,successAs the dominant color
  • [COLOR]-up,[COLOR]-downIs the secondary color whose lightness is closer to the primary color
  • [COLOR]-top,[COLOR]-bottomIs the auxiliary color whose lightness differs greatly from the main color
  • base-0,base-12Is the darkest color and the brightest color
  • base-[1~11]Is colorless (grey) arranged according to lightness

The reason for not using words with color information (such as light, dark-primary, etc.) but using numbers and directions as color names is to make it easier for users to define arbitrary colors on a name. If you define a pure black name as dark, but the user configates it as # FFF pure white, The name is misleading. In non-color, we use numbers as names, while in color, we use directions as names, which can not only fit the hierarchical design of color, but also avoid ambiguity.

Your set of configuration rules expects the user to be able to configure colors according to their lightness, so that each type of color has a consistent lightness arrangement. This is to facilitate shading between colors, such as using light text on a dark background. The benefit of having this limitation is that users can configure some custom colors.

Also, to further streamline the color configuration, you decide to automatically calculate the shadow, non-primary, and non-color defaults based on the primary color and some secondary configurations. Therefore, the actual configuration of users can be further simplified:

export default {
  theme: {
    colors: { // Color configuration
      primary: '#1c86e2'.info: '#68217a'.warning: '#f5ae08'.danger: '#ea3a46'.success: '#0cb470'
    },
    shadows: { // Shadow configuration
      // primary: '0 0 4px #1c86e2',
      // info: '0 0 4px #68217a',
      // warning: '0 0 4px #f5ae08',
      // danger: '0 0 4px #ea3a46',
      // success: '0 0 4px #0cb470'
    },
    radiuses: {
      small: '3px'.large: '5px'}},lightnessReverse: false.// Reverse lightness sort (black and white theme)
  colorTopBottom: 5.// Top and bottom color distance from pure black and pure white lightness, the smaller the closer to pure black and pure white
  colorUpDown: 10.// The lightness distance between the close color and the positive color
  baseColorLeve: 12.// No number of color grades
  baseColorHue: '20%'.// No color saturation
  baseShadowOpacity: 0.2.// No color shadow opacity
  colorShadowOpacity: 0.6 // Color shadow opacity
}

Copy the code

The file structure of the theme system is as follows:

Next, think about how users can apply the theme system to elements once they have configured it. Your theme system provides style classes that need a memorizable syntax for users to use, so you might design syntax rules like the following:

Prefix [-pseudo-class name] - Attribute name - Attribute value [-weight]Copy the code
  • Prefix: Theme style class prefix
  • Pseudo-class name: Optional, you can concatenate the pseudo-class name in the class name if the topic is applied to the pseudo-class of the current element
  • Attribute name: The attribute name of the style
  • Property value: The property value of the style, which is the name configured in config
  • Weight: Optional, which can be added for the theme style! importantThe suffix

With this set of syntax rules, users can use it like this:

<div class=" au-theme-background-color--base-12 au-theme-border-color--primary au-theme-font-color--base-3 au-theme-box-shadow--base au-theme-radius--small"></div>
Copy the code

Finally, your theme system converts the user’s incoming configuration into concrete style class code based on your syntax rules and inserts it into the page using the

3 Provides a set of form components

Any UI component library, especially one for management systems, inevitably needs to provide a set of forms components. The reason for this is simple. First, the default form controls provided by various browsers are not only different in style but also extremely ugly. Second form typesetting, verification and other functions are just needed, there is no reason not to abstract out.

So first you LIST the common form components: text fields, multiple selections, radio selections, switches, dropdowns, cascades, dates, times, and file uploads, all of which you put into the TODO LIST.

3.1 Unified form interface

You’ll notice that many form components behave the same way: they all need a value interface, they all need v-Model support, they all have input or change events, and so on. These unified behaviors are best placed together, so you use Vue’s mixin functionality to extract these unified behaviors together, on the one hand for ease of management, and on the other hand to keep the form components as consistent as possible in function and behavior, thereby reducing the user’s cognitive cost.

3.2 Unified Authentication Mode

In fact, the validation part of the function is technically a unified form interface, so it can be included in the above file, but the validation part of the logic is relatively independent, so you will probably make a separate minxin to manage it.

If you write a lot of forms, you’ll notice that there are really only two cases of validation:

  • Interactive validation: When a user fills in a form element, the validation of that element is triggered
  • Submit validation: The user triggers validation of all elements of the entire form when submitting

Supporting interactive validation is as simple as using events. To support submission validation, each form component needs to provide a specific validation method for external calls, such as this.$refs.$usernameInput.validate (). Call the validation methods of all form components once when submitting the form.

When your program runs validation code, there are two situations:

  • Concurrent validation
  • Asynchronous validation

Supporting synchronous validation is as simple as calling a given external validator function normally and then returning its results. However, asynchronous validation can be troublesome. Let’s dig a little deeper and assume that the current user specifies a validator for
as follows:

<au-input
  :validatiors="[{validator (v) {return v > 0}, Warning: 'Must be greater than 0'}]"/>
Copy the code

When you get the validator, you don’t know if it’s synchronous or asynchronous, so you might ask the user to specify whether it’s synchronous or asynchronous:

<au-input
  :validatiors="validators"/>
Copy the code
export default {
  data () {
    return {
      validators: [
        {
          validator (v) { return v && v.length && !/^\s*$/g.test(v) },
          warning: 'Cannot be empty'
        },
        {
          validator () {
            return new Promise(resolve= > {
              axios.get('is-duplicated-name')
                .then(data= > resolve(data.result))
            })
          },
          warning: 'It already has a duplicate name'.async: true}]}}}Copy the code

When users like the above points the synchronous or asynchronous validation, and verify the function returns is a promise, you can advance the validator of all can be divided into two categories: synchronous and asynchronous validation, and verify the first synchronization function, if there is any failed validation, you can not verify the asynchronous function to reduce spending. Here is my Admin UI validation logic, put out for your reference:

// the common validation logic of enhanced form components
export default {
  / /... Omitted code...
  methods: {
    validate () {
      let vm = this
      if (vm.warnings && vm.warnings.length) return false
      if(! vm.validators)return false
      // separate async and sync
      let syncStack = []
      let asyncStack = []
      vm.validators.forEach((v) = > {
        if (v.async) {
          asyncStack.push(v)
        } else {
          syncStack.push(v)
        }
      })

      // handler warnings
      let handleWarnings = (res, i, warning) = > {
        if(! res) { vm.$set(vm.localWarnings, i, warning) }else {
          vm.$delete(vm.localWarnings, i)
        }
      }

      return new Promise((resolve) = > {
        let asyncCount = asyncStack.length
        // execute sync validation first
        syncStack.forEach((v, i) = > {
          handleWarnings(v.validator(vm.value), i, v.warning)
        })
        // if sync validation passed, execute async validationg
        if(! vm.hasLocalWarnings) {if (asyncCount <= 0) { // no asyncresolve(! vm.hasLocalWarnings) }else {
            Promise.all(asyncStack.map((av, i) = > {
              return av.validator(vm.value).then(res= > {
                handleWarnings(res, i, av.warning)
              })
            })).then(results= > {
              if (results.includes(false)) resolve(! vm.hasLocalWarnings)elseresolve(! vm.hasLocalWarnings) }) } }else { // if sync validation failedresolve(! vm.hasLocalWarnings) } }) } } }Copy the code

The validation method of the form component returns a promise, and its resolve method returns a specific validation result. The benefit is that the user does not need to distinguish between synchronous and asynchronous validation.

export default {
  validateAllFormitems () {
    Promise.all([
      this.$refs.name.validate(),
      this.$refs.age.validate(),
      this.$refs.gender.validate()
    ]).then(results= > {
      if(! results.includes(false)) this.submit()
    })
  }
}
Copy the code

3.3 Packaging and Typesetting

There are two common types of form scheduling, one is label and form elements up and down, the other is left and right. The layout of your form components should be consistent, so you might create a container component for your form components to do this. Of course, your form component interfaces should also have interfaces that control typography.

In addition to the normal width, components such as text input fields and drop-down selection boxes should also be considered 100% wide when arranged up and down. You may need another full-width interface to let the user choose whether to fill the interface with the full width.

When left and right, consider the alignment of labels. It is common practice to specify the label of all form elements to a suitable width, after which the text in the label is aligned to the right. Since the components themselves are independent, you would expect the user to tell you the appropriate width of the label, so you provide a label-width interface for each form component.

You wrap these features in a container component called form-item and use it in each form component.

3.4 Date, time, and date time range

The question I want you to ask about the date, time, and date-time range components is how to divide the functionality of the components.

The common division is

  • Date picker: You can select a single point date or a range of dates
  • Time picker: You can select a single point of time or a time range
  • Date and time picker: You can select a single point of date and time or a range of date and time

However, my preferred division is

  • Date picker: Only single point dates can be selected
  • Time picker: Only a single point of time can be selected
  • Date-time range selector: Can select ranges only, but can be a date range only, a time range only, or a date + time range

The advantage of this division is that your date picker and time picker can be reused to the greatest extent possible, and the three components are much simpler to implement than the three components in the previous division. This is not easy to understand, you need to understand.

4 Provides an icon library

Most UI libraries provide an icon component. The reason is simple: no one likes that annoying font file path problem. Importing font files through a fixed component can save your users from being trapped by it.

Earlier versions of the Admin UI used a more beautiful Ionicons library with a slightly smaller variety of ICONS, and later versions have been replaced with a Font Awesome library.

It doesn’t matter which icon you choose. You can even skip using a third party icon library and just provide the smallest set of ICONS your component library needs, leaving the choice of which icon library to use up to your users — the icon component should support the use of third party ICONS.

Necessary grid system

Almost all UI libraries on the market come with a grid system that allows developers to quickly adapt their layouts. Earlier techniques, such as UI libraries such as Bootstrap, used CSS properties such as float and Width and CSS media queries to implement grid systems. Modern UI libraries mostly use Flex to implement grids.

You may want to use modern techniques to implement a grid system similar to the one in Bootstrap. However, in Bootstrap, the use of the grid relies on the style class, and it requires a parent element and several child elements to form the required structure. You may not be happy with this. Style classes can be used as props instead of fixed parent elements, which you probably don’t want users to care about. To do this, you are considering implementing the entire grid system with only one Grid component, so you need to handle the parent element at initialization, especially if the parent element involves using the display attribute, because you always need to use the Flex attribute on the parent element.

Regardless of the spacing between cells, you can use media queries and Flex properties to complete the grid system. But if spacing is involved, using CSS can be tricky. The Flex property enables the grid to be aligned horizontally, but the width of the grid itself, because it involves calculating the spacing, you might use JavaScript to do that.

The user passes in a property like props to tell the component how many grids it is on the screen, and a property like space to tell the component how close it is to the next component. You need to calculate which components are in a row, because the last grid at the end of the line has no space to the right. Otherwise it will be squeezed to the next row because it exceeds the total width of the row.

The realization of grid is not difficult, and the main demand points are the characteristics of grid itself: the number of grid under different screen width, offset distance and spacing. You might get the idea. I’m not going to go into the implementation, but you can check out the source code. To be honest, this part of the implementation is not elegant, mostly experimental. Welcome to refactor!

Unit testing and internationalization

To be honest, I’m not going to talk about this part. But you need to know that these two parts are essential if your component library is going to be open source in the future.

7 conclusion

When it comes to implementation details, this article is mostly dry. I’ve just outlined the problems you face writing a component library and outlined the solutions. Anyway, I hope that inspired you.

In the end, even wheel building has its joys. I’m going to dig a hole here. I’ll probably pick some of the more interesting components in the future and write another article to share the implementation details

In the meantime, you are welcome to fork the UI library, which is full of private Issues and Pull requests