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
4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
testEnvironment: 'jsdom',
};
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@
"devDependencies": {
"@testing-library/jest-dom": "^4.2.3",
"@testing-library/react": "^9.3.2",
"@types/dot-object": "^1.7.0",
"@types/history": "^4.7.3",
"@types/jest": "^24.0.22",
"@types/react": "^16.9.11",
"@types/react-dom": "^16.9.3",
"@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 All @@ -57,6 +59,7 @@
"lint-staged": "^9.4.2",
"prettier": "^1.18.2",
"react": "^16.11.0",
"react-dom": "^16.12.0",
"react-redux": "^7.1.1",
"rollup": "^1.26.0",
"rollup-plugin-babel": "^4.3.3",
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}>There is no data</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" }
]
};

describe("The List component tests", () => {
test("It should render custom EmptyComponent and message", () => {
const { queryByText } = render(
<List<TestRowProps, TestRowProps, TestEmptyProps>
{...sharedProps}
rows={[]}
EmptyComponent={TestEmpty}
EmptyProps={{ message: "No Data" }}
/>
);
expect(queryByText("custom message:")).toBeInTheDocument();
});

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

test("It should render firstChild and lastChild at correct positions", () => {
const firstChild = <span data-testid="first-child" />;
const lastChild = <span data-testid="last-child" />;
const { getByTestId } = render(
<List {...sharedProps} firstChild={firstChild} lastChild={lastChild} />
);
expect(getByTestId("root").firstChild).toBe(getByTestId("first-child"));
expect(getByTestId("root").lastChild).toBe(getByTestId("last-child"));
});

test("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();
});
});
42 changes: 28 additions & 14 deletions src/components/List/index.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,57 @@
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,
EmptyProps extends BaseEmptyProps = BaseEmptyProps
> extends ListRowsProps<DataProps, RowProps> {
className?: string;
EmptyComponent?: ComponentType<V>;
EmptyProps?: V;
Container?: ElementType<{ className?: string; children: ReactNode }>;
EmptyComponent?: ComponentType<EmptyProps>;
EmptyProps?: EmptyProps;
hasEmpty?: boolean;
firstChild?: ReactNode;
lastChild?: ReactNode;
}

function BList<T, U = T, V extends EmptyProps = EmptyProps>({
function BList<
DataProps,
RowProps = DataProps,
EmptyProps extends BaseEmptyProps = BaseEmptyProps
>({
className,
Container = "div",
EmptyComponent = Empty,
EmptyProps,
hasEmpty = true,
firstChild,
lastChild,
rows,
...other
}: Props<T, U, V>) {
}: Props<DataProps, RowProps, EmptyProps>) {
return (
<div className={className}>
<Container data-testid="root" className={className}>
{firstChild}
<ListRows rows={rows} {...other} />
{hasEmpty && rows.length === 0 && (
<EmptyComponent {...(EmptyProps as V)} />
<EmptyComponent
data-testid="empty-component"
{...(EmptyProps as EmptyProps)}
/>
)}
{lastChild}
</div>
</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,
EmptyProps extends BaseEmptyProps = BaseEmptyProps
>(props: Props<DataProps, RowProps, EmptyProps>) {
return <OList {...props} />;
}
79 changes: 79 additions & 0 deletions src/components/ListRows/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
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 {
person: { code: number };
name: string;
family: string;
id: unknown;
}

function TestRow({ person: { code }, name, family }: TestRowProps) {
return (
<div>
{code} - {name} {family}
</div>
);
}

const sharedProps: ListRowsProps<TestRowProps> = {
RowComponent: TestRow,
rows: [
{ person: { code: 11 }, name: "Sara", family: "Doe", id: 11 },
{ person: { code: 12 }, name: "John", family: "Doe", id: 12 }
],
dataKey: "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", () => {
console.error = jest.fn();
try {
render(<ListRows {...sharedProps} dataKey="invalid.key" />);
} catch (e) {
expect(e.message).toBe("The `invalid.key` property does not exist");
}
});

test("It should throw an Error if the type of dataKey property is not number or string", () => {
const keys = [
{ key: 11, error: false },
{ key: "11", error: false },
{ key: true, error: true },
{ key: () => {}, error: true },
{ key: new Date(), error: true },
{ key: { test: { a: "b" } }, error: true }
];

console.error = jest.fn();

keys.forEach(record => {
let hasError = false;
try {
render(
<ListRows
{...sharedProps}
RowProps={row => ({ ...row, id: record.key })}
dataKey="id"
/>
);
} catch (e) {
hasError =
e.message === "The type of `id` property must be string or number";
}

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

test("It should render rows correctly", () => {
const { queryByText } = render(<ListRows {...sharedProps} />);
expect(queryByText("11 - Sara Doe")).toBeInTheDocument();
expect(queryByText("12 - John Doe")).toBeInTheDocument();
});
});
35 changes: 19 additions & 16 deletions src/components/ListRows/index.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +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;
export interface Props<DataProps, RowProps = DataProps> {
rows: DataProps[];
RowComponent: ComponentType<RowProps>;
RowProps?: (row: DataProps) => RowProps;
dataKey?: string;
}

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

if (typeof key !== "string" && typeof key !== "number") {
throw new Error("Invalid row key");
if (key === undefined) {
throw new Error("The `" + dataKey + "` property does not exist");
} else if (typeof key !== "string" && typeof key !== "number") {
throw new Error(
"The type of `" + dataKey + "` property must be string or number"
);
}

const rowProps = RowProps ? RowProps(row) : row;
return (
<RowComponent
key={(row[dataKey] as unknown) as Key}
{...(rowProps as U)}
/>
<RowComponent key={(key as unknown) as Key} {...(rowProps as RowProps)} />
);
})}
</>
);
}

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} />;
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as ListRows } from "./components/ListRows";
export { default as List } from "./components/List";
Loading