toggle-corp/react-store

View on GitHub
v2/View/List/index.tsx

Summary

Maintainability
A
0 mins
Test Coverage
import React, { useMemo, Fragment } from 'react';
import {
    isNotDefined,
    listToGroupList,
} from '@togglecorp/fujs';

import { OptionKey } from '../../types';

/*
# Breaking Change
- Remove modifier prop
- Remove default ListItem and GroupItem
- Add prop grouped to identify grouped / non-grouped list
*/

const emptyList: unknown[] = [];

interface BaseProps<D, P, K extends OptionKey> {
    data: D[] | undefined;
    keySelector(datum: D, index: number): K;
    renderer: React.ComponentType<P>;
    rendererClassName?: string;
    rendererParams: (key: K, datum: D, index: number, data: D[]) => P;
}

interface GroupOptions<D, GP, GK extends OptionKey> {
    groupComparator?: (a: GK, b: GK) => number;
    groupKeySelector(datum: D): GK;
    groupRenderer: React.ComponentType<GP>;
    groupRendererClassName?: string;
    groupRendererParams: (key: GK, index: number, data: D[]) => GP;
    grouped: true;
}

interface NoGroupOptions {
    grouped?: false;
}

export type ListProps<D, P, K extends OptionKey, GP, GK extends OptionKey> = (
    BaseProps<D, P, K> & (GroupOptions<D, GP, GK> | NoGroupOptions)
);

export type GroupedListProps<D, P, K extends OptionKey, GP, GK extends OptionKey> = (
    BaseProps<D, P, K> & GroupOptions<D, GP, GK>
);

function hasGroup<D, P, K extends OptionKey, GP, GK extends OptionKey>(
    props: ListProps<D, P, K, GP, GK>,
): props is (BaseProps<D, P, K> & GroupOptions<D, GP, GK>) {
    return !!(props as BaseProps<D, P, K> & GroupOptions<D, GP, GK>).grouped;
}

function GroupedList<D, P, K extends OptionKey, GP, GK extends OptionKey>(
    props: GroupedListProps<D, P, K, GP, GK>,
) {
    const {
        groupKeySelector,
        groupComparator,
        renderer: Renderer,
        groupRenderer: GroupRenderer,
        groupRendererClassName,
        groupRendererParams,
        data = emptyList as D[],
        keySelector,
        rendererParams,
        rendererClassName,
    } = props;

    const renderListItem = (datum: D, i: number) => {
        const key = keySelector(datum, i);
        const extraProps = rendererParams(key, datum, i, data);

        return (
            <Renderer
                key={String(key)}
                className={rendererClassName}
                {...extraProps}
            />
        );
    };
    const renderGroup = (groupKey: GK, index: number) => {
        const extraProps = groupRendererParams(groupKey, index, data);

        return (
            <GroupRenderer
                key={String(groupKey)}
                className={groupRendererClassName}
                {...extraProps}
            />
        );
    };

    const groups = useMemo(
        () => listToGroupList(data, groupKeySelector),
        [data, groupKeySelector],
    );

    const sortedGroupKeys = useMemo(
        () => {
            const keys = Object.keys(groups) as GK[];
            return keys.sort(groupComparator);
        },
        [groups, groupComparator],
    );

    const children: React.ReactNode[] = [];
    sortedGroupKeys.forEach((groupKey, i) => {
        children.push(renderGroup(groupKey, i));
        children.push(...groups[String(groupKey)].map(renderListItem));
    });

    return (
        <Fragment>
            {children}
        </Fragment>
    );
}

function List<D, P, K extends OptionKey, GP, GK extends OptionKey>(
    props: ListProps<D, P, K, GP, GK>,
) {
    const {
        data,
        keySelector,
        renderer: Renderer,
        rendererClassName,
        rendererParams,
    } = props;

    if (isNotDefined(data)) {
        return null;
    }

    const renderListItem = (datum: D, i: number) => {
        const key = keySelector(datum, i);
        const extraProps = rendererParams(key, datum, i, data);

        return (
            <Renderer
                key={String(key)}
                className={rendererClassName}
                {...extraProps}
            />
        );
    };

    if (!hasGroup(props)) {
        return (
            <Fragment>
                {data.map(renderListItem)}
            </Fragment>
        );
    }

    return (
        <GroupedList
            {...props}
        />
    );
}

export default List;

List.defaultProps = {
    data: [],
};