Elm 0.18: How to route messages with Sub.map based on their content? - elm

I have dynamically created Javascript components that communicate via ports with Elm model. They send data through a port of form port sendData : ((ComponentId, String) ...
On the elm side there is a ChildComponent module that represents their models:
type alias ComponentId =
Int
port sendData : ((ComponentId, String) -> msg) -> Sub msg
type Msg
= ProcessData String
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch [
sendData ProcessData
]
In the Parent I have:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ChildMessage componentId msg -> ...
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ Sub.map (ChildMessage componentId) (ChildComponent.subscriptions model.child) ]
Obviously this results in
Cannot find variable `componentId`
310| Sub.map (ChildMessage componentId) (ChildComponent.subscriptions (getChildModel (componentId model))) ]
^^^^^^^^^^^
How do I "extract" componentId from data coming from port? Or perhaps what I am doing is completely wrong?
Update
I have isolated the problem (with a few additions suggested by #ChadGilbert) into a project https://github.com/lguminski/ElmMultipleComponentsSubscription

When using Sub.map, you need a function that accepts a child model as its parameter. You could instead define ChildMessage as
-- in Parent.elm
type Model =
...
| ChildMessage Child.Model
Your parent subscriptions function will need to be updated:
-- in Parent.elm
subscriptions model =
Sub.batch
[ Sub.map ChildMessage (ChildComponent.subscriptions model.child) ]
When it comes to handling the data from the port in the child, you will need a Msg that has as its first parameter the same type defined in your port, mainly (ComponentId, String). Consider adding something like this to your child, where the new RouteData message can then call the ProcessData update case when needed, or ignore it otherwise.
-- in Child.elm
type Msg
= ProcessData String
| RouteData (ComponentId, String)
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch [
sendData RouteData
]
update msg model =
case msg of
RouteData (componentId, val) ->
if componentId == "my-component-id" then
update (ProcessData val) model
else
model ! []
ProcessData val -> ...
Update
Based on your additional detail, I would propose that you move the port and child Msg routing out of the Child module and into the Parent. You could then add the following update cases to the Parent:
ChildMessage componentId childMessage ->
let
childUpdates =
List.map (\c -> if c.id == componentId then updateChild c else c ! []) model.children
updateChild child =
let
(updatedChildModel, cmd) =
Child.update childMessage child
in
updatedChildModel ! [ Cmd.map (ChildMessage child.id) cmd ]
in
{ model | children = List.map Tuple.first childUpdates }
! (List.map Tuple.second childUpdates)
RouteData (componentId, val) ->
update (ChildMessage componentId (Child.ProcessData val)) model
I have created a pull request for your repository here

Related

Elm browser application not displaying

I copied the HTML from here, and the Elm code from here. The only change I made to the Elm code was the addition of the first line - module Main exposing (..). My IDE was complaining. Yet when I open index.html in a browser, I get a blank screen and the title of the page is still "Main". What am I doing wrong?
Here is my project structure
new-project
elm-stuff
src
Main.elm
elm.json
index.html
main.js
Here is index.html:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Main</title>
<script src="main.js"></script>
</head>
<body>
<script>var app = Elm.Main.init();</script>
</body>
</html>
Here is Main.elm:
module Main exposing (..)
import Browser
import Browser.Navigation as Nav
import Html exposing (..)
import Html.Attributes exposing (..)
import Url
-- MAIN
main : Program () Model Msg
main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
-- MODEL
type alias Model =
{ key : Nav.Key
, url : Url.Url
}
init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
( Model key url, Cmd.none )
-- UPDATE
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LinkClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
( model, Nav.pushUrl model.key (Url.toString url) )
Browser.External href ->
( model, Nav.load href )
UrlChanged url ->
( { model | url = url }
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
-- VIEW
view : Model -> Browser.Document Msg
view model =
{ title = "URL Interceptor"
, body =
[ text "The current URL is: "
, b [] [ text (Url.toString model.url) ]
, ul []
[ viewLink "/home"
, viewLink "/profile"
, viewLink "/reviews/the-century-of-the-self"
, viewLink "/reviews/public-opinion"
, viewLink "/reviews/shah-of-shahs"
]
]
}
viewLink : String -> Html msg
viewLink path =
li [] [ a [ href path ] [ text path ] ]
EDIT per answer by #pdamoc. I am trying to use elm-live to compile and display the elm file. I am on Ubuntu 18.04.5 LTS, with npm version 6.14.9, node version v8.10.0.
I get this error using elm-live:
$ elm-live src/Main.elm --pushstate
events.js:239
throw new TypeError('"listener" argument must be a function');
^
TypeError: "listener" argument must be a function
at _addListener (events.js:239:11)
at Server.addListener (events.js:297:10)
at new Server (_http_server.js:269:10)
at Object.createServer (http.js:34:10)
at model (/usr/local/lib/node_modules/elm-live/lib/src/start.js:259:75)
at /usr/local/lib/node_modules/elm-live/node_modules/crocks/core/compose.js:8:14
at settle (/usr/local/lib/node_modules/elm-live/node_modules/crocks/Async/index.js:151:16)
at /usr/local/lib/node_modules/elm-live/node_modules/crocks/Async/index.js:27:62
at fork (/usr/local/lib/node_modules/elm-live/node_modules/crocks/Async/index.js:155:20)
at /usr/local/lib/node_modules/elm-live/node_modules/crocks/Async/index.js:224:16
You need a webserver that would serve the index.html on every path that is requested. The easiest way is to install elm-live globally and then start it like elm-live src/Main.elm --pushstate
Without serving index.html on every path (let's say you use live-server), if you navigate to an internal path and reload you will get a 404.

Sequence Http.get in Elm

Below I have a button that attempts to load remote content ...
import Post exposing (Post)
import Html exposing (..)
import Html.Events exposing (..)
import Http
import Json.Decode as Decode
type alias Model =
{ posts : List Post }
type Msg
= Search String
| PostsReceived (Result Http.Error (List Post))
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Search s ->
let
cmd =
(Decode.list Post.decode)
|> Http.get ("/posts?author=" ++ s)
|> Http.send PostsReceived
in
( model, cmd )
PostsReceived (Ok posts) ->
{ model | posts = posts }
! []
PostsReceived (Err error) ->
( model, Cmd.none )
view : Model -> Html Msg
view model =
button
[ onClick (Search "amelia") ]
[ text "Read posts by Amelia" ]
This is a valid Elm program, only there's one little problem: The API doesn't allow me to search by string. This is not allowed
/posts?author=amelia => Malformed Request Error
However, this is allowed
/posts?author=2 => [ {...}, {...}, ... ]
So I must first fetch an author to get his/her id, and then I can fetch posts using the author's id...
/author?name=amelia => { id: 2, name: "amelia", ... }
/posts?author=2
How can I sequence one request after the next? Ideally I'd like to cache the authors somewhere in the model so we're only requesting ones that we haven't seen before.
You can use Task.andThen to chain two tasks together. Assuming that the /posts response includes the author ID, you can then add that author ID into you model when you handle the response.
Search s ->
let
getAuthor =
Author.decode
|> Http.get ("/author?name=" ++ s)
|> Http.toTask
getPosts author =
(Decode.list Post.decode)
|> Http.get ("/posts?author=" ++ author.id)
|> Http.toTask
cmd =
getAuthor
|> Task.andThen getPosts
|> Task.attempt PostsReceived
in
( model, cmd )
I've got this compiling at https://ellie-app.com/DBJc6Kn3G6a1 if that helps
You can chain together tasks using Task.andThen. You'll first have to convert the web requests to tasks using Http.toTask:
postsByAuthorName : String -> Cmd Msg
postsByAuthorName name =
Http.get ("/author?name=" ++ name) (Decode.field "id" Decode.int)
|> Http.toTask
|> Task.andThen (\id ->
Http.get ("/posts?author=" ++ toString id) (Decode.list decodePost)
|> Http.toTask)
|> Task.attempt PostsReceived
A a dictionary and a couple more Msg options should do it.
You'll have to write the decoder for the Author response, but other than that this should work
type alias Model =
{ posts : List Post
, authors : Dict String Int }
type Msg
= Search String
| SearchAuthor String
| AuthorReceived (Result Http.Error Int String)
| PostsReceived (Result Http.Error (List Post))
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Search author ->
case (Dict.get author model.authors) of
Nothing ->
let
cmd =
(Decode.list Post.decode)
|> Http.get ("/author?name=" ++ author)
|> Http.send AuthorReceived
in
(model,cmd)
Just num ->
let
cmd =
(Decode.list Author.decode)
|> Http.get ("/posts?author=" ++ num)
|> Http.send PostsReceived
in
( model, cmd )
AuthorReceived (Ok number name) ->
let
updatedAuthors = Dict.inster name number model.authors
cmd =
(Decode.list Post.decode)
|> Http.get ("/posts?author=" ++ number)
|> Http.send PostsReceived
in
{model | authors = updatedAuthors } ! [cmd]
AuthorReceived (Err error) ->
(mode, Cmd.none )
PostsReceived (Ok posts) ->
{ model | posts = posts }
! []
PostsReceived (Err error) ->
( model, Cmd.none )
view : Model -> Html Msg
view model =
button
[ onClick (Search "amelia") ]
[ text "Read posts by Amelia" ]

Uncaught TypeError: Cannot read property 'week' of undefined

I'm getting an uncaught typeerror in elm and don't know why.
I'm decoding a json string from an api; the api is giving a list of rostars and each rostar has either a planningId or a flexplanningId. I'd like to map over the list and give each planning a unique id based either on the planningId or the flexplanningId, depending on which it has. Here's the code:
The record definition and the decoders:
type alias Rostar =
{ employee : Employee }
type alias Employee =
{ week : Week
, name : String
, id : Int
, contractHours : Float
}
type alias Week =
{ monday : List Planning
, tuesday : List Planning
, wednesday : List Planning
, thursday : List Planning
, friday : List Planning
, saturday : List Planning
, sunday : List Planning
}
type alias Planning =
{ time : String
, planningId : Maybe Int
, groupId : Int
, groupName : String
, flex : Bool
, employeeTimeslotId : Maybe Int
, flexplanningId : Maybe Int
, employeeId : Maybe Int
, id : Maybe Int
}
responseDecoder : Decoder (List Rostar)
responseDecoder =
list rostarDecoder
rostarDecoder : Decoder Rostar
rostarDecoder =
decode Rostar
|> required "employee" employeeDecoder
employeeDecoder : Decoder Employee
employeeDecoder =
decode Employee
|> required "rostar" weekDecoder
|> required "name" string
|> required "id" int
|> required "contract_hours" float
weekDecoder : Decoder Week
weekDecoder =
decode Week
|> required "monday" (list planningDecoder)
|> required "tuesday" (list planningDecoder)
|> required "wednesday" (list planningDecoder)
|> required "thursday" (list planningDecoder)
|> required "friday" (list planningDecoder)
|> required "saturday" (list planningDecoder)
|> required "sunday" (list planningDecoder)
planningDecoder : Decoder Planning
planningDecoder =
decode Planning
|> required "time" string
|> optional "planning_id" (nullable int) Nothing
|> required "group_id" int
|> required "group_name" string
|> required "flex" bool
|> optional "employee_timeslot_id" (nullable int) Nothing
|> optional "flexplanning_id" (nullable int) Nothing
|> required "employee_id" (nullable int)
|> hardcoded Nothing
The mapping:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
HandleFeedResponse response ->
let
assignPlanningId : Planning -> Planning
assignPlanningId planning =
case planning.planningId of
Just id ->
{ planning | id = Just (id + 10000000) }
Nothing ->
case planning.flexplanningId of
Just id ->
{ planning | id = Just (id + 90000000) }
Nothing ->
{ planning | id = Nothing }
planningWithId : List Planning -> List Planning
planningWithId day =
List.map assignPlanningId day
mapWeek : Week -> Week
mapWeek week =
{ week
| monday = planningWithId week.monday
, tuesday = planningWithId week.tuesday
, wednesday = planningWithId week.wednesday
, thursday = planningWithId week.thursday
, friday = planningWithId week.friday
, saturday = planningWithId week.saturday
, sunday = planningWithId week.sunday
}
updateResponse : List Rostar
updateResponse =
List.map
(\r ->
let
employee =
{ employee | week = mapWeek employee.week }
in
{ r | employee = employee }
)
response
check =
Debug.log "updatedResponse" updateResponse
in
{ model | rostar = updateResponse } ! []
Here's the error I'm getting:
Uncaught TypeError: Cannot read property 'week' of undefined
Blockquote
Thanks for the help!
I think your problem is caused by the let binding of employee in the updateResponse mapping function. The label employee already exists, so this line is causing a recursive definition.
let
employee =
{ employee | week = mapWeek employee.week }
In Elm 0.18, this is a compile error and gives you a detailed error message rather than leaving the possibility for a runtime error:
Detected errors in 1 module.
-- BAD RECURSION ------------------------------------------------------ Main.elm
employee is defined directly in terms of itself, causing an infinite loop.
132|> employee =
133| { employee | week = mapWeek employee.week }
Maybe you are trying to mutate a variable? Elm does not have mutation, so when I
see employee defined in terms of employee, I treat it as a recursive
definition. Try giving the new value a new name!
Maybe you DO want a recursive value? To define employee we need to know what
employee is, so let’s expand it. Wait, but now we need to know what employee
is, so let’s expand it... This will keep going infinitely!
To really learn what is going on and how to fix it, check out:
https://github.com/elm-lang/elm-compiler/blob/0.18.0/hints/bad-recursion.md
Prior to 0.18, I would see these types of odd "undefined" runtime errors when accidentally performing some kind of unintended recursion. In 0.18, they've added compiler checks for some of the most basic types of problems.
This may not the source of your error,
but your employee-decoder says it needs a rostar variable, which contains a week. Is this correct? Or should it be called week?
Here is the code snippet:
employeeDecoder : Decoder Employee
employeeDecoder =
decode Employee
-- |> required "rostar" weekDecoder -- is this correct?
|> required "week" weekDecoder -- what I would have expected
|> required "name" string
|> required "id" int
|> required "contract_hours" float

Elm 0.17: Task.perform and Maybe

I'm hoping someone could help me with Task.perform as I don't really understand how to handle a Maybe response - and the docs aren't making things clearer for me.
In my model I have results which Maybe a list of items or Nothing.
-- model
type alias Item =
{ name : String}
type alias Model =
{ results : Maybe (List Item) }
model = {
results = Nothing
}
I perform a Task and decode it like so:
-- Task
fetch : String -> Cmd Msg
fetch query =
let url =
"https://some_url" ++ query
in
Task.perform FetchFail FetchSuccess (Http.get decode url)
-- decoder
decoder: Json.Decoder (List Item)
decoder =
Json.at ["data"] (Json.list nestedListDecoder)
-- nestedListDecoder
nestedListDecoder : Json.Decoder Item
nestedListDecoder =
Json.object1 Item
("name" := Json.string)
I then handle the response in update:
-- update
type Msg
= FetchSuccess (Maybe (List Item))
| FetchFail Http.Error
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FetchSuccess results ->
case results of
Nothing ->
( { model | results = Nothing}, Cmd.none)
Just res ->
( { model | results = res }, Cmd.none)
FetchFail err ->
-- ... handle error
And cater for the Maybe in the view:
-- view
result : Item -> Html Msg
result item =
li [] [ text item.name ]
view : Model -> Html Msg
view model =
ul [ ] (List.map result (Maybe.withDefault [] model.results))
I am getting this error when dealing with Maybe of results.
198| Task.perform FetchFail FetchSuccess (Http.get repos url)
^^^^^^^^^^^^^^^^^^
Function `perform` is expecting the 3rd argument to be:
Task Http.Error (Maybe (List Repo))
But it is:
Task Http.Error (List Repo)
Can anyone advise where else I need to cater for the Maybe ?
A simple tweak to your decoder should fix it. The decoder just needs to use Json.Decode.maybe:
decoder: Json.Decoder (Maybe (List Item))
decoder =
Json.maybe <| Json.at ["data"] (Json.list nestedListDecoder)

Modeling condition, nested forms with elm-simple-forms

I have a form that starts with a select. Depending on what was selected the form then expands to a common main bit and a details section that depends on the selection.
I started modeling with a separate details section
type ProductDetails
= Book Book.Model
| Brochure Brochure.Model
| Card Card.Model
type alias Model =
{ form : Form CustomError Estimate
, details : ProductDetails -- a Form CustomerError CardModel / BookModel / ....
, msg : String
}
but this is becoming quite convoluted to handle in e.g. view.
The alternative would seem to be conditionally to add the details into the main form model - e.g.
type alias Estimate =
{ customer : String
, project : String
, product : String
, details : ProductDetails
}
Before I get started I’d welcome experience from others on what has worked well
If I understand correctly, you have separate modules for Book, Brochure and Card? I don't quite understand what is the purpose of your Model but I would structure it like this:
import Book
import Brochure
import Card
type Products
= Book
| Brochure
| Card
type Msg
= Details Products
type alias Model =
{ selectedProduct : Product
}
update : Msg -> Model -> Model
update msg model =
case msg of
Details prd ->
Model prd
view : Model -> Html
view model =
model.selectedProduct.view
So as you can see, now you define all available products and then you say that Msg can be Details, which would show details, and it's function would be to set selectedProduct value in Model to selected Product. You can implement selecting with:
button [ onClick (Details Book) ] [ text "Book" ]
for example to select a book. Later on you want to make it dynamic and the first instinct should be to be able to call the view function of selected Product.
In other case you could define view which would require some fields, which every Product's model would contain and then you could use them to write some information on site.
Please note that code above isn't meant to work, it's just to represent the idea.
I'm not familiar with elm-simple-forms, but this seems like a good representation of your form:
type ProductType
= TBook
| TBrochure
| TCard
type Product
= Book Book.Model
| Brochure Brochure.Model
| Card Card.Model
type alias Model =
{ product : Maybe Product
, customer : String
, project : String
}
type Msg
= SelectProductType ProductType
init : Model
init =
{ product = Nothing
, customer = ""
, project = ""
}
update : Msg -> Model -> Model
update msg model =
case msg of
SelectProductType product ->
{model | product =
case product of
TBook -> Book Book.init
TBrochure -> Brochure Brochure.init
TCard -> Card Card.init
}
view : Model -> Html Msg
view model =
case model.product of
Nothing ->
myProductTypeSelect
Just product ->
withCommonFormInputs
<| case product of
Book submodel -> Book.view submodel
Brochure submodel -> Brochure.view submodel
Card submodel -> Card.view submodel
The Maybe gives you a nice way to choose between the first form (just the select) and the second form (customer details + selected product type details).
The Book.view etc. give you Html which you can add to the common case:
withCommonFormInputs : Model -> Html Msg -> Html Msg
withCommonFormInputs model productInputs =
div
[]
[ input [] [] -- customer
, input [] [] -- project
, productInputs -- product (Book, Brochure, Card) subform
]
I ended up using a Dict of the various fields and changed the fields when the product changed. Trying to model each product more explicitly create more boiler plate than I needed.