Skip to content

Commit

Permalink
feat(recursiveDataLister): add ISO date string detection and improve …
Browse files Browse the repository at this point in the history
…date handling

- option to detect ISO Dates from strings
- converts found date strings to Date object with parseISO from date-fns package,
then passes to user supplied (or default) date to string formatter
- add doc comments to main interface
  • Loading branch information
unleashit committed Apr 17, 2024
1 parent e4b54c0 commit 4f6dc43
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 32 deletions.
3 changes: 2 additions & 1 deletion packages/recursiveDataLister/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"react": "16.x || 17.x || 18.x"
},
"dependencies": {
"@unleashit/common": "workspace:*"
"@unleashit/common": "workspace:*",
"date-fns": "^3.6.0"
},
"devDependencies": {
"@unleashit/configs": "workspace:*"
Expand Down
16 changes: 10 additions & 6 deletions packages/recursiveDataLister/src/Row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import * as React from 'react';
import {
DateFormat,
getChildTag,
handleDate,
isDate,
handleDateOrPrimitive,
isObjectNotArray,
isObjectNotDate,
isPrimitive,
Expand All @@ -17,6 +16,7 @@ interface RowProps {
branchProp: string | null;
removeRepeatedProp: boolean;
clsName: ClsName;
handleISOStringDates: boolean;
dateFormat: DateFormat;
}
const Row: React.FC<RowProps> = ({
Expand All @@ -26,6 +26,7 @@ const Row: React.FC<RowProps> = ({
branchProp,
removeRepeatedProp,
clsName,
handleISOStringDates,
dateFormat,
}): React.ReactElement => {
const Child = getChildTag(Parent);
Expand All @@ -39,7 +40,7 @@ const Row: React.FC<RowProps> = ({
<span className={clsName('value')}>{row}</span>
</Child>
) : (
Object.keys(row).map((field): React.ReactElement | null => {
Object.keys(row).map((field): React.ReactNode => {
if (isObjectNotDate(row[field])) {
return (
<Child
Expand Down Expand Up @@ -69,6 +70,7 @@ const Row: React.FC<RowProps> = ({
clsName={clsName}
branchProp={branchProp}
removeRepeatedProp={removeRepeatedProp}
handleISOStringDates={handleISOStringDates}
dateFormat={dateFormat}
nested
/>
Expand All @@ -84,9 +86,11 @@ const Row: React.FC<RowProps> = ({
<span className={clsName('label')}>{field}: </span>
)}
<span className={clsName('value')}>
{isDate(row[field])
? handleDate(row[field], dateFormat)
: row[field]}
{handleDateOrPrimitive(
row[field],
dateFormat,
handleISOStringDates,
)}
</span>
</Child>
);
Expand Down
37 changes: 28 additions & 9 deletions packages/recursiveDataLister/src/recursiveDataLister.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,37 @@ import { DateFormat, isObjectNotArray } from './utils';

// mdx_recursive_dl_props_start
export interface RecursiveDataListerProps {
/** Array or object of data to display */
data: Record<string, any> | any[];
// Top level html tag for the list, like ul, ol or div
/** Top level html tag for the list, like ul, ol or div */
tag?: keyof JSX.IntrinsicElements;
// Display in multiple ul, ol, etc. lists per parent
// Data must be an array
/**
* Display in multiple ul, ol, etc. lists per parent
* Data must be an array
*/
multiList?: boolean;
// When a branch is an array, select a property to be used as a label instead
// of the index. Note: this is a global setting, and applies to all child arrays
// If the prop isn't found, the index will be used anyway
/**
* When a branch is an array of objects, select an object property to be used as a label instead
* of the index. Note: this is a global setting, and applies to all child arrays
* If the key isn't found, the index will be used anyway
*/
arrayBranchProp?: string | null;
// By default, the arrayBranchProp will be repeated in the list
/** By default, the arrayBranchProp will be repeated in the list */
removeRepeatedProp?: boolean;
// Function to transform Date objects
/**
* Detect ISO 8601 date strings and convert into Date objects
* for the purpose of formatting with the dateFormat prop.
* Uses the parseISO function from date-fns
*/
handleISOStringDates?: boolean;
/**
* Function to customize the printed output of date objects
* The default is toString()
*/
dateFormat?: DateFormat;
/** CSS custom property overrides */
cssVars?: CSSVars<typeof varNames>;
/** CSS module to target internal styles */
cssModule?: Record<string, string>;
}
// mdx_recursive_dl_props_end
Expand All @@ -44,12 +60,15 @@ const varNames = [
'topLevelParentBorder',
] as const;

const key = (): string => Math.random().toString();

const RecursiveDataLister = ({
data,
tag = 'ul',
multiList = false,
arrayBranchProp = null,
removeRepeatedProp = false,
handleISOStringDates = false,
dateFormat = (val: Date) => val.toString(),
cssVars,
cssModule,
Expand All @@ -65,14 +84,14 @@ const RecursiveDataLister = ({
);
}

const key = (): string => Math.random().toString();
const renderRow = <T,>(rowData: T): React.ReactElement => (
<Row
key={key()}
row={rowData}
parentTag={tag}
branchProp={arrayBranchProp}
removeRepeatedProp={removeRepeatedProp}
handleISOStringDates={handleISOStringDates}
dateFormat={dateFormat}
clsName={clsName}
/>
Expand Down
43 changes: 27 additions & 16 deletions packages/recursiveDataLister/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import { parseISO } from 'date-fns/parseISO';

export const getChildTag = (tag: string): keyof JSX.IntrinsicElements => {
if (tag === 'ul' || tag === 'ol') return 'li';
return 'div';
};

export const isPrimitive = (elem: unknown): boolean =>
typeof elem === 'string' ||
typeof elem === 'number' ||
typeof elem === 'bigint' ||
typeof elem === 'boolean';

export const isObjectNotArray = (
elem: Record<string, unknown> | any[],
): boolean => typeof elem === 'object' && !Array.isArray(elem);

// const isObjectNotNull = (elem: any): boolean =>
// !!elem && typeof elem === 'object' && !Array.isArray(elem);

export const isDate = (elem: unknown): boolean =>
Object.prototype.toString.call(elem) === '[object Date]' ||
// eslint-disable-next-line no-restricted-globals
(typeof elem === 'string' && !isNaN(new Date(elem) as any));
export const isDate = (elem: unknown): elem is Date =>
Object.prototype.toString.call(elem) === '[object Date]' &&
!Number.isNaN(elem);

// const isStringDate = (elem: unknown): boolean =>
// // eslint-disable-next-line no-restricted-globals
// typeof elem === 'string' && !isNaN(new Date(elem) as any);
export const isObjectNotDate = (
elem: Record<string, unknown> | any[],
): boolean => typeof elem === 'object' && !isDate(elem);
Expand All @@ -26,10 +27,20 @@ export const isObjectNotDate = (
export type DateFormat = (val: Date) => string | number;
// mdx_recursive_dl_date_end

export const handleDate = (elem: Date, cb: DateFormat): string | number =>
cb(elem);
const ISODateFormat =
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d*)?(?:[-+]\d{2}:?\d{2}|Z)?$/;

export const isPrimitive = (elem: unknown): boolean =>
typeof elem === 'string' ||
typeof elem === 'number' ||
typeof elem === 'boolean';
export const isIsoDateString = (value: unknown): value is string =>
typeof value === 'string' && ISODateFormat.test(value);

export const handleDateOrPrimitive = (
elem: string | number | boolean | Date | null | undefined,
cb: DateFormat,
handleISOStringDates: boolean,
) => {
if (isDate(elem)) return cb(elem);
if (handleISOStringDates && isIsoDateString(elem)) {
return cb(parseISO(elem));
}
return elem;
};

0 comments on commit 4f6dc43

Please sign in to comment.