Update 12 and 13

This commit is contained in:
Richard Feldman
2016-04-03 10:07:53 -07:00
parent c004be3f72
commit bb92151d61
9 changed files with 235 additions and 116 deletions

View File

@@ -1,7 +1,7 @@
module ElmHub (..) where module ElmHub (..) where
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (class, target, href, property)
import Html.Events exposing (..) import Html.Events exposing (..)
import Html.Lazy exposing (..) import Html.Lazy exposing (..)
import Http import Http

View File

@@ -1,4 +1,4 @@
module Component.ElmHub (..) where module ElmHub (..) where
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
@@ -11,7 +11,8 @@ import Effects exposing (Effects)
import Json.Decode exposing (Decoder, (:=)) import Json.Decode exposing (Decoder, (:=))
import Json.Encode import Json.Encode
import Signal exposing (Address) import Signal exposing (Address)
import Component.SearchResult exposing (ResultId) import Dict exposing (Dict)
import SearchResult exposing (ResultId)
searchFeed : String -> Task x Action searchFeed : String -> Task x Action
@@ -32,31 +33,21 @@ searchFeed query =
Task.onError task (\_ -> Task.succeed (SetResults [])) Task.onError task (\_ -> Task.succeed (SetResults []))
responseDecoder : Decoder (List Component.SearchResult.Model) responseDecoder : Decoder (List SearchResult.Model)
responseDecoder = responseDecoder =
"items" := Json.Decode.list searchResultDecoder "items" := Json.Decode.list SearchResult.decoder
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)
type alias Model = type alias Model =
{ query : String { query : String
, results : List Component.SearchResult.Model , results : Dict SearchResult.ResultId SearchResult.Model
} }
initialModel : Model initialModel : Model
initialModel = initialModel =
{ query = "tutorial" { 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 = viewSearchResults address results =
results results
|> filterResults |> Dict.values
|> List.map (lazy2 viewSearchResult address) |> List.sortBy (.stars >> negate)
|> List.map (viewSearchResult address)
filterResults : List Component.SearchResult.Model -> List Component.SearchResult.Model viewSearchResult : Address Action -> SearchResult.Model -> Html
filterResults results = viewSearchResult address result =
case results of SearchResult.view
[] -> (Signal.forwardTo address (UpdateSearchResult result.id))
[] result
first :: rest ->
if first.stars > 0 then
first :: (filterResults rest)
else
filterResults rest
onInput address wrap = onInput address wrap =
@@ -105,18 +91,11 @@ defaultValue str =
property "defaultValue" (Json.Encode.string 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 type Action
= Search = Search
| SetQuery String | SetQuery String
| SetResults (List Component.SearchResult.Model) | SetResults (List SearchResult.Model)
| UpdateSearchResult ResultId Component.SearchResult.Action | UpdateSearchResult ResultId SearchResult.Action
update : Action -> Model -> ( Model, Effects Action ) update : Action -> Model -> ( Model, Effects Action )
@@ -130,31 +109,31 @@ update action model =
SetResults results -> SetResults results ->
let let
newModel = resultsById : Dict SearchResult.ResultId SearchResult.Model
{ model | results = results } resultsById =
results
|> List.map (\result -> ( result.id, result ))
|> Dict.fromList
in in
( newModel, Effects.none ) ( { model | results = resultsById }, Effects.none )
UpdateSearchResult id childAction -> UpdateSearchResult id childAction ->
let let
updateResult childModel = updated =
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 ) =
model.results model.results
|> List.map updateResult |> Dict.get id
|> List.unzip |> Maybe.map (SearchResult.update childAction)
newModel =
{ model | results = newResults }
in 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 )

View File

@@ -1,7 +1,7 @@
module Main (..) where module Main (..) where
import StartApp import StartApp
import Component.ElmHub exposing (..) import ElmHub exposing (..)
import Effects exposing (Effects) import Effects exposing (Effects)
import Task exposing (Task) import Task exposing (Task)
import Html exposing (Html) import Html exposing (Html)

View File

@@ -15,3 +15,7 @@ to fail; in that case, just run `elm package install` again.)
```bash ```bash
elm live Main.elm --open -- --output=elm.js elm live Main.elm --open -- --output=elm.js
``` ```
## References
* [Elm Architecture Tutorial](https://github.com/evancz/elm-architecture-tutorial)

View File

@@ -1,10 +1,12 @@
module Component.SearchResult (..) where module SearchResult (..) where
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (class, target, href, property)
import Html.Events exposing (..) import Html.Events exposing (..)
import Signal exposing (Address) import Signal exposing (Address)
import Effects exposing (Effects) import Effects exposing (Effects)
import Json.Decode exposing (Decoder, (:=))
import Json.Decode.Pipeline exposing (..)
type alias Model = type alias Model =
@@ -24,9 +26,18 @@ type Action
| Collapse | 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 -> ( Model, Effects Action )
update action model = update action model =
-- TODO make expand and collapse work -- TODO implement Expand and Collapse logic
( model, Effects.none ) ( model, Effects.none )

View File

@@ -4,10 +4,12 @@
"repository": "https://github.com/rtfeldman/elm-workshop.git", "repository": "https://github.com/rtfeldman/elm-workshop.git",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"source-directories": [ "source-directories": [
".", ".." ".",
".."
], ],
"exposed-modules": [], "exposed-modules": [],
"dependencies": { "dependencies": {
"NoRedInk/elm-decode-pipeline": "1.0.0 <= v < 2.0.0",
"elm-lang/core": "3.0.0 <= 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-effects": "2.0.0 <= v < 3.0.0",
"evancz/elm-html": "4.0.0 <= v < 5.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" "evancz/start-app": "2.0.0 <= v < 3.0.0"
}, },
"elm-version": "0.16.0 <= v < 0.17.0" "elm-version": "0.16.0 <= v < 0.17.0"
} }

View File

@@ -12,7 +12,7 @@ import Json.Decode exposing (Decoder, (:=))
import Json.Encode import Json.Encode
import Signal exposing (Address) import Signal exposing (Address)
import Dict exposing (Dict) import Dict exposing (Dict)
import SearchResult import SearchResult exposing (ResultId)
searchFeed : String -> Task x Action 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 = viewSearchResults address results =
results results
|> Dict.values |> Dict.values
|> List.sortBy (.stars >> negate) |> List.sortBy (.stars >> negate)
|> filterResults |> List.map (viewSearchResult address)
|> List.map (lazy3 SearchResult.view address DeleteById)
filterResults : List SearchResult.Model -> List SearchResult.Model viewSearchResult : Address Action -> SearchResult.Model -> Html
filterResults results = viewSearchResult address result =
case results of SearchResult.view
[] -> (Signal.forwardTo address (UpdateSearchResult result.id))
[] result
result :: rest ->
if result.stars > 0 then
result :: (filterResults rest)
else
filterResults rest
onInput address wrap = onInput address wrap =
@@ -101,8 +94,8 @@ defaultValue str =
type Action type Action
= Search = Search
| SetQuery String | SetQuery String
| DeleteById SearchResult.ResultId
| SetResults (List SearchResult.Model) | SetResults (List SearchResult.Model)
| UpdateSearchResult ResultId SearchResult.Action
update : Action -> Model -> ( Model, Effects Action ) update : Action -> Model -> ( Model, Effects Action )
@@ -124,9 +117,23 @@ update action model =
in in
( { model | results = resultsById }, Effects.none ) ( { model | results = resultsById }, Effects.none )
DeleteById id -> UpdateSearchResult id childAction ->
let let
newModel = updated =
{ model | results = Dict.remove id model.results } model.results
|> Dict.get id
|> Maybe.map (SearchResult.update childAction)
in 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 )

View File

@@ -1,44 +1,65 @@
module SearchResult (..) where module SearchResult (..) where
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (class, target, href, property)
import Html.Events exposing (..) import Html.Events exposing (..)
import Json.Decode exposing (Decoder, (:=))
import Signal exposing (Address) 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 = type alias ResultId =
Int Int
type alias Model = type Action
{ id : ResultId = Expand
, name : String | Collapse
, stars : Int
}
decoder : Decoder Model decoder : Decoder Model
decoder = decoder =
Json.Decode.object3 decode Model
Model |> required "id" Json.Decode.int
("id" := Json.Decode.int) |> required "full_name" Json.Decode.string
("full_name" := Json.Decode.string) |> required "stargazers_count" Json.Decode.int
("stargazers_count" := Json.Decode.int) |> hardcoded True
view : Address a -> (Int -> a) -> Model -> Html update : Action -> Model -> ( Model, Effects Action )
view address delete result = 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 li
[] []
[ span [ class "star-count" ] [ text (toString result.stars) ] <| if model.expanded then
, a [ span [ class "star-count" ] [ text (toString model.stars) ]
[ href ("https://github.com/" ++ result.name) , a
, target "_blank" [ 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" ]
]

View File

@@ -1,6 +1,101 @@
.content { .content {
width: 960px; width: 960px;
margin: 0 auto; margin: 0 auto;
padding: 30px; padding: 30px;
font-family: Helvetica, Arial, serif; 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;
} }