Sequence Http.get in Elm - 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" ]

Related

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

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

Validates two fields in Elm and Elm-Form

I'm using Elm Form https://github.com/etaque/elm-form, but I can't figure out the validations of two fields, I want to validate the password and password confirmation fields match.
This is what I have so far:
validate : Validation String RegUser
validate =
map6 RegUser
(field "email" email)
(field "password" (string |> andThen nonEmpty))
(field "passwordConfirmation" (string |> andThen nonEmpty))
(field "firstName" (string |> defaultValue ""))
(field "lastName" (string |> defaultValue ""))
(field "companyName" (string |> defaultValue ""))
The whole code: https://github.com/werner/madison-elm/blob/master/src/elm/Components/Register/Models.elm
Thanks for any help.
Any time you see packages that expose andThen, succeed, and fail functions, that's a good indication that you can "peel apart" the value to inspect and bind its value with another function. In this case, we can use andThen twice to build up a validation function that peeks inside two named fields and checks that they match:
matchingFields : String -> String -> Validation String String
matchingFields masterField confirmField =
field masterField string
|> andThen (\masterVal -> field confirmField string
|> andThen (\confirmVal ->
if masterVal == confirmVal then
succeed masterVal
else
fail (customError "Values do not match")))
You can then use it in your overall validation function like this:
validate : Validation String RegUser
validate =
map6 RegUser
(field "email" email)
(matchingFields "password" "passwordConfirmation" |> andThen nonEmpty)
(field "passwordConfirmation" (string |> andThen nonEmpty))
...
The solution was close to the one provided by Chad, based on https://github.com/etaque/elm-form/issues/75#issuecomment-269861043:
validate : Validation TranslationId RegUser
validate =
map6 RegUser
(field "email" email)
(field "password" (string |> andThen nonEmpty))
((field "password" string) |> andThen validateConfirmation)
(field "firstName" (string |> defaultValue ""))
(field "lastName" (string |> defaultValue ""))
(field "companyName" (string |> defaultValue ""))
validateConfirmation : String -> Validation TranslationId String
validateConfirmation password =
field "passwordConfirmation"
(string
|> andThen
(\confirmation ->
if password == confirmation then
succeed confirmation
else
fail (customError PasswordNotMatch)
)
)

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)

Dynamic SQL Parameters with Anorm and Scala Play Framework

Is it possible to dynamically create a list for anorm's "on" method?
I have a form with optional inputs and currently I check each Option and create a list with the defined Options and am trying to pass this through to anorm. Currently I get this compilation error
type mismatch; found : List[java.io.Serializable] required: (Any, anorm.ParameterValue[_])
I'm not sure how I would go about creating this list.
Current code :
val onList = List(
'school_id = input.school,
if(input.rooms isDefined) ('rooms -> input.rooms) else "None" ,
if(input.bathrooms isDefined) ('bathrooms -> input.bathrooms) else "None" ,
if(input.houseType isDefined) ('houseType -> input.houseType) else "None" ,
if(input.priceLow isDefined) ('priceLow -> input.priceLow) else "None" ,
if(input.priceHigh isDefined) ('priceHigh -> input.priceHigh) else "None" ,
if(input.utilities isDefined) ('utilities -> input.utilities) else "None"
).filter(_!="None")
SQL("SELECT * FROM Houses WHERE " + whereString).on(onList).as(sqlToHouse *)
I've tried doing this because initially I thought it would be the same as
.on('rooms -> input.rooms, 'bathroom -> input.bathrooms... etc)
EDIT:
Code is now:
val onList = Seq(
('school_id -> input.school),
if(input.rooms isDefined) ('rooms -> input.rooms.get) else None ,
if(input.bathrooms isDefined) ('bathrooms -> input.bathrooms.get) else None ,
if(input.houseType isDefined) ('houseType -> input.houseType.get) else None ,
if(input.priceLow isDefined) ('priceLow -> input.priceLow.get) else None ,
if(input.priceHigh isDefined) ('priceHigh -> input.priceHigh.get) else None ,
if(input.utilities isDefined) ('utilities -> input.utilities.get) else None
).filter(_!=None).asInstanceOf[Seq[(Any,anorm.ParameterValue[_])]]
using SQL command:
SQL("SELECT * FROM Houses WHERE " + whereString).on(onList:_*).as(sqlToHouse *)
Now getting the exception
[ClassCastException: java.lang.Integer cannot be cast to anorm.ParameterValue]
The important thing is that you have to create values of type ParameterValue.
This is normally done using the toParameterValue() function.
One way would be to create a sequence of Options that you flatten:
val onList = Seq(
Some('school_id -> input.school),
input.rooms.map('rooms -> _),
input.bathrooms.map('bathrooms -> _)
).flatten
This sequence can then be mapped to correct values:
SQL(
"SELECT * FROM Houses WHERE " + whereString
).on(
onList.map(v => v._1 -> toParameterValue(v._2)): _*
)
This can be simplified like this:
val onList = Seq(
Some('school_id -> input.school),
input.rooms.map('rooms -> _),
input.bathrooms.map('bathrooms -> _)
).flatMap(_.map(v => v._1 -> toParameterValue(v._2)))
SQL(
"SELECT * FROM Houses WHERE " + whereString
).on(
onList: _*
)
Or maybe the simplest solution would be this:
val onList = Seq(
Some('school_id -> toParameterValue(input.school)),
input.rooms.map('rooms -> toParameterValue(_)),
input.bathrooms.map('bathrooms -> toParameterValue(_))
).flatten
SQL(
"SELECT * FROM Houses WHERE " + whereString
).on(
onList: _*
)
So I ended up just calling on multiple times.
var query = SQL("SELECT * FROM Houses WHERE " + whereString).on('school_id -> input.school)
if(input.rooms isDefined) query= query.on('rooms -> input.rooms.get)
if(input.bathrooms isDefined) query= query.on('bathrooms -> input.bathrooms.get)
if(input.houseType isDefined) query= query.on('houseType -> input.houseType.get)
if(input.priceLow isDefined) query= query.on('priceLow -> input.priceLow.get)
if(input.priceHigh isDefined) query= query.on('priceHigh -> input.priceHigh.get)
if(input.utilities isDefined) query= query.on('utilities -> input.utilities.get)
query.as(sqlToHouse *)
You can have a look at multivalue parameter is next Anorm (coming Play 2.3/master).