Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
"@types/react-redux": "^7.1.5",
"@typescript-eslint/eslint-plugin": "^2.6.0",
"@typescript-eslint/parser": "^2.6.0",
"dot-object": "^2.1.2",
"eslint": "^6.6.0",
"eslint-config-airbnb": "^18.0.1",
"eslint-config-prettier": "^6.5.0",
Expand Down
4 changes: 2 additions & 2 deletions src/components/Empty/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export interface Props {
className?: string;
}

function Empty({ className }: Props) {
return <div className={className}>There is no data</div>;
function Empty({ className, ...other }: Props) {
return <div className={className} { ...other}>No data is listed here</div>;
}

export default memo(Empty);
85 changes: 85 additions & 0 deletions src/components/List/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import React, { ReactNode } from "react";
import { render } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import List, { Props as ListProps } from "./index";

interface TestRowProps {
id: number;
label: string;
}

function TestRow({ id, label }: TestRowProps) {
return (
<div>
{id} - {label}
</div>
);
}

interface TestEmptyProps {
className?: string;
message: string;
}

function TestEmpty({ message }: TestEmptyProps) {
return (
<>
<span>custom message:</span> {message}
</>
);
}

const sharedProps: ListProps<TestRowProps, TestRowProps> = {
RowComponent: TestRow,
rows: [
{ id: 10, label: "A" },
{ id: 11, label: "B" },
{ id: 12, label: "C" }
],
rowKey: row => row.id,
};

describe("The List component tests", () => {
it("Should render custom EmptyComponent and message", () => {
const { queryByText } = render(
<List<TestRowProps, TestRowProps>
{...sharedProps}
rows={[]}
EmptyComponent={TestEmpty}
/>
);
expect(queryByText("custom message:")).toBeInTheDocument();
});

it("Should not render EmptyComponent if hasEmpty is false", () => {
const { queryByTestId } = render(
<List {...sharedProps} rows={[]} hasEmpty={false} />
);
expect(queryByTestId("empty-component")).toBeNull();
});

it("Should render beforeRows and afterRows at correct positions", () => {
const beforeRows = <span data-testid="before-rows" />;
const afterRows = <span data-testid="after-rows" />;
const { getByTestId } = render(
<List {...sharedProps} beforeRows={beforeRows} afterRows={afterRows} />
);
expect(getByTestId("root").firstChild).toBe(getByTestId("before-rows"));
expect(getByTestId("root").lastChild).toBe(getByTestId("after-rows"));
});

it("Should render custom container", () => {
function CustomContainer({ children }: { children: ReactNode }) {
return (
<section data-testid="custom-root">
{children}
</section>
);
}
const { getByTestId } = render(
<List {...sharedProps} Container={CustomContainer} />
);

expect(getByTestId("custom-root")).toBeInTheDocument();
});
});
48 changes: 27 additions & 21 deletions src/components/List/index.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,49 @@
import React, { ComponentType, memo, ReactNode } from "react";
import React, { ComponentType, memo, ReactNode, ElementType } from "react";
import ListRows, { Props as ListRowsProps } from "../ListRows";
import Empty, { Props as EmptyProps } from "../Empty";
import Empty, { Props as BaseEmptyProps } from "../Empty";

export interface Props<T, U = T, V extends EmptyProps = EmptyProps>
extends ListRowsProps<T, U> {
export interface Props<
DataProps,
RowProps = DataProps,
> extends ListRowsProps<DataProps, RowProps> {
className?: string;
EmptyComponent?: ComponentType<V>;
EmptyProps?: V;
Container?: ElementType<{ className?: string; children: ReactNode }>;
EmptyComponent?: ComponentType<BaseEmptyProps>;
hasEmpty?: boolean;
firstChild?: ReactNode;
lastChild?: ReactNode;
beforeRows?: ReactNode;
afterRows?: ReactNode;
}

function BList<T, U = T, V extends EmptyProps = EmptyProps>({
function BList<
DataProps,
RowProps = DataProps,
>({
className,
Container = "div",
EmptyComponent = Empty,
EmptyProps,
hasEmpty = true,
firstChild,
lastChild,
beforeRows,
afterRows,
rows,
...other
}: Props<T, U, V>) {
}: Props<DataProps, RowProps>) {
return (
<div className={className}>
{firstChild}
<Container data-testid="root" className={className}>
{beforeRows}
<ListRows rows={rows} {...other} />
{hasEmpty && rows.length === 0 && (
<EmptyComponent {...(EmptyProps as V)} />
<EmptyComponent data-testid="empty-component" />
)}
{lastChild}
</div>
{afterRows}
</Container>
);
}

const OList = memo(BList);

export default function List<T, U = T, V extends EmptyProps = EmptyProps>(
props: Props<T, U, V>
) {
export default function List<
DataProps,
RowProps = DataProps,
>(props: Props<DataProps, RowProps>) {
return <OList {...props} />;
}
20 changes: 10 additions & 10 deletions src/components/ListRows/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "@testing-library/jest-dom/extend-expect";
import React from "react";
import { render } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";
import ListRows, { Props as ListRowsProps } from "./index";

interface TestRowProps {
Expand All @@ -24,23 +24,23 @@ const sharedProps: ListRowsProps<TestRowProps> = {
{ person: { code: 11 }, name: "Sara", family: "Doe", id: 11 },
{ person: { code: 12 }, name: "John", family: "Doe", id: 12 }
],
dataKey: "person.code"
rowKey: (row) => row.person.code,
};

const originalError = console.error;
afterEach(() => (console.error = originalError));

describe("The ListRows component tests", () => {
test("It should throw an Error if the dataKey does not exist", () => {
it("Should throw an Error if the rowKey returns invalid key", () => {
console.error = jest.fn();
try {
render(<ListRows {...sharedProps} dataKey="invalid.key" />);
render(<ListRows {...sharedProps} rowKey={row => row['invalid.key']} />);
} catch (e) {
expect(e.message).toBe("The `invalid.key` property does not exist");
expect(e.message).toBe("The rowKey returns undefined value as key");
}
});

test("It should throw an Error if the type of dataKey property is not number or string", () => {
it("Should throw an Error if rowKey props does not return a number or string", () => {
const keys = [
{ key: 11, error: false },
{ key: "11", error: false },
Expand All @@ -58,20 +58,20 @@ describe("The ListRows component tests", () => {
render(
<ListRows
{...sharedProps}
RowProps={row => ({ ...row, id: record.key })}
dataKey="id"
rows={[{...sharedProps.rows[0], id: record.key}]}
rowKey={row => row.id as React.Key}
/>
);
} catch (e) {
hasError =
e.message === "The type of `id` property must be string or number";
e.message === "The rowKey must returns an string or number";
}

expect(hasError).toBe(record.error);
});
});

test("It should render rows correctly", () => {
it("Should render rows correctly", () => {
const { queryByText } = render(<ListRows {...sharedProps} />);
expect(queryByText("11 - Sara Doe")).toBeInTheDocument();
expect(queryByText("12 - John Doe")).toBeInTheDocument();
Expand Down
43 changes: 22 additions & 21 deletions src/components/ListRows/index.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,44 @@
import React, { memo, ComponentType, Key } from "react";
import dot from "dot-object";

export interface Props<T, U = T> {
rows: T[];
RowComponent: ComponentType<U>;
RowProps?: (row: T) => U;
dataKey?: string;
export interface Props<DataProps, RowProps = DataProps> {
rows: DataProps[];
RowComponent: ComponentType<RowProps>;
RowProps?: Partial<RowProps> | ((row: DataProps) => RowProps);
rowKey: (row: DataProps) => Key;
}

function BListRows<T, U = T>({
function BListRows<DataProps, RowProps = DataProps>({
rows,
RowComponent,
RowProps,
dataKey = "id"
}: Props<T, U>) {
rowKey,
}: Props<DataProps, RowProps>) {
return (
<>
{rows.map(row => {
const rowProps = RowProps ? RowProps(row) : row;
const key = dot.pick(dataKey, rowProps);

if(key===undefined) {
throw new Error("The `"+ dataKey +"` property does not exist");
const key = rowKey(row);

if (key === undefined) {
throw new Error("The rowKey returns undefined value as key");
} else if (typeof key !== "string" && typeof key !== "number") {
throw new Error("The type of `"+ dataKey +"` property must be string or number");
throw new Error(
"The rowKey must returns an string or number"
);
}


const rowProps = RowProps ? ( typeof RowProps==="function" ? RowProps(row) : {...row, ...RowProps}) : row;

return (
<RowComponent
key={(key as unknown) as Key}
{...(rowProps as U)}
/>
<RowComponent {...(rowProps as RowProps)} key={key} />
);
})}
</>
);
}

const OListRows = memo(BListRows);
export default function ListRows<T, U = T>(props: Props<T, U>) {
export default function ListRows<DataProps, RowProps = DataProps>(
props: Props<DataProps, RowProps>
) {
return <OListRows {...props} />;
}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default } from "./components/ListRows";
export { default as ListRows } from "./components/ListRows";
export { default as List } from "./components/List";