diff --git a/part10/ElmHub.elm b/part10/ElmHub.elm index 38aead5..6300c50 100644 --- a/part10/ElmHub.elm +++ b/part10/ElmHub.elm @@ -3,6 +3,7 @@ module ElmHub (..) where import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (..) +import Html.Lazy exposing (..) import Http import Auth import Task exposing (Task) @@ -34,7 +35,8 @@ searchFeed query = responseDecoder : Decoder (List SearchResult.Model) responseDecoder = - "items" := Json.Decode.list SearchResult.decoder + -- TODO make use of SearchResult's decoder + Json.Decode.succeed [] type alias Model = @@ -72,15 +74,7 @@ viewSearchResults address results = results |> Dict.values |> List.sortBy (.stars >> negate) - |> filterResults - |> List.map (SearchResult.view address DeleteById) - - -filterResults : List SearchResult.Model -> List SearchResult.Model -filterResults results = - -- TODO filter out repos with 0 stars - -- using a case-expression rather than List.filter - results + |> List.map (\_ -> div [] [ text "TODO replace this line with view logic from SearchResult" ]) onInput address wrap = diff --git a/part10/README.md b/part10/README.md index 50a317c..f08ee4f 100644 --- a/part10/README.md +++ b/part10/README.md @@ -1,5 +1,5 @@ -Part 10 -======= +Part 9 +====== ## Installation diff --git a/part10/SearchResult.elm b/part10/SearchResult.elm index 95a843c..e93bd0c 100644 --- a/part10/SearchResult.elm +++ b/part10/SearchResult.elm @@ -28,23 +28,18 @@ decoder = ("stargazers_count" := Json.Decode.int) -view : Address a -> (Int -> a) -> Model -> Html -view address delete result = +view : Address a -> Model -> Html +view address result = li [] [ span [ class "star-count" ] [ text (toString result.stars) ] , a - [ href - ("https://github.com/" - ++ (Debug.log - "TODO we should not see this when typing in the search box!" - result.name - ) - ) + [ href ("https://github.com/" ++ result.name) , target "_blank" ] [ text result.name ] , button - [ class "hide-result", onClick address (delete result.id) ] + -- TODO onClick, send a delete action to the address + [ class "hide-result" ] [ text "X" ] ] diff --git a/part11/ElmHub.elm b/part11/ElmHub.elm index 8476dbf..38aead5 100644 --- a/part11/ElmHub.elm +++ b/part11/ElmHub.elm @@ -3,7 +3,6 @@ module ElmHub (..) where import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (..) -import Html.Lazy exposing (..) import Http import Auth import Task exposing (Task) @@ -74,20 +73,14 @@ viewSearchResults address results = |> Dict.values |> List.sortBy (.stars >> negate) |> filterResults - |> List.map (lazy3 SearchResult.view address DeleteById) + |> List.map (SearchResult.view address DeleteById) filterResults : List SearchResult.Model -> List SearchResult.Model filterResults results = - case results of - [] -> - [] - - result :: rest -> - if result.stars > 0 then - result :: (filterResults rest) - else - filterResults rest + -- TODO filter out repos with 0 stars + -- using a case-expression rather than List.filter + results onInput address wrap = diff --git a/part11/README.md b/part11/README.md index f01b7a8..50a317c 100644 --- a/part11/README.md +++ b/part11/README.md @@ -1,4 +1,4 @@ -Part 11 +Part 10 ======= ## Installation @@ -15,9 +15,3 @@ to fail; in that case, just run `elm package install` again.) ```bash elm live Main.elm --open -- --output=elm.js ``` - -## Compiling CSS - -```bash -elm css Stylesheets.elm -``` diff --git a/part11/SearchResult.elm b/part11/SearchResult.elm index daf012a..95a843c 100644 --- a/part11/SearchResult.elm +++ b/part11/SearchResult.elm @@ -34,7 +34,13 @@ view address delete result = [] [ span [ class "star-count" ] [ text (toString result.stars) ] , a - [ href ("https://github.com/" ++ result.name) + [ href + ("https://github.com/" + ++ (Debug.log + "TODO we should not see this when typing in the search box!" + result.name + ) + ) , target "_blank" ] [ text result.name ] diff --git a/part11/elm-package.json b/part11/elm-package.json index 7f72d5a..ba40466 100644 --- a/part11/elm-package.json +++ b/part11/elm-package.json @@ -12,8 +12,7 @@ "evancz/elm-effects": "2.0.0 <= v < 3.0.0", "evancz/elm-html": "4.0.0 <= v < 5.0.0", "evancz/elm-http": "3.0.0 <= v < 4.0.0", - "evancz/start-app": "2.0.0 <= v < 3.0.0", - "rtfeldman/elm-css": "1.0.0 <= v < 2.0.0" + "evancz/start-app": "2.0.0 <= v < 3.0.0" }, "elm-version": "0.16.0 <= v < 0.17.0" } diff --git a/part11/style.css b/part11/style.css index 9d13aae..64c74d7 100644 --- a/part11/style.css +++ b/part11/style.css @@ -1,6 +1,92 @@ + .content { - width: 960px; - margin: 0 auto; - padding: 30px; - font-family: Helvetica, Arial, serif; + width: 960px; + margin: 0 auto; + padding: 30px; + font-family: Helvetica, Arial, serif; +} + +header { + position: relative; + padding: 6px 12px; + height: 36px; + background-color: rgb(96, 181, 204); +} + +h1 { + color: white; + font-weight: normal; + margin: 0; +} + +.tagline { + color: #eee; + position: absolute; + right: 16px; + top: 12px; + font-size: 24px; + font-style: italic; +} + +.results { + list-style-image: url('http://img-cache.cdn.gaiaonline.com/76bd5c99d8f2236e9d3672510e933fdf/http://i278.photobucket.com/albums/kk81/d3m3nt3dpr3p/Tiny-Star-Icon.png'); + list-style-position: inside; + padding: 0; +} + +.results li { + font-size: 18px; + margin-bottom: 16px; +} + +.star-count { + font-weight: bold; + margin-right: 16px; +} + +a { + color: rgb(96, 181, 204); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.search-query { + padding: 8px; + font-size: 24px; + margin-bottom: 18px; + margin-top: 36px; +} + +.search-button { + padding: 8px 16px; + font-size: 24px; + color: white; + border: 1px solid #ccc; + background-color: rgb(96, 181, 204); + margin-left: 12px +} + +.search-button:hover { + color: rgb(96, 181, 204); + background-color: white; +} + +.hide-result { + background-color: transparent; + border: 0; + font-weight: bold; + font-size: 18px; + margin-left: 18px; + cursor: pointer; +} + +.hide-result:hover { + color: rgb(96, 181, 204); +} + +button:focus, input:focus { + outline: none; } diff --git a/part12/ElmHub.elm b/part12/ElmHub.elm new file mode 100644 index 0000000..8476dbf --- /dev/null +++ b/part12/ElmHub.elm @@ -0,0 +1,132 @@ +module ElmHub (..) where + +import Html exposing (..) +import Html.Attributes exposing (..) +import Html.Events exposing (..) +import Html.Lazy exposing (..) +import Http +import Auth +import Task exposing (Task) +import Effects exposing (Effects) +import Json.Decode exposing (Decoder, (:=)) +import Json.Encode +import Signal exposing (Address) +import Dict exposing (Dict) +import SearchResult + + +searchFeed : String -> Task x Action +searchFeed query = + let + -- See https://developer.github.com/v3/search/#example for how to customize! + url = + "https://api.github.com/search/repositories?access_token=" + ++ Auth.token + ++ "&q=" + ++ query + ++ "+language:elm&sort=stars&order=desc" + + task = + Http.get responseDecoder url + |> Task.map SetResults + in + Task.onError task (\_ -> Task.succeed (SetResults [])) + + +responseDecoder : Decoder (List SearchResult.Model) +responseDecoder = + "items" := Json.Decode.list SearchResult.decoder + + +type alias Model = + { query : String + , results : Dict SearchResult.ResultId SearchResult.Model + } + + +initialModel : Model +initialModel = + { query = "tutorial" + , results = Dict.empty + } + + +view : Address Action -> Model -> Html +view address model = + div + [ class "content" ] + [ header + [] + [ h1 [] [ text "ElmHub" ] + , span [ class "tagline" ] [ text "“Like GitHub, but for Elm things.”" ] + ] + , input [ class "search-query", onInput address SetQuery, defaultValue model.query ] [] + , button [ class "search-button", onClick address Search ] [ text "Search" ] + , ul + [ class "results" ] + (viewSearchResults address model.results) + ] + + +viewSearchResults : Address Action -> Dict SearchResult.ResultId SearchResult.Model -> List Html +viewSearchResults address results = + results + |> Dict.values + |> List.sortBy (.stars >> negate) + |> filterResults + |> List.map (lazy3 SearchResult.view address DeleteById) + + +filterResults : List SearchResult.Model -> List SearchResult.Model +filterResults results = + case results of + [] -> + [] + + result :: rest -> + if result.stars > 0 then + result :: (filterResults rest) + else + filterResults rest + + +onInput address wrap = + on "input" targetValue (\val -> Signal.message address (wrap val)) + + +defaultValue str = + property "defaultValue" (Json.Encode.string str) + + +type Action + = Search + | SetQuery String + | DeleteById SearchResult.ResultId + | SetResults (List SearchResult.Model) + + +update : Action -> Model -> ( Model, Effects Action ) +update action model = + case action of + Search -> + ( model, Effects.task (searchFeed model.query) ) + + SetQuery query -> + ( { model | query = query }, Effects.none ) + + SetResults results -> + let + resultsById : Dict SearchResult.ResultId SearchResult.Model + resultsById = + results + |> List.map (\result -> ( result.id, result )) + |> Dict.fromList + in + ( { model | results = resultsById }, Effects.none ) + + DeleteById id -> + let + newModel = + { model | results = Dict.remove id model.results } + in + ( newModel, Effects.none ) diff --git a/part11/ElmHub/Css.elm b/part12/ElmHub/Css.elm similarity index 100% rename from part11/ElmHub/Css.elm rename to part12/ElmHub/Css.elm diff --git a/part12/Main.elm b/part12/Main.elm index 5b8e88e..fa4bada 100644 --- a/part12/Main.elm +++ b/part12/Main.elm @@ -1,7 +1,7 @@ module Main (..) where import StartApp -import Component.ElmHub exposing (..) +import ElmHub exposing (..) import Effects exposing (Effects) import Task exposing (Task) import Html exposing (Html) diff --git a/part12/README.md b/part12/README.md index deddbd0..f01b7a8 100644 --- a/part12/README.md +++ b/part12/README.md @@ -1,4 +1,4 @@ -Part 12 +Part 11 ======= ## Installation @@ -15,3 +15,9 @@ to fail; in that case, just run `elm package install` again.) ```bash elm live Main.elm --open -- --output=elm.js ``` + +## Compiling CSS + +```bash +elm css Stylesheets.elm +``` diff --git a/part9/SearchResult.elm b/part12/SearchResult.elm similarity index 83% rename from part9/SearchResult.elm rename to part12/SearchResult.elm index e93bd0c..daf012a 100644 --- a/part9/SearchResult.elm +++ b/part12/SearchResult.elm @@ -28,8 +28,8 @@ decoder = ("stargazers_count" := Json.Decode.int) -view : Address a -> Model -> Html -view address result = +view : Address a -> (Int -> a) -> Model -> Html +view address delete result = li [] [ span [ class "star-count" ] [ text (toString result.stars) ] @@ -39,7 +39,6 @@ view address result = ] [ text result.name ] , button - -- TODO onClick, send a delete action to the address - [ class "hide-result" ] + [ class "hide-result", onClick address (delete result.id) ] [ text "X" ] ] diff --git a/part11/Stylesheets.elm b/part12/Stylesheets.elm similarity index 100% rename from part11/Stylesheets.elm rename to part12/Stylesheets.elm diff --git a/part12/elm-package.json b/part12/elm-package.json index ba40466..7f72d5a 100644 --- a/part12/elm-package.json +++ b/part12/elm-package.json @@ -12,7 +12,8 @@ "evancz/elm-effects": "2.0.0 <= v < 3.0.0", "evancz/elm-html": "4.0.0 <= v < 5.0.0", "evancz/elm-http": "3.0.0 <= v < 4.0.0", - "evancz/start-app": "2.0.0 <= v < 3.0.0" + "evancz/start-app": "2.0.0 <= v < 3.0.0", + "rtfeldman/elm-css": "1.0.0 <= v < 2.0.0" }, "elm-version": "0.16.0 <= v < 0.17.0" } diff --git a/part12/style.css b/part12/style.css index 64c74d7..9d13aae 100644 --- a/part12/style.css +++ b/part12/style.css @@ -1,92 +1,6 @@ - .content { - width: 960px; - margin: 0 auto; - padding: 30px; - font-family: Helvetica, Arial, serif; -} - -header { - position: relative; - padding: 6px 12px; - height: 36px; - background-color: rgb(96, 181, 204); -} - -h1 { - color: white; - font-weight: normal; - margin: 0; -} - -.tagline { - color: #eee; - position: absolute; - right: 16px; - top: 12px; - font-size: 24px; - font-style: italic; -} - -.results { - list-style-image: url('http://img-cache.cdn.gaiaonline.com/76bd5c99d8f2236e9d3672510e933fdf/http://i278.photobucket.com/albums/kk81/d3m3nt3dpr3p/Tiny-Star-Icon.png'); - list-style-position: inside; - padding: 0; -} - -.results li { - font-size: 18px; - margin-bottom: 16px; -} - -.star-count { - font-weight: bold; - margin-right: 16px; -} - -a { - color: rgb(96, 181, 204); - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -.search-query { - padding: 8px; - font-size: 24px; - margin-bottom: 18px; - margin-top: 36px; -} - -.search-button { - padding: 8px 16px; - font-size: 24px; - color: white; - border: 1px solid #ccc; - background-color: rgb(96, 181, 204); - margin-left: 12px -} - -.search-button:hover { - color: rgb(96, 181, 204); - background-color: white; -} - -.hide-result { - background-color: transparent; - border: 0; - font-weight: bold; - font-size: 18px; - margin-left: 18px; - cursor: pointer; -} - -.hide-result:hover { - color: rgb(96, 181, 204); -} - -button:focus, input:focus { - outline: none; + width: 960px; + margin: 0 auto; + padding: 30px; + font-family: Helvetica, Arial, serif; } diff --git a/part12/Component/ElmHub.elm b/part13/part12/Component/ElmHub.elm similarity index 100% rename from part12/Component/ElmHub.elm rename to part13/part12/Component/ElmHub.elm diff --git a/part12/Component/SearchResult.elm b/part13/part12/Component/SearchResult.elm similarity index 100% rename from part12/Component/SearchResult.elm rename to part13/part12/Component/SearchResult.elm diff --git a/part13/part12/Main.elm b/part13/part12/Main.elm new file mode 100644 index 0000000..5b8e88e --- /dev/null +++ b/part13/part12/Main.elm @@ -0,0 +1,27 @@ +module Main (..) where + +import StartApp +import Component.ElmHub exposing (..) +import Effects exposing (Effects) +import Task exposing (Task) +import Html exposing (Html) + + +main : Signal Html +main = + app.html + + +app : StartApp.App Model +app = + StartApp.start + { view = view + , update = update + , init = ( initialModel, Effects.task (searchFeed initialModel.query) ) + , inputs = [] + } + + +port tasks : Signal (Task Effects.Never ()) +port tasks = + app.tasks diff --git a/part13/part12/README.md b/part13/part12/README.md new file mode 100644 index 0000000..deddbd0 --- /dev/null +++ b/part13/part12/README.md @@ -0,0 +1,17 @@ +Part 12 +======= + +## Installation + +```bash +elm package install +``` + +(Answer `y` at the prompt. In rare cases a known issue can cause the download +to fail; in that case, just run `elm package install` again.) + +## Building + +```bash +elm live Main.elm --open -- --output=elm.js +``` diff --git a/part13/part12/elm-hub.png b/part13/part12/elm-hub.png new file mode 100644 index 0000000..ba32816 Binary files /dev/null and b/part13/part12/elm-hub.png differ diff --git a/part13/part12/elm-package.json b/part13/part12/elm-package.json new file mode 100644 index 0000000..ba40466 --- /dev/null +++ b/part13/part12/elm-package.json @@ -0,0 +1,18 @@ +{ + "version": "1.0.0", + "summary": "Like GitHub, but for Elm stuff.", + "repository": "https://github.com/rtfeldman/elm-workshop.git", + "license": "BSD-3-Clause", + "source-directories": [ + ".", ".." + ], + "exposed-modules": [], + "dependencies": { + "elm-lang/core": "3.0.0 <= v < 4.0.0", + "evancz/elm-effects": "2.0.0 <= v < 3.0.0", + "evancz/elm-html": "4.0.0 <= v < 5.0.0", + "evancz/elm-http": "3.0.0 <= v < 4.0.0", + "evancz/start-app": "2.0.0 <= v < 3.0.0" + }, + "elm-version": "0.16.0 <= v < 0.17.0" +} diff --git a/part13/part12/index.html b/part13/part12/index.html new file mode 100644 index 0000000..5db9b93 --- /dev/null +++ b/part13/part12/index.html @@ -0,0 +1,26 @@ + + + + + + ElmHub + + + + + + + + + + + + + + + diff --git a/part13/part12/style.css b/part13/part12/style.css new file mode 100644 index 0000000..64c74d7 --- /dev/null +++ b/part13/part12/style.css @@ -0,0 +1,92 @@ + +.content { + width: 960px; + margin: 0 auto; + padding: 30px; + font-family: Helvetica, Arial, serif; +} + +header { + position: relative; + padding: 6px 12px; + height: 36px; + background-color: rgb(96, 181, 204); +} + +h1 { + color: white; + font-weight: normal; + margin: 0; +} + +.tagline { + color: #eee; + position: absolute; + right: 16px; + top: 12px; + font-size: 24px; + font-style: italic; +} + +.results { + list-style-image: url('http://img-cache.cdn.gaiaonline.com/76bd5c99d8f2236e9d3672510e933fdf/http://i278.photobucket.com/albums/kk81/d3m3nt3dpr3p/Tiny-Star-Icon.png'); + list-style-position: inside; + padding: 0; +} + +.results li { + font-size: 18px; + margin-bottom: 16px; +} + +.star-count { + font-weight: bold; + margin-right: 16px; +} + +a { + color: rgb(96, 181, 204); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.search-query { + padding: 8px; + font-size: 24px; + margin-bottom: 18px; + margin-top: 36px; +} + +.search-button { + padding: 8px 16px; + font-size: 24px; + color: white; + border: 1px solid #ccc; + background-color: rgb(96, 181, 204); + margin-left: 12px +} + +.search-button:hover { + color: rgb(96, 181, 204); + background-color: white; +} + +.hide-result { + background-color: transparent; + border: 0; + font-weight: bold; + font-size: 18px; + margin-left: 18px; + cursor: pointer; +} + +.hide-result:hover { + color: rgb(96, 181, 204); +} + +button:focus, input:focus { + outline: none; +} diff --git a/part13/part12/test/TestRunner.elm b/part13/part12/test/TestRunner.elm new file mode 100644 index 0000000..0baa6f2 --- /dev/null +++ b/part13/part12/test/TestRunner.elm @@ -0,0 +1,15 @@ +module Main where + +import Signal exposing (Signal) + +import ElmTest exposing (consoleRunner) +import Console exposing (IO, run) +import Task + +import Tests + +console : IO () +console = consoleRunner Tests.all + +port runner : Signal (Task.Task x ()) +port runner = run console diff --git a/part13/part12/test/Tests.elm b/part13/part12/test/Tests.elm new file mode 100644 index 0000000..7c06fe2 --- /dev/null +++ b/part13/part12/test/Tests.elm @@ -0,0 +1,35 @@ +module Tests (..) where + +import ElmTest exposing (..) +import ElmHub exposing (responseDecoder) +import Json.Decode exposing (decodeString) + + +all : Test +all = + suite + "Decoding responses from GitHub" + [ test "they can decode empty responses" + <| let + emptyResponse = + """{ "items": [] }""" + in + assertEqual + (decodeString responseDecoder emptyResponse) + (Ok []) + , test "they can decode responses with results in them" + <| let + response = + """{ "items": [ + { "id": 5, "full_name": "foo", "stargazers_count": 42 }, + { "id": 3, "full_name": "bar", "stargazers_count": 77 } + ] }""" + in + assertEqual + (decodeString responseDecoder response) + (Ok + [ { id = 5, name = "foo", stars = 42 } + , { id = 3, name = "bar", stars = 77 } + ] + ) + ] diff --git a/part13/part12/test/elm-package.json b/part13/part12/test/elm-package.json new file mode 100644 index 0000000..a440485 --- /dev/null +++ b/part13/part12/test/elm-package.json @@ -0,0 +1,21 @@ +{ + "version": "1.0.0", + "summary": "Like GitHub, but for Elm stuff.", + "repository": "https://github.com/rtfeldman/elm-workshop.git", + "license": "BSD-3-Clause", + "source-directories": [ + ".", + ".." + ], + "exposed-modules": [], + "dependencies": { + "deadfoxygrandpa/elm-test": "3.1.1 <= v < 4.0.0", + "elm-lang/core": "3.0.0 <= v < 4.0.0", + "evancz/elm-effects": "2.0.0 <= v < 3.0.0", + "evancz/elm-html": "4.0.0 <= v < 5.0.0", + "evancz/elm-http": "3.0.0 <= v < 4.0.0", + "evancz/start-app": "2.0.0 <= v < 3.0.0", + "laszlopandy/elm-console": "1.0.3 <= v < 2.0.0" + }, + "elm-version": "0.16.0 <= v < 0.17.0" +} diff --git a/part7/ElmHub.elm b/part7/ElmHub.elm index 2151a61..68fb0f5 100644 --- a/part7/ElmHub.elm +++ b/part7/ElmHub.elm @@ -1,35 +1,32 @@ module ElmHub (..) where +import Auth import Html exposing (..) -import Html.Attributes exposing (..) +import Html.Attributes exposing (class, target, href, property) import Html.Events exposing (..) import Http -import Auth import Task exposing (Task) import Effects exposing (Effects) import Json.Decode exposing (Decoder, (:=)) +import Json.Decode.Pipeline exposing (..) import Json.Encode import Signal exposing (Address) -searchFeed : Address String -> String -> Task x Action -searchFeed address query = +searchFeed : String -> Task x Action +searchFeed query = let - -- See https://developer.github.com/v3/search/#example for how to customize! url = "https://api.github.com/search/repositories?access_token=" ++ Auth.token ++ "&q=" ++ query ++ "+language:elm&sort=stars&order=desc" - - -- These only talk to JavaScript ports now. They don't - -- actually do any actions themselves. - task = - Signal.send address query - |> Task.map (\_ -> DoNothing) in - Task.onError task (\_ -> Task.succeed DoNothing) + performAction + (\response -> HandleSearchResponse response) + (\error -> HandleSearchError error) + (Http.get responseDecoder url) responseDecoder : Decoder (List SearchResult) @@ -39,16 +36,27 @@ responseDecoder = searchResultDecoder : Decoder SearchResult searchResultDecoder = - Json.Decode.object3 - SearchResult - ("id" := Json.Decode.int) - ("full_name" := Json.Decode.string) - ("stargazers_count" := Json.Decode.int) + decode SearchResult + |> required "id" Json.Decode.int + |> required "full_name" Json.Decode.string + |> required "stargazers_count" Json.Decode.int + + +{-| Note: this will be a standard function in Elm 0.17 +-} +performAction : (a -> b) -> (y -> b) -> Task y a -> Task x b +performAction successToAction errorToAction task = + let + successTask = + Task.map successToAction task + in + Task.onError successTask (\err -> Task.succeed (errorToAction err)) type alias Model = { query : String , results : List SearchResult + , errorMessage : String } @@ -67,6 +75,7 @@ initialModel : Model initialModel = { query = "tutorial" , results = [] + , errorMessage = "" } @@ -113,26 +122,30 @@ type Action = Search | SetQuery String | DeleteById ResultId - | SetResults (List SearchResult) - | DoNothing + | HandleSearchResponse (List SearchResult) + | HandleSearchError Http.Error -update : Address String -> Action -> Model -> ( Model, Effects Action ) -update searchAddress action model = +update : Action -> Model -> ( Model, Effects Action ) +update action model = case action of Search -> - ( model, Effects.task (searchFeed searchAddress model.query) ) + ( model, Effects.task (searchFeed model.query) ) + + HandleSearchResponse response -> + -- TODO update the model to incorporate these search results. + -- Hint: where would you look to find out the type of `response` here? + ( model, Effects.none ) + + HandleSearchError error -> + -- TODO if decoding failed, store the message in model.errorMessage + -- Hint: look for "decode" in the documentation for this union type: + -- http://package.elm-lang.org/packages/evancz/elm-http/3.0.0/Http#Error + ( model, Effects.none ) SetQuery query -> ( { model | query = query }, Effects.none ) - SetResults results -> - let - newModel = - { model | results = results } - in - ( newModel, Effects.none ) - DeleteById idToHide -> let newResults = @@ -143,6 +156,3 @@ update searchAddress action model = { model | results = newResults } in ( newModel, Effects.none ) - - DoNothing -> - ( model, Effects.none ) diff --git a/part7/Main.elm b/part7/Main.elm index 290b13c..fa4bada 100644 --- a/part7/Main.elm +++ b/part7/Main.elm @@ -5,9 +5,6 @@ import ElmHub exposing (..) import Effects exposing (Effects) import Task exposing (Task) import Html exposing (Html) -import Signal -import Json.Encode -import Json.Decode main : Signal Html @@ -19,40 +16,12 @@ app : StartApp.App Model app = StartApp.start { view = view - , update = update search.address - , init = ( initialModel, Effects.task (searchFeed search.address initialModel.query) ) - , inputs = [ responseActions ] + , update = update + , init = ( initialModel, Effects.task (searchFeed initialModel.query) ) + , inputs = [] } port tasks : Signal (Task Effects.Never ()) port tasks = app.tasks - - -search : Signal.Mailbox String -search = - Signal.mailbox "" - - -port githubSearch : Signal String -port githubSearch = - search.signal - - -responseActions : Signal Action -responseActions = - Signal.map decodeGithubResponse githubResponse - - -decodeGithubResponse : Json.Encode.Value -> Action -decodeGithubResponse value = - case Json.Decode.decodeValue responseDecoder value of - Ok results -> - SetResults results - - Err _ -> - DoNothing - - -port githubResponse : Signal Json.Encode.Value diff --git a/part7/README.md b/part7/README.md index ddf6b06..d95f0ca 100644 --- a/part7/README.md +++ b/part7/README.md @@ -1,4 +1,4 @@ -Part 7 +Part 6 ====== ## Installation @@ -16,10 +16,9 @@ to fail; in that case, just run `elm package install` again.) elm live Main.elm --open -- --output=elm.js ``` -## Running Tests +## References -```bash -cd test -elm package install -elm test TestRunner.elm -``` +* [HTTP Tasks tutorial](http://elm-lang.org/guide/reactivity#http-tasks) +* [HTTP Error documentation](http://package.elm-lang.org/packages/evancz/elm-http/3.0.0/Http#Error) +* [Modules syntax reference](http://elm-lang.org/docs/syntax#modules) +* [Syntax reference for **case-expressions** and **if-expressions**](http://elm-lang.org/docs/syntax#conditionals) diff --git a/part7/elm-package.json b/part7/elm-package.json index ba40466..6968946 100644 --- a/part7/elm-package.json +++ b/part7/elm-package.json @@ -4,10 +4,12 @@ "repository": "https://github.com/rtfeldman/elm-workshop.git", "license": "BSD-3-Clause", "source-directories": [ - ".", ".." + ".", + ".." ], "exposed-modules": [], "dependencies": { + "NoRedInk/elm-decode-pipeline": "1.0.0 <= v < 2.0.0", "elm-lang/core": "3.0.0 <= v < 4.0.0", "evancz/elm-effects": "2.0.0 <= v < 3.0.0", "evancz/elm-html": "4.0.0 <= v < 5.0.0", diff --git a/part7/index.html b/part7/index.html index 9ecef8f..5db9b93 100644 --- a/part7/index.html +++ b/part7/index.html @@ -4,37 +4,23 @@ ElmHub - + + + + -
diff --git a/part7/test/Tests.elm b/part7/test/Tests.elm index 7c06fe2..b017897 100644 --- a/part7/test/Tests.elm +++ b/part7/test/Tests.elm @@ -16,13 +16,12 @@ all = in assertEqual (decodeString responseDecoder emptyResponse) - (Ok []) + ({- TODO: what goes here? -}) , test "they can decode responses with results in them" <| let response = """{ "items": [ - { "id": 5, "full_name": "foo", "stargazers_count": 42 }, - { "id": 3, "full_name": "bar", "stargazers_count": 77 } + /* TODO: dummy JSON goes here */ ] }""" in assertEqual diff --git a/part8/ElmHub.elm b/part8/ElmHub.elm index 958366a..2151a61 100644 --- a/part8/ElmHub.elm +++ b/part8/ElmHub.elm @@ -10,11 +10,10 @@ import Effects exposing (Effects) import Json.Decode exposing (Decoder, (:=)) import Json.Encode import Signal exposing (Address) -import Dict exposing (Dict) -searchFeed : String -> Task x Action -searchFeed query = +searchFeed : Address String -> String -> Task x Action +searchFeed address query = let -- See https://developer.github.com/v3/search/#example for how to customize! url = @@ -24,11 +23,13 @@ searchFeed query = ++ query ++ "+language:elm&sort=stars&order=desc" + -- These only talk to JavaScript ports now. They don't + -- actually do any actions themselves. task = - Http.get responseDecoder url - |> Task.map SetResults + Signal.send address query + |> Task.map (\_ -> DoNothing) in - Task.onError task (\_ -> Task.succeed (SetResults [])) + Task.onError task (\_ -> Task.succeed DoNothing) responseDecoder : Decoder (List SearchResult) @@ -47,7 +48,7 @@ searchResultDecoder = type alias Model = { query : String - , results : Dict ResultId SearchResult + , results : List SearchResult } @@ -65,7 +66,7 @@ type alias ResultId = initialModel : Model initialModel = { query = "tutorial" - , results = Dict.empty + , results = [] } @@ -82,16 +83,10 @@ view address model = , button [ class "search-button", onClick address Search ] [ text "Search" ] , ul [ class "results" ] - (viewSearchResults address model.results) + (List.map (viewSearchResult address) model.results) ] -viewSearchResults : Address Action -> Dict ResultId SearchResult -> List Html -viewSearchResults address results = - -- TODO sort by star count and render - [] - - onInput address wrap = on "input" targetValue (\val -> Signal.message address (wrap val)) @@ -119,26 +114,35 @@ type Action | SetQuery String | DeleteById ResultId | SetResults (List SearchResult) + | DoNothing -update : Action -> Model -> ( Model, Effects Action ) -update action model = +update : Address String -> Action -> Model -> ( Model, Effects Action ) +update searchAddress action model = case action of Search -> - ( model, Effects.task (searchFeed model.query) ) + ( model, Effects.task (searchFeed searchAddress model.query) ) SetQuery query -> ( { model | query = query }, Effects.none ) SetResults results -> let - resultsById : Dict ResultId SearchResult - resultsById = - -- TODO convert results list into a Dict - Dict.empty + newModel = + { model | results = results } in - ( { model | results = resultsById }, Effects.none ) + ( newModel, Effects.none ) - DeleteById id -> - -- TODO delete the result with the given id + DeleteById idToHide -> + let + newResults = + model.results + |> List.filter (\{ id } -> id /= idToHide) + + newModel = + { model | results = newResults } + in + ( newModel, Effects.none ) + + DoNothing -> ( model, Effects.none ) diff --git a/part8/Main.elm b/part8/Main.elm index fa4bada..290b13c 100644 --- a/part8/Main.elm +++ b/part8/Main.elm @@ -5,6 +5,9 @@ import ElmHub exposing (..) import Effects exposing (Effects) import Task exposing (Task) import Html exposing (Html) +import Signal +import Json.Encode +import Json.Decode main : Signal Html @@ -16,12 +19,40 @@ app : StartApp.App Model app = StartApp.start { view = view - , update = update - , init = ( initialModel, Effects.task (searchFeed initialModel.query) ) - , inputs = [] + , update = update search.address + , init = ( initialModel, Effects.task (searchFeed search.address initialModel.query) ) + , inputs = [ responseActions ] } port tasks : Signal (Task Effects.Never ()) port tasks = app.tasks + + +search : Signal.Mailbox String +search = + Signal.mailbox "" + + +port githubSearch : Signal String +port githubSearch = + search.signal + + +responseActions : Signal Action +responseActions = + Signal.map decodeGithubResponse githubResponse + + +decodeGithubResponse : Json.Encode.Value -> Action +decodeGithubResponse value = + case Json.Decode.decodeValue responseDecoder value of + Ok results -> + SetResults results + + Err _ -> + DoNothing + + +port githubResponse : Signal Json.Encode.Value diff --git a/part8/README.md b/part8/README.md index 7f690df..ddf6b06 100644 --- a/part8/README.md +++ b/part8/README.md @@ -1,4 +1,4 @@ -Part 8 +Part 7 ====== ## Installation @@ -15,3 +15,11 @@ to fail; in that case, just run `elm package install` again.) ```bash elm live Main.elm --open -- --output=elm.js ``` + +## Running Tests + +```bash +cd test +elm package install +elm test TestRunner.elm +``` diff --git a/part7/github.js b/part8/github.js similarity index 100% rename from part7/github.js rename to part8/github.js diff --git a/part8/index.html b/part8/index.html index 5db9b93..9ecef8f 100644 --- a/part8/index.html +++ b/part8/index.html @@ -4,23 +4,37 @@ ElmHub + - - - - +
diff --git a/part9/ElmHub.elm b/part9/ElmHub.elm index 6300c50..958366a 100644 --- a/part9/ElmHub.elm +++ b/part9/ElmHub.elm @@ -3,7 +3,6 @@ module ElmHub (..) where import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (..) -import Html.Lazy exposing (..) import Http import Auth import Task exposing (Task) @@ -12,7 +11,6 @@ import Json.Decode exposing (Decoder, (:=)) import Json.Encode import Signal exposing (Address) import Dict exposing (Dict) -import SearchResult searchFeed : String -> Task x Action @@ -33,18 +31,37 @@ searchFeed query = Task.onError task (\_ -> Task.succeed (SetResults [])) -responseDecoder : Decoder (List SearchResult.Model) +responseDecoder : Decoder (List SearchResult) responseDecoder = - -- TODO make use of SearchResult's decoder - Json.Decode.succeed [] + "items" := Json.Decode.list searchResultDecoder + + +searchResultDecoder : Decoder SearchResult +searchResultDecoder = + Json.Decode.object3 + SearchResult + ("id" := Json.Decode.int) + ("full_name" := Json.Decode.string) + ("stargazers_count" := Json.Decode.int) type alias Model = { query : String - , results : Dict SearchResult.ResultId SearchResult.Model + , results : Dict ResultId SearchResult } +type alias SearchResult = + { id : ResultId + , name : String + , stars : Int + } + + +type alias ResultId = + Int + + initialModel : Model initialModel = { query = "tutorial" @@ -69,12 +86,10 @@ view address model = ] -viewSearchResults : Address Action -> Dict SearchResult.ResultId SearchResult.Model -> List Html +viewSearchResults : Address Action -> Dict ResultId SearchResult -> List Html viewSearchResults address results = - results - |> Dict.values - |> List.sortBy (.stars >> negate) - |> List.map (\_ -> div [] [ text "TODO replace this line with view logic from SearchResult" ]) + -- TODO sort by star count and render + [] onInput address wrap = @@ -85,11 +100,25 @@ defaultValue str = property "defaultValue" (Json.Encode.string str) +viewSearchResult : Address Action -> SearchResult -> Html +viewSearchResult address result = + li + [] + [ span [ class "star-count" ] [ text (toString result.stars) ] + , a + [ href ("https://github.com/" ++ result.name), target "_blank" ] + [ text result.name ] + , button + [ class "hide-result", onClick address (DeleteById result.id) ] + [ text "X" ] + ] + + type Action = Search | SetQuery String - | DeleteById SearchResult.ResultId - | SetResults (List SearchResult.Model) + | DeleteById ResultId + | SetResults (List SearchResult) update : Action -> Model -> ( Model, Effects Action ) @@ -103,17 +132,13 @@ update action model = SetResults results -> let - resultsById : Dict SearchResult.ResultId SearchResult.Model + resultsById : Dict ResultId SearchResult resultsById = - results - |> List.map (\result -> ( result.id, result )) - |> Dict.fromList + -- TODO convert results list into a Dict + Dict.empty in ( { model | results = resultsById }, Effects.none ) DeleteById id -> - let - newModel = - { model | results = Dict.remove id model.results } - in - ( newModel, Effects.none ) + -- TODO delete the result with the given id + ( model, Effects.none ) diff --git a/part9/README.md b/part9/README.md index f08ee4f..7f690df 100644 --- a/part9/README.md +++ b/part9/README.md @@ -1,4 +1,4 @@ -Part 9 +Part 8 ====== ## Installation