Note: in order to keep the page as concise as possible, IN some places I have posted the source code that best reflects the content of the article, leaving out other repetitive code, so if you try to read the source code yourself, you may find discrepancies with the code in this article. You are Naive UI. github.com/pftom/naive…
Concise abstraction
Front-end developers are now almost inseparable from UI component libraries, such as Ant Design, Material Design and, more recently, Naive UI, which are emerging from the Vue ecosystem. Component libraries offer simple, flexible and easy-to-use forms of use. The most common use of a Button on a page is as follows:
<template> <n-button>Default</n-button> <n-button type="primary">Default</n-button> <n-button type="info" dashed>Default</n-button> <n-button type="success" dashed size="small">Default</n-button> <n-button Text > Default < / n - button > < n - the button text tag = "a" href = "https://anyway.fm/news.php" type = "warning" > AnNiWei times < / n - button > <n-button disabled> </n-button> </template>Copy the code
These simple lines of code can do something interesting:
You can even switch skins with one click, as in Dark Mode:
We can also handle events, add ICONS, handle Loading, and so on. By simply giving some Props, we can have a nice, functional Button that is nothing compared to the original HTML tag…
Iceberg theory
While component libraries bring flexibility and convenience, their internal principles are not as simple as they are to use, just like the iceberg diagram above.
Take a look at the latest Vue component library, Naive UI’s CHANGELOG, to get an idea of how much time it takes to write a beginner’s component library:
As you can see, the 1.x version was released on March 21, 2020-21, and there was a long period of thinking, design and development before 1.x, which is almost two years now.
To run a Naive UI Button, you need the following files or code:
. |_____utils | |____color | | |____index.js | |____vue | | |____index.js | | |____flatten.js | | |____call.js | | |____get-slot.js | |____index.js | |____naive | | |____warn.js | | |____index.js | |____cssr | | |____create-key.js | | |____index.js |_____internal | |____loading | | |____index.js | | |____src | | | |____Loading.jsx | | | |____styles | | | | |____index.cssr.js | |____index.js | |____icon-switch-transition | | |____index.js | | |____src | | | |____IconSwitchTransition.jsx | |____fade-in-expand-transition | | |____index.js | | |____src | | | |____FadeInExpandTransition.jsx | |____wave | | |____index.js | | |____src | | | |____Wave.jsx | | | |____styles | | | | |____index.cssr.js | |____icon | | |____index.js | | |____src | | | |____Icon.jsx | | | |____styles | | | | |____index.cssr.js |_____styles | |____common | | |_____common.js | | |____light.js | | |____index.js | |____transitions | | |____fade-in-width-expand.cssr.js | | |____icon-switch.cssr.js | |____global | | |____index.cssr.js |____config-provider | |____src | | |____ConfigProvider.js |____button | |____styles | | |_____common.js | | |____light.js | | |____index.js | |____src | | |____Button.jsx | | |____styles | | | |____button.cssr.js |____assets | |____logo.png |_____mixins | |____use-style.js | |____use-theme.js | |____index.js | |____use-form-item.js | |____use-config.jsCopy the code
Behind the seemingly difficult
Although running through a deceptively simple requires a lot of work, involving dozens of file dependencies, for a component library the complexity is approximately of an order of magnitude, from a simple to a complex
Therefore, to understand the core of a front-end component library, it is necessary to understand the various preparations behind a run, which is the first column in the figure above, and the development of a component library should first focus on the design of at least one Button run solution.
The technical chain behind Button
We take Naive UI as the research object to analyze the various principles behind the implementation of in detail. There are two intuitive reasons:
- The stack is mainly Vite, Vue3 and TypeScript, which is consistent with the author’s recent stack
- Compared with other component libraries, it is at a relatively compromise level in maturity, popularity and code excellence. It is not too complex but involves relatively more knowledge, so it is more suitable for learning and studying its principles
Start from the template
The first thing you need to know about a component is its skeleton, which is also known as HTML/JSX content. See Naive UI’s Button component template:
const Button = defineComponent({ name: 'Button', props: }, setup(props) {}, render() {// n const {$slots, mergedClsPrefix, tag: Component} = this; const children = flatten(getSlot(this)); <Component ref="selfRef" class={[' ${mergedClsPrefix}-button ', `${mergedClsPrefix}-button--${this.type}-type`, { [`${mergedClsPrefix}-button--disabled`]: this.disabled, [`${mergedClsPrefix}-button--block`]: this.block, [`${mergedClsPrefix}-button--pressed`]: this.enterPressed, [`${mergedClsPrefix}-button--dashed`]: !this.text && this.dashed, [`${mergedClsPrefix}-button--color`]: this.color, [`${mergedClsPrefix}-button--ghost`]: this.ghost, // required for button group border collapse }, ]} tabindex={this.mergedFocusable ? Zero: -1} type={this.attrType} style={this.cssVars} disabled={this.disabled} onClick={this.handleClick} onBlur={this.handleBlur} onMousedown={this.handleMouseDown} onKeyup={this.handleKeyUp} onKeydown={this.handleKeyDown} > $slot.default && this.iconPlacement === "right"? (<div class={`${mergedClsPrefix}-button__content`}>{children}</div> ) : <NFadeInExpandTransition width> {{default: () => $slots.icon || this.loading ? ( <span class={`${mergedClsPrefix}-button__icon`} style={{ margin: ! $slots.default ? 0 : "", }} > <NIconSwitchTransition> {{ default: () => this.loading ? ( <NBaseLoading clsPrefix={mergedClsPrefix} key="loading" class={`${mergedClsPrefix}-icon-slot`} strokeWidth={20} /> ) : ( <div key="icon" class={`${mergedClsPrefix}-icon-slot`} role="none" > {renderSlot($slots, "icon")} </div> ), }} </NIconSwitchTransition> </span> ) : null, }} </NFadeInExpandTransition> // Section 3 {$slot.default && this.iconPlacement === "left"? (<span Class ={' ${mergedClsPrefix}-button__content '}>{children}</span>) : null} this.text ? (<NBaseWave ref="waveRef" clsPrefix={mergedClsPrefix} />) : null} ( <div aria-hidden class={`${mergedClsPrefix}-button__border`} style={this.customColorCssVars} /> ) : Null} // Part 6 {this.showborder? ( <div aria-hidden class={`${mergedClsPrefix}-button__state-border`} style={this.customColorCssVars} /> ) : null} </Component> ) } });Copy the code
As can be seen, the above mainly shows the template part of component. The component is defined based on Vue3 defineComponent, and the template is written in the form of JSX based on render method. The template part is divided into 6 parts. To comment out in code:
- It is mainly related to attributes, mainly including three attributes:
$slots
、mergedClsPrefix
、tag
, including$slots
Objects like child nodes in the Vue domain,mergedClsPrefix
Is the namespace prefix for the entire component library, which in Naive UI isn
,tag
What label should this component be presented with<button />
You can also change it to<a />
Make the button look like a link
-
Define button-related attributes:
- Among them
class
It is determined by the attributes that are passed intype
:primary
、info
、warning
、success
、error
, and what state it is in:disabled
、block
、pressed
、dashed
、color
、ghost
According to thesetype
And state are given the appropriate class name to define the CSS style to which the component belongs tabIndex
“Is in usetab
Key, whether the button will be selected,0
Means available for selection,- 1
Indicates that it is not available.type
Is represented asbutton
、submit
、reset
Button type, so that the button can be integrated into<Form />
Components to perform more complex operations, such as triggering a form submission;style
Pass in the required CSS Variables for this component, that is, CSS Variables, while insetup
Function, will passuseTheme
(more on this later) Hook to mountButton
These styles use CSS Variables extensively to customize various CSS properties of components, as well as handle global theme switching, such as Dark Modedisabled
Is to control whether the button can be operated,true
Indicates disabled, not operable,false
Indicates the default value- The rest are the associated event handlers:
click
、blur
、mouseup
、keyup
、keydown
等
- Among them
- The main decision is in
iconPlacement
为left
、right
, component child node display form, that is, icon on the left and right, child node distribution to<span />
或<div />
The form of the label is displayed when forright
Is set to<div />
For better layout and positioning
- Is icon related content,
NFadeInExpandTransition
To control the transition animation of the Icon appearing and disappearing,NIconSwitchTransition
Is the controlloading
Form Icon and other Icon switch transition animation
- When the button does not
text
In the form of node display, there should be ripples on it to process feedback. It can also be seen from the above video that there will be corresponding ripple effect to give click feedback when the button is clicked. The figure below shows a text-like form, and ripple diffusion effect cannot appear when the button is clicked
- Mainly through
<div />
To simulate component borders:border
和state-border
, the former is mainly static, default processing border color, width, etc., while the latter is processing in different states:focus
、hover
、active
、pressed
Such as theborder
style
Here’s a practical example of how both work:
.n-button .n-button__border {
border: var(--border);
}
.n-button .n-button__border, .n-button .n-button__state-border {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
border-radius: inherit;
transition: border-color .3s var(--bezier);
pointer-events: none;
}
.n-button:not(.n-button--disabled):hover .n-button__state-border {
border: var(--border-hover);
}
.n-button:not(.n-button--disabled):pressed .n-button__state-border {
border: var(--border-pressed);
}
style attribute {
--bezier: ;
--bezier-ease-out: ;
--border: 1px ;
--border-hover: 1px ;
--border-pressed: 1px ;
--border-focus: 1px ;
--border-disabled: 1px ;
}
Copy the code
As you can see, the state border handles dynamic effects such as hover, pressed, etc. while the border handles the initial default effects.
Now that you know about the main template, you may be confused about the one that comes up most often in the entire template, which is:
${mergedClsPrefix}-button
${mergedClsPrefix}-button--${``this``.type}-type
${mergedClsPrefix}-button__content
${mergedClsPrefix}-button--disabled
Why is there such a strange CSS class notation? And when assigning attributes to components:
style``={``this``.cssVars}
A typical example is:
const cssVars = {
// default type
color: "#0000",
colorHover: "#0000",
colorPressed: "#0000",
colorFocus: "#0000",
colorDisabled: "#0000",
textColor: textColor2,
}
Copy the code
Why do I need to assign a bunch of CSS Variables?
If you are puzzled by these questions and want to understand the principles behind them, then you should take a breath and keep focused on the next part of this article: the Art of organizing styles.
Style of organization art
In the component library space, most of the time has been spent on how to better and more customizable organize the overall style system.
Naive UI has an interesting feature. It does not use any pre-processing or post-processing style languages such as Less/Sass/PostCSS. Instead, it makes its own csS-in-JS solutions that are framework-based and framework-based independent with SSR features: Css-render, and give this program to design a set of plug-in system, there are two main plug-ins:
- vue3-ssr
- plugin-bem
This article focuses on CSR, so I will only focus on plugin-BEM.
Css-render the current basic use scenario for the use of plugin-BEM plug-in, write based on BEM style, easy to organize CSS in JS code, as for why here is the “class” CSS in JS solution, will be explained later.
After we have installed the corresponding package:
$ npm install --save-dev css-render @css-render/plugin-bem
Copy the code
It can be used as follows:
import CssRender from 'css-render' import bem from '@css-render/plugin-bem' const cssr = CssRender() const plugin = bem({ blockPrefix: '.ggl-'}) CSSR. Use (plugin) const {cB, cE, cM} = plugin ('container', [ cE( 'left, right', { width: '50%' } ), cM( 'dark', [ cE( 'left, right', { backgroundColor: 'black'})])]) console.log(style.render()) You can provide options style.mount(/* options */) // remove the mounted style style.unmount(/* options */)Copy the code
The effect of the above Log is as follows:
.ggl-container .ggl-container__left,
.ggl-container .ggl-container__right {
width: 50%;
}
.ggl-container.ggl-container--dark .ggl-container__left,
.ggl-container.ggl-container--dark .ggl-container__right{
background-color: black;
}
Copy the code
As you can see, the above code mainly uses cB, cE and cM functions for nested combinations of tags and styles to define canonical CSS classes and their corresponding styles. In order to further explain the function of this library and its effects in Naive UI, it is necessary to know what BEM is.
What is BEM?
B (Block), E (Element), and M (Modifier) — blocks, elements, and modifiers — are widely used naming conventions for classes used in HTML/CSS:
Block / * * / BTN {} * /. / * rely on block elements btn__price {}. Btn__text {} / * the state of the modified block modifier. * / BTN - orange {}. The BTN - big {}Copy the code
- The above middle Block, i.e
btn
Is used to represent an abstract top-level new component, that is, a block cannot contain a block, and is also treated as a parent node in a tree.btn
said - Element, “Element,” i.e
price
、text
Is a child element of the block, followed by the block, separated by a double underscore.btn__price
、.btn__text
said - Modifier, that is
orange
、big
, used to modify the state of a block, add a specific theme or style to the block, followed by a block, separated by a double hyphen, used by.btn--orange
、.btn--big
said
When the CSS form above is reflected in HTML, it should have the following structure:
<a class=" BTN BTN --big BTN --orange" href=""> <span class="btn__price">¥9.99</span> <span class="btn__text"> order </span> </a>Copy the code
This BEM style of class naming has several basic advantages:
- Can represent almost all elements and their dependencies, and the relationship is clear, semantic clear
- And even developers in other fields, such as client development, or designers, who don’t know CSS, can learn about elements, their hierarchical relationships, and states from this naming style
- With a similar naming structure, you can then change the class name slightly to get elements with different styles, such as buttons:
/* Block */
.btn {
text-decoration: none;
background-color: white;
color: #888;
border-radius: 5px;
display: inline-block;
margin: 10px;
font-size: 18px;
text-transform: uppercase;
font-weight: 600;
padding: 10px 5px;
}
/* Element */
.btn__price {
background-color: white;
color: #fff;
padding-right: 12px;
padding-left: 12px;
margin-right: -10px; /* realign button text padding */
font-weight: 600;
background-color: #333;
opacity: .4;
border-radius: 5px 0 0 5px;
}
/* Element */
.btn__text {
padding: 0 10px;
border-radius: 0 5px 5px 0;
}
/* Modifier */
.btn--big {
font-size: 28px;
padding: 10px;
font-weight: 400;
}
/* Modifier */
.btn--blue {
border-color: #0074D9;
color: white;
background-color: #0074D9;
}
/* Modifier */
.btn--orange {
border-color: #FF4136;
color: white;
background-color: #FF4136;
}
/* Modifier */
.btn--green {
border-color: #3D9970;
color: white;
background-color: #3D9970;
}
body {
font-family: "fira-sans-2", sans-serif;
background-color: #ccc;
}
Copy the code
The above modifiers orange, green, blue, big, etc., can have different effects:
How does CSS Render work?
CSS Render is essentially a CSS generator, which then provides mount and unmount apis for mounting the generated CSS string to the HTML template and removing the CSS style tag from the HTML. It implements the Sass/Less/CSS-in-JS scheme with the help of the BEM naming convention plug-in and CSS Variables, reducing the repetitive logic and package size of the overall CSS.
With BEM and CSS Render in mind, let’s review the following code:
import CssRender from 'css-render' import bem from '@css-render/plugin-bem' const cssr = CssRender() const plugin = bem({ blockPrefix: '.ggl-'}) CSSR. Use (plugin) const {cB, cE, cM} = plugin ('container', [ cE( 'left, right', { width: '50%' } ), cM( 'dark', [ cE( 'left, right', { backgroundColor: 'black'})])]) console.log(style.render()) You can provide options style.mount(/* options */) // remove the mounted style style.unmount(/* options */)Copy the code
The above code does the following:
-
Initialize the CSS Render instance, then initialize the BEM plug-in instance and prefix the overall style class with.ggl-
-
From the BEM plug-in to export the relevant cB, cE, cM methods, and then based on these three methods to comply with the concept of BEM style class arrangement, nesting, combination to form our final style class and the corresponding style
- The first is
cB
, defines a top-level block element ascontainer
- Then the block contains two child elements, respectively
cE
Represents the child element that belongs to the parent blockleft
和right
, corresponding to aboutwidth
The style; As well ascM
Modifier that modifies the parent blockdark
dark
The modifier in turn contains a child element that belongs tocE
Represents belonging to the block that this modifier modifies and contains child elementsleft
和right
, corresponding to aboutbackgroundColor
The style of the
- The first is
Now that we know about the hierarchy nesting, we can render the style above:
//. GGL - prefix, and cB('container', [cE('left, right', {width: '50%' } )]) .ggl-container .ggl-container__left, .ggl-container .ggl-container__right { width: 50%; } //. GGL - prefix, and cB('container', [cM('dark', [cE('left, right', {backgroundColor: 'black' } )])]) .ggl-container.ggl-container--dark .ggl-container--left, .ggl-container.ggl-container--dark .ggl-container__right { background-color: black; }Copy the code
Gcl-container. Gcl-container –dark is written directly with the parent block’s class, and is a modifier, not a dependency.
Naive UI style organization
Naive UI mainly follows the following logic in style organization, again using Button as an example:
- Mount CSS Variables, which contain default Variables and user-passed Variables from the definition, will
cssVars
taggedstyle
Field to mount - Mount button-related base styles, theme-related styles, and generate CSS class names
- Mount global default styles (at the end of this step, make sure global default styles are not overridden)
With the above three steps, you can define all the classes and styles associated with a Button, and support theme customization and theme reloading through CSS Variables.
Use the useTheme hook in the setup function to handle button-related style mounting and global default style mounting, and then handle CSS Variables definition and use:
const Button = defineComponent({ name: "Button", props: buttonProps, setup(props) { const themeRef = useTheme( "Button", "Button", style, buttonLight, props, mergedClsPrefixRef ); Return {// define border color related customColorCssVars: computed(() => {}), // define font, border, color, size related cssVars: computed(() => {}), }} render() {// define button-related CSS variables <Component style={this.cssvars}> // define border color unique CSS variables <div class={`${mergedClsPrefix}-button__border`} style={this. customColorCssVars} /> <div class={`${mergedClsPrefix}-button__state-border`} style={this. customColorCssVars} /> </Component> } });Copy the code
Mount buttn-related styles
UseTheme Hooks are used in the Button component setup method. UseTheme is a hook function with the following structure:
/* Global CSS Variables */ type ThemeCommonVars = typeof {primaryHover: '# 36ad6A ', errorHover: '#de576d',... } // Button's CSS Variable type ButtonThemeVars = ThemeCommonVars & {/* The type of button-related CSS Variables */} // Theme Interface Theme<N, T = {}, R = any> {// Theme name: N // Theme Some common CSS Variables common? : ThemeCommonVars // CSS Variables that depend on components, such as buttons in the Form. : R // Some personalized CSS Variables self for the theme itself? : (vars: ThemeCommonVars) => T} // Type ButtonTheme = Theme<'Button', ButtonThemeVars > interface GlobalThemeWithoutCommon { Button? : ButtonTheme Icon? Type UseThemeProps<T> = Readonly<{// Theme-related variables, such as darkTheme theme? : T | undefined / / theme of variables can be overloaded themeOverrides? : ExtractThemeOverrides<T> // builtinThemeOverrides, a variable in a built-in theme that can be overridden? Abstract: Theme<T> = T extends Theme<unknown, infer V, infer W>? { common: ThemeCommonVars self: V // CSS Variables for dependent components, such as buttons in Form, should be included with peers: W // Some CSS Variables related to dependent components, such as the Form relies on Button, corresponding to the Button // need to contain CSS Variables should be limited, The CSS in the Variables can be overloaded variable peerOverrides: ExtractMergedPeerOverrides < T >} : T useTheme < N, T, R > (resolveId: keyof GlobalThemeWithoutCommon, mountId: string, style: CNode | undefined, defaultTheme: Theme<N, T, R>, props: UseThemeProps<Theme<N, T, R>>, // n clsPrefixRef? : Ref<string | undefined> ) => ComputedRef<MergedTheme<Theme<N, T, R>>>Copy the code
As you can see, useTheme takes 6 arguments:
resolveId
Used to locate the key value in the global style topic, here is'Button'
mountId
Style mount tohead
The label,style
的id
style
The component’s CSS Render form generates style tags, style strings, which are button-related classes, and the skeleton of classes and styles. Inside are a series of CSS Variables to be useddefaultTheme
为Button
The default theme related to CSS Variablesprops
Customizable attributes passed in for users to use components to override default style variablesclsPrefixRef
Is the overall style class prefix. In Naive UI, this isn
UseTheme returns a collection of styles ComputedRef
>> combining the built-in styles, globally defined button-related styles, and user-defined styles.
Now that you know the input and output of the useTheme hook function, you can look at its body logic:
function useTheme( resolveId, mountId, style, defaultTheme, props, clsPrefixRef ) { if (style) { const mountStyle = () => { const clsPrefix = clsPrefixRef? .value; style.mount({ id: clsPrefix === undefined ? mountId : clsPrefix + mountId, head: true, props: { bPrefix: clsPrefix ? `.${clsPrefix}-` : undefined, }, }); globalStyle.mount({ id: "naive-ui/global", head: true, }); }; onBeforeMount(mountStyle); } const NConfigProvider = inject(configProviderInjectionKey, null); const mergedThemeRef = computed(() => { const { theme: { common: selfCommon, self, peers = {} } = {}, themeOverrides: selfOverrides = {}, builtinThemeOverrides: builtinOverrides = {}, } = props; const { common: selfCommonOverrides, peers: peersOverrides } = selfOverrides; const { common: globalCommon = undefined, common: globalSelfCommon = undefined, self: globalSelf = undefined, peers: globalPeers = {}, } = {}, } = NConfigProvider? .mergedThemeRef.value || {}; const { common: globalCommonOverrides = undefined, = {}, } = NConfigProvider? .mergedThemeOverridesRef.value || {}; const { common: globalSelfCommonOverrides, peers: globalPeersOverrides = {}, } = globalSelfOverrides; const mergedCommon = merge( {}, selfCommon || globalSelfCommon || globalCommon || defaultTheme.common, globalCommonOverrides, globalSelfCommonOverrides, selfCommonOverrides ); const mergedSelf = merge( // {}, executed every time, no need for empty obj (self || globalSelf || defaultTheme.self)? .(mergedCommon), builtinOverrides, globalSelfOverrides, selfOverrides ); return { common: mergedCommon, self: mergedSelf, peers: merge({}, defaultTheme.peers, globalPeers, peers), peerOverrides: merge({}, globalPeersOverrides, peersOverrides), }; }); return mergedThemeRef; }Copy the code
You can see that the useTheme body logic consists of two parts:
-
The first part is to mount button-related styles to clsPrefix + mountId, including the button-related style class skeleton, and to mount the global generic styles to naive- UI /global. This style is mounted on the onBeforeMount hook. This style is mounted in the onBeforeMount hook.
- The order
setup
It returns CSS Variables and passes the tagstyle
Registered CSS Variables - Then mount the style skeleton associated with the Button
- Then mount the global generic style skeleton to ensure that the button-related style skeleton does not overwrite the global generic style
- The order
-
The second part generates a new set of theme variables for the integration of user-defined themes and internally configured themes
- User-defined theme
props
: containstheme
、themeOverrides
、builtinThemeOverrides
- Topic of internal configuration
NConfigProvider? .mergedThemeRef.value
与NConfigProvider? .mergedThemeOverridesRef.value
- User-defined theme
The following sections will focus on the specific code and the meaning of the related variables.
The button-related styles in Part 1 are as follows:
import { c, cB, cE, cM, cNotM } from ".. /.. /.. /_utils/cssr"; import fadeInWidthExpandTransition from ".. /.. /.. /_styles/transitions/fade-in-width-expand.cssr"; import iconSwitchTransition from ".. /.. /.. /_styles/transitions/icon-switch.cssr"; export default c([ cB( "button", ` font-weight: var(--font-weight); line-height: 1; font-family: inherit; padding: Var (--padding); //.... more definitions', [// border, border-related style class skeleton cM("color", [cE("border", {borderColor: "var(--border-color)", }), cM("disabled", [ cE("border", { borderColor: "var(--border-color-disabled)", }), ]), cNotM("disabled", [ c("&:focus", [ cE("state-border", { borderColor: "var(--border-color-focus)", }), ]), c("&:hover", [ cE("state-border", { borderColor: "var(--border-color-hover)", }), ]), c("&:active", [ cE("state-border", { borderColor: "var(--border-color-pressed)", }), ]), cM("pressed", [ cE("state-border", { borderColor: "Var (- border - color - pressed)",}),),),), / / icon style class skeleton cE (" icon ", ` margin: var (-- icon - margin); margin - left: 0; height: var(--icon-size); width: var(--icon-size); max-width: var(--icon-size); font-size: var(--icon-size); position: relative; flex-shrink: 0; `, [ cB( "icon-slot", ` height: var(--icon-size); width: var(--icon-size); position: absolute; left: 0; top: 50%; transform: translateY(-50%); display: flex; `, [ iconSwitchTransition({ top: "50%", originalTransform: "TranslateY (50%),"}),]), fadeInWidthExpandTransition (),]), / / the content > child element content related style class skeleton cE (" content ", ` display: flex; align-items: center; flex-wrap: nowrap; `, [ c("~", [ cE("icon", { margin: "var(--icon-margin)", marginRight: 0,}),]),]), // More about backgroundColor, base-wave click feedback ripple, icon, content, C ("@keyframes button-wave-spread", {from: {boxShadow: "0 0 0.5px 0 var(--ripple-color)",}, to: { // don't use exact 5px since chrome will display the animation with glitches boxShadow: 0 0 0.5px 4.5px var(-- rip-color)",},}), c("@keyframes button-wave-opacity", {from: {opacity: "var(--wave-opacity)", }, to: { opacity: 0, }, }), ]);Copy the code
The CSS Render related code above will eventually produce something of the following type:
.n-button { font-weight: var(--font-weight); line-height: 1; font-family: inherit; padding: var(--padding); transition: color .3s var(--bezier), background-color .3s var(--bezier), opacity .3s var(--bezier), border-color .3s var(--bezier); } .n-button.n-button--color .n-button__border { border-color: var(--border-color); } .n-button.n-button--color.n-button--disabled .n-button__border { border-color: var(--border-color-disabled); } .n-button.n-button--color:not(.n-button--disabled):focus .n-button__state-border { border-color: var(--border-color-focus); } .n-button .n-base-wave { pointer-events: none; top: 0; right: 0; bottom: 0; left: 0; animation-iteration-count: 1; animation-duration: var(--ripple-duration); animation-timing-function: var(--bezier-ease-out), var(--bezier-ease-out); } .n-button .n-base-wave.n-base-wave--active { z-index: 1; animation-name: button-wave-spread, button-wave-opacity; } .n-button .n-button__border, .n-button .n-button__state-border { position: absolute; left: 0; top: 0; right: 0; bottom: 0; border-radius: inherit; transition: border-color .3s var(--bezier); pointer-events: none; } .n-button .n-button__icon { margin: var(--icon-margin); margin-left: 0; height: var(--icon-size); width: var(--icon-size); max-width: var(--icon-size); font-size: var(--icon-size); position: relative; flex-shrink: 0; } .n-button .n-button__icon .n-icon-slot { height: var(--icon-size); width: var(--icon-size); position: absolute; left: 0; top: 50%; transform: translateY(-50%); display: flex; } .n-button .n-button__icon.fade-in-width-expand-transition-enter-active { overflow: hidden; transition: opacity .2s cubic-bezier(.4, 0, .2, 1) .1s, max-width .2s cubic-bezier(.4, 0, .2, 1), margin-left .2s cubic-bezier(.4, 0, .2, 1), margin-right .2s cubic-bezier(.4, 0, .2, 1); } .n-button .n-button__content { display: flex; align-items: center; flex-wrap: nowrap; } .n-button .n-button__content ~ .n-button__icon { margin: var(--icon-margin); margin-right: 0; } .n-button.n-button--block { display: flex; width: 100%; } .n-button.n-button--dashed .n-button__border, .n-button.n-button--dashed .n-button__state-border { border-style: dashed ! important; } .n-button.n-button--disabled { cursor: not-allowed; opacity: var(--opacity-disabled); } @frames button-wave-spread {from {box-shadow: 0 0 0.5px 0 var(--ripple-color); } to {box-shadow: 0 0 0.5px 4.5px var(--ripple-color); } } @keyframes button-wave-opacity { from { opacity: var(--wave-opacity); } to { opacity: 0; }}Copy the code
As you can see, button-related styles handle various scenarios using BEM naming styles:
- Border and state-border for disabled, pressed, hover, active, etc
.n-button.n-button--color:not(.n-button--disabled):focus .n-button__state-border {
border-color: var(--border-color-focus);
}
Copy the code
- The pattern of ripples that appear when a button is clicked
.n-button .n-base-wave
- The style associated with icon in the button
.n-button .n-button__icon
- The style of content such as text in a button
.n-button .n-button__content
You can also see that CSS Variables are reserved for various attributes in the style, including –ripple-color for box-shadow, –icon-size for icon width and height, and –bezier for transition animation. These variables leave room for various styles and themes to be customized later.
That is, when designing the style system of a component library, the component-related style templates are defined in advance using BEM style, and then the subject-related Variables that need to be customized are changed through CSS Variables to achieve the effect of customized theme.
Mount global styles
The global-related styles are mostly simple basic configurations, with the following code:
import { c } from ".. /.. /_utils/cssr"; import commonVariables from ".. /common/_common"; export default c( "body", ` margin: 0; font-size: ${commonVariables.fontSize}; font-family: ${commonVariables.fontFamily}; line-height: ${commonVariables.lineHeight}; -webkit-text-size-adjust: 100%; `, [ c( "input", ` font-family: inherit; font-size: inherit; ` ), ] );Copy the code
Margin, font-size, font-family, line-height and other related content is necessary for the compatibility of the browser CSS code standardization, the typical example is normalize.css: Make render all elements more preferably.
CommonVariables is as follows:
export default {
fontFamily:
'v-sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
fontFamilyMono: "v-mono, SFMono-Regular, Menlo, Consolas, Courier, monospace",
fontWeight: "400",
fontWeightStrong: "500",
cubicBezierEaseInOut: "cubic-bezier(.4, 0, .2, 1)",
cubicBezierEaseOut: "cubic-bezier(0, 0, .2, 1)",
cubicBezierEaseIn: "cubic-bezier(.4, 0, 1, 1)",
borderRadius: "3px",
borderRadiusSmall: "2px",
fontSize: "14px",
fontSizeTiny: "12px",
fontSizeSmall: "14px",
fontSizeMedium: "14px",
fontSizeLarge: "15px",
fontSizeHuge: "16px",
lineHeight: "1.6",
heightTiny: "22px",
heightSmall: "28px",
heightMedium: "34px",
heightLarge: "40px",
heightHuge: "46px",
transformDebounceScale: "scale(1)",
};
Copy the code
The above generic variables are some of the most basic “raw materials” for UI component libraries to build up, and are also the best practices in the industry that are not recommended to be modified by default. For example, there are five classes of size definition, namely tiny, Small, Medium, Large, huge, font definition, code font, etc.
Define and register CSS Variables
The main code of this section is:
const NConfigProvider = inject(configProviderInjectionKey, null); const mergedThemeRef = computed(() => { const { theme: { common: selfCommon, self, peers = {} } = {}, themeOverrides: selfOverrides = {}, builtinThemeOverrides: builtinOverrides = {}, } = props; const { common: selfCommonOverrides, peers: peersOverrides } = selfOverrides; const { common: globalCommon = undefined, [resolveId]: { common: globalSelfCommon = undefined, self: globalSelf = undefined, peers: globalPeers = {}, } = {}, } = NConfigProvider? .mergedThemeRef.value || {}; const { common: globalCommonOverrides = undefined, [resolveId]: globalSelfOverrides = {}, } = NConfigProvider? .mergedThemeOverridesRef.value || {}; const { common: globalSelfCommonOverrides, peers: globalPeersOverrides = {}, } = globalSelfOverrides; const mergedCommon = merge( {}, selfCommon || globalSelfCommon || globalCommon || defaultTheme.common, globalCommonOverrides, globalSelfCommonOverrides, selfCommonOverrides ); const mergedSelf = merge( // {}, executed every time, no need for empty obj (self || globalSelf || defaultTheme.self)? .(mergedCommon), builtinOverrides, globalSelfOverrides, selfOverrides ); return { common: mergedCommon, self: mergedSelf, peers: merge({}, defaultTheme.peers, globalPeers, peers), peerOverrides: merge({}, globalPeersOverrides, peersOverrides), };Copy the code
First through the inject from get configProviderInjectionKey related content, including configProviderInjectionKey related contents are defined in the following:
provide(configProviderInjectionKey, {
mergedRtlRef,
mergedIconsRef,
mergedComponentPropsRef,
mergedBorderedRef,
mergedNamespaceRef,
mergedClsPrefixRef,
mergedLocaleRef: computed(() => {
// xxx
}),
mergedDateLocaleRef: computed(() => {
// xxx
}),
mergedHljsRef: computed(() => {
// ...
}),
mergedThemeRef,
mergedThemeOverridesRef
})
Copy the code
You can see RTL, icon, border, namespace, clsPrefix, locale, date, theme, themeOverrides, and almost any other configuration.
mergedThemeRef
: adjustable theme, such as
<template> <n-config-provider :theme="darkTheme"> <app /> </n-config-provider> </template> <script> import { darkTheme } from 'naive-ui' export default { setup() { return { darkTheme } } } </script>Copy the code
mergedThemeOverridesRef
: Adjustable topic variables, such as
const themeOverrides = {
common: {
primaryColor: '#FF0000'
},
Button: {
textColor: '#FF0000'
backgroundColor: '#FFF000',
},
Select: {
peers: {
InternalSelection: {
textColor: '#FF0000'
}
}
}
// ...
}
Copy the code
The above two variables mainly include global common related, common related uniform variables in Button, self related variables of Button custom, and peers variables involving correlation restrictions when Button is used with other components.
After returning themeRef from the useTheme hook function, themeRef is used to assemble the various styles involved in the Button, which are handled in four main directions:
fontProps
colorProps
borderProps
sizeProps
cssVars: Computed (() => {// fontProps // colorProps // borderProps // sizeProps return {// Handles animation transition functions, transparency related variables "--bezier": cubicBezierEaseInOut, "--bezier-ease-out": cubicBezierEaseOut, "--ripple-duration": Animation-opacity rippleDuration, "-- animation-disabled ": opacityDisabled, "--wave-opacity": waveOpacity, // Handle variables related to font, color, border, size... fontProps, ... colorProps, ... borderProps, ... sizeProps, }; });Copy the code
The fontProps code is as follows:
const theme = themeRef.value;
const {
self,
} = theme;
const {
rippleDuration,
opacityDisabled,
fontWeightText,
fontWeighGhost,
fontWeight,
} = self;
const { dashed, type, ghost, text, color, round, circle } = props;
// font
const fontProps = {
fontWeight: text
? fontWeightText
: ghost
? fontWeighGhost
: fontWeight,
};
Copy the code
Mainly judge the font related CSS variables and values in standard state when Button is displayed as text node and transparent background.
The related codes of colorProps are as follows
let colorProps = { "--color": "initial", "--color-hover": "initial", "--color-pressed": "initial", "--color-focus": "initial", "--color-disabled": "initial", "--ripple-color": "initial", "--text-color": "initial", "--text-color-hover": "initial", "--text-color-pressed": "initial", "--text-color-focus": "initial", "--text-color-disabled": "initial", }; if (text) { const { depth } = props; const textColor = color || (type === "default" && depth ! == undefined ? self[createKey("textColorTextDepth", String(depth))] : self[createKey("textColorText", type)]); colorProps = { "--color": "#0000", "--color-hover": "#0000", "--color-pressed": "#0000", "--color-focus": "#0000", "--color-disabled": "#0000", "--ripple-color": "#0000", "--text-color": textColor, "--text-color-hover": color ? createHoverColor(color) : self[createKey("textColorTextHover", type)], "--text-color-pressed": color ? createPressedColor(color) : self[createKey("textColorTextPressed", type)], "--text-color-focus": color ? createHoverColor(color) : self[createKey("textColorTextHover", type)], "--text-color-disabled": color || self[createKey("textColorTextDisabled", type)], }; } else if (ghost || dashed) { colorProps = { "--color": "#0000", "--color-hover": "#0000", "--color-pressed": "#0000", "--color-focus": "#0000", "--color-disabled": "#0000", "--ripple-color": color || self[createKey("rippleColor", type)], "--text-color": color || self[createKey("textColorGhost", type)], "--text-color-hover": color ? createHoverColor(color) : self[createKey("textColorGhostHover", type)], "--text-color-pressed": color ? createPressedColor(color) : self[createKey("textColorGhostPressed", type)], "--text-color-focus": color ? createHoverColor(color) : self[createKey("textColorGhostHover", type)], "--text-color-disabled": color || self[createKey("textColorGhostDisabled", type)], }; } else { colorProps = { "--color": color || self[createKey("color", type)], "--color-hover": color ? createHoverColor(color) : self[createKey("colorHover", type)], "--color-pressed": color ? createPressedColor(color) : self[createKey("colorPressed", type)], "--color-focus": color ? createHoverColor(color) : self[createKey("colorFocus", type)], "--color-disabled": color || self[createKey("colorDisabled", type)], "--ripple-color": color || self[createKey("rippleColor", type)], "--text-color": color ? self.textColorPrimary : self[createKey("textColor", type)], "--text-color-hover": color ? self.textColorHoverPrimary : self[createKey("textColorHover", type)], "--text-color-pressed": color ? self.textColorPressedPrimary : self[createKey("textColorPressed", type)], "--text-color-focus": color ? self.textColorFocusPrimary : self[createKey("textColorFocus", type)], "--text-color-disabled": color ? self.textColorDisabledPrimary : self[createKey("textColorDisabled", type)], }; }Copy the code
It is mainly processed in four forms: normal, text node, ghost background transparency and dashed line mode. Related CSS attributes and values are processed for different state standards, pressed, hover, focus and disabled
The code for borderProps is as follows:
let borderProps = {
"--border": "initial",
"--border-hover": "initial",
"--border-pressed": "initial",
"--border-focus": "initial",
"--border-disabled": "initial",
};
if (text) {
borderProps = {
"--border": "none",
"--border-hover": "none",
"--border-pressed": "none",
"--border-focus": "none",
"--border-disabled": "none",
};
} else {
borderProps = {
"--border": self[createKey("border", type)],
"--border-hover": self[createKey("borderHover", type)],
"--border-pressed": self[createKey("borderPressed", type)],
"--border-focus": self[createKey("borderFocus", type)],
"--border-disabled": self[createKey("borderDisabled", type)],
};
}
Copy the code
It mainly deals with the processing of five different state standards, pressed, hover, focus, disabled and so on under the display in text form and ordinary form.
The border props is defined as the border properties, and the border color properties are defined as customColorCssVars in setup:
customColorCssVars: computed(() => { const { color } = props; if (! color) return null; const hoverColor = createHoverColor(color); return { "--border-color": color, "--border-color-hover": hoverColor, "--border-color-pressed": createPressedColor(color), "--border-color-focus": hoverColor, "--border-color-disabled": color, }; })Copy the code
The code for sizeProps is as follows:
const sizeProps = { "--width": circle && ! text ? height : "initial", "--height": text ? "initial" : height, "--font-size": fontSize, "--padding": circle ? "initial" : text ? "initial" : round ? paddingRound : padding, "--icon-size": iconSize, "--icon-margin": iconMargin, "--border-radius": text ? "initial" : circle || round ? height : borderRadius, };Copy the code
Width, height, font-size, padding, icon, border, etc. Margin is handled when the global default style is mounted, which defaults to 0.
summary
Take the three steps above:
- Mount the button-related style class skeleton, leaving plenty of CSS Variables for custom styling
- Mount global default styles
- Assemble and define the relevant CSS Variables to populate the style class skeleton
We successfully applied CSS Render, BEM Plugin and CSS Variables to complete the overall style design of Button, which is easy to understand and customize.
The main use scenario of CSS Render is to normalize all the conditions beforehand, pre-set all the relevant CSS Variables, and then give the following
There is no shortage of necessary event handling
Naive UI provides the following types of events:
mousedown
:handleMouseDown
keyup
:handleKeyUp
keydown
:handleKeyDown
click
:handleClick
blur
:handleBlur
Take a look at the code separately:
HandleMouseDown:
const handleMouseDown = (e) => {
e.preventDefault();
if (props.disabled) {
return;
}
if (mergedFocusableRef.value) {
selfRef.value?.focus({ preventScroll: true });
}
};
Copy the code
It handles the non-response in the case of disabled, and the call to selfRef to focus and activate the corresponding style if it can focus.
HandleKeyUp:
const handleKeyUp = (e) => { switch (e.code) { case "Enter": case "NumpadEnter": if (! props.keyboard) { e.preventDefault(); return; } enterPressedRef.value = false; void nextTick(() => { if (! props.disabled) { selfRef.value? .click(); }}); }};Copy the code
The main processing Enter, NumpadEnter keys, determine whether keyboard processing support, and in the appropriate circumstances to activate button click.
HandleKeyDown:
const handleKeyDown = (e) => {
switch (e.code) {
case "Enter":
case "NumpadEnter":
if (!props.keyboard) return;
e.preventDefault();
enterPressedRef.value = true;
}
};
Copy the code
It mainly processes Enter and NumpadEnter keys, checks whether keyboard processing is supported, and updates the value of enterPressedRef if appropriate, indicating that the current keyDown is over.
HandleClick:
const handleClick = (e) => { if (! props.disabled) { const { onClick } = props; if (onClick) call(onClick, e); if (! props.text) { const { value } = waveRef; if (value) { value.play(); }}}};Copy the code
The corresponding click-handling function is called based on the state and the click-ripple effect of the play button under the non-text node.
HandleBlur:
const handleBlur = () => {
enterPressedRef.value = false;
};
Copy the code
Update the value of enterPressedRef to indicate that it is blur.
Summary and Prospect
This paper analyzes the whole process of Naive UI buttons layer by layer and source code level. It can be found that for the field of component library, most of the ideas are spent on how to Design extensible style system, from Ant Design, Element UI using Less to organize the style system. From Material Design, the style system is organized using CSS-in-JS, such as Styled components, to Naive UI, which uses CSS Render. Although there are many ways to organize style systems, But in practice, as FAR as I can see, the design style classes, corresponding styles, style extensions, and theme customization should remain broadly similar.
If you can understand the whole process of Button operation through this messy article and keep your interest in the whole source code and engineering direction of Naive UI, you can follow this logic to understand the design principles of other components, as SHOWN in the picture I showed at the beginning. The more you learn about the overall code, the easier it will feel:
Understand the source code of excellent library design, reading Daniu’s source code can help us understand the industry best practices, excellent design ideas and improve the way to write code, become more excellent developers, you and I encourage 💪!
The resources
- css-tricks.com/bem-101/
- www.smashingmagazine.com/2018/06/bem…
- Getbem.com/introductio…
- Necolas. Making. IO/normalize. C…
- www.naiveui.com/zh-CN/os-th…
- Github.com/07akioni/cs…
- www.woshipm.com/ucd/4243012…
- Getbem.com/introductio…
❤ ️ /Thank you for support /
That is all the content of this sharing. I hope it will help you
Don’t forget to share, like and bookmark your favorite things
Welcome to the public number programmer bus, from byte, shrimp, zhaoyin three brothers, share programming experience, technical dry goods and career planning, help you to avoid detours into the factory.