Skip to content

Commit

Permalink
feat: Implement stateParamsControlledTable HOC
Browse files Browse the repository at this point in the history
  • Loading branch information
wms committed Sep 18, 2017
1 parent 120a5bf commit 8d62a29
Show file tree
Hide file tree
Showing 2 changed files with 309 additions and 16 deletions.
222 changes: 222 additions & 0 deletions src/hoc/stateParamsControlledTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import React from 'react';
import PropTypes from 'prop-types';
import { UIRouterReact } from '@uirouter/react';
import { TableProps, TableColumnConfig } from 'antd/lib/table/Table';
import { PaginationProps } from 'antd/lib/pagination';
import 'antd/lib/table/style';

import { stateParamsObserver } from '../hoc/stateParamsObserver';

const SORT_TO_ORDER = {
ascend: 'asc',
descend: 'desc'
};

const ORDER_TO_SORT = {
asc: 'ascend',
desc: 'descend'
};

export interface Props<T> extends TableProps<T> {
defaultPageSize: number;
}

export interface StateParamsProps {
stateParams?: {
page: number;
where: {
[key: string]: any
}
order: {
[key: string]: 'asc' | 'desc'
}
}
}

export interface Sorter {
column: TableColumnConfig<any>;
columnKey?: string;
field: string;
order: 'ascend' | 'descend';
}

export interface State {
pagination: PaginationProps
}

export interface Context {
router: UIRouterReact
}

export type StateParamsControlledTable =
<T>(Component: React.ComponentType<TableProps<T>>) =>
React.ComponentClass<Props<T>>;

export const stateParamsControlledTable: StateParamsControlledTable =
(Component) => {
class StateParamsControlledTable extends React.Component<Props<any> & StateParamsProps> {
static contextTypes = {
router: PropTypes.object
}

context: Context;

state: State = {
pagination: {
pageSize: this.props.defaultPageSize,
current: 1,
total: 0
}
}

render() {
const {
stateParams,
defaultPageSize,
columns,
...tableProps
} = this.props;

const pagination = tableProps.pagination === false ?
false : this.state.pagination;

return (
<Component
{...tableProps}
columns={this.transformColumns(columns || [])}
onChange={this.onChange}
pagination={pagination}
/>
);
}

componentWillReceiveProps(nextProps: Props<any> & StateParamsProps) {
if (!nextProps.stateParams || !nextProps.stateParams.page) {
return;
}

const updatePage = (
(!this.props.stateParams || !this.props.stateParams.page) ||
(this.props.stateParams.page !== nextProps.stateParams.page)
);

if (updatePage) {
this.setState({
pagination: {
...this.state.pagination,
current: nextProps.stateParams.page
}
})
}
}

/**
* Ant type declaration of `filters` is wrong; should be {[key: string]:
* string[]}, so uses explicit `any` instead.
*
* `filtersToWhereParam` uses correct type.
*/
onChange = (pagination: PaginationProps, filters: any, sorter: Sorter) => {
this.context.router.stateService
.go('.', {
page: pagination.current,
where: this.filtersToWhereParam(filters),
order: this.sorterToOrderParam(sorter)
});

if (this.props.onChange) {
this.props.onChange(pagination, filters, sorter);
}
}

filtersToWhereParam = (filters: { [key: string]: string | string[] }) => {
const result: { [key: string]: string | string[] } = {};

Object.keys(filters).forEach(key => {
if (!this.props.columns) {
return;
}

const column = this.props.columns.find(column => (
column.key === key || column.dataIndex === key
));

if (!column) {
return;
}

const values = filters[key];
result[key] = column.filterMultiple ? values : values[0];
});

return result;
}

sorterToOrderParam = (sorter: Sorter) => {
if (!sorter.columnKey) {
return;
}

return {
[sorter.columnKey]: SORT_TO_ORDER[sorter.order]
}
}

transformColumns = (columns: TableColumnConfig<any>[]): TableColumnConfig<any>[] =>
columns.map(column => ({
...column,
filteredValue: this.whereParamToFilteredValue(column),
sortOrder: this.orderParamToSortOrder(column)
}))

whereParamToFilteredValue = (column: TableColumnConfig<any>) => {
if (!this.props.stateParams) {
return [];
}

const { where } = this.props.stateParams;
const key = column.key || column.dataIndex;

if (!where || !key) {
return [];
}

const value = where[key];

if (!value) {
return [];
}

return value instanceof Array ? value : [value];
}

orderParamToSortOrder = (column: TableColumnConfig<any>) => {
if (!this.props.stateParams) {
return;
}

const { order } = this.props.stateParams;
const key = column.key || column.dataIndex;

if (!order || !key) {
return;
}

const direction = order[key];

if (!direction) {
return;
}

return ORDER_TO_SORT[direction] as 'ascend' | 'descend';
}
}

return stateParamsObserver(
StateParamsControlledTable,
(stateParams, props: any) => ({
...props,
stateParams
})
);
}
103 changes: 87 additions & 16 deletions stories/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { UIRouter, UISref, Transition } from '@uirouter/react';
import { Resource } from '@optics/hal-client';
import { Input, Icon } from 'antd';
import { Input, Icon, Table, LocaleProvider, Card, Button } from 'antd';
import enUS from 'antd/lib/locale-provider/en_US';

import { CredentialStore } from '../src/CredentialStore';
import { Login } from '../src/containers/Login';
import { SchemaField } from '../src/components/SchemaField';
import { buildRouter } from '../src/ui-router';
import { stateParamsObserver } from '../src/hoc/stateParamsObserver';
import { stateParamsControlledTable } from '../src/hoc/stateParamsControlledTable';

promiseFinally.shim();

Expand Down Expand Up @@ -123,32 +125,101 @@ router.stateRegistry.register({
url: '/iframe.html',
params: {
page: 1,
query: null
where: null,
order: null
}
});

const DebugComponent: React.StatelessComponent<any> = props => (
<div>
Raw Params:
const DisplayStateParams: React.StatelessComponent<any> = props => (
<Card title="State Params">
<code>{JSON.stringify(props)}</code>
</Card>
);

const Paginator: React.StatelessComponent<any> = props => (
<Button.Group>
<UISref to="main" params={{ page: props.page - 1 }}>
<a>Previous page</a>
<Button icon="left-circle">Previous page</Button>
</UISref>

<UISref to="main" params={{ page: props.page + 1 }}>
<a>Next page</a>
<Button icon="right-circle">Next page</Button>
</UISref>
</div>
</Button.Group>
);

const StateObservingDebugComponent = stateParamsObserver(DebugComponent);
const StateObservingStateParams = stateParamsObserver(DisplayStateParams);

const StateObservingPaginator = stateParamsObserver(Paginator);

const StateParamsControlledTable = stateParamsControlledTable(Table as any);

storiesOf('stateParamsObserver', module)
.add('Default', () => {
return (
<UIRouter router={router}>
<StateObservingDebugComponent />
</UIRouter>
);
});
.add('Default', () => (
<UIRouter router={router}>
<div>
<StateObservingStateParams />
<StateObservingPaginator />
</div>
</UIRouter>
));

storiesOf('stateParamsControlledTable', module)
.add('Default', () => (
<UIRouter router={router}>
<LocaleProvider locale={enUS}>
<div>
<StateObservingStateParams />
<StateParamsControlledTable
defaultPageSize={15}
columns={[{
title: 'Forename',
dataIndex: 'forename',
sorter: true
}, {
title: 'Surname',
dataIndex: 'surname',
sorter: true
}, {
title: 'Date of birth',
dataIndex: 'date_of_birth',
sorter: true
}, {
title: 'Active',
dataIndex: 'active',
filterMultiple: false,
filters: [{
text: 'Active',
value: 'true'
}, {
text: 'Inactive',
value: 'false'
}]
}]}
/>

<Button.Group>
<UISref to="main" params={{ page: 1 }}>
<Button>Page 1</Button>
</UISref>

<UISref to="main" params={{ page: 2 }}>
<Button>Page 2</Button>
</UISref>
</Button.Group>

<UISref to="main" params={{ order: { forename: 'asc' } }}>
<Button>Order by forename</Button>
</UISref>

<UISref to="main" params={{ where: { active: 'true' } }}>
<Button>Filter by active: 'true'</Button>
</UISref>

<UISref to="main" params={{ page: null, where: null, order: null }}>
<Button>Reset all</Button>
</UISref>
</div>
</LocaleProvider>
</UIRouter>
))

0 comments on commit 8d62a29

Please sign in to comment.