Skip to content

Commit

Permalink
Added ResponsiveTable and DataList controls
Browse files Browse the repository at this point in the history
  • Loading branch information
gius committed Dec 7, 2020
1 parent 13b0c80 commit 38e6dc0
Show file tree
Hide file tree
Showing 23 changed files with 270 additions and 749 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- Added `ManualPromise`, `groupBy()` to @frui.ts/helpers.
- Added `hideValidationErrors()` to @frui.ts/validation.
- Added `ResponsiveTable` and `DataList` controls to @frui.ts/dataviews.
- OpenAPI generator refactored. Supports `allOf` and `oneOf` features.
- OpenAPI generator supports Date conversion with date-fns. Use `dates: "date-fns"` in the config file.

Expand Down
721 changes: 0 additions & 721 deletions packages/cra-template/yarn.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions packages/data/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


3 changes: 2 additions & 1 deletion packages/dataviews/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"postbuild": "copyfiles src/**/*.scss styles -f"
},
"dependencies": {
"@frui.ts/data": "^0.14.0-beta.2",
"@frui.ts/data": "^999.0.0",
"@frui.ts/helpers": "^999.0.0",
"mobx-react-lite": "^2.0.6"
},
"devDependencies": {
Expand Down
56 changes: 56 additions & 0 deletions packages/dataviews/src/dataList/dataListRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { observer } from "mobx-react-lite";
import React from "react";
import { PropsWithColumns, ResponsiveColumnDefinition } from "../dataTypes";

export interface DataListRowProps<TItem, TContext> extends PropsWithColumns<TItem, TContext> {
item: TItem;
columns: ResponsiveColumnDefinition<TItem, TContext>[];
}

interface HeaderProps<TItem, TContext> {
column: ResponsiveColumnDefinition<TItem, TContext>;
context: TContext;
}

function Header<TItem, TContext>({ column, context }: HeaderProps<TItem, TContext>) {
if (column.headerFormatter) {
return <>{column.headerFormatter({ key: "list-header", column, context })}</>;
} else {
return (
<th scope="row" className={column.headerClassName}>
{column.responsiveTitleFactory?.(context) ?? column.responsiveTitle ?? column.titleFactory?.(context) ?? column.title}
</th>
);
}
}

function DataListRowImpl<TItem, TContext>({ item, columns, context }: DataListRowProps<TItem, TContext>) {
return (
<tbody>
{columns
.filter(x => x.responsiveVisible !== false)
.map((column, i) => {
const key = column.property ?? i;
const value = column.property ? item[column.property] : undefined;
const hasHeader = column.responsiveTitle !== false;

return (
<tr key={key}>
{hasHeader && <Header column={column} context={context} />}
<td colSpan={hasHeader ? 1 : 2}>
{column.cellFormatter
? column.cellFormatter({ key, value, item, column, context })
: column.valueFormatter
? column.valueFormatter({ value, item, column, context })
: value}
</td>
</tr>
);
})}
</tbody>
);
}

const DataListRow = observer(DataListRowImpl) as typeof DataListRowImpl;

export default DataListRow;
26 changes: 26 additions & 0 deletions packages/dataviews/src/dataList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { observer } from "mobx-react-lite";
import React from "react";
import { DataTableProps } from "../dataTable";
import { ResponsiveColumnDefinition } from "../dataTypes";
import DataListRow from "./dataListRow";

export interface DataListProps<TItem, TContext> extends DataTableProps<TItem, TContext> {
columns: ResponsiveColumnDefinition<TItem, TContext>[];
}

const defaultProps: Omit<Partial<DataListProps<any, any>>, "id" | "columns" | "context"> = {};

function DataListImpl<TItem, TContext>(props: DataListProps<TItem, TContext>) {
return (
<table id={props.id} className={props.className}>
{props.items?.map(item => (
<DataListRow key={String(item[props.itemKey])} item={item} columns={props.columns} context={props.context} />
))}
</table>
);
}

DataListImpl.defaultProps = defaultProps;
const DataList = observer(DataListImpl) as typeof DataListImpl;

export default DataList;
2 changes: 1 addition & 1 deletion packages/dataviews/src/dataRepeater/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function dataRepeater<
const Wrapper = props.wrapperType ?? "table";
const ItemWrapper = props.bodyWrapperType ?? "tbody";
return (
<Wrapper {...props.wrapperProps}>
<Wrapper id={props.id} {...props.wrapperProps}>
{props.displayHeader && (
<RepeaterHeader
columns={props.columns}
Expand Down
7 changes: 3 additions & 4 deletions packages/dataviews/src/dataRepeater/repeaterHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,16 @@ function repeaterHeader<TItem, TContext, TWrapper extends React.ElementType, TIt
return (
<Item
key={key}
className="sortable"
className={column.headerClassName ? `sortable ${column.headerClassName}` : "sortable"}
onClick={() => props.onColumnSort?.(column)}
{...props.itemCellProps}
style={column.headerStyle}>
{...props.itemCellProps}>
{column.titleFactory ? column.titleFactory(context) : column.title}
<span className={getSortIndicatorClass(props.pagingFilter, column.property)}></span>
</Item>
);
} else {
return (
<Item key={key} {...props.itemCellProps} style={column.headerStyle}>
<Item key={key} className={column.headerClassName} {...props.itemCellProps}>
{column.titleFactory ? column.titleFactory(context) : column.title}
</Item>
);
Expand Down
6 changes: 5 additions & 1 deletion packages/dataviews/src/dataRepeater/repeaterRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ function repeaterRow<TItem, TContext, TWrapper extends React.ElementType, TItemC
return column.cellFormatter({ key, value, item, column, context });
} else {
return (
<Item key={key} {...itemCellProps}>
<Item
key={key}
className={column.cellClassName}
{...itemCellProps}
{...column.cellProps?.({ value, item, column, context })}>
{column.valueFormatter ? column.valueFormatter({ value, item, column, context }) : value}
</Item>
);
Expand Down
4 changes: 1 addition & 3 deletions packages/dataviews/src/dataTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export interface DataTableProps<TItem, TContext>
className?: string;

headerRowClassName?: string;
headerCellClassName?: string;
}

const defaultProps: Partial<DataTableProps<any, any>> = {
Expand All @@ -19,7 +18,7 @@ const defaultProps: Partial<DataTableProps<any, any>> = {

function dataTable<TItem, TContext>(props: DataTableProps<TItem, TContext>) {
return (
<table className={props.className}>
<table id={props.id} className={props.className}>
{props.displayHeader && (
<thead>
<TableHeader
Expand All @@ -28,7 +27,6 @@ function dataTable<TItem, TContext>(props: DataTableProps<TItem, TContext>) {
pagingFilter={props.pagingFilter}
onColumnSort={props.onColumnSort}
className={props.headerRowClassName}
cellClassName={props.headerCellClassName}
/>
</thead>
)}
Expand Down
8 changes: 5 additions & 3 deletions packages/dataviews/src/dataTable/tableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export interface HeaderRowProps<TItem, TContext> extends PropsWithColumns<TItem,
onColumnSort?: (column: ColumnDefinition<TItem, TContext>) => any;

className?: string;
cellClassName?: string;
}

function getSortIndicatorClass(pagingFilter: IPagingFilter, columnName: string | number) {
Expand All @@ -28,14 +27,17 @@ function tableHeader<TItem, TContext>(props: HeaderRowProps<TItem, TContext>) {
return column.headerFormatter({ key, column, context: props.context });
} else if (props.pagingFilter && column.sortable && column.property) {
return (
<th key={key} className="sortable" style={column.headerStyle} onClick={() => props.onColumnSort?.(column)}>
<th
key={key}
className={column.headerClassName ? `sortable ${column.headerClassName}` : "sortable"}
onClick={() => props.onColumnSort?.(column)}>
{column.titleFactory ? column.titleFactory(props.context) : column.title}
<span className={getSortIndicatorClass(props.pagingFilter, column.property)}></span>
</th>
);
} else {
return (
<th key={key} className={props.cellClassName} style={column.headerStyle}>
<th key={key} className={column.headerClassName}>
{column.titleFactory ? column.titleFactory(props.context) : column.title}
</th>
);
Expand Down
2 changes: 1 addition & 1 deletion packages/dataviews/src/dataTable/tableRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ function tableRow<TItem, TContext>({ item, columns, context, rowProps }: TableRo
return column.cellFormatter({ key, value, item, column, context });
} else {
return (
<td key={key} className={column.cellClassName} style={column.cellStyle}>
<td key={key} className={column.cellClassName} {...column.cellProps?.({ value, item, column, context })}>
{column.valueFormatter ? column.valueFormatter({ value, item, column, context }) : value}
</td>
);
Expand Down
13 changes: 10 additions & 3 deletions packages/dataviews/src/dataTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,21 @@ export interface ColumnDefinition<TItem, TContext = any, TProperty extends Prope
property?: TProperty;
sortable?: boolean;

// you can use either headerStyle and pass CSS styles, or provide headerFormatter to override the whole node
headerStyle?: React.CSSProperties;
// you can use either headerClassName and pass CSS class, or provide headerFormatter to override the whole node
headerClassName?: string;
headerFormatter?: (props: ColumnRenderProps<TItem, TContext, TProperty> & KeyRenderProps) => ReactNode;

// you can either use valueFormatter to format the value displayed, or cellFormatter to override the whole node
valueFormatter?: (props: ValueRenderProps<TItem, TContext, TProperty>) => ReactNode;
cellStyle?: React.CSSProperties;
cellClassName?: string;
cellFormatter?: (props: ValueRenderProps<TItem, TContext, TProperty> & KeyRenderProps) => ReactNode;
cellProps?: (props: ValueRenderProps<TItem, TContext, TProperty>) => any;
}

export interface ResponsiveColumnDefinition<TItem, TContext> extends ColumnDefinition<TItem, TContext> {
responsiveTitle?: string | false;
responsiveTitleFactory?: (context: TContext) => string;
responsiveVisible?: boolean;
}

export interface PropsWithColumns<TItem, TContext> {
Expand All @@ -46,6 +52,7 @@ export interface PropsWithItems<TItem> {
}

export interface DataTablePropsBase<TItem, TContext> extends PropsWithColumns<TItem, TContext>, PropsWithItems<TItem> {
id?: string;
pagingFilter?: IPagingFilter;
onColumnSort?: (column: ColumnDefinition<TItem, TContext>) => any;
displayHeader?: boolean;
Expand Down
6 changes: 4 additions & 2 deletions packages/dataviews/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { DataListProps, default as DataList } from "./dataList";
export { DataRepeaterProps, default as DataRepeater } from "./dataRepeater";
export { DataTableProps, default as DataTable } from "./dataTable";
export * from "./dataTypes";
export { default as DataRepeater, DataRepeaterProps } from "./dataRepeater";
export { default as DataTable, DataTableProps } from "./dataTable";
export { default as ResponsiveTable, ResponsiveTableProps } from "./responsiveTable";
61 changes: 61 additions & 0 deletions packages/dataviews/src/responsiveTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { observer } from "mobx-react-lite";
import React, { useEffect, useState } from "react";
import DataList from "./dataList";
import DataTable, { DataTableProps } from "./dataTable";
import { ResponsiveColumnDefinition } from "./dataTypes";
import { combineClassNames } from "@frui.ts/helpers";

function getWidth() {
return document.body.clientWidth || document.documentElement.clientWidth || window.innerWidth;
}

export type ViewMode = "table" | "list";

export interface ResponsiveTableProps<TItem, TContext> extends DataTableProps<TItem, TContext> {
columns: ResponsiveColumnDefinition<TItem, TContext>[];
widthBreakpoint: number;
listModeClassName?: string;
onModeChanged?: (mode: ViewMode) => void;
}

const defaultProps: Omit<Partial<ResponsiveTableProps<any, any>>, "id" | "columns" | "context"> = {
widthBreakpoint: 576,
listModeClassName: "table-list-view",
};

function ResponsiveTableImpl<TItem, TContext>({
widthBreakpoint,
listModeClassName,
onModeChanged,
...restProps
}: ResponsiveTableProps<TItem, TContext>) {
const [mode, setMode] = useState<ViewMode>("table");

useEffect(() => {
const resizeHandler = () => {
const newMode: ViewMode = getWidth() < widthBreakpoint ? "list" : "table";
if (newMode !== mode) {
onModeChanged?.(newMode);
setMode(newMode);
}
};

resizeHandler();
window.addEventListener("resize", resizeHandler);

return () => {
window.removeEventListener("resize", resizeHandler);
};
});

if (mode === "list") {
return <DataList {...restProps} className={combineClassNames(restProps.className, listModeClassName)} />;
} else {
return <DataTable {...restProps} />;
}
}

ResponsiveTableImpl.defaultProps = defaultProps;
const ResponsiveTable = observer(ResponsiveTableImpl) as typeof ResponsiveTableImpl;

export default ResponsiveTable;
68 changes: 68 additions & 0 deletions packages/dataviews/src/responsiveTableHeaders.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { observer } from "mobx-react-lite";
import React from "react";
import { ResponsiveColumnDefinition } from ".";
import "./responsiveTableHeaders.scss";

/*
Responsive table headers HOWTO:
0. Customize the breakpoint for collpased view in both ./responsiveTableHeaders.scss and ResponsiveTableHeaders.defaultProps.
1. Wrap the <DataTable> inside <ResponsiveTableHeaders id="someID" columns={columns} context={tableContext}>
2. Use `responsiveTitle` or `responsiveTitleFactory` to customize label used in the collapsed view
3. Use `cellClassName: "responsive-hidden"` on a column you want to hide in the collapsed view
4. Use `cellClassName: "responsive-no-label"` on a column you want to be displayed without a label (and with full width)
5. Use `className="btn-responsive-block"` on a button to be displayed as block only within the collapsed view
*/

export interface ResponsiveTableHeadersProps<TItem, TContext> {
id: string;
columns: ResponsiveColumnDefinition<TItem, TContext>[];
context: TContext;

className?: string;
mediaQuery?: string;
}

const defaultProps: Omit<Partial<ResponsiveTableHeadersProps<any, any>>, "id" | "columns" | "context"> = {
mediaQuery: "@media only screen and (max-width: 576px) ",
};

function getStyleWithHeaders(id: string, headers: string[], mediaQuery?: string) {
const headerStyles = headers
.map((label, index) => `#${id} td:nth-of-type(${index + 1})::before { content: "${headers[index]}"; }`)
.join(" ");

return mediaQuery ? `${mediaQuery} { ${headerStyles} }` : headerStyles;
}

function responsiveTableHeaders<TItem, TContext>(props: React.PropsWithChildren<ResponsiveTableHeadersProps<TItem, TContext>>) {
const columnHeaders: string[] = props.columns.map(column =>
column.responsiveTitle === false
? ""
: column.responsiveTitleFactory?.(props.context) ??
column.responsiveTitle ??
column.titleFactory?.(props.context)?.toString() ??
column.title?.toString() ??
""
);

return (
<>
<style
scoped
dangerouslySetInnerHTML={{
__html: getStyleWithHeaders(props.id, columnHeaders),
}}
/>
{props.children && (
<div className={props.className} id={props.id}>
{props.children}
</div>
)}
</>
);
}

responsiveTableHeaders.defaultProps = defaultProps;
const ResponsiveTableHeaders = observer(responsiveTableHeaders) as typeof responsiveTableHeaders;

export default ResponsiveTableHeaders;
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ export default class ObjectEntityWriter {

if (type === "Date") {
if (this.config.dates === "date-fns") {
writer.writeLine(`@Transform(value => value ? new Date(value) : undefined, { toClassOnly: true })`);
writer.writeLine(`@Transform(value => (value ? new Date(value) : undefined), { toClassOnly: true })`);

const format = property.restrictions?.get(Restriction.format);
if (format === "date") {
Expand Down
Loading

0 comments on commit 38e6dc0

Please sign in to comment.