UI 5 release
We've recently released @dhis2/ui
version 5. It unifies ui-core
, ui-widgets
and ui-forms
to simplify the user experience and allow for some architectural changes. In this post we'll go through the most important changes to try and help you with the upgrading process. To view a complete list of all the changes see the changelog.
For upgrading to the latest version of @dhis2/ui
we recommend to not mix the upgrade changes with other changes in your codebase. To keep the process manageable, branch off of your production branch (commonly master
) then upgrade and merge back your changes. A common best practice for all changes to a codebase of course.
If there's anything you've missed in this post, or encountered whilst upgrading, please let us know and we'll add it.
Import style
Since @dhis2/ui
now bundles all our ui libraries, you can now import everything that you imported previously from ui-core
, ui-widgets
and ui-forms
directly from @dhis2/ui
. An example:
// Before ui version 5:
import { Button } from '@dhis2/ui-core'
import { HeaderBar } from '@dhis2/ui-widgets'
import { composeValidators } from '@dhis2/ui-forms'
// With ui version 5:
import { Button, HeaderBar, composeValidators } from '@dhis2/ui'
The original libraries are still published alongside @dhis2/ui
, so if you want you can still import from the underlying libraries. We do recommend that you use @dhis2/ui
though, as that way you don't have to know which underlying library the component is exported from.
Noticebox
We've added a notice box component. A notice box highlights useful information that is directly relevant to the page the user is viewing. It is meant to be used wherever there is important, temporary information about a page or situation that the user needs to be aware of.
See the design specs for more information about the component. As an example, this is how you could use the NoticeBox to display a warning to a user:
const Warning = () => (
<Noticebox warning title="Possible duplicate of another entry">
This entry has been marked as a duplicate. This should be fixed by an
admin.
<a href="https://link.to">Further explanation</a>
</Noticebox>
)
Specifying selected options
To simplify specifying selected options with our components, we've moved to a slightly simpler API. Instead of having to pass objects, or an array of objects with value
and label
, now only a value
string or an array of value
strings is sufficient. This change affects the Transfer
, the SingleSelect
and the MultiSelect
.
Transfer
As noted above, instead of accepting an array of objects for the selected
prop, the Transfer
now expects an array of strings. The strings supplied to the selected
prop should match the value
prop of a supplied option. So as an illustration:
// Before ui version 5:
const Old = () => (
<Transfer selected={[{ label: 'label 1', value: 'value 1' }]} />
)
// With ui version 5:
const New = () => (
<Transfer
selected={[
'value 1', // This should match the value of an existing option
]}
/>
)
SingleSelect and MultiSelect
Just like the Transfer
, the SingleSelect
and MultiSelect
now also expect strings instead of objects for the selected
prop. To illustrate:
// Before ui version 5:
const OldSingle = () => (
<SingleSelect selected={{ label: 'label 1', value: 'value 1' }} />
)
const OldMulti = () => (
<MultiSelect selected={[{ label: 'label 1', value: 'value 1' }]} />
)
// With ui version 5:
const NewSingle = () => <SingleSelect selected="value 1" />
const NewMulti = () => <MultiSelect selected={['value 1']} />
Note that the SingleSelect
expects a single string, and the MultiSelect
an array of strings.
This change also means that the Selects can no longer distinguish between options with identical values, even if the labels are different, as they now only have the values to compare by. Make sure that this is not a problem in your application before using the new Selects.
Transfer options
The Transfer now expects options to be passed to an options
prop instead of as children. It is our intention to eventually move all our components to this API, as it allows for far simpler internals. An example:
// Before ui version 5:
const Old = () => (
<Transfer>
<TransferOption label="label 1" value="value 1" />
<TransferOption label="label 2" value="value 2" />
</Transfer>
)
// With ui version 5:
const New = () => (
<Transfer
options={[
{ label: 'label 1', value: 'value 1' },
{ label: 'label 2', value: 'value 2' },
]}
/>
)
If you want to use a custom option component, you can still do so. The Transfer
accepts a renderOption
prop, where you can supply a callback that returns the markup for a custom option. This pattern is called the render prop pattern, see the React docs for further information. An illustration of how this works:
const WithCustomOptions = () => (
<Transfer
options={[
{ label: 'label 1', value: 'value 1' },
{ label: 'label 2', value: 'value 2' },
]}
renderOption={({
value,
label,
onClick,
onDoubleClick,
highlighted,
}) => (
<div onClick={onClick} onDoubleClick={onDoubleClick}>
The value of this option is: {value}
The label of this option is: {label}
{highlighted && 'This option is highlighted'}
</div>
)}
/>
)
Layering components
We've changed the set of components used to produce various types of overlays. The underlying logic has been improved and we've clarified the scope of certain components that did a bit too much.
More specifically, Layer
and CenteredContent
have been introduced to replace the Backdrop
and ScreenCover
and the API for the ComponentCover
component has been aligned with the Layer
.
Layer
Layer
is an overlay component that fills the entire viewport and allows you to stack various components on top of one another. The Layer
accepts an onClick
callback, so you can catch clicks on the background of whatever you're rendering. It also has a translucent
prop, if you want the Layer
to darken the background slightly (the default is fully transparent). An example:
const LayerExample = () => (
<div>
<Layer>
<p>
This will be rendered on top of the app, regardless of where you
place it in your markup
</p>
</Layer>
<p>Text behind the layer</p>
</div>
)
The Layer
uses React context internally to control the stacking logic. This context has been exposed via the useLayerContext
hook, which can be used to append portals to the current layer-node, for advanced users.
ComponentCover
The ComponentCover
is similar to the Layer
, except ComponentCover
only fills its parent, provided that the parent is positioned (so has a relative
,absolute
, fixed
or sticky
position). The ComponentCover
also accepts an onClick
and translucent
prop, just like the Layer
. An example of the ComponentCover
:
const ComponentCoverExample = () => (
<div style={{ position: 'relative' }}>
<ComponentCover>
<p>
The ComponentCover will fill the parent div, and render this
paragraph on top of the div visually.
</p>
</ComponentCover>
</div>
)
CenteredContent
CenteredContent
is a component that centers its children. It has a position
prop which can be used to vertically align the children at the top
, middle
(default), or bottom
. It can be useful when you want to render a loading spinner on top of your app for example:
// This will render a spinner on top of your app, centered in the middle
const Loading = () => (
<Layer>
<CenteredContent>
<CircularLoader />
</CenteredContent>
</Layer>
)
Click based Menu
The Menu
has been refactored to make it easier to use. It is now click-based instead of hover-based, so sub-menus will stay open even if the user is no longer hovering over them.
Renamed components
The MenuList
has been renamed to Menu
, this component will expand to fill the full width of the parent container and is not meant to contain submenus. It is a good fit for a sidebar menu for example.
The original Menu
component has been renamed to FlyoutMenu
, which will render a menu in a Card
and allows for submenus. An example of both:
// So this will render full-width
const FullWidthMenu = () => (
<Menu>
<MenuItem label="Item 1" />
<MenuItem label="Item 2" />
</Menu>
)
// And this will render in a Card with width/height restrictions
const CardMenu = () => (
<FlyoutMenu>
<MenuItem label="Item 1" />
<MenuItem label="Item 2" />
</FlyoutMenu>
)
MenuDivider and MenuSectionHeader
We've also introduced two new components, the MenuDivider
and MenuSectionHeader
. As you would expect, the MenuDivider
renders a divider between MenuItems
. The MenuSectionHeader
can be used to render a header in between menu-items. For example:
const WithHeaderAndDivider = () => (
<Menu>
<MenuSectionHeader label="The header" />
<MenuItem label="Item 1" />
<MenuItem label="Item 2" />
<MenuDivider />
<MenuItem label="Item 3" />
<MenuItem label="Item 4" />
</Menu>
)
Submenus
Finally, to create sub-menus, you can now add MenuItems directly under a parent MenuItem, there's no need to wrap them in another component anymore:
const WithSubMenus = () => (
<FlyoutMenu>
<MenuItem label="Item 1" />
<MenuItem label="Item 2">
<MenuItem label="Item 2 a" />
<MenuItem label="Item 2 b">
<MenuItem label="Item 2 b i" />
<MenuItem label="Item 2 b ii" />
</MenuItem>
<MenuItem label="Item 2 c" />
</MenuItem>
</FlyoutMenu>
)
Form components
Final-form enabled components suffix
If you were using our final-form
enabled components from @dhis2/ui-forms
you'll probably know that they did not have a suffix. They were just exported as Checkbox
, Radio
, etc. However, those names collided with our regular form fields in @dhis2/ui
.
To remedy that, we've now suffixed all our final-form
enabled components with FieldFF
. The Field suffix indicates the relation with our regular Field components and the FF stands for final-form
. Note that this is only important for you if you were using the final-form
enabled components, our regular form components are unaffected! So, for example:
// Before ui version 5:
import { Field, Input } from '@dhis2/ui-forms'
const InputField = () => (
<Field
name="text"
label="Text"
component={Input}
helpText="Please enter text"
/>
)
// With ui version 5:
import { ReactFinalForm, InputFieldFF } from '@dhis2/ui-forms'
const { Field } = ReactFinalForm // this change is explained in the next section
const InputField = () => (
<Field
name="text"
label="Text"
component={InputFieldFF}
helpText="Please enter text"
/>
)
Scoped react-final-form and final-form exports
As you can see in the example above, the re-export style for react-final-form
and final-form
has changed. With @dhis2/ui-forms
we re-exported the react-final-form
and final-form
exports directly. We still re-export these libraries with @dhis2/ui
, but we've scoped the exports to the ReactFinalForm
and FinalForm
named exports. To clarify:
// Before ui version 5:
import { useField } from '@dhis2/ui-forms'
// With ui version 5:
import { ReactFinalForm, FinalForm } from '@dhis2/ui'
const { useField } = ReactFinalForm
const { FORM_ERROR } = FinalForm
Explicit api for final-form enabled toggle components
We've modified our toggle components (SwitchFieldFF
, CheckboxFieldFF
, and RadioFieldFF
) to use an API that aligns more closely with the react-final-form
API. This means that when using one of these toggle components, you have to provide a type
attribute, to signal the type of field to react-final-form
:
import {
ReactFinalForm,
RadioFieldFF,
CheckboxFieldFF,
SwitchFieldFF,
} from '@dhis2/ui'
const { Field } = ReactFinalForm
const Fields = () => (
<div>
<Field type="radio" component={RadioFieldFF} />
<Field type="checkbox" component={CheckboxFieldFF} />
<Field type="checkbox" component={SwitchFieldFF} />
</div>
)
Because of this change, our components will now behave like explained in the react-final-form docs (see the type
section):
If set to "checkbox" or "radio", React Final Form will know to manage your values as a checkbox or radio button respectively. Results in a checked boolean inside the input value given to your render prop.
Generic Field component
To simplify the creation of custom form fields, our Field
component has been modified. The Field
can now be used to wrap a custom form field and will allow displaying a label, helptext and validation messages in the same style as our other form components. So for example, say that you have a custom input
field, and you want to use it with our Field
:
import { Field } from '@dhis2/ui'
const CustomFormComponent = () => (
<Field
label="The label for this input"
helpText="This is a custom form field"
validationText="The input is valid"
required
valid
>
<input />
</Field>
)
Note that the Field
we're covering in this section, the one exported directly from @dhis2/ui
, is different from the Field
from react-final-form
. The latter exists for integration with final-form
, whereas the Field
exported directly from @dhis2/ui
is meant to help align styles for custom form fields with our other form components.
Removal of grouping components
We've removed the grouping components for our toggle components: RadioGroup
, RadioGroupField
, CheckboxGroup
, and CheckboxGroupField
. These components could be used to wrap our toggle components and would add the necessary props and event handlers by cloning their children. We've removed them because these components were blocking a generic Field
and because we're trying to move to more explicit patterns, to improve readability and maintainability. A simplified example:
// Before ui version 5:
import React, { useState } from 'react'
import { RadioGroup, Radio } from '@dhis2/ui-core'
const RadioButtons = () => {
const [selection, setSelection] = useState('1')
const options = [
{ value: "1", label: "one" }
{ value: "2", label: "two" }
]
return (
<RadioGroup
value={selection}
onChange={({ value }) => setSelection(value)}
>
{options.map({ value, label }) => (
<Radio
value={value}
label={label}
key={value}
/>
)}
</RadioGroup>
)
}
// With ui version 5:
import React, { useState } from 'react'
import { FieldGroup, Radio } from '@dhis2/ui'
const RadioButtons = () => {
const [selection, setSelection] = useState('1')
const options = [
{ value: "1", label: "one" }
{ value: "2", label: "two" }
]
// The FieldGroup below can be omitted if necessary, see the explanation of FieldGroup below
return (
<FieldGroup>
{options.map({ value, label }) => (
<Radio
value={value}
label={label}
key={value}
onChange={({ value: newValue }) => setSelection(newValue)}
checked={value === selection}
/>
)}
</FieldGroup>
)
}
New grouping components
Instead of the grouping components mentioned above, we've introduced simplified grouping components that don't modify their children via cloning: FieldSet
, FieldGroup
and FieldGroupFF
.
FieldSet
The FieldSet
is the most basic of the three, it can be used if you want to group several related controls as well as labels in a form (see the MDN docs on fieldset). An example:
import { FieldSet, Legend, Radio } from '@dhis2/ui'
const RadioButtons = () => (
<FieldSet>
<Legend>Choose your favourite monster</Legend>
<Radio label="Kraken" value="kraken" />
<Radio label="Sasquatch" value="sasquatch" />
</FieldSet>
)
FieldGroup
The FieldGroup
wraps your controls in a FieldSet
as well as a Field
. It can be used in the same manner as our regular Field
component, but for a group of controls. For example:
import { FieldGroup, Radio } from '@dhis2/ui'
const RadioButtons = () => (
<FieldGroup
label="The label for this group"
helpText="This is a custom form field"
validationText="The input is valid"
required
valid
>
<Legend>Choose your favourite monster</Legend>
<Radio label="Kraken" value="kraken" />
<Radio label="Sasquatch" value="sasquatch" />
</FieldGroup>
)
FieldGroupFF
The FieldGroupFF
does the same as the FieldGroup
, but also connects to final-form
, and displays error state and error messages from the underlying fields automatically. Just like our other final-form
connected components. An example:
import { FieldGroupFF, RadioFieldFF, ReactFinalForm } from '@dhis2/ui'
const { Field } = ReactFinalForm
const RadioButtons = () => (
<FieldGroupFF
name="monster"
label="The label for this group"
required
>
<Legend>Choose your favourite monster</Legend>
<Field
name="monster"
type="radio"
component={RadioFieldFF}
/>
<Field
name="monster"
type="radio"
component={RadioFieldFF}
/>
</FieldGroup>
)
Translations
We've added translations to the following widgets: FileInputField
, SingleSelectField
, MultiSelectField
. We'll be adding translations to all components in widgets, so that you're not required to add these yourself every time you use them.
Other notable changes
Popover
:onBackdropClick
has been renamed toonClickOutside
Constrictor
: has been renamed toBox
since it now does more than just restrict sizes