lucid-ui
Version:
A UI component library from Xandr.
360 lines (322 loc) • 10.3 kB
text/typescript
import React from 'react';
import createReactClass from 'create-react-class';
import PropTypes from 'prop-types';
import _ from 'lodash';
import {
isPlainObjectOrEsModule,
omitFunctionPropsDeep,
} from './state-management';
import { ValidationMap } from 'prop-types';
export interface StandardProps {
/** Appended to the component-specific class names set on the root element.
* Value is run through the `classnames` library. */
className?: string;
/** Any valid React children. */
children?: React.ReactNode;
/** Styles that are passed through to native control. */
style?: React.CSSProperties;
/** A "magic" prop that's always excluded from DOM elements.
* `callbackId` can be used to identify a component in a list
* without having to create extra closures. */
callbackId?: string | number;
}
// Like `T & U`, but where there are overlapping properties using the type from U only.
// From https://github.com/pelotom/type-zoo/blob/1a08384d77967ed322356005636fbb8db3c16702/types/index.d.ts#L43
export type Overwrite<T, U> = Omit<T, keyof T & keyof U> & U;
// `D`: default props (should be provided when a functional component supports
// default props)
export interface FC<P> extends React.FC<P> {
peek?: {
description: string;
notes?: {
overview: string;
intendedUse: string;
technicalRecommendations: string;
};
categories?: string[];
extend?: string;
madeFrom?: string[];
};
categories?: string[];
description?: string;
propTypes?: object;
propName?: string | string[];
_isPrivate?: boolean;
}
type TypesType<P> =
| ICreateClassComponentClass<P>
| Array<ICreateClassComponentClass<P>>
| FC<P>
| Array<FC<P>>
| { propName?: string | string[] }
| any; // TODO: figure out a type that works with inferred functional components
interface ICreateClassComponentSpec<P, S> extends React.Mixin<P, S> {
_isPrivate?: boolean;
initialState?: S;
propName?: string | string[];
propTypes?: Required<{ [key in keyof P]: any }>;
components?: {
[key: string]: ICreateClassComponentClass<{}>;
};
statics?: {
definition?: ICreateClassComponentSpec<P, S>;
[key: string]: any;
};
// TODO: improve these with a stricter type https://stackoverflow.com/a/54775885/895558
reducers?: { [K in keyof P]?: (arg0: S, ...args: any[]) => S };
selectors?: { [K in keyof P]?: (arg0: S) => any };
render?(
this: { props: P } & {
[k: string]: any;
} /* allow for extra properties on `this` to be backward compatible */
): React.ReactNode;
// TODO: could this be better handled by adding a third type parameter that
// allows the components to define what the extra class properties would
// be?
[key: string]: any;
}
export interface ICreateClassComponentClass<P>
extends React.ClassicComponentClass<P> {
propName?: string | string[];
}
/**
* creates a React component
*/
export function createClass<P, S = {}>(
spec: ICreateClassComponentSpec<P, S>
): ICreateClassComponentClass<P> {
const {
_isPrivate = false,
getDefaultProps,
statics = {},
components = {},
reducers = {},
selectors = {},
initialState = getDefaultProps &&
omitFunctionPropsDeep(getDefaultProps.apply(spec)),
propName = null,
propTypes = {},
render = (): null => null,
...restDefinition
} = spec;
const propTypeValidators: ValidationMap<any> = {
...propTypes,
..._.mapValues(
spec.components,
(componentValue, componentKey): {} =>
PropTypes.any /* Props for ${componentValue.displayName || componentKey} */
),
};
// Intentionally keep this object type inferred so it can be passed to
// `createReactClass`
const newDefinition: React.ComponentSpec<P, S> = {
getDefaultProps,
...restDefinition,
statics: {
...statics,
...components,
_isPrivate,
reducers,
selectors,
initialState,
propName,
},
propTypes: propTypeValidators,
render,
};
if (!_.isUndefined(newDefinition.statics)) {
newDefinition.statics.definition = newDefinition;
}
const newClass = createReactClass(newDefinition);
// This conditional (and breaking change) was introduced to help us move from
// legacy React classes to functional components & es6 classes which lack
// `getDefaultProps`.
if (newClass.getDefaultProps) {
newClass.defaultProps = newClass.getDefaultProps();
delete newClass.getDefaultProps;
}
return newClass;
}
/**
* Return all elements matching the specified types
*/
export function filterTypes<P>(
children: React.ReactNode,
types?: TypesType<P>
): React.ReactElement[] {
if (types === undefined) return [];
return _.filter(
React.Children.toArray(children),
(element): boolean =>
React.isValidElement(element) &&
_.includes(_.castArray<any>(types), element.type)
) as React.ReactElement[];
}
/**
* Return all elements found in props and children of the specified types
*/
export function findTypes<P extends { children?: React.ReactNode }>(
props: P,
types?: TypesType<P>
): React.ReactNode[] {
if (types === undefined) {
return [];
}
// get elements from props (using types.propName)
const elementsFromProps: React.ReactNode[] = _.reduce(
_.castArray<any>(types),
(acc: React.ReactNode[], type): React.ReactNode[] => {
return _.isNil(type.propName)
? []
: createElements(
type,
_.flatten(_.values(_.pick(props, type.propName)))
);
},
[]
);
if (props.children === undefined) {
return elementsFromProps;
}
// return elements from props and elements from children
return elementsFromProps.concat(filterTypes<P>(props.children, types));
}
// return all elements found in props and children of the specified types
// export function findTypes<P2>(
// props: { children?: React.ReactNode },
// types?: TypesType<P2>
// ): React.ReactNode[] {
// if (types === undefined) {
// return [];
// }
// // get elements from props (using types.propName)
// const elementsFromProps: React.ReactNode[] = _.reduce(
// _.castArray<any>(types),
// (acc: React.ReactNode[], type): React.ReactNode[] => {
// return _.isNil(type.propName)
// ? []
// : createElements(
// type,
// _.flatten(_.values(_.pick(props, type.propName)))
// );
// },
// []
// );
// if (props.children === undefined) {
// return elementsFromProps;
// }
// // return elements from props and elements from children
// return elementsFromProps.concat(filterTypes<P2>(props.children, types));
// }
/**
* Return all elements not matching the specified types
*/
export function rejectTypes<P>(
children: React.ReactNode,
types: TypesType<P> | Array<TypesType<P>>
): React.ReactNode[] {
types = ([] as Array<TypesType<P>>).concat(types); // coerce to Array
return _.reject(
React.Children.toArray(children),
(element): boolean =>
React.isValidElement(element) && _.includes<any>(types, element.type)
);
}
/**
* Return an array of elements (of the given type) for each of the values
*/
export function createElements<P>(
type: ICreateClassComponentClass<P>,
values: Array<React.ReactElement<P> | P> = []
): React.ReactElement[] {
return _.reduce(
values,
(
elements: Array<React.ReactElement<P>>,
typeValue
): React.ReactElement[] => {
if (React.isValidElement(typeValue) && typeValue.type === type) {
return elements.concat(typeValue);
} else if (
isPlainObjectOrEsModule(typeValue) &&
!React.isValidElement(typeValue)
) {
return elements.concat(React.createElement(type, typeValue));
} else if (_.isUndefined(typeValue)) {
return elements;
} else {
return elements.concat(React.createElement(type, null, typeValue));
}
},
[]
);
}
/**
* Return the first element found in props and children of the specificed type(s)
*/
export function getFirst<P>(
props: P,
types: TypesType<P> | undefined,
defaultValue?: React.ReactNode
): React.ReactNode | null | undefined {
return _.first(findTypes<P>(props, types)) || defaultValue;
}
/**
* Adds any speicial omitted props to an array
* @param {string[]} componentProps - an array of the component's props
* @param {boolean} targetIsDOMElement - true by default; specifies if the top-level element of the component is a plain DOM Element or a React Component Element
* @return {string[]} the array of component props plus the additional omitted keys
* */
export function addSpecialOmittedProps<P>(
componentProps: string[] = [],
targetIsDOMElement: boolean = true
): { [key: string]: any } {
// We only want to exclude the `callbackId` key when we're omitting props
// destined for a DOM element.
// We always want to add the `initialState` key
// to the list of excluded props.
const additionalOmittedKeys = targetIsDOMElement
? ['initialState', 'callbackId']
: ['initialState'];
return componentProps.concat(additionalOmittedKeys);
}
/**
* Deprecated from lucid-ui May 25, 2022 by Noah Yasskin
* Do not use this method because
* the import PropTypes from 'prop-types' stopped working as desired
* component.propTypes does not compile correctly
* and props in the passThroughs object leak through
* because they are not being omitted.
*/
// Omit props defined in propTypes of the given type and any extra keys given
// in third argument
//
// We also have a "magic" prop called `callbackId`.
// It can be used to identify a component in a list
// without having to create extra closures.
//
// It is always excluded from a DOM elements
//
// Note: The Partial<P> type is referring to the props passed into the omitProps,
// not the props defined on the component.
export function omitProps<P extends object>(
props: Partial<P>,
component: ICreateClassComponentClass<P> | undefined,
keys: string[] = [],
targetIsDOMElement: boolean = true
): { [key: string]: any } {
// We only want to exclude the `callbackId` key when we're omitting props
// destined for a DOM element.
// We always want to exclude the `initialState` key.
const additionalOmittedKeys = targetIsDOMElement
? ['initialState', 'callbackId']
: ['initialState'];
// this is to support non-createClass components that we've converted to TypeScript
if (component === undefined) {
return _.omit(props, keys.concat(additionalOmittedKeys));
}
return _.omit(
props,
_.keys(component.propTypes).concat(keys).concat(additionalOmittedKeys)
);
}