@genestack/ui v15.0.1
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
rendermethod. - The target element – the element that bears main component's load, e.g.
inputforAutocomplete. Usually both root and target elements are the same but not always. Main purpose of thetargetelement 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.