diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5f2846c..48a486e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -82,3 +82,23 @@ jobs: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }} IS_PRE_RELEASE: ${{ needs.check_if_version_upgraded.outputs.is_pre_release }} + + publish_playground: + runs-on: ubuntu-latest + needs: + - publish_on_npm + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + - uses: actions/setup-node@v4 + with: + registry-url: https://registry.npmjs.org/ + - uses: bahmutov/npm-install@v1 + with: + working-directory: demo + - run: | + cd demo + yarn publish-to-gh-pages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..efd261b --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +demo \ No newline at end of file diff --git a/README.md b/README.md index 3398da2..dab843b 100644 --- a/README.md +++ b/README.md @@ -124,3 +124,13 @@ The following options can be set for fields of type `array`: > "ui:hideTitle": true, > ... > ``` + +## Development + +Launch the playground to play with the DSFR theme : + +```sh +cd demo +yarn +yarn start +``` diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 0000000..13118bb --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1,2 @@ + +/public/dsfr/ diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 0000000..c23adf1 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,20 @@ + + + + + + + rjsf-dsfr playground + + + + +
+ + + + diff --git a/demo/package.json b/demo/package.json new file mode 100644 index 0000000..0800503 --- /dev/null +++ b/demo/package.json @@ -0,0 +1,79 @@ +{ + "name": "playground", + "version": "1.0.0", + "description": "rjsf-dsfr playground", + "license": "Apache-2.0", + "private": true, + "scripts": { + "cs-check": "prettier -l \"src/**/*.ts?(x)\"", + "cs-format": "prettier \"src/**/*.ts?(x)\" --write", + "build": "rimraf build && cross-env NODE_ENV=production vite build", + "publish-to-gh-pages": "npm run build && gh-pages --dist dist/", + "start": "vite --force", + "preview": "vite preview", + "postinstall": "copy-dsfr-to-public", + "prestart": "only-include-used-icons", + "prebuild": "only-include-used-icons" + }, + "lint-staged": { + "src/**/*.ts?(x)": [ + "eslint --fix" + ] + }, + "files": [ + "dist", + "lib", + "src" + ], + "engineStrict": false, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@codegouvfr/react-dsfr": "^1.9.22", + "@codegouvfr/rjsf-dsfr": "^0.1.4", + "@rjsf/bootstrap-4": "^5.18.5", + "@rjsf/core": "^5.18.4", + "@rjsf/utils": "^5.18.4", + "@rjsf/validator-ajv8": "^5.18.4", + "ajv-i18n": "^4.2.0", + "atob": "^2.1.2", + "core-js": "^3.35.1", + "dayjs": "^1.11.10", + "deep-freeze-es6": "^1.4.1", + "framer-motion": "^5.6.0", + "jss": "^10.10.0", + "lodash": "^4.17.21", + "monaco-editor": "^0.38.0", + "react": "^18.3.1", + "react-bootstrap": "^1.6.8", + "react-dom": "^18.3.1", + "react-frame-component": "^4.1.3", + "react-is": "^18.2.0", + "react-portal": "^4.2.2" + }, + "devDependencies": { + "@emotion/cache": "^11.11.0", + "@emotion/react": "^11.11.3", + "@emotion/styled": "^11.11.0", + "@monaco-editor/react": "^4.6.0", + "@types/lodash": "^4.14.202", + "@types/node": "^20.11.20", + "@types/react-frame-component": "^4.1.6", + "@vitejs/plugin-react": "^4.2.1", + "cross-env": "^7.0.3", + "esbuild": "^0.18.20", + "gh-pages": "^5.0.0", + "html": "^1.0.0", + "html-webpack-plugin": "^5.6.0", + "loader-utils": "^3.2.1", + "mini-css-extract-plugin": "^2.8.0", + "prettier": "^3.3.2", + "react-transform-catch-errors": "^1.0.2", + "react-transform-hmr": "^1.0.4", + "rimraf": "^5.0.5", + "source-map-loader": "^4.0.2", + "typescript": "^4.9.5", + "vite": "^4.5.2" + } +} diff --git a/demo/public/.gitkeep b/demo/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/demo/src/app.tsx b/demo/src/app.tsx new file mode 100644 index 0000000..e713be7 --- /dev/null +++ b/demo/src/app.tsx @@ -0,0 +1,39 @@ +import { templatesDSFR, widgetsDSFR } from '@codegouvfr/rjsf-dsfr' +import { customizeValidator } from '@rjsf/validator-ajv8' +import localize_fr from 'ajv-i18n/localize/fr' +import { Theme as Bootstrap4Theme } from '@rjsf/bootstrap-4' + +import Layout from './layout' +import Playground, { PlaygroundProps } from './components' + +const DSFRTheme = { + templates: templatesDSFR, + widgets: widgetsDSFR, +} + +const frV8Validator = customizeValidator({}, localize_fr) + +const validators: PlaygroundProps['validators'] = { + AJV8: frV8Validator, +} + +const themes: PlaygroundProps['themes'] = { + dsfr: { + stylesheets: ['/dsfr/dsfr.main.css', '/dsfr/utility/icons/icons.css'], + theme: DSFRTheme, + }, + 'bootstrap-4': { + stylesheets: [ + 'https://maxcdn.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css', + ], + theme: Bootstrap4Theme, + }, +} + +export default function App() { + return ( + + + + ) +} diff --git a/demo/src/components/CopyLink.tsx b/demo/src/components/CopyLink.tsx new file mode 100644 index 0000000..925ceda --- /dev/null +++ b/demo/src/components/CopyLink.tsx @@ -0,0 +1,34 @@ +import { useRef } from 'react'; + +interface CopyLinkProps { + shareURL: string | null; + onShare: () => void; +} + +export default function CopyLink({ shareURL, onShare }: CopyLinkProps) { + const input = useRef(null); + + function onCopyClick() { + input.current?.select(); + navigator.clipboard.writeText(input.current?.value ?? ''); + } + + if (!shareURL) { + return ( + + ); + } + + return ( +
+ + + + +
+ ); +} diff --git a/demo/src/components/DemoFrame.tsx b/demo/src/components/DemoFrame.tsx new file mode 100644 index 0000000..cae05b6 --- /dev/null +++ b/demo/src/components/DemoFrame.tsx @@ -0,0 +1,89 @@ +import { + useState, + useRef, + useCallback, + cloneElement, + ReactElement, + ReactNode, +} from 'react' +import { CacheProvider } from '@emotion/react' +import createCache, { EmotionCache } from '@emotion/cache' +import { create, Jss } from 'jss' +import Frame, { + FrameComponentProps, + FrameContextConsumer, +} from 'react-frame-component' + +interface DemoFrameProps extends FrameComponentProps { + theme: string + /** override children to be ReactElement to avoid Typescript issue. In this case we don't need to worry about + * children being of the other valid ReactNode types, undefined and string as it always contains an RJSF `Form` + */ + children: ReactElement +} + +export default function DemoFrame(props: DemoFrameProps) { + const { children, head, theme, ...frameProps } = props + + const [jss, setJss] = useState() + const [ready, setReady] = useState(false) + const [sheetsManager, setSheetsManager] = useState(new Map()) + const [emotionCache, setEmotionCache] = useState( + createCache({ key: 'css' }) + ) + const [container, setContainer] = useState() + const [window, setWindow] = useState() + + const instanceRef = useRef() + + const handleRef = useCallback( + (ref: any) => { + instanceRef.current = { + contentDocument: ref ? ref.node.contentDocument : null, + contentWindow: ref ? ref.node.contentWindow : null, + } + }, + [instanceRef] + ) + + const onContentDidMount = useCallback(() => { + setReady(true) + setJss( + create({ + //plugins: jssPreset().plugins, + insertionPoint: instanceRef.current.contentWindow['demo-frame-jss'], + }) + ) + setSheetsManager(new Map()) + setEmotionCache( + createCache({ + key: 'css', + prepend: true, + container: instanceRef.current.contentWindow['demo-frame-jss'], + }) + ) + setContainer(instanceRef.current.contentDocument.body) + + setWindow(() => instanceRef.current.contentWindow) + }, []) + + let body: ReactNode = children + if (theme === 'dsfr') { + body = ready ? ( +
{children}
+ ) : // TODO {children} + null + } + + return ( + +
+ {body} + + ) +} diff --git a/demo/src/components/Editors.tsx b/demo/src/components/Editors.tsx new file mode 100644 index 0000000..12d7f08 --- /dev/null +++ b/demo/src/components/Editors.tsx @@ -0,0 +1,146 @@ +import { useCallback, useState } from 'react'; +import MonacoEditor from '@monaco-editor/react'; +import { ErrorSchema, RJSFSchema, UiSchema } from '@rjsf/utils'; +import isEqualWith from 'lodash/isEqualWith'; + +const monacoEditorOptions = { + minimap: { + enabled: false, + }, + automaticLayout: true, +}; + +type EditorProps = { + title: string; + code: string; + onChange: (code: string) => void; +}; + +function Editor({ title, code, onChange }: EditorProps) { + const [valid, setValid] = useState(true); + + const onCodeChange = useCallback( + (code: string | undefined) => { + if (!code) { + return; + } + + try { + const parsedCode = JSON.parse(code); + setValid(true); + onChange(parsedCode); + } catch (err) { + setValid(false); + } + }, + [setValid, onChange] + ); + + const icon = valid ? 'ok' : 'remove'; + const cls = valid ? 'valid' : 'invalid'; + + return ( +
+
+ + {' ' + title} +
+ +
+ ); +} + +const toJson = (val: unknown) => JSON.stringify(val, null, 2); + +type EditorsProps = { + schema: RJSFSchema; + setSchema: React.Dispatch>; + uiSchema: UiSchema; + setUiSchema: React.Dispatch>; + formData: any; + setFormData: React.Dispatch>; + extraErrors: ErrorSchema | undefined; + setExtraErrors: React.Dispatch>; + setShareURL: React.Dispatch>; +}; + +export default function Editors({ + extraErrors, + formData, + schema, + uiSchema, + setExtraErrors, + setFormData, + setSchema, + setShareURL, + setUiSchema, +}: EditorsProps) { + const onSchemaEdited = useCallback( + (newSchema) => { + setSchema(newSchema); + setShareURL(null); + }, + [setSchema, setShareURL] + ); + + const onUISchemaEdited = useCallback( + (newUiSchema) => { + setUiSchema(newUiSchema); + setShareURL(null); + }, + [setUiSchema, setShareURL] + ); + + const onFormDataEdited = useCallback( + (newFormData) => { + if ( + !isEqualWith(newFormData, formData, (newValue, oldValue) => { + // Since this is coming from the editor which uses JSON.stringify to trim undefined values compare the values + // using JSON.stringify to see if the trimmed formData is the same as the untrimmed state + // Sometimes passing the trimmed value back into the Form causes the defaults to be improperly assigned + return JSON.stringify(oldValue) === JSON.stringify(newValue); + }) + ) { + setFormData(newFormData); + setShareURL(null); + } + }, + [formData, setFormData, setShareURL] + ); + + const onExtraErrorsEdited = useCallback( + (newExtraErrors) => { + setExtraErrors(newExtraErrors); + setShareURL(null); + }, + [setExtraErrors, setShareURL] + ); + + return ( +
+ +
+
+ +
+
+ +
+
+ {extraErrors && ( +
+
+ +
+
+ )} +
+ ); +} diff --git a/demo/src/components/ErrorBoundary.tsx b/demo/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..bfcb55a --- /dev/null +++ b/demo/src/components/ErrorBoundary.tsx @@ -0,0 +1,52 @@ +import { Component, ReactNode } from 'react'; + +type Props = { + children: ReactNode; +}; + +type State = + | { + hasError: false; + error: null; + } + | { hasError: true; error: Error }; + +type Error = { message: string; [key: string]: unknown }; + +class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false, error: null }; + } + + /** Update state so the next render will show the fallback UI. */ + static getDerivedStateFromError(error: Error) { + return { hasError: true, error: error }; + } + + resetErrorBoundary = () => { + this.setState({ hasError: false, error: null }); + }; + + /** You can render any custom fallback UI */ + render() { + const { children } = this.props; + const { error, hasError } = this.state; + + if (hasError) { + return ( +
+

The following error was encountered:

+
{error.message}
+ +
+ ); + } + + return children; + } +} + +export default ErrorBoundary; diff --git a/demo/src/components/GeoPosition.tsx b/demo/src/components/GeoPosition.tsx new file mode 100644 index 0000000..32deefe --- /dev/null +++ b/demo/src/components/GeoPosition.tsx @@ -0,0 +1,38 @@ +import { useState } from 'react'; + +export default function GeoPosition() { + const [lat, setLat] = useState(0); + const [lon, setLon] = useState(0); + + return ( +
+

Hey, I'm a custom component

+

+ I'm registered as geo and referenced in + uiSchema as the ui:field to use for this schema. +

+
+
+ + setLat(parseFloat(e.target.value))} + /> +
+
+ + setLon(parseFloat(e.target.value))} + /> +
+
+
+ ); +} diff --git a/demo/src/components/Header.tsx b/demo/src/components/Header.tsx new file mode 100644 index 0000000..918676a --- /dev/null +++ b/demo/src/components/Header.tsx @@ -0,0 +1,348 @@ +import { useCallback } from 'react' +import Form, { IChangeEvent } from '@rjsf/core' +import { RJSFSchema, UiSchema, ValidatorType } from '@rjsf/utils' +import localValidator from '@rjsf/validator-ajv8' +import base64 from '../utils/base64' + +import CopyLink from './CopyLink' +import ThemeSelector, { ThemesType } from './ThemeSelector' +import Selector from './Selector' +import ValidatorSelector from './ValidatorSelector' +import SubthemeSelector from './SubthemeSelector' +import RawValidatorTest from './RawValidatorTest' + +const HeaderButton: React.FC< + { + title: string + onClick: () => void + } & React.ButtonHTMLAttributes +> = ({ title, onClick, children, ...buttonProps }) => { + return ( + + ) +} + +function HeaderButtons({ + playGroundFormRef, +}: { + playGroundFormRef: React.MutableRefObject +}) { + return ( + <> + +
+ playGroundFormRef.current.submit()} + > + Submit + + playGroundFormRef.current.validateForm()} + > + Validate + + playGroundFormRef.current.reset()} + > + Reset + +
+ + ) +} + +const liveSettingsBooleanSchema: RJSFSchema = { + type: 'object', + properties: { + liveValidate: { type: 'boolean', title: 'Live validation' }, + disabled: { type: 'boolean', title: 'Disable whole form' }, + readonly: { type: 'boolean', title: 'Readonly whole form' }, + omitExtraData: { type: 'boolean', title: 'Omit extra data' }, + liveOmit: { type: 'boolean', title: 'Live omit' }, + noValidate: { type: 'boolean', title: 'Disable validation' }, + noHtml5Validate: { type: 'boolean', title: 'Disable HTML 5 validation' }, + focusOnFirstError: { type: 'boolean', title: 'Focus on 1st Error' }, + }, +} + +const liveSettingsSelectSchema: RJSFSchema = { + type: 'object', + properties: { + showErrorList: { + type: 'string', + default: 'top', + title: 'Show Error List', + enum: [false, 'top', 'bottom'], + }, + experimental_defaultFormStateBehavior: { + title: 'Default Form State Behavior (Experimental)', + type: 'object', + properties: { + arrayMinItems: { + type: 'object', + properties: { + populate: { + type: 'string', + default: 'populate', + title: 'Populate minItems in arrays', + oneOf: [ + { + type: 'string', + title: + 'Populate remaining minItems with default values (legacy behavior)', + enum: ['all'], + }, + { + type: 'string', + title: + 'Only populate minItems with default values when field is required', + enum: ['requiredOnly'], + }, + { + type: 'string', + title: 'Never populate minItems with default values', + enum: ['never'], + }, + ], + }, + mergeExtraDefaults: { + title: 'Merge array defaults with formData', + type: 'boolean', + default: false, + }, + }, + }, + allOf: { + type: 'string', + title: 'allOf defaults behaviour', + default: 'skipDefaults', + oneOf: [ + { + type: 'string', + title: 'Populate defaults with allOf', + enum: ['populateDefaults'], + }, + { + type: 'string', + title: 'Skip populating defaults with allOf', + enum: ['skipDefaults'], + }, + ], + }, + emptyObjectFields: { + type: 'string', + title: 'Object fields default behavior', + default: 'populateAllDefaults', + oneOf: [ + { + type: 'string', + title: + 'Assign value to formData when default is primitive, non-empty object field, or is required (legacy behavior)', + enum: ['populateAllDefaults'], + }, + { + type: 'string', + title: + 'Assign value to formData when default is an object and parent is required, or default is primitive and is required', + enum: ['populateRequiredDefaults'], + }, + { + type: 'string', + title: 'Assign value to formData when only default is set', + enum: ['skipEmptyDefaults'], + }, + { + type: 'string', + title: 'Does not set defaults', + enum: ['skipDefaults'], + }, + ], + }, + }, + }, + }, +} + +const liveSettingsSelectUiSchema: UiSchema = { + experimental_defaultFormStateBehavior: { + 'ui:options': { + label: false, + }, + arrayMinItems: { + 'ui:options': { + label: false, + }, + }, + }, +} + +export interface LiveSettings { + showErrorList: false | 'top' | 'bottom' + [key: string]: any +} + +type HeaderProps = { + schema: RJSFSchema + uiSchema: UiSchema + formData: any + shareURL: string | null + themes: { [themeName: string]: ThemesType } + theme: string + subtheme: string | null + validators: { + [validatorName: string]: ValidatorType + } + validator: string + liveSettings: LiveSettings + playGroundFormRef: React.MutableRefObject + load: (data: any) => void + onThemeSelected: (theme: string, themeObj: ThemesType) => void + setSubtheme: React.Dispatch> + setStylesheets: React.Dispatch> + setValidator: React.Dispatch> + setLiveSettings: React.Dispatch> + setShareURL: React.Dispatch> +} + +export default function Header({ + schema, + uiSchema, + formData, + shareURL, + themes, + theme, + subtheme, + validators, + validator, + liveSettings, + playGroundFormRef, + load, + onThemeSelected, + setSubtheme, + setStylesheets, + setValidator, + setLiveSettings, + setShareURL, +}: HeaderProps) { + const onSubthemeSelected = useCallback( + (subtheme: any, { stylesheets }: { stylesheets: [] }) => { + setSubtheme(subtheme) + setStylesheets(stylesheets) + }, + [setSubtheme, setStylesheets] + ) + + const onValidatorSelected = useCallback( + (validator: string) => { + setValidator(validator) + }, + [setValidator] + ) + + const handleSetLiveSettings = useCallback( + ({ formData }: IChangeEvent) => { + setLiveSettings((previousLiveSettings) => ({ + ...previousLiveSettings, + ...formData, + })) + }, + [setLiveSettings] + ) + + const onShare = useCallback(() => { + const { + location: { origin, pathname }, + } = document + + try { + const hash = base64.encode( + JSON.stringify({ + formData, + schema, + uiSchema, + theme, + liveSettings, + }) + ) + + setShareURL(`${origin}${pathname}#${hash}`) + } catch (error) { + setShareURL(null) + console.error(error) + } + }, [formData, liveSettings, schema, theme, uiSchema, setShareURL]) + + return ( +
+

react-jsonschema-form

+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ + {themes[theme] && themes[theme].subthemes && ( + + )} + + +
+ +
+
+ +
+
+
+ ) +} diff --git a/demo/src/components/Playground.tsx b/demo/src/components/Playground.tsx new file mode 100644 index 0000000..6defb12 --- /dev/null +++ b/demo/src/components/Playground.tsx @@ -0,0 +1,205 @@ +import { ComponentType, FormEvent, useCallback, useEffect, useRef, useState } from 'react'; +import { FormProps, IChangeEvent, withTheme } from '@rjsf/core'; +import { ErrorSchema, RJSFSchema, RJSFValidationError, UiSchema, ValidatorType } from '@rjsf/utils'; +import base64 from '../utils/base64'; + +import { samples } from '../samples'; +import Header, { LiveSettings } from './Header'; +import DemoFrame from './DemoFrame'; +import ErrorBoundary from './ErrorBoundary'; +import GeoPosition from './GeoPosition'; +import { ThemesType } from './ThemeSelector'; +import Editors from './Editors'; +import SpecialInput from './SpecialInput'; +import { Sample } from '../samples/Sample'; + +export interface PlaygroundProps { + themes: { [themeName: string]: ThemesType }; + validators: { [validatorName: string]: ValidatorType }; +} + +export default function Playground({ themes, validators }: PlaygroundProps) { + const [loaded, setLoaded] = useState(false); + const [schema, setSchema] = useState(samples.Simple.schema as RJSFSchema); + const [uiSchema, setUiSchema] = useState(samples.Simple.uiSchema!); + const [formData, setFormData] = useState(samples.Simple.formData); + const [extraErrors, setExtraErrors] = useState(); + const [shareURL, setShareURL] = useState(null); + const [theme, setTheme] = useState('dsfr'); + const [subtheme, setSubtheme] = useState(null); + const [stylesheets, setStylesheets] = useState([]); + const [validator, setValidator] = useState('AJV8'); + const [showForm, setShowForm] = useState(false); + const [liveSettings, setLiveSettings] = useState({ + showErrorList: 'top', + validate: false, + disabled: false, + noHtml5Validate: false, + readonly: false, + omitExtraData: false, + liveOmit: false, + experimental_defaultFormStateBehavior: { arrayMinItems: 'populate', emptyObjectFields: 'populateAllDefaults' }, + }); + const [FormComponent, setFormComponent] = useState>(withTheme({})); + const [otherFormProps, setOtherFormProps] = useState>({}); + + const playGroundFormRef = useRef(null); + + const onThemeSelected = useCallback( + (theme: string, { stylesheets, theme: themeObj }: ThemesType) => { + setTheme(theme); + setSubtheme(null); + setFormComponent(withTheme(themeObj)); + if (stylesheets) setStylesheets(stylesheets); + }, + [setTheme, setSubtheme, setFormComponent, setStylesheets] + ); + + const load = useCallback( + (data: Sample & { theme: string; liveSettings: LiveSettings }) => { + const { + schema, + // uiSchema is missing on some examples. Provide a default to + // clear the field in all cases. + uiSchema = {}, + // Always reset templates and fields + templates = {}, + fields = {}, + formData, + theme: dataTheme = theme, + extraErrors, + liveSettings, + ...rest + } = data; + + onThemeSelected(dataTheme, themes[dataTheme]); + + // force resetting form component instance + setShowForm(false); + setSchema(schema); + setUiSchema(uiSchema); + setFormData(formData); + setExtraErrors(extraErrors); + setTheme(dataTheme); + setShowForm(true); + setLiveSettings(liveSettings); + setOtherFormProps({ fields, templates, ...rest }); + }, + [theme, onThemeSelected, themes] + ); + + useEffect(() => { + const hash = document.location.hash.match(/#(.*)/); + + if (hash && typeof hash[1] === 'string' && hash[1].length > 0 && !loaded) { + try { + const decoded = base64.decode(hash[1]); + load(JSON.parse(decoded)); + setLoaded(true); + } catch (error) { + alert('Unable to load form setup data.'); + console.error(error); + } + + return; + } + + // initialize theme + onThemeSelected(theme, themes[theme]); + + setShowForm(true); + }, [onThemeSelected, load, loaded, setShowForm, theme, themes]); + + const onFormDataChange = useCallback( + ({ formData }: IChangeEvent, id?: string) => { + if (id) { + console.log('Field changed, id: ', id); + } + + setFormData(formData); + setShareURL(null); + }, + [setFormData, setShareURL] + ); + + const onFormDataSubmit = useCallback(({ formData }: IChangeEvent, event: FormEvent) => { + console.log('submitted formData', formData); + console.log('submit event', event); + window.alert('Form submitted'); + }, []); + + return ( + <> +
+ +
+ + {showForm && ( + + {stylesheets?.map(s => )} + + } + style={{ + width: '100%', + height: 1000, + border: 0, + }} + theme={theme} + > + console.log(`Touched ${id} with value ${value}`)} + onFocus={(id: string, value: string) => console.log(`Focused ${id} with value ${value}`)} + onError={(errorList: RJSFValidationError[]) => console.log('errors', errorList)} + ref={playGroundFormRef} + /> + + )} + +
+ + ); +} diff --git a/demo/src/components/RawValidatorTest.tsx b/demo/src/components/RawValidatorTest.tsx new file mode 100644 index 0000000..766b005 --- /dev/null +++ b/demo/src/components/RawValidatorTest.tsx @@ -0,0 +1,50 @@ +import { useState } from 'react'; +import { ValidatorType, RJSFSchema } from '@rjsf/utils'; + +interface RawValidatorTestProps { + validator: ValidatorType; + schema: RJSFSchema; + formData: object; +} + +export default function RawValidatorTest({ validator, schema, formData }: RawValidatorTestProps) { + const [rawValidation, setRawValidation] = useState<{ errors?: any[]; validationError?: Error } | undefined>(); + const handleClearClick = () => setRawValidation(undefined); + const handleRawClick = () => setRawValidation(validator.rawValidation(schema, formData)); + + let displayErrors = 'Validation not run'; + if (rawValidation) { + displayErrors = + rawValidation.errors || rawValidation.validationError + ? JSON.stringify(rawValidation, null, 2) + : 'No AJV errors encountered'; + } + return ( +
+
+ Raw Ajv Validation + To determine whether a validation issue is really a BUG in Ajv use the button to trigger the raw Ajv validation. + This will run your schema and formData through Ajv without involving any react-jsonschema-form specific code. If + there is an unexpected error, then{' '} + + file an issue + {' '} + with Ajv instead. +
+
+ + {rawValidation && ( + <> + + + + )} +
+