How to decode JSON to record built by a data constructor? - elm

There is the type
type User
= Anonymous
| Named {name : String, email : String}
Json.Decode.object2 doesn't fit here because its first arguments type is (a -> b -> c) but Named has { email : String, name : String } -> User type.
How to decode to User?

Another way of doing this could be to define a function that accepts a name and email and returns your Named constructor:
userDecoder : Decoder User
userDecoder =
let
named =
object2
(\n e -> Named { name = n, email = e })
("name" := string)
("email" := string)
in
oneOf [ null Anonymous, named ]

Since your Named constructor takes a record as a parameter, it might be a little cleaner to create a type alias for the name and email record.
type alias NamedUserInfo =
{ name : String
, email : String
}
You could then redefine User to use the alias:
type User
= Anonymous
| Named NamedUserInfo
While the above isn't strictly necessary, I find that aliasing record types proves useful in many ways down the road. Here it is useful because it gives us a constructor NamedUserInfo that lets us clearly define a decoder:
import Json.Decode exposing (..)
namedUserInfoDecoder : Decoder NamedUserInfo
namedUserInfoDecoder =
object2
NamedUserInfo
("name" := string)
("email" := string)
And finally, your user decoder could be constructed like this:
userDecoder : Decoder User
userDecoder =
oneOf
[ null Anonymous
, object1 Named namedUserInfoDecoder
]
You can run your example through a quick test like this:
exampleJson =
"""
[{"user":null}, {"user": {"name": "John Doe", "email": "j.doe#mailg.com"}}]
"""
main =
text <| toString <| decodeString (list ("user" := userDecoder)) exampleJson
-- Ouputs:
-- Ok ([Anonymous,Named { name = "John Doe", email = "j.doe#mailg.com" }])

Related

Ignore invalid item when decoding list

is it possible to ignore invalid items when decoding the list?
example: I have a Model
type Type
= A
| B
type alias Section =
{ sectionType : Type
, index : Int
}
getTypeFromString : String -> Maybe Type
getTypeFromString input =
case input of
“a” ->
Just A
“b” ->
Just B
_ ->
Nothing
decodeType : Decoder Type
decodeType =
Decode.string
|> Decode.andThen
(\str ->
case getTypeFromString str of
Just sectionType ->
Decode.succeed sectionType
Nothing ->
Decode.fail <| ("Unknown type" ++ str)
)
decodeSection : Decoder Section
decodeSection =
Decode.map2 Section
(Decode.field "type" decodeType)
(Decode.field "index" Decode.int)
if I decode the JSON
{
"sections": [{type: "A", index: 1}, {type: "invalid-type", index: 2}]
}
I expect my sections = [ {type = A, index= 1} ]
Generally the way you can deal with these is by decoding it to an Elm type that expresses the options, and then post processing with a map.
So for instance in your example, I would go for something like this:
decodeMaybeType : Decoder (Maybe Type)
decodeMaybeType =
Decode.string
|> Decode.map getTypeFromString
decodeMaybeSection : Decoder (Maybe Section)
decodeMaybeSection =
Decode.map2 (\maybeType index -> Maybe.map (\t -> Section t index) maybeType)
(Decode.field "type" decodeMaybeType)
(Decode.field "index" Decode.int)
decodeSections : Decoder (List Section)
decodeSections =
Decode.list decodeMaybeSection
|> Decode.map (List.filterMap identity)
NB: List.filterMap identity is a List (Maybe a) -> List a, it filters out the Nothing and gets rid of the Maybes in one go.
Given the comment by Quan Vo about one of many fields could be invalid, using Decode.oneOf might be a better fit.
You write the decoders for each field. If any field is illegal, the Section decoder fails and in oneOf, Nothing is returned.
(Here I am also using Json.Decode.Pipeline from NoRedInk).
import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Pipeline exposing (required)
type Type
= A
| B
type alias Section =
{ sectionType : Type
, index : Int
}
getTypeFromString : String -> Maybe Type
getTypeFromString input =
case input |> String.toLower of
"a" ->
Just A
"b" ->
Just B
_ ->
Nothing
decodeType : Decoder Type
decodeType =
Decode.string
|> Decode.andThen
(\str ->
case getTypeFromString str of
Just sectionType ->
Decode.succeed sectionType
Nothing ->
Decode.fail <| ("Unknown type" ++ str)
)
decodeSection : Decoder Section
decodeSection =
Decode.succeed Section
|> required "type" decodeType
|> required "index" Decode.int
-- Either we succeed in decoding a Section or fail on some field.
decodeMaybeSection : Decoder (Maybe Section)
decodeMaybeSection =
Decode.oneOf
[ decodeSection |> Decode.map Just
, Decode.succeed Nothing
]
decodeSections : Decoder (List Section)
decodeSections =
Decode.list decodeMaybeSection
|> Decode.map (List.filterMap identity)

Elm `update` is not executing the case statement when dropdown's value is changed

I am new to Elm. I am not able to call the update function once the dropdown value changes.
Scenario:
I have two dropdowns Grade and Environment. What I want is when Grade dropdown values change, the options of Environment dropdown will dependently change.
For example, if Grade dropdown value is 3 then the options of Environment should change to Imagine Math
gradeDropdown : String -> List String -> Html Msg
gradeDropdown grade grades =
let
buildOption =
gradeOption grade
in
select [ class "importWizard--gradeSelection", name "gradeSelection", onChange (UpdateStudent Grade) ]
(map buildOption grades)
gradeOption : String -> String -> Html Msg
gradeOption optSelectedVal temp =
let
optSelected =
temp == optSelectedVal
in
option [ value temp, selected optSelected ] [ text temp ]
environmentDropdown : Model -> String -> List String -> String -> Html Msg
environmentDropdown model learningEnvironment learningEnvironments selectedGrade =
let
buildOption =
environmentOption model learningEnvironment
blueprint_grades = ["PreKindergarten", "Kindergarten", "1"]
environmentDropdownOption =
if (selectedGrade == "" || (List.member selectedGrade blueprint_grades)) then
["Blueprint"]
else if (selectedGrade == "2") then
learningEnvironments
else
["Imagine Math"]
in
select [
class "importWizard--learningEnvironmentSelection"
, name "learningEnvironmentSelection"
, onChange (UpdateStudent LearningEnvironments)
]
(map buildOption environmentDropdownOption)
environmentOption : Model -> String -> String -> Html Msg
environmentOption model optSelectedVal temp =
let
optSelected =
temp == optSelectedVal
in
option [ value temp, selected optSelected ] [ text temp ]
And in Update
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
update flags message model =
case message of
UpdateStudent updateType newValue ->
let
validate =
validateStudent flags validatableFieldsForScreen
in
case updateType of
LastName ->
( validate { model | lastName = newValue } <| Just LastNameField, Cmd.none )
FirstName ->
( validate { model | firstName = newValue } <| Just FirstNameField, Cmd.none )
Sin ->
( validate { model | sin = newValue } <| Just SinField, Cmd.none )
Grade ->
( validate { model | grade = newValue, selectedGrade = newValue } Nothing, Cmd.none )
LearningEnvironments ->
( validate { model | learningEnvironments = newValue } Nothing, Cmd.none )
View:
, td [ class wizardTableInput ] [ gradeDropdown model.grade flags.grades ]
, td [ class wizardTableInput ] [ environmentDropdown model model.learningEnvironments flags.learningEnvironments model.selectedGrade ]
In this code, the environment dropdown's value is changing, however the model's value is not updated. From what I understand, I can see is environment dropdown's id re-rendered, but it is not updating the model's value of learningEnvironments. This means it is not executing the update function matching LearningEnvironments.
select widgets where the options change is one of the use cases for Html.Keyed.node
Use a helper function like the one bellow:
keyedSelect : (String -> a) -> String -> List ( String, String ) -> Html a
keyedSelect message selectedValue kvs =
let
toOption ( k, v ) =
( k
, option
[ value k
, selected (k == selectedValue)
, disabled (k == "-1")
]
[ text v ]
)
in
Keyed.node "select"
[ class "form-control", onChange message ]
(List.map toOption kvs)
I usually have a "Please select Foo" first option with the value -1 if the user never selected any of the options. This is why the code checks for -1 and disables the option. You can remove the disabled attribute if you don't need it.

How access multiples objects in a list

I have this code in Elm-lang:
import Json.Decode exposing (..)
import Html exposing (..)
json = -- List that contains and will have many users
"""
[{\"ssn\":\"111.111.111-11\",\"name\":\"People Silva\",\"email\":\"people#eat.com\"}, {\"ssn\":\"000.000.000-00\",\"name\":\"Pet Silva\",\"email\":\"pet#eat.com\"}]
"""
type alias User =
{ name : String
, email: String
, ssn: String
}
userDecoder : Json.Decode.Decoder User
userDecoder =
Json.Decode.map3 User
(field "name" string)
(field "email" string)
(field "ssn" string)
userListDecoder : Json.Decode.Decoder (List User)
userListDecoder =
Json.Decode.list userDecoder
main =
let
decoded = (decodeString userListDecoder json)
in
case decoded of
Ok u ->
span [] [text (toString u)]
Err e ->
span [] [text (toString e)]
This code work very well, and output this(how expected):
[{ name = "People Silva", email = "people#eat.com", ssn = "111.111.111-11" },{ name = "Pet Silva", email = "pet#eat.com", ssn = "000.000.000-00" }]
And here begins my doubt, how get list of all users in list?(And also as a bonus, get the number of users in the list)
-- already tried, unsuccessfully
u[0].name
u[1].name
The users are in a List User value, so you can use the functions from the List package to access them.
If you want to get a list of user names from a list of users, you can call List.map .name users.
If you want to write the user names in their own divs, you can write it like this:
showUser : User -> Html msg
showUser user =
div [] [ text user.name ]
and call it from main like this:
main =
let
decoded = (decodeString userListDecoder json)
in
case decoded of
Ok users ->
div [] (List.map showUser users)
Err e ->
span [] [text (toString e)]
Obtaining the length of a list is just a matter of using the List.length function: List.length users

Create a New Record in a List of data in Elm

I finished loading resources from an API in Elm, everything is fine... except for one litte problem : I don't know how to update or create a new record without persisting it.
I have a type Msg (I striped some code for this demo)
type Msg
= NoOp
| FetchSucceed (List User)
| FetchError Http.Error
| UpdateTitle String
| ...
update msg model =
case model of
NoOp ->
( model, Cmd.none )
FetchSucceed newModel =
( { model | users = newModel, isLoading = False }, Cmd.none )
FetchError _ =
( { model | isLoading = False }, Cmd.none )
UpdateTitle newTitle =
-- I don't know what to put here, the previous messages
-- have a list, and I Just want to add ONE model
view model =
div []
[ List.map displayRow model.users
, formCreateUser {title = "", username = "", email = ""}
]
formCreateUser user =
div []
[ input [ onInput UpdateTitle, placeholder "Title" ] []
, button [ onClick SaveUser ] [ text "Save" ]
]
I would love to be able to add a new model from this form (formCreateUser), but I keep getting this error :
The 3rd element has this type:
VirtualDom.Node Msg
But the 4th is:
Html Link -> Html (String -> Msg)
edit2: Add some context
If I understand your example snippets, you have a page that shows the list of existing user, and you want to have a "quick add" form that lets you create another user given only a title. I'll give a quick example of how to achieve this which should hopefully shed some light on the problems you've run into.
I'm assuming your User and Model look like this at present:
type alias Model =
{ users : List User
, isLoading : Bool
}
type alias User =
{ title : String
, username : String
, email : String
}
Since you have that quick add form, I don't think you want to append the new user until they hit Submit. With that notion in mind, let's update Model to store the pending new user title:
type alias Model =
{ users : List User
, isLoading : Bool
, newUserTitle : Maybe String
}
Now we can change your view function accordingly. Since we want to display the typed title in the textbox, let's change formCreateUser to this:
formCreateUser model =
div []
[ input [ onInput UpdateTitle, placeholder "Title", value (Maybe.withDefault "" model.newUserTitle) ] []
, button [ onClick SaveUser ] [ text "Save" ]
]
That means the calling code in view needs updating too:
view model =
div []
[ div [] (List.map displayRow model.users)
, formCreateUser model
]
Now we need to handle the UpdateTitle Msg to set the contents as they are typed:
UpdateTitle newTitle ->
( { model | newUserTitle = Just newTitle }, Cmd.none )
And now we can also handle the submit button. This is where you would create the new user and append it to the list of existing users:
SaveUser ->
case model.newUserTitle of
Nothing -> (model, Cmd.none)
Just title ->
( { model
| newUserTitle = Nothing
, users = model.users ++ [{ title = title, username = "", email = "" }]
}, Cmd.none)
If you wanted SaveUser to submit it to your API endpoint, you'd also return an appropriate Cmd, but that seems outside the scope of your question.
While this all isn't an ideal way to handle your situation, hopefully this explanation gives you more understanding of the building blocks needed for this type of thing. I've posted the full gist here which can be pasted and run in elm-lang.org/try.

Extending the Elm tutorial form app to include a numbered input Age

I've been following this tutorial: http://guide.elm-lang.org/architecture/user_input/forms.html
The text there makes sense to me, my question pertains to the exercise it lists at the bottom of the page. It asks that I:
"Add an additional field for age and check that it is a number."
I am having difficulty with this because the onInput function seems to only accept a String input. I find it odd that there is no equivalent for type="number" inputs.
Nevertheless, this is my attempt which does not work:
import Html exposing (..)
import Html.App as Html
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import String exposing (length)
main =
Html.beginnerProgram { model = model, view = view, update = update }
-- MODEL
type alias Model =
{ name : String
, password : String
, passwordAgain : String
, age : Int
}
model : Model
model =
Model "" "" "" 0
-- UPDATE
type Msg
= Name String
| Password String
| PasswordAgain String
| Age Int
update : Msg -> Model -> Model
update msg model =
case msg of
Name name ->
{ model | name = name }
Password password ->
{ model | password = password }
PasswordAgain password ->
{ model | passwordAgain = password }
Age age ->
{ model | age = age }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input [ type' "text", placeholder "Name", onInput Name ] []
, input [ type' "password", placeholder "Password", onInput Password ] []
, input [ type' "password", placeholder "Re-enter Password", onInput PasswordAgain ] []
, input [ type' "number", placeholder "Age", onInput Age ] []
, viewValidation model
]
viewValidation : Model -> Html msg
viewValidation model =
let
(color, message) =
if model.password /= model.passwordAgain then
("red", "Passwords do not match!")
else if length model.password <= 8 then
("red", "Password must be more than 8 characters!")
else
("green", "OK")
in
div [ style [("color", color)] ] [ text message ]
The error I get is the following:
-- TYPE MISMATCH ----------------------------------------------------- forms.elm
The argument to function `onInput` is causing a mismatch.
58| onInput Age
^^^
Function `onInput` is expecting the argument to be:
String -> a
But it is:
Int -> Msg
Note: I am aware that I could create the Age input as just another text input, but the exercise specifically asked me to check that it is a `number type. I assume this means I should hold it inside the model as an Int.
I am clear about what the error is. I simply want to know the idiomatic way to fix this in Elm. Thanks.
Any user-input from onInput event is a String.
Your Model expects it to be an Int
Use String.toInt to parse the integer value from a string value.
Adjust update function to convert the type to an Int and change the type signature to Age String
Age age ->
case String.toInt age of
Ok val ->
{ model | age = val }
-- Notify the user, or simply ignore the value
Err err ->
model
That way you have an option to notify the user about the error.
In case if Maybe value suits you better, the whole statement can be simplified to:
Age age ->
{ model | age = Result.toMaybe (String.toInt age) }
You'll need the equivalent of onInput but for one that operates on integers. Based on how targetValue is defined, you can do something similar with the addition of Json.Decode.int to parse it as an integer:
onIntInput : (Int -> msg) -> Attribute msg
onIntInput tagger =
Html.Events.on "input" (Json.map tagger (Json.at ["target", "value"] Json.int))
You can then use it as such:
, input [ type' "number", placeholder "Age", onIntInput Age ] []