====== Programming with Elm ======
===== Maybe & Nothing =====
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 ''.
You can provide a default value with ''[[https://package.elm-lang.org/packages/elm/core/latest/Maybe#withDefault|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
===== Error handling =====
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:
* map: Apply a function to a result. If the result is Ok, it will be converted. If the result is an Err, the same error value will propagate through.
* andThen: Chain together a sequence that may fail.
* withDefault
* toMaybe
* fromMaybe
===== Commands and messages =====
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.
==== To trigger several commands ====
update msg model =
case msg of
FirstMsg ->
let
cmd = Cmd.batch [
Task.perform SecondMsg (Task.succeed ()),
Task.perform ThirdMsg (Task.succeed ())
]
in
( model, cmd )
==== Trigger after one calling ====
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
==== Notes ====
* If something waits for a Command but you do not have any Command to apply you can send ''Cmd.none''.
===== Subscriptions =====
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 =====
* https://package.elm-lang.org/packages/elm/core/latest/Task
* https://package.elm-lang.org/packages/elm/core/1.0.5/Process
Tasks are asynchronous operations.
===== Debug =====
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
===== Write HTML code =====
==== Html, Html.Attributes, Html.Events ====
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"]
]
==== Create a custom tag ====
node "intl-date"
[ attribute "lang" lang
, attribute "year" (String.fromInt year)
, attribute "month" (String.fromInt month)
]
[]
==== Have into account... ====
=== To add more than one css class... ===
saveButton = H.a [A.class "button", A.class "big", A.href "#"] [H.i [A.class "fas", A.class "fa-save"] [], H.text " Save Article"]
===== Js <--> Elm =====
It is not possible to call arbitrary JavaScript functions at any time. However there are other ways that allow Js interop.
==== Flags. Js values on app initialization ====
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)
=== Decode input values ===
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//
==== Ports for events ====
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:
* Sending ''Json.Encode.Value'' through ports is recommended.
* All port declarations must appear in a ''port module''.
* Ports are available for applications, not for packages.
=== Ports: JS -> Elm ===
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
...
=== Ports: Elm -> JS ===
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');
...
==== Custom Js elements ====
* https://guide.elm-lang.org/interop/custom_elements.html
* https://github.com/elm-community/js-integration-examples/tree/master/internationalization
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)
] []
===== HTTP Requests =====
Basically an ''Http.get'', ''Http.post'', or ''Http.request''. They mainly receive:
- An ''Url''.
- What to expect in a [[https://package.elm-lang.org/packages/elm/http/latest/Http#Expect|Expect value]].
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
}
===== Encoders and Decoders =====
==== Useful ====
=== succeed ===
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 []))
=== oneOf ===
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)
==== Maps ====
Lets imagine we want to produce a ''Message'' when the Decoder has decoded the the object.
This can be chained. Lets imagine the [[https://package.elm-lang.org/packages/elm/html/latest/Html-Events#preventDefaultOn|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))] [
...
===== Useful types =====
==== Lists ====
=== join lists ===
1 :: [2,3] -- [1,2,3]
1 :: [] -- [1]
=== map ===
updatedToDo : (Int, ToDo) -> ToDo
updatedToDo (index, list) = {list | position = index}
List.map updatedToDo listsByPosition
=== reduce with foldl ===
From left with ''foldl'' and from right with ''foldr''.
foldr (+) 0 [1,2,3] -- Apply the function (+) 6
=== indexedMap ===
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
==== Dict ====
-- 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)
===== How to... =====
==== Lambdas ====
-- Lambda
> List.map (\x -> x * 2) [1, 2, 3, 4]
[2,4,6,8] : List number
==== Create a program without a view ====
Use ''Platform.worker''. Documentation [[https://package.elm-lang.org/packages/elm/core/1.0.5/Platform#worker|here]].