diff --git a/README.md b/README.md index 52d78ca5..69e7a1a7 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ const Component = () => () ### Customizable data type ```tsx -import JsonViewer from '@textea/json-viewer' +import { JsonViewer, createDataType } from '@textea/json-viewer' const object = { // what if I want to inspect a image? @@ -56,14 +56,14 @@ const Component = () => ( value={object} // just define it valueTypes={[ - { - is: (value) => - typeof value === 'string' && + createDataType( + (value) => typeof value === 'string' && value.startsWith('https://i.imgur.com'), - Component: (props) => { - return {props.value}/; - }, - }, + (props) => { + return {props.value}/ + } + ) ]} /> ) @@ -128,10 +128,10 @@ const Component = () => ( - [X] 100% TypeScript - [X] Customizable - - [X] `keyRenderer` for customize key renderer - - [X] `valueTypes` for customize any value types you want - - [X] `light | dark | base16` Theme support - - [ ] custom metadata + - [X] `keyRenderer` for customize key renderer + - [X] `valueTypes` for customize any value types you want + - [X] `light | dark | base16` Theme support + - [ ] custom metadata - [X] Support `Next.js` SSR - [X] `onChange` props allow users to edit value - [X] Inspect `object`, `Array`, primitive type, even `Map` and `Set` by default. diff --git a/examples/basic/pages/index.tsx b/examples/basic/pages/index.tsx index 5450d538..99880a1e 100644 --- a/examples/basic/pages/index.tsx +++ b/examples/basic/pages/index.tsx @@ -8,6 +8,7 @@ import { } from '@mui/material' import { applyValue, + createDataType, JsonViewer, JsonViewerKeyRenderer, JsonViewerOnChange, @@ -179,14 +180,14 @@ const IndexPage: React.FC = () => { groupArraysAfterLength={groupArraysAfterLength} keyRenderer={KeyRenderer} valueTypes={[ - { - is: (value): value is string => typeof value === 'string' && + createDataType( + (value) => typeof value === 'string' && value.startsWith('https://i.imgur.com'), - Component: (props) => { + (props) => { return {props.value}/ } - } + ) ]} onChange={ useCallback( diff --git a/package.json b/package.json index 0c0b2e92..55c93dc5 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "@types/web": "^0.0.73", "@vitest/coverage-c8": "^0.23.4", "@vitest/ui": "^0.23.4", + "expect-type": "^0.14.2", "husky": "^8.0.1", "jsdom": "^20.0.0", "lint-staged": "^13.0.3", diff --git a/src/index.tsx b/src/index.tsx index 0e136a9e..4425bb86 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -16,9 +16,9 @@ import { import { registerType } from './stores/typeRegistry' import { darkColorspace, lightColorspace } from './theme/base16' import type { JsonViewerProps } from './type' -import { applyValue } from './utils' +import { applyValue, createDataType, isCycleReference } from './utils' -export { applyValue } +export { applyValue, createDataType, isCycleReference } /** * @internal diff --git a/src/type.ts b/src/type.ts index e5131ef2..448bfb22 100644 --- a/src/type.ts +++ b/src/type.ts @@ -22,6 +22,9 @@ export type EditorProps = { } export type DataType = { + /** + * Whether the value belongs to the data type + */ is: (value: unknown) => value is ValueType Component: React.ComponentType> Editor?: React.ComponentType> diff --git a/src/utils/index.ts b/src/utils/index.ts index 3f3f0c3e..2dc949bf 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,7 @@ +import type React from 'react' + +import type { DataItemProps, EditorProps } from '../type' + export const applyValue = (obj: any, path: (string | number)[], value: any) => { if (typeof obj !== 'object' || obj === null) { return value @@ -16,7 +20,70 @@ export const applyValue = (obj: any, path: (string | number)[], value: any) => { return obj } -export const isCycleReference = (root: any, path: (string | number)[], value: unknown): false | string => { +// case 1: you only render with a single component +export function createDataType ( + is: (value: unknown) => boolean, + Component: React.ComponentType> +): { + is: (value: unknown) => value is ValueType + Component: React.ComponentType> +} +// case 2: you only render with a single component with editor +export function createDataType ( + is: (value: unknown) => boolean, + Component: React.ComponentType>, + Editor: React.ComponentType> +): { + is: (value: unknown) => value is ValueType + Component: React.ComponentType> + Editor: React.ComponentType> +} +// case 3: you only render with a component with pre and post. +export function createDataType ( + is: (value: unknown) => boolean, + Component: React.ComponentType>, + Editor: undefined, + PreComponent: React.ComponentType>, + PostComponent: React.ComponentType> +): { + is: (value: unknown) => value is ValueType + Component: React.ComponentType> + PreComponent: React.ComponentType> + PostComponent: React.ComponentType> +} +// case 4: need all of these +export function createDataType ( + is: (value: unknown) => boolean, + Component: React.ComponentType>, + Editor: React.ComponentType>, + PreComponent: React.ComponentType>, + PostComponent: React.ComponentType> +): { + is: (value: unknown) => value is ValueType + Component: React.ComponentType> + Editor: React.ComponentType> + PreComponent: React.ComponentType> + PostComponent: React.ComponentType> +} + +export function createDataType ( + is: (value: unknown) => boolean, + Component: React.ComponentType>, + Editor?: React.ComponentType> | undefined, + PreComponent?: React.ComponentType> | undefined, + PostComponent?: React.ComponentType> | undefined +): any { + return { + is, + Component, + Editor, + PreComponent, + PostComponent + } +} + +export const isCycleReference = ( + root: any, path: (string | number)[], value: unknown): false | string => { if (root === null || value === null) { return false } diff --git a/tests/index.test.tsx b/tests/index.test.tsx index a0ae02fe..c4e211c5 100644 --- a/tests/index.test.tsx +++ b/tests/index.test.tsx @@ -1,8 +1,9 @@ import { render, screen } from '@testing-library/react' +import { expectTypeOf } from 'expect-type' import React from 'react' import { describe, expect, it } from 'vitest' -import { JsonViewer } from '../src' +import { createDataType, JsonViewer } from '../src' function aPlusB (a: number, b: number) { return a + b @@ -141,7 +142,10 @@ describe('render with props', () => { }) it('render with objectSortKeys', () => { - const selection = [true, false, (a: string, b: string) => a.localeCompare(b)] + const selection = [ + true, + false, + (a: string, b: string) => a.localeCompare(b)] selection.forEach(objectSortKeys => { render() }) @@ -149,6 +153,27 @@ describe('render with props', () => { it('render with rootName false', async () => { render() - expect((await screen.findByTestId('data-key-pair')).innerText).toEqual(undefined) + expect((await screen.findByTestId('data-key-pair')).innerText) + .toEqual(undefined) + }) + + it('render with dataTypes', async () => { + render() + render( typeof value === 'string', + Component: (props) => { + expectTypeOf(props.value).toMatchTypeOf() + return null + } + }, + createDataType( + (value) => typeof value === 'string', + (props) => { + expectTypeOf(props.value).toMatchTypeOf() + return null + } + ) + ]}/>) }) }) diff --git a/tests/util.test.tsx b/tests/util.test.tsx new file mode 100644 index 00000000..e40e48b1 --- /dev/null +++ b/tests/util.test.tsx @@ -0,0 +1,130 @@ +import { expectTypeOf } from 'expect-type' +import type React from 'react' +import { describe, expect, test } from 'vitest' + +import type { DataItemProps } from '../src' +import { createDataType } from '../src/utils' + +describe('function createDataType', () => { + test('case 1', () => { + const dataType = createDataType( + (value) => { + expectTypeOf(value).toBeUnknown() + return true + }, + (props) => { + expectTypeOf(props.value).toBeString() + return null + } + ) + expectTypeOf(dataType).toEqualTypeOf<{ + is:(value: unknown) => value is string + Component: React.ComponentType> + }>() + const someValue: unknown = null + expectTypeOf(dataType.is).returns.toBeBoolean() + if (dataType.is(someValue)) { + expectTypeOf(someValue).toMatchTypeOf() + } else { + expectTypeOf(someValue).not.toMatchTypeOf() + expectTypeOf(someValue).toMatchTypeOf() + } + expect(dataType.is).toBeTypeOf('function') + expect(dataType.Component).toBeTypeOf('function') + }) + test('case 2', () => { + const dataType = createDataType( + (value) => { + expectTypeOf(value).toBeUnknown() + return true + }, + (props) => { + expectTypeOf(props.value).toBeString() + return null + }, + (props) => { + expectTypeOf(props.value).toBeString() + return null + } + ) + expectTypeOf(dataType).toEqualTypeOf<{ + is:(value: unknown) => value is string + Component: React.ComponentType> + Editor: React.ComponentType> + }>() + expectTypeOf(dataType.is).returns.toBeBoolean() + expect(dataType.is).toBeTypeOf('function') + expect(dataType.Component).toBeTypeOf('function') + expect(dataType.Editor).toBeTypeOf('function') + }) + test('case 3', () => { + const dataType = createDataType( + (value) => { + expectTypeOf(value).toBeUnknown() + return true + }, + (props) => { + expectTypeOf(props.value).toBeString() + return null + }, + undefined, + (props) => { + expectTypeOf(props.value).toBeString() + return null + }, + (props) => { + expectTypeOf(props.value).toBeString() + return null + } + ) + expectTypeOf(dataType).toEqualTypeOf<{ + is:(value: unknown) => value is string + Component: React.ComponentType> + PreComponent: React.ComponentType> + PostComponent: React.ComponentType> + }>() + expectTypeOf(dataType.is).returns.toBeBoolean() + expect(dataType.is).toBeTypeOf('function') + expect(dataType.Component).toBeTypeOf('function') + expect(dataType.PreComponent).toBeTypeOf('function') + expect(dataType.PostComponent).toBeTypeOf('function') + }) + + test('case 4', () => { + const dataType = createDataType( + (value) => { + expectTypeOf(value).toBeUnknown() + return true + }, + (props) => { + expectTypeOf(props.value).toBeString() + return null + }, + (props) => { + expectTypeOf(props.value).toBeString() + return null + }, + (props) => { + expectTypeOf(props.value).toBeString() + return null + }, + (props) => { + expectTypeOf(props.value).toBeString() + return null + } + ) + expectTypeOf(dataType).toEqualTypeOf<{ + is:(value: unknown) => value is string + Component: React.ComponentType> + Editor: React.ComponentType> + PreComponent: React.ComponentType> + PostComponent: React.ComponentType> + }>() + expectTypeOf(dataType.is).returns.toBeBoolean() + expect(dataType.is).toBeTypeOf('function') + expect(dataType.Component).toBeTypeOf('function') + expect(dataType.Editor).toBeTypeOf('function') + expect(dataType.PreComponent).toBeTypeOf('function') + expect(dataType.PostComponent).toBeTypeOf('function') + }) +}) diff --git a/yarn.lock b/yarn.lock index 0c5ce5fc..b056567f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1728,6 +1728,7 @@ __metadata: "@vitest/coverage-c8": ^0.23.4 "@vitest/ui": ^0.23.4 copy-to-clipboard: ^3.3.2 + expect-type: ^0.14.2 group-items: ^2.2.0 husky: ^8.0.1 jsdom: ^20.0.0 @@ -4064,6 +4065,13 @@ __metadata: languageName: node linkType: hard +"expect-type@npm:^0.14.2": + version: 0.14.2 + resolution: "expect-type@npm:0.14.2" + checksum: b8dba1f67d6562d203359d5f5e7ee9c2066c091a7bf3c8744858cbe801fb6becab760961fa206ad5bfd2c532b2a01f835f8f1a4f86f2ad8e6881c0930b48aca5 + languageName: node + linkType: hard + "extend@npm:^3.0.2": version: 3.0.2 resolution: "extend@npm:3.0.2"