pete-murphy/elm-loadable/1.0.0

NOTE: the latest version is 2.1.0

pete-murphy/elm-loadable

A data type for tracking loadable (and reloadable) data, like data fetched from a backend server.

Installation

elm install pete-murphy/elm-loadable

Design Goals

This package is centered around the Loadable type, which is a minimal extension of RemoteData from krisajenkins/remotedata that adds a "reloading" state to the error and success cases. See this discussion on alternative solutions to this problem for background.

Differences from RemoteData

RemoteData e a is a sum type defined as

type RemoteData e a
    = NotAsked
    | Loading
    | Failure e
    | Success a

As the "sum type" name implies, we can think of this type as having $$2 + e + a$$ inhabitants (see The algebra (and calculus!) of algebraic data types). Under this lens, Loadable e a is a product of sums represented internally as

type Value e a
    = Empty
    | Failure e
    | Success a

with a loading : Boolean added to all cases. Expressed algebraically, Value is $$1 + e + a$$ (very similar to RemoteData) and Loadable is the product of that sum and Bool's 2 inhabitants: $$2 \times (1 + e + a)$$ or $$2 + 2e + 2a$$ (compare to RemoteData's $$2 + e + a$$).

You can losslessly convert from RemoteData to Loadable

remoteDataToLoadable : RemoteData e a -> Loadable e a
remoteDataToLoadable remoteData =
    case remoteData of
        RemoteData.NotAsked ->
            Loadable.notAsked

        RemoteData.Loading ->
            Loadable.loading

        RemoteData.Success a ->
            Loadable.succeed a

        RemoteData.Failure e ->
            Loadable.fail e

Going the other direction just means discarding the loading state in the success & error cases.

loadableToRemoteData : Loadable e a -> RemoteData e a
loadableToRemoteData loadable =
    case ( Loadable.value loadable, Loadable.isLoading loadable ) of
        ( Loadable.Empty, False ) ->
            RemoteData.NotAsked

        ( Loadable.Empty, True ) ->
            RemoteData.Loading

        ( Loadable.Success a, _ ) ->
            RemoteData.Success a

        ( Loadable.Failure e, _ ) ->
            RemoteData.Failure e

Overview

Example usage

type alias Model =
    { todos : Loadable Http.Error (List Todo) }


init : ( Model, Cmd Msg )
init =
    ({ todos = Loadable.notAsked }, fetchTodos )


type Msg
    = UserClickedFetchTodos
    | BackendRespondedWithTodos (Result Http.Error (List Todo))


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        UserClickedFetchTodos ->
            ( { model | todos = Loadable.toLoading model.todos }, fetchTodos )

        BackendRespondedWithTodos result ->
            ( { model | todos = Loadable.fromResult result }, Cmd.none )


view : Model -> Html Msg
view model =
    Html.div []
        [ Html.h1 [] [ Html.text "Todos" ]
        , Html.button [ Events.onClick UserClickedFetchTodos ] [ Html.text "Fetch Todos" ]
        , Html.div [ Attributes.classList [ ( "loading", Loadable.isLoading model.todos ) ] ]
            [ case Loadable.value model.todos of
                Loadable.Empty ->
                    Html.text "Loading..."

                Loadable.Success todos ->
                    Html.ul [] (List.map viewTodo todos)

                Loadable.Failure _ ->
                    Html.text "Something went wrong"
            ]
        ]