react-elmish-utils
TypeScript icon, indicating that this package has built-in type declarations

10.0.0 • Public • Published

react-elmish-utils

Build npm

Utility functions and types for react-elmish.

[[TOC]]

Installation

npm install react-elmish-utils

Usage

Dependency Injection

You can use the function initWithDependencies to get a wrapper around the useElmish hook.

const dependencies = {
    // Contains dependencies
}

const { useElmish } = initWithDependencies({ /* Elmish options */ }, dependencies);

Instead of using the useElmish hook from the react-elmish package directly, you can use the returned hook in your components. With that you can use a function around the init, the update, and the subscription functions:

// This function can also have a return type of ElmishStateFunction if you are using an update function instead of a map.
function createState(dependencies): ElmishStateMap<Props, State, Message> {
    // Here you can access the dependencies

    function init(props: Props): InitResult<State, Message> {
        return [{
            // init state here
        }];
    }
    const update: UpdateMap<Props, State, Message> = {
        // update functions
    };

    return {
        init,
        update,
        // optional subscription
    };
}

You need to pass the createState function to the useElmish hook.

Testing

To test your init, update, and subscriptions functions you can use the getElmishState or getElmishStateFactory functions from react-elmish-utils/dist/Testing:

function initProps(): Props {
    return {
        // initial props
    };
}

const { createUpdateArgs, createModelAndProps, init, update, updateAndExecCmd, subscription } = getElmishState(createState, initProps, dependencies);
// or
const createStateWithDependencies = getElmishStateFactory(createState, initProps);
const { createUpdateArgs, createModelAndProps, init, update, updateAndExecCmd, subscription } = createStateWithDependencies(dependencies);

To test React components with dependencies you can use the renderWithDependencies function from react-elmish-utils/dist/Testing:

renderWithDependencies(() => <Component />, {
    dependencies: { /* optional mocked dependencies */ },
    model: { /* mocked model */ }
    dispatch: jest.fn(), // optional
});

This renders the component using the dependencies, model, and dispatch function you pass in. The dependencies are used until you manually reset them using the resetDependencies function.

Form

This module handles common tasks of a form.

If you want to use a function component and use an UpdateMap, you can use the FormMap.

FormMap Example

import { createFormMap, FormMapMessage, FormModel } from "react-elmish-utils";
import { cmd, InitResult, UpdateReturnType } from "react-elmish";

// The fields of the form
interface FormData {
    userName: string,
    password: string,
}

// We need the Form messages only
export type Message =
    | FormMapMessage<FormData>;

// Add the Form model to our model
export interface Model extends FormModel<FormData> {}

export interface Props {
    initialUserName: string,
}

// Create the form object with options
const form = createFormMap({
    initValues (props: Props): FormData {
        // Here we set the initial form values
        return {
            userName: props.initialUserName,
            password: "",
        }
    }
    // You can provide a validate function which gets called by the form component, see [Validation](#validation) for further information.
});

const Msg = {
    ...form.Msg,
};

function init (props: Props): InitResult<Model, Message> {
    return [
        {
            // Initialize the Form model
            ...form.init(props),
        },
    ];
}

// Add the Form update map to our update map
const updateMap: UpdateMap<Props, Model, Message> = {
    ...form.updateMap,
};

In your UI component you can dispatch the valueChanged message to update one or more values:

function Form (props: Props): JSX.Element {
    const [{ values }, dispatch] = useElmish({ name: "Form", props, init, updateMap });
    const { userName, password } = values;

    return (
        <>
            <input value={userName} onChange={event => dispatch(Msg.valueChanged({ userName: event.target.value }))} />
            <input value={password} onChange={event => dispatch(Msg.valueChanged({ password: event.target.value }))} />
        </>
    );
}

To accept or cancel the form dispatch the acceptRequest and cancelRequest Form messages in the onClick event handlers of the buttons.

The createFormMap function takes an Options object:

Property Description
initValues Function to set the initial form values.
validate (optional) Function to validate the data when the user accepts the form. It returns an array of ValidationErrors. See Validation. The validate function is not called when validators is specified.
onValueChange (optional) This function is called if one ore more values were changed. You can add code here to modify the changed values.
onValidated (optional) This function is called after the validation.
onCancel (optional) This function is called if the form should be cancelled. You can add code here to cancel the form.
onAccept (optional) This function is called if the form should be accepted. You can add code here to accept the form.

Hook into or overwrite messages

To make the form work, you need to overwrite at least the accept and the cancel messages, or provide onAccept and onCancel to the form options.

By default cancelRequest only calls cancel. If you want to override this behavior, i.e. to show some confirmation to the user, also overwrite this message.

const updateMap: UpdateMap<Props, Model, Message> = {
    ...form.updateMap,
    cancelRequest: () => [{}, cmd.ofPromise.perform(showConfirmation, Msg.cancel)],
};

Form Example

For Class Components or with usage of an update function, you need to use the classic way of composition:

import { createForm, FormModel, FormMessage } from "react-elmish-utils";
import { cmd, InitResult, UpdateReturnType } from "react-elmish";

// The fields of the form
interface FormData {
    userName: string,
    password: string,
}

// Add Form model to our model
interface Model extends FormModel<FormData> {}

interface Props {
    initialUserName: string,
}

// Create the form object with options
const form = createForm({
    initValues (props: Props): FormData {
        // Here we set the initial form values
        return {
            userName: props.initialUserName,
            password: "",
        }
    }
    // You can provide a validate function which gets called by the form component, see [Validation](#validation) for further information.
});

// We only need the Form messages
type Message =
    | FormMessage<FormData>;

const Msg = {
    ...form.Msg,
};

function init (props: Props): InitResult<Model, Message> {
    return [
        {
            // Initialize the Form model
            ...form.init(props),
        },
    ];
}

function update (model: Model, msg: Message, props: Props): UpdateReturnType<Model, Message> {
    // Distinguish between our messages and Form messages (here we only have form messages)
    switch (msg.source) {
        case "Form":
            // We need to overwrite some form messages here
            switch (msg.name) {
                case "Accept":
                    // Add code here to accept the form
                    return [{}];

                case "Cancel":
                    // Add code here to cancel the form
                    return [{}];

                case "CancelRequest":
                    // You can add code here to handle a cancel request, i.e. to ask the user if he really wants to cancel.
                    // By default this messages simply calls the Cancel message
                    return [{}];
            }

            // Call the update function for all other Form messages
            return form.update(model, msg, props);
    }
}

In your UI component you can dispatch the valueChanged message to update one or more values:

const { values: { userName, password } } = model;

return (
    <>
        <input value={userName} onChange={event => dispatch(Msg.valueChanged({ userName: event.target.value }))} />
        <input value={password} onChange={event => dispatch(Msg.valueChanged({ password: event.target.value }))} />
    </>
);

To accept or cancel the form dispatch the acceptRequest and cancelRequest Form messages in the onClick event handlers of the buttons.

The createForm function takes an Options object:

Property Description
initValues Function to set the initial form values.
validate (optional) Function to validate the data when the user accepts the form. It returns an array of ValidationErrors. See Validation. The validate function is not called when validators is specified.
onValueChange (optional) This function is called if one ore more values were changed. You can add code here to modify the changed values.
onValidated (optional) This function is called after the validation.
onCancel (optional) This function is called if the form should be cancelled. You can add code here to cancel the form.
onAccept (optional) This function is called if the form should be accepted. You can add code here to accept the form.

For the validate and onValidated functions the model parameter is extended by a boolean reValidating value which indicates whether they are called during a re-validation:

async function validate(model: Model & { reValidating: boolean }, prop: Props): Promise<ValidationError<keyof FormData> []> {
    // Validate here
}

function onValidated(errors: ValidationError[], model: Model & { reValidating: boolean }, prop: Props): void {
    // Validate here
}

Hook into or overwrite messages

To make the form work, you need to overwrite at least the Accept and the Cancel messages, or provide onAccept and onCancel to the form options.

By default CancelRequest only calls Cancel. If you want to override this behavior, i.e. to show some confirmation to the user, also overwrite this message.

function update (model: Model, msg: Message, props: Props): UpdateReturnType<Model, Message> {
    switch (msg.source) {
        case "Form":
            switch (msg.name) {
                case "Accept":
                    // Close the form here and provide the values
                    return [{}];

                case "Cancel":
                    // Close the form here
                    return [{}];

                case "CancelRequest":
                    return [{}, cmd.ofPromise.perform(showConfirmation, Msg.cancel)];

                default:
                    return form.update(model, msg, props);
            }
        case "Local":
            return localUpdate(model, msg);
    }
}

Validation

This module contains some helper functions and types for validation.

Function/Type Description
ValidationError Represents a validation error.
Validator Tuple consisting of a string (key for an error) and a ValidatorFunc.
ValidatorFunc Executes a validation and returns an error message or null.
runValidation This function executes Validators and returns an array of ValidationErrors.
getError Extracts an error message for a specified key out of an array of ValidationErrors. Can be used in the UI to get an error message for a specific control. But it is recommended to use the getError function of the created form object.

Example: Use validation in FormMap

You can pass a validation function to the Options object when creating a form.

import { createFormMap, ValidationError, runValidation } from "react-elmish-utils";

// The fields of the form
interface FormData {
    userName: string,
    password: string,
}

// Create the Form object with the validation
const form = createFormMap({
    // ...
    validate,
});

...
// Validate the inputs of a Form
function validate ({ values }: Model, prop: Props): Promise<ValidationError<keyof FormData> []> {
    const validateUserName = (): Nullable<string> => {
        if (!values.userName) {
            return "Username is missing";
        }

        return null;
    };
    const validatePassword = (): Nullable<string> => {
        if (!values.password) {
            return "Password is missing";
        }

        return null;
    };

    // Pass one or more tuples consisting of a key and a ValidatorFunc to runValidation
    return runValidation<keyof FormData>(
        ["userName", validateUserName],
        ["password", validatePassword],
    );
}

...

In the UI you get a validation error like that:

...
// in the render function of a react component
const { errors } = this.model;

// The created form returns a `getError` function which can be used to get an error message for a specific key
<Input
    ...
    error={form.getError("userName", errors)}
/>
...

By default the validation keys are the keys of the form values. If you want to use different keys, you can pass a second type parameter to the forms model and the forms message type:

type ValidationErrorKeys = "name" | "age";

interface Model extends FormModel<FormData, ValidationErrorKeys> {}

type Message =
    | FormMessage<FormData, ValidationErrorKeys>;

type MyValidationError = ValidationError<ValidationErrorKeys>;

List screen

The list screen provides common functionalities for showing a list of items.

First you need to extend your messages and model:

import { ListScreenMessage, ListScreenModel } from "react-elmish-utils";

interface Data {}

type Message =
    | { name: "loadData" }
    | ListScreenMessage<Data>;

interface Model extends ListScreenModel<Data> {}

Then create a list:

import { createList } from "react-elmish-utils";

const list = createList();

const Msg = {
    // Spread the message factories of the list:
    ...list.Msg,
}

function init (): InitResult<Model, Message> {
    return {
        // Initialize the list model:
        ...list.init(),
    };
}

const update: UpdateMap<Props, Model, Message> = {
    loadData () {
        const data = // Load the data here ...

        // Call the dataLoaded message of the list:
        return [{}, cmd.ofMsg(Msg.dataLoaded(data))];
    },

    // Spread the update map of the list:
    ...list.updateMap,
};

In your UI you can use the items property of the model:

function List (props: Props): JSX.Element {
    const [{ items }, dispatch] = useElmish({ name: "List", props, init, update });

    return (
        <List
            data={items}
        />
    );
}

List Options

You can provide options when creating a list:

Property Description
sorter See Sorting.
onUpdateSorting This callback is called whenever the current sorting has changed.
onSorterChanged This callback is called when the used Sorter or the sort direction has changed.

Sorting

You can sort the list by providing a sort function or an array of Sorter objects.

If you provide a function, this function is used to sort the items.

createList({
    sorter: (value1, value2): number => value1.compareTo(value2),
});

If you provide one or multiple Sorter objects the first one is used by default.

createList({
    sorter: [
        {
            key: "by-date",
            name: "Sort by date",
            sorter: (value1, value2) => value1.date.compareTo(value2.date),
        },
        {
            key: "by-name",
            name: "Sort by name",
            sorter: (value1, value2) => value1.name.compareTo(value2.name),
        },
    ],
});

The current Sorter can be changed by calling the setSorter or the setSorting message with the key of the Sorter to use.

The sort direction can be changed by calling the setSortDirection or toggleSortDirection message.

Search screen

The search screen provides common functionalities of a search screen.

First you need to extend your messages and model:

import { SearchScreenMessage, SearchScreenModel } from "react-elmish-utils";

interface Data {}

type Message =
    | { name: "loadData" }
    | SearchScreenMessage<Data>;

interface Model extends SearchScreenModel<Data> {}

Then create a search screen:

import { createSearch } from "react-elmish-utils";

const search = createSearch();

const Msg = {
    // Spread the message factories of the search object:
    ...search.Msg,
}

function init (): InitResult<Model, Message> {
    return {
        // Initialize the search model:
        ...search.init(),
    };
}

const update: UpdateMap<Props, Model, Message> = {
    loadData () {
        const data = // Load the data here ...

        // Call the refreshSearch message of the search object:
        return [{}, cmd.ofMsg(Msg.refreshSearch())];
    },

    // Spread the update map of the search object:
    ...search.updateMap,
};

In your UI you can use the visibleItems property of the model:

function List (props: Props): JSX.Element {
    const [{ visibleItems }, dispatch] = useElmish({ name: "Search", props, init, update });

    return (
        <List
            data={visibleItems}
        />
    );
}

Search Options

You can provide options when creating a list:

Property Description
filterByQuery A function to filter the items by the query string.
filterGroups Optional array of FilterGroupDefinitions.
showAllItemsByDefault If true, all items are shown when the query string is empty and no filters are set. (optional, false by default)

Filtering

To update the query string dispatch the queryChanged message and pass the new query string.

To toggle the state of filter dispatch the toggleFilter message and pass the filter to toggle.

Package Sidebar

Install

npm i react-elmish-utils

Weekly Downloads

339

Version

10.0.0

License

MIT

Unpacked Size

191 kB

Total Files

39

Last publish

Collaborators

  • atheck