diff --git a/part10/ElmHub.elm b/part10/ElmHub.elm index 6300c50..1af4bc5 100644 --- a/part10/ElmHub.elm +++ b/part10/ElmHub.elm @@ -1,7 +1,7 @@ module ElmHub (..) where import Html exposing (..) -import Html.Attributes exposing (..) +import Html.Attributes exposing (class, target, href, property) import Html.Events exposing (..) import Html.Lazy exposing (..) import Http diff --git a/part12/Component/ElmHub.elm b/part12/ElmHub.elm similarity index 53% rename from part12/Component/ElmHub.elm rename to part12/ElmHub.elm index 8bf3f90..89e2a17 100644 --- a/part12/Component/ElmHub.elm +++ b/part12/ElmHub.elm @@ -1,4 +1,4 @@ -module Component.ElmHub (..) where +module ElmHub (..) where import Html exposing (..) import Html.Attributes exposing (..) @@ -11,7 +11,8 @@ import Effects exposing (Effects) import Json.Decode exposing (Decoder, (:=)) import Json.Encode import Signal exposing (Address) -import Component.SearchResult exposing (ResultId) +import Dict exposing (Dict) +import SearchResult exposing (ResultId) searchFeed : String -> Task x Action @@ -32,31 +33,21 @@ searchFeed query = Task.onError task (\_ -> Task.succeed (SetResults [])) -responseDecoder : Decoder (List Component.SearchResult.Model) +responseDecoder : Decoder (List SearchResult.Model) responseDecoder = - "items" := Json.Decode.list searchResultDecoder - - -searchResultDecoder : Decoder Component.SearchResult.Model -searchResultDecoder = - Json.Decode.object4 - Component.SearchResult.Model - ("id" := Json.Decode.int) - ("full_name" := Json.Decode.string) - ("stargazers_count" := Json.Decode.int) - (Json.Decode.succeed True) + "items" := Json.Decode.list SearchResult.decoder type alias Model = { query : String - , results : List Component.SearchResult.Model + , results : Dict SearchResult.ResultId SearchResult.Model } initialModel : Model initialModel = { query = "tutorial" - , results = [] + , results = Dict.empty } @@ -77,24 +68,19 @@ view address model = ] -viewSearchResults : Address Action -> List Component.SearchResult.Model -> List Html +viewSearchResults : Address Action -> Dict ResultId SearchResult.Model -> List Html viewSearchResults address results = results - |> filterResults - |> List.map (lazy2 viewSearchResult address) + |> Dict.values + |> List.sortBy (.stars >> negate) + |> List.map (viewSearchResult address) -filterResults : List Component.SearchResult.Model -> List Component.SearchResult.Model -filterResults results = - case results of - [] -> - [] - - first :: rest -> - if first.stars > 0 then - first :: (filterResults rest) - else - filterResults rest +viewSearchResult : Address Action -> SearchResult.Model -> Html +viewSearchResult address result = + SearchResult.view + (Signal.forwardTo address (UpdateSearchResult result.id)) + result onInput address wrap = @@ -105,18 +91,11 @@ defaultValue str = property "defaultValue" (Json.Encode.string str) -viewSearchResult : Address Action -> Component.SearchResult.Model -> Html -viewSearchResult address result = - Component.SearchResult.view - (Signal.forwardTo address (UpdateSearchResult result.id)) - (Debug.log "rendering result..." result) - - type Action = Search | SetQuery String - | SetResults (List Component.SearchResult.Model) - | UpdateSearchResult ResultId Component.SearchResult.Action + | SetResults (List SearchResult.Model) + | UpdateSearchResult ResultId SearchResult.Action update : Action -> Model -> ( Model, Effects Action ) @@ -130,31 +109,31 @@ update action model = SetResults results -> let - newModel = - { model | results = results } + resultsById : Dict SearchResult.ResultId SearchResult.Model + resultsById = + results + |> List.map (\result -> ( result.id, result )) + |> Dict.fromList in - ( newModel, Effects.none ) + ( { model | results = resultsById }, Effects.none ) UpdateSearchResult id childAction -> let - updateResult childModel = - if childModel.id == id then - let - ( newChildModel, childEffects ) = - Component.SearchResult.update childAction childModel - in - ( newChildModel - , Effects.map (UpdateSearchResult id) childEffects - ) - else - ( childModel, Effects.none ) - - ( newResults, effects ) = + updated = model.results - |> List.map updateResult - |> List.unzip - - newModel = - { model | results = newResults } + |> Dict.get id + |> Maybe.map (SearchResult.update childAction) in - ( newModel, Effects.batch effects ) + case updated of + Nothing -> + ( model, Effects.none ) + + Just ( newChildModel, childEffects ) -> + let + effects = + Effects.map (UpdateSearchResult id) childEffects + + newResults = + Dict.insert id newChildModel model.results + in + ( { model | results = newResults }, effects ) 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..f597959 100644 --- a/part12/README.md +++ b/part12/README.md @@ -15,3 +15,7 @@ to fail; in that case, just run `elm package install` again.) ```bash elm live Main.elm --open -- --output=elm.js ``` + +## References + +* [Elm Architecture Tutorial](https://github.com/evancz/elm-architecture-tutorial) diff --git a/part12/Component/SearchResult.elm b/part12/SearchResult.elm similarity index 69% rename from part12/Component/SearchResult.elm rename to part12/SearchResult.elm index 4dedbd3..2d1256d 100644 --- a/part12/Component/SearchResult.elm +++ b/part12/SearchResult.elm @@ -1,10 +1,12 @@ -module Component.SearchResult (..) where +module SearchResult (..) where import Html exposing (..) -import Html.Attributes exposing (..) +import Html.Attributes exposing (class, target, href, property) import Html.Events exposing (..) import Signal exposing (Address) import Effects exposing (Effects) +import Json.Decode exposing (Decoder, (:=)) +import Json.Decode.Pipeline exposing (..) type alias Model = @@ -24,9 +26,18 @@ type Action | Collapse +decoder : Decoder Model +decoder = + decode Model + |> required "id" Json.Decode.int + |> required "full_name" Json.Decode.string + |> required "stargazers_count" Json.Decode.int + |> hardcoded True + + update : Action -> Model -> ( Model, Effects Action ) update action model = - -- TODO make expand and collapse work + -- TODO implement Expand and Collapse logic ( model, Effects.none ) diff --git a/part12/elm-package.json b/part12/elm-package.json index ba40466..f4a6d00 100644 --- a/part12/elm-package.json +++ b/part12/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", @@ -15,4 +17,4 @@ "evancz/start-app": "2.0.0 <= v < 3.0.0" }, "elm-version": "0.16.0 <= v < 0.17.0" -} +} \ No newline at end of file diff --git a/part13/ElmHub.elm b/part13/ElmHub.elm index 8476dbf..89e2a17 100644 --- a/part13/ElmHub.elm +++ b/part13/ElmHub.elm @@ -12,7 +12,7 @@ import Json.Decode exposing (Decoder, (:=)) import Json.Encode import Signal exposing (Address) import Dict exposing (Dict) -import SearchResult +import SearchResult exposing (ResultId) searchFeed : String -> Task x Action @@ -68,26 +68,19 @@ view address model = ] -viewSearchResults : Address Action -> Dict SearchResult.ResultId SearchResult.Model -> List Html +viewSearchResults : Address Action -> Dict ResultId SearchResult.Model -> List Html viewSearchResults address results = results |> Dict.values |> List.sortBy (.stars >> negate) - |> filterResults - |> List.map (lazy3 SearchResult.view address DeleteById) + |> List.map (viewSearchResult address) -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 +viewSearchResult : Address Action -> SearchResult.Model -> Html +viewSearchResult address result = + SearchResult.view + (Signal.forwardTo address (UpdateSearchResult result.id)) + result onInput address wrap = @@ -101,8 +94,8 @@ defaultValue str = type Action = Search | SetQuery String - | DeleteById SearchResult.ResultId | SetResults (List SearchResult.Model) + | UpdateSearchResult ResultId SearchResult.Action update : Action -> Model -> ( Model, Effects Action ) @@ -124,9 +117,23 @@ update action model = in ( { model | results = resultsById }, Effects.none ) - DeleteById id -> + UpdateSearchResult id childAction -> let - newModel = - { model | results = Dict.remove id model.results } + updated = + model.results + |> Dict.get id + |> Maybe.map (SearchResult.update childAction) in - ( newModel, Effects.none ) + case updated of + Nothing -> + ( model, Effects.none ) + + Just ( newChildModel, childEffects ) -> + let + effects = + Effects.map (UpdateSearchResult id) childEffects + + newResults = + Dict.insert id newChildModel model.results + in + ( { model | results = newResults }, effects ) diff --git a/part13/SearchResult.elm b/part13/SearchResult.elm index daf012a..5099bfd 100644 --- a/part13/SearchResult.elm +++ b/part13/SearchResult.elm @@ -1,44 +1,65 @@ module SearchResult (..) where import Html exposing (..) -import Html.Attributes exposing (..) +import Html.Attributes exposing (class, target, href, property) import Html.Events exposing (..) -import Json.Decode exposing (Decoder, (:=)) import Signal exposing (Address) -import Dict exposing (Dict) +import Effects exposing (Effects) +import Json.Decode exposing (Decoder, (:=)) +import Json.Decode.Pipeline exposing (..) + + +type alias Model = + { id : Int + , name : String + , stars : Int + , expanded : Bool + } type alias ResultId = Int -type alias Model = - { id : ResultId - , name : String - , stars : Int - } +type Action + = Expand + | Collapse decoder : Decoder Model decoder = - Json.Decode.object3 - Model - ("id" := Json.Decode.int) - ("full_name" := Json.Decode.string) - ("stargazers_count" := Json.Decode.int) + decode Model + |> required "id" Json.Decode.int + |> required "full_name" Json.Decode.string + |> required "stargazers_count" Json.Decode.int + |> hardcoded True -view : Address a -> (Int -> a) -> Model -> Html -view address delete result = +update : Action -> Model -> ( Model, Effects Action ) +update action model = + case action of + Expand -> + ( { model | expanded = True }, Effects.none ) + + Collapse -> + ( { model | expanded = False }, Effects.none ) + + +view : Address Action -> Model -> Html +view address model = li [] - [ span [ class "star-count" ] [ text (toString result.stars) ] - , a - [ href ("https://github.com/" ++ result.name) - , target "_blank" + <| if model.expanded then + [ span [ class "star-count" ] [ text (toString model.stars) ] + , a + [ href ("https://github.com/" ++ model.name), target "_blank" ] + [ text model.name ] + , button + [ class "hide-result", onClick address Collapse ] + [ text "X" ] + ] + else + [ button + [ class "expand-result", onClick address Expand ] + [ text "Show" ] ] - [ text result.name ] - , button - [ class "hide-result", onClick address (delete result.id) ] - [ text "X" ] - ] diff --git a/part13/style.css b/part13/style.css index 9d13aae..d030733 100644 --- a/part13/style.css +++ b/part13/style.css @@ -1,6 +1,101 @@ + .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; +} + +.error { + background-color: #FF9632; + padding: 20px; + box-sizing: border-box; + overflow-x: auto; + font-family: monospace; + font-size: 18px; }