turboMaCk / glue / Glue

Composing Elm applications from smaller isolated parts (modules). You can think of this as a lightweight abstraction built around (model, Cmd msg) and (model, Sub msg) pairs, composing init, update, view and subscribe using Cmd.map, Sub.map and Html.map.

It's recommended to avoid usage of pattern with stateful modules unless there is clear benefit to choose it. In cases where one would like to use Cmd.map pattern anyway though, Glue can be used to avoid repeatable patterns for mapping the msg types and updating models.

Data type


type alias Glue model subModel msg subMsg =
Internal.Glue model subModel msg subMsg

Glue describes an interface between the parent and child module.

You can create Glue with the simple, poly or glue function constructors. Every glue layer is parametrized over:

Constructors

simple : { get : model -> subModel, set : subModel -> model -> model } -> Glue model subModel Basics.Never Basics.Never

Simple Glue constructor for modules that don't produce Cmds.

Note that with this constructor you won't be able to use some functions provided within this library.

poly : { get : model -> subModel, set : subModel -> model -> model } -> Glue model subModel msg msg

A specialized Glue constructor. Useful when the module's API has generic msg type and maps Cmds etc. internally.

This constructor will do nothing to the child msgs.

glue : { msg : subMsg -> msg, get : model -> subModel, set : subModel -> model -> model } -> Glue model subModel msg subMsg

General Glue constructor.

Init

Designed for chaining initialization of child modules from parent init function.

init : Glue model subModel msg subMsg -> ( subModel, Platform.Cmd.Cmd subMsg ) -> ( subModel -> a, Platform.Cmd.Cmd msg ) -> ( a, Platform.Cmd.Cmd msg )

Initialize child module in parent.

type alias Model =
    { message : String
    , firstCounterModel : Counter.Model
    , secondCounterModel : Counter.Model
    }

init : ( Model, Cmd msg )
init =
    ( Model "", Cmd.none )
        |> Glue.init firstCounter Counter.init
        |> Glue.init secondCounter Counter.init

Updates

update : Glue model subModel msg subMsg -> (a -> subModel -> ( subModel, Platform.Cmd.Cmd subMsg )) -> a -> ( model, Platform.Cmd.Cmd msg ) -> ( model, Platform.Cmd.Cmd msg )

Call the child module update function with a given message. Useful for nesting update calls. This function expects the child update to work with Cmds.

-- Child module
updateCounter : Counter.Msg -> Counter.Model -> ( Counter.Model, Cmd Counter.Msg )
updateCounter msg model =
    case msg of
        Increment ->
            ( model + 1, Cmd.none )

-- Parent module
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        CounterMsg counterMsg ->
            ( { model | message = "Counter has changed" }, Cmd.none )
                |> Glue.update counter updateCounter counterMsg

updateModel : Glue model subModel msg subMsg -> (a -> subModel -> subModel) -> a -> model -> model

Call the child module update function with a given message. This function expects the child update to not work with Cmds.

Note you can use different functions than the child's main update. For example the child module might have an updateForRouteChange function specialized for a specific parent module situation - you can plug it in here too!

-- Child module
updateCounter : Counter.Msg -> Counter.Model -> Counter.Model
updateCounter msg model =
    case msg of
        Increment ->
            model + 1

-- Parent module
update : Msg -> Model -> Model
update msg model =
    case msg of
        CounterMsg counterMsg ->
            Glue.updateModel counter updateCounter counterMsg model

updateWith : Glue model subModel msg subMsg -> (subModel -> ( subModel, Platform.Cmd.Cmd subMsg )) -> ( model, Platform.Cmd.Cmd msg ) -> ( model, Platform.Cmd.Cmd msg )

Updates the child module with a function other than update. This function expects the child function to work with Cmds.

increment : Counter.Model -> ( Counter.Model, Cmd Counter.Msg )
increment model =
    ( model + 1, Cmd.none )

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        IncrementCounter ->
            ( model, Cmd.none )
                |> Glue.updateWith counter increment

updateModelWith : Glue model subModel msg subMsg -> (subModel -> subModel) -> model -> model

Updates the child module with a function other than update. This function expects the child function to not work with Cmds.

increment : Counter.Model -> Counter.Model
increment model =
    model + 1

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        IncrementCounter ->
            ( model
                |> Glue.updateModelWith counter increment
            , Cmd.none
            )

trigger : Glue model subModel msg subMsg -> (subModel -> Platform.Cmd.Cmd subMsg) -> ( model, Platform.Cmd.Cmd msg ) -> ( model, Platform.Cmd.Cmd msg )

Trigger Cmd in child's function Commands are async. Therefore trigger doesn't make any update directly. Use updateModel over trigger when you can.

-- Child module
triggerEmit : Counter.Model -> Cmd Counter.Msg
triggerEmit model ->
    Task.perform identity <| Task.succeed <| Counter.Emit model

-- Parent module
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        IncrementCounter ->
            ( model, Cmd.none )
                |> Glue.trigger counter triggerIncrement

Subscriptions

subscriptions : Glue model subModel msg subMsg -> (subModel -> Platform.Sub.Sub subMsg) -> (model -> Platform.Sub.Sub msg) -> model -> Platform.Sub.Sub msg

Subscribe to the subscriptions defined in the child module.

subscriptions : Model -> Sub Msg
subscriptions =
    (\model -> Mouse.clicks Clicked)
        |> Glue.subscriptions foo Foo.subscriptions
        |> Glue.subscriptions bar Bar.subscriptions

subscriptionsWhen : (model -> Basics.Bool) -> Glue model subModel msg subMsg -> (subModel -> Platform.Sub.Sub subMsg) -> (model -> Platform.Sub.Sub msg) -> model -> Platform.Sub.Sub msg

Subscribe to child's subscriptions based on some condition in the parent module.

type alias Model =
    { useCounter : Bool
    , counterModel : Counter.Model
    }

subscriptions : Model -> Sub Msg
subscriptions =
    (\_ -> Mouse.clicks Clicked)
        |> Glue.subscriptionsWhen .useCounter counter Counter.subscriptions

View

view : Glue model subModel msg subMsg -> (subModel -> Html subMsg) -> model -> Html msg

Render child module's view.

view : Model -> Html msg
view model =
    Html.div []
        [ Html.text model.message
        , Glue.view counter Counter.view model
        ]

viewSimple : Glue model subModel Basics.Never Basics.Never -> (subModel -> Html subMsg) -> (subMsg -> msg) -> model -> Html msg

View the Glue constructed with the simple constructor.

Because the Msg is not part of the Glue definition (Never type) it needs to be passed in as a argument.

Other

id : Glue model model msg msg

Identity Glue.

This can be for instance used for chaining updates of model.

chainUpdates model =
    ( model, Cmd.none )
        |> Glue.updateWith Glue.id f
        |> Glue.updateWith Glue.id g

combine : Glue model subModel1 msg subMsg1 -> Glue subModel1 subModel2 subMsg1 subMsg2 -> Glue model subModel2 msg subMsg2

Combine two glues into a single glue.

map : (subMsg -> msg) -> ( subModel, Platform.Cmd.Cmd subMsg ) -> ( subModel, Platform.Cmd.Cmd msg )

A tiny abstraction over Cmd.map packed in (model, Cmd msg).