diff --git a/README.md b/README.md index e2a7fbd1c5..809cf9003d 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i - [Multiple files](#multiple-files) - [File widget input ref](#file-widget-input-ref) - [Object fields ordering](#object-fields-ordering) + - [Object item options](#object-item-options) + - [expandable option](#expandable-option) - [Array item options](#array-item-options) - [orderable option](#orderable-option) - [addable option](#addable-option) @@ -484,6 +486,20 @@ const uiSchema = { }; ``` +### Object item options + +#### `expandable` option + +If `additionalProperties` contains a schema object, an add button for new properies is shown by default. You can turn this off with the `expandable` option in `uiSchema`: + +```jsx +const uiSchema = { + "ui:options": { + expandable: false + } +}; +``` + ### Array item options #### `orderable` option diff --git a/playground/samples/additionalProperties.js b/playground/samples/additionalProperties.js new file mode 100644 index 0000000000..6a28a138f9 --- /dev/null +++ b/playground/samples/additionalProperties.js @@ -0,0 +1,32 @@ +module.exports = { + schema: { + title: "A customizable registration form", + description: "A simple form with additional properties example.", + type: "object", + required: ["firstName", "lastName"], + additionalProperties: { + type: "string", + }, + properties: { + firstName: { + type: "string", + title: "First name", + }, + lastName: { + type: "string", + title: "Last name", + }, + }, + }, + uiSchema: { + firstName: { + "ui:autofocus": true, + "ui:emptyValue": "", + }, + }, + formData: { + firstName: "Chuck", + lastName: "Norris", + assKickCount: "infinity", + }, +}; diff --git a/playground/samples/index.js b/playground/samples/index.js index 9d8806f8dc..b496c0fb6d 100644 --- a/playground/samples/index.js +++ b/playground/samples/index.js @@ -17,6 +17,7 @@ import customObject from "./customObject"; import alternatives from "./alternatives"; import propertyDependencies from "./propertyDependencies"; import schemaDependencies from "./schemaDependencies"; +import additionalProperties from "./additionalProperties"; export const samples = { Simple: simple, @@ -38,4 +39,5 @@ export const samples = { Alternatives: alternatives, "Property dependencies": propertyDependencies, "Schema dependencies": schemaDependencies, + "Additional Properties": additionalProperties, }; diff --git a/src/components/AddButton.js b/src/components/AddButton.js new file mode 100644 index 0000000000..b16c3475d8 --- /dev/null +++ b/src/components/AddButton.js @@ -0,0 +1,19 @@ +import React from "react"; +import IconButton from "./IconButton"; + +export default function AddButton({ className, onClick, disabled }) { + return ( +
+

+ +

+
+ ); +} diff --git a/src/components/IconButton.js b/src/components/IconButton.js new file mode 100644 index 0000000000..d67bf0bd63 --- /dev/null +++ b/src/components/IconButton.js @@ -0,0 +1,13 @@ +import React from "react"; + +export default function IconButton(props) { + const { type = "default", icon, className, ...otherProps } = props; + return ( + + ); +} diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js index 991345bb14..4985bf3f19 100644 --- a/src/components/fields/ArrayField.js +++ b/src/components/fields/ArrayField.js @@ -1,3 +1,5 @@ +import AddButton from "../AddButton"; +import IconButton from "../IconButton"; import React, { Component } from "react"; import PropTypes from "prop-types"; import includes from "core-js/library/fn/array/includes"; @@ -35,18 +37,6 @@ function ArrayFieldDescription({ DescriptionField, idSchema, description }) { return ; } -function IconBtn(props) { - const { type = "default", icon, className, ...otherProps } = props; - return ( - - ); -} - // Used in the two templates function DefaultArrayItem(props) { const btnStyle = { @@ -70,7 +60,7 @@ function DefaultArrayItem(props) { justifyContent: "space-around", }}> {(props.hasMoveUp || props.hasMoveDown) && ( - @@ -176,6 +167,7 @@ function DefaultNormalArrayFieldTemplate(props) { {props.canAdd && ( @@ -668,23 +660,6 @@ class ArrayField extends Component { } } -function AddButton({ onClick, disabled }) { - return ( -
-

- -

-
- ); -} - if (process.env.NODE_ENV !== "production") { ArrayField.propTypes = { schema: PropTypes.object.isRequired, diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js index b6908ae00c..c4ab7cf802 100644 --- a/src/components/fields/ObjectField.js +++ b/src/components/fields/ObjectField.js @@ -1,3 +1,4 @@ +import AddButton from "../AddButton"; import React, { Component } from "react"; import PropTypes from "prop-types"; @@ -5,9 +6,27 @@ import { orderProperties, retrieveSchema, getDefaultRegistry, + getUiOptions, } from "../../utils"; function DefaultObjectFieldTemplate(props) { + const canExpand = function canExpand() { + const { formData, schema, uiSchema } = props; + if (!schema.additionalProperties) { + return false; + } + const { expandable } = getUiOptions(uiSchema); + if (expandable === false) { + return expandable; + } + // if ui:options.expandable was not explicitly set to false, we can add + // another property if we have not exceeded maxProperties yet + if (schema.maxProperties !== undefined) { + return Object.keys(formData).length < schema.maxProperties; + } + return true; + }; + const { TitleField, DescriptionField } = props; return (
@@ -27,6 +46,13 @@ function DefaultObjectFieldTemplate(props) { /> )} {props.properties.map(prop => prop.content)} + {canExpand() && ( + + )}
); } @@ -42,6 +68,10 @@ class ObjectField extends Component { readonly: false, }; + state = { + additionalProperties: {}, + }; + isRequired(name) { const schema = this.props.schema; return ( @@ -63,6 +93,62 @@ class ObjectField extends Component { }; }; + getAvailableKey = (preferredKey, formData) => { + var index = 0; + var newKey = preferredKey; + while (this.props.formData.hasOwnProperty(newKey)) { + newKey = `${preferredKey}-${++index}`; + } + return newKey; + }; + + onKeyChange = oldValue => { + return (value, errorSchema) => { + value = this.getAvailableKey(value, this.props.formData); + const newFormData = { ...this.props.formData }; + const property = newFormData[oldValue]; + delete newFormData[oldValue]; + newFormData[value] = property; + this.props.onChange( + newFormData, + errorSchema && + this.props.errorSchema && { + ...this.props.errorSchema, + [value]: errorSchema, + } + ); + }; + }; + + getDefaultValue(type) { + switch (type) { + case "string": + return "New Value"; + case "array": + return []; + case "boolean": + return false; + case "null": + return null; + case "number": + return 0; + case "object": + return {}; + default: + // We don't have a datatype for some reason (perhaps additionalProperties was true) + return "New Value"; + } + } + + handleAddClick = schema => () => { + const type = schema.additionalProperties.type; + const newFormData = { ...this.props.formData }; + newFormData[ + this.getAvailableKey("newKey", newFormData) + ] = this.getDefaultValue(type); + this.props.onChange(newFormData); + }; + render() { const { uiSchema, @@ -120,6 +206,7 @@ class ObjectField extends Component { idSchema={idSchema[name]} idPrefix={idPrefix} formData={formData[name]} + onKeyChange={this.onKeyChange(name)} onChange={this.onPropertyChange(name)} onBlur={onBlur} onFocus={onFocus} @@ -141,7 +228,7 @@ class ObjectField extends Component { formData, formContext, }; - return