@rtfeldman - frontend masters · reverse : list val -> list val reverse : list thing -> list...
TRANSCRIPT
elm@rtfeldman
8. JavaScript Interop
function guarantees
same arguments?same return value
no side effects
Math.random()
localStorage.foo = "bar";
access huge JS ecosystem
while maintaining guarantees
access huge JS ecosystem
"client/server" communication
"client/server" communication
Elm sends data to JS
JS sends data to Elm
"client/server" communication
Elm sends data to JS
JS sends data to Elmno direct function calls
Cmd on the Elm side
callback on JS side
Cmd Msg
Cmd Msg
Cmd msg
type variables
List.reverse [ "foo", "bar", "baz" ] == [ "baz", "bar", "foo" ]
List.reverse [ 1.1, 2.2, 3.3 ] == [ 3.3, 2.2, 1.1 ]
List.reverse [ True, False, False ] == [ False, False, True ]
reverse :
reverse : List ??? -> List ???
reverse : List val -> List val
reverse : List val -> List val
type variable
reverse : List val -> List val
reverse : List thing -> List thing
reverse : List a -> List a
elmHubHeader : Html MsgelmHubHeader = header [] [ h1 [] [ text "ElmHub" ] , span [ class "tagline" ] [ text "Like GitHub..." ] ]
elmHubHeader : Html MsgelmHubHeader = header [] [ h1 [] [ text "ElmHub" ] , span [ class "tagline" ] [ text "Like GitHub..." ] ]
"this is compatible with Html that produces Msg"
elmHubHeader : Html msgelmHubHeader = header [] [ h1 [] [ text "ElmHub" ] , span [ class "tagline" ] [ text "Like GitHub..." ] ]
elmHubHeader : Html aelmHubHeader = header [] [ h1 [] [ text "ElmHub" ] , span [ class "tagline" ] [ text "Like GitHub..." ] ]
Cmd Msg
produces messages of type Msg
Cmd Msg
works with update functions that accept Msg
produces messages of type Msg
Cmd Msg
Cmd msgworks with any update function
Cmd Msg
Cmd msgworks with any update function
...because it never produces any messages!
the port keyword
&
index.html
subscriptions
viewupdate
Msg Html Msg
Elm Runtime
Cmd Msg
Model
viewupdate
Msg Html Msg
Elm Runtime
Cmd Msg
Modelsubscriptions
viewupdate
Msg Html Msg
Elm Runtime
Cmd Msg
Modelsubscriptions Msg
viewupdate
Msg Html Msg
Elm Runtime
Cmd Msg
Modelsubscriptions Msg
Model
github.js
Exercise: resolve the TODOs in part8
decodeString responseDecoder json
decodeValue responseDecoder json
9. Testing
{ "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": { "NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0", "elm-lang/core": "4.0.1 <= v < 5.0.0", "elm-lang/html": "1.0.0 <= v < 2.0.0", "evancz/elm-http": "3.0.1 <= v < 4.0.0" }, "elm-version": "0.17.0 <= v < 0.18.0"}
elm-package.json
{ "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": { "NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0", "elm-lang/core": "4.0.1 <= v < 5.0.0", "elm-lang/html": "1.0.0 <= v < 2.0.0", "evancz/elm-http": "3.0.1 <= v < 4.0.0" }, "elm-version": "0.17.0 <= v < 0.18.0"}
elm-package.json
{ "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": { "NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0", "elm-lang/core": "4.0.1 <= v < 5.0.0", "elm-lang/html": "1.0.0 <= v < 2.0.0", "evancz/elm-http": "3.0.1 <= v < 4.0.0" }, "elm-version": "0.17.0 <= v < 0.18.0"}
elm-package.json
elmHub/ elm-package.json Main.elm ElmHub.elm
{ "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": { "NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0", "elm-lang/core": "4.0.1 <= v < 5.0.0", "elm-lang/html": "1.0.0 <= v < 2.0.0", "evancz/elm-http": "3.0.1 <= v < 4.0.0" }, "elm-version": "0.17.0 <= v < 0.18.0"}
elm-package.json
{ "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": { "NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0", "elm-lang/core": "4.0.1 <= v < 5.0.0", "elm-lang/html": "1.0.0 <= v < 2.0.0", "evancz/elm-http": "3.0.1 <= v < 4.0.0" }, "elm-version": "0.17.0 <= v < 0.18.0"}
elm-package.json
{ "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": { "NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0", "elm-lang/core": "4.0.1 <= v < 5.0.0", "elm-lang/html": "1.0.0 <= v < 2.0.0", "evancz/elm-http": "3.0.1 <= v < 4.0.0" }, "elm-version": "0.17.0 <= v < 0.18.0"}
elm-package.json
"NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0"
"NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0"
"NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0"
"NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0"
"NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0"
1.1.2major minor patch
"NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0"
1.1.2major minor patch
semantic versioning automatically enforced
elmHub/ elm-package.json Main.elm ElmHub.elm
elmHub/ elm-package.json Main.elm ElmHub.elm
tests/ elm-package.json Main.elm Tests.elm
"source-directories": [ ".", ".."],"dependencies": { "NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0", "elm-community/elm-test": "2.0.1 <= v < 3.0.0", "elm-lang/core": "4.0.1 <= v < 5.0.0", "elm-lang/html": "1.0.0 <= v < 2.0.0", "evancz/elm-http": "3.0.1 <= v < 4.0.0", "rtfeldman/html-test-runner": "1.0.0 <= v < 2.0.0", "rtfeldman/node-test-runner": "2.0.0 <= v < 3.0.0"}
tests/elm-package.json
tests
"source-directories": [ ".", ".."],"dependencies": { "NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0", "elm-community/elm-test": "2.0.1 <= v < 3.0.0", "elm-lang/core": "4.0.1 <= v < 5.0.0", "elm-lang/html": "1.0.0 <= v < 2.0.0", "evancz/elm-http": "3.0.1 <= v < 4.0.0", "rtfeldman/html-test-runner": "1.0.0 <= v < 2.0.0", "rtfeldman/node-test-runner": "2.0.0 <= v < 3.0.0"}
tests/elm-package.json
testsmain source
"source-directories": [ ".", ".."],"dependencies": { "NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0", "elm-community/elm-test": "2.0.1 <= v < 3.0.0", "elm-lang/core": "4.0.1 <= v < 5.0.0", "elm-lang/html": "1.0.0 <= v < 2.0.0", "evancz/elm-http": "3.0.1 <= v < 4.0.0", "rtfeldman/html-test-runner": "1.0.0 <= v < 2.0.0", "rtfeldman/node-test-runner": "2.0.0 <= v < 3.0.0"}
tests/elm-package.json
"source-directories": [ ".", ".."],"dependencies": { "NoRedInk/elm-decode-pipeline": "1.1.2 <= v < 2.0.0", "elm-community/elm-test": "2.0.1 <= v < 3.0.0", "elm-lang/core": "4.0.1 <= v < 5.0.0", "elm-lang/html": "1.0.0 <= v < 2.0.0", "evancz/elm-http": "3.0.1 <= v < 4.0.0", "rtfeldman/html-test-runner": "1.0.0 <= v < 2.0.0", "rtfeldman/node-test-runner": "2.0.0 <= v < 3.0.0"}
tests/elm-package.json
package.elm-lang.org
unit tests
"[ 1, 2, 3 ]" |> decodeString (list int) |> Expect.equal (Ok [ 1, 2, 3])
expectation : Expectation expectation = "[ 1, 2, 3 ]" |> decodeString (list int) |> Expect.equal (Ok [ 1, 2, 3])
\throwawayArgument -> "[ 1, 2, 3 ]" |> decodeString (list int) |> Expect.equal (Ok [ 1, 2, 3])
() empty tuple, aka "Unit"
\throwawayArgument -> "[ 1, 2, 3 ]" |> decodeString (list int) |> Expect.equal (Ok [ 1, 2, 3])
\() -> "[ 1, 2, 3 ]" |> decodeString (list int) |> Expect.equal (Ok [ 1, 2, 3])
test "it successfully decodes" ( \() -> "[ 1, 2, 3 ]" |> decodeString (list int) |> Expect.equal (Ok [ 1, 2, 3]) )
[ 2, 4, 6, 8 ] |> List.filter (\num -> num < 5) |> List.reverse
[ 2, 4, 6, 8 ] |> List.filter (\num -> num < 5) |> List.reverse
List.reverse (List.filter (\num -> num < 5) [ 2, 4, 6, 8 ])
[ 2, 4, 6, 8 ] |> List.filter (\num -> num < 5) |> List.reverse
List.reverse (List.filter (\num -> num < 5) [ 2, 4, 6, 8 ])
List.reverse <| List.filter (\num -> num < 5) [ 2, 4, 6, 8 ]
test "it successfully decodes" ( \() -> "[ 1, 2, 3 ]" |> decodeString (list int) |> Expect.equal (Ok [ 1, 2, 3]) )
test "it successfully decodes" <| \() -> "[ 1, 2, 3 ]" |> decodeString (list int) |> Expect.equal (Ok [ 1, 2, 3])
describe "decoder tests" [ test "it successfully decodes" <| \() -> "[ 1, 2, 3 ]" |> decodeString (list int) |> Expect.equal (Ok [ 1, 2, 3]) ]
describe "all my tests" [ describe "decoder tests" [ test "it successfully decodes" <| \() -> "[ 1, 2, 3 ]" |> decodeString (list int) |> Expect.equal (Ok [ 1, 2, 3]) ] ]
test "Reversing twice does nothing" <| \() -> [ 1, 2, 3 ] |> List.reverse |> List.reverse |> Expect.equal [ 1, 2, 3 ]
fuzz int "Reversing twice does nothing" <| \randomInt -> [ 1, 2, randomInt ] |> List.reverse |> List.reverse |> Expect.equal [ 1, 2, randomInt ]
fuzz (list int) "Reversing twice does nothing" <| \randomList -> randomList |> List.reverse |> List.reverse |> Expect.equal randomList
fuzz2 int float "Integers are bigger than floats" <| \randomInt randomFloat-> randomInt |> Expect.greaterThan randomFloat
fuzz2 int float "Integers are bigger than floats" <| \randomInt randomFloat-> randomInt |> Expect.greaterThan randomFloat
Exercise: resolve the TODOs in part9
test "it decodes successfully" <| \() -> "[ 1, 2, 3 ]" |> decodeString (list int) |> Expect.equal (Ok [ 1, 2, 3])
10. Delegation
Adding Search Options
Adding Search OptionssearchIn
userFilterminStars
type Msg = Search | SetQuery String | DeleteById Int | SetMinStars Int | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , minStars : Int , searchIn : String , userFilter : String }
type Msg = Search | SetQuery String | DeleteById Int | SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
type Msg = Search | SetQuery String | DeleteById Int | SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
type Msg = Search | SetQuery String | DeleteById Int | SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
type Msg = Search | SetQuery String | DeleteById Int | SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String }
type alias SearchOptions = { minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
type Msg = Search | SetQuery String | DeleteById Int | SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , options : SearchOptions }
type alias SearchOptions = { minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
type Msg = Search | SetQuery String | DeleteById Int | SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , options : SearchOptions }
type alias SearchOptions = { minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
input [] [ value model.sort, onInput SetSort ]
type Msg = Search | SetQuery String | DeleteById Int | SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , options : SearchOptions }
type alias SearchOptions = { minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
input [] [ value model.options.sort, onInput SetSort ]
type Msg = Search | SetQuery String | DeleteById Int | SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , options : SearchOptions }
type alias SearchOptions = { minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
type Msg = Search | SetQuery String | DeleteById Int
type OptionsMsg = SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , options : SearchOptions }
type alias SearchOptions = { minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
type Msg = Search | SetQuery String | DeleteById Int | Options OptionsMsg
type OptionsMsg = SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , options : SearchOptions }
type alias SearchOptions = { minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
type Msg = Search | SetQuery String | DeleteById Int | Options OptionsMsg
type OptionsMsg = SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , options : SearchOptions }
type alias SearchOptions = { minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
input [] [ value model.options.sort, onInput SetSort ]
type Msg = Search | SetQuery String | DeleteById Int | Options OptionsMsg
type OptionsMsg = SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , options : SearchOptions }
type alias SearchOptions = { minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
input [] [ value model.options.sort, onInput (Options SetSort) ]
type OptionsMsg = SetMinStars String | SetSearchIn String | SetUserFilter String
type alias SearchOptions = { minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
input [] [ value model.options.sort, onInput (Options SetSort) ]
viewOptions : Model -> Html Msg
type OptionsMsg = SetMinStars String | SetSearchIn String | SetUserFilter String
type alias SearchOptions = { minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
viewOptions options = input [] [ value options.sort, onInput SetSort ]
viewOptions : Model -> Html MsgviewOptions : SearchOptions -> Html OptionsMsg
input [] [ value model.options.sort, onInput (Options SetSort) ]
viewOptions : SearchOptions -> Html OptionsMsg
viewOptions options = input [] [ value options.sort, onInput SetSort ]
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] ]
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] ]
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] ]
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] ]
List StringList Int
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] ]
List StringList Int
List.map String.length foo
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] ]
List StringList Int
List.map String.length fooString.length : String -> Int
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] ]
Html StringHtml Int
Html.map String.length fooString.length : String -> Int
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] ]
Html StringHtml Int
Html.map String.length fooString.length : String -> Int
viewOptions : SearchOptions -> Html OptionsMsg
Html OptionsMsg
Html Msg
Html OptionsMsg Html.map
Html Msg
Html OptionsMsg Html.map ???
Html Msg
Html OptionsMsg OptionsMsg -> Msg Html.map ??? Html Msg
type Msg = Search | SetQuery String | DeleteById Int | Options OptionsMsg
Html OptionsMsg OptionsMsg -> Msg Html.map ??? Html Msg
type Msg = Search | SetQuery String | DeleteById Int | Options OptionsMsg
Html OptionsMsg OptionsMsg -> Msg Html.map ??? Html Msg
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] ]
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] , viewOptions model.options ]
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] , viewOptions model.options ]
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] , Html.map ??????? (viewOptions model.options) ]
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] , Html.map Options (viewOptions model.options) ]
type Msg = Search | SetQuery String | DeleteById Int | Options OptionsMsg
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] , Html.map Options (viewOptions model.options) ]
type Msg = Search | SetQuery String | DeleteById Int | Options OptionsMsg
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] , Html.map Options (viewOptions model.options) , ul [] (List.map viewSearchResult model.results) ]
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] , Html.map Options (viewOptions model.options) , ul [] (List.map viewSearchResult model.results) ]
viewSearchResult : SearchResult -> Html Msg
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] , Html.map Options (viewOptions model.options) , ul [] (List.map viewSearchResult model.results) ]
viewSearchResult : SearchResult -> Html Msg
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] , Html.map Options (viewOptions model.options) , ul [] (List.map viewSearchResult model.results) ]
viewSearchResult : SearchResult -> Html Msg
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [ class "content" ] [ input [ defaultValue model.query, onInput SetQuery ] [] , button [ onClick Search ] [ text "Search" ] , Html.map Options (viewOptions model.options) , ul [] (List.map viewSearchResult model.results) ]
viewSearchResult : SearchResult -> Html Msg
Recap
type Msg = Search | SetQuery String | DeleteById Int | SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
viewSearchResult : Model -> Html MsgviewSearchResult model = input [] [ value model.sort, onInput SetSort ]
type Msg = Search | SetQuery String | DeleteById Int | Options OptionsMsg
type OptionsMsg = SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , options : SearchOptions }
type alias SearchOptions = { minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
viewSearchResult : Model -> Html MsgviewSearchResult model = input [] [ value model.options.sort, onInput (Options SetSort) ]
type Msg = Search | SetQuery String | DeleteById Int | Options OptionsMsg
type OptionsMsg = SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , options : SearchOptions }
type alias SearchOptions = { minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
viewSearchResult : SearchOptions -> Html OptionsMsgviewSearchResult options = input [] [ value options.sort, onInput SetSort ]
type Msg = Search | SetQuery String | DeleteById Int | Options OptionsMsg
type OptionsMsg = SetMinStars String | SetSearchIn String | SetUserFilter String
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , options : SearchOptions }
type alias SearchOptions = { minStars : Int , minStarsError : Maybe String , searchIn : String , userFilter : String }
viewSearchResult : SearchOptions -> Html OptionsMsgviewSearchResult options = input [] [ value options.sort, onInput SetSort ]
Html.map Options Html Msg
other ways to map
List.map : (originalVal -> newVal) -> List originalVal -> List newVal
List.map : (originalVal -> newVal) -> List originalVal -> List newVal
Html.map : (originalMsg -> newMsg) -> Html originalMsg -> Html newMsg
List.map : (originalVal -> newVal) -> List originalVal -> List newVal
Html.map : (originalMsg -> newMsg) -> Html originalMsg -> Html newMsg
Cmd.map : (originalMsg -> newMsg) -> Cmd originalMsg -> Cmd newMsg
List.map : (originalVal -> newVal) -> List originalVal -> List newVal
Html.map : (originalMsg -> newMsg) -> Html originalMsg -> Html newMsg
Cmd.map : (originalMsg -> newMsg) -> Cmd originalMsg -> Cmd newMsg
Sub.map : (originalMsg -> newMsg) -> Sub originalMsg -> Sub newMsg
List.map : (originalVal -> newVal) -> List originalVal -> List newVal
Html.map : (originalMsg -> newMsg) -> Html originalMsg -> Html newMsg
Cmd.map : (originalMsg -> newMsg) -> Cmd originalMsg -> Cmd newMsg
Sub.map : (originalMsg -> newMsg) -> Sub originalMsg -> Sub newMsg
delegation!
viewOptions : SearchOptions -> Html OptionsMsg
view : Model -> Html Msgview model = div [] [ input [ defaultValue model.query, onInput SetQuery ] [] , Html.map Options (viewOptions model.options) ]
type Msg = Search | SetQuery String | DeleteById Int | Options OptionsMsg
Exercise: resolve the TODOs in part10type alias Model = { query : String , results : List SearchResult , options : SearchOptions }
11. Scaling Elm Code
The Deeply Nested Component Problem
The Deeply Nested Component Problem
solving
Component
Web ComponentsReact Components
Angular ComponentsEmber ComponentsMithril ComponentsVue Components
Componentowns its own state
Header Componentowns its own state
Footer Componentowns its own state
Body Componentowns its own state
owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own stateowns its own state
owns its own state owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own state owns its own state
owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own stateowns its own state
owns its own state owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own state owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own stateowns its own state
owns its own state owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own state owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
deeply nested components
owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own stateowns its own state
owns its own state owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own state choose language
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
new feature
owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own stateowns its own state
owns its own state owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own state choose language
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own stateowns its own state
owns its own state owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own state choose language
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own stateowns its own state
owns its own state owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own state choose language
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
parent-child communication
owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own stateowns its own state
owns its own state owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own state choose language
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own stateowns its own state
owns its own state owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own state choose language
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own stateowns its own state
owns its own state owns its own state
owns its own state
owns its own state
owns its own state owns its own state owns its own state choose language
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
owns its own state
The Deeply Nested Component Problem
The Deeply Nested Component Problem
solving
The Deeply Nested Component Problem
solving
unidirectional data flow
The Deeply Nested Component Problem
solving
unidirectional data flow
Flux
The Deeply Nested Component Problem
solving
unidirectional data flow
FluxRedux
viewupdate Model
Elm Runtime
viewupdate Model
Elm Runtime
unidirectional data flow
viewupdate Model
Elm Runtime
The Elm Architecture
unidirectional data flow
quick summary
Nest stateful components to describe application
The Deeply Nested Component Problem
Nest stateful components to describe application
The Deeply Nested Component Problem
Unidirectional Data Flow
Nest stateful components to describe application
The Deeply Nested Component Problem
Unidirectional Data Flow
Nest stateful components to describe application
The Deeply Nested Component Problem
Nest stateful components to describe application
Unidirectional Data Flow
is this a problem we're allowed to not have?
is this a problem we're allowed to not have?
yes!
45,000 lines of Production Elm Code
45,000 lines of Production Elm Code
45,000 lines of Production Elm Code
simpler
45,000 lines of Production Elm Code
simpler
different
how?
45,000 lines of Production Elm Code
how do we keep code modular at scale?
45,000 lines of Production Elm Code
how do we keep code modular at scale?
how do we share code without duplication?
45,000 lines of Production Elm Code
viewupdate Model
Elm Runtime
specific techniques
viewupdate Model
Elm Runtime
specific techniques
for scaling in a modular way
viewupdate Model
Elm Runtime
specific techniques
for scaling in a modular way
for sharing code without duplication
Scaling Fundamentals
1. Model2. view3. update
viewupdate Model
Elm Runtime
Step 1: expandStep 2: refactor
Scaling Fundamentals
1. Model2. view3. update
1. When view gets painfully large, subdivide it.
Split it into smaller helper functions.
1. When view gets painfully large, subdivide it.
Split it into smaller helper functions.
1. When view gets painfully large, subdivide it.
viewSearchResult : SearchResult -> Html Msg
viewErrorMessage : Maybe String -> Html msg
viewOptions : SearchOptions -> Html OptionsMsg
Split it into smaller helper functions.
Don't change how update works!
Don't change how Model works!
1. When view gets painfully large, subdivide it.
2. When Model gets painfully large, subdivide it.
2. When Model gets painfully large, subdivide it.
Split out smaller pieces.
2. When Model gets painfully large, subdivide it.
Split out smaller pieces.
type alias Model = { query : String , results : List SearchResult , errorMessage : Maybe String , options : SearchOptions }
2. When Model gets painfully large, subdivide it.
Split out smaller pieces.
Don't change how view works!
Don't change how update works!
3. When update gets painfully large, subdivide it.
Split it into smaller helper functions.
3. When update gets painfully large, subdivide it.
Split it into smaller helper functions.
3. When update gets painfully large, subdivide it.
updateOptions : OptionsMsg -> SearchOptions -> SearchOptions
type Msg = Search | Options OptionsMsg
Split it into smaller helper functions.
Don't change how view works!
Don't change how Model works!
3. When update gets painfully large, subdivide it.
Reusing Code without Duplication
thinking in terms of functions
thinking in terms of functionsnot components
logo : Html msglogo = img [ src "logo.png" ] []
view : Model -> Html Msgview model = logo
fancyLink : String -> String -> Html msgfancyLink url caption = a [ class "fancy-link", href url ] [ text caption ]
view : Model -> Html Msgview model = fancyLink "/about" "About Us"
profile : User -> Html msgprofile user = div [ class "profile" ] [ a [ href ("/users/" ++ user.username) ] [ img [ class "user-photo", src user.photo ] [] ] ]
view : Model -> Html Msgview model = profile model.currentUser
checkbox : String -> Bool -> Html msgcheckbox caption isChecked = label [ input [ type' "checkbox" , checked isChecked ] , text caption ]
view : Model -> Html Msgview model = checkbox "enable sounds" model.enableSounds
checkbox : msg -> String -> Bool -> Html msgcheckbox toggleChecked caption isChecked = label [ input [ type' "checkbox" , checked isChecked , onClick toggleChecked ] , text caption ]
view : Model -> Html Msgview model = checkbox ToggleSounds "enable sounds" model.enableSounds
dropdown : List String -> String -> Html msgdropdown options selectedOpt = let viewOpt opt = option [ selected (opt == selectedOpt) ] [ text opt ] in select [] (List.map viewOpt options)
view : Model -> Html Msgview model = dropdown [ "cat", "dog", "orangutan" ] "cat"
dropdown : (String -> msg) -> List String -> String -> Html msgdropdown selectOpt options selectedOpt = let viewOpt opt = option [ selected (opt == selectedOpt) ] [ text opt ] in select [ onChange selectOpt ] (List.map viewOpt options)
view : Model -> Html Msgview model = dropdown ChooseSpiritAnimal [ "cat", "dog", "orangutan" ] "cat"
portable signup form
signup : { setUsername : (String -> msg) , setPassword : (String -> msg) , setFirstName : (String -> msg) , setLastName : (String -> msg) , setEmail : (String -> msg) , cancel : msg , submit : (List ValidationError -> msg) } -> { username : String , password : String , firstName : String , lastName : String , email : String } -> Html msg
view : SignupState -> Html SignupMsg
view : SignupState -> Html SignupMsg
update : SignupMsg -> SignupModel -> ( SignupModel, Cmd SignupMsg )
view : SignupState -> Html SignupMsg
update : SignupMsg -> SignupModel -> ( SignupModel, Cmd SignupMsg )
init : ( SignupModel, SignupMsg )
type alias Model = { signup : SignupModel , … }
view : Model -> Html Msgview model = Signup.view model.signup |> Html.map ...
view : SignupState -> Html SignupMsg
update : SignupMsg -> SignupModel -> ( SignupModel, Cmd SignupMsg )
init : ( SignupModel, SignupMsg )
view : SignupState -> Html SignupMsg
update : SignupMsg -> SignupModel -> ( SignupModel, Cmd SignupMsg )
init : ( SignupModel, SignupMsg )
type alias Model = { signup : SignupModel , … }
view : Model -> Html Msgview model = Signup.view model.signup |> Html.map ...
type Msg = SignupMsg SignupMsg | OtherStuff
update : Msg -> Model -> ( Model, Cmd Msg)update msg model = case msg of SignupMsg signupMsg -> ….
An Easy Mistake to Make
An Easy Mistake to Make
MVU lets me isolate state...
An Easy Mistake to Make
MVU lets me isolate state...nesting isolated state feels familiar...
!!!
An Easy Mistake to Make
MVU lets me isolate state...nesting isolated state feels familiar...
An Easy Mistake to Make
MVU lets me isolate state...nesting isolated state feels familiar...
sortable table
Exercise: resolve the TODOs in part11
type alias Model = { signup : SignupModel , … }
view : Model -> Html Msgview model = Signup.view model.signup |> Html.map ...
type Msg = SignupMsg SignupMsg | OtherStuff
update : Msg -> Model -> ( Model, Cmd Msg)update msg model = case msg of SignupMsg signupMsg -> ….
12. Performance Optimization
Collections Performance
5 :: []
5 :: []
== [ 5 ]
1 :: (5 :: [])
1 :: (5 :: [])
== [ 1, 5 ]
4 :: (1 :: (5 :: []))
4 :: (1 :: (5 :: []))
== [ 4, 1, 5 ]
4 :: (1 :: (5 :: []))
== [ 4, 1, 5 ]
adding to the front: cheap!
4 :: (1 :: (5 :: []))
== [ 4, 1, 5 ]
adding to the front: cheap!
adding to the back: expensive!
4 :: (1 :: (5 :: []))
== [ 4, 1, 5 ]
adding to the front: cheap!
adding to the back: expensive!
reading the first element: cheap!
4 :: (1 :: (5 :: []))
== [ 4, 1, 5 ]
adding to the front: cheap!
adding to the back: expensive!
reading the first element: cheap!reading other elements: expensive!
case myList of [] -> -- some logic goes here
4 :: (1 :: (5 :: []))== [ 4, 1, 5 ]
case myList of [] -> -- some logic goes here
first :: rest -> -- some logic goes here
4 :: (1 :: (5 :: []))== [ 4, 1, 5 ]
case myList of [] -> -- some logic goes here
first :: rest -> -- some logic goes here
4 :: (1 :: (5 :: []))== [ 4, 1, 5 ]
case myList of [] -> -- some logic goes here
first :: rest -> -- some logic goes here
4 :: (1 :: (5 :: []))== [ 4, 1, 5 ]
case myList of [] -> -- some logic goes here
singleton :: [] -> -- some logic goes here
first :: rest -> -- some logic goes here
4 :: (1 :: (5 :: []))== [ 4, 1, 5 ]
countEmptyStrings : List String -> Int
countEmptyStrings : List String -> IntcountEmptyStrings list = case list of [] -> 0
countEmptyStrings : List String -> IntcountEmptyStrings list = case list of [] -> 0
first :: rest ->
countEmptyStrings : List String -> IntcountEmptyStrings list = case list of [] -> 0
first :: rest -> if first == "" then 1 + (countEmptyStrings rest)
countEmptyStrings : List String -> IntcountEmptyStrings list = case list of [] -> 0
first :: rest -> if first == "" then 1 + (countEmptyStrings rest) else countEmptyStrings rest
countEmptyStrings : List String -> IntcountEmptyStrings list = case list of [] -> 0
first :: rest -> if first == "" then 1 + (countEmptyStrings rest) else countEmptyStrings rest
recurse, then immediately return
countEmptyStrings : List String -> IntcountEmptyStrings list = case list of [] -> 0
first :: rest -> if first == "" then 1 + (countEmptyStrings rest) else countEmptyStrings rest
recurse, then immediately return: tail call!
countEmptyStrings : List String -> IntcountEmptyStrings list = case list of [] -> 0
first :: rest -> if first == "" then 1 + (countEmptyStrings rest) else countEmptyStrings rest
recurse, then do something else before returning: not tail call
countEmptyStrings : List String -> IntcountEmptyStrings list = case list of [] -> 0
first :: rest -> if first == "" then 1 + (countEmptyStrings rest) else countEmptyStrings rest
myArray : Array FloatmyArray = Array.fromList [ 1.1, 2.2, 3.3 ]
myArray : Array FloatmyArray = Array.fromList [ 1.1, 2.2, 3.3 ]
adding to the front: less cheap
adding to the back: less expensive
myArray : Array FloatmyArray = Array.fromList [ 1.1, 2.2, 3.3 ]
adding to the front: less cheap
adding to the back: less expensive
reading any element: cheap!removing first element: expensive!
flavors : Dict String Intflavors = Dict.fromList [ ( "vanilla", 5 ) , ( "chocolate", 9 ) , ( "strawberry", 7 ) ]
Dict.get "strawberry" flavors
flavors : Dict String Intflavors = Dict.fromList [ ( "vanilla", 5 ) , ( "chocolate", 9 ) , ( "strawberry", 7 ) ]
Dict.get "strawberry" flavors == Just 7
flavors : Dict String Intflavors = Dict.fromList [ ( "vanilla", 5 ) , ( "chocolate", 9 ) , ( "strawberry", 7 ) ]
Dict.remove "strawberry" flavors == Dict.fromList [ ( "vanilla", 5 ) , ( "chocolate", 9 ) ]
flavors : Dict String Intflavors = Dict.fromList [ ( "vanilla", 5 ) , ( "chocolate", 9 ) , ( "strawberry", 7 ) ]
div [ class "flavor-list" ] flavors
flavors : Dict String Intflavors = Dict.fromList [ ( "vanilla", 5 ) , ( "chocolate", 9 ) , ( "strawberry", 7 ) ]
div [ class "flavor-list" ] flavors
flavors : Dict String Intflavors = Dict.fromList [ ( "vanilla", 5 ) , ( "chocolate", 9 ) , ( "strawberry", 7 ) ]
Rendering from a Dict
Dict.values
List.sortBy
Dict.values
Skipping Virtual DOM building
viewMenu : List MenuItem -> Html MsgviewMenu items = -- lots of view code goes here
viewMenu : List MenuItem -> Html MsgviewMenu items = -- lots of view code goes here
view : Model -> Html Msgview model = -- lots of other code goes here...
viewMenu model.menuItems
viewMenu : List MenuItem -> Html MsgviewMenu items = -- lots of view code goes here
view : Model -> Html Msgview model = -- lots of other code goes here...
lazy viewMenu model.menuItems
viewMenu : Config -> List MenuItem -> Html MsgviewMenu config items = -- lots of view code goes here
view : Model -> Html Msgview model = -- lots of other code goes here...
lazy2 viewMenu model.config model.menuItems
Debug.log
Debug.log "some numbers" [ 1, 2, 3 ]
someNumbers = Debug.log "some numbers" [ 1, 2, 3 ]
someNumbers = [ 1, 2, 3 ]
Debug.log "some numbers"
requestAnimationFrame
viewupdate Model
Elm Runtime
viewupdate Model
Elm Runtime
browser repaints at ~60fps
viewupdate Model
Elm Runtime
browser repaints at ~60fps
computing virtual DOM more often than that is a waste!
viewupdate Model
Elm Runtime
browser repaints at ~60fps
computing virtual DOM more often than that is a waste!
keep updating model and running commands
viewupdate Model
Elm Runtime
browser repaints at ~60fps
computing virtual DOM more often than that is a waste!
keep updating model and running commandsdon't bother running view until the next repaint
viewupdate Model
Elm Runtime
keep updating model and running commandsdon't bother running view until the next repaint
how do we get this?
viewupdate Model
Elm Runtime
keep updating model and running commandsdon't bother running view until the next repaint
This is just how Elm rolls.
don't count on Debug.log in views!
Exercise: resolve the TODOs in part13
lazy
Debug.log "foo is" foo
13. Tools
HTML-to-Elm
JSON-to-Elm
sample JSON
Error Message Catalog
create-elm-app
elm-format
elm-reactor
elm-css
Built With Elm: builtwithelm.co
Links of Interest
Elm Community: elm-community.org
NRI Tech Blog: tech.noredink.com
Exercise: resolve the TODO in ElmHubCss.elm
package.elm-lang.org/packages/rtfeldman/elm-css/latest
Implement additional search options:
ElmHub Hacking!
https://developer.github.com/v3/search/#search-repositories
Refactor to results : Dict Int SearchResult
Move SearchOptions into its own module
Migrate tests from part9 to part14 and add tests for update