Skip to content

Commit

Permalink
feat: Render Custom widget for standalone array fields (#2697)
Browse files Browse the repository at this point in the history
* feat: Render Custom widget for standlone array fields

* remove redudnat example from docs

* fix: english wording

* Update packages/core/src/utils.js

* Update utils.js

Co-authored-by: Ashwin Ramaswami <[email protected]>
  • Loading branch information
alfonsoar and epicfaace authored Feb 18, 2022
1 parent b54635b commit ccd4ac9
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ should change the heading of the (upcoming) version to include a major version b
## @rjsf/core
- Add React 17 as a supported peer-dependency
- Introduce `idSeparator` prop to change the path separator used to generate field names (https://github.com/rjsf-team/react-jsonschema-form/pull/2628)
- Array fields support custom widgets (previously, only multiple-choice arrays with `enums` or `uniqueItems` support it) (https://github.com/rjsf-team/react-jsonschema-form/pull/2697)

## @rjsf/material-ui
- Added React 17 as an optional peer dependency
Expand Down
5 changes: 3 additions & 2 deletions docs/advanced-customization/custom-templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ _ | Custom Field | Custom Template | Custom Widget

## ArrayFieldTemplate

You can use an `ArrayFieldTemplate` to customize how your arrays are rendered.
This allows you to customize your array, and each element in the array.
You can use an `ArrayFieldTemplate` to customize how your arrays are rendered.
This allows you to customize your array, and each element in the array. You can also customize arrays by specifying a widget in the relevant `ui:widget` schema, more details over on [Custom Widgets](../usage/arrays.md#custom-widgets).


```jsx
const schema = {
Expand Down
1 change: 1 addition & 0 deletions docs/advanced-customization/custom-widgets-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ You can provide your own custom widgets to a uiSchema for the following json dat
- `number`
- `integer`
- `boolean`
- `array`

```jsx
const schema = {
Expand Down
37 changes: 37 additions & 0 deletions docs/usage/arrays.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,43 @@ render((
), document.getElementById("app"));
```

## Custom widgets

In addition to [ArrayFieldTemplate](../advanced-customization/custom-templates.md#arrayfieldtemplate) you use your own widget by providing it to the uiSchema with the property of `ui:widget`.

Example:

```jsx
const CustomSelectComponent = props => {
return (
<select>
{props.value.map((item, index) => (
<option key={index} id="custom-select">
{item}
</option>
))}
</select>
);
};

const schema = {
type: "array",
title: "A multiple-choice list",
items: {
type: "string",
},
};

const uiSchema = {
"ui:widget": "CustomSelect"
};

const widgets = {
CustomSelect: CustomSelectComponent,
},

render((<Form schema={schema} uiSchema={uiSchema} widgets={widgets} />), document.getElementById("app"));
```

## Specifying the minimum or maximum number of items

Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"tdd": "cross-env NODE_ENV=test mocha --require @babel/register --watch --require ./test/setup-jsdom.js test/**/*_test.js",
"test": "cross-env BABEL_ENV=test NODE_ENV=test mocha --require @babel/register --require ./test/setup-jsdom.js test/**/*_test.js",
"test-coverage": "cross-env NODE_ENV=test nyc --reporter=lcov mocha --require @babel/register --require ./test/setup-jsdom.js test/**/*_test.js",
"test-debug": "cross-env NODE_ENV=test mocha --require @babel/register --require ./test/setup-jsdom.js --debug-brk --inspect test/Form_test.js"
"test-debug": "cross-env NODE_ENV=test mocha --require @babel/register --require ./test/setup-jsdom.js --inspect-brk --inspect test/Form_test.js"
},
"lint-staged": {
"{src,test}/**/*.js": [
Expand Down
58 changes: 55 additions & 3 deletions packages/core/src/components/fields/ArrayField.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
isFilesArray,
isFixedItems,
allowAdditionalItems,
isCustomWidget,
optionsList,
retrieveSchema,
toIdSchema,
Expand Down Expand Up @@ -442,15 +443,19 @@ class ArrayField extends Component {
/>
);
}
if (isMultiSelect(schema, rootSchema)) {
// If array has enum or uniqueItems set to true, call renderMultiSelect() to render the default multiselect widget or a custom widget, if specified.
return this.renderMultiSelect();
}
if (isCustomWidget(uiSchema)) {
return this.renderCustomWidget();
}
if (isFixedItems(schema)) {
return this.renderFixedArray();
}
if (isFilesArray(schema, uiSchema, rootSchema)) {
return this.renderFiles();
}
if (isMultiSelect(schema, rootSchema)) {
return this.renderMultiSelect();
}
return this.renderNormalArray();
}

Expand Down Expand Up @@ -532,6 +537,53 @@ class ArrayField extends Component {
return <Component {...arrayProps} />;
}

renderCustomWidget() {
const {
schema,
idSchema,
uiSchema,
disabled,
readonly,
required,
placeholder,
autofocus,
onBlur,
onFocus,
formData: items,
registry = getDefaultRegistry(),
rawErrors,
name,
} = this.props;
const { widgets, formContext } = registry;
const title = schema.title || name;

const { widget, ...options } = {
...getUiOptions(uiSchema),
};
const Widget = getWidget(schema, widget, widgets);
return (
<Widget
id={idSchema && idSchema.$id}
multiple
onChange={this.onSelectChange}
onBlur={onBlur}
onFocus={onFocus}
options={options}
schema={schema}
registry={registry}
value={items}
disabled={disabled}
readonly={readonly}
required={required}
label={title}
placeholder={placeholder}
formContext={formContext}
autofocus={autofocus}
rawErrors={rawErrors}
/>
);
}

renderMultiSelect() {
const {
schema,
Expand Down
12 changes: 11 additions & 1 deletion packages/core/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,8 @@ export function getDisplayLabel(schema, uiSchema, rootSchema) {
if (schemaType === "array") {
displayLabel =
isMultiSelect(schema, rootSchema) ||
isFilesArray(schema, uiSchema, rootSchema);
isFilesArray(schema, uiSchema, rootSchema) ||
isCustomWidget(uiSchema);
}

if (schemaType === "object") {
Expand Down Expand Up @@ -557,6 +558,15 @@ export function isFixedItems(schema) {
);
}

export function isCustomWidget(uiSchema) {
return (
// TODO: Remove the `&& uiSchema["ui:widget"] !== "hidden"` once we support hidden widgets for arrays.
// https://react-jsonschema-form.readthedocs.io/en/latest/usage/widgets/#hidden-widgets
"widget" in getUiOptions(uiSchema) &&
getUiOptions(uiSchema)["widget"] !== "hidden"
);
}

export function allowAdditionalItems(schema) {
if (schema.additionalItems === true) {
console.warn("additionalItems=true is currently not supported");
Expand Down
65 changes: 65 additions & 0 deletions packages/core/test/ArrayField_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ describe("ArrayField", () => {
return <div id="custom">{props.rawErrors}</div>;
};

const CustomSelectComponent = props => {
return (
<select>
{props.value.map((item, index) => (
<option key={index} id="custom-select">
{item}
</option>
))}
</select>
);
};

beforeEach(() => {
sandbox = createSandbox();
});
Expand Down Expand Up @@ -1803,6 +1815,59 @@ describe("ArrayField", () => {
});
});

describe("Custom Widget", () => {
it("if it does not contain enums or uniqueItems=true, it should still render the custom widget.", () => {
const schema = {
type: "array",
items: {
type: "string",
},
};

const { node } = createFormComponent({
schema,
uiSchema: {
"ui:widget": "CustomSelect",
},
formData: ["foo", "bar"],
widgets: {
CustomSelect: CustomSelectComponent,
},
});

expect(node.querySelectorAll("#custom-select")).to.have.length.of(2);
});

it("if the schema has fixed items, it should still render the custom widget.", () => {
const schema = {
type: "array",
items: [
{
type: "string",
title: "Some text",
},
{
type: "number",
title: "A number",
},
],
};

const { node } = createFormComponent({
schema,
uiSchema: {
"ui:widget": "CustomSelect",
},
formData: ["foo", "bar"],
widgets: {
CustomSelect: CustomSelectComponent,
},
});

expect(node.querySelectorAll("#custom-select")).to.have.length.of(2);
});
});

describe("Title", () => {
const TitleField = props => <div id={`title-${props.title}`} />;

Expand Down
19 changes: 19 additions & 0 deletions packages/core/test/utils_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
schemaRequiresTrueValue,
canExpand,
optionsList,
isCustomWidget,
getMatchingOption,
} from "../src/utils";
import { createSandbox } from "./test_utils";
Expand Down Expand Up @@ -3673,6 +3674,24 @@ describe("utils", () => {
getDisplayLabel({ type: "array" }, { "ui:widget": "files" })
).eql(true);
});
it("custom type", () => {
expect(
getDisplayLabel(
{ type: "array", title: "myAwesomeTitle" },
{ "ui:widget": "MyAwesomeWidget" }
)
).eql(true);
});
});
});

describe("isCustomWidget()", () => {
it("When the function is called with a custom widget in the uiSchema it returns true", () => {
expect(isCustomWidget({ "ui:widget": "MyAwesomeWidget" })).eql(true);
});

it("When the function is called without a custom widget in the schema it returns false", () => {
expect(isCustomWidget({ "ui:fields": "randomString" })).eql(false);
});
});

Expand Down

0 comments on commit ccd4ac9

Please sign in to comment.