elm architecture

Modules

  • Way to organize files
  • Each file begins with module keyword
  • Name has to contain full path from root, separated with a dot
  • 
    							module Main exposing (..)
    						
    
    							module DataModels.Color exposing (..)
    						
You can expose everything or only selected functions and types

						-- exposing everything
						exposing (..)
					

						-- exposing certain functions
						exposing (init, encode, decoder)
					

						-- exposing union type
						exposing (Color)

						-- exposing union type AND type values
						exposing (Color(..))
					

Importing


						import DataModels.Color
						
						> DataModels.Color.Blue
					

						import DataModels.Color exposing (..)
						
						> Blue
					

						import DataModels.Color exposing (init, toString)

						> Blue -- not found
					

						import DataModels.Color as Color

						> Color.Blue
					
Common pattern

						import DataModels.Color as Color exposing (Color, colorDecoder)

						> giveMeRed : Color
						  giveMeRed =
						  	Color.Red

						> Color.init

						> Color.toString

						> colorDecoder
					

Commands

and

Subscriptions

Communication with the outside world

Side effects? 👎
Managed effects. 👍

Cmd and Sub modules have functions that work nicely with them

Main

There are several ways to initialize your elm app Module Browser exposes the following functions:
  • sandbox - no effects, only for learning
  • element - effects, your app is a node in DOM
  • document - also manages page title
  • application - also manages URL changes
The whole program is described with a Program type, parameterized with types of your choosing

						Program flags model msg
					
  • flags - type of input parameters, there is a way to ignore them
  • model - type of your main model, usually a record
  • msg - type of the events that can flow through the program, usually a union type

Example with an element


						{ init 			: flags -> ( model, Cmd msg )
						, view 			: model -> Html msg
						, update        : msg 	-> model -> ( model, Cmd msg )
						, subscriptions : model -> Sub msg
						}
					

						{ init = init
						, view = view
						, update = update
						, subscriptions = subscriptions
						}
					

init


					init : flags -> ( model, Cmd msg )
					

						type alias InputParams = { currentTime : String }
	
						type alias Model = 
							{ timeOfInitialization : String
							, messages : List String
							, typedValue : String
							}
	
						init : InputParams -> ( Model, Cmd msg )
						init inputParams =
							( { timeOfInitialization = inputParams.currentTime
							  , messages = []
							  , typedValue = ""
							  }
							, Cmd.none
							)
					

Initializing program without flags?

Unit type.

						Program () Model Msg
						...
						init : () -> ( Model, Cmd msg )
						init _ =
							...
					

view


						view : model -> Html msg
					

					import Html exposing (Html)
					...
					view : model -> Html msg
				

Elm manages its own Virtual DOM

Html and Html.Attributes modules have a function for every HTML element / attribute


						

This is some serious stuff


						import Html exposing (Html)
						import Html.Attributes as Attr
						...
						view : Model -> Html msg
						view model =
							Html.div [ Attr.id "main", Attr.class "blue" ]
							[ 
								Html.p [ Attr.class "red" ]
								[ 
								  Html.text "This is some"
								, Html.span [ Attr.class "purple-text" ]
									[ Html.text "serious" ]
								, Html.text "stuff"
								]
							]
					

						

						import Html.Events exposing (onInput, onClick)
						...
						type Msg = Input String | Click
						...
						view : Model -> Html Msg
						view model =
							div []
								[ input [ type_ "text", onInput Input ] []
								, button [ onClick Click ] []
								]
					

						onInput : (String -> msg) -> Attribute msg
					

						> Input : (String -> Msg)
					

update


						update : msg -> model -> ( model, Cmd msg )
					

						update : Msg -> Model -> ( Model, Cmd Msg )
						update msg model =
							case msg of
								Input str ->
									( { model | typedValue = str }
									, Cmd.none
									)

								Click ->
									( model, WebSocket.sendMessage model.typedValue )
					

						-- For multiple commands
						Cmd.batch [ cmd1, cmd2 ]
					
State management

					view model =
						...
						input 
							[ type_ "text"
							, onInput Input
							, value model.typedValue
							]
							[]
					

					update msg model =
						...
						Click ->
							( { model | typedValue = "" }
							, WebSocket.sendMessage model.typedValue
							)
					

subscriptions


						subscriptions : model -> Sub msg
					

						type Msg =
							...
							| MessageReceived String

						subscriptions : Model -> Sub Msg
						subscriptions model =
							Sub.batch
								[ WebSocket.onMessage MessageReceived
								]
					

						onMessage : (String -> msg) -> Sub msg
					

						-- Error: 
						-- This `case` does not have branches for all possibilities
						--
						-- Missing possibilities include:
						-- `MessageReceived`
					

						update msg model =
							...
							MessageReceived message ->
								( { model | messages = message :: model.messages
								  }
								, Cmd.none
								)
					

						(::) -- "cons" operator
						-- adds to the beginning of the list
						-- can be used for pattern matching
					

						sum : List number -> number
						sum list =
							case list of
								[] ->
									0
								head :: rest ->
									head + sum rest
					

						length : List a -> a
						length list =
							case list of
								[] ->
									0
								_ :: rest ->
									1 + length rest
					

						view model =
							let
								viewMessage message =
									li [] [ text message ]
							in
							div []
								[ input
									[ type_ "text"
									, onInput Input
									, value model.typedValue
									]
									[]
								, button [ onClick Click ] [ text "Send" ]
								, ul [] <| List.map viewMessage model.messages
								]
					

Compiling

Generate JavaScript file

						$ elm make src/elm/Main.elm --output=build/elm.js
					
Create a custom HTML file

						<body>
							
</body> <script src="build/elm.js"></script>
Embed your elm program

						<script>
							Elm.Main.init({
								node: document.querySelector('#elm'),
								flags: {
									currentTime: new Date().toLocaleTimeString()
								}
							});
						</script>
					
Add additional HTML elements, JS/CSS code as you wish...

Ports

Ports allow communication between Elm and JavaScript

						-- Module has to be declared as port
						port module WebSocket exposing (..)
					
They are functions that have no definition

						-- elm -> js
						port sendMessage : String -> Cmd msg

						-- js -> elm
						port onMessage : (String -> msg) -> Sub msg
					
To handle them in JS

						const app = Elm.Main.init();

						// elm -> js
						app.ports.sendMessage.subscribe((message) => {
							ws.send(message);
						});

						// js -> elm
						app.ports.onMessage.send(message);
					

Tasks

They describe asynchronous operations that may fail, like HTTP requests or DOM manipulations
Example with fetching time

						import Time  -- elm install elm/time
						import Task

						type Msg
							= Click
							| NewTime Time.Posix

						getNewTime : Cmd Msg
						getNewTime =
							Task.perform NewTime Time.now
					
Example with scroll

						import Browser.Dom as Dom
						import Task

						type Msg
							= NoOp

						jumpToBottom : String -> Cmd Msg
						jumpToBottom id =
							Dom.getViewportOf id
								|> Task.andThen (\info -> Dom.setViewportOf id 0 info.scene.height)
								|> Task.attempt (\_ -> NoOp)
					

Encoders

and

Decoders

Package Json.Encode helps to convert Elm values into Value which represents a JavaScript value, or into a JSON string.
Json.Encode.Value can be sent through ports to JS.

						import Json.Encode as JE

						encode : Person -> JE.Value
						encode person =
							JE.object
								[ ( "name", JE.string person.name )
								, ( "age", JE.int person.age )
								, ( "height", JE.float person.height )
								]
					
Package Json.Decode turns JSON values into Elm values.
Json.Decode.Value can be received as a flag or through ports from JS.

						module Json.Decode exposing (..)

						import Json.Encode

						type alias Value = Json.Encode.Value
					
Package Json.Decode.Pipeline makes it easier to create decoders for objects/records.

						import Json.Decode as JD
						import Json.Decode.Pipeline as JDP

						personDecoder : JD.Decoder Person
						personDecoder =
							JD.succeed Person
								|> JDP.required "name" JD.int
								|> JDP.optional "age" JD.string 20
								|> JDP.hardcoded 1.8
					
Once you create a decoder you can use functions decodeString/decodeValue.
Those functions both return a Result type.

						decodeValue : Decoder a -> Value -> Result Error a
					

						type Result error value
							= Ok value
							| Err error
					

						res =
							JD.decodeValue JD.string value

						parseString =
							case res of
								Ok value ->
									value
								
								Err error
									let
										_ = Debug.log "Error" error
									in
									""
					

Nested modules


						module Login exposing (..)

						init = ...
						view = ...
						update = ...
						subscriptions = ...
					

						module Chat exposing (..)

						init = ...
						view = ...
						update = ...
						subscriptions = ...
					

						module Main exposing (..)

						import Login
						import Chat

						type Model 
							= LoginModel Login.Model
							| ChatModel Chat.Model

						type Msg
							= LoginMsg Login.Msg
							| ChatMsg Chat.Msg

						main =
							Browser.element ...
					

						> LoginMsg : Login.Msg -> Msg
					

						init : () -> ( Model, Cmd Msg )
						init _ =
							( LoginModel Login.init
							, Cmd.none	
							)
					

					view : Model -> Html Msg
					view model =
						case model of
							LoginModel loginModel ->
								Login.view loginModel -- Error
							
							ChatModel chatModel ->
								Chat.view chatModel -- Error
					

					> Html Login.Msg -> Html Msg ???
					

					> Html.map : (a -> b) -> Html a -> Html b
					

					view : Model -> Html Msg
					view model =
						case model of
							LoginModel loginModel ->
								Login.view loginModel |> Html.map LoginMsg
							
							ChatModel chatModel ->
								Chat.view chatModel |> Html.map ChatMsg
					

					update : Msg -> Model -> ( Model, Cmd Msg )
					update msg model =
						case ( msg, model ) of
							( LoginMsg loginMsg, LoginModel loginModel ) ->
								handleLoginMsg loginMsg loginModel
							
							( ChatMsg chatMsg, ChatModel chatModel ) ->
								handleChatMsg chatMsg chatModel

							_ ->
								( model, Cmd.none )
					

						handleLoginMsg : Login.Msg -> Login.Model -> ( Model, Cmd Msg )
						handleLoginMsg loginMsg loginModel =
							let
								( updatedLoginModel, loginCmd ) =
									Login.update loginMsg loginModel
							in
							( LoginModel updatedLoginModel
							, Cmd.map LoginMsg loginCmd
							)
						

						handleChatMsg : Chat.Msg -> Chat.Model -> ( Model, Cmd Msg )
						handleChatMsg chatMsg chatModel =
							let
								( updatedChatModel, chatCmd ) =
									Chat.update chatMsg chatModel
							in
							( ChatModel updatedChatModel
							, Cmd.map ChatMsg chatCmd
							)
					

					subscriptions : Model -> Sub Msg
					subscriptions model =
						case model of
							LoginModel loginModel ->
								Login.subscriptions loginModel |> Sub.map LoginMsg
	
							ChatModel chatModel ->
								Chat.subscriptions chatModel |> Sub.map ChatMsg
					
Exercise: Implement switching between modules
(log in / log out)

Resources

Q & A