diff --git a/docs/3.x upgrade guide.md b/docs/3.x upgrade guide.md index ad362ad384..8b08eed3ca 100644 --- a/docs/3.x upgrade guide.md +++ b/docs/3.x upgrade guide.md @@ -15,3 +15,25 @@ For a slightly more elaborate setup, [@babel/preset-env](https://babeljs.io/docs From `@babel/preset-env`'s docs > We leverage [`browserslist`, `compat-table`, and `electron-to-chromium`] to maintain mappings of which version of our supported target environments gained support of a JavaScript syntax or browser feature, as well as a mapping of those syntaxes and features to Babel transform plugins and core-js polyfills. + +### Typescript usage of withTheme + +The typings for `withTheme` have been updated to more accurately reflect its nature as a factory function that returns a `Form` component and to properly type the ref forwarded to this component. See more about [the changes here](https://github.com/rjsf-team/react-jsonschema-form/pull/2279) + +If you're currently using `withTheme`, the typing for the formData was always `any`, as the generated Form component was not able to accept a generic argument. + +In v3.x these typings will properly infer formData if no type is given, or validate it if one is, similar to how the exported Form from core would work. + +You can maintain the old behavior of `withTheme`, if necessary, by changing usage of your ThemedForm to override the generic for formData with `any`. + +```tsx +const schema = { ... } +const ThemedForm = withTheme({ ... }) +const formData = { ... } + +// change this + + +// to this to override the formData type with any and maintain the v2 behavior + schema={schema} formData={formData} /> +``` diff --git a/packages/bootstrap-4/src/Form/Form.tsx b/packages/bootstrap-4/src/Form/Form.tsx index c24d85da93..fc9819bb1b 100644 --- a/packages/bootstrap-4/src/Form/Form.tsx +++ b/packages/bootstrap-4/src/Form/Form.tsx @@ -1,10 +1,7 @@ -import { withTheme, FormProps } from "@rjsf/core"; - +import CoreForm from "@rjsf/core"; +import { withTheme } from "@rjsf/core"; import Theme from "../Theme"; -import { StatelessComponent } from "react"; -const Form: - | React.ComponentClass> - | StatelessComponent> = withTheme(Theme); +const Form: typeof CoreForm = withTheme(Theme); export default Form; diff --git a/packages/bootstrap-4/test/Form.test.tsx b/packages/bootstrap-4/test/Form.test.tsx index 7fb76f4841..05e865e0bb 100644 --- a/packages/bootstrap-4/test/Form.test.tsx +++ b/packages/bootstrap-4/test/Form.test.tsx @@ -1,8 +1,8 @@ -import React from "react"; -import Form from "../src/index"; +import CoreForm, { UiSchema } from "@rjsf/core"; import { JSONSchema7 } from "json-schema"; +import React from "react"; import renderer from "react-test-renderer"; -import { UiSchema } from "@rjsf/core"; +import Form from "../src/index"; describe("single fields", () => { describe("string field", () => { diff --git a/packages/core/index.d.ts b/packages/core/index.d.ts index 821ffb0e81..784b93c87b 100644 --- a/packages/core/index.d.ts +++ b/packages/core/index.d.ts @@ -61,6 +61,7 @@ declare module '@rjsf/core' { onChange: (formData: T, newErrorSchema: ErrorSchema) => void; onBlur: (id: string, value: boolean | number | string | null) => void; submit: () => void; + formElement: HTMLFormElement | null; } export type UiSchema = { @@ -274,7 +275,7 @@ declare module '@rjsf/core' { export function withTheme( themeProps: ThemeProps, - ): React.ComponentClass> | React.StatelessComponent>; + ): typeof Form; export type AddButtonProps = { className: string; @@ -436,8 +437,7 @@ declare module '@rjsf/core' { } declare module '@rjsf/core/lib/components/fields/SchemaField' { - import { JSONSchema7 } from 'json-schema'; - import { FieldProps, UiSchema, IdSchema, FormValidation } from '@rjsf/core'; + import { FieldProps } from '@rjsf/core'; export type SchemaFieldProps = Pick< FieldProps, diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index 60f488699e..05d6d41280 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -473,6 +473,12 @@ "dev": true, "optional": true }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, @@ -6786,6 +6792,12 @@ "dev": true, "optional": true }, + "ini": { + "version": "1.3.5", + "bundled": true, + "dev": true, + "optional": true + }, "is-fullwidth-code-point": { "version": "1.0.0", "bundled": true, @@ -7717,13 +7729,6 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, - "ini": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", - "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", - "dev": true, - "optional": true - }, "inquirer": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", @@ -13392,6 +13397,12 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typescript": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", + "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", + "dev": true + }, "ua-parser-js": { "version": "0.7.18", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.18.tgz", diff --git a/packages/core/package.json b/packages/core/package.json index f1b45b6fda..b113ae666c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -15,9 +15,10 @@ "publish-to-npm": "npm run build && npm publish", "start": "concurrently \"npm:build:* -- --watch\"", "tdd": "cross-env NODE_ENV=test mocha --require @babel/register --watch --require ./test/setup-jsdom.js test/**/*_test.js", - "test": "cross-env BABEL_ENV=test NODE_ENV=test mocha --require @babel/register --require ./test/setup-jsdom.js test/**/*_test.js", + "test": "concurrently \"cross-env BABEL_ENV=test NODE_ENV=test mocha --require @babel/register --require ./test/setup-jsdom.js test/**/*_test.js\" \"npm:test-type\"", "test-coverage": "cross-env NODE_ENV=test nyc --reporter=lcov mocha --require @babel/register --require ./test/setup-jsdom.js test/**/*_test.js", - "test-debug": "cross-env NODE_ENV=test mocha --require @babel/register --require ./test/setup-jsdom.js --debug-brk --inspect test/Form_test.js" + "test-debug": "cross-env NODE_ENV=test mocha --require @babel/register --require ./test/setup-jsdom.js --debug-brk --inspect test/Form_test.js", + "test-type": "tsc" }, "lint-staged": { "{src,test}/**/*.js": [ @@ -100,6 +101,7 @@ "rimraf": "^2.5.4", "sinon": "^9.0.2", "style-loader": "^0.13.1", + "typescript": "^4.2.3", "webpack": "^4.42.1", "webpack-cli": "^3.1.2", "webpack-dev-middleware": "^3.4.0", diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000000..471f698dae --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": [ + "es6", + "dom" + ], + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "baseUrl": "../", + "typeRoots": [ + "../" + ], + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react" + }, + "files": [ + "index.d.ts", + "typing_test.tsx" + ] +} diff --git a/packages/core/typing_test.tsx b/packages/core/typing_test.tsx new file mode 100644 index 0000000000..eb28577773 --- /dev/null +++ b/packages/core/typing_test.tsx @@ -0,0 +1,321 @@ +// Originally from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/93797a6d6f67508c7ab7d60b89fa2a6c2681a219/types/react-jsonschema-form/react-jsonschema-form-tests.tsx + +import * as React from 'react'; +import Form, { + UiSchema, + ErrorListProps, + FieldProps, + WidgetProps, + ErrorSchema, + withTheme, + FieldTemplateProps, + ArrayFieldTemplateProps, + ObjectFieldTemplateProps, + IdSchema, + PathSchema, + utils +} from '@rjsf/core'; +import SchemaField, { SchemaFieldProps } from '@rjsf/core/lib/components/fields/SchemaField'; +import validateFormData from '@rjsf/core/lib/validate'; +import { JSONSchema7 } from 'json-schema'; + +const { + ADDITIONAL_PROPERTY_FLAG, + allowAdditionalItems, + isFixedItems, + stubExistingAdditionalProperties, + retrieveSchema, +} = utils; + +// example taken from the react-jsonschema-form playground: +// https://github.com/mozilla-services/react-jsonschema-form/blob/fedd830294417969d88e38fb9f6b3a85e6ad105e/playground/samples/simple.js + +const schema: JSONSchema7 = { + title: 'A registration form', + type: 'object', + required: ['firstName', 'lastName'], + properties: { + firstName: { + type: 'string', + title: 'First name', + }, + lastName: { + type: 'string', + title: 'Last name', + }, + age: { + type: 'integer', + title: 'Age', + }, + bio: { + type: 'string', + title: 'Bio', + }, + password: { + type: 'string', + title: 'Password', + minLength: 3, + }, + }, +}; + +const ExampleFieldTemplate = (_props: FieldTemplateProps) => null; + +const ExampleArrayFieldTemplate = ({ items }: ArrayFieldTemplateProps) => ( +
+ {items.map(element => ( +
{element.children}
+ ))} +
+); + +const ExampleObjectFieldTemplate = (_props: ObjectFieldTemplateProps) => null; + +const uiSchema: UiSchema = { + age: { + 'ui:widget': 'updown', + }, + bio: { + 'ui:widget': 'textarea', + }, + password: { + 'ui:widget': 'password', + 'ui:help': 'Hint: Make it strong!', + }, + date: { + 'ui:widget': 'alt-datetime', + }, + 'ui:FieldTemplate': ExampleFieldTemplate, + 'ui:ArrayFieldTemplate': ExampleArrayFieldTemplate, + 'ui:ObjectFieldTemplate': ExampleObjectFieldTemplate, +}; + +interface IExampleState { + formData: any; +} + +export default function ErrorListExample(props: ErrorListProps) { + const { errors } = props; + return ( +
+
+

Errors

+
+
    + {errors.map((error, i) => { + return ( +
  • + {error.stack} +
  • + ); + })} +
+
+ ); +} + +export class Example extends React.Component { + public state: IExampleState = { + formData: { + firstName: 'Chuck', + lastName: 'Norris', + age: 75, + bio: 'Roundhouse kicking asses since 1940', + password: 'noneed', + }, + }; + + constructor(props: any) { + super(props); + } + + public render() { + return ( +
+ { +
this.setState({ formData })} + customFormats={{ + 'phone-us': /\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{4}$/, + }} + /> + } +
+ ); + } +} + +export class ExampleSchemaField extends React.Component { + constructor(props: SchemaFieldProps) { + super(props); + } + + public render() { + return ; + } +} + +interface FuncExampleProps { + formData: object; + onError: (e: ErrorSchema) => void; + onChange: (e: any) => void; +} + +export const FuncExample = (props: FuncExampleProps) => { + const { formData, onChange, onError } = props; + return ( + { + onChange(formData); + errorSchema && onError(errorSchema); + }} + /> + ); +}; + +export const BooleanCustomWidget: React.SFC = props => ( + props.onFocus('id', true)} onBlur={() => props.onFocus('id', true)} /> +); + +export const NumberCustomWidget: React.SFC = props => ( + props.onFocus('id', 0)} onBlur={() => props.onFocus('id', 0)} /> +); + +export const StringCustomWidget: React.SFC = props => ( + props.onFocus('id', 'value')} onBlur={() => props.onFocus('id', 'value')} /> +); + +export const NullCustomWidget: React.SFC = props => ( + props.onFocus('id', null)} onBlur={() => props.onFocus('id', null)} /> +); + +export const withThemeExample = () => { + const Form = withTheme({ + showErrorList: false, + noValidate: false, + noHtml5Validate: false, + }); + const forwardedRef = React.useRef>(null); + + return ; +}; + +export const formWithGenericFormData = () => { + type GenericFormData = { + field: string + } + const forwardedRef = React.useRef>(null); + + return schema={schema} formData={{ field: '' }} ref={forwardedRef} />; +}; + +export const withThemeExampleWithGenricFormData= () => { + const Form = withTheme({ + showErrorList: false, + noValidate: false, + noHtml5Validate: false, + }); + + type GenericFormData = { + field: string + } + + const forwardedRef = React.useRef>(null); + + return schema={schema} formData={{ field: '' }} ref={forwardedRef} />; +}; + +export const additionalPropertyFlagExample = () => { + return ADDITIONAL_PROPERTY_FLAG; +}; + +export const ExternalFormSubmissionExample = () => { + const formRef = React.useRef>(null); + + return ( + + + + ); +}; + +export const allowAdditionalItemsExample = (schema: JSONSchema7) => { + return allowAdditionalItems(schema); +}; + +export const isFixedItemsExample = (schema: JSONSchema7) => { + return isFixedItems(schema); +}; + +export const stubExistingAdditionalPropertiesExample = ( + schema: JSONSchema7, + definitions: { [name: string]: any }, + formData: any, +) => { + return stubExistingAdditionalProperties(schema, definitions, formData); +}; + +export const retrieveSchemaExample = (schema: JSONSchema7) => { + return retrieveSchema(schema); +}; + +export const getValidationDataExample = (formData: any, schema: JSONSchema7) => { + return validateFormData(formData, schema); +}; + +export const customFieldExample = (props: FieldProps) => { + const customProps: Pick = { + onBlur: (id, value) => { + return props.onBlur(id, value); + }, + onChange: (formData, errorSchema) => { + return props.onChange(formData, errorSchema); + }, + }; + return ; +}; + +export const omitExtraDataExample = (schema: JSONSchema7) => { + return
; +}; + +export const customTagName = (schema: JSONSchema7) => { + return ; +}; + +const TestForm = (props: React.ComponentProps<'form'>) => ; +export const customTagNameUsingComponent = (schema: JSONSchema7) => { + return ; +}; + +const idSchema: IdSchema<{ test: {} }> = { + $id: 'test', + test: { + $id: 'test', + }, +}; +void idSchema.$id; +void idSchema.test.$id; + +const pathSchema: PathSchema<{ test: {} }> = { + $name: 'test', + test: { + $name: 'test', + }, +}; +void pathSchema.$name; +void pathSchema.test.$name; diff --git a/packages/fluent-ui/src/FuiForm/FuiForm.tsx b/packages/fluent-ui/src/FuiForm/FuiForm.tsx index 02ca878950..6860718f27 100644 --- a/packages/fluent-ui/src/FuiForm/FuiForm.tsx +++ b/packages/fluent-ui/src/FuiForm/FuiForm.tsx @@ -1,8 +1,7 @@ -import { withTheme, FormProps } from '@rjsf/core'; +import CoreForm from "@rjsf/core"; +import { withTheme } from "@rjsf/core"; +import Theme from "../Theme"; -import Theme from '../Theme'; -import { StatelessComponent } from 'react'; - -const FuiForm: React.ComponentClass> | StatelessComponent> = withTheme(Theme); +const FuiForm: typeof CoreForm = withTheme(Theme); export default FuiForm; diff --git a/packages/fluent-ui/test/Form.test.tsx b/packages/fluent-ui/test/Form.test.tsx index 08157ff539..4c01b14829 100644 --- a/packages/fluent-ui/test/Form.test.tsx +++ b/packages/fluent-ui/test/Form.test.tsx @@ -1,75 +1,62 @@ -import React from 'react'; -import Form from "../src/index"; +import CoreForm from "@rjsf/core"; import { JSONSchema7 } from "json-schema"; +import React from "react"; import renderer from "react-test-renderer"; +import Form from "../src/index"; describe("single fields", () => { describe("string field", () => { test("regular", () => { const schema: JSONSchema7 = { - type: "string" + type: "string", }; - const tree = renderer - .create() - .toJSON(); + const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); }); test("format email", () => { const schema: JSONSchema7 = { type: "string", - format: "email" + format: "email", }; - const tree = renderer - .create() - .toJSON(); + const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); }); test("format uri", () => { const schema: JSONSchema7 = { type: "string", - format: "uri" + format: "uri", }; - const tree = renderer - .create() - .toJSON(); + const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); }); test("format data-url", () => { const schema: JSONSchema7 = { type: "string", - format: "data-url" + format: "data-url", }; - const tree = renderer - .create() - .toJSON(); + const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); }); }); test("number field", () => { const schema: JSONSchema7 = { - type: "number" + type: "number", }; - const tree = renderer - .create() - .toJSON(); + const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); }); test("null field", () => { const schema: JSONSchema7 = { - type: "null" + type: "null", }; - const tree = renderer - .create() - .toJSON(); + const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); }); test("unsupported field", () => { const schema: JSONSchema7 = { - type: undefined + type: undefined, }; - const tree = renderer - .create() - .toJSON(); + const tree = renderer.create().toJSON(); expect(tree).toMatchSnapshot(); }); -}); \ No newline at end of file +}); diff --git a/packages/material-ui/src/MuiForm/MuiForm.tsx b/packages/material-ui/src/MuiForm/MuiForm.tsx index 3ec526b0a2..887f0038c5 100644 --- a/packages/material-ui/src/MuiForm/MuiForm.tsx +++ b/packages/material-ui/src/MuiForm/MuiForm.tsx @@ -1,8 +1,6 @@ -import { withTheme, FormProps } from '@rjsf/core'; +import CoreForm, { withTheme } from "@rjsf/core"; +import Theme from "../Theme"; -import Theme from '../Theme'; -import { StatelessComponent } from 'react'; - -const MuiForm: React.ComponentClass> | StatelessComponent> = withTheme(Theme); +const MuiForm: typeof CoreForm = withTheme(Theme); export default MuiForm; diff --git a/packages/material-ui/test/Form.test.tsx b/packages/material-ui/test/Form.test.tsx index 39362a0365..e32a88ae01 100644 --- a/packages/material-ui/test/Form.test.tsx +++ b/packages/material-ui/test/Form.test.tsx @@ -1,8 +1,8 @@ -import React from "react"; -import Form from "../src/index"; +import CoreForm, { UiSchema } from "@rjsf/core"; import { JSONSchema7 } from "json-schema"; +import React from "react"; import renderer from "react-test-renderer"; -import { UiSchema } from "@rjsf/core"; +import Form from "../src/index"; describe("single fields", () => { describe("string field", () => {