UI

This package is concerned with providing high level components and a standard interface for specifying how to render UI elements for ViewModel Field's. For example, creating a form to collect data for a ViewModel or rendering the values of a ViewModel record.

The UI components this package deals with are:

  • formComponent - The main form component that is used by form implementations (e.g. see @prestojs/final-form)
  • formItemComponent - The component to use for form 'items'. A form item is the input to collect a value for a field and any relate information (e.g. the label for the field, help text, errors etc.).
  • widget - a widget is a component used in a form to collect input from a user (e.g. a select or checkbox). This package provides a way to map a Field to the widget component that should be used to render its value.
  • formatter - a formatter is a component that takes a value and formats it for display. This package provides a way to map a Field to the formatter component that should be used to render its value.

This package provides concrete implementations for formatters (see getFormatterForField). For form components you either need to provide an implementation or use @prestojs/ui-antd and @prestojs/final-form.


Installation

yarn add @prestojs/ui

Usage

At the top level is UiProvider which allows you to define the various UI specific components to use throughout the system. For example you can define the formItemComponent which is used by @prestojs/final-form or getWidgetForField which is used by FieldWidget to render the widget for a specific field.

function RootView() {
    return (
        <UiProvider
            getFormatterForField={getFormatterForField}
            getWidgetForField={getWidgetForField}
            formItemComponent={FormItemWrapper}
        >
            <App />
        </UiProvider>
    );
}

See UiProvider for more details.

Formatters

FieldFormatter is used for rendering the value of a field. For example the ChoiceFormatter component renders the label for a selected choice field. This is generally used with a ViewModel but doesn't have to be.

class Person extends viewModelFactory(
    {
        id: new Field(),
        isActive: new BooleanField({ label: 'Active?' }),
    },
    { pkFieldName: 'id' }
) {}

function PersonDetail({ person }) {
    return (
        <dl>
            <dt>{Person.fields.isActive.label}</dt>
            <dd>
                <FieldFormatter field={person._f.isActive} />
            </dd>
        </dl>
    );
}

In the above example BooleanFormatter will be used as that's the default formatter for BooleanField. The advantage of this over hard-coding the formatter everywhere is you can change how a particular field type is formatted in a central location. If you wanted to change how BooleanField is formatted you could implement your own formatter. All a formatter is is a component that accepts, at minimum, a value prop:

function BooleanEmojiFormatter({ value }) {
    if (value == null || value == '') {
        return <>'❓'</>;
    }
    return <>{value ? '✅' : '❌'}</>;
}

To use this formatter everywhere a BooleanField appears provide you own implementation of getFormatterForField:

import { getFormatterForField as defaultGetFormatterForField } from '@prestojs/ui';

function getFormatterForField(field) {
    if (field instanceof BooleanField) {
        // You can just return the formatter component directly but it's good practice to return
        // the component & `field.getFormatterProps` which allows per field customisations. See
        // example further below where `formatterProps` are passed to a field for how this can
        // be used.
        return [BooleanEmojiFormatter, field.getFormatterProps()];
    }
    // Fall back to the default implementation
    return defaultGetFormatterForField(field);
}

// Then pass this to the `UiProvider` that wraps your app
<UiProvider getFormatterForField={getFormatterForField}>{...}</UiProvider>

As shown in the above example getFormatterForField can return a 2-element array of the form [component, defaultProps]. This allows you to inject props used on a component based on the field. We could rewrite the above to use the options provided by the existing BooleanFormatter component:

import { getFormatterForField as defaultGetFormatterForField } from '@prestojs/ui';

function getFormatterForField(field) {
    if (field instanceof BooleanField) {
        return [
            BooleanFormatter,
            {
                trueLabel: '✅',
                falseLabel: '❌',
                blankLabel: '❓',
                // Include this last so individual fields can override that above defaults
                ...field.getFormatterProps(),
            },
        ];
    }
    // Fall back to the default implementation
    return defaultGetFormatterForField(field);
}

See getFormatterForField for the default implementation.

If you just need to override formatter props for a specific field you can pass that through to the field constructor itself using the formatterProps option. This is then returned by field.getFormatterProps():

new BooleanField({
    label: 'Active?',
    formatterProps: {
        trueLabel: '✅',
        falseLabel: '❌',
        blankLabel: '❓',
    },
});

Widgets

Widgets are conceptually similar to formatters but instead of rendering a value they are used to collect a value. For example a DateField may render as a calendar input widget as implemented by DateWidget.

FieldWidget resolves the UI specific widget to use for a Field. @prestojs/ui doesn't currently provide a default implementation for these as they tend to be highly specific. @prestojs/ui-antd provides an implementation for antd or you can implement your own.

Generic components

With this structure in place you can write generic components that don't need to know the type of the field.

For example, here's a view that renders all the fields on a record without know anything about the ViewModel.

function RecordDetailView({ record }) {
    return (
        <dl>
            {record._assignedFields.map(fieldName => {
                const field = record._f[fieldName];
                if (field.writeOnly || record._model.pkFieldNames.includes(fieldName)) {
                    return null;
                }
                return (
                    <React.Fragment key={fieldName}>
                        <dt>{field.label}</dt>
                        <dd>
                            <FieldFormatter field={field} />
                        </dd>
                    </React.Fragment>
                );
            })}
        </dl>
    );
}