feat: dependent fields
Closes #565
Based on comment https://github.com/netlify/netlify-cms/issues/565#issuecomment-506787922, came up with the following:
- Added support for a new field config option
conditionswhich should hold an array of objects. - Each object should have a
fieldPathattribute to reference the field, wildcards*are supported as well similar to relation widget attrs. And any one ofequal,notEqual,oneOfandpatternconfig option.
So based on the issue's OP, a config like so would work.
- label: 'structure'
name: structure
widget: 'list'
fields:
- {label: "type", name: "type", widget: "select", default: '', options: ['copy', 'divider', 'image', 'infographic', 'productCircle']}
- label: content
name: content
widget: object
fields:
- {label: 'text', name: 'text', widget: 'markdown', conditions: [{ fieldPath: 'structure.*.type', oneOf: ['divider', 'productCircle', infographic, image, ''] }] }
- {label: 'src', name: 'src', widget: 'image', conditions: [{ fieldPath: 'structure.*.type', oneOf: ['divider', 'productCircle', copy, ''] }] }
- {label: 'overlay', name: 'overlay', widget: 'image', conditions: [{ fieldPath: 'structure.*.type', oneOf: ['divider', 'productCircle', copy, image, ''] }] }
Example config that tries to mimic dynamic dropdown:
- {label: "Continent", name: "continent", widget: "select", default: '', options: ['Europe', 'Asia']}
- {label: "Europe", name: "countries_europe", widget: "select", options: ['spain', 'italy', 'france'], conditions: [{ fieldPath: 'continent', oneOf: ['Asia', ''] }] }
- {label: "Asia", name: "countries_asia", widget: "select", options: ['india', 'japan', 'china'], conditions: [{ fieldPath: 'continent', oneOf: ['Europe', ''] }] }
To do:
- [ ] Add cypress test
- [ ] Update docs
Looks like the field will be hidden when the condition is met and not shown only when the condition is met. Am I correct?
The field will be hidden when any(not all) of the conditions are met, I think we should change conditions config to something like hideConditions for clarity.
Doing the following seems more intuitive to me (and matches #565 (comment)):
The empty string is just to hide both Europe and Asia dropdown( and then show the appropriate dropdown as the continent dropdown value changes) for new entries since the default value of the continent dropdown is an empty string, the empty string can be omitted, it's up to the user.
Using the wildcard * is great for supporting lists. I wonder if we should create a dedicated data structure to hold the tree of widgets since we already pass parentIds for the nested validation.
Not sure how the tree structure will be useful here if you can explain with some codes, better. The fieldPath value is used directly on the entry data. The trick here is for the user to match the fieldPath correctly especially when using wildcard for list.
The field will be hidden when any(not all) of the conditions are met, I think we should change
conditionsconfig to something likehideConditionsfor clarity.
With hide notation each time someone adds a select option it will require editing all other fields or the condition will break right?
With show notation you'd only need to do:
- {label: "Continent", name: "continent", widget: "select", default: '', options: ['Europe', 'Asia']}
- {label: "Europe", name: "countries_europe", widget: "select", options: ['spain', 'italy', 'france'], conditions: [{ fieldPath: 'continent', equals: 'Europe' }] }
- {label: "Asia", name: "countries_asia", widget: "select", options: ['india', 'japan', 'china'], conditions: [{ fieldPath: 'continent', equals: 'Asia' }] }
Then adding a new option:
- {label: "Continent", name: "continent", widget: "select", default: '', options: ['Europe', 'Asia','America']}
- {label: "Europe", name: "countries_europe", widget: "select", options: ['spain', 'italy', 'france'], conditions: [{ fieldPath: 'continent', equal: 'Europe' }] }
- {label: "Asia", name: "countries_asia", widget: "select", options: ['india', 'japan', 'china'], conditions: [{ fieldPath: 'continent', equal: 'Asia' }] }
- {label: "America", name: "countries_america", widget: "select", options: ['mexico', 'us'], conditions: [{ fieldPath: 'continent', equal: 'America' }] }
Not sure how the tree structure will be useful here if you can explain with some codes, better. The
fieldPathvalue is used directly on the entry data. The trick here is for the user to match thefieldPathcorrectly especially when using wildcard for list.
Let me dig into that a bit more.
With hide notation each time someone adds a select option it will require editing all other fields or the condition will break right?
With show notation you'd only need to do:
Agreed.
When will this feature will be available?
When will this feature will be available?
See my comment here https://github.com/netlify/netlify-cms/issues/565#issuecomment-792674198
Closing this as stale. We can revisit in the future, or if someone wants to pick it up per https://github.com/netlify/netlify-cms/pull/3891#pullrequestreview-434938425
Hey there! I'm in need of this so happy to try to pick this up again from https://github.com/decaporg/decap-cms/pull/3891#pullrequestreview-434938425
sooo is this not a feature? seems like a really basic need for ANY CMS lol
@larenelg I reopened this PR if you're still interested
Could this use case be solved by making variable types work for object widgets? That's effectively what we do, by having lists with min and/or max set to 1.
It would have the benefits of being (presumably) easier to implement/maintain and easier to understand by users.
@mmkal can you explain how that would enable the conditional feature? Maybe with an example? Thanks
Sure, I'll have to adjust the original example, because the example from the OP, and proposed solution, is already a list so can just use variable types right now: https://github.com/decaporg/decap-cms/issues/565#issuecomment-506787922
- label: structure
name: structure
widget: list
types:
- name: copy
widget: object
fields:
- name: text
widget: markdown
- name: productCircle
widget: object
fields:
- name: text
widget: markdown
- name: src
widget: image
- name: infographic
widget: object
fields:
- name: text
widget: markdown
- name: src
widget: image
- name: overlay
widget: image
...
I think this is much easier to follow when looking at the config too. Answering the question "what subfields does an infographic have" doesn't involve darting around and looking at all of the condition labels, and mentally parsing the custom condition syntax (which has to be learned and remembered).
Worth noting, there is one slight downside which is that fields in common between the various types have to be defined explicitly on each. That can be solved fairly easily with yaml references (or my team generate the config from typescript anyway so we can use helper methods and strong types), but also more importantly, by the same token, it allows for the different types to independently define their fields. markdown on productCircle might have a different hint than markdown on infographic.
So, IMO the original issue could be closed with a suggestion like this. But if someone wanted to make a non-list field like this, that is a missing feature. It'd be nice to be able to do the same thing for objects, e.g.:
- label: structure
name: structure
widget: object # 👈 not supported right now, only `widget: list` is
types:
- name: copy
widget: object
fields:
- name: text
widget: markdown
- name: productCircle
widget: object
fields:
- name: text
widget: markdown
- name: src
widget: image
- name: infographic
widget: object
fields:
- name: text
widget: markdown
- name: src
widget: image
- name: overlay
widget: image
...
I think the use case for that is smaller, but it could make sense on folder collections - different entries could use different structures with the example above.
Thanks for the comment @mmkal. This is a feature that has been discussed many times with many proposed solutions, so I would like to take some time with the team to carefully review them and decide where we want to go. It is one of the most requested features, so it's important not to rush.
any updates? come on
Here's something we've started doing, along the lines of my above comment, but also adds support for objects. Basically, just inspect the field and use a shimmed list widget control instead of object when the types field is defined:
import {List} from 'immutable'
import React from 'react'
import CMS from 'decap-cms-app'
export function optionalizeObject() {
const listWidget = CMS.getWidget('list')
const objectWidget = CMS.getWidget('object')
class NewObjectControl extends React.Component<any> {
render() {
if (this.props.field.get('required') === false || this.props.field.get('types')) {
return (
<listWidget.control
{...this.props}
onChange={(e: List<any>) => this.props.onChange(e.get(0))}
value={List([this.props.value].filter(Boolean))}
/>
)
}
return <objectWidget.control {...this.props} />
}
}
CMS.registerWidget('object', NewObjectControl, listWidget.preview)
}
Here's a demo:
collection definition:
{
label: 'test test',
name: 'test',
folder: 'shared/content/src/data/marketing/test',
slug: '{{slug}}',
format: 'yml',
create: true,
fields: [
{
name: 'some_string_field',
widget: 'string',
},
{
name: 'structure',
widget: 'object',
types: [
{
name: 'copy',
widget: 'object',
fields: [
{name: 'text', widget: 'markdown'},
],
},
{
name: 'productCircle',
widget: 'object',
fields: [
{name: 'text', widget: 'markdown'},
{name: 'src', widget: 'image'},
],
},
{
name: 'infographic',
widget: 'object',
fields: [
{name: 'text', widget: 'markdown'},
{name: 'src', widget: 'image'},
{name: 'overlay', widget: 'image'},
],
},
],
}
]
},
How it renders in the UI:
https://github.com/decaporg/decap-cms/assets/15040698/76d8df8d-be2a-4a6e-88d6-52e70aa7a653
Files generated by the above:
Bonus: this also improves support for object widgets with required: false - they become "lists" too, visually - the add/remove buttons define the object and set it to null respectively. Before this, we had trouble with objects that were optional, but had required subfields, when defined.
In the spirit of not adding complexity to both the product and the codebase, I think the above would be a better solution than this pull request, so I would propose the following changes instead:
- Make the above shim built into decapcms, so it doesn't have to be done in userland. This should be easy to do, and improves support for optional object, for all users, as a bonus.
- Allow for
typesto be define on folder collections, instead of requiringfields. Without this, there's a limitation of the above method that you can't have thetypefield on the top level (i.e. it has to be{structure: {type: copy, text: abc}}and can't be{type: copy, text: abc}. If we're lucky this wouldn't be too hard to achieve either.
1 can be done first, then 2. I'd be happy to open a PR.
@martinjagodic what do you think?
@mmkal I like this a lot! It achieves a lot with very little intervention. A PR for solution 1 would be amazing.