UNPKG

@datalayer/primer-rjsf

Version:

React JSON Schema Form (RJSF) for Primer

311 lines (310 loc) 18.4 kB
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import React, { Component } from "react"; import MonacoEditor from "@monaco-editor/react"; import { samples } from "./samples"; import Form, { withTheme } from "@rjsf/core"; import { shouldRender } from "@rjsf/utils"; import localValidator from "@rjsf/validator-ajv8"; import isEqualWith from "lodash/isEqualWith"; import DemoFrame from "./DemoFrame"; import ErrorBoundary from "./ErrorBoundary"; const log = (type) => console.log.bind(console, type); const toJson = (val) => JSON.stringify(val, null, 2); const liveSettingsSchema = { type: "object", properties: { liveValidate: { type: "boolean", title: "Live validation" }, disable: { 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" }, focusOnFirstError: { type: "boolean", title: "Focus on 1st Error" }, showErrorList: { type: "string", "default": "top", title: "Show Error List", enum: [false, "top", "bottom"] } }, }; const monacoEditorOptions = { minimap: { enabled: false, }, automaticLayout: true, }; class GeoPosition extends Component { constructor(props) { super(props); this.state = { ...props.formData }; } onChange(name) { return (event) => { this.setState({ [name]: parseFloat(event.target.value) }); setTimeout(() => this.props.onChange(this.state), 0); }; } render() { const { lat, lon } = this.state; return (_jsxs("div", { className: "geo", children: [_jsx("h3", { children: "Hey, I'm a custom component" }), _jsxs("p", { children: ["I'm registered as ", _jsx("code", { children: "geo" }), " and referenced in", _jsx("code", { children: "uiSchema" }), " as the ", _jsx("code", { children: "ui:field" }), " to use for this schema."] }), _jsxs("div", { className: "row", children: [_jsxs("div", { className: "col-sm-6", children: [_jsx("label", { children: "Latitude" }), _jsx("input", { className: "form-control", type: "number", value: lat, step: "0.00001", onChange: this.onChange("lat") })] }), _jsxs("div", { className: "col-sm-6", children: [_jsx("label", { children: "Longitude" }), _jsx("input", { className: "form-control", type: "number", value: lon, step: "0.00001", onChange: this.onChange("lon") })] })] })] })); } } class Editor extends Component { constructor(props) { super(props); this.state = { valid: true, code: props.code }; } UNSAFE_componentWillReceiveProps(props) { this.setState({ valid: true, code: props.code }); } shouldComponentUpdate(nextProps, nextState) { if (this.state.valid) { return (JSON.stringify(JSON.parse(nextProps.code)) !== JSON.stringify(JSON.parse(this.state.code))); } return false; } onCodeChange = (code) => { try { const parsedCode = JSON.parse(code); this.setState({ valid: true, code }, () => this.props.onChange(parsedCode)); } catch (err) { this.setState({ valid: false, code }); } }; render() { const { title } = this.props; const icon = this.state.valid ? "ok" : "remove"; const cls = this.state.valid ? "valid" : "invalid"; return (_jsxs("div", { className: "panel panel-default", children: [_jsxs("div", { className: "panel-heading", children: [_jsx("span", { className: `${cls} glyphicon glyphicon-${icon}` }), " " + title] }), _jsx(MonacoEditor, { language: "json", value: this.state.code, theme: "vs-light", onChange: this.onCodeChange, height: 400, options: monacoEditorOptions })] })); } } class Selector extends Component { constructor(props) { super(props); this.state = { current: "Simple" }; } shouldComponentUpdate(nextProps, nextState) { return shouldRender(this, nextProps, nextState); } onLabelClick = (label) => { return (event) => { event.preventDefault(); this.setState({ current: label }); setTimeout(() => this.props.onSelected(samples[label]), 0); }; }; render() { return (_jsx("ul", { className: "nav nav-pills", children: Object.keys(samples).map((label, i) => { return (_jsx("li", { role: "presentation", className: this.state.current === label ? "active" : "", children: _jsx("a", { href: "#", onClick: this.onLabelClick(label), children: label }) }, i)); }) })); } } function ThemeSelector({ theme, themes, select }) { const schema = { type: "string", enum: Object.keys(themes), }; const uiSchema = { "ui:placeholder": "Select theme", }; return (_jsx(Form, { className: "form_rjsf_themeSelector", idPrefix: "rjsf_themeSelector", schema: schema, uiSchema: uiSchema, formData: theme, validator: localValidator, onChange: ({ formData }) => formData && select(formData, themes[formData]), children: _jsx("div", {}) })); } function SubthemeSelector({ subtheme, subthemes, select }) { const schema = { type: "string", enum: Object.keys(subthemes), }; const uiSchema = { "ui:placeholder": "Select subtheme", }; return (_jsx(Form, { className: "form_rjsf_subthemeSelector", idPrefix: "rjsf_subthemeSelector", schema: schema, uiSchema: uiSchema, formData: subtheme, validator: localValidator, onChange: ({ formData }) => formData && select(formData, subthemes[formData]), children: _jsx("div", {}) })); } function ValidatorSelector({ validator, validators, select }) { const schema = { type: "string", enum: Object.keys(validators), }; const uiSchema = { "ui:placeholder": "Select validator", }; return (_jsx(Form, { className: "form_rjsf_validatorSelector", idPrefix: "rjsf_validatorSelector", schema: schema, uiSchema: uiSchema, formData: validator, validator: localValidator, onChange: ({ formData }) => formData && select(formData), children: _jsx("div", {}) })); } class CopyLink extends Component { onCopyClick = (event) => { this.input.select(); document.execCommand("copy"); }; render() { const { shareURL, onShare } = this.props; if (!shareURL) { return (_jsx("button", { className: "btn btn-default", type: "button", onClick: onShare, children: "Share" })); } return (_jsxs("div", { className: "input-group", children: [_jsx("input", { type: "text", ref: (input) => (this.input = input), className: "form-control", defaultValue: shareURL }), _jsx("span", { className: "input-group-btn", children: _jsx("button", { className: "btn btn-default", type: "button", onClick: this.onCopyClick, children: _jsx("i", { className: "glyphicon glyphicon-copy" }) }) })] })); } } function RawValidatorTest({ validator, schema, formData }) { const [rawValidation, setRawValidation] = React.useState(); 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 (_jsxs("div", { children: [_jsxs("details", { style: { marginBottom: "10px" }, children: [_jsx("summary", { style: { display: "list-item" }, children: "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 ", _jsx("a", { href: "https://github.com/ajv-validator/ajv/issues/new/choose", target: "_blank", rel: "noopener", children: "file an issue" }), " with Ajv instead."] }), _jsxs("div", { style: { marginBottom: "10px" }, children: [_jsx("button", { className: "btn btn-default", type: "button", onClick: handleRawClick, children: "Raw Validate" }), rawValidation && (_jsxs(_Fragment, { children: [_jsx("span", { children: " " }), _jsx("button", { className: "btn btn-default", type: "button", onClick: handleClearClick, children: "Clear" })] }))] }), _jsx("textarea", { rows: 4, readOnly: true, disabled: !rawValidation, value: displayErrors })] })); } class Playground extends Component { constructor(props) { super(props); console.log(props); // set default theme const theme = "primer"; const validator = "AJV8"; // initialize state with Simple data sample const { schema, uiSchema, formData, validate } = samples.Simple; this.playGroundForm = React.createRef(); this.state = { form: false, schema, uiSchema, formData, validate, theme, validator, subtheme: null, liveSettings: { showErrorList: 'top', validate: false, disable: false, readonly: false, omitExtraData: false, liveOmit: false, }, shareURL: null, FormComponent: withTheme({}), }; } componentDidMount() { const { themes } = this.props; const { theme } = this.state; const hash = document.location.hash.match(/#(.*)/); if (hash && typeof hash[1] === "string" && hash[1].length > 0) { try { this.load(JSON.parse(atob(hash[1]))); } catch (err) { alert("Unable to load form setup data."); } } else { // initialize theme this.onThemeSelected(theme, themes[theme]); this.setState({ form: true }); } } shouldComponentUpdate(nextProps, nextState) { return shouldRender(this, nextProps, nextState); } load = (data) => { // Reset the ArrayFieldTemplate whenever you load new data const { ArrayFieldTemplate, ObjectFieldTemplate, extraErrors } = data; // uiSchema is missing on some examples. Provide a default to // clear the field in all cases. const { uiSchema = {} } = data; const { theme = this.state.theme } = data; const { themes } = this.props; this.onThemeSelected(theme, themes[theme]); // force resetting form component instance this.setState({ form: false }, () => this.setState({ ...data, form: true, ArrayFieldTemplate, ObjectFieldTemplate, uiSchema, extraErrors, })); }; onSchemaEdited = (schema) => this.setState({ schema, shareURL: null }); onUISchemaEdited = (uiSchema) => this.setState({ uiSchema, shareURL: null }); onFormDataEdited = (formData) => { if (!isEqualWith(formData, this.state.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); })) { this.setState({ formData, shareURL: null }); } }; onExtraErrorsEdited = (extraErrors) => this.setState({ extraErrors, shareURL: null }); onThemeSelected = (theme, { subthemes, stylesheet, theme: themeObj } = {}) => { this.setState({ theme, subthemes, subtheme: null, FormComponent: withTheme(themeObj), stylesheet, }); }; onSubthemeSelected = (subtheme, { stylesheet }) => { this.setState({ subtheme, stylesheet, }); }; onValidatorSelected = (validator) => { this.setState({ validator }); }; setLiveSettings = ({ formData }) => this.setState({ liveSettings: formData }); onFormDataChange = ({ formData = "" }, id) => { if (id) { console.log("Field changed, id: ", id); } return this.setState({ formData, shareURL: null }); }; onShare = () => { const { formData, schema, uiSchema, liveSettings, errorSchema, theme } = this.state; const { location: { origin, pathname }, } = document; try { const hash = btoa(JSON.stringify({ formData, schema, uiSchema, theme, liveSettings, errorSchema, })); this.setState({ shareURL: `${origin}${pathname}#${hash}` }); } catch (err) { this.setState({ shareURL: null }); } }; render() { const { schema, uiSchema, formData, extraErrors, liveSettings, validate, theme, validator, subtheme, FormComponent, ArrayFieldTemplate, ObjectFieldTemplate, transformErrors, } = this.state; const { themes, validators } = this.props; let templateProps = {}; if (ArrayFieldTemplate) { templateProps.ArrayFieldTemplate = ArrayFieldTemplate; } if (ObjectFieldTemplate) { templateProps.ObjectFieldTemplate = ObjectFieldTemplate; } if (extraErrors) { templateProps.extraErrors = extraErrors; } return (_jsxs("div", { className: "container-fluid", children: [_jsxs("div", { className: "page-header", children: [_jsx("h1", { children: "react-jsonschema-form" }), _jsxs("div", { className: "row", children: [_jsx("div", { className: "col-sm-6", children: _jsx(Selector, { onSelected: this.load }) }), _jsx("div", { className: "col-sm-2", children: _jsx(Form, { idPrefix: "rjsf_options", schema: liveSettingsSchema, formData: liveSettings, validator: localValidator, onChange: this.setLiveSettings, children: _jsx("div", {}) }) }), _jsxs("div", { className: "col-sm-2", children: [_jsx(ThemeSelector, { themes: themes, theme: theme, select: this.onThemeSelected }), themes[theme] && themes[theme].subthemes && (_jsx(SubthemeSelector, { subthemes: themes[theme].subthemes, subtheme: subtheme, select: this.onSubthemeSelected })), _jsx(ValidatorSelector, { validators: validators, validator: validator, select: this.onValidatorSelected }), _jsx("button", { title: "Click me to submit the form programmatically.", className: "btn btn-default", type: "button", onClick: () => this.playGroundForm.current.submit(), children: "Prog. Submit" }), _jsx("span", { children: " " }), _jsx("button", { title: "Click me to validate the form programmatically.", className: "btn btn-default", type: "button", onClick: () => { const valid = this.playGroundForm.current.validateForm(); alert(valid ? "Form is valid" : "Form has errors"); }, children: "Prog. Validate" }), _jsx("div", { style: { marginTop: "5px" } }), _jsx(CopyLink, { shareURL: this.state.shareURL, onShare: this.onShare })] }), _jsx("div", { className: "col-sm-2", children: _jsx(RawValidatorTest, { validator: validators[validator], schema: schema, formData: formData }) })] })] }), _jsxs("div", { className: "col-sm-7", children: [_jsx(Editor, { title: "JSONSchema", code: toJson(schema), onChange: this.onSchemaEdited }), _jsxs("div", { className: "row", children: [_jsx("div", { className: "col-sm-6", children: _jsx(Editor, { title: "UISchema", code: toJson(uiSchema), onChange: this.onUISchemaEdited }) }), _jsx("div", { className: "col-sm-6", children: _jsx(Editor, { title: "formData", code: toJson(formData), onChange: this.onFormDataEdited }) })] }), extraErrors && (_jsx("div", { className: "row", children: _jsx("div", { className: "col", children: _jsx(Editor, { title: "extraErrors", code: toJson(extraErrors || {}), onChange: this.onExtraErrorsEdited }) }) }))] }), _jsx("div", { className: "col-sm-5 py-4", style: (subtheme && subtheme.startsWith("dark")) ? { backgroundColor: "#141c24" } : {}, children: _jsx(ErrorBoundary, { children: this.state.form && (_jsx(DemoFrame, { head: _jsx(React.Fragment, { children: _jsx("link", { rel: "stylesheet", id: "theme", href: this.state.stylesheet || "" }) }), style: { width: "100%", height: 1000, border: 0, }, subtheme: subtheme, children: _jsx(FormComponent, { ...templateProps, ...liveSettings, schema: schema, uiSchema: uiSchema, formData: formData, onChange: this.onFormDataChange, noHtml5Validate: true, onSubmit: ({ formData }, e) => { console.log("submitted formData", formData); console.log("submit event", e); window.alert("Form submitted"); }, fields: { geo: GeoPosition }, customValidate: validate, validator: validators[validator], onBlur: (id, value) => console.log(`Touched ${id} with value ${value}`), onFocus: (id, value) => console.log(`Focused ${id} with value ${value}`), transformErrors: transformErrors, onError: log("errors"), ref: this.playGroundForm }) })) }) }), _jsx("div", { className: "col-sm-12", children: _jsxs("p", { style: { textAlign: "center" }, children: ["Powered by", " ", _jsx("a", { href: "https://github.com/rjsf-team/react-jsonschema-form", children: "react-jsonschema-form" }), ".", import.meta.env.VITE_SHOW_NETLIFY_BADGE === "true" && (_jsx("div", { style: { float: "right" }, children: _jsx("a", { href: "https://www.netlify.com", children: _jsx("img", { src: "https://www.netlify.com/img/global/badges/netlify-color-accent.svg" }) }) }))] }) })] })); } } export default Playground;