29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-json-form
2 |
3 | React Component for editing JSON data using form inputs.
4 |
5 | [][npm]
6 |
7 | ### Live demo
8 |
9 | https://bhch.github.io/react-json-form/playground/
10 |
11 | ### Documentation
12 |
13 | https://bhch.github.io/react-json-form/
14 |
15 | ### ⚠️ Important notes
16 |
17 | 1. **Consider this library as a work-in-progress** (at least until version 3 which will be a more stable release).
18 | 2. Currently, this library doesn't provide default CSS styles. So, you're required to write
19 | your own CSS styles. You can also copy the styles from the demo page (see [`docs.css` after `Line 433`](https://github.com/bhch/react-json-form/blob/master/docs/static/css/docs.css#L433)) as a starting point.
20 | 3. Be prepared for breaking changes regarding UI structure and CSS class names. Currently, CSS class names don't
21 | follow a particular naming standard. But this will change in v3.
22 | 4. Support for UI themes will be added soon.
23 |
24 | [npm]: https://www.npmjs.com/package/@bhch/react-json-form/
25 |
--------------------------------------------------------------------------------
/docs/docs/schema.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page.html
3 | title: Schema
4 | ---
5 |
6 |
7 |
Note
8 |
This document will be completed in future.
9 |
10 | Meanwhile, all the following things have been documented in React JSON Form's sister
11 | project: django-jsonform.
12 |
80 |
81 |
95 |
96 | {% if project.node_env == 'production' %}
97 |
98 |
99 |
106 | {% endif %}
107 |
108 | {% for link in scripts -%}
109 |
110 | {% endfor -%}
111 |
112 |
113 |
--------------------------------------------------------------------------------
/docs/docs/usage/browser.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page.html
3 | title: Using in Browser
4 | ---
5 |
6 | In the browser, the library will be available under `reactJsonForm` variable.
7 |
8 | ## Creating the form
9 |
10 | Use the [`reactJsonForm.createForm()`](#reactjsonform.createform(config)) function to
11 | create the form from your schema.
12 |
13 | You'll also need to have a `textarea` where the form will save the data.
14 |
15 | ```html
16 |
17 |
18 |
19 |
20 |
23 |
24 |
25 |
40 | ```
41 |
42 |
43 | ## Handling events
44 |
45 | ```js
46 | form.addEventListener('change', function(e) {
47 | // ...
48 | });
49 | ```
50 |
51 | See [`addEventListener()`](#forminstance.addeventlistener(event%2C-callback)) section for
52 | further details about handling events.
53 |
54 |
55 | ## Data validation
56 |
57 | *New in version 2.1*
58 |
59 | The form component provides basic data validation.
60 |
61 | Usage example:
62 |
63 | ```js
64 | var validation = form.validate();
65 |
66 | var isValid = validation.isValid; // it will be false if data is not valid
67 |
68 | var errorMap = validation.errorMap; // object containing error messages
69 |
70 | if (!isValid) {
71 | // notify user
72 | alert('Please correct the errors');
73 |
74 | // update the form component
75 | // it will display error messages below each input
76 | form.update({errorMap: errorMap});
77 | }
78 |
79 | ```
80 |
81 | You can adopt the above code example to validate the data before a form is submitted.
82 |
83 | You can also implement custom validation instead of calling `.validate()`. In that
84 | case, you'll have to manually create an [`errorMap` object]({{ '/docs/usage/node/#data-validation' | url }})
85 | for displaying error messages.
86 |
87 | ## API reference
88 |
89 | ### Library functions
90 |
91 | ##### `reactJsonForm.createForm(config)`
92 |
93 | Function used for creating the form UI. The `config` parameter is an object
94 | which may contain these keys:
95 |
96 | - `containerId`: The HTML ID of the element in which the form should be rendered.
97 | - `dataInputId`: The HTML ID of the textarea element in which the form data should be kept.
98 | - `schema`: The schema of the form.
99 | - `data` *(Optional)*: The initial data of the form.
100 | - `fileHandler` *(Optional)*: URL for the common file handler endpoint for all file fields.
101 | - `fileHandlerArgs` *(Optional)*: Key-value pairs which will be sent via querystring to the `fileHandler` URL.
102 | - `errorMap` *(Optional)*: An object containing error messages for fields.
103 | - `readonly` *(Optional)*: A boolean. If `true`, the whole form will be read-only.
104 |
105 | *Changed in version 2.1*: `errorMap` option was added.
106 | *Changed in version 2.2*: `fileHandlerArgs` option was added.
107 | *Changed in version 2.10*: `readonly` option was added.
108 |
109 |
110 | ##### `reactJsonForm.getFormInstance(containerId)`
111 |
112 | Call this function to get a previously created form instance.
113 |
114 | If you've earlier created an instance in a scoped function, then to get
115 | the form instance in another scope, this function can be helpful.
116 |
117 | This helps you avoid keeping track of the form instances yourself.
118 |
119 | ```js
120 | var form = reactJsonForm.getFormInstance('formContainer');
121 | ```
122 |
123 | ### Form instance
124 |
125 | The following methods, attributes & events are available on a form instance.
126 |
127 | #### Methods
128 |
129 | ##### `formInstance.addEventListener(event, callback)`
130 |
131 | Register a callback for a given event ([see available events](#events)).
132 |
133 | ##### `formInstance.render()`
134 |
135 | Function to render the form.
136 |
137 | ##### `formInstance.update(config)`
138 |
139 | Re-render the form with the given `config`.
140 |
141 |
142 | ##### `formInstance.validate()`
143 |
144 | *New in version 2.1*
145 |
146 | Validates the current data against the instance's schema.
147 |
148 | Returns a *validation* object with following keys:
149 |
150 | - `isValid`: It will be `true` if data is valid, else `false`.
151 | - `errorMap`: An object containing field names and validation errors.
152 |
153 | ##### `formInstance.getData()`
154 |
155 | *New in version 2.1*
156 |
157 | Returns the current data of the form instance.
158 |
159 | ##### `formInstance.getSchema()`
160 |
161 | *New in version 2.1*
162 |
163 | Returns the current schema of the form instance.
164 |
165 |
166 | #### Events
167 |
168 | Following is the list of currently available events:
169 |
170 | ##### `change`
171 |
172 | This event is fired for every change in the form's data.
173 |
174 | The callback for this event will be passed an `Object` with the following keys:
175 |
176 | - `data`: Current data of the form.
177 | - `prevData`: Previous data of the form (before the event).
178 | - `schema`: Current schema of the form.
179 | - `prevSchema`: Previous schema of the form (before the event).
180 |
181 | Example:
182 |
183 | ```js
184 | var form = reactjsonform.createform(...);
185 |
186 | form.addEventListener('change', function(e) {
187 | var data = e.data;
188 | var prevData: e.prevData;
189 | var schema: e.schema;
190 | var prevSchema: e.prevSchema;
191 |
192 | // do something ...
193 | });
194 | ```
195 |
196 |
197 |
Attention!
198 |
199 | If you want to call the update()
200 | method from the change event listener, you must call it conditionally
201 | or else it might cause an infite loop.
202 |
203 |
204 | For example, only call the update() after checking that the
205 | current data and prevData are not same.
206 |
215 | );
216 | }
217 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {JOIN_SYMBOL, FIELD_NAME_PREFIX} from './constants';
3 |
4 |
5 | export const EditorContext = React.createContext();
6 |
7 |
8 | export function capitalize(string) {
9 | if (!string)
10 | return '';
11 |
12 | return string.charAt(0).toUpperCase() + string.substr(1).toLowerCase();
13 | }
14 |
15 |
16 | export function convertType(value, to) {
17 | if (typeof value === to)
18 | return value;
19 |
20 | if (to === 'number' || to === 'integer') {
21 | if (typeof value === 'string') {
22 | value = value.trim();
23 | if (value === '')
24 | value = null;
25 | else if (!isNaN(Number(value)))
26 | value = Number(value);
27 | } else if (typeof value === 'boolean') {
28 | value = value === true ? 1 : 0;
29 | }
30 | } else if (to === 'boolean') {
31 | if (value === 'false' || value === false)
32 | value = false;
33 | else
34 | value = true;
35 | }
36 |
37 | return value;
38 | }
39 |
40 |
41 | export function actualType(value) {
42 | /* Returns the "actual" type of the given value.
43 |
44 | - array -> 'array'
45 | - null -> 'null'
46 | */
47 |
48 | let type = typeof value;
49 |
50 | if (type === 'object') {
51 | if (Array.isArray(value))
52 | type = 'array';
53 | else if (value === null)
54 | type = 'null';
55 | }
56 |
57 | return type;
58 | }
59 |
60 |
61 | export function getSchemaType(schema) {
62 | /* Returns type of the given schema.
63 |
64 | If schema.type is not present, it tries to guess the type.
65 |
66 | If data is given, it will try to use that to guess the type.
67 | */
68 | let type;
69 |
70 | if (schema.hasOwnProperty('const'))
71 | type = actualType(schema.const);
72 | else
73 | type = normalizeKeyword(schema.type);
74 |
75 | if (!type) {
76 | if (schema.hasOwnProperty('properties') ||
77 | schema.hasOwnProperty('keys')
78 | )
79 | type = 'object';
80 | else if (schema.hasOwnProperty('items'))
81 | type = 'array';
82 | else if (schema.hasOwnProperty('allOf'))
83 | type = 'allOf';
84 | else if (schema.hasOwnProperty('oneOf'))
85 | type = 'oneOf';
86 | else if (schema.hasOwnProperty('anyOf'))
87 | type = 'anyOf';
88 | else
89 | type = 'string';
90 | }
91 |
92 | return type;
93 | }
94 |
95 |
96 |
97 | export function getVerboseName(name) {
98 | if (name === undefined || name === null)
99 | return '';
100 |
101 | name = name.replace(/_/g, ' ');
102 | return capitalize(name);
103 | }
104 |
105 |
106 | export function getCsrfCookie() {
107 | let csrfCookies = document.cookie.split(';').filter((item) => item.trim().indexOf('csrftoken=') === 0);
108 |
109 | if (csrfCookies.length) {
110 | return csrfCookies[0].split('=')[1];
111 | } else {
112 | // if no cookie found, get the value from the csrf form input
113 | let input = document.querySelector('input[name="csrfmiddlewaretoken"]');
114 | if (input)
115 | return input.value;
116 | }
117 |
118 | return null;
119 | }
120 |
121 |
122 | export function joinCoords() {
123 | /* Generates coordinates from given arguments */
124 | return Array.from(arguments).join(JOIN_SYMBOL);
125 | }
126 |
127 |
128 | export function splitCoords(coords) {
129 | /* Generates coordinates */
130 | return coords.split(JOIN_SYMBOL);
131 | }
132 |
133 |
134 | export function getCoordsFromName(name) {
135 | /* Returns coordinates of a field in the data from
136 | * the given name of the input.
137 | * Field names have FIELD_NAME_PREFIX prepended but the coordinates don't.
138 | * e.g.:
139 | * name: rjf-0-field (where rjf- is the FIELD_NAME_PREFIX)
140 | * coords: 0-field
141 | */
142 | return name.slice((FIELD_NAME_PREFIX + JOIN_SYMBOL).length);
143 | }
144 |
145 |
146 | export function debounce(func, wait) {
147 | let timeout;
148 |
149 | return function() {
150 | clearTimeout(timeout);
151 |
152 | let args = arguments;
153 | let context = this;
154 |
155 | timeout = setTimeout(function() {
156 | func.apply(context, args);
157 | }, (wait || 1));
158 | }
159 | }
160 |
161 |
162 | export function normalizeKeyword(kw) {
163 | /* Converts custom supported keywords to standard JSON schema keywords */
164 |
165 | if (Array.isArray(kw))
166 | kw = kw.find((k) => k !== 'null') || 'null';
167 |
168 | switch (kw) {
169 | case 'list': return 'array';
170 | case 'dict': return 'object';
171 | case 'keys': return 'properties';
172 | case 'choices': return 'enum';
173 | case 'datetime': return 'date-time';
174 | default: return kw;
175 | }
176 | }
177 |
178 | export function getKeyword(obj, keyword, alias, default_value) {
179 | /* Function useful for getting value from schema if a
180 | * keyword has an alias.
181 | */
182 | return getKey(obj, keyword, getKey(obj, alias, default_value));
183 | }
184 |
185 | export function getKey(obj, key, default_value) {
186 | /* Approximation of Python's dict.get() function. */
187 |
188 | let val = obj[key];
189 | return (typeof val !== 'undefined') ? val : default_value;
190 | }
191 |
192 |
193 | export function choicesValueTitleMap(choices) {
194 | /* Returns a mapping of {value: title} for the given choices.
195 | * E.g.:
196 | * Input: [{'title': 'One', 'value': 1}, 2]
197 | * Output: {1: 'One', 2: 2}
198 | */
199 |
200 | let map = {};
201 |
202 | for (let i = 0; i < choices.length; i++) {
203 | let choice = choices[i];
204 | let value, title;
205 | if (actualType(choice) === 'object') {
206 | value = choice.value;
207 | title = choice.title;
208 | } else {
209 | value = choice;
210 | title = choice;
211 | }
212 |
213 | map[value] = title;
214 | }
215 |
216 | return map;
217 | }
218 |
219 |
220 | export function valueInChoices(schema, value) {
221 | /* Checks whether the given value is in schema choices or not.
222 | If schema doesn't have choices, returns true.
223 | */
224 |
225 | let choices = getKeyword(schema, 'choices', 'enum');
226 | if (!choices)
227 | return true;
228 |
229 | let found = choices.find((choice) => {
230 | if (typeof choice == 'object')
231 | choice = choice.value;
232 |
233 | return value == choice;
234 | })
235 |
236 | return found !== undefined ? true : false;
237 | }
238 |
239 |
240 | /* Set operations */
241 |
242 | export function isEqualset(a, b) {
243 | return a.size === b.size && Array.from(a).every((i) => b.has(i));
244 | }
245 |
246 | export function isSuperset(set, subset) {
247 | for (const elem of subset) {
248 | if (!set.has(elem)) {
249 | return false;
250 | }
251 | }
252 | return true;
253 | }
254 |
255 | export function isSubset(set, superset) {
256 | for (const elem of set) {
257 | if (!superset.has(elem)) {
258 | return false;
259 | }
260 | }
261 | return true;
262 | }
263 |
--------------------------------------------------------------------------------
/docs/docs/usage/node.md:
--------------------------------------------------------------------------------
1 | ---
2 | layout: page.html
3 | title: Using in Node
4 | ---
5 |
6 | ## Basic usage
7 |
8 | **React JSON Form** delegates the state management to you. Your component is
9 | responsible for saving the state.
10 |
11 | ### Example using hooks
12 |
13 | ```jsx
14 | import {ReactJSONForm, EditorState} from '@bhch/react-json-form';
15 |
16 |
17 | const MyForm = () => {
18 | const [editorState, setEditorState] = useState(() =>
19 | EditorState.create(schema, data)
20 | );
21 |
22 | return (
23 |
27 | );
28 | };
29 | ```
30 |
31 | ### Example using class component
32 |
33 | ```jsx
34 | import {ReactJSONForm, EditorState} from '@bhch/react-json-form';
35 |
36 |
37 | class MyComponent extends React.Component {
38 | constructor(props) {
39 | super(props);
40 |
41 | this.state = {
42 | editorState: EditorState.create(schema, data);
43 | }
44 | }
45 |
46 | handleFormChange = (editorState) => {
47 | this.setState({editorState: editorState});
48 | }
49 |
50 | render() {
51 | return (
52 |
53 |
57 |
58 | );
59 | }
60 | }
61 | ```
62 |
63 | ## `ReactJSONForm` API reference
64 |
65 | ### Props
66 |
67 | - `editorState`: Instance of [`EditorState`](#editorstate-api-reference) containing the schema and data.
68 | - `onChange`: Callback function for handling changes. This function will receive a new instance of
69 | `EditorState` (because `EditorState` is immutable, instead of modifying the previous instance, we
70 | replace it with a new one).
71 | - `fileHandler`: A URL to a common file handler endpoint for all file input fields.
72 | - `fileHandlerArgs` (*Optional*): Key-value pairs which will be sent via querystring to the `fileHandler` URL.
73 | - `errorMap`: An object containing error messages for input fields. [See data validation section](#data-validation)
74 | for more.
75 | - `readonly`: A boolean. If `true`, the whole form will be read-only.
76 |
77 | *Changed in version 2.1*: `errorMap` prop was added.
78 | *Changed in version 2.10*: `readonly` prop was added.
79 |
80 | ## `EditorState` API reference
81 |
82 | `EditorState` must be treated as an immutable object. If you want to make any
83 | changes to the state, such as updating the schema, or changing the data, you
84 | must do it by calling the methods provided by `EditorState`.
85 |
86 | Always avoid directly mutating the `EditorState` object.
87 |
88 | ### Static methods
89 |
90 | ##### `EditorState.create(schema, data)`
91 |
92 | **Returns**: New `EditorState` instance.
93 |
94 | **Arguments**:
95 |
96 | - `schema`: Schema for the form. It can either be a JS `Object` or a JSON string.
97 | - `data` *(Optional)*: Initial data for the form. It can either be a JS `Object`,
98 | `Array`, or a JSON string.
99 |
100 | This method also tries to validate the schema and data and it will raise an exception
101 | if in case the schema is invalid or the data structure doesn't match the given schema.
102 |
103 |
104 | ##### `EditorState.update(editorState, data)`
105 |
106 | **Returns**: New `EditorState` instance.
107 |
108 | **Arguments**:
109 |
110 | - `editorState`: Instance of `editorState`.
111 | - `data`: Must be either a JS `Object` or `Array`. It can not be a JSON string.
112 |
113 | Use this method to update an existing `EditorState` with the given data.
114 |
115 | Since, the `EditorState` object is considered immutable, it doesn't actually
116 | modify the given `editorState`. Instead, it creates and return a new `EditorState`
117 | instance.
118 |
119 | This method is only for updating data. It doesn't do any validations to keep
120 | updates as fast as possible.
121 |
122 | If you want to validate the schema, you must create a new state using
123 | `EditorState.create()` method as that will also validate the schema and the data.
124 |
125 |
126 | ### Instance methods
127 |
128 | The following methods are available on an instance of `EditorState`.
129 |
130 |
131 | ##### `EditorState.getData()`
132 |
133 | Use this method to get the current data of the form.
134 |
135 | It will either return an `Array` or an `Object` depending upon the outermost `type`
136 | declared in the schema.
137 |
138 |
139 | ##### `EditorState.getSchema()`
140 |
141 | This method returns the schema.
142 |
143 |
144 | #### Data validation
145 |
146 | *New in version 2.1*
147 |
148 | **React JSON Form** comes with a basic data validator called [`DataValidator`](#datavalidator-api-reference).
149 | But you are free to validate the data however you want.
150 |
151 | After the validation, you may also want to display error messages below the
152 | input fields. For this purpose, the `ReactJSONForm` component accepts an `errorMap`
153 | prop which is basically a mapping of field names in the data and error messages.
154 |
155 | An `errorMap` looks like this:
156 |
157 | ```js
158 | let errorMap = {
159 | 'name': 'This field is required',
160 |
161 | // multiple error messages
162 | 'age': [
163 | 'This field is required',
164 | 'This value must be greater than 18'
165 | ]
166 |
167 | // nested arrays and objects
168 |
169 | // first item in array
170 | '0': 'This is required',
171 |
172 | // first item > object > property: name
173 | // (see note below about the section sign "§")
174 | '0§name': 'This is required'
175 | }
176 | ```
177 |
178 |
179 |
The section sign (§)
180 |
181 | The section sign (§) is used as the separator symbol for
182 | doing nested items lookup.
183 |
184 |
185 | Earlier, the hyphen (-) was used but that complicated things
186 | when the the schema object properties (i.e. field names) also had a hyphen
187 | in them. Then it became impossible to determine whether the hyphen was the
188 | separator or part of the key.
189 |
190 |
191 |
192 |
193 | ##### `DataValidator` API reference
194 |
195 | ##### Constructor
196 |
197 | ##### `new DataValidator(schema)`
198 |
199 | **Returns**: An instance of `DataValidator`
200 |
201 | **Arguments**:
202 |
203 | - `schema`: Schema object (not JSON string).
204 |
205 | ##### Instance methods
206 |
207 | Following methods must be called form the instance of `DataValidator`.
208 |
209 | ##### `validatorInstance.validate(data)`
210 |
211 | **Returns**: A *validation* object containing these keys:
212 |
213 | - `isValid`: A boolean denoting whether the data is valid or not.
214 | - `errorMap`: An object containing error messages for invalid data fields.
215 |
216 | **Arguments**:
217 |
218 | - `data`: The data to validate against the `schema` provided to the constructor.
219 |
220 | Example:
221 |
222 | ```jsx
223 | import {DataValidator} from '@bhch/react-json-form';
224 |
225 | const validator = new DataValidator(schema);
226 | const validation = validator.validate(data);
227 |
228 | const isValid = validation.isValid;
229 | const errorMap = validation.errorMap;
230 |
231 | if (isValid)
232 | alert('Success');
233 | else
234 | alert('Invalid');
235 |
236 | // pass the errorMap object to ReactJSONForm
237 | // and error messages will be displayed under
238 | // input fields
239 |
243 | ```
244 |
--------------------------------------------------------------------------------
/src/form.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {getArrayFormRow, getObjectFormRow, getOneOfFormRow, getAnyOfFormRow, getAllOfFormRow} from './ui';
3 | import {EditorContext, joinCoords, splitCoords, getSchemaType} from './util';
4 | import {FIELD_NAME_PREFIX} from './constants';
5 | import EditorState from './editorState';
6 |
7 |
8 | export default class ReactJSONForm extends React.Component {
9 | handleChange = (coords, value) => {
10 | /*
11 | e.target.name is a chain of indices and keys:
12 | xxx-0-key-1-key2 and so on.
13 | These can be used as coordinates to locate
14 | a particular deeply nested item.
15 |
16 | This first coordinate is not important and should be removed.
17 | */
18 | coords = splitCoords(coords);
19 |
20 | coords.shift(); // remove first coord
21 |
22 | // :TODO: use immutable JS instead of JSON-ising the data
23 | let data = setDataUsingCoords(coords, JSON.parse(JSON.stringify(this.props.editorState.getData())), value);
24 |
25 | this.props.onChange(EditorState.update(this.props.editorState, data));
26 | }
27 |
28 | getRef = (ref) => {
29 | /* Returns schema reference. Nothing to do with React's refs.*/
30 |
31 | return EditorState.getRef(ref, this.props.editorState.getSchema());
32 | }
33 |
34 | getFields = () => {
35 | let data = this.props.editorState.getData();
36 | let schema = this.props.editorState.getSchema();
37 | let formGroups = [];
38 |
39 | if (schema.hasOwnProperty('$ref')) {
40 | schema = {...this.getRef(schema['$ref']), ...schema};
41 | delete schema['$ref'];
42 | }
43 |
44 | let type = getSchemaType(schema);
45 |
46 | let args = {
47 | data: data,
48 | schema: schema,
49 | name: FIELD_NAME_PREFIX,
50 | onChange: this.handleChange,
51 | onAdd: this.addFieldset,
52 | onRemove: this.removeFieldset,
53 | onEdit: this.editFieldset,
54 | onMove: this.moveFieldset,
55 | level: 0,
56 | getRef: this.getRef,
57 | errorMap: this.props.errorMap || {}
58 | };
59 |
60 | if (this.props.readonly)
61 | args.schema.readOnly = true;
62 |
63 | if (type === 'array')
64 | return getArrayFormRow(args);
65 | else if (type === 'object')
66 | return getObjectFormRow(args);
67 | else if (type === 'oneOf')
68 | return getOneOfFormRow(args);
69 | else if (type === 'anyOf')
70 | return getAnyOfFormRow(args);
71 | else if (type === 'allOf')
72 | return getAllOfFormRow(args);
73 |
74 | return formGroups;
75 | }
76 |
77 | addFieldset = (blankData, coords) => {
78 | coords = splitCoords(coords);
79 | coords.shift();
80 |
81 | // :TODO: use immutable JS instead of JSON-ising the data
82 | let data = addDataUsingCoords(coords, JSON.parse(JSON.stringify(this.props.editorState.getData())), blankData);
83 |
84 | this.props.onChange(EditorState.update(this.props.editorState, data));
85 | }
86 |
87 | removeFieldset = (coords) => {
88 | coords = splitCoords(coords);
89 | coords.shift();
90 |
91 | // :TODO: use immutable JS instead of JSON-ising the data
92 | let data = removeDataUsingCoords(coords, JSON.parse(JSON.stringify(this.props.editorState.getData())));
93 |
94 | this.props.onChange(EditorState.update(this.props.editorState, data));
95 | }
96 |
97 | editFieldset = (value, newCoords, oldCoords) => {
98 | /* Add and remove in a single state update
99 |
100 | newCoords will be added
101 | oldCoords willbe removed
102 | */
103 |
104 | newCoords = splitCoords(newCoords);
105 | newCoords.shift();
106 |
107 | oldCoords = splitCoords(oldCoords);
108 | oldCoords.shift();
109 |
110 | let data = addDataUsingCoords(newCoords, JSON.parse(JSON.stringify(this.props.editorState.getData())), value);
111 |
112 | data = removeDataUsingCoords(oldCoords, data);
113 |
114 | this.props.onChange(EditorState.update(this.props.editorState, data));
115 | }
116 |
117 | moveFieldset = (oldCoords, newCoords) => {
118 | oldCoords = splitCoords(oldCoords);
119 | oldCoords.shift();
120 |
121 | newCoords = splitCoords(newCoords);
122 | newCoords.shift();
123 |
124 | // :TODO: use immutable JS instead of JSON-ising the data
125 | let data = moveDataUsingCoords(oldCoords, newCoords, JSON.parse(JSON.stringify(this.props.editorState.getData())));
126 |
127 | this.props.onChange(EditorState.update(this.props.editorState, data));
128 | }
129 |
130 | render() {
131 | return (
132 |
133 |
143 |
144 | );
145 | }
146 | }
147 |
148 | function setDataUsingCoords(coords, data, value) {
149 | let coord = coords.shift();
150 |
151 | if (!isNaN(Number(coord)))
152 | coord = Number(coord);
153 |
154 | if (coords.length) {
155 | data[coord] = setDataUsingCoords(coords, data[coord], value);
156 | } else {
157 | if (coord === undefined) // top level array with multiselect widget
158 | data = value;
159 | else
160 | data[coord] = value;
161 | }
162 |
163 | return data;
164 | }
165 |
166 | function addDataUsingCoords(coords, data, value) {
167 | let coord = coords.shift();
168 | if (!isNaN(Number(coord)))
169 | coord = Number(coord);
170 |
171 | if (coords.length) {
172 | data[coord] = addDataUsingCoords(coords, data[coord], value);
173 | } else {
174 | if (Array.isArray(data[coord])) {
175 | data[coord].push(value);
176 | } else {
177 | if (Array.isArray(data)) {
178 | data.push(value);
179 | } else {
180 | data[coord] = value;
181 | }
182 | }
183 | }
184 |
185 | return data;
186 | }
187 |
188 | function removeDataUsingCoords(coords, data) {
189 | let coord = coords.shift();
190 | if (!isNaN(Number(coord)))
191 | coord = Number(coord);
192 |
193 | if (coords.length) {
194 | removeDataUsingCoords(coords, data[coord]);
195 | } else {
196 | if (Array.isArray(data))
197 | data.splice(coord, 1); // in-place mutation
198 | else
199 | delete data[coord];
200 | }
201 |
202 | return data;
203 | }
204 |
205 |
206 | function moveDataUsingCoords(oldCoords, newCoords, data) {
207 | let oldCoord = oldCoords.shift();
208 |
209 | if (!isNaN(Number(oldCoord)))
210 | oldCoord = Number(oldCoord);
211 |
212 | if (oldCoords.length) {
213 | moveDataUsingCoords(oldCoords, newCoords, data[oldCoord]);
214 | } else {
215 | if (Array.isArray(data)) {
216 | /* Using newCoords allows us to move items from
217 | one array to another.
218 | However, for now, we're only moving items in a
219 | single array.
220 | */
221 | let newCoord = newCoords[newCoords.length - 1];
222 |
223 | let item = data[oldCoord];
224 |
225 | data.splice(oldCoord, 1);
226 | data.splice(newCoord, 0, item);
227 | }
228 | }
229 |
230 | return data;
231 | }
232 |
--------------------------------------------------------------------------------
/docs/static/img/logo.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/src/schemaValidation.js:
--------------------------------------------------------------------------------
1 | import {normalizeKeyword, getSchemaType} from './util';
2 |
3 |
4 | export function validateSchema(schema) {
5 | if (!(schema instanceof Object))
6 | return {isValid: false, msg: "Schema must be an object"};
7 |
8 | let type = getSchemaType(schema);
9 |
10 | let validation = {isValid: true, msg: ""};
11 | if (type === 'object')
12 | validation = validateObject(schema);
13 | else if (type === 'array')
14 | validation = validateArray(schema);
15 | else {
16 | if (schema.hasOwnProperty('allOf')) {
17 | validation = validateAllOf(schema);
18 | } else if (schema.hasOwnProperty('oneOf')) {
19 | validation = validateOneOf(schema);
20 | } else if (schema.hasOwnProperty('anyOf')) {
21 | validation = validateAnyOf(schema);
22 | } else if (schema.hasOwnProperty('$ref')) {
23 | validation = {isValid: true};
24 | } else {
25 | validation = {
26 | isValid: false,
27 | msg: "Outermost schema can only be of type array, list, object or dict"
28 | };
29 | }
30 | }
31 |
32 | if (!validation.isValid || !schema.hasOwnProperty('$defs'))
33 | return validation;
34 |
35 | // validate $defs
36 | // :TODO: validate $defs nested inside objects/arrays
37 | if (!(schema['$defs']) instanceof Object)
38 | return {
39 | isValid: false,
40 | msg: "'$defs' must be a valid JavaScript Object"
41 | };
42 |
43 | return validation;
44 | }
45 |
46 |
47 | export function validateObject(schema) {
48 | if (
49 | !schema.hasOwnProperty('keys') &&
50 | !schema.hasOwnProperty('properties') &&
51 | !schema.hasOwnProperty('oneOf') &&
52 | !schema.hasOwnProperty('anyOf') &&
53 | !schema.hasOwnProperty('allOf')
54 | )
55 | return {
56 | isValid: false,
57 | msg: "Schema of type '" + schema.type + "' must have at least one of these keys: " +
58 | "['properties' or 'keys' or 'oneOf' or 'anyOf' or 'allOf']"
59 | };
60 |
61 | let validation;
62 |
63 | let keys = schema.properties || schema.keys;
64 | if (keys) {
65 | validation = validateKeys(keys);
66 | if (!validation.isValid)
67 | return validation;
68 | }
69 |
70 | if (schema.hasOwnProperty('additionalProperties')) {
71 | if (!(schema.additionalProperties instanceof Object) && typeof schema.additionalProperties !== 'boolean')
72 | return {
73 | isValid: false,
74 | msg: "'additionalProperties' must be either a JavaScript boolean or a JavaScript object"
75 | };
76 |
77 | if (schema.additionalProperties instanceof Object) {
78 | if (schema.additionalProperties.hasOwnProperty('$ref')) {
79 | validation = validateRef(schema.additionalProperties);
80 | if (!validation.isValid)
81 | return validation;
82 | } else {
83 | let type = normalizeKeyword(schema.additionalProperties.type);
84 |
85 | if (type === 'object')
86 | return validateObject(schema.additionalProperties);
87 | else if (type === 'array')
88 | return validateSchema(schema.additionalProperties);
89 | /* :TODO: else validate allowed types */
90 | }
91 | }
92 |
93 | }
94 |
95 | if (schema.hasOwnProperty('oneOf')) {
96 | validation = validateOneOf(schema);
97 | if (!validation.isValid)
98 | return validation;
99 | }
100 |
101 | if (schema.hasOwnProperty('anyOf')) {
102 | validation = validateAnyOf(schema);
103 | if (!validation.isValid)
104 | return validation;
105 | }
106 |
107 | if (schema.hasOwnProperty('allOf')) {
108 | validation = validateAllOf(schema);
109 | if (!validation.isValid)
110 | return validation;
111 | }
112 |
113 | return {isValid: true, msg: ""};
114 | }
115 |
116 |
117 | function validateKeys(keys) {
118 | if (!(keys instanceof Object))
119 | return {
120 | isValid: false,
121 | msg: "The 'keys' or 'properties' key must be a valid JavaScript Object"
122 | };
123 |
124 | for (let key in keys) {
125 | if (!keys.hasOwnProperty(key))
126 | continue;
127 |
128 | let value = keys[key];
129 |
130 | if (!(value instanceof Object))
131 | return {
132 | isValid: false,
133 | msg: "Key '" + key + "' must be a valid JavaScript Object"
134 | };
135 |
136 | let validation = {isValid: true};
137 |
138 | let value_type = normalizeKeyword(value.type);
139 |
140 | if (value_type) {
141 | if (value_type === 'object')
142 | validation = validateObject(value);
143 | else if (value_type === 'array')
144 | validation = validateArray(value);
145 | } else if (value.hasOwnProperty('$ref')) {
146 | validation = validateRef(value);
147 | } else if (value.hasOwnProperty('oneOf')) {
148 | validation = validateOneOf(value);
149 | } else if (value.hasOwnProperty('anyOf')) {
150 | validation = validateAnyOf(value);
151 | } else if (value.hasOwnProperty('allOf')) {
152 | validation = validateAllOf(value);
153 | } else if (value.hasOwnProperty('const')) {
154 | validation = validateConst(value);
155 | } else {
156 | validation = {isValid: false, msg: "Key '" + key + "' must have a 'type' or a '$ref"};
157 | }
158 |
159 | if (!validation.isValid)
160 | return validation;
161 | }
162 |
163 | return {isValid: true, msg: ""};
164 | }
165 |
166 |
167 | export function validateArray(schema) {
168 | if (!schema.hasOwnProperty('items'))
169 | return {
170 | isValid: false,
171 | msg: "Schema of type '" + schema.type + "' must have a key called 'items'"
172 | };
173 |
174 | if (!(schema.items instanceof Object))
175 | return {
176 | isValid: false,
177 | msg: "The 'items' key must be a valid JavaScript Object'"
178 | };
179 |
180 | let items_type = normalizeKeyword(schema.items.type);
181 |
182 | if (items_type) {
183 | if (items_type === 'object')
184 | return validateObject(schema.items);
185 | else if (items_type === 'array')
186 | return validateArray(schema.items);
187 | /* :TODO: else validate allowed types */
188 | } else if (schema.items.hasOwnProperty('$ref')) {
189 | return validateRef(schema.items);
190 | } else {
191 | if (!schema.items.hasOwnProperty('oneOf') &&
192 | !schema.items.hasOwnProperty('anyOf') &&
193 | !schema.items.hasOwnProperty('allOf') &&
194 | !schema.items.hasOwnProperty('const')
195 | )
196 | return {isValid: false, msg: "Array 'items' must have a 'type' or '$ref' or 'oneOf' or 'anyOf'"};
197 | }
198 |
199 | if (schema.items.hasOwnProperty('oneOf')) {
200 | validation = validateOneOf(schema.items);
201 | if (!validation.isValid)
202 | return validation;
203 | }
204 |
205 | if (schema.items.hasOwnProperty('anyOf')) {
206 | validation = validateAnyOf(schema.items);
207 | if (!validation.isValid)
208 | return validation;
209 | }
210 |
211 | if (schema.items.hasOwnProperty('allOf')) {
212 | // we don't support allOf inside array yet
213 | return {
214 | isValid: false,
215 | msg: "Currently, 'allOf' inside array items is not supported"
216 | }
217 | }
218 |
219 | if (schema.items.hasOwnProperty('const')) {
220 | validation = validateConst(schema.items);
221 | if (!validation.isValid)
222 | return validation;
223 | }
224 |
225 | return {isValid: true, msg: ""};
226 | }
227 |
228 |
229 | export function validateRef(schema) {
230 | if (typeof schema['$ref'] !== 'string')
231 | return {
232 | isValid: false,
233 | msg: "'$ref' keyword must be a string"
234 | };
235 |
236 | if (!schema['$ref'].startsWith('#'))
237 | return {
238 | isValid: false,
239 | msg: "'$ref' value must begin with a hash (#) character"
240 | };
241 |
242 | if (schema['$ref'].lenght > 1 && !schema['$ref'].startsWith('#/'))
243 | return {
244 | isValid: false,
245 | msg: "Invalid '$ref' path"
246 | };
247 |
248 | return {isValid: true, msg: ""};
249 | }
250 |
251 |
252 | export function validateOneOf(schema) {
253 | return validateSubschemas(schema, 'oneOf');
254 | }
255 |
256 |
257 | export function validateAnyOf(schema) {
258 | return validateSubschemas(schema, 'anyOf');
259 | }
260 |
261 |
262 | export function validateAllOf(schema) {
263 | let validation = validateSubschemas(schema, 'allOf');
264 | if (!validation.isValid)
265 | return validation;
266 |
267 | // currently, we only support anyOf inside an object
268 | // so, we'll check if all subschemas are objects or not
269 |
270 | let subschemas = schema['allOf'];
271 |
272 | for (let i = 0; i < subschemas.length; i++) {
273 | let subschema = subschemas[i];
274 | let subType = getSchemaType(subschema);
275 |
276 | if (subType !== 'object') {
277 | return {
278 | isValid: false,
279 | msg: "Possible conflict in 'allOf' subschemas. Currently, we only support subschemas listed in 'allOf' to be of type 'object'."
280 | }
281 | }
282 | }
283 |
284 | return validation
285 | }
286 |
287 |
288 | function validateConst(schema) {
289 | return {isValid: true, msg: ""};
290 | }
291 |
292 |
293 | function validateSubschemas(schema, keyword) {
294 | /*
295 | Common validator for oneOf/anyOf/allOf
296 |
297 | Params:
298 | schema: the schema containing the oneOf/anyOf/allOf subschema
299 | keyword: one of 'oneOf' or 'anyOf' or 'allOf'
300 |
301 | Validation:
302 | 1. Must be an array
303 | 2. Must have at least one subschema
304 | 3. If directly inside an object, each subschema in array must have 'properties' or 'keys keyword
305 | */
306 | let subschemas = schema[keyword];
307 |
308 | if (!Array.isArray(subschemas))
309 | return {
310 | isValid: false,
311 | msg: "'" + keyword + "' property must be an array"
312 | };
313 |
314 | if (!subschemas.length)
315 | return {
316 | isValid: false,
317 | msg: "'" + keyword + "' must contain at least one subschema"
318 | };
319 |
320 | for (let i = 0; i < subschemas.length; i++) {
321 | let subschema = subschemas[i];
322 | let subType = getSchemaType(subschema);
323 |
324 | if (subType === 'object') {
325 | let validation = validateObject(subschema);
326 | if (!validation.isValid)
327 | return validation;
328 | } else if (subType === 'array') {
329 | let validation = validateArray(subschema);
330 | if (!validation.isValid)
331 | return validation;
332 | }
333 | }
334 |
335 | return {isValid: true, msg: ""};
336 | }
337 |
--------------------------------------------------------------------------------
/docs/src/tabs.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {EditorState} from "@codemirror/state";
3 | import {EditorView} from "@codemirror/view";
4 | import {lineNumbers, highlightActiveLineGutter, highlightSpecialChars,
5 | drawSelection, highlightActiveLine, keymap
6 | } from '@codemirror/view';
7 | import {indentOnInput, syntaxHighlighting, defaultHighlightStyle,bracketMatching} from '@codemirror/language';
8 | import {history, defaultKeymap, historyKeymap, indentWithTab} from '@codemirror/commands';
9 | import {closeBrackets, completionKeymap} from '@codemirror/autocomplete';
10 | import {lintKeymap, lintGutter, linter} from '@codemirror/lint';
11 | import {json, jsonParseLinter} from "@codemirror/lang-json";
12 |
13 | import {ReactJSONForm, EditorState as RJFEditorState, DataValidator} from 'react-json-form';
14 |
15 | import DEMOS from './demos.js';
16 |
17 | export function Tabs(props) {
18 | return (
19 |
You can recursively nest an item within itself.
182 | However, there are certain edge cases where it might lead to infinite recursion error. So, be careful!
183 |
210 | File upload to server (file-url)
211 | will not work in this demo because a server is required.
212 | However, Base64 upload (data-url) will work fine.
213 |
270 | Multiple selections only work inside an array.
271 | This demo uses a placeholder JSON API to load data. Hence, autocompletion may not work as expected.
272 |