manonthemat
manonthemat

Reputation: 6251

Making HTTP call to modify data with elm

As pictured here, I've created a small sample program that has some controls to modify parts of the model.

What I've been unsuccessfully trying to do is to make a HTTP request to get the initial data (it's hardcoded right now), or later on replace the data with the response from said HTTP request when a Reset message is received. I did read the HTTP chaper of the introduction to Elm, but I can't seem to piece things together.

screenshot of running compiled elm program in browser

The goal is to have a loadTraits function that takes a String (SomeId) and returns a List of type TraitWithRelevance, so I can replace the model with that incoming data.

module Main exposing (..)

import Html exposing (Html, button, div, text, input, ul, img)
import Html.Attributes as Attr
import Html.Events exposing (onClick, onInput)
import Http exposing (..)
import Json.Decode as Decode


main =
    Html.program
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }



-- MODEL


type alias ContentWithTraits =
    { someId : SomeId
    , traits : List TraitWithRelevance
    }


type alias MetaInfo =
    { name : String
    , imageUrl : String
    }


type alias Name =
    String


type alias SomeId =
    String


type alias Relevance =
    String


type alias TraitWithRelevance =
    ( Name, SomeId, Relevance )


type TraitToAdd
    = Nothing
    | TraitWithRelevance


type alias Model =
    { contentWithTraits : ContentWithTraits
    , metaInfo : MetaInfo
    , traitToAdd : TraitToAdd
    }


init : ( Model, Cmd Msg )
init =
    ( Model contentWithTraits { name = "content name", imageUrl = "http://weknowmemes.com/generator/uploads/generated/g1369409960206058073.jpg" } Nothing, Cmd.none )


contentWithTraits : ContentWithTraits
contentWithTraits =
    { someId = "some default id"
    , traits =
        [ ( "name for trait a", "a", "1" )
        , ( "this is the name for trait b", "b", "50" )
        ]
    }



-- UPDATE


type Msg
    = EditTrait SomeId Relevance
    | Reset


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        EditTrait someId relevance ->
            let
                _ =
                    Debug.log "model: " model
            in
                ( replaceTraits model <| List.map (updateTrait ( someId, relevance ))
                , Cmd.none
                )

        Reset ->
            -- ( replaceTraits model <| loadTraits model.contentWithTraits.someId, Cmd.none )
            {-
               NOTE: I'm stuck here...
               should make HTTP GET request, then replace the model.contentWithTraits.traits with the decoded JSON's traits field
            -}
            ( model, Cmd.none )


replaceTraits : Model -> (List TraitWithRelevance -> List TraitWithRelevance) -> Model
replaceTraits model func =
    { model
        | contentWithTraits =
            { someId = model.contentWithTraits.someId
            , traits = func model.contentWithTraits.traits
            }
    }


updateTrait : ( SomeId, Relevance ) -> TraitWithRelevance -> TraitWithRelevance
updateTrait updatedTrait originalTrait =
    let
        ( name, someId, _ ) =
            originalTrait

        ( someIdVerification, newValue ) =
            updatedTrait

        _ =
            Debug.log "updatedTrait: " updatedTrait
    in
        if someId == someIdVerification then
            ( name, someId, newValue )
        else
            originalTrait



-- VIEW


valueRange : String -> TraitWithRelevance -> Html Msg
valueRange typ trait =
    let
        ( name, someId, relevance ) =
            trait
    in
        input [ Attr.type_ typ, Attr.min <| toString 0, Attr.max <| toString 100, Attr.value relevance, Attr.step <| toString 1, onInput <| EditTrait someId ] []


traitView : TraitWithRelevance -> Html Msg
traitView trait =
    let
        ( name, someId, relevance ) =
            trait
    in
        div []
            [ text someId
            , valueRange "range" trait
            , valueRange "number" trait
            , text name
            ]


view : Model -> Html Msg
view model =
    div []
        [ text model.contentWithTraits.someId
        , img [ Attr.src model.metaInfo.imageUrl, Attr.width 300 ] []
        , ul [] (List.map traitView model.contentWithTraits.traits)
        , button [ onClick Reset ] [ text "Reset" ]
        ]

Here's an example response from the http server. I've chosen this format, because I thought it'd map easiest to the elm model. I can easily change the response if there's a better way to consume this data in elm.

{"traits":[["name for trait a","a",1],["this is the name for trait b,"b",50]]}

P.S. Even though there's plenty of lines of code, please be aware that I tried to strip down the problem as much as possible, while keeping enough context.

Upvotes: 0

Views: 156

Answers (3)

manonthemat
manonthemat

Reputation: 6251

While both answers have been correct and helpful, it took me still another mile to reach my destination.

First off, I didn't want to rely on a non elm-lang package. Json.Decode.Pipeline therefore doesn't meet that requirement.

With the examples posted, I still had the trouble of decoding the list of traits. What I ended up doing was changing the response from the server so that the traits list is not of type list, but of type object with name, someId, and relevance as keys and its values as strings. This helped me distinguish between what's coming back from the server and what's being represented internally in my elm model.

So the Reset functionality works nicely, the next step is to get those values into the initial state of the model without user interaction.

module Main exposing (..)

import Html exposing (Html, button, div, text, input, ul, img)
import Html.Attributes as Attr
import Html.Events exposing (onClick, onInput)
import Http exposing (..)
import Json.Decode as Decode exposing (Decoder)


main =
    Html.program
        { init = init
        , view = view
        , update = update
        , subscriptions = \_ -> Sub.none
        }



-- MODEL


type alias ContentWithTraits =
    { someId : SomeId
    , traits : List TraitWithRelevance
    }


type alias MetaInfo =
    { name : String
    , imageUrl : String
    }


type alias Name =
    String


type alias SomeId =
    String


type alias Relevance =
    String


type alias TraitWithRelevance =
    ( Name, SomeId, Relevance )


type TraitToAdd
    = Nothing
    | TraitWithRelevance


type alias Model =
    { contentWithTraits : ContentWithTraits
    , metaInfo : MetaInfo
    , traitToAdd : TraitToAdd
    }


type alias TraitInfo =
    { traits :
        List TraitObject
    }


type Traits
    = TraitInfoFromServer (List TraitObject)


type alias TraitObject =
    { name : String, someId : String, relevance : String }


fetchTraits : String -> Cmd Msg
fetchTraits someId =
    Http.get
        ("http://localhost:8000/traits/" ++ someId)
        decodeTraits
        |> Http.send OnFetchTraits


decodeTraits : Decoder TraitInfo
decodeTraits =
    Decode.map TraitInfo
        (Decode.field "traits" (Decode.list decodeTrait))


decodeTrait : Decoder TraitObject
decodeTrait =
    (Decode.map3 TraitObject
        (Decode.field "name" Decode.string)
        (Decode.field "someId" Decode.string)
        (Decode.field "relevance" Decode.string)
    )


init : ( Model, Cmd Msg )
init =
    ( Model contentWithTraits { name = "content name", imageUrl = "http://weknowmemes.com/generator/uploads/generated/g1369409960206058073.jpg" } Nothing, Cmd.none )


contentWithTraits : ContentWithTraits
contentWithTraits =
    { someId = "someIdToStartWith"
    , traits =
        [ ( "trait a", "a", "1" )
        , ( "trait b", "b", "50" )
        ]
    }



-- UPDATE


type Msg
    = EditTrait SomeId Relevance
    | Reset
    | OnFetchTraits (Result Http.Error TraitInfo)


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        EditTrait someId relevance ->
            ( replaceTraits model <| List.map (updateTrait ( someId, relevance ))
            , Cmd.none
            )

        Reset ->
            ( model, fetchTraits model.contentWithTraits.someId )

        OnFetchTraits resp ->
            let
                newTraits =
                    case resp of
                        Ok val ->
                            val.traits

                        Result.Err e ->
                            []
            in
                ( { model
                    | contentWithTraits =
                        { someId = model.contentWithTraits.someId
                        , traits = List.map traitObjToTuple newTraits
                        }
                  }
                , Cmd.none
                )


traitObjToTuple : TraitObject -> TraitWithRelevance
traitObjToTuple obj =
    ( obj.name, obj.someId, obj.relevance )


replaceTraits : Model -> (List TraitWithRelevance -> List TraitWithRelevance) -> Model
replaceTraits model func =
    { model
        | contentWithTraits =
            { someId = model.contentWithTraits.someId
            , traits = func model.contentWithTraits.traits
            }
    }


updateTrait : ( SomeId, Relevance ) -> TraitWithRelevance -> TraitWithRelevance
updateTrait updatedTrait originalTrait =
    let
        ( name, someId, _ ) =
            originalTrait

        ( someIdVerification, newValue ) =
            updatedTrait
    in
        if someId == someIdVerification then
            ( name, someId, newValue )
        else
            originalTrait



-- VIEW


valueRange : String -> TraitWithRelevance -> Html Msg
valueRange typ trait =
    let
        ( name, someId, relevance ) =
            trait
    in
        input [ Attr.type_ typ, Attr.min <| toString 0, Attr.max <| toString 100, Attr.value relevance, Attr.step <| toString 1, onInput <| EditTrait someId ] []


traitView : TraitWithRelevance -> Html Msg
traitView trait =
    let
        ( name, someId, relevance ) =
            trait
    in
        div []
            [ text name
            , valueRange "range" trait
            , valueRange "number" trait
            , text someId
            ]


view : Model -> Html Msg
view model =
    div []
        [ text model.contentWithTraits.someId
        , img [ Attr.src model.metaInfo.imageUrl, Attr.width 100 ] []
        , ul [] (List.map traitView model.contentWithTraits.traits)
        , button [ onClick Reset ] [ text "Reset" ]
        ]

Upvotes: 0

smogger914
smogger914

Reputation: 25

So basically you need to do two things, the reset button should call a command to get the traits. Then, in the update you have to handle the response from the command. After getting the Result back you can use it to update your model.

Here is an update to your code. I added a person to the model which gets updated when the user presses the reset button.

    module Main exposing (..)

    import Html exposing (Html, button, div, text, input, ul, img)
    import Html.Attributes as Attr
    import Html.Events exposing (onClick, onInput)
    import Http exposing (..)
    import Json.Decode exposing (Decoder, string)
    import Json.Decode.Pipeline exposing (decode, required)


    main =
        Html.program
            { init = init
            , view = view
            , update = update
            , subscriptions = \_ -> Sub.none
            }



    -- Commands
    type alias Person =
        { name : String
        , gender : String
        }

    decodePerson : Decoder Person
    decodePerson =
        decode Person
            |> required "name" string
            |> required "gender" string

    getTraits =
        let
            url =
                "http://swapi.co/api/people/1/"

            request =
                Http.get url decodePerson
        in
            Http.send GetTraitResponse request



    -- MODEL


    type alias ContentWithTraits =
        { someId : SomeId
        , traits : List TraitWithRelevance
        }


    type alias MetaInfo =
        { name : String
        , imageUrl : String
        }


    type alias Name =
        String


    type alias SomeId =
        String


    type alias Relevance =
        String


    type alias TraitWithRelevance =
        ( Name, SomeId, Relevance )


    type TraitToAdd
        = Nothing
        | TraitWithRelevance


    type alias Model =
        { contentWithTraits : ContentWithTraits
        , metaInfo : MetaInfo
        , traitToAdd : TraitToAdd
        , person : Person
        }


    init : ( Model, Cmd Msg )
    init =
        ( Model contentWithTraits { name = "content name", imageUrl = "http://weknowmemes.com/generator/uploads/generated/g1369409960206058073.jpg"} Nothing {name = "", gender=""}, Cmd.none )


    contentWithTraits : ContentWithTraits
    contentWithTraits =
        { someId = "some default id"
        , traits =
            [ ( "name for trait a", "a", "1" )
            , ( "this is the name for trait b", "b", "50" )
            ]
        }



    -- UPDATE


    type Msg
        = EditTrait SomeId Relevance
        | GetTraitResponse (Result Http.Error Person)
        | Reset


    update : Msg -> Model -> ( Model, Cmd Msg )
    update msg model =
        case msg of
            EditTrait someId relevance ->
                let
                    _ =
                        Debug.log "model: " model
                in
                    ( replaceTraits model <| List.map (updateTrait ( someId, relevance ))
                    , Cmd.none
                    )
            -- handle the response
            GetTraitResponse resp ->
                let
                    _ =
                        Debug.log "response" resp
                    person = 
                        case resp of
                            Ok val -> 
                                val
                            Result.Err e -> 
                                {name = "", gender=""}
                in
                    ( {model | person = person }, Cmd.none )

            Reset ->
                -- ( replaceTraits model <| loadTraits model.contentWithTraits.someId, Cmd.none )
                {-
                NOTE: I'm stuck here...
                should make HTTP GET request, then replace the model.contentWithTraits.traits with the decoded JSON's traits field
                -}
                -- call the command to get the traits
                ( model, getTraits )


    replaceTraits : Model -> (List TraitWithRelevance -> List TraitWithRelevance) -> Model
    replaceTraits model func =
        { model
            | contentWithTraits =
                { someId = model.contentWithTraits.someId
                , traits = func model.contentWithTraits.traits
                }
        }


    updateTrait : ( SomeId, Relevance ) -> TraitWithRelevance -> TraitWithRelevance
    updateTrait updatedTrait originalTrait =
        let
            ( name, someId, _ ) =
                originalTrait

            ( someIdVerification, newValue ) =
                updatedTrait

            _ =
                Debug.log "updatedTrait: " updatedTrait
        in
            if someId == someIdVerification then
                ( name, someId, newValue )
            else
                originalTrait



    -- VIEW


    valueRange : String -> TraitWithRelevance -> Html Msg
    valueRange typ trait =
        let
            ( name, someId, relevance ) =
                trait
        in
            input [ Attr.type_ typ, Attr.min <| toString 0, Attr.max <| toString 100, Attr.value relevance, Attr.step <| toString 1, onInput <| EditTrait someId ] []


    traitView : TraitWithRelevance -> Html Msg
    traitView trait =
        let
            ( name, someId, relevance ) =
                trait
        in
            div []
                [ text someId
                , valueRange "range" trait
                , valueRange "number" trait
                , text name
                ]


    view : Model -> Html Msg
    view model =
        div []
            [ text model.contentWithTraits.someId
            , img [ Attr.src model.metaInfo.imageUrl, Attr.width 300 ] []
            , ul [] (List.map traitView model.contentWithTraits.traits)
            , button [ onClick Reset ] [ text "Reset" ]
            , text <| toString model
            ]

Upvotes: 1

Shaun the Sheep
Shaun the Sheep

Reputation: 22742

You need to have a message which returns the data. Assuming it's a simple list of Traits:

type Msg
    = EditTrait SomeId Relevance
    | Reset
    | OnFetchTraits (Result Http.Error (List Traits))

Then you need a command to send the request, something like

fetchTraits : Cmd Msg
fetchTraits =
    Http.get "http://localhost:4000/traits" traitListDecoder
        |> Http.send OnFetchTraits

and you need to implement traitListDecoder, to decode your JSON into the list that is returned in the msg.

Then instead of returning Cmd.none in your update function where you are stuck, you return fetchTraits. Elm will then make the request and you will get an OnFetchTraits msg passed into update. You need a separate case to handle this. You unpack the Result type and extract your data if the request was successful, or handle the error if not.

Upvotes: 1

Related Questions