[RFC] Change terra-select to Data API from Children API
See original GitHub issueFeature Request
Description
This is a companion RFC to https://github.com/cerner/terra-core/issues/2492.
We have been discussing using a data API instead of (or in addition to) the
children API for terra-form-select
.
Related
- https://github.com/cerner/terra-core/issues/2492
- https://github.com/cerner/terra-core/issues/2161
- https://github.com/cerner/terra-core/issues/2369
Children API (current)
This is the current API for all terra-form-select
variants. Simply pass a list
of Select.Option
components as children to the Select
.
Predefined Data Children API Select Example
import React from 'react';
import Select from 'terra-form-select';
function PredefinedDataChildrenSelect() {
// nothing special, hardcode a list of options here
// NOTE: this is really rare in real world applications
return (
<Select placeholder="Select a color">
<Select.Option value="blue" display="Blue" />
<Select.Option value="green" display="Green" />
<Select.Option value="purple" display="Purple" />
<Select.Option value="red" display="Red" />
<Select.Option value="violet" display="Violet" />
</Select>
);
}
Prop Driven Data Children API Select Example
import React from 'react';
import Select from 'terra-form-select';
function PropDrivenDataChildrenSelect({ options }) {
// map over list of options, transform shape if necessary
// for this example, we use the shape { id: String, text: String },
// instead of the expected { value: String, display: String }
return (
<Select placeholder="Select a color">
{options.map(option => (
<Select.Option
key={option.id}
value={option.id}
display={option.text}
/>
))}
</Select>
);
}
Predefined Grouped Data Children API Select Example
import React from 'react';
import Select from 'terra-form-select';
function PredefinedGroupedDataChildrenSelect() {
// nothing special, hardcode a list of grouped options here
// NOTE: this is really rare in real world applications
return (
<Select placeholder="Select a color">
<Select.OptGroup label="Shade of blue">
<Select.Option value="blue" display="Blue" />
<Select.Option value="cyan" display="Cyan" />
<Select.Option value="teal" display="Teal" />
<Select.Option value="azul" display="Azul" />
<Select.Option value="aqua" display="Aqua" />
</Select.OptGroup>
<Select.OptGroup label="Shades of green">
<Select.Option value="green" display="Green" />
<Select.Option value="forest" display="Forest Green" />
<Select.Option value="dark" display="Dark Green" />
<Select.Option value="neon" display="Neon Green" />
</Select.OptGroup>
</Select>
);
}
Prop Driven Grouped Data Children API Select Example
import React from 'react';
import Select from 'terra-form-select';
function PropDrivenGroupedDataChildrenSelect({ groups }) {
// map over list of groups
// for each group, map over list of options
// no need to transform data for this example
return (
<Select placeholder="Select a color">
{groups.map(group => (
<Select.OptGroup label={group.label} key={group.label}>
{group.options.map(option => (
<Select.Option
key={option.value}
value={option.value}
display={option.display}
/>
))}
</Select.OptGroup>
))}
</Select>
);
}
All Children API Examples Usage & Render
import React from 'react';
import { render } from 'react-dom';
function ChildrenApiExamples() {
// this list could come from anywhere, or be any shape really
// to illustrate data transformations (a common use case),
// we use the shape { id: String, text: String }
// instead of the expected { value: String, display: String }
const options = [
{ id: 'blue', text: 'Blue' },
{ id: 'green', text: 'Green' },
{ id: 'purple', text: 'Purple' },
{ id: 'red', text: 'Red' },
{ id: 'violet', text: 'Violet' },
];
const groupedOptions = [
{
label: 'Shades of blue',
options: [
{ value: 'blue', display: 'Blue' },
{ value: 'cyan', display: 'Cyan' },
{ value: 'teal', display: 'Teal' },
{ value: 'azul', display: 'Azul' },
{ value: 'aqua', display: 'Aqua' },
],
},
{
label: 'Shades of green',
options: [
{ value: 'green', display: 'Green' },
{ value: 'forest', display: 'Forest Green' },
{ value: 'dark', display: 'Dark Green' },
{ value: 'neon', display: 'Neon Green' },
],
},
];
return (
<div>
<PredefinedDataChildrenSelect />
<PropDrivenDataChildrenSelect options={options} />
<PredefinedGroupedDataChildrenSelect />
<PropDrivenGroupedDataChildrenSelect groups={groupedOptions} />
</div>
);
}
render(<ChildrenApiExamples />, document.getElementById('root'));
Pros
- Matches the native
<select>
API fairly closely, with a few changes<Select.Option>
usesdisplay
prop instead ofchildren
forinnerText
- For hardcoded data, it’s really easy to use
- Allows flexibility in using
<OptGroup>
s or<Option>
s manually
Cons
- Doesn’t handle dynamic data very well (more on this later)
- In real world examples, you need to manually map over data, applying
transforms if necessary, and know that you need to render
<Select.Option>
and<Select.OptGroup>
components here - larger surface area in userland code === more surface area for bugs
Data API
Predefined Data, Data API Select Example
import React from 'react';
import Select from 'terra-form-select';
function PredefinedDataSelect() {
// nothing special, hardcode a list of options here
// can either define the data as a local, or just inline it as done below
// NOTE: this is really rare in real world applications
return (
<Select
placeholder="Select a color"
data={[
{ value: 'blue', display: 'Blue' },
{ value: 'green', display: 'Green' },
{ value: 'purple', display: 'Purple' },
{ value: 'red', display: 'Red' },
{ value: 'violet', display: 'Violet' }
]}
/>
);
}
Prop Driven Data, Data API Select Example
import React from 'react';
import Select from 'terra-form-select';
function PropDrivenDataSelect({ options }) {
// Thought: maybe we allow the consumer to pass an optional dataTransform
// callback which allows the select to handle when the data items aren't
// of the expected shape, like so:
function dataTransform(option) {
return { value: option.id, display: option.text };
}
// delegate the display of options to the Select itself
return (
<Select
placeholder="Select a color"
data={options}
dataTransform={dataTransform}
/>
);
}
Predefined Grouped Data, Data API Select Example
import React from 'react';
import Select from 'terra-form-select';
function PredefinedGroupedDataSelect() {
// nothing special, hardcode a list of grouped options here
// NOTE: this is really rare in real world applications
const groupedOptions = [
{
label: 'Shades of blue',
options: [
{ value: 'blue', display: 'Blue' },
{ value: 'cyan', display: 'Cyan' },
{ value: 'teal', display: 'Teal' },
{ value: 'azul', display: 'Azul' },
{ value: 'aqua', display: 'Aqua' },
],
},
{
label: 'Shades of green',
options: [
{ value: 'green', display: 'Green' },
{ value: 'forest', display: 'Forest Green' },
{ value: 'dark', display: 'Dark Green' },
{ value: 'neon', display: 'Neon Green' },
],
},
];
// Note, this isn't necessarily a good idea here since we need to explore
// a way to tell the select that the options are in fact grouped, another
// prop is used here as a boolean, for example
return (
<Select
placeholder="Select a color"
data={groupedOptions}
groupedOptions
/>
);
}
Prop Driven Grouped Data, Data API Select Example
import React from 'react';
import Select from 'terra-form-select';
function PropDrivenGroupedDataSelect({ groupedOptions }) {
// Note, this isn't necessarily a good idea here since we need to explore
// a way to tell the select that the options are in fact grouped, another
// prop is used here as a boolean, for example
return (
<Select
placeholder="Select a color"
data={groupedOptions}
groupedOptions
/>
);
}
All Data API Examples Usage & Render
import React from 'react';
import { render } from 'react-dom';
function DataApiExample() {
// this list could come from anywhere, or be any shape really
// to illustrate data transformations (a common use case),
// we use the shape { id: String, text: String }
// instead of the expected { value: String, display: String }
const options = [
{ id: 'blue', text: 'Blue' },
{ id: 'green', text: 'Green' },
{ id: 'purple', text: 'Purple' },
{ id: 'red', text: 'Red' },
{ id: 'violet', text: 'Violet' },
];
const groupedOptions = [
{
label: 'Shades of blue',
options: [
{ value: 'blue', display: 'Blue' },
{ value: 'cyan', display: 'Cyan' },
{ value: 'teal', display: 'Teal' },
{ value: 'azul', display: 'Azul' },
{ value: 'aqua', display: 'Aqua' },
],
},
{
label: 'Shades of green',
options: [
{ value: 'green', display: 'Green' },
{ value: 'forest', display: 'Forest Green' },
{ value: 'dark', display: 'Dark Green' },
{ value: 'neon', display: 'Neon Green' },
],
},
];
return (
<div>
<PredefinedDataSelect />
<PropDrivenDataSelect options={options} />
<PredefinedGroupedDataSelect />
<PropDrivenGroupedDataSelect groupedOptions={groupedOptions} />
</div>
);
}
render(<DataApiExample />, document.getElementById('root'));
Pros
- Simpler userland code === less surface area for bugs
- API is more focused and opinionated
- Allows for easier access for optimizations on terra’s end (e.g. windowing and async data)
- Delegates presentational responsibility of options to the
<Select>
itself- OCS doesn’t allow you to roll custom options (yet lol)
- Still flexible enough to allow for grouped options, especially if we allow a
first class prop for
dataTransform
and the interface shape is strictly defined and well documented
Cons
- Deviates from the native
<select>
API - API is more focused and opinionated
- For hardcoded data, it’s not as easy to use. But again, this is really rare in real world code.
Extra Information
Asynchronous Data
The issues with the current Select
’s implementation become clear when you are
using a multi-select variant. If you make a list of selections, they are simply
held in state by value (id).
This is problematic because the selected options display as <Tag>
components
which use the value
(id, non-presentational text) to
find the child with the correct value and get the display from and use that
as the child text
(Related, see: https://github.com/cerner/terra-core/issues/2369)
Could we simply cache the entire selected object? Sure. This would allow us to not have to search through the children for a child with a specified value to get the presentational text. Actually, that’s what I’m proposing here. We could do the following:
Example code
this.state {
selected: []
};
function addSelection(option) {
// this is just for demonstrating the expected shape
const newSelection = {
value: option.props.value,
display: option.props.display,
};
this.setState(state => ({
selected: [...state.selected, newSelection];
}))
}
function removeLastSelected() {
this.setState(state => ({
selected: state.selected.slice(0, state.selected.length - 1),
}));
}
function removeSelectedByValue(value) {
this.setState(state => ({
selected: state.selected.filter(selection => selection.value !== value),
}));
}
This is actually a slight tangent, but is still related to the current children implementation since we search through the children to find the display of the option with the matching value.
Allowing both APIs at the same time
We could do this as well, but I’m cautious here since I think we should err on the side of simpler implementation and an opinionated API. It is also possible to limit the children API to only the default variant since it shouldn’t contain dynamic data or long lists, however, I question the overall value of doing this.
Do you think this is a feature you would use? Could it simplify your userland code? Do you “Thanks, I hate it”? Comments welcome!
Issue Analytics
- State:
- Created 4 years ago
- Reactions:1
- Comments:19 (16 by maintainers)
Top GitHub Comments
For asynchronous operations I think users would favor the data driven API, but I would like to see an analysis of how and why the data API reduces the overall amount of code and complexity within the Select as is alluded to with this RFC.
Many of the arguments presented for the data API can be resolved in the same way, or in a similar way, with a children API. The arguments themselves actually appear to add more code and complexity. This RFC itself presents the potential of additional props for data transforms and managing the opt groups.
dataTransform
andgroupedOptions
. The usage of groups in general appears ambiguous and undecided with the data API whereas this is a non-issue with the children API.The data transform and mapping is an important discussion point. The data returned from a service will likely not match the API necessary for the select options. So the data will need to be massaged into the correct format in both the data API and the children API. The mapping that is repeatedly marked as a pain point with the existing children API is also present in the data driven API. In the scenario where the
dataTransform
isn’t a part of the API the users will still need to map and transform the data returned from a service into correctly formatted options. This would be a requirement for either API, the data driven API does not solve the mapping. It only moves the logic into the additionaldataTransform
(hypothetically this gets added) prop which would make the prop type for options unknown and unstructured. The user could pass any shape and transform the data appropriately for the options. This allows a high level of freedom for the user but also means the shape of the options is unknown and cannot be reliably interpreted.Caching the selected options for dynamic loading would resolve the behavior of the displays reverting back to their base values for both the children and data API. This is not inherently an issue with the children API, this is an issue that is present in both the data API and the children API. A cache of selection options would resolve the issue in either API.
I don’t think enough data has been collected to support this. The data mapping / transforms will be required for both API proposals. The only real difference is where and what objects the data is mapped into.
<Select.Options>
vs{}
.Probably the exact same way planned with the data API. Caching.
Probably the exact same way planned with the data API.
Many teams have reached out with this question wanting to implement their own custom options. Although we are not directly supporting this, many teams are wanting this functionality. Our decision thus far has been not to allow custom options, largely for accessibility reason, but the want and need are present.
No, this is not strange. This is the behavior many React components follow and would be documented on the UI site. It’s also a very similar behavior you’d expect with native html. Most engineers that have prior experience with a Select would likely be familiar with the native select, which uses a children API, or Select2 which also uses the children API.
Fundamentally the children API is just an object. This object could theoretically be treated the same as the
options
prop within the data API. I haven’t seen any use-case presented that the data API resolves that cannot also be resolved in a near identical way with the children API.There is also a really high tax that hasn’t been addressed with moving to the data API. This change will impact every consumer of the Select to transform their APIs. The changes required by splitting the packages are intended to be small initially, this API change will be a lot more impactful.
Many of the pros and cons mentioned above apply to both APIs and would be resolved in a similar way.
All of that said. I think we should continue to discuss and consider the data API, fundamentally I think the decision should come down to “which API is easier to use for the 90% use-case”.
We have some discussion about that here - https://github.com/cerner/terra-core/issues/2254. Seems like we should not open up options to be custom. Instead, if the use case arises, a new component should be created to handle it.