emilianobovetti / elm-toast / Toast

All you need to create, append and render toast stacks in the Elm architecture.

Pick one kind of toast

persistent : content -> Toast content

Create a new toast that won't be automatically removed, it will stay visible until you explicitly remove it.

Toast.persistent
    { message = "hello, world"
    , color = "#7f7"
    }

expireIn : Basics.Int -> content -> Toast content

Create a new toast with a fixed expiry. Toast's lifespan is expressed in milliseconds.

Toast.expireIn 5000 "I'll disappear in five seconds"

expireOnBlur : Basics.Int -> content -> Toast content

This kind of toast has an interaction-based expiration. It'll be removed automatically in given time if user doesn't interact with the toast, but it'll stay visible if receives focus or mouse over.

When the interaction has ended and the toast lost both focus and mouse over the given amount of milliseconds is awaited before it's removed.

Toast.expireOnBlur 5000 "I won't go away while I'm focused!"

Set an exit transition length

withExitTransition : Basics.Int -> Toast content -> Toast content

Add a delay between toast exit phase and actual removal.

Toast.persistent { message = "hello, world", color = "#7f7" }
    |> Toast.withExitTransition 1000

Start with an empty tray, add your toasts


type alias Toast content =
Private (Toast_ content)

Toast.Toast is something you'll need if you have to reference the output type of persistent, expireIn, expireOnBlur, this is one of those things you'll know when you need it, so don't worry about this.


type alias Tray content =
Private (Tray_ content)

Toast.Tray represents the stack where toasts are stored. You probably want to use this opaque type in your model:

type alias MyToast =
    { message : String
    , color : String
    }

type alias Model =
    { tray : Toast.Tray MyToast

    -- model fields...
    }

tray : Tray content

An empty tray, it's a thing you can put in an init.

init : anything -> ( Model, Cmd msg )
init _ =
    ( { tray = Toast.tray }, Cmd.none )

add : Tray content -> Toast content -> ( Tray content, Platform.Cmd.Cmd Msg )

Add a toast to a tray, produces an updated tray and a Cmd Toast.Msg.

updateTuple :
    ( Toast.Tray { message : String, color : String }
    , Cmd Toast.Msg
    )
updateTuple =
    Toast.persistent { message = "hello, world", color = "#7f7" }
        |> Toast.withExitTransition 1000
        |> Toast.add currentTray

addUnique : Tray content -> Toast content -> ( Tray content, Platform.Cmd.Cmd Msg )

Add a toast only if its content is not already in the tray.

Toast contents are compared with structural equality.

-- if currentTray already contains a toast with the same
-- message and color it won't be added again
Toast.persistent { message = "hello, world", color = "#7f7" }
    |> Toast.addUnique currentTray

addUniqueBy : (content -> a) -> Tray content -> Toast content -> ( Tray content, Platform.Cmd.Cmd Msg )

This is what you need if, for example, you want to have toast with unique content.message.

-- no two "hello, world" in the same tray
Toast.persistent { message = "hello, world", color = "#7f7" }
    |> Toast.addUniqueBy .message currentTray

addUniqueWith : (content -> content -> Basics.Bool) -> Tray content -> Toast content -> ( Tray content, Platform.Cmd.Cmd Msg )

Most powerful addUnique version: it takes a function that compares two toast contents.

type alias MyToast =
    { message : String
    , color : String
    }

sameMessageLength : MyToast -> MyToast -> Bool
sameMessageLength t1 t2 =
    String.length t1.message == String.length t2.message

-- we can't have two toast with same message length
-- for some reason...
Toast.persistent { message = "hello, world", color = "#7f7" }
    |> Toast.addUniqueWith sameMessageLength currentTray

Forward messages and update toast's tray


type Msg

Internal message, you probably want to do something like

type Msg
    = ToastMsg Toast.Msg
      -- other stuff...
    | AddToast MyToastContent

in your app Msg.

update : Msg -> Tray content -> ( Tray content, Platform.Cmd.Cmd Msg )

Nothing fancy here: given a Toast.Msg and a Toast.Tray updates tray's state and produces a Cmd.

tuple : (Msg -> msg) -> { model | tray : Tray content } -> ( Tray content, Platform.Cmd.Cmd Msg ) -> ( { model | tray : Tray content }, Platform.Cmd.Cmd msg )

Helps in conversion between ( Toast.Tray, Cmd Toast.Msg ) and ( Model, Cmd Msg ).

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        AddToast content ->
            Toast.persistent content
                |> Toast.add model.tray
                |> Toast.tuple ToastMsg model

        ToastMsg tmsg ->
            Toast.update tmsg model.tray
                |> Toast.tuple ToastMsg model

Customize & render toasts


type alias Config msg =
Private (Config_ msg)

Toast.Config is something that holds information about rendering stuff.

config : (Msg -> msg) -> Config msg

To create an empty Toast.Config you have to provide a Toast.Msg -> msg function, this probably should look like

type Msg
    = ToastMsg Toast.Msg
      -- other stuff...
    | AddToast MyToastContent

toastConfig : Toast.Config Msg
toastConfig =
    Toast.config ToastMsg

render : (List (Html.Attribute msg) -> Info content -> Html msg) -> Tray content -> Config msg -> Html msg

This function is where our money are: all our data shrunk down to a beautiful Html msg, ready to be served.

The first thing needed to make this magic is a viewToast function that I'll try to explain how it works:

Secondly you have to provide a Toast.Tray and last but not least your Toast.Config.

viewToast : List (Html.Attribute Msg) -> Toast.Info Toast -> Html Msg
viewToast attrs toast =
    Html.div
        attrs -- do not forget this little friend!
        [ Html.text toast.content.message ]

Toast.render viewToast model.tray toastConfig


type Phase
    = Enter
    | In
    | Exit

A toast go through three phases:

You can control how much time a toast is kept in Toast.Exit phase through withExitTransition.

Both Toast.Enter and Toast.Exit are considered transition phases.


type Interaction
    = Focus
    | Blur

Can be Toast.Focus or Toast.Blur, just like Toast.Phase you'll have this information while rendering a toast through Toast.Info.

viewToast :
    List (Html.Attribute Msg)
    -> Toast.Info Toast
    -> Html Msg
viewToast attributes toast =
    Html.div
        (if toast.interaction == Toast.Focus then
            class "toast-active" :: attributes

         else
            attributes
        )
        [ Html.text toast.content.message ]

view : Model -> Html Msg
view model =
    Toast.config ToastMsg
        |> Toast.render viewToast model.tray


type alias Info content =
{ id : Private Id
, phase : Phase
, interaction : Interaction
, content : content 
}

Toast.Info represent data publicly exposed about a toast.

You already know Toast.Phase and Toast.Interaction, of course you also know content since this is your own data.

Meet id, this little field contains a unique value for each toast that you need to pass to Toast.remove and Toast.exit.

withAttributes : List (Html.Attribute msg) -> Config msg -> Config msg

Add custom attributes to every toasts.

Toast.config ToastMsg
    |> Toast.withAttributes [ style "background" "#4a90e2" ]
    |> Toast.render viewToast model.tray

withFocusAttributes : List (Html.Attribute msg) -> Config msg -> Config msg

Add custom attributes to toasts when they're focused.

Toast.config ToastMsg
    |> Toast.withFocusAttributes [ style "box-shadow" "2px 3px 7px 2px #c8cdd0" ]
    |> Toast.render viewToast model.tray

withEnterAttributes : List (Html.Attribute msg) -> Config msg -> Config msg

Add custom attributes to toasts only during their Toast.Enter phase.

import Html.Attributes exposing (style)

Toast.config ToastMsg
    |> Toast.withEnterAttributes [ style "opacity" "0" ]
    |> Toast.render viewToast model.tray

withExitAttributes : List (Html.Attribute msg) -> Config msg -> Config msg

Add custom attributes to toasts only during their Toast.Exit phase.

Toast.config ToastMsg
    |> Toast.withExitAttributes [ style "transform" "translateX(20em)" ]
    |> Toast.render viewToast model.tray

withTransitionAttributes : List (Html.Attribute msg) -> Config msg -> Config msg

Shortcut for withEnterAttributes plus withExitAttributes.

Toast.config ToastMsg
    |> Toast.withEnterAttributes [ class "toast-fade-out" ]
    |> Toast.withExitAttributes [ class "toast-fade-out" ]
    |> Toast.render viewToast model.tray

Toast.config ToastMsg
    |> Toast.withTransitionAttributes [ class "toast-fade-out" ]
    |> Toast.render viewToast model.tray

withTrayAttributes : List (Html.Attribute msg) -> Config msg -> Config msg

Add custom attributes to rendered Toast.Tray.

Toast.config ToastMsg
    |> Toast.withTrayAttributes [ class "nice-tray" ]
    |> Toast.render viewToast model.tray

withTrayNode : String -> Config msg -> Config msg

Set nodeName of rendered Toast.Tray.

By default this is "div". I know, as a String, but hey that's what you get from Html.Keyed.node

Remove toasts

remove : Private Id -> Msg

Inside your viewToast you may want to remove your toast programmatically. Remove means that the toast is deleted right away. If you want to go through the exit transition use exit.

closeButton : Toast.Info Toast -> Html Msg
closeButton toast =
    Html.div
        [ onClick <| ToastMsg (Toast.exit toast.id) ]
        [ Html.text "✘" ]

viewToast : List (Html.Attribute Msg) -> Toast.Info Toast -> Html Msg
viewToast attrs toast =
    Html.div
        attrs
        [ Html.text toast.content.message
        , closeButton toast
        ]

exit : Private Id -> Msg

Same as remove, but the toast goes through its exit transition phase. If you have a fade-out animation it'll be showed.