├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── babel.config.js ├── bundle.css ├── example ├── .env ├── .gitignore ├── README.md ├── example-schemas │ ├── oneOf.json │ ├── pipeline.json │ └── recursive.json ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── App.css │ ├── App.js │ ├── ErrorBoundary.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── schema-form.css │ └── serviceWorker.js └── yarn.lock ├── jest.config.js ├── package-lock.json ├── package.json ├── restspace-schema-form-1.0.12.tgz ├── rollup.config.js ├── src ├── components │ ├── component-for-type.tsx │ ├── schema-form-array.tsx │ ├── schema-form-component.tsx │ ├── schema-form-interfaces.ts │ ├── schema-form-object.tsx │ ├── schema-form-value-context.ts │ ├── schema-form.tsx │ ├── schema-paged-form.tsx │ └── schema-submit-form.tsx ├── css │ ├── default-skin.css │ ├── default-skin.css.map │ ├── layout.css │ └── layout.css.map ├── declaration.d.ts ├── editors │ ├── link.svg │ ├── multi-select-buttons-editor.tsx │ ├── oneOf-radio-editor.tsx │ ├── radio-buttons-editor.tsx │ ├── upload-editor.tsx │ └── upload.svg ├── error.ts ├── index.tsx ├── schema │ ├── schema.test.ts │ ├── schema.ts │ └── schemaContext.ts ├── scss │ ├── default-skin.scss │ └── layout.scss ├── utility.test.ts └── utility.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /lib 3 | /build 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest All", 11 | "program": "${workspaceFolder}/node_modules/.bin/jest", 12 | "args": ["--runInBand"], 13 | "console": "integratedTerminal", 14 | "internalConsoleOptions": "neverOpen", 15 | "disableOptimisticBPs": true, 16 | "windows": { 17 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 18 | } 19 | }, 20 | { 21 | "type": "node", 22 | "request": "launch", 23 | "name": "Jest Current File", 24 | "program": "${workspaceFolder}/node_modules/.bin/jest", 25 | "args": [ 26 | "${fileBasenameNoExtension}", 27 | "--config", 28 | "jest.config.js" 29 | ], 30 | "console": "integratedTerminal", 31 | "internalConsoleOptions": "neverOpen", 32 | "disableOptimisticBPs": true, 33 | "windows": { 34 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 35 | } 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorTheme": "Monokai" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 restspace 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![license](https://img.shields.io/github/license/restspace/schema-form) 2 | ![issues](https://img.shields.io/github/issues/restspace/schema-form) 3 | # React JSON Schema Form Generator 4 | ![schema form animation](https://restspace.io/img/json/blog/this%20react%20component%20turns%20a%20headless%20cms%20into%20a%20form%20builder.json/blocks/0/image/schemaformdemo.gif) 5 | ## Introduction 6 | This tool is designed to drastically reduce the effort needed to create complex forms in React. It uses JSON schema as a description language for the form you want to create. It is highly flexible and can deal with pretty much any requirement 7 | through the power of JSON schema or customisations. A summary of its features is as follows: 8 | - without customisation, covers form fields of types text, single checkbox, number, currency, date, date-time, 9 | email, password, hidden, textarea, select, multibutton, multicheckbox, radiobuttons, radio checkboxes, file upload 10 | - full heirarchical subforms and arrays of fields or arrays of subforms. Arrays have add, reorder, duplicate, delete functions. Heirarchy can be recursive. 11 | - conditional fields e.g. radio buttons and if you select 'other' a textbox appears 12 | - heterogeneous arrays e.g. an array of individuals and companies, with two different forms and a selector 13 | - JSON schema based validation of all fields 14 | - form value returned as a JSON object 15 | - custom field components, full event model 16 | 17 | Schema form is a key part of the [Restspace](https://restspace.io) project. Restspace defines its data store structure using JSON Schema files which are available over the web, allowing for a single source of truth for structure for data input and data storage. It's available as an NPM package @restspace/schema-form. You can find a demo playground here: [https://restspace.io/react/schema-form/demo](https://restspace.io/react/schema-form/demo). 18 | 19 | ## Using the components 20 | The simplest component is SchemaForm which is a controlled component which has a schema and a value that matches the schema passed 21 | in as props. It has no submit logic, and simply updates the current value via a changed event. 22 | 23 | import SchemaForm from "schema-form"; 24 | 36 | 37 | Then you can use SchemaSubmitForm. This has a submit button and an onSubmit event plus it can suppress error messages until the first submit. 38 | 39 | import { SchemaSubmitForm } from "schema-form"; 40 | ( 55 |
Submit
56 | )} /> 57 | 58 | The final component is designed to support paged forms, this is SchemaPagedForm. 59 | 60 | import { SchemaPagedForm } from "schema-form"; 61 | ( 77 |
onClick(previousPage)}>Previous
78 | )} 79 | makeNextLink={(nextPage, onClick) => ( 80 |
onClick(nextPage)}>Next
81 | )} 82 | makeSubmitLink={(onClick) => ( 83 |
onClick()}>Submit
84 | )} /> 85 | 86 | An explanation of each of the props follows: 87 | 88 | | Prop | Type | Description | 89 | |------|------|-------------| 90 | |schema|object|This is the JSON Schema which defines the form's structure and validation. It can optionally be an array of schemas, in which case the first is the base schema actually used and the others provide schemas to which it can refer using $ref references.| 91 | |value|object|This is the current value for the form fields to display. This can be used to make the form controlled| 92 | |page|number|On the paged form, the current page number being shown (starts at 0)| 93 | |onChange|function|This function is called when the form fields' value is displayed. It's called with parameters (*value*, *path*, *errors*, *action*). *value* is an object representing the value of all the fields in the form. *path* is a list of property name strings indicating the path to the property which just changed. *errors* is an ErrorObject describing any current errors. *action* is an enum number indicating what action was just taken in terms mainly of array manipulation.| 94 | |changeOnBlur|boolean|This prop determines whether the onChange event is called whenever a form value changes (the default) or only when the focus moves away from a changed form field.| 95 | |onFocus|function|This function is called when focus moves to a new form field. The parameter is (*path*). *path* is a list of property name strings indicating the path to the property of the field which just gained focus.| 96 | |onBlur|function|This function is called when focus moves away from a form field. The parameter is (*path*). *path* is a list of property name strings indicating the path to the property of the field which just lost focus.| 97 | |onEditor|function|This function is called when a custom editor needs to raise an event. The parameters are (*data*, *path*). *data* is a custom data object depending on the editor, *path* is a list of property name strings indicating the path to the property of the field which has the custom editor.| 98 | |onSubmit|function|This function is called when a submit or paged form is successfully submitted. The parameter is (*value*). *value* is the object representing the value of the form fields when submitted.| 99 | |onSubmitError|function|This function is called when a submit or paged form is submitted but has an error. The parameters are (*value*, *error*). *value* is an object representing the current value of the form fields. *error* is an ErrorObject describing the current errors.| 100 | |onDirty|function|This function is called when the 'dirty' status of a form changes. A form is not dirty initially and when just submitted. It becomes dirty the first time a field value is changed. The parameter is (*dirty*). *dirty* is a boolean indicating whether the form is now dirty or not.| 101 | |onPage|function|This function is called when a paged form changes page. The parameters are (*value*, *page*, *previousPage*). *value* is an object representing the value of all the form fields on all the pages. *page* is the new page number. *previousPage* is the previous page number.| 102 | |showErrors|boolean|This property only has an effect on SchemaForm. The SchemaForm only renders validation errors on the form if it is true.| 103 | |className|string|Appends the class or classes in this string to the classes on the SchemaForm wrapper div.| 104 | |collapsible|boolean|Whether the form renders collapsers for form sections (i.e. sub arrays or objects). Default is true.| 105 | |componentContext|object|Used to communicate data into custom editor components| 106 | |components|object|Property key values - the key is the name of an editor type, the value is the editor component type used. This is used to override the default map of editor types to editor components. The editor type comes from the schema, in priority order the *editor* property, the *format* property or the *type* property. If the *type* property is 'array' or 'object', the editor is found in the *containers* prop below.| 107 | |containers|object|Property key values - the key is the name of a container type, the value is the editor component type used. The container type comes from the schema. Containers have 'array' or 'object' types only. In priority order, the *editor* property then the *type* property is used.| 108 | |makeSubmitLink|function|A render prop which returns an element which lets the user tell the form to submit. The parameter is (*onClick*). *onClick* is a function the rendered element must call when the user has requested to submit.| 109 | |makePreviousLink|function|A render prop which returns an element which lets the user tell the paged form to return to the previous page. The parameters is (*previousPage*, *onClick*). *previousPage* is the page number (starting 0) of the previous page. *onClick* is a function the rendered element must call when the user has requested to go to the previous page. It has one argument which is the number of the page to which to go.| 110 | |makeNextLink|function|A render prop which returns an element which lets the user tell the paged form to go to the next page. The parameters is (*nextPage*, *onClick*). *nextPage* is the page number (starting 0) of the next page. *onClick* is a function the rendered element must call when the user has requested to go to the next page. It has one argument which is the number of the page to which to go.| 111 | 112 | ## Styling the components 113 | 114 | The package comes with a base CSS file which sets up expected layout. This can then be overriden with style customisations. 115 | 116 | import "@restspace/schema-form/build/index.css"; 117 | 118 | ## JSON schema 119 | JSON Schema is a (provisional but widely used) web standard defining a system for describing validity conditions on a JSON object. It is described here: [https://json-schema.org/](https://json-schema.org/). 120 | 121 | JSON schema is an extremely powerful descriptive language and this package while not yet implementing every feature of JSON schema includes the most powerful ones: 122 | - list of different fields of all standard form field types plus others 123 | - forms with arbitrary nested subsections 124 | - forms with arbitrary nested lists (with ability to create, delete and reorder items) 125 | - powerful conditionality system allowing for fields or sections to appear conditional on other values, lists of different kinds of subsections etc. 126 | - complex validation rules 127 | 128 | ## Supported JSON Schema features 129 | ### Types 130 | | Type | Implementation | 131 | |---------|----------------| 132 | | type: string | Generally implemented as an input type="text" | 133 | | type: number | By default a text input field type="number" | 134 | | type: boolean | By default a checkbox input | 135 | | type: object | The top level field will generally be an object: below this an object is a subsection on the form and a subobject in the Javascript object value | 136 | | type: array | An array is by default shown as a user-manageable list of fields or subsections in the form, and corresponds to a Javascript array in the object value | 137 | | type: null | This field won't be shown on the form | 138 | | type: [ multiple types ] | This is not supported | 139 | ### Type specific keywords 140 | | Type: Keyword | Implementation | 141 | |---------|----------------| 142 | | string: length | Implemented only as validation | 143 | | string: pattern | Implemented only as validation | 144 | | string: format | Supported formats are described below | 145 | | number: multipleOf | Implemented only as validation | 146 | | number: minimum | Implemented only as validation | 147 | | number: exclusiveMinimum | Implemented only as validation | 148 | | number: maximum | Implemented only as validation | 149 | | number: exclusiveMaximum | Implemented only as validation | 150 | | object: properties | Every property is rendered as a field or form subsection | 151 | | object: required | Implemented as validation | 152 | | object: propertyNames | Implemented as validation | 153 | | object: minProperties | Implemented as validation | 154 | | object: maxProperties | Implemented as validation | 155 | | object: dependencies | Not supported | 156 | | object: patternProperties | Not supported | 157 | | object: propertyOrder | NON-STANDARD. This custom keyword should be followed by a list of property names as strings or sublists of the same. It has no effect on validation. It allows specification of the order in which properties are displayed in the data entry form. See below for how orders are merged with combined schemas. Sublists are rendered contained within an outer div to allow more control over layout. | 158 | | object: currencySymbol | NON-STANDARD. This custom keyword can only be present on the top-level schema object. It determines the string used as a currency prefix for the currency editor. | 159 | | object: additionalProperties | Assumed to be true when validating, and false when constructing a UI. See below for more. This means that additionalProperties: false in a schema is not supported. | 160 | | array: items | This schema is used to render each subsection of the array. Tuple validation (where items is a list of schemas) is not supported. | 161 | | array: contains | Not supported except for validation | 162 | | array: additionalItems | Not supported | 163 | | array: maxItems | Implemented only as validation | 164 | | array: minItems | Implemented only as validation | 165 | | array: uniqueItems | Implemented as validation | 166 | | generic: title | The label of a field uses title for preference, otherwise it defaults to convering camel case into separated words e.g. initialRepeatingCost -> Initial repeating cost | 167 | | generic: description | Description is rendered as part of the label and can be styled differently | 168 | | generic: default | Not supported. Normally a default would be set on the initial value object passed to the form. | 169 | | generic: enum | A string or number-typed schema with an enum property will be rendered by default as a drop-down list i.e. an HTML select. The custom property 'enumText' as a sibling of 'enum' is an array of equal length giving select display text corresponding to the value in the 'enum' array. | 170 | | generic: readOnly | If true, the value should not be editable just displayed | 171 | | generic: const | Only implemented as validation: for use in conditionals only. | 172 | | generic: editor | NON-STANDARD. This custom keyword which has no effect on validation allows definition of an editor to be used to render this schema. See Built-in editors below for a list of possibilities. | 173 | | combiner: allOf | A conjoin (see below) is done between the list of subschemas given and the main schema. Validation is also applied | 174 | | combiner: anyOf | A disjoin (see below) is done between the list of subschemas given filtered to those which when conjoined to the parent schema validate against the current entered values. This is then conjoined with the main schema. Validation is also applied | 175 | | combiner: oneOf | This is rendered as a selector which allows you to choose one of the subschemas which is then rendered as a subform | 176 | | combiner: not | Only supported as validation | 177 | | conditional: if then else | Supported by validating the current values against the 'if' schema, then conjoining the main schema with the 'then' clause if the validate, or else the 'else' clause if they don't | 178 | | references: $ref | Supported: can be used to refer to a schema by its $id value: this also works for a schema to refer to itself recursively. For an example see [recursive.json](https://github.com/restspace/schema-form/blob/master/example/example-schemas/recursive.json). Also supports refering to other subschemas by path within the main schema, see [oneOf.json](https://github.com/restspace/schema-form/blob/master/example/example-schemas/oneOf.json). Won't fetch schemas by url but external schemas can be listed in the object array version of the *schema* prop. | 179 | | references: $id | Supported, but only at the top level of a schema, nested subschemas have to be referred to by path | 180 | 181 | ### Rendering based on combiners and conditionals 182 | Combiners and conditionals are a powerful means for creating conditional forms, 183 | however it is conceptually difficult to understand how they should be used to build a UI. 184 | 185 | Schema-form's rationale for how it renders a data entry UI for a JSON schema is based on providing a UI that can be used to enter AT LEAST all the possible allowed values the JSON schema might validate. With arrays and primitives, it is pretty clear what these allowed values are. With an object, there is an ambiguity about the semantics of the properties. This is expressed by the 'additionalProperties' keyword, which says whether it is allowed for properties not listed to validate or whether they are forbidden. 186 | 187 | 'additionalProperties' is assumed to be true when validating. Otherwise there is no way a conditional schema could add properties to the base schema. Given this, conjoining two sets of properties results in a properties object containing the union of the properties, and where the property exists in both sets, the subschema of that property is the conjunction of the subschemas in the two sets. 188 | 189 | 'additionalProperties' is however taken as if it were false when constructing a UI, i.e. it is assumed that the user will not be able to add data for any additional properties. 190 | 191 | This inconsistency is required to cross the bridge between a constraint validation language where each element in the language restricts the output to a UI language where each element of the UI extends the output. 192 | 193 | The result of adding an 'allOf' property to a schema is straightforward according to this method, you simply conjoin all the schema constraints with the parent schema and render the UI which results. 194 | 195 | However for the 'anyOf' property a problem arises as you have now created a number of disjoint possibilities where you may have a property in one and not in another. This requires the UI to take on different states corresponding to the 'anyOf' subschemas. To choose which state(s) to use, schema-form looks at the current value and uses it to rule out any subschemas which do not currently validate. If used carefully, 'anyOf' can then be used to produce a usable conditional form: however it is easy to create a situation where the form contains current values which mean it cannot be transitioned to allow data entry which would validate to another 'anyOf' subschema. 196 | 197 | Another choice ideal for creating a heterogeneous list is 'oneOf'. This is rendered as a selector control (whose value is not returned as part of the 198 | 199 | Similarly, the 'if', 'then', 'else' keywords are used on the basis that the UI to be shown is the one conjoining the subschema on the 'then' keyword if the 'if' subschema currently validates, or the 'else' keyword if not. This is a little less likely to result in an unusable form and works better for conditional form generation than 'anyOf'. Indeed an ideal structure for a form with many conditional elements is 'allOf' with a number if 'if' 'then' 'else' subschemas. 200 | 201 | We may include an optional feature where the 'anyOf' property gives rise to a UI element allowing manual selection of which 'anyOf' clause the user wishes to satisfy. This UI element would have no effect on the output value. 202 | 203 | ## Built-in and Custom editors 204 | ### Built-in editors 205 | schema-form has default editors for all schemas it supports. However a number of obvious alternatives are included as built-in options that can be made use of via the custom 'editor' schema property. 206 | 207 | Here are the built-in editors: 208 | | Keyword | Valid for Type | Description | 209 | |---------|----------------|-------------| 210 | | textarea | string | Renders a textarea HTML tag rather than an input for text entry | 211 | | currency | number | Renders a text entry box with an automatic currency label (set with the currencySymbol property on the top-level schema object). Supplies a decimal number to the output value. | 212 | | hidden | string | Renders a type='hidden' text box | 213 | | multiCheck | array of enums | Renders a multi-select set of check boxes which supplies an array of values of an enum whose values correspond to the check box labels | 214 | | upload | string | Renders a file upload. The behaviour of the file upload is determined a property on the object passed in to the optional componentContext prop. 215 | 216 | { 217 | uploadEditor: { 218 | getFileUrl: (file: File, filePath: string[], schema: object) => string, 219 | sendFile: (url: string, file: File, progress: (percent: number) => void) => Promise, 220 | saveSiteRelative: boolean 221 | } 222 | } 223 | 224 | getFileUrl takes the file object, the property path describing the position of the uploader in the schema, and the schema itself and returns a url to where the file should be sent. sendFile actually sends the file to that url, taking the url to send to, the file object, and a callback to be called within the body of the sendFile function to update progress. saveSiteRelative indicates whether the url saved in the field is site relative or absolute. 225 | 226 | A default function value for 'sendFile' is provided as a named export that can be imported from the schema-form module: 227 | 228 | import { sendFileAsBody } from '@lynicon/schema-form'; 229 | 230 | This posts the file as the HTTP request body. 231 | 232 | The file uploader uploads a file dropped onto its rendered area or one chosen with the file chooser. It does this immediately before the form is submitted. It shows upload progress. When the file is uploaded, the value of the field is set to the url where the file has been saved and from where it can be retrieved. 233 | ### Custom editors 234 | Custom editors can be built as single React components according to certain rules and these components passed in via the *components* or *containers* props (see above for these props' details). 235 | 236 | Examples can be seen in this repo under [editors](https://github.com/restspace/schema-form/tree/master/src/editors). 237 | 238 | Editor components take a props as defined in ISchemaComponentProps (single value editors) or ISchemaContainerProps (array or object value editors) - see (schema-form-interfaces.ts)[https://github.com/restspace/schema-form/blob/master/src/components/schema-form-interfaces.ts]. 239 | 240 | Generally speaking, the editor will set render itself for the value in the *value* prop. On interaction which updates the value, it reports this by dispatching an update to a context. The context is called ValueDispatch. It's reducer-style, and updates are sent to it via actions which are created using methods on ValueAction. 241 | 242 | (radio-buttons-editor.tsx)[https://github.com/restspace/schema-form/blob/master/src/editors/radio-buttons-editor.tsx] provides a good basic example for a component. Notice it wraps the rendered output in SchemaFormComponentWrapper to which all the properties are passed. This allows for standardised field rendering. 243 | 244 | (oneOf-radio-editor.tsx)[https://github.com/restspace/schema-form/blob/master/src/editors/oneOf-radio-editor.tsx] is a good container example. It recurses into its children objects using ComponentForType to render them, passing through the relevant sub value (in this case actually the original value) and sub schema. 245 | 246 | 247 | 248 | 249 | 250 | 251 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | module.exports = { 3 | presets: [ 4 | [ 5 | '@babel/preset-env', 6 | { 7 | targets: { 8 | node: 'current', 9 | }, 10 | }, 11 | ], 12 | '@babel/preset-typescript', 13 | ] 14 | }; -------------------------------------------------------------------------------- /bundle.css: -------------------------------------------------------------------------------- 1 | form { 2 | background-color: blue; 3 | } -------------------------------------------------------------------------------- /example/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /example/example-schemas/oneOf.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "$id": "http://schema-form.org/test", 4 | "definitions": { 5 | "item2": { 6 | "type": "number", 7 | "className": "item2-class" 8 | } 9 | }, 10 | "properties": { 11 | "things": { 12 | "type": "array", 13 | "items": { 14 | "type": [ 15 | "string", 16 | "number" 17 | ], 18 | "oneOf": [ 19 | { 20 | "type": "string" 21 | }, 22 | { 23 | "$ref": "#/definitions/item2" 24 | } 25 | ], 26 | "editor": "oneOfRadio" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /example/example-schemas/pipeline.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "properties": { 4 | "pipeline": { 5 | "$ref": "#/definitions/pipeline" 6 | }, 7 | "onlyLowerServices": { 8 | "type": "boolean" 9 | } 10 | }, 11 | "required": [ 12 | "pipeline" 13 | ], 14 | "definitions": { 15 | "pipeline": { 16 | "type": "array", 17 | "items": { 18 | "type": [ 19 | "string", 20 | "array" 21 | ], 22 | "oneOf": [ 23 | { 24 | "type": "string" 25 | }, 26 | { 27 | "$ref": "#/definitions/pipeline" 28 | } 29 | ], 30 | "editor": "oneOfRadio" 31 | } 32 | } 33 | }, 34 | "$id": "http://restspace.io/services/pipeline" 35 | } -------------------------------------------------------------------------------- /example/example-schemas/recursive.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "object", 3 | "$id": "http://schema-form.org/test", 4 | "definitions": { 5 | "item2": { 6 | "type": "number", 7 | "className": "item2-class" 8 | }, 9 | "test": { 10 | "type": "object", 11 | "properties": { 12 | "item1": { 13 | "type": "string", 14 | "const": "x" 15 | } 16 | }, 17 | "required": [ 18 | "item1" 19 | ] 20 | } 21 | }, 22 | "properties": { 23 | "item1": { 24 | "type": "string" 25 | }, 26 | "item2": { 27 | "$ref": "#/definitions/item2" 28 | } 29 | }, 30 | "allOf": [ 31 | { 32 | "if": { 33 | "$ref": "#/definitions/test" 34 | }, 35 | "then": { 36 | "type": "object", 37 | "properties": { 38 | "item4": { 39 | "$ref": "http://schema-form.org/test" 40 | } 41 | } 42 | } 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@reach/router": "^1.2.1", 7 | "@types/reach__router": "^1.2.4", 8 | "react": "^16.8.6", 9 | "react-dom": "^16.8.6", 10 | "react-scripts": "3.0.1" 11 | }, 12 | "scripts": { 13 | "start": "react-scripts start", 14 | "build": "react-scripts build", 15 | "test": "react-scripts test", 16 | "eject": "react-scripts eject" 17 | }, 18 | "eslintConfig": { 19 | "extends": "react-app" 20 | }, 21 | "browserslist": { 22 | "production": [ 23 | ">0.2%", 24 | "not dead", 25 | "not op_mini all" 26 | ], 27 | "development": [ 28 | "last 1 chrome version", 29 | "last 1 firefox version", 30 | "last 1 safari version" 31 | ] 32 | }, 33 | "devDependencies": { 34 | "@babel/core": "^7.5.5", 35 | "@babel/preset-env": "^7.5.5", 36 | "@babel/preset-typescript": "^7.3.3", 37 | "babel-jest": "^24.9.0", 38 | "jest": "^24.9.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/restspace/schema-form/814d6726a55b300467b708fdc701e34b4f812966/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 |
28 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /example/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /example/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | } 3 | 4 | .container { 5 | display: flex; 6 | } 7 | 8 | .schema-input { 9 | width: 33%; 10 | height: 80vh; 11 | border: 1px solid black; 12 | } 13 | 14 | .schema-input.invalid { 15 | color: darkred; 16 | } 17 | 18 | .container .sf-form { 19 | width: 33%; 20 | height: 80vh; 21 | } 22 | 23 | .value-output { 24 | width: 33%; 25 | height: 80vh; 26 | border: 1px solid black; 27 | } -------------------------------------------------------------------------------- /example/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, useState, useCallback, useMemo } from "react"; 2 | import { Router, Link } from "@reach/router"; 3 | import "./App.css"; 4 | import "schema-form/build/index.css"; 5 | import SchemaForm, { SchemaSubmitForm, SchemaPagedForm, sendFileAsBody } from "@restspace/schema-form"; 6 | import { ErrorBoundary } from "./ErrorBoundary"; 7 | 8 | const loginSchema = { 9 | type: "object", 10 | title: "Log In", 11 | properties: { 12 | email_x: { 13 | type: "string" 14 | }, 15 | password_x: { 16 | type: "string" 17 | } 18 | }, 19 | required: [ "email_x", "password_x" ] 20 | } 21 | 22 | const schema = { 23 | type: "object", 24 | properties: { 25 | salutation: { type: "string", enum: ['Mr', 'Mrs', 'Ms', 'Dr'] }, 26 | firstName: { type: "string", maxLength: 10 }, 27 | lastName: { type: "string", readOnly: true }, 28 | canContact: { type: "string", enum: [ "yes", "no" ], editor: "radioButtons", description: "Whether can contact" }, 29 | preferredContact: { type: "array", items: { 30 | type: "string", enum: [ "phone", "email", "text" ] 31 | }, 32 | editor: "multiCheck" 33 | }, 34 | dateOfBirth: { type: "string", format: "date" }, 35 | password: { type: "string", format: "password" }, 36 | comments: { type: "string", editor: "textarea" }, 37 | files: { type: "string", editor: "upload" }, 38 | things: { type: "array", items: { 39 | type: "object", properties: { 40 | first: { type: "string" }, 41 | second: { type: "string" } 42 | } } 43 | }, 44 | address: { type: "object", properties: { 45 | addressLine: { type: "string" }, 46 | postcode: { type: "string" } 47 | }, 48 | required: [ "postcode" ] 49 | } 50 | }, 51 | propertyOrder: [ "salutation", [ "firstName", "lastName" ], "canContact", "preferredContact", "dateOfBirth", "password", "comments", "files", "things", "address" ], 52 | if: { 53 | type: "object", properties: { 54 | salutation: { type: "string", const: "Dr" } 55 | } 56 | }, 57 | then: { 58 | type: "object", properties: { 59 | isMedical: { type: "boolean" } 60 | }, 61 | propertyOrder: [ "canContact", "isMedical" ] 62 | } 63 | } 64 | 65 | const schemaSelector = { 66 | type: "object", 67 | properties: { 68 | selector: { 69 | type: "string", 70 | enum: [ "text", "checkbox", "nothing" ] 71 | } 72 | }, 73 | anyOf: [ 74 | { 75 | type: "object", 76 | properties: { 77 | selector: { type: "string", const: "text" }, 78 | textInput: { type: "string" } 79 | } 80 | },{ 81 | type: "object", 82 | properties: { 83 | selector: { type: "string", const: "checkbox" }, 84 | checkboxInput: { type: "boolean" } 85 | } 86 | },{ 87 | type: "object", 88 | properties: { 89 | selector: { type: "string", const: "nothing" } 90 | } 91 | } 92 | ] 93 | } 94 | 95 | const schemaSelector2 = { 96 | type: "object", 97 | anyOf: [ 98 | { 99 | type: "object", 100 | properties: { 101 | number: { type: "string", pattern: "[0-9]+" }, 102 | sign: { type: "boolean" } 103 | }, 104 | required: [ "number" ] 105 | },{ 106 | type: "object", 107 | properties: { 108 | number: { type: "string", not: { pattern: "[0-9]+" } } 109 | } 110 | } 111 | ] 112 | } 113 | 114 | const schemaPaged = { 115 | type: "object", 116 | properties: { 117 | page0: { 118 | type: "object", 119 | properties: { 120 | salutation: { 121 | type: "string", 122 | enum: ['Mr', 'Mrs', 'Ms', 'Dr'] 123 | }, 124 | firstName: { 125 | type: "string", 126 | maxLength: 10 127 | }, 128 | lastName: { 129 | type: "string", 130 | readOnly: true 131 | } 132 | } 133 | }, 134 | page1: { 135 | type: "object", 136 | properties: { 137 | abc: { 138 | type: "number" 139 | }, 140 | def: { 141 | type: "number" 142 | } 143 | } 144 | } 145 | } 146 | } 147 | 148 | const testValue = { 149 | salutation: "Dr", 150 | firstName: "John", 151 | lastName: "Smith", 152 | canContact: false, 153 | password: "abc", 154 | things: [ { first: "thing1", second: "thing2" } ], 155 | address: { 156 | addressLine: "13 Rose St", 157 | postcode: "" 158 | } 159 | } 160 | 161 | const testValuePaged = { 162 | page0: { 163 | salutation: "Dr", 164 | firstName: "John", 165 | lastName: "Smith" 166 | }, 167 | page1: { 168 | abc: "1", 169 | def: "2" 170 | } 171 | } 172 | 173 | function Playground() { 174 | let baseSchema = null; 175 | const baseSchemaInput = localStorage.getItem('schema'); 176 | try { 177 | baseSchema = JSON.parse(baseSchemaInput); 178 | } catch (er) { } 179 | if (!baseSchema) { 180 | baseSchema = { 181 | type: "object", 182 | properties: { 183 | item1: { type: "string" } 184 | } 185 | }; 186 | } 187 | 188 | const [schemaInput, setSchemaInput] = useState(JSON.stringify(baseSchema, null, 2)); 189 | const [schema, setSchema] = useState(baseSchema); 190 | const [value, setValue] = useState({}); 191 | const [isValid, setIsValid] = useState(true); 192 | 193 | const onChange = (e) => { 194 | const newSchemaInput = e.target.value; 195 | setSchemaInput(newSchemaInput); 196 | try { 197 | const newSchema = JSON.parse(newSchemaInput); 198 | setSchema(newSchema); 199 | setIsValid(true); 200 | localStorage.setItem('schema', newSchemaInput); 201 | } catch(e) { 202 | setIsValid(false); 203 | } 204 | } 205 | 206 | const valueChange = (v) => setValue(v); 207 | 208 | return ( 209 |
210 |