Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Array of strings using custom widget? #774

Closed
1 task done
daldridge-cs opened this issue Nov 22, 2017 · 10 comments
Closed
1 task done

Array of strings using custom widget? #774

daldridge-cs opened this issue Nov 22, 2017 · 10 comments
Labels
possibly close To confirm if this issue can be closed

Comments

@daldridge-cs
Copy link

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?

@glasserc
Copy link
Contributor

glasserc commented Jan 5, 2018

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.

@elyobo
Copy link

elyobo commented Feb 22, 2018

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
Copy link

elyobo commented Feb 22, 2018

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
Copy link

elyobo commented Mar 5, 2018

@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?

@glasserc
Copy link
Contributor

glasserc commented Mar 7, 2018

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.",
   },
 };

@elyobo
Copy link

elyobo commented Mar 7, 2018

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.

@glasserc
Copy link
Contributor

glasserc commented Mar 7, 2018

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.

@daldridge-cs
Copy link
Author

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.)

@glasserc
Copy link
Contributor

glasserc commented Mar 8, 2018

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.

@heath-freenome
Copy link
Member

I believe the #2697 may fix this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
possibly close To confirm if this issue can be closed
Projects
None yet
Development

No branches or pull requests

5 participants