z5h / component-result / ComponentResult

This library helps move data between components, where

A Component is a model that can be initialized, and updated. In addition to returning a model from init or update, a component can

Importantly, a component's init/update may instead return just an error , and let the parent/caller decide how to deal with it.

This is most helpful in large apps where you have constructs like "sub-pages" and component-style modules (i.e. having model + init + update + view).

The purpose of this library is to standardize boilerplate within one's app, not necessarily to reduce it.

Definition


type ComponentResult model msg externalMsg err

A ComponentResult is an encapsulation of the typicall results of updating a component. It can represent model state, as well as dispatched Cmd msg, external messages and errors.

Creating

withModel : model -> ComponentResult model msg externalMsg err

At minimum, a non-error-state ComponentResult must always have a model. This creates a ComponentResult with a model.

Use withCmd, withExternalMsg, etc, to augment.

justError : err -> ComponentResult model msg externalMsg err

An error-state ComponentResult may be created with an error parameter. Note that withCmd, withExternalMsg etc have no effect on an error-state component result.

Augmenting

Add Cmds and external messages to a ComponentResult.

withCmd : Platform.Cmd.Cmd msg -> ComponentResult model msg externalMsg err -> ComponentResult model msg externalMsg err

Add a Cmd msg to a ComponentResult. This is a noop for error-state ComponentResult. Batches cmd with any existing ones.

withModel myModel
    |> withCmd myHttpGet
    |> withCmd myPortCmd

withCmds : List (Platform.Cmd.Cmd msg) -> ComponentResult model msg externalMsg err -> ComponentResult model msg externalMsg err

Add a list of Cmd msg to a ComponentResult. This is a noop for error-state ComponentResult. Batches cmd with any existing ones.

withModel myModel
    |> withCmds myHttpGets
    |> withCmds myPortCmds

withExternalMsg : externalMsg -> ComponentResult model msg Basics.Never err -> ComponentResult model msg externalMsg err

Add an external message (intended for the caller to interpret) to a ComponentResult which does not yet have an external message. This is a noop for error-state ComponentResult.

withModel myModel
    |> withCmd myHttpGet
    |> withExternalMsg LoadingData

Basic Mapping

Transform the model, error, or (Cmd) msg of a ComponentResult.

mapError : (err -> newErr) -> ComponentResult model msg externalMsg err -> ComponentResult model msg externalMsg newErr

Transform a ComponentResult's error value, if it is in an error state.

mapModel : (model -> newModel) -> ComponentResult model msg externalMsg err -> ComponentResult newModel msg externalMsg err

Transform a ComponentResult's model, if it exists (i.e. it is not a justError). Typical usage:

update : Msg -> Model -> ComponentResult Msg Model externMsg err
update msg model =
    case msg of
        PageMsg pageMsg ->
            Page.update pageMsg model.pageModel
                |> ComponentResult.mapModel (\newPageModel -> { model | pageModel = newPageModel })
                |> ComponentResult.mapCmd PageMsg

mapMsg : (msg -> newMsg) -> ComponentResult model msg externalMsg err -> ComponentResult model newMsg externalMsg err

Transform a ComponentResult's cmds, if it has any. Typical usage:

update : Msg -> Model -> ComponentResult Msg Model externMsg err
update msg model =
    case msg of
        PageMsg pageMsg ->
            Page.update pageMsg model.pageModel
                |> ComponentResult.mapModel (\newPageModel -> { model | pageModel = newPageModel })
                |> ComponentResult.mapCmd PageMsg

Advanced

map2Model : (model1 -> model2 -> newModel) -> ComponentResult model1 msg externalMsg err -> ComponentResult model2 msg Basics.Never err -> ComponentResult newModel msg externalMsg err

Given a function to map 2 models into a new model, and 2 ComponentResults with such models, map the ComponentResults into a new one, maintinaing error state, if any, and batching Cmd msg if any.

init : ComponentResult Model Cmd externalMsg err
init =
    map2Model (\modelA modelB -> { a = modelA, b = modelB , sort = Default, ...})
        (SubComponentA.init |> ComponentResult.mapCmd ComponentACmd)
        (SubComponentB.init |> ComponentResult.mapCmd ComponentBCmd)

applyExternalMsg : (externalMsg -> ComponentResult model msg never err -> ComponentResult model msg newExternalMessage err) -> ComponentResult model msg externalMsg err -> ComponentResult model msg newExternalMessage err

Apply the internal externalMsg (if any). The caller therefore has the opportuinity to remove the bound externalMsg type (and optionally replace it).

In general, the idea is that the caller is an update function calling into another (sub-component's) update function. The caller will get back a ComponentResult and needs to transform that into the ComponentResult it will return to it's caller.

An externalMsg is used to inform the caller that it may need to augment it's processing. e.g.

update : Msg -> Model -> ComponentResult Model Msg externalMsg err
update model msg =
    case ( msg, pageModel ) of
        ( AccountPageMsg pageMsg, AccountPageModel pageModel ) ->
            AccountPage.update pageMsg pageModel
                |> ComponentResult.mapModel (\newPageModel -> { model | pageModel = AccountPageModel newPageModel })
                |> ComponentResult.mapCmd AccountPageMsg
                |> ComponentResult.applyExternalMsg
                    (\externalMsg result ->
                        case externalMsg of
                            AccountPage.LoggedOut ->
                                result
                                    |> ComponentResult.withCmd (Ports.logout ())
                    )

discardExternalMsg : ComponentResult model msg externalMsg err -> ComponentResult model msg neverExternalMsg err

Discard the externalMsg of a ComponentResult (if one is present).

sequence : List (model -> ComponentResult model msg Basics.Never err) -> ComponentResult model msg Basics.Never err -> ComponentResult model msg neverExternalMsg err

Sequence several ComponentResult returning operations. E.g. suppose we need to initialize a model and immediately update it as well:

DataStore.init credentials
    |> sequence
        [ \model -> DataStore.update (DataStore.DeleteUserPosts user) model
        , \model -> DataStore.update (DataStore.DeleteUser user) model
        ]

NOTE: Elm docs say "there are no ordering guarantees" for batched Cmds and the same is true here. update calls are processed in sequence but the resulting batched commands are not.

Consuming

resolve : ComponentResult model msg Basics.Never Basics.Never -> ( model, Platform.Cmd.Cmd msg )

Given a non-error ComponentResult with no external message, transorfm it into the familiar ( model, Cmd msg ) type.

This is useful at the top-level update function, because the Browser package requires a return of ( model, Cmd msg ).

resolveError : (err -> ComponentResult model msg externalMsg Basics.Never) -> ComponentResult model msg externalMsg err -> ComponentResult model msg externalMsg never

Provided a function that can map an error to a non-error-state ComponentResult, we can accept any ComponentResult and guarantee a return of a non-error ComponentResult.

Other

escape : ComponentResult model msg externalMsg err -> Result err ( model, Platform.Cmd.Cmd msg, Maybe externalMsg )

"Escape" out of the ComponentResult format, and into Core Elm types. Doing this loses the benifits of the ComponentResult type and related functions.

This shouldn't typically be required in production, but might be handy for debugging/testing/prototyping.

tapModel : (model -> any) -> ComponentResult model msg externalMsg err -> ComponentResult model msg externalMsg err

"Taps" into a ComponentResult when a model is available. The only (useful) thing one can do with the model is use Debug to log info.

ComponentResult.withModel { myModel | user = newUser }
    |> ComponentResult.tapModel (\model -> Debug.log "user is " model.user)
    |> ComponentResult.withExternalMsg SomeMsg

tapError : (err -> any) -> ComponentResult model msg externalMsg err -> ComponentResult model msg externalMsg err

"Taps" into a ComponentResult when an error is available. The only (useful) thing one can do with the error is use Debug to log info. The ComponentResult supplied will be returned as is

someComponentResult
    |> ComponentResult.tapError (\err -> Debug.log "error occurred: " err)
    |> ComponentResult.withExternalMsg SomeMsg