====== 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]].