react-value-adapter v0.1.0
react-value-adapter
Map/adapt a component's value
/onChange
props using a Map or function.
Getting started
yarn add react-value-adapter
Usage
Tri-state checkbox
Let's say you have a tri-state enum field in your database, whose values can be one of 'never', 'sometimes', or 'always'.
But your tri-state checkbox requires its value prop to be one of false
, null
, or true
, respectively.
So you have to convert between the value from your database and the value required by your child component.
You would use a mapping like this:
"never"
maps tofalse
"sometimes"
maps tonull
"always"
maps totrue
Sure, you could write your own conversion logic in the parent component. But keep in mind that you not only have to map from the parent value, you also have to take the value provided to the child component's onChange handler and map it back to a parent value.
react-value-adapter
encapsulates that concern, providing an adapter
component that maps between the values required by one component and those
required by the other component (both directions).
You tell it what mapping to use, and how to extract the value
out of the onChange
handler, and it handles the rest:
Example:
const onChangeAdapterTriStateCheckbox = (onChangeFromParent, parentToChildValues) => (
(event, checked) => {
if (!onChangeFromParent) return
const parentValue = parentToChildValues.get(checked)
onChangeFromParent(parentValue, checked)
}
)
const parentToChildValues = new Map([
['never', false],
['sometimes', null],
['always', true],
])
<AdaptValue
// How to extract the value out of the onChange handler
onChangeAdapter={onChangeAdapterTriStateCheckbox}
// What mapping to use
parentToChildValues={parentToChildValues}
// The props that you would like to be able to just pass to your child
// component
childValueProp='checked'
value={parentValue}
onChange={(parentValue, childValue) => {
setChildValue(childValue)
setParentValue(parentValue)
}}
>
{({checked, onChange}) => (<>
<MuiTriStateCheckbox
value="value"
checked={checked}
onChange={onChange}
/>
</>)}
</AdaptValue>
At this point you're probably thinking that seems a bit verbose for what it's trying to do. Couldn't we extract things further to make this even simpler and cleaner?
And the answer is yes, of course we can. This child-as-a-function option is the most flexible approach, but it tends to be kind of verbose. Read on for some other options...
Extracting an adapter component using higher-order component / currying
What if we could create a new component based on another component, and it would automatically convert between values of one type (the values our parent component provides) and the values our child component expects?
Then we could easily reuse it anywhere in our app without having to pass all the adapter configuration around each time we used it (like we would if we were using <AdaptValue>
directly).
How about something like this?
export const MuiTriStateCheckbox_MappedValues = withAdaptValue(MuiTriStateCheckbox, {
parentValueProp: 'value',
childValueProp: 'checked',
onChangeAdapter,
}
)
or even:
export const AlwaysSometimesNever_MuiTriStateCheckbox = withAdaptValue(MuiTriStateCheckbox, {
parentValueProp: 'value',
childValueProp: 'checked',
onChangeAdapter,
parentToChildValues: parentToChildValues
}
)
This example says "I want a component that is the MuiTriStateCheckbox
except that it accepts a value
and translates it to a checked
prop that gets passed to the underlying MuiTriStateCheckbox
. It extracts the value from the onChange handler thusly."
The withAdaptValue
higher-order component returns an adapted component according to the configuration given.
It can then be used like a stand-in for the original MuiTriStateCheckbox
:
<AlwaysSometimesNever_MuiTriStateCheckbox
value={map_parentValue}
onChange={(parentValue, checked) => { setParentValue(parentValue); setChecked(checked) } }
/>
... with the values transparently being converted between your "parent value" domain and the "child value" domain, and the name of the value prop translated as you specified, all in the background. You wouldn't even have to notice or care that that's what it's doing for you.
Other examples
Checkbox
Let's say you have a checkbox and you want to map a 'yes' value to checked={true} in the checkbox.
(To do: simplify and explain)
<AdaptValue
onChangeAdapter={onChangeAdapterUsingMapFromChecked}
parentToChildValues={check_parentToChildValues}
childValueProp={'checked'}
value={check_parentValue}
onChange={(parentValue, childValue) => {
check_setChildValue(childValue)
check_setParentValue(parentValue)
}}
>
{({parentValue, childValue, onChange}) => (<>
<label>
<input type="checkbox" name="check_demo"
checked={childValue}
onChange={onChange}
/>
Checkbox
</label>
(parentValue: {JSON.stringify(parentValue)}, childValue: {JSON.stringify(childValue)})
</>)}
</AdaptValue>
Other examples
See the demo (yarn start
) for more examples.
Use composition: create your own wrapper components
To avoid duplication, you can wrap <AdaptValue>
in your own wrapper component:
function Rot13ValueAdapter(props) {
return (
<AdaptValue
parentToChildValue={rot13}
onChangeAdapter={onChangeForParent => ({ target: { value } }) => onChangeForParent(rot13(value), value)}
{...props}
/>
)
}
<Rot13ValueAdapter
onChange={(parentValue, value) => { setParentValue(parentValue) }}
value={text_parentValue}
>
{({childValue, onChange}) => (<>
<input type="text"
value={childValue}
onChange={onChange}
/>
</>)}
</Rot13ValueAdapter>
...
If you need even more control, you can use the provided useAdaptValue
hook.
API reference
(Coming soon!)
Value adapters
useAdaptValue
<AdaptValue>
<AdaptValue_>
HiddenInput
withHiddenInput
StateWrapper
<StateWrapper defaultValue="text">
{({onChange, ...props}) => (
<input type="text" {...props} onChange={({target: {value}}) => onChange(value)} />
)}
</StateWrapper>
Frequently anticipated questions
Why is a Map
used for the mapping instead of a regular object literal?
Because object literals only support string keys and we want to be able to map between any object and any object. That is exactly what a Map gives us.
Should I use AdaptValue
or AdaptValue_
?
You should probably use AdaptValue
.
AdaptValue_
is an injector
component and
provides a more concise syntax. That is its main advantage. Its disadvantages are:
- It is inflexibile. You can't, for example, wrap multiple elements.
- It is harder to understand
- Since it uses
React.cloneElement
, it adds no new components/elements to the tree, so if you inspect it in the React Components dev tool, you may be surprised to seeAdaptValue_
listed in the tree — without any children. Your child component won't be listed because this component takes its place.
AdaptValue
uses the "children as function" pattern, which means you have to provide a children prop
that is a function. That function, when called, can return any elements, it is more flexible than
the injector component pattern.
The AdaptValue
option is also more explicit: You can see exactly which values are yielded to your
children, and you are responsible for connecting those values to your child element(s), which makes
it easier to follow the flow of data and understand what it is doing.
Contributing
Pull requests welcome!
To do
See to-do list or list of open issues.
License
This project is free software, licensed under the terms of the MIT license.