Skip to content

Commit

Permalink
Add preliminary support for object "additionalProperties" (rjsf-team#…
Browse files Browse the repository at this point in the history
…1021)

* Initial commit for additionalProperties

* Resolve PR feedback:
* avoid modifying parameters
* avoid unnecessary let using returns
* eliminate unnecessary bind
* terse closure return syntax
* take advantage of class fields

* Fix validation naming issue on additional key change
Fix misnamed "additionalProperties" attribute
Invert conditional to reduce indentation
Added unit tests to ensure non-schema properties are maintained
  • Loading branch information
christianlent authored and glasserc committed Oct 2, 2018
1 parent ac226bd commit 1d54db6
Show file tree
Hide file tree
Showing 10 changed files with 539 additions and 34 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions playground/samples/additionalProperties.js
Original file line number Diff line number Diff line change
@@ -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",
},
};
2 changes: 2 additions & 0 deletions playground/samples/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -38,4 +39,5 @@ export const samples = {
Alternatives: alternatives,
"Property dependencies": propertyDependencies,
"Schema dependencies": schemaDependencies,
"Additional Properties": additionalProperties,
};
19 changes: 19 additions & 0 deletions src/components/AddButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from "react";
import IconButton from "./IconButton";

export default function AddButton({ className, onClick, disabled }) {
return (
<div className="row">
<p className={`col-xs-3 col-xs-offset-9 text-right ${className}`}>
<IconButton
type="info"
icon="plus"
className="btn-add col-xs-12"
tabIndex="0"
onClick={onClick}
disabled={disabled}
/>
</p>
</div>
);
}
13 changes: 13 additions & 0 deletions src/components/IconButton.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react";

export default function IconButton(props) {
const { type = "default", icon, className, ...otherProps } = props;
return (
<button
type="button"
className={`btn btn-${type} ${className}`}
{...otherProps}>
<i className={`glyphicon glyphicon-${icon}`} />
</button>
);
}
39 changes: 7 additions & 32 deletions src/components/fields/ArrayField.js
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -35,18 +37,6 @@ function ArrayFieldDescription({ DescriptionField, idSchema, description }) {
return <DescriptionField id={id} description={description} />;
}

function IconBtn(props) {
const { type = "default", icon, className, ...otherProps } = props;
return (
<button
type="button"
className={`btn btn-${type} ${className}`}
{...otherProps}>
<i className={`glyphicon glyphicon-${icon}`} />
</button>
);
}

// Used in the two templates
function DefaultArrayItem(props) {
const btnStyle = {
Expand All @@ -70,7 +60,7 @@ function DefaultArrayItem(props) {
justifyContent: "space-around",
}}>
{(props.hasMoveUp || props.hasMoveDown) && (
<IconBtn
<IconButton
icon="arrow-up"
className="array-item-move-up"
tabIndex="-1"
Expand All @@ -81,7 +71,7 @@ function DefaultArrayItem(props) {
)}

{(props.hasMoveUp || props.hasMoveDown) && (
<IconBtn
<IconButton
icon="arrow-down"
className="array-item-move-down"
tabIndex="-1"
Expand All @@ -94,7 +84,7 @@ function DefaultArrayItem(props) {
)}

{props.hasRemove && (
<IconBtn
<IconButton
type="danger"
icon="remove"
className="array-item-remove"
Expand Down Expand Up @@ -138,6 +128,7 @@ function DefaultFixedArrayFieldTemplate(props) {

{props.canAdd && (
<AddButton
className="array-item-add"
onClick={props.onAddClick}
disabled={props.disabled || props.readonly}
/>
Expand Down Expand Up @@ -176,6 +167,7 @@ function DefaultNormalArrayFieldTemplate(props) {

{props.canAdd && (
<AddButton
className="array-item-add"
onClick={props.onAddClick}
disabled={props.disabled || props.readonly}
/>
Expand Down Expand Up @@ -668,23 +660,6 @@ class ArrayField extends Component {
}
}

function AddButton({ onClick, disabled }) {
return (
<div className="row">
<p className="col-xs-3 col-xs-offset-9 array-item-add text-right">
<IconBtn
type="info"
icon="plus"
className="btn-add col-xs-12"
tabIndex="0"
onClick={onClick}
disabled={disabled}
/>
</p>
</div>
);
}

if (process.env.NODE_ENV !== "production") {
ArrayField.propTypes = {
schema: PropTypes.object.isRequired,
Expand Down
89 changes: 88 additions & 1 deletion src/components/fields/ObjectField.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import AddButton from "../AddButton";
import React, { Component } from "react";
import PropTypes from "prop-types";

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 (
<fieldset>
Expand All @@ -27,6 +46,13 @@ function DefaultObjectFieldTemplate(props) {
/>
)}
{props.properties.map(prop => prop.content)}
{canExpand() && (
<AddButton
className="object-property-expand"
onClick={props.onAddClick(props.schema)}
disabled={props.disabled || props.readonly}
/>
)}
</fieldset>
);
}
Expand All @@ -42,6 +68,10 @@ class ObjectField extends Component {
readonly: false,
};

state = {
additionalProperties: {},
};

isRequired(name) {
const schema = this.props.schema;
return (
Expand All @@ -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,
Expand Down Expand Up @@ -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}
Expand All @@ -141,7 +228,7 @@ class ObjectField extends Component {
formData,
formContext,
};
return <Template {...templateProps} />;
return <Template {...templateProps} onAddClick={this.handleAddClick} />;
}
}

Expand Down
Loading

0 comments on commit 1d54db6

Please sign in to comment.