A Maybe variable could really have a value or not. If not, then, it is Nothing.
It is easily managed with a case:
url = case Url.fromString flags.url of Just value -> value Nothing -> (Url.Url Url.Https "127.0.0.1" (Just 3000) "" (Just "") (Just ""))
The Url.fromString is a Maybe Url. If it has a value (the url parse was good), then it returns a Just value which resolves the Maybe. When is Nothing in this example a Url is returned. Maybe parameters required to construct a Url.Url are given with Just <value>.
You can provide a default value with Maybe.withDefault default:
withDefault 100 (Just 42) -- Just 42 is given, so it's resolved as 42 withDefault 100 Nothing -- Nothing is given, so it's resolved as 100
To handle errors Elm uses a return type called Result.
type Result error value = Ok value | Err error
To receive a Result force you to treat the value (Ok value) and the error (Error error).
OnSavedList result -> case result of
Ok _ -> ({ model | currentText = "" }, Srv.getLists model.baseUrl)
Err _ -> (model, Cmd.none)
You can do other things:
Messages are generated by the View in response to the user's interaction. Messages represent user requests to alter the application's state.
Commands (Cmd) are something you want the system do, when it is done it will produce a Message.
Messages and commands are not synonymous. Messages represent the communication between an end user and the application while commands represent how an Elm application communicates with other entities. A command is triggered in response to a message.
The type Message is defined my the user, sometimes called Msg. Again, it's upon the user.
update msg model =
case msg of
FirstMsg ->
let
cmd = Cmd.batch [
Task.perform SecondMsg (Task.succeed ()),
Task.perform ThirdMsg (Task.succeed ())
]
in
( model, cmd )
You can do something like this for sending message UpdateTotals with value 100:
GetAccount result -> case result of
Ok account -> ({model | account = account }, Task.succeed (UpdateTotals 100.0) |> Task.perform identity)
Err err -> log (toString err) (model, Cmd.none)
Or just define:
send : msg -> Cmd msg send msg = Task.succeed msg |> Task.perform identity
Cmd.none.It's the way to pay attention to events (callings from JS code, clock ticks…):
subscriptions : Model -> Sub Message
subscriptions _ = Sub.batch [
updateListsOrder OnUpdateListsOrder,
updateToDosOrder OnUpdateToDosOrder
]
Tasks are asynchronous operations.
To log in console use Debug.log, its signature is:
String -> a -> a
1 + log "number" 1 -- logs in console: "number: 1". Result value: 2 length (log "start" []) -- logs in console: "start: []". Result value: 0
These contains the members required to write HTML with Elm.
A common tag like Html.div or Html.span requires two lists as parameters: One with attributes and events, other with other nested tags.
Hhtml.text receives an String as parameter.
import Html as H import Html.Attributes as A import Html.Events as E H.div [] [ H.span [A.class "label"] [H.text "Summary: "], H.span [] [H.text "lalala"] ]
node "intl-date"
[ attribute "lang" lang
, attribute "year" (String.fromInt year)
, attribute "month" (String.fromInt month)
]
[]
saveButton = H.a [A.class "button", A.class "big", A.href "#"] [H.i [A.class "fas", A.class "fa-save"] [], H.text " Save Article"]
It is not possible to call arbitrary JavaScript functions at any time. However there are other ways that allow Js interop.
Flags are a way to pass values into Elm on initialization. The idea is that you use the type of those values that are passed to your script for produce the first model status.
So an initial function like this:
init : () -> (Model, Cmd Msg)
init _ =
( initialModel, Http.get{ url = "/api/bookmarks", expect = Http.expectJson BookmarksReceived resultsDecoder} )
With Flags will be similar to this:
init : FlagsType -> (Model, Cmd Msg)
In JS we can create the application with the chosen flags:
const app = Elm.Main.init({ node: wundelmlist, flags: { url: Config.restUrl, token: token, list: currentList } });
In Elm we would define the required type and use it:
type alias Flags = {
url: String,
token: String,
list: Maybe String
}
main = Browser.element { init = initVar, update = update, subscriptions=subscriptions, view = view }
initVar : Flags -> (Model, Cmd Message)
initVar flags =
let
currentListId = Maybe.withDefault "" flags.list
currentList = { emptyList | id = currentListId }
url : Url.Url
url = case Url.fromString flags.url of
Just value -> value
Nothing -> (Url.Url Url.Https "127.0.0.1" (Just 3000) "" (Just "") (Just ""))
in
(Model url "" currentList None emptyToDo Dict.empty Dict.empty, Srv.getLists url)
Instead of using a type, other people use a Json.Decode.Value because it gives them really precise control. They write a decoder to handle any weird scenarios in Elm code, recovering from unexpected data in a nice way. A “weird” value goes through the decoder, guaranteeing that you implement some sort of fallback behavior.
Do it some time
For sending “events” from JS to Elm and reversal.
For sending messages from Elm to Js you need to produce a command. On the Js side you can subscribe and unsubscribe multiple functions.
With the next code we say we are going to receive String values:
port messageReceiver : (String -> msg) -> Sub msg
Definitely do not try to make a port for every JS function you need. You may really like Elm and want to do everything in Elm no matter the cost, but ports are not designed for that. Instead, focus on questions like “who owns the state?” and use one or two ports to send messages back and forth. If you are in a complex scenario, you can even simulate Msg values by sending JS like { tag: “active-users-changed”, list: … } where you have a tag for all the variants of information you might send across.
Some Elm guidelines:
Json.Encode.Value through ports is recommended.port module.In javascript:
let ids = Array.from(el.getElementsByTagName("li")) .filter(li => li.id !== "starred") .map(li => { return li.dataset.id; } ); app.ports.updateListsOrder.send(ids);
In Elm:
port updateListsOrder : ((List String) -> msg) -> Sub msg
port ...
subscriptions _ = Sub.batch [
updateListsOrder OnUpdateListsOrder,
...
]
update : Message -> Model -> (Model, Cmd Message)
update msg model = case msg of
OnUpdateListsOrder listIds ->
let
...
In Elm:
port module Main exposing (main)
port currentListsChanged : String -> Cmd msg
update : Message -> Model -> (Model, Cmd Message)
update msg model = case msg of
GotToDos result -> case result of
Ok todos ->
let
todoTuple : ToDo -> (String, ToDo)
todoTuple todo = (todo.id, todo)
dictTodos = List.map todoTuple todos
in
({ model | todos = Dict.fromList dictTodos }, currentListsChanged model.currentList.id)
In JS:
app.ports.currentListsChanged.subscribe(function(data) { window.localStorage.setItem('current-list', data); var el = document.getElementById('lists'); ...
You can create a custom Js element for a personalized tag. Then, from Elm, you just need to create a Html.node.
customElements.define('intl-date', class extends HTMLElement { ...
In Elm:
import Html exposing (Html, node)
import Html.Attributes (attribute)
viewDate : String -> Int -> Int -> Html msg
viewDate lang year month =
node "intl-date"
[ attribute "lang" lang
, attribute "year" (String.fromInt year)
, attribute "month" (String.fromInt month)
] []
Basically an Http.get, Http.post, or Http.request. They mainly receive:
Url.Common HTTP Request:
getLists : Url.Url -> Cmd Types.Message
getLists baseUrl = Http.get {
url = buildUrl baseUrl "lists" [ UrlBuilder.string "order" "position" ],
expect = Http.expectJson Types.GotLists listsDecoder
}
addToDoList : Url.Url -> Types.ToDoList -> Cmd Types.Message
addToDoList baseUrl list = Http.post {
url = buildUrl baseUrl "lists" [],
body = Http.jsonBody (listEncoder list),
expect = Http.expectWhatever Types.OnSavedList
}
Another one a bit more complex:
updateToDoList : Url.Url -> Types.ToDoList -> Cmd Types.Message
updateToDoList baseUrl list = Http.request {
method = "PATCH",
headers = [],
url = buildUrl baseUrl "lists" [ UrlBuilder.string "id" ("eq." ++ list.id) ],
body = Http.jsonBody (listEncoder list),
expect = Http.expectWhatever Types.OnSavedList,
timeout = Maybe.Nothing,
tracker = Maybe.Nothing
}
You can set a fixed value with Decode.succeed:
decodePageGroupForm : T.PageGroup -> D.Decoder T.PageGroup
decodePageGroupForm group = D.map5 T.PageGroup
(D.succeed group.uuid)
(D.at ["target", "title", "value"] D.string)
(D.succeed group.subtitle)
(D.succeed group.order)
(D.succeed group.pages)
zineDecoder : D.Decoder T.Zine
zineDecoder = D.map4 T.Zine
(D.field "uuid" D.string)
(D.field "title" D.string)
(D.field "cover" D.string)
{- (D.maybe (D.field "groups" <| D.list groupDecoder)) -}
(D.maybe (D.succeed []))
It tries a decoder, if fails, tries the next one and so on:
groupDecoder : D.Decoder T.PageGroup
groupDecoder = D.map5 T.PageGroup
(D.field "uuid" D.string)
(D.field "title" D.string)
(D.oneOf [D.field "subtitle" D.string, D.succeed ""]) -- If subtitle is null, a different type than string... it will put ""
(D.field "order" D.int)
(D.field "pages" <| D.list pageDecoder)
Lets imagine we want to produce a Message when the Decoder has decoded the the object.
This can be chained. Lets imagine the preventDefaultOn of a form:
type Message = FormSubmission FormFields
type alias FormFields = { ... }
update : Message -> Model -> Model
update msg model = case msg of
FormSubmission _ -> { model | counter = model.counter + 1 }
decodeForm = Decode.map2 FormFields ...
view : Model -> Html.Html Message
view model =
let
alwaysPreventDefault : msg -> ( msg, Bool )
alwaysPreventDefault msg = ( msg, True )
in
Html.div [] [
Html.form [Events.preventDefaultOn "submit" (Decode.map alwaysPreventDefault (Decode.map FormSubmission decodeForm))] [
...
1 :: [2,3] -- [1,2,3] 1 :: [] -- [1]
updatedToDo : (Int, ToDo) -> ToDo
updatedToDo (index, list) = {list | position = index}
List.map updatedToDo listsByPosition
From left with foldl and from right with foldr.
foldr (+) 0 [1,2,3] -- Apply the function (+) 6
idToToDo : Int -> String -> (Int, ToDo) idToToDo idx id = (idx, Maybe.withDefault emptyToDo (Dict.get id model.todos)) listsByPosition : List (Int, ToDo) listsByPosition = List.indexedMap idToToDo listIds
-- Declare an empty dict Dict.empty -- Get a value from a dict (model.todos) using a key (id). If it is not foud take a default: Maybe.withDefault emptyToDo (Dict.get id model.todos)
-- Lambda > List.map (\x -> x * 2) [1, 2, 3, 4] [2,4,6,8] : List number
Use Platform.worker. Documentation here.