-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
getDefaultFormState populates default data to whole formData #579
Conversation
… descriptions & titles.
non-defined properties should probably be out of scope) This reverts commit 742c679.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for your contribution! Sorry it took me so long to look at it -- I was intimidated by the number of commits 😊
I'm glad you tackled this. I think you're exposing some very complicated and even strange behavior regarding the use of default
. For example, we have an existing test "should keep parent defaults if they don't have a node level default"
that I sort of disagree with. I think we need a good, clear, crisp definition of when we fill in defaults. To my mind, if a schema has a default, we use that default only when the field is completely missing. However, the test I mentioned fills in a schema's children even though the field is present!
Similarly, I am a little confused about when parentDefaults
enter into play and when they override the child's defaults. If I have {default: {"a": 1}, properties: {"a": {default: 4}}}
, my intuition is that the computed default should be {"a": 1}
-- you fill in the default for the object, and then check its children. But that's at odds with your implementation, and I could be swayed to an alternate viewpoint/strategy, especially if it has a clear summary.
How about tests for:
- An array with both a default value, and items which have a default value.
- Arrays with fewer items than
minItems
successfully get filled from the defaults - Arrays with fixed fields (
items
is an array) as well asdefault
- Arrays with a default value and a
$ref
that points to something with a default value - Some of the above for objects too :)
- Objects with a default value that contains nested children as well as a
$ref
that points to something with a default value that also has nested children
src/utils.js
Outdated
schema.items[i] && | ||
"type" in schema.items[i] | ||
? schema.items[i] | ||
: schema.items; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If schema.items
is an array, but it isn't long enough, or its "type"
is missing from one element, you will use the whole array as the child schema.
Hehe yeah, for some reason all my pull requests show all the commits I've made combined in my previous pull requests. My hands hands pretty full at the moment, so this will have to wait. I'll dig deeper into your feedback later - hopefully sometime next month. Thanks for the input :-) |
…est cases for array default data filling.
I found some issues when writing the tests. Local schema defaults weren't used if there was a The fixes broke one test - the logic is indeed inconsistent at the moment. Now I'd remove the
The logic I'm using is that we favor always the most nested default. I think your logic that the parent default should be filled first is logical, but somehow extending the parent default with child defaults seems intuitive to me. The situation with nested defaults is nonsensical, since no matter which way you compute the defaults, one of those defaults wouldn't be used. It's conflicting any way you look at it. From a practical point of view, the only reason I can think of why someone would want to use nested defaults is when someone wants to extend an existing schema object, which has defaults and wants to override some child default by providing a default to the child. By using the most nested defaults, like in the logic I'm using here, that would be possible. It works the same way how we favor local schemas instead of a refered schema, without using a defined schema (I wrote a test case Here's an example, since that was probably hard to read. We have a schema A This is just one point of view - I'm not using nested defaults so I'm not sure what the practical point really is. :) |
I decided to revert the default population order to favor parents instead of children, because the newly merged pull requests #663 (see tests
So this is the logic now. Hence, I made these changes to the existing tests:
|
Bumping, since this is ~3rd time I've got to handle a conflict with this PR. This is a complicated issue and I've failed to express clearly why this solution is better than the current default populating. Partly that's because, as we have discussed, we currently don't have a clear reasoning with populating the defaults. My previous comment summarises the current conflicting approaches and the solution that this PR offers. |
does this solve #768 also? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, just to reiterate, thank you very much for your patience, your thoroughness, and your forbearance with this. I think it's really important to make sure that the fundamental "axioms" of a system are coherent and make sense, and I see that you're doing that here. I'm sorry for taking so long to review it in favor of tiny little PRs that don't really contribute much. I'll try to follow up more aggressively on this PR in particular. I've "starred" this PR in my email so hopefully that will help.
I'd like to consider renaming this PR to something like Clean up edge cases with getDefaultFormState
. What do you think?
I still feel somewhat confused by the nature of the axiom being set forth in this PR. The axiom I would have expected is "If an object is completely absent (or undefined), fill it in using its schema's default
value, and then recur on its properties
/items
." However, it seems like this PR's effect is something more like "If an object is missing, fill it in using its schema's default
value, or alternately from the default
of any of its ancestors' defaults." I tried to express what I thought made sense in a previous comment, but I realize now that I spoke incompletely. I would expect {default: {"a": 1}, properties: {"a": {default: 4}}
to have two different behaviors depending on formData
. With formData
undefined
, I would expect to get {"a": 1}
. With formData
{}
(or {"b": 5}
), I would expect to get {"a": 4}
(or {"b": 5, "a": 4}
).
Now, I'm not strongly wedded to this behavior and there could be reasons why it doesn't really make sense (maybe because in an HTML form, there's no clear distinction between a missing object and an empty one?). So I'm happy to be convinced why the behavior you've chosen is consistent/logical/makes sense, but I still don't even think I understand this behavior. Could you try to distill it down to a one-to-three sentence summary for me?
I know this is really frustrating because you've obviously spent a lot of time on this PR and it doesn't feel like it's making progress, but I actually feel like it's pretty good and would like to see something like it land, so please don't give up hope! If you don't have a large amount of time to carry it forward, just say so and maybe I can find the time to take over.
let childSchema = undefined; | ||
|
||
if (Array.isArray(schema.items)) { | ||
if (schema.items.length > i && schema.items[i]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this schema.items[i]
condition correct? My reading of http://json-schema.org/latest/json-schema-validation.html#rfc.section.6.4.2 is that we only use additionalItems
"at a position greater than the size of 'items'".
(parentDefaults || []).length, | ||
minItemsLength, | ||
Array.isArray(schema.items) ? schema.items.length : 0 | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is quite a hairy condition. Could we break this into a few if statements, possibly with comments explaining why e.g. we want 0 elements if it's a multi-select with minItems (but not in multiselects without minItems)? (I know to some extent you inherited this code, but feel free to change anything that you can't come up with an explanation for.)
if (parentDefaults && parentDefaults.length > i) { | ||
defaultItem = parentDefaults[i]; | ||
} else if ("default" in schema && schema.default.length > i) { | ||
defaultItem = schema.default[i]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this a bit surprising. I guess I would have assumed that we take either parentDefaults
in their entirety, or schema.default
in its entirety, but not both.
if (parentDefaults && key in parentDefaults) { | ||
propParentDefault = parentDefaults[key]; | ||
} else if (schema.default && key in schema.default) { | ||
propParentDefault = schema.default[key]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same concern here about taking both parentDefaults
and schema.default
.
@@ -168,7 +168,7 @@ describe("utils", () => { | |||
expect( | |||
getDefaultFormState(schema, { level1: { level2: { leaf4: 4 } } }) | |||
).eql({ | |||
level1: { level2: { leaf1: 1, leaf2: 2, leaf3: 3, leaf4: 4 } }, | |||
level1: { level2: { leaf1: 1, leaf2: 1, leaf3: 1, leaf4: 4 } }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See, this doesn't make any sense to me. I would have expected { level1: { level2: { leaf3: 3, leaf4: 4 } } }
. I would have expected level1.default
not to apply because there is already a level1
field, and level1.level2.default
to not apply because there is already a level1.level2
field, and level1.level2.leaf3.default
to apply because there is no level1.level2.leaf3
. Could you summarize under what circumstances a default is filled in?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree @glasserc
expect(getDefaultFormState(schema, undefined, schema.definitions)).eql({ | ||
foo: 42, | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
}, | ||
}; | ||
|
||
expect(getDefaultFormState(schema, [0, undefined])).eql([0, 2]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same concern here about filling in elements of default
when the field itself already exists.
expect(getDefaultFormState(schema, [1])).eql([1, 0]); | ||
}); | ||
|
||
it("should use array array default in favor or array item default even if it is a ref", () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
array array default
?
expect(getDefaultFormState(schema, [undefined], schema.definitions)).eql([ | ||
0, | ||
]); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same concern about filling in the toplevel default when the element exists. I think this test would make sense to me if instead of [undefined]
, you had undefined
. However, as written, I would have expected it to be [1]
.
"Do you have a pet?": "Yes", | ||
"Does it bite?": true, | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very cool!
Hey, Currently I don't have the time for this one. I read quickly your feedback (thanks!), and the axiom you are suggesting seems intuitive. Feel free to take the wheel :-) |
Any update on this? |
@glasserc has left some comments, so if anyone is interested in continuing from this PR and resolving the issues (or fixing this problem another way), feel free to do so. |
I'm closing this since the original issue is fixed by #1499 and I don't remember anymore if this PR had something else relevant in it, and there are so many conflicts that I don't find it likely that anyone would dig into this anymore. |
Reasons for making this change
getDefaultFormState
didn't populate formData with existing array items inside object properties with default data, and therefore wasn't working correctly with deeply nested data. I'm sorrycomputeDefaults
had to be rewritten - I had to addformData
to the parameters.Since the logic is pretty complex and I had to rewrite it, feel free to suggest tests to add.
Checklist
npm run cs-format
on my branch to conform my code to prettier coding style