Shift all the parts up a number.

This commit is contained in:
Richard Feldman
2016-04-03 06:37:08 -07:00
parent 49926901e5
commit 39846484fd
40 changed files with 718 additions and 301 deletions

View File

@@ -3,6 +3,7 @@ module ElmHub (..) where
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (..) import Html.Events exposing (..)
import Html.Lazy exposing (..)
import Http import Http
import Auth import Auth
import Task exposing (Task) import Task exposing (Task)
@@ -34,7 +35,8 @@ searchFeed query =
responseDecoder : Decoder (List SearchResult.Model) responseDecoder : Decoder (List SearchResult.Model)
responseDecoder = responseDecoder =
"items" := Json.Decode.list SearchResult.decoder -- TODO make use of SearchResult's decoder
Json.Decode.succeed []
type alias Model = type alias Model =
@@ -72,15 +74,7 @@ viewSearchResults address results =
results results
|> Dict.values |> Dict.values
|> List.sortBy (.stars >> negate) |> List.sortBy (.stars >> negate)
|> filterResults |> List.map (\_ -> div [] [ text "TODO replace this line with view logic from SearchResult" ])
|> 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
onInput address wrap = onInput address wrap =

View File

@@ -1,5 +1,5 @@
Part 10 Part 9
======= ======
## Installation ## Installation

View File

@@ -28,23 +28,18 @@ decoder =
("stargazers_count" := Json.Decode.int) ("stargazers_count" := Json.Decode.int)
view : Address a -> (Int -> a) -> Model -> Html view : Address a -> Model -> Html
view address delete result = view address result =
li li
[] []
[ span [ class "star-count" ] [ text (toString result.stars) ] [ span [ class "star-count" ] [ text (toString result.stars) ]
, a , a
[ href [ href ("https://github.com/" ++ result.name)
("https://github.com/"
++ (Debug.log
"TODO we should not see this when typing in the search box!"
result.name
)
)
, target "_blank" , target "_blank"
] ]
[ text result.name ] [ text result.name ]
, button , button
[ class "hide-result", onClick address (delete result.id) ] -- TODO onClick, send a delete action to the address
[ class "hide-result" ]
[ text "X" ] [ text "X" ]
] ]

View File

@@ -3,7 +3,6 @@ module ElmHub (..) where
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (..) import Html.Events exposing (..)
import Html.Lazy exposing (..)
import Http import Http
import Auth import Auth
import Task exposing (Task) import Task exposing (Task)
@@ -74,20 +73,14 @@ viewSearchResults address results =
|> Dict.values |> Dict.values
|> List.sortBy (.stars >> negate) |> List.sortBy (.stars >> negate)
|> filterResults |> filterResults
|> List.map (lazy3 SearchResult.view address DeleteById) |> List.map (SearchResult.view address DeleteById)
filterResults : List SearchResult.Model -> List SearchResult.Model filterResults : List SearchResult.Model -> List SearchResult.Model
filterResults results = filterResults results =
case results of -- TODO filter out repos with 0 stars
[] -> -- using a case-expression rather than List.filter
[] results
result :: rest ->
if result.stars > 0 then
result :: (filterResults rest)
else
filterResults rest
onInput address wrap = onInput address wrap =

View File

@@ -1,4 +1,4 @@
Part 11 Part 10
======= =======
## Installation ## Installation
@@ -15,9 +15,3 @@ 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
``` ```
## Compiling CSS
```bash
elm css Stylesheets.elm
```

View File

@@ -34,7 +34,13 @@ view address delete result =
[] []
[ span [ class "star-count" ] [ text (toString result.stars) ] [ span [ class "star-count" ] [ text (toString result.stars) ]
, a , 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" , target "_blank"
] ]
[ text result.name ] [ text result.name ]

View File

@@ -12,8 +12,7 @@
"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",
"evancz/elm-http": "3.0.0 <= v < 4.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" "elm-version": "0.16.0 <= v < 0.17.0"
} }

View File

@@ -1,6 +1,92 @@
.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;
}

132
part12/ElmHub.elm Normal file
View File

@@ -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 )

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

@@ -1,4 +1,4 @@
Part 12 Part 11
======= =======
## Installation ## Installation
@@ -15,3 +15,9 @@ 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
``` ```
## Compiling CSS
```bash
elm css Stylesheets.elm
```

View File

@@ -28,8 +28,8 @@ decoder =
("stargazers_count" := Json.Decode.int) ("stargazers_count" := Json.Decode.int)
view : Address a -> Model -> Html view : Address a -> (Int -> a) -> Model -> Html
view address result = view address delete result =
li li
[] []
[ span [ class "star-count" ] [ text (toString result.stars) ] [ span [ class "star-count" ] [ text (toString result.stars) ]
@@ -39,7 +39,6 @@ view address result =
] ]
[ text result.name ] [ text result.name ]
, button , button
-- TODO onClick, send a delete action to the address [ class "hide-result", onClick address (delete result.id) ]
[ class "hide-result" ]
[ text "X" ] [ text "X" ]
] ]

View File

@@ -12,7 +12,8 @@
"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",
"evancz/elm-http": "3.0.0 <= v < 4.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" "elm-version": "0.16.0 <= v < 0.17.0"
} }

View File

@@ -1,92 +1,6 @@
.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;
}

27
part13/part12/Main.elm Normal file
View File

@@ -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

17
part13/part12/README.md Normal file
View File

@@ -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
```

BIN
part13/part12/elm-hub.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -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"
}

26
part13/part12/index.html Normal file
View File

@@ -0,0 +1,26 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>ElmHub</title>
<script type="text/javascript" src="elm.js"></script>
<!-- Uncomment the below line to enable elm-reactor support. -->
<!-- <script type="text/javascript" src="/_reactor/debug.js"></script> -->
<link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="elm-hub.png">
</head>
<body>
</body>
<script type="text/javascript">
var app = Elm.fullscreen(Elm.Main, {});
// Uncomment this line and comment out the above to enable elm-reactor support.
// var app = Elm.fullscreenDebug("ElmHub", "Main.elm");
</script>
</html>

92
part13/part12/style.css Normal file
View File

@@ -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;
}

View File

@@ -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

View File

@@ -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 }
]
)
]

View File

@@ -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"
}

View File

@@ -1,35 +1,32 @@
module ElmHub (..) where module ElmHub (..) where
import Auth
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 Http import Http
import Auth
import Task exposing (Task) import Task exposing (Task)
import Effects exposing (Effects) import Effects exposing (Effects)
import Json.Decode exposing (Decoder, (:=)) import Json.Decode exposing (Decoder, (:=))
import Json.Decode.Pipeline exposing (..)
import Json.Encode import Json.Encode
import Signal exposing (Address) import Signal exposing (Address)
searchFeed : Address String -> String -> Task x Action searchFeed : String -> Task x Action
searchFeed address query = searchFeed query =
let let
-- See https://developer.github.com/v3/search/#example for how to customize!
url = url =
"https://api.github.com/search/repositories?access_token=" "https://api.github.com/search/repositories?access_token="
++ Auth.token ++ Auth.token
++ "&q=" ++ "&q="
++ query ++ query
++ "+language:elm&sort=stars&order=desc" ++ "+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 in
Task.onError task (\_ -> Task.succeed DoNothing) performAction
(\response -> HandleSearchResponse response)
(\error -> HandleSearchError error)
(Http.get responseDecoder url)
responseDecoder : Decoder (List SearchResult) responseDecoder : Decoder (List SearchResult)
@@ -39,16 +36,27 @@ responseDecoder =
searchResultDecoder : Decoder SearchResult searchResultDecoder : Decoder SearchResult
searchResultDecoder = searchResultDecoder =
Json.Decode.object3 decode SearchResult
SearchResult |> 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)
{-| 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 = type alias Model =
{ query : String { query : String
, results : List SearchResult , results : List SearchResult
, errorMessage : String
} }
@@ -67,6 +75,7 @@ initialModel : Model
initialModel = initialModel =
{ query = "tutorial" { query = "tutorial"
, results = [] , results = []
, errorMessage = ""
} }
@@ -113,26 +122,30 @@ type Action
= Search = Search
| SetQuery String | SetQuery String
| DeleteById ResultId | DeleteById ResultId
| SetResults (List SearchResult) | HandleSearchResponse (List SearchResult)
| DoNothing | HandleSearchError Http.Error
update : Address String -> Action -> Model -> ( Model, Effects Action ) update : Action -> Model -> ( Model, Effects Action )
update searchAddress action model = update action model =
case action of case action of
Search -> 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 -> SetQuery query ->
( { model | query = query }, Effects.none ) ( { model | query = query }, Effects.none )
SetResults results ->
let
newModel =
{ model | results = results }
in
( newModel, Effects.none )
DeleteById idToHide -> DeleteById idToHide ->
let let
newResults = newResults =
@@ -143,6 +156,3 @@ update searchAddress action model =
{ model | results = newResults } { model | results = newResults }
in in
( newModel, Effects.none ) ( newModel, Effects.none )
DoNothing ->
( model, Effects.none )

View File

@@ -5,9 +5,6 @@ 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)
import Signal
import Json.Encode
import Json.Decode
main : Signal Html main : Signal Html
@@ -19,40 +16,12 @@ app : StartApp.App Model
app = app =
StartApp.start StartApp.start
{ view = view { view = view
, update = update search.address , update = update
, init = ( initialModel, Effects.task (searchFeed search.address initialModel.query) ) , init = ( initialModel, Effects.task (searchFeed initialModel.query) )
, inputs = [ responseActions ] , inputs = []
} }
port tasks : Signal (Task Effects.Never ()) port tasks : Signal (Task Effects.Never ())
port tasks = port tasks =
app.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

View File

@@ -1,4 +1,4 @@
Part 7 Part 6
====== ======
## Installation ## Installation
@@ -16,10 +16,9 @@ to fail; in that case, just run `elm package install` again.)
elm live Main.elm --open -- --output=elm.js elm live Main.elm --open -- --output=elm.js
``` ```
## Running Tests ## References
```bash * [HTTP Tasks tutorial](http://elm-lang.org/guide/reactivity#http-tasks)
cd test * [HTTP Error documentation](http://package.elm-lang.org/packages/evancz/elm-http/3.0.0/Http#Error)
elm package install * [Modules syntax reference](http://elm-lang.org/docs/syntax#modules)
elm test TestRunner.elm * [Syntax reference for **case-expressions** and **if-expressions**](http://elm-lang.org/docs/syntax#conditionals)
```

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",

View File

@@ -4,37 +4,23 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>ElmHub</title> <title>ElmHub</title>
<script type="text/javascript" src="github.js"></script>
<script type="text/javascript" src="elm.js"></script> <script type="text/javascript" src="elm.js"></script>
<!-- Uncomment the below line to enable elm-reactor support. -->
<!-- <script type="text/javascript" src="/_reactor/debug.js"></script> -->
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="elm-hub.png"> <link rel="icon" type="image/png" href="elm-hub.png">
</head> </head>
<body> <body>
<div id="elm-landing-pad"></div>
</body> </body>
<script type="text/javascript"> <script type="text/javascript">
// documentation: https://github.com/michael/github var app = Elm.fullscreen(Elm.Main, {});
var github = new Github();
var app = Elm.embed( // Uncomment this line and comment out the above to enable elm-reactor support.
Elm.Main, // var app = Elm.fullscreenDebug("ElmHub", "Main.elm");
document.getElementById("elm-landing-pad"),
{githubResponse: []});
function searchGithub(query) {
console.log("Searching for", query);
var search = github.getSearch(query);
search.repositories({}, function (err, repositories) {
console.log("Got response", repositories);
// TODO: app.ports.portNameGoesHere.send(repositories);
});
}
// TODO app.ports.portNameGoesHere.subscribe(searchGithub);
</script> </script>
</html> </html>

View File

@@ -16,13 +16,12 @@ all =
in in
assertEqual assertEqual
(decodeString responseDecoder emptyResponse) (decodeString responseDecoder emptyResponse)
(Ok []) ({- TODO: what goes here? -})
, test "they can decode responses with results in them" , test "they can decode responses with results in them"
<| let <| let
response = response =
"""{ "items": [ """{ "items": [
{ "id": 5, "full_name": "foo", "stargazers_count": 42 }, /* TODO: dummy JSON goes here */
{ "id": 3, "full_name": "bar", "stargazers_count": 77 }
] }""" ] }"""
in in
assertEqual assertEqual

View File

@@ -10,11 +10,10 @@ 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 Dict exposing (Dict)
searchFeed : String -> Task x Action searchFeed : Address String -> String -> Task x Action
searchFeed query = searchFeed address query =
let let
-- See https://developer.github.com/v3/search/#example for how to customize! -- See https://developer.github.com/v3/search/#example for how to customize!
url = url =
@@ -24,11 +23,13 @@ searchFeed query =
++ query ++ query
++ "+language:elm&sort=stars&order=desc" ++ "+language:elm&sort=stars&order=desc"
-- These only talk to JavaScript ports now. They don't
-- actually do any actions themselves.
task = task =
Http.get responseDecoder url Signal.send address query
|> Task.map SetResults |> Task.map (\_ -> DoNothing)
in in
Task.onError task (\_ -> Task.succeed (SetResults [])) Task.onError task (\_ -> Task.succeed DoNothing)
responseDecoder : Decoder (List SearchResult) responseDecoder : Decoder (List SearchResult)
@@ -47,7 +48,7 @@ searchResultDecoder =
type alias Model = type alias Model =
{ query : String { query : String
, results : Dict ResultId SearchResult , results : List SearchResult
} }
@@ -65,7 +66,7 @@ type alias ResultId =
initialModel : Model initialModel : Model
initialModel = initialModel =
{ query = "tutorial" { query = "tutorial"
, results = Dict.empty , results = []
} }
@@ -82,16 +83,10 @@ view address model =
, button [ class "search-button", onClick address Search ] [ text "Search" ] , button [ class "search-button", onClick address Search ] [ text "Search" ]
, ul , ul
[ class "results" ] [ 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 = onInput address wrap =
on "input" targetValue (\val -> Signal.message address (wrap val)) on "input" targetValue (\val -> Signal.message address (wrap val))
@@ -119,26 +114,35 @@ type Action
| SetQuery String | SetQuery String
| DeleteById ResultId | DeleteById ResultId
| SetResults (List SearchResult) | SetResults (List SearchResult)
| DoNothing
update : Action -> Model -> ( Model, Effects Action ) update : Address String -> Action -> Model -> ( Model, Effects Action )
update action model = update searchAddress action model =
case action of case action of
Search -> Search ->
( model, Effects.task (searchFeed model.query) ) ( model, Effects.task (searchFeed searchAddress model.query) )
SetQuery query -> SetQuery query ->
( { model | query = query }, Effects.none ) ( { model | query = query }, Effects.none )
SetResults results -> SetResults results ->
let let
resultsById : Dict ResultId SearchResult newModel =
resultsById = { model | results = results }
-- TODO convert results list into a Dict
Dict.empty
in in
( { model | results = resultsById }, Effects.none ) ( newModel, Effects.none )
DeleteById id -> DeleteById idToHide ->
-- TODO delete the result with the given id let
newResults =
model.results
|> List.filter (\{ id } -> id /= idToHide)
newModel =
{ model | results = newResults }
in
( newModel, Effects.none )
DoNothing ->
( model, Effects.none ) ( model, Effects.none )

View File

@@ -5,6 +5,9 @@ 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)
import Signal
import Json.Encode
import Json.Decode
main : Signal Html main : Signal Html
@@ -16,12 +19,40 @@ app : StartApp.App Model
app = app =
StartApp.start StartApp.start
{ view = view { view = view
, update = update , update = update search.address
, init = ( initialModel, Effects.task (searchFeed initialModel.query) ) , init = ( initialModel, Effects.task (searchFeed search.address initialModel.query) )
, inputs = [] , inputs = [ responseActions ]
} }
port tasks : Signal (Task Effects.Never ()) port tasks : Signal (Task Effects.Never ())
port tasks = port tasks =
app.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

View File

@@ -1,4 +1,4 @@
Part 8 Part 7
====== ======
## Installation ## Installation
@@ -15,3 +15,11 @@ 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
``` ```
## Running Tests
```bash
cd test
elm package install
elm test TestRunner.elm
```

View File

@@ -4,23 +4,37 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>ElmHub</title> <title>ElmHub</title>
<script type="text/javascript" src="github.js"></script>
<script type="text/javascript" src="elm.js"></script> <script type="text/javascript" src="elm.js"></script>
<!-- Uncomment the below line to enable elm-reactor support. -->
<!-- <script type="text/javascript" src="/_reactor/debug.js"></script> -->
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<link rel="icon" type="image/png" href="elm-hub.png"> <link rel="icon" type="image/png" href="elm-hub.png">
</head> </head>
<body> <body>
<div id="elm-landing-pad"></div>
</body> </body>
<script type="text/javascript"> <script type="text/javascript">
var app = Elm.fullscreen(Elm.Main, {}); // documentation: https://github.com/michael/github
var github = new Github();
// Uncomment this line and comment out the above to enable elm-reactor support. var app = Elm.embed(
// var app = Elm.fullscreenDebug("ElmHub", "Main.elm"); Elm.Main,
document.getElementById("elm-landing-pad"),
{githubResponse: []});
function searchGithub(query) {
console.log("Searching for", query);
var search = github.getSearch(query);
search.repositories({}, function (err, repositories) {
console.log("Got response", repositories);
// TODO: app.ports.portNameGoesHere.send(repositories);
});
}
// TODO app.ports.portNameGoesHere.subscribe(searchGithub);
</script> </script>
</html> </html>

View File

@@ -3,7 +3,6 @@ module ElmHub (..) where
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (..) import Html.Events exposing (..)
import Html.Lazy exposing (..)
import Http import Http
import Auth import Auth
import Task exposing (Task) import Task exposing (Task)
@@ -12,7 +11,6 @@ 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
searchFeed : String -> Task x Action searchFeed : String -> Task x Action
@@ -33,18 +31,37 @@ searchFeed query =
Task.onError task (\_ -> Task.succeed (SetResults [])) Task.onError task (\_ -> Task.succeed (SetResults []))
responseDecoder : Decoder (List SearchResult.Model) responseDecoder : Decoder (List SearchResult)
responseDecoder = responseDecoder =
-- TODO make use of SearchResult's decoder "items" := Json.Decode.list searchResultDecoder
Json.Decode.succeed []
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 = type alias Model =
{ query : String { 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 : Model
initialModel = initialModel =
{ query = "tutorial" { 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 = viewSearchResults address results =
results -- TODO sort by star count and render
|> Dict.values []
|> List.sortBy (.stars >> negate)
|> List.map (\_ -> div [] [ text "TODO replace this line with view logic from SearchResult" ])
onInput address wrap = onInput address wrap =
@@ -85,11 +100,25 @@ defaultValue str =
property "defaultValue" (Json.Encode.string 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 type Action
= Search = Search
| SetQuery String | SetQuery String
| DeleteById SearchResult.ResultId | DeleteById ResultId
| SetResults (List SearchResult.Model) | SetResults (List SearchResult)
update : Action -> Model -> ( Model, Effects Action ) update : Action -> Model -> ( Model, Effects Action )
@@ -103,17 +132,13 @@ update action model =
SetResults results -> SetResults results ->
let let
resultsById : Dict SearchResult.ResultId SearchResult.Model resultsById : Dict ResultId SearchResult
resultsById = resultsById =
results -- TODO convert results list into a Dict
|> List.map (\result -> ( result.id, result )) Dict.empty
|> Dict.fromList
in in
( { model | results = resultsById }, Effects.none ) ( { model | results = resultsById }, Effects.none )
DeleteById id -> DeleteById id ->
let -- TODO delete the result with the given id
newModel = ( model, Effects.none )
{ model | results = Dict.remove id model.results }
in
( newModel, Effects.none )

View File

@@ -1,4 +1,4 @@
Part 9 Part 8
====== ======
## Installation ## Installation