Herramientas de usuario

Herramientas del sitio


wiki2:elm-programming

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 <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

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

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

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:

  1. An Url.
  2. What to expect in a 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 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 here.

wiki2/elm-programming.txt · Última modificación: 2021/05/02 10:06 (editor externo)