English | 简体中文
We wrote this document so that you can know the basic design ideas of UUI and how UUI components are constructed. While we are glad to see community contributions, we are not likely to choose a path that violates one or more of these principles. And, since UUI is in the early development stage, we also welcome your comments and suggestions about these principles.
Note
This document describes the basic ideas of UUI and how it implemented, not how to use it. For an introduction to UUI, check out our README instead.
UUI's open source code repository contains a lot of content, including but not limited to UI component files, Storybook document files, and unit test files. Files of different parts should be stored separately in the correct directory folder, and should not be placed randomly.
src
stores the code of UUI to implement the features. The files insrc
will be compiled and bundled into js files by rollup and released on NPM. (Storybook and other irrelevant files should not be stored insrc
)core
is a place to store core tools, which are used to build UI components of UUIutils
Common toolshooks
React Hooks toolstyles
Sass style file for componentsicons
Icon librarycomponents
Component library
stories
Where the stories of Storybook are storedtests
Unit test filesdocs
Documentation for UUI development and implementation and usage instructions
A major feature of UUI is the component style customization. In order to allow developers who use UUI to modify the style of components more conveniently and quickly, we have followed some design patterns when implementing them to efficiently integrate the customization function applies to all UUI components. These design patterns were finally implemented in the form of HOC tools.
UUI's UI components have some common functions. In order not to repeatedly implement these functions in each component, UUI has developed a set of HOC tool functions. The HOC tools mentioned here mainly refer to the UUIFunctionComponent
and UUIClassComponent
located in src/core/UUIComponent.tsx
.
All components built and implemented by HOC tools include the following features:
- All elements within a component have types, which are called Node in UUI;
- Each Node has its own name, and when it is displayed in the DOM, there will be a corresponding className;
- Because each element in the component is a kind of Node, we can locate it accurately and quickly through NodeName and NodeClassName;
- The component can pass in a
props.customize
property to modify theclassName
,style
,children
, etc. of the internal elements of this component
At the same time, the use of HOC tools is accompanied by some conventions and restrictions:
- Components can only use the Nodes provided by the HOC tool when implementing feature functions
- The name of the first Node defined must be
Root
(currently there is such a restriction, we will consider removing it in the future) - Define the feature function Props of the component, use the naming format of
XxxFeatureProps
(not mandatory) - For some Props that describe component styles, use the naming format of
XxxStylingProps
(not mandatory) - Use
STATE_xxx
andYYY_xxx
naming formats to mark component states and attributes in className (not mandatory)
Let's take the component Button
as an example:
import classNames from 'classnames';
import { omit } from 'lodash-es';
import React from 'react';
import { UUIFunctionComponent, UUIFunctionComponentProps } from '../../core';
import { LoadingSpinner } from '../Loading';
export interface ButtonStylingProps {
styling?: {
type?: 'default' | 'primary' | 'text';
};
}
export interface ButtonFeatureProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/**
* Whether the control is loading.
* @default false
*/
loading?: boolean;
}
export const Button = UUIFunctionComponent({
name: 'Button',
nodes: {
Root: 'button',
LoadingSpinner: LoadingSpinner,
Content: 'div',
},
}, (props: ButtonFeatureProps & ButtonStylingProps, nodes) => {
const { Root, LoadingSpinner, Content } = nodes
return (
<Root
role="button"
{...omit(props, 'customize', 'styling', 'className', 'style', 'loading')}
className={classNames({
...(props.styling?.type ? {
[`TYPE_${props.styling?.type}`]: true,
} : {}),
'STATE_disabled': props.disabled || props.loading,
'STATE_loading': props.loading,
})}>
{props.loading ? <LoadingSpinner animate width={14} height={14} /> : null}
{props.children ? <Content>{props.children}</Content> : null}
</Root>
)
})
export type ButtonProps = UUIFunctionComponentProps<typeof Button>
The first is that we define two Props, namely ButtonStylingProps
and ButtonFeatureProps
. These two Props are used as attributes of the feature function of the component Button
, so they are defined in the src/components/Button/Button.tsx
file instead of the src/core/UUIComponent.tsx
file.
Then we created a Button Component through the UUIFunctionComponent
HOC function tool.
The two HOC methods provided by UUI (UUIFunctionComponent
and UUIClassComponent
) are used to create function-based component
and class-based component
respectively. The first parameter options
of these two methods accepts the same type of data to define some basic information of this component:
prefix
defines the component node className prefix (optional parameter, the default isUUI
)separator
defines node className prefix, name and connection character before node name (optional parameter, default is-
)name
defines the name of the componentnodes
defines which Nodes this component contains, including the name and content of the Node. This parameter accepts an Object:key
can only be a string; value can be HTML built-in components such asdiv
,p
,span
andh1
(calledIntrinsicNode
in UUI), It can also be a UUI component (calledComponentNode
in UUI). The HOC tool constructs the truly usable IntrinsicNode and ComponentNode based on the incomingoptions.nodes
data, and provides them to the components to implement feature functions.
Similarly, we can also use UUIClassComponent
to create a Button
component:
export class Button extends UUIClassComponent({
name: 'Button',
nodes: {
Root: 'button',
LoadingSpinner: LoadingSpinner,
Content: 'div',
},
})<ButtonFeatureProps & ButtonStylingProps, {}> {
render() {
const { Root, LoadingSpinner, Content } = this.state.nodes
return (
<Root
role="button"
{...omit(props, 'customize', 'styling', 'className', 'style', 'loading')}
className={classNames({
...(props.styling?.type ? {
[`TYPE_${props.styling?.type}`]: true,
} : {}),
'STATE_disabled': props.disabled || props.loading,
'STATE_loading': props.loading,
})}>
{props.loading ? <LoadingSpinner animate width={14} height={14} /> : null}
{props.children ? <Content>{props.children}</Content> : null}
</Root>
)
}
}
After completing the implementation of Button
, you can use it normally. The general usage is similar to other UI library components:
<Button loading styling={{ type: 'primary' }}>Click me!</Button>
Since it was created by the HOC tool, this component has three more Props than other general button components, namely className
, style
and customize
:
className
For UUI components, a component contains multiple elements. This className is inserted into the Root Node'sclassName
.style
Same asclassName
, this style is merged into Root Node'sstyle
.- The type of
customize
is different in different components, depending on which Nodes the component declares and which types of Nodes are.
props.customize
has a more complex type:
export type UUIComponentCustomizeProps<
X extends { [key in string]?: keyof IntrinsicNodeT | FunctionComponentNodeT | ClassComponentNodeT },
> = {
/**
* Customize component nodes
* @default none
*/
customize?: {
[key in keyof X]?: X[key] extends keyof IntrinsicNodeT
? NodeCustomizeProps & Partial<JSX.IntrinsicElements[X[key]]>
: (
X[key] extends FunctionComponentNodeT
? NonNullable<Parameters<X[key]>[0]['customize']>
: (
X[key] extends ClassComponentNodeT
? React.ComponentProps<X[key]>['customize']
: never
)
)
};
}
The first level key of props.customize
accepts the nodeName defined by the current component. For example, Button
defines three Nodes Root
, LoadingSpinner
and Content
, so customize only supports these three string literals as key.
<Button
customize={{
Root: { /* ... */ },
LoadingSpinner: {
Root: { /* ... */ },
Icon: { /* ... */ },
},
Content: { /* ... */ },
}}
>
Customize Button
</Button>
Thanks to TypeScript's type inference, for some complex components (there are many Nodes and nested components), you can use the editor or IDE's completion feature to complete the key.
The first layer value of props.customize
can be passed into the type NodeCustomizeProps & Partial<JSX.IntrinsicElements[X[key]]>
, which is an intersection type ([Intersection Types](https://www.typescriptlang. org/docs/handbook/unions-and-intersections.html#intersection-types)), composed of a group of types; or a Customize Props (nested structure) of a UUI Component:
export interface NodeCustomizeClassNameProps {
className?: string;
overrideClassName?: string;
extendClassName?: string;
}
export interface NodeCustomizeStyleProps {
style?: React.CSSProperties;
overrideStyle?: React.CSSProperties;
extendStyle?: React.CSSProperties;
}
export interface NodeCustomizeChildrenProps {
children?: React.ReactNode;
overrideChildren?: React.ReactNode;
extendChildrenBefore?: React.ReactNode;
extendChildrenAfter?: React.ReactNode;
}
export interface NodeCustomizeDataAttributesProps {
dataAttributes?: {
[key: string]: any;
};
}
export interface NodeCustomizeAriaAttributesProps {
ariaAttributes?: UUICustomizeAriaAttributes;
}
export type NodeCustomizeProps =
& NodeCustomizeClassNameProps
& NodeCustomizeStyleProps
& NodeCustomizeChildrenProps
& NodeCustomizeDataAttributesProps
& NodeCustomizeAriaAttributesProps
& React.RefAttributes<any>
From this type of NodeCustomizeProps & Partial<JSX.IntrinsicElements[X[key]]>
, it can be seen that customize[NodeName]
can pass these properties:
overrideClassName
Override the target Node classNameextendClassName
Appended after the target Node classNameoverrideStyle
Override the target Node styleextendStyle
Merged into target Node style (same key coverage)overrideChildren
Override the target Node childrenextendChildrenBefore
Added before the target Node childrenextendChildrenAfter
Added after the target Node childrendataAttributes
Overridedata-*
attributesaria-*
Overridearia-*
attributesref
Merged into ref (implementation referencesrc/utils/mergeRefs.ts
)onXXX
supports a series of function attributes starting withon
. The HOC tool will merge the two function attributes into one function to execute, internally (props) execute first, and externally (customize) execute later
Since all components are built using HOC tools, the robustness and correctness of the code of HOC tools is very important to UUI. Therefore, we require 100% unit test coverage for the files inside the
src/uui/*
folder. For more unit testing information, please refer to TESTING.zh-CN.md.
export type UUIComponentProps<P, X extends { [key in string]?: keyof IntrinsicNodeT | FunctionComponentNodeT | ClassComponentNodeT }> = P & UUIConvenienceProps & UUIComponentCustomizeProps<X>
export type UUIFunctionComponentProps<T extends (...args: any) => any> = Parameters<T>[0]
export type UUIClassComponentProps<T extends React.JSXElementConstructor<any>> = React.ComponentProps<T>
When using the HOC tool to implement a component, you will encounter a variety of Props type definitions. When using a component, we check the type hints according to the text editor or IDE, and we can usually see these types:
XXXFeatureProps
related to the feature functionXXXStylingProps
related to the styleUUIConvenienceProps
UUI component className and style type definitionUUIComponentCustomizeProps
customize type inferred by TypeScript
If the developer wants to define some styles to encapsulate UUI components, they can write it like the following:
function StyledButton(props: ButtonFeatureProps) {
return (
<Button
customize={{
Root: {
backgroundColor: 'red',
color: 'blue',
}
}}
{...props}
>
{props.children}
</Button>
)
}
<StyledButton>Click me!</StyledButton>
Note: Currently, customize support after packaging is not supported.
UUI's components support defining the prefix
and separator
of the component in the development phase and the use phase.
For example, we can define a component during the development phase:
const Test = UUIFunctionComponent({
prefix: "XUI",
name: "Test",
separator: "+",
nodes: { Root: 'div' },
}, (props: {}, nodes) => {
// ...
return <Root></Root>
})
Then the rendered HTML looks like this:
<div className="XUI+Test+Root">
</div>
The prefix and separator in this option are mainly provided to other developers who may need to use UUI Core Utils to create their own components.
When using components, users can add props.prefix
and props.separator
into component:
function Demo() {
return <Test prefix="YUI" separator="#"></Test>
}
// output html
<div className="YUI#Test#Root">
</div>
This is mainly used when the user needs to completely customize the style. After modifying the prefix or separator, the UUI's own style is invalid (the className is all changed, and the UUI's own style is implemented according to these className)
- All components are implemented in Controlled Mode. The only exception is that form-related components also support Uncontrolled Mode.
- The style code and function code of UUI components are completely separated, and css style codes should not be written inside the components.
- All style codes should be written in a separate style file and placed in
src/styles
.
UUI currently uses the icons provided by [Feather] (https://feathericons.com/). If you need to use a new icon, select the icon from Feather first. The component library should not contain too many icon files, nor should it contain some unused icon files, and the icons should not be exported to developers.