This is a beginner tutorial to build animated snackbars. It should be understandable also if you never coded in Elm before. If you are already familiar with Elm you can skip the first section Short Elm overview.
Short Elm overview
Find here some random concept about Elm specifically related to this tutorial for a more serious or comprehensive introduction to Elm you should refer to the official guide.
In Elm everything is a function and functions are NOT written like
var sum = function(x, y) { return x + y; };
or
const sum = (x, y) => x + y;
but like
sum x y = x + y
it is basically the same as your favorite language but without all the interstitial decorations.
Sometime above the function there is the "type signature". It describes the type of arguments that go in and out. It is optional but I usually write them everywhere. Like in
sum: Int -> Int -> Int
sum x y = x + y
The last thing in the type signature, after the last arrow, is always the type of the returned value as functions can only return one value.
Some other time we will find triangles (▷ ◁) in the code . These are "pipe operators" and will display as triangles only if we use a font with ligatures, otherwise they will just render as |> <|.
The pipe operators reduce the needs of parentheses and in Elm we like uncluttered code. In practices they pass stuff from one side to the other.
So for example, let's suppose we have 4 functions calling each other:
f ( g ( h ( i "Ciao" ) ) )
the same thing can be written as
f <| g <| h <| i "Ciao"
or
"Ciao" |> i |> h |> g |> f
that is usually formatted as
"Ciao"
|> i
|> h
|> g
|> f
No need to count parenthesis anymore 🎉
For more info about syntax, refer to the official syntax documentation
In Elm we can define our own types, that is pretty cool 😎
For example:
type Fruit = Apple | Pear | Banana
is a type that can be either Apple, Pear or Banana.
When instead we see the word "alias" after "type", that is not a type definition but we are just giving a different name to the existing type. For example
type alias HealthyFood = Fruit
Elm applications are usually written following what we call TEA that stand for The Elm Architecture. It is just a loop that waits for events (clicks, etc.) and when they happen, it sends them to us so that we can react and change the interface accordingly.
This animation explains The Elm Architecture cycle:
All that we need to do is to write the 2 functions on the right of the diagram:
- The
update
that take the event (we call "message" in Elm) together with the model that is a data structure containing the entire state of the application and return a new model (and possibly other stuff) - The
view
that simply convert the model into HTML
Moreover:
- All data is immutable
- Functions must be pure (i.e. "No side effects" and "Same input == Same output")
- Function start with a small case letter (e.g.
sum
), types or type aliases with capital case letter (e.g.Fruit
) - Instead of objects Elm has records. They resemble Javascript objects but instead of
:
they need=
as in{ x = 3, y = 4 }
. Also being immutable we don't dopoint.x = 42
but we can do{ point | x = 42 }
that is like saying "Please make a copy ofpoint
and change only thex
value". If you are coming from Javascript, you can find this and other useful examples in https://elm-lang.org/docs/from-javascript - We will use
mdgriffith/elm-ui
, the library that will take care of smartly generate HTML and CSS for us, so we don't need to write them. elm-ui has some resemblance to CSS, for example the concept ofpadding
. But it is quite different from CSS, for example it doesn't havemargin
but ratherspacing
that is simply the distance between children. Other two main concepts arecolumn
androw
that are not existing in HTML/CSS.
I think this is all that we need to know about Elm. Elm is a pretty simple language as it has a small but expressive set of language constructs. This doesn't mean that we cannot write complex applications with it.
Some of the Javascript concepts that are not present (or not needed) in Elm are:
- null
- undefined
- this
- closures
- callbacks
- promise
- async/await
- hoisting
- prototypes
- class
- apply/call/bind
- truthiness
- try...catch
- type coercion
- throw
- var/let/const
- return
- for/do...while
- spread syntax
- rest syntax
- new
- super
- ++
- +=
- == vs. ===
- IIFE
- value vs. reference
- yield/generators
- runtime exceptions
- order of execution
- strange results are better than errors
Some of the concepts that are in Elm but not in Javascript are:
- immutability
- purity
- custom types
- pattern matching
- function composition
- static types
- type inference
- a compiler
- errors at compile time are better than strange results
In Elm we don't write a list of operations to be performed in order but rather we define a list of pure functions that call each other.
In the case of a "list of operations" we need to keep track of everything computed to that point, that is the "state". Changing the state is the "side effect" of the operations.
In the case of "list of pure functions", there is no need to keep track of anything (i.e. stateless) because the output of pure functions depends completely on the input. The code can be reasoned just by looking at a function’s input and output.
An as John Carmack once said "A large fraction of the flaws in software development are due to programmers not fully understanding all the possible states their code may execute in”
To learn more:
https://guide.elm-lang.org/
https://package.elm-lang.org/packages/elm/core/latest/
Tutorial
- The Model data structure
- The Messages
- The update function
- The view function
- Wire all together
This is what we will get at the end
The Model data structure
Let's decide the structure of the model that will contain the entire state of the application.
But wait, why are we talking about the state of the application while we are also saying that functional programming is stateless?
How can we change such a state while remaining pure?
The trick is simple. We pass the-entire-state-of-the-applications to functions that need to modify it and they will return an updated version of the the-entire-state-of-the-applications.
This is why the update function has this type signature
update : Msg -> Model -> Model
It is saying "I want to modify the-entire-state-of-the-applications (model) but I also want to remain pure so please pass me the model, I will make a copy of it and I will return to you the new version without changing the old version".
The Elm Runtime will then take this new Model and, with the help of the view function, will change the state of the browser. So eventually is only the Elm Runtime that can have side effects. 100% of the code that we write is pure, without side effects.
But let's move back to our tutorial.
A description of a Snackbar can be
- Some content, for example text, images, etc.
- A style, for example a background color
- A state that describe if a snackbar is Entering, is Active or is Leaving the view
- The lifeSpan, that is the duration in milliseconds of the snackbar, 0 meaning forever active
In code:
type alias Snackbar =
{ content : Element.Element Msg
, style : Style
, state : State
, lifeSpan : Float
}
Let me explain a bit more why the type of content
is Element.Element Msg
.
The prefix Element.
in the reference to the external package elm-ui
. The second Element
is actually a type (capitalized first letter) defined inside the library.
In the following example snippets I will omit the prefix Element.
because I usually import this library with import Element exposing (..)
that will automatically expose all of the functions/types of the module so that Element.Element
can be written as Element
, Element.columns
as column
, etc.
All other functions also live in modules but they don't need any import or prefix because they are already imported and exposed by default
Why is elm-ui
not imported as import Ui
? This is because this is not an enforced rule. I opened an issue to make this more explicit in the documentation.
Back to the tutorial.
For the state that describe if a snackbar is Entering, is Active or is Leaving the view we use a custom type:
type State
= Entering Float
| Active Float
| Leaving Float
The number attached to each state will tell us how "old" is the snackbar in each state. It is the number of milliseconds since the snackbar entered that state.
This is an example of snackbar life time:
added to the Model => Entering 0.000
Entering 0.033
Entering 0.066
...
Entering 0.966
Entering 1.000 => change state to Active 0.000
Active 0.001
Active 0.002
...
Active 0.999
Active 1.000 => change state to Leaving 0.000
Leaving 0.033
Leaving 0.066
...
Leaving 0.966
Leaving 1.000 => removed from the Model
Numbers increase 60 times per seconds and the quantity they increase depends on the speed of each transition.
They are always in the range 0 ~ 1. 0 is the beginning of the transition, 1 is the end.
For example if we want the Entering transition to last 0.5 seconds (30 increases), each time we should increase by 1/(60 * 0.5) = 1/30 = 0.03333 so that after 30 increases, it reaches 1.
Don't worry if this is confusing. Just remember that the number always moves gradually from 0 to 1.
At any point in time there can be 0 or more snackbar on the page so the entire state of the application (the Model
) will be
type alias Model = List Snackbar
When the application load the first time there will be no snackbars so we can initialize the Model with an empty list:
initModel : Model
initModel = []
Note that in the demo we actually pre-populate the model with some snackbar examples.
The Messages
Let's make a list of action that we want to perform
- Add a snackbar
- Remove a snackbar
- Remove all snackbars
Now we can translate this into a list of Messages that will tell to our update
function what to do
type Msg
= Add Snackbar
| Close Int
| CloseAll
| OnAnimationFrame Time.Posix
We also added OnAnimationFrame Time.Posix
that is a special message that fires approximately 60 times per second and is synced with the browser's repainting. This is the same of Javascript requestAnimationFrame and is the beast approach to build smooth animations.
Also note that Add
requires a Snackbar
while Close
requires an Int
that is the position in the list of the snackbar that we want to remove.
The update function
The update function, in its simplest form, has this type signature
update : Msg -> Model -> Model
A more advanced form is
update : Msg -> Model -> (Model, Cmd.msg)
This version can handle Commands but we don't need them for this tutorial.
Commands are needed when we need the runtime to do some side effects but if the only side effects that we need are related to changing the page, these are already taken care of by the view
function.
An example of Command is a request to send an HTTP request.
Going back to the simplest form, this is the skeleton:
update : Msg -> Model -> Model
update msg model =
case msg of
Add snackbar ->
...
Close index ->
...
CloseAll ->
...
OnAnimationFrame _ ->
...
Let's start filling the [...] in order of simplicity
Add
Adding s snackbar is trivial:
snackbar :: model
::
is the operator that appends an item at the beginning of the list. The new snackbar should be in the state Entering 0
but we don't enforce it here.
This is too simple so let's rewrite in a more complicated way:
model
|> (::) snackbar
The reason why we use the form above is for readability. 🤔
It will be clearer later once the update function is completed.
Note that (::)
is the same as ::
but it accepts the argument as a regular function, all of them on the right side. It is called infix notation and yes, the ::
operator, like all other operators, is actually a function.
CloseAll
This is also pretty simple. We want to change the state of all snackbar to Leaving 0
meaning "The beginning of the Leaving transition".
Let's make a mini-helper as it can be used in multiple places:
close : Snackbar -> Snackbar
close snackbar =
{ snackbar | state = Leaving 0 }
then we close all the snackbars
model
|> List.map close
Close
This is also pretty simple as it resembles CloseAll
but should be applied to only one snackbar. Considering index
as the position of the snackbar in the list of snackbars we can use List.indexedMap:
model
|> List.indexedMap (\index_ snackbar -> closeIf (index_ == index) snackbar)
Few notes here
-
List.indexedMap
is like an usual map function but also gives the index of each item used to detect the snackbar that we want to close. - When things start with "\" (that looks like a "λ" if we squint, a tribute to Lambda Calculus), it means it is an anonymous function. We use this when we need a throwaway function to be used only once. So, the sum functions explained at the beginning
sum a b = a + b
can be written assum = \a b -> a + b
. -
index_ == index
is the condition that we use to decide if we close a snackbar or not. The decision is taken by this little helper.
closeIf : Bool -> Snackbar -> Snackbar
closeIf bool snackbar =
if bool then
close snackbar
else
snackbar
OnAnimationFrame
Now we need to take care of the last section of the update function: OnAnimationFrame
. This is a two step process:
- We increase the "age" associate to the state and, if the number reaches 1, we change to the next state.
- We remove all snackbars that reached the final state of
Leaving 1
model
|> List.map (\snackbar -> { snackbar | state = nextState snackbar.lifeSpan snackbar.state })
|> List.filter (\snackbar -> snackbar.state /= Leaving 1)
The first step is using couple of extra functions:
nextState : Float -> State -> State
nextState lifeSpan state =
case state of
Entering value ->
nextStateHelper (value + durationEntering) Entering (Active 0)
Active value ->
nextStateHelper (value + lifeSpan) Active (Leaving 0)
Leaving value ->
nextStateHelper (value + durationLeaving) Leaving (Leaving 1)
nextStateHelper : number -> (number -> a) -> a -> a
nextStateHelper newValue thisState_ nextState_ =
if newValue > 1 then
nextState_
else
thisState_ newValue
Done! This is all about the update
function that is the core of our application. This is how the final results looks like:
update : Msg -> Model -> Model
update msg model =
case msg of
Add snackbar ->
model
|> (::) snackbar
Close index ->
model
|> List.indexedMap (\index_ snackbar -> closeIf (index_ == index) snackbar)
CloseAll ->
model
|> List.map close
OnAnimationFrame _ ->
model
|> List.map (\snackbar -> { snackbar | state = nextState snackbar.lifeSpan snackbar.state })
|> List.filter (\snackbar -> snackbar.state /= Leaving 1)
See how all cases now follow a model |>
pattern?
The view function
Let's start with some helpers.
This is a function that, depending on the state, it calculate the proper sizes of the snackbar:
calculateValues : State -> { alpha : Float, height : Float, width : Float, font : Float, widthTimer : Float }
calculateValues state =
case state of
Entering value ->
{ alpha = value
, height = maxHeight * value
, width = maxWidth * value
, font = maxFont * value
, widthTimer = 0
}
Active value ->
{ alpha = 1
, height = maxHeight
, width = maxWidth
, font = maxFont
, widthTimer = maxWidth * value
}
Leaving value ->
{ alpha = 1 - value
, height = maxHeight * (1 - value)
, width = maxWidth * (1 - value)
, font = maxFont * (1 - value)
, widthTimer = maxWidth * (1 - value)
}
Thanks to the fact that value goes from 0 to 1, all these calculations are trivial. alpha
is the same as the opacity in CSS.
Now we can apply the calculated value to render the snackbar. Let's enter in the elm-ui realm
viewSnackbar : Int -> Snackbar -> Element Msg
viewSnackbar index snackbar =
let
calculated =
calculateValues snackbar.state
in
Input.button
[ centerX
, height <| px <| round calculated.height
]
{ onPress = Just <| Close index
, label =
el
[ paddingEach { top = 10, right = 0, bottom = 0, left = 0 }
, height fill
]
<|
column
([ Border.rounded 10
, clip
, width <| px <| round calculated.width
, height fill
, alpha calculated.alpha
]
++ snackbar.style
)
[ row
[ width fill
, centerY
, paddingXY 10 0
, fontSize <| calculated.font
]
[ snackbar.content
, el [ alignRight ] <| text "✕"
]
, el
[ height <| px 5
, width <| px <| round calculated.widthTimer
, Background.color <| rgba 1 1 1 0.5
, alignBottom
]
<|
none
]
}
This is all needed to render one snackbar. I believe it is quite intuitive. I can write a separate post about this if there are questions.
To render all the snackbars:
viewSnackbars : Model -> List (Element Msg)
viewSnackbars model =
List.indexedMap (\index snackbar -> viewSnackbar index snackbar) model
and to add this to a generic page:
view : Model -> Html.Html Msg
view model =
layout
[ inFront <|
column
[ alignBottom
, moveUp 20
, centerX
]
<|
List.indexedMap
(\index snackbar -> viewSnackbar index snackbar)
model
]
<|
text "Your regular page here"
Wire all together
Time to wire all together! We need to tell the Elm runtime where to find the update
and view
functions and how to initialize the Model
.
Elm has several ways to connect these things together based on the level of expertise of the developer. For this demo we could almost use the simpler, the Browser.sandbox
sandbox :
{ init : model
, view : model -> Html msg
, update : msg -> model -> model
}
-> Program () model msg
The only thing that is missing here is that we need to subscribe to the browser's onAnimationFrame and this is only allowed from the next level, the Browser.element
element :
{ init : flags -> ( model, Cmd msg )
, view : model -> Html msg
, update : msg -> model -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
}
-> Program flags model msg
This level introduces 3 new concepts, the Commands, the Flags and the Subscriptions.
We don't need either Commands nor Flags. Commands are a way to tell Elm to do some side effects, as we discussed earlier, while Flags are a system to pass values from Javascript at the start of the application.
Anyway, let's plug everything, assuming small modifications are done so that all type signature matches:
main : Program () Model Msg
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions =
\model ->
if List.isEmpty model then
Sub.none
else
Browser.Events.onAnimationFrame OnAnimationFrame
}
Couple of notes here.
- The
()
in the type signature, in the place that contain the type of Flags, means that we are no expecting any Flag.()
is the unit type that allows only one value so it cannot hold any information. - The subscription to OnAnimationFrame is done only in the case that at least one snackbar exists, otherwise is not necessary and the application will keep looping when there is no need for it.
If you are interested in animation there are several packages in the Elm repository, including elm-playground and elm-animator.
This is all. Thank you for reading!
Picture in the cover: "The interior of a snack bar in the Netherlands" by Takeaway CC BY-SA 4.0
Top comments (0)