Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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