react-jsonschema-form icon indicating copy to clipboard operation
react-jsonschema-form copied to clipboard

Array of strings using custom widget?

Open daldridge-cs opened this issue 6 years ago • 9 comments

Prerequisites

Description

I have a custom component SelectProperty wrapping a <select> element with string choices. I can use this as a custom widget for a string field without issue; however, I cannot figure out how to also use it for strings in an array.

I'm not (yet) interested in customizing add, delete, or order buttons, etc., so it feels like ArrayFieldTemplate is not the way to go?

Object data schema:

const schema = {
  type: 'object',
  properties: {
    identity: {
      type: 'object',
      title: null,
      properties: {
        name: { type: 'string', title: 'Name', enum: ['stuff', 'things'] },
      },
    },
    editable: {
      type: 'object',
      title: null,
      properties: {
        inputs: {
          type: 'array',
          items: { type: 'string', enum: ['foo', 'bar'] },
          uniqueItems: true,
        },
        outputs: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              output: {
                type: 'string',
                enum: ['baz', 'quux'],
              },
            },
          },
          uniqueItems: true,
        },
        fields: {
          type: 'array',
          items: { type: 'string', enum: ['choice1', 'choice2'] },
          uniqueItems: true,
        },
      },
    },
  },
};

UI schema:

const uiSchema = {
  identity: {
    name: {
      'ui:widget': props => (
        <SelectProperty
          value={props.value}
          options={props.options.enumOptions}
          required
          handleChange={value => props.onChange(value)}
        />
      ),
    },
  },
  editable: {
    inputs: {
      items: {
        'ui:widget': props => (
          <SelectProperty
            value={props.value}
            options={props.options.enumOptions}
            required
            handleChange={value => props.onChange(value)}
          />
        ),
      },
    },
    outputs: {
      items: {
        output: {
          'ui:widget': props => (
            <SelectProperty
              value={props.value}
              options={props.options.enumOptions}
              required
              handleChange={value => props.onChange(value)}
            />
          ),
        },
      },
    },
  },
};

Component attempting to use the form:

export default function PropertyView({
  data,
  handleChange,
}) {
  return (
    <div className="propertyview">
      <Form
        schema={schema}
        uiSchema={uiSchema}
        formData={data}
        onChange={({ formData }) => { handleChange(formData); }}
      />
    </div>
  );
}

The data provided definitely validates against the schema, so that's not the problem. identity.name renders with the SelectProperty just fine. Neither editable.inputs nor editable.outputs uses the custom widget. What am I missing?

daldridge-cs avatar Nov 22 '17 03:11 daldridge-cs

I'm not really sure what's going on. What you posted seems like it ought to work. For example, here's the Widgets page in the Playground setting arrays of booleans. Here's one with strings. It sounds like you hit a bug but I'm not sure what or where.

glasserc avatar Jan 05 '18 19:01 glasserc

The docs for custom widget components mention that only string, number, integer, and boolean types are supported, so presumably you can't do custom widgets for arrays. @glasserc if you're saying that it should work, is that a doc error? If the docs are correct and it doesn't work, I might be able to take a poke at making it work if you can point me in the right direction.

If arrays are not currently supported, the workaround that occurs to me is to have a custom string widget instead that just deserialises and serialises the string into an array and back again as needed.

elyobo avatar Feb 22 '18 21:02 elyobo

OK, so @daldridge-cs is trying to provide a custom widget for an array type, which the docs say are unsupported. The array of strings that @glasserc pointed to, those are not doing that, it uses the ArrayFieldTemplate option instead, which is much less precise because it overrides all array fields, rather than having it specified for a single widget.

Digging further, "normal" arrays do not seem to support custom widgets but it looks like multiselect arrays should.

Is there any particular reason that custom components are supported for "normal" arrays @glasserc?

elyobo avatar Feb 22 '18 22:02 elyobo

@glasserc is the limitation on custom widgets not supporting arrays deliberate or would a PR to address it be considered? What is the rationale behind having a global array field template instead of supporting custom widgets?

elyobo avatar Mar 05 '18 23:03 elyobo

Just to be perfectly clear, we're not talking about using a widget for the array itself, but rather just for the items in that array, right? Multi-select uses a widget because one widget can accommodate all the elements in the array. Not so for an ordinary array.

I guess my array-of-string example isn't really relevant because it functions the same way with or without the ui:widget declaration. However, the booleans one definitely does work correctly.

I just tried the following and it seemed to work totally fine. In other words (except for a bug which I think has to do with serializing functions in the playground), arrays of strings using a custom widget works. I don't see anything wrong with the example posted in the original comment. Sorry, but please let me know if you figure out what's going on.

diff --git a/playground/app.js b/playground/app.js
index 5b30930..40caf6d 100644
--- a/playground/app.js
+++ b/playground/app.js
@@ -361,7 +361,7 @@ class App extends Component {
 
   onSchemaEdited = schema => this.setState({ schema, shareURL: null });
 
-  onUISchemaEdited = uiSchema => this.setState({ uiSchema, shareURL: null });
+  //onUISchemaEdited = uiSchema => this.setState({ uiSchema, shareURL: null });
 
   onFormDataEdited = formData => this.setState({ formData, shareURL: null });
 
@@ -437,7 +437,7 @@ class App extends Component {
                 title="UISchema"
                 theme={editor}
                 code={toJson(uiSchema)}
-                onChange={this.onUISchemaEdited}
+      //onChange={this.onUISchemaEdited}
               />
             </div>
             <div className="col-sm-6">
diff --git a/playground/samples/widgets.js b/playground/samples/widgets.js
index 9a7d575..a04eff5 100644
--- a/playground/samples/widgets.js
+++ b/playground/samples/widgets.js
@@ -40,6 +40,12 @@ module.exports = {
           },
         },
       },
+      nameArray: {
+        type: "array",
+        items: {
+          type: "string"
+        }
+      },
       string: {
         type: "object",
         title: "String field",
@@ -115,6 +121,17 @@ module.exports = {
     readonly: {
       "ui:readonly": true,
     },
+    nameArray: {
+      items: {
+        "hi": "there",
+        "ui:widget": props => {
+          console.log("Rendering items", props);
+          return (
+            <div>Hi!</div>
+          );
+        }
+      }
+    },
     widgetOptions: {
       "ui:widget": ({ value, onChange, options }) => {
         const { backgroundColor } = options;
@@ -169,6 +186,8 @@ module.exports = {
       default: "Hello...",
       textarea: "... World",
     },
+    nameArray: ["hi"],
+
     secret: "I'm a hidden string.",
   },
 };

glasserc avatar Mar 07 '18 21:03 glasserc

Thanks @glasserc

The docs do not include array as a type for which you can have a custom widget - the example is using a custom widget to render each item in the array instead (which is a string and so supports custom widgets), so I guess that is supported, but the docs could possibly be extended to note that use case. I hadn't realised that was how custom widgets and arrays were meant to interact.

I don't think I understand what you mean by "Multi-select uses a widget because one widget can accommodate all the elements in the array. Not so for an ordinary array". I would like to have one widget that handles all of the items at once (e.g. react-select, which would handle single and multi select uses cases for an array) rather than one separate widget for each item, and it doesn't seem like this is supported.

As I noted above, a workaround is to serialize the array options to a string and have the widget unpack and repack the string, but this is a bit clunky (and loses the validation aspects of enum and enumNames as well). Being able to specify a custom widget that handles rendering everything for an array seems more appropriate.

elyobo avatar Mar 07 '18 22:03 elyobo

Ah, I understand now. The original code example shows using a widget for individual items, but actually both you and the original commenter want to use a single widget for the entire array. I think the rationale is because we expected arrays to normally include any number of arbitrary items, but it sounds like you want to use it only for a set of known-in-advance items. In that case, I think you can add uniqueItems to your schema and trigger the MultiSelect code path, which does support widgets as far as I can tell.

glasserc avatar Mar 07 '18 23:03 glasserc

My original example was trying to use a custom widget (a select/drop-down, using the enum/enumNames from the data schema as the allowable options) for each item in an array, homogeneously. Looking at @glasserc's example above, the editable.inputs.items perhaps should have worked?

Separately (not part of this example), I do have the desire to be able to customize the labels for each array entry displayed. Rather than displaying the name of the field in the schema identically for each item as the tile (in the example above, that would either be 'inputs' or 'items' repeated for each -- I forget the behavior this many months after the fact), I'd want to add a number/index -- so "Item 1", "Item 2", etc.

I've managed this by hacking a CustomSchemaField and mutating the props sent to the SchemaField. I don't like it, but it works...

const CustomSchemaField = (props) => {
  // This is empirically our indication that we're handling an array item.
  const label = getOr(null, 'label', props);
  if (label !== null) {
    // Take whatever is configured as the title and append the index+1.
    const index = parseInt(label, 10) + 1;
    const nodes = jsonPath.nodes(props, '$..[\'ui:title\']', 1);
    if (nodes.length) {
      const title = nodes[0].value;
      const targetPath = nodes[0].path.slice(1).join('.');
      const targetValue = `${title.length ? title.concat(' ') : ''}${index}`;
      const newProps = set(targetPath, targetValue, props);
      return (<SchemaField {...newProps} />);
    }
  }
  return (<SchemaField {...props} />);
};

const fields = {
  SchemaField: CustomSchemaField,
};

...

<Form ... fields={fields} ... />

Additionally, I'd also like to customize what is rendered for the add/remove/reorder elements (or better yet, allow drag/drop to reorder) -- but we're talking three separate feature requests/questions at this point.

The biggest win for me would be able to use a custom widget for each item in an array. I'm pretty sure I tried specifying ui:field in the UI schema for array items, and that it was not honored/used. (I do have non-array usage of ui:field working just fine, FWIW.)

daldridge-cs avatar Mar 08 '18 14:03 daldridge-cs

Yes, it does seem like this issue has been a stand-in for three or four issues which should maybe be broken out. @elyobo, if you still have trouble with getting the multi-select thing to work, please feel free to open a different issue.

@daldridge-cs, from what I've seen from poking around the code, there shouldn't be any difference in using array items and non-array items. Array items are handled by use of SchemaField, which then dispatches to e.g. StringField, which supports widgets. If it doesn't, that seems like a bug, but I don't know immediately where the bug is.

glasserc avatar Mar 08 '18 16:03 glasserc

I believe the #2697 may fix this

heath-freenome avatar Jan 15 '23 17:01 heath-freenome