afformative
✅ ✍️ 👀
A standardized way to format values in your React components.
Afformative helps UI component libraries visualise arbitrary data in a reusable, plug-and-play fashion. Formatting dates and enumeration translations in your select fields and data grids has never been easier.
Installation
Use either of these commands, depending on the package manager you prefer:
yarn add afformative
npm i afformative
Quick Start
I'll try not to bore you too much, I promise.
The Holy Standard
Thou shalt not format thy values without afformative.
A formatter is an object with a .format
method. Formatters should be created solely using the makeFormatter
factory.
import { makeFormatter } from "afformative"
const upperCaseFormatter = makeFormatter(value => value.toUpperCase())
upperCaseFormatter.format("foo")
Consume formatters in your UI component library using a conventional formatter
prop.
import { makeFormatter } from "afformative"
const identityFormatter = makeFormatter(value => value)
const Select = ({ formatter = identityFormatter, items, ...otherProps }) => (
<select {...otherProps}>
{items.map(item => {
const text = formatter.format(item)
return (
<option key={item} value={item}>
{text}
</option>
)
})}
</select>
)
Usage Context
Although formatters can render icons or custom translation components, we often need to access primitive data instead of React elements.
Lexicographic sorting of items based on translations is a typical real world example, especially when you are using a custom React component for visualising the translation keys alongside the actual translations.
This is where usage suggestions comes into play. Suggestions can be used to tell formatters that a value needs to be rendered with some special care. For example, pass "primitive"
to tell a formatter that it should return a primitive value, such as a string.
import { makeFormatter } from "afformative"
const booleanFormatter = makeFormatter((value, usageSuggestions) => {
if (usageSuggestions.includes("primitive")) {
return value ? "True" : "False"
}
return <Icon type={value ? "success" : "failure"} />
})
booleanFormatter.format(true)
booleanFormatter.format(true, ["primitive"])
You can also pass arbitrary data to formatters as the third argument: data context. Let's use a dummy table component as an example.
const Table = ({ rows, formatter = identityFormatter }) => (
<table>
{rows.map(row => (
<tr>
{row.map((cell, cellIndex) => (
<td>{formatter.format(cell, [], { row, cellIndex })}</td>
))}
</tr>
))}
</table>
)
Data context allows the users of this table component to write purpose-built formatters, making it possible to take other values in the same row into account. For example, the following formatter would change the color of the cell value based on the previous value in the same row.
const rowTrendFormatter = makeFormatter((value, suggestions, { row, cellIndex }) => {
if (cellIndex === 0) {
return <span>{value}</span>
}
const previousValue = row[cellIndex - 1]
return <span style={{ color: value >= previousValue ? "green" : "red" }}>{value}</span>
})
Of course, this formatter only makes sense for our table component, nowhere else.
Because row
and cellIndex
are passed as the data context, the formatter still receives just the cell value as its first parameter! This allows us to pass other generic formatters (e.g. a currency formatter) to the table component without having to worry about the value structure.
Accessing React Context Reliably
Use hooks.
const useEnumFormatter = enumType => {
const enumTranslationKeys = useSelector(makeSelectEnumTranslationKeys(enumType))
const intl = useIntl()
return useMemo(
() =>
makeFormatter(value =>
intl.formatMessage({
defaultMessage: value,
id: enumTranslationKeys[value],
}),
),
[intl, enumTranslationKeys],
)
}
const someEnumFormatter = useEnumFormatter("someEnum")
Consuming Formatters
As mentioned earlier, formatters should be passed to components using the conventional formatter
prop. Alternatively, if you need to pass multiple formatters (e.g. in case of column definitions), pass an array of objects where each object has a formatter
property.
All formatters also expose the .wrap
method. You can use this method to alter the behaviour of the formatter for some specific values.
const useSnowflakeAwareFormatter = formatter => {
const intl = useIntl()
return useMemo(
() =>
formatter.wrap((delegate, value) => {
if (isSnowflake(value)) {
return intl.formatMessage(messages.snowflake)
}
return delegate(value)
}),
[formatter, intl],
)
}
Changelog
See the CHANGELOG.md file.
License
All packages are distributed under the MIT license. See the license here.