Skip to content

Corrected defaulting & non-required empty property handling#542

Closed
dropdevcoding wants to merge 3 commits intorjsf-team:masterfrom
dropdevcoding:master
Closed

Corrected defaulting & non-required empty property handling#542
dropdevcoding wants to merge 3 commits intorjsf-team:masterfrom
dropdevcoding:master

Conversation

@dropdevcoding
Copy link
Copy Markdown

Reasons for making this change

In the current version imho the defaulting is broken:

  • When having a non required array property minItems will be set though. Behavior should be: When required, minItems will be applied, when not required and no defaults are set, minItems should not be applied

The value handling imho also is broken for non required fields:

  • When having defined an object which is not required but has required properties, the following should apply:

    • When one of the object's property is set the invariant rules shall apply
    • When no property of the object is set the whole object should be undefined since it is not required because otherwise an empty object is left in formData and the invariant rules will be applied which causes an incorrect validation error
  • When deleting the last item of a non required array, the following should apply:

    • When the last element of a non required array is removed, the whole array should be set to undefined, otherwise the array is left empty in formData and eventual invariants like minItems would be applied and cause validation errors although they should not in this case

If this is related to existing tickets, include links to them as well.

#465

Checklist

  • I'm updating documentation
    • I've checked the rendering of the Markdown text I've added
    • If I'm adding a new section, I've updated the Table of Content
  • I'm adding or updating code
    • I've added and/or updated tests
    • I've updated docs if needed
    • I've run npm run cs-format on my branch to conform my code to prettier coding style
  • I'm adding a new feature
    • I've updated the playground with an example use of the feature

Copy link
Copy Markdown
Contributor

@glasserc glasserc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! Thanks for your contribution! Sorry it took me so long to get to this. In thinking about it, I do agree that there are some issues with defaults and non-requried property handling, but I'm not completely sure about the fixes you propose. I'm concerned about the increased complexity here, where adding required now doubles the possible code paths through all these functions. More test cases would make me feel better, but I only see one.

Here's an example of the kind of edge case I'm concerned about. With this patch applied, I tried this schema:

{
  "type": "object",
  "definitions": {
    "Thing": {
      "type": "object",
      "properties": {
        "name": {
          "type": "string"
        }
      }
    }
  },
  "properties": {
    "foo": {
      "type": "array",
      "minItems": 2,
      "items": {
        "$ref": "#/definitions/Thing"
      }
    }
  }
}

(based on the test case you added). Then, I clicked the "Add" button twice, and then I clicked one "X" button once. I'd expect to have one element left in the array afterwards, but both of them will be removed.

I think this PR should mostly focus on validation/filling in defaults without trying to get too fancy in the UX. But I'd be willing to be convinced if you find that we can't do validation without updating formData or something like that.

// refs #195: revalidate to ensure properly reindexing errors
onChange(formData.filter((_, i) => i !== index), { validate: true });

formData = formData ? formData.filter((_, i) => i !== index) : formData;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we be doing this filter before the if (!required) check?

return value => {
const { formData, onChange } = this.props;
const newFormData = formData.map((item, i) => {
let newFormData = formData.map((item, i) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Does this still need to be let?

if (required && schema.minItems) {
return new Array(schema.minItems).fill(
computeDefaults(schema.items, defaults, definitions)
computeDefaults(schema.items, defaults, definitions, required)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should schema.required be relevant here?


export function cleanUpNonRequiredArray(values) {
values = values.filter(item => {
return item !== null && item !== undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to filter out undefined values here?

export function mergeObjects(obj1, obj2, concatArrays = false) {
// Recursively merge deeply nested objects.
obj1 = obj1 || {};
obj2 = obj2 || {};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this change about?

@dropdevcoding
Copy link
Copy Markdown
Author

Okay, I'll sum up the reasons for the changes:
Having given a schema like

{
  "type": "object"
  "properties": {
   "nonRequiredOuterProp": {
     "type": "object",
     "properties": {
       "requiredInnerProp1": {
         "type": "string"
       },
       "requiredInnerProp2": {
         "type":"array",
         "items": {
           "type": "string"
         }
       }
     },
     "required": [
      "requiredInnerProp1",
      "requiredInnerProp2"
     ]
   }
  }
}

Which means nonRequiredOuterProp may be undefined or if set must match the given criteria,
the form doesn't allow the user to restore the undefined state.

As soon as I fill in requiredInnerProp1 and requiredInnerProp2 the object turns from undefined to

{
   "nonRequiredOuterProp": {
     "requiredInnerProp1": "myInputForRequiredInnerProp1",
     "requiredInnerProp2": "myInputForRequiredInnerProp2"
}

which is correct, but when I remove the whole input the objects falls back to {} which is not valid, since requiredInnerProp1and requiredInnerProp2are not defined but required for nonRequiredPropwhere nonRequiredProp is not required at all.

So the expected behaviour is:
When an object in NOT required, it collapses to undefined (initial state) if none of its properties is defined. Otherwise validation would fail (possibly jsonschemadoes not handle this case right. I also introduced ajvsince I use it for backend validation).

As soon as the user fills out a property the empty object remains and the user hasn't got any chance to transfer it to initial state.

Same behaviour applies to non required arrays.

I hope this will help to clarify. If you got further questions, please don't hesitate to ask :-)

@eggyal
Copy link
Copy Markdown
Contributor

eggyal commented Apr 28, 2017

If I've understood this issue correctly, then I think it's also fixed by #557? In particular 9d0b741 and 132bc60?

@glasserc
Copy link
Copy Markdown
Contributor

Hi @dropdevcoding, thanks for explaining the reasoning behind the PR. What do you think about my objections to the behavior of the changes, and my comments on the code review?

@epicfaace
Copy link
Copy Markdown
Member

Closing this PR due to inactivity. If you'd still like to get this PR in, please make a new PR rebased to the latest code. Thanks!

@epicfaace epicfaace closed this Dec 22, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants