Skip to main content

UI 5 release

· 14 min read

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:

{% raw %}

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>
)

{% endraw %}

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>
)

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>
)

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 to onClickOutside
  • Constrictor: has been renamed to Box since it now does more than just restrict sizes