Rails-like Flash Messages without Subscriptions
Simpler and it Works!
A New Approach
This is my third post in three days on this subject. While I learned a lot while pursuing the subscription strategy, in the end it didn’t work. But thanks to some great advice I got from Ian Mackenzie, I now know how to do this without using Elm subscriptions. Not only does the resulting product work, but it’s a lot simpler and is much easier to debug.
The subscription approach didn’t work because there was no obvious way to produce a subscription event that created a set delay from the time it was created. Thankfully, Ian pointed me to the Process.sleep function. This function takes in a Time and returns a task that is delayed by the supplied time.
The changes are pretty small, so we’ll walk through the highlights of the entire app.
The Model
The model doesn’t change at all from the previous implementation. We create a flash element that stores the text, color and duration and the model instantiates a list of these elements
type alias FlashElement =
{ id : Int
, text : String
, color : String
, duration : Time
}type alias Model =
{ flashElements : List FlashElement
, nextId : Int
}
Update Function
The DeleteFlashElement action also remains exactly the same. It’s just that this action will be called in a different manner.
DeleteFlashElement id time ->
let
newList =
List.filter
(\elem -> elem.id /= id)
model.flashElements
in
{ model | flashElements = newList } ! []
The big change occurs in the CreateFlashElement action. It adds a new element to the flashElements list in the model, but now also executes a command that will actually schedule the deletion of itself.
CreateFlashElement text color duration ->
let
newFlashElement =
{ id = model.nextId
, text = text
, color = color
, duration = duration
} newList =
newFlashElement :: model.flashElements
in
{ model
| flashElements = newList
, nextId = model.nextId + 1
}
! [ deleteCmd
model.nextId
newFlashElement.duration
]
You’ll note that we call a new deleteCmd function that accepts an id and duration. This function returns an Elm cmd. This function looks like this.
deleteCmd : Int -> Time -> Cmd Msg
deleteCmd id duration =
Process.sleep (duration * second)
|> Task.perform
(\_ -> DeleteFlashElement id duration)
This is where the magic occurs. The Process.sleep function is passed a timeout time. This produces a task that is executed by Task.perform. After completion, it invokes the DeleteFlashElement action. This effectively delivers the DeleteFlashElement action to the update function after the specified duration.
Subscriptions
Subscriptions are no longer needed so our subscriptions function becomes a No-op.
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
Conclusion
Moving from the attempted use of subscriptions to a more conventional use of Elm effects results in a few advantages
- The code works as expected
- The code is much more straightforward
- The code is easier to debug. You can now use the Elm debugger and see the events that cause both creation and deletion of events.
You can see a working copy of this code at this github release.