14.0.5 • Published 2 months ago

@genestack/ui v14.0.5

Weekly downloads
-
License
MIT
Repository
github
Last release
2 months ago

Genestack Frontend UI Kit

Development

To see UI kit demo run npm start.

API Design Approach

This approach is based on Material UI API Design Approach.

Composition

Use children property to populate main area/slot of the component. Component has its main area when the base abstraction it provides could be considered as a "wrapper", i.e. element, which main purpose is to decorate its content and the content of the element is something user really cares about. Good examples could be a Dialog, a TableCell or Menu.

<Menu>
    <MenuItem />
    <MenuItem />
</Menu>

<Dialog>
    <form>
        <DialogTitle />
        <DialogContent />
        <DialogContent />
        <DialogActions />
    </form>
</Dialog>

In case when component's content decorates component itself e.g. icon for a Tab or FileLink use named props with components in them.

<Tab icon={<AddIcon />}>
    <span>Add Element</span>
</Tab>

Spread props

There are few common types of elements that any component could render:

  • The root element – just the root element of the virtual DOM tree that is been returned from component's render method.
  • The target element – the element that bears main component's load, e.g. input for Autocomplete. Usually both root and target elements are the same but not always. Main purpose of the target element than is to deliver business logic.
  • Additional elements – all the rest elements playing less significant roles
// root == target
interface TabProps extends React.HTMLAttributes<HTMLDivElement> {
    icon: React.ReactNode;
    label: React.ReactNode;
}

function Tab(props: TabProps) {
    const {icon, label, className, ...targetProps} = props;
    return (
        <div {...targetProps} className={cn(className, 'gs-tab')}>
            {/* additional element `icon` */}
            <div className="gs-tab-icon">{icon}</div>
            {/* additional element `label` */}
            <div className="gs-tab-icon">{label}</div>
        </div>
    );
}

// root = div
// target = input
interface InputProps extends React.HTMLAttributes<HTMLInputElement> {}

function Input(props: InputProps) {
    // about root props see `Elements props` section
    const {className, style, rootProps, ...targetProps} = props;
    return (
        <div className={cn(rootProps.className, 'gs-input-root')} style={rootProps.style}>
            <input {...targetProps} className={cn(className, 'gs-input')} />
        </div>
    );
}

Ref props

In react components ref is a special prop which is used as a fallback in those cases where user may need access to component's imperative API. It makes sense either for stateful components (which can declare imperative API as its public methods) or for html elements. It doesn't make sense for functional components and therefore is not used for them. Though, there is a workaround with React.forwardRef provided. It could be used to allow functional components to "proxy" their ref property towards one of their children. In our API approach this method is forbidden. If you need to provide a ref pointing to component's children, use {elementName}Ref property. Set its type as React.RefObject (React of version >= 16.3 needed) so that user could make a ref with React.createRef() method:

interface Props {
    inputRef: React.RefObject<HTMLInputElement>;
}

function Input(props: Props) {
    return <input ref={props.inputRef} />;
}

Use rootRef for the root element and semantic name for the target element (ex: inputRef) if it is not the root. If you'd like to allow access to additional elements through refs, use semantic names as well (ex. labelRef or iconRef for Tab component).

interface TabProps extends React.HTMLAttributes<HTMLDivElement> {
    rootRef?: React.RefObject<HTMLDivElement>;
    iconRef?: React.RefObject<HTMLDivElement>;
    labelRef?: React.RefObject<HTMLDivElement>;
}

function Tab(props: TabProps) {
    const {rootRef, iconRef, labelRef, ...rootProps} = props;
    return (
        <div ref={rootRef}>
            <div ref={iconRef} />
            <div ref={labelRef} />
        </div>
    );
}

interface InputProps extends React.HTMLAttributes<HTMLInputElement> {
    rootRef?: React.RefObject<HTMLDivElement>;
    inputRef?: React.RefObject<HTMLInputElement>;
}

function Input(props: InputProps) {
    const {rootRef, inputRef} = props;
    return (
        <div ref={rootRef}>
            <input ref={inputRef} />
        </div>
    );
}

Elements props

In some cases you can provide access to any additional or non-target/root props to make API more flexible. In those cases you should define {componentName}Props for additional elements (ex. labelProps and iconProps for Tab). Use rootProps for the root element if it is not the target element.

interface TabProps extends React.HTMLAttributes<HTMLDivElement> {
    iconProps?: SlotProps<'div'>;
    labelProps?: SlotProps<'div'>;
}

function Tab(props: TabProps) {
    const {iconProps, labelProps, ...rootProps} = props;
    return (
        <div {...rootProps}>
            <div {...iconProps} />
            <div {...labelProps} />
        </div>
    );
}

interface InputProps extends React.HTMLAttributes<HTMLInputElement> {
    rootProps?: SlotProps<'div'>;
}

function Input(props: InputProps) {
    const {rootProps, ...targetProps} = props;
    return (
        <div {...rootProps}>
            <input {...targetProps} />
        </div>
    );
}

Do not use targetRef or targetProps because props for target element are made by spreading component's own props on it. Also targetRef is not semantic enough.

See also Material UI Approach sections:

ClassNames

For more flexible customization of components look we should provide a way to add custom className strings to nested components or amend their modifiers (e.g. disabled, active, primary etc.). Use classes property and mergeClassesProps utility function for that purpose.

/* tab.module.css */

.root {
    /* ... */
}

.icon {
    /* ... */
}

.label {
    /* ... */
}

.disabled {
    /* ... */
}
// tab.tsx
import styles from './tab.module.css';
import cn from 'classnames';
import {mergeClassesProps, WithClasses} from '../../utils/styles';

type ClassKeys = 'root' | 'icon' | 'label' | 'disabled';

interface TabProps extends WithStyles<ClassKeys> {
    disabled?: boolean;
}

function Tab(props: TabProps) {
    const {className, classes, style, disabled} = mergeClassesProps(props, styles);

    return (
        <div className={cn(className, classes.root, {[classes.disabled]: disabled})} style={style}>
            <div className={classes.icon} />
            <div className={classes.icon} />
        </div>
    );
}

The mergePropsWithClasses function returns a props object extended with classes property that is merged with styles. WithStyles interface adds className and classes properties to component's props.

After that we can use Tab component like this:

.mainTab {
    /* ... */
}

.mainTabDisabled {
    /* ... */
}
import {ClassNamesMap} from '../../utils/style';
import {Tab} from './components/tab';
import _styles from './page.module.css';

// you also can use `ClassNamesMap` from utils to type your styles
const styles = _styles as ClassNamesMap<'mainTab' | 'mainTabDisabled'>;

function MainPage() {
    return (
        <Header>
            <PageTabs>
                <Tab
                    className={styles.mainTab}
                    classes={{disabled: styles.mainTabDisabled}}
                    label="Tab With Extended Styles"
                />
            </PageTabs>
        </Header>
    );
}

onChange

We follow the approach to not change standard React API whenever possible. So we should not change standard method signatures, specifically we should not change signature of onChage method for custom inputs. Use custom property onValueChanged(value: InputType) for that purpose as it is more straightforward and simpler.

Pure components

Avoid using PureComponent in common UI components since optimization is mostly application-specific task.

Type definitions at the top

Declare component's Props and State interfaces at the top of component's file. First of all we check which component API has and only then how it works.