@datalayer/primer-rjsf
Version:
React JSON Schema Form (RJSF) for Primer
311 lines (310 loc) • 18.4 kB
JavaScript
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;