├── .gitignore ├── elm-package.json ├── LICENSE ├── src └── Styles │ └── Styles.elm ├── README.md └── examples ├── Pure ├── Dropdown.elm └── Main.elm ├── Single └── Main.elm └── Stateful ├── Dropdown.elm └── Main.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff/ 2 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Example of a scalable dropdown menu in elm", 4 | "repository": "https://github.com/user/elm-dropdown.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src", 8 | "examples" 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 13 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 14 | "elm-lang/mouse": "1.0.1 <= v < 2.0.0" 15 | }, 16 | "elm-version": "0.18.0 <= v < 0.19.0" 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Wouter In 't Velt 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /src/Styles/Styles.elm: -------------------------------------------------------------------------------- 1 | module Styles.Styles exposing (..) 2 | {- Css styles for dropdown, moved off to separate module 3 | -} 4 | -- styles for main 5 | mainContainer : List (String, String) 6 | mainContainer = 7 | [ ("height","100%") 8 | , ("background-color","#fafafa") 9 | , ("padding","16px") 10 | ] 11 | 12 | -- styles for dropdown container 13 | dropdownContainer : List (String, String) 14 | dropdownContainer = 15 | [ ("position","relative") 16 | , ("margin","16px") 17 | , ("width", "216px") 18 | , ("display", "inline-block") 19 | , ("fontFamily", "sans-serif") 20 | , ("fontSize", "16px") 21 | ] 22 | 23 | -- styles for main input field 24 | dropdownInput : List (String, String) 25 | dropdownInput = 26 | [ ("padding","6px 12px 8px 15px") 27 | , ("margin","0") 28 | , ("border","1px solid rgba(0,0,0,.17)") 29 | , ("border-radius","4px") 30 | , ("background-color","white") 31 | , ("display","flex") 32 | , ("alignItems","center") 33 | ] 34 | 35 | -- disabled style 36 | dropdownDisabled : List (String, String) 37 | dropdownDisabled = 38 | [ ("color","rgba(0,0,0,.54") ] 39 | 40 | 41 | -- styles for the text of selected item 42 | dropdownText : List (String, String) 43 | dropdownText = 44 | [ ("flex","1 0 auto")] 45 | 46 | -- styles for list container 47 | dropdownList : List (String, String) 48 | dropdownList = 49 | [ ("position", "absolute") 50 | , ("top", "36px") 51 | , ("border-radius","4px") 52 | , ("box-shadow", "0 1px 2px rgba(0,0,0,.24)") 53 | , ("padding", "4px 8px") 54 | , ("margin", "0") 55 | , ("width", "200px") 56 | , ("background-color", "white") 57 | ] 58 | 59 | -- styles for list items 60 | dropdownListItem : List (String, String) 61 | dropdownListItem = 62 | [ ("display", "block") 63 | , ("padding","8px 8px") 64 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example of a dropdown menu 2 | 3 | Comparing a pure versus stateful (component) setup. 4 | 5 | A extensive explanation and comparison of the examples can be found [here on Medium.com][1] 6 | 7 | The example folder contains the following examples: 8 | - single example: baseline, a single dropdown in one main module 9 | - stateful example: using an extracted dropdown (component) - which manages its own state 10 | - pure example: using an extraction without internal state management 11 | 12 | ## Comparison pure and stateful 13 | A rough comparison of the two extraction methods reveals: 14 | - In total size (lines of code - including comments and docs) of the *Main.elm* module, there is not much difference between the stateful and pure extraction. The pure version is slightly larger. 15 | - The stateful *Dropdown.elm* has about 40% more code. In itself, the implications of this are small. The reason for extracting the dropdown is to make the overall structure, in particular the main module, less complex. So the size of the extracted module is not very important. 16 | 17 | The *key difference* becomes clear when we zoom in on the building blocks within the Main.elm modules in the 2 variants: 18 | 19 | - the `update` function has about 30-35% more lines of code in the stateful setup, compared to pure setup 20 | 21 | 22 | ## In a pure setup, the code is much easier to read and to debug 23 | 24 | In the pure setup, most of the code in our Main.elm module is static. 25 | The key blocks of code relevant in the programs lifecycle, where most of the "action" takes place, tends to be in the `update` function. This is much more compact and readable in the pure setup. 26 | 27 | 28 | [1]: https://medium.com/@wintvelt/a-reusable-dropdown-in-elm-part-1-d7ac2d106f13 29 | 30 | ## Run 31 | 32 | Use `elm reactor` or `elm-live`: 33 | 34 | `elm-live examples/pure/Main.elm` 35 | 36 | You can also run `elm-live` with `debug`: 37 | 38 | `elm-live examples/pure/Main.elm -- --debug` 39 | -------------------------------------------------------------------------------- /examples/Pure/Dropdown.elm: -------------------------------------------------------------------------------- 1 | module Pure.Dropdown exposing (Context, Config, view) 2 | {- a stateless Dropdown component -} 3 | import Html exposing (..) 4 | import Html.Attributes exposing (..) 5 | import Html.Events exposing (onWithOptions) 6 | import Json.Decode as Json 7 | import Styles.Styles as Styles 8 | 9 | 10 | -- MODEL 11 | {- Context type alias 12 | this is dynamic stuff - may change in each update cycle 13 | this is not managed by the dropdown, but passed in from parent 14 | kind of like props (including callbacks) in react 15 | -} 16 | type alias Context = 17 | { selectedItem : Maybe String 18 | , isOpen : Bool 19 | } 20 | 21 | {- Config is the static stuff, that won't change during life cycle 22 | Like functions and message constructors 23 | Also transparent, because this is also owned by parent 24 | -} 25 | type alias Config msg = 26 | { defaultText : String 27 | , clickedMsg : msg 28 | , itemPickedMsg : String -> msg 29 | } 30 | 31 | 32 | 33 | -- VIEW 34 | 35 | 36 | view : Config msg -> Context -> List String -> Html msg 37 | view config context data = 38 | let 39 | mainText = 40 | context.selectedItem 41 | |> Maybe.withDefault config.defaultText 42 | 43 | displayStyle = 44 | if context.isOpen then 45 | ( "display", "block" ) 46 | else 47 | ( "display", "none" ) 48 | 49 | mainAttr = 50 | case data of 51 | [] -> 52 | [ style <| Styles.dropdownDisabled ++ Styles.dropdownInput 53 | ] 54 | 55 | _ -> 56 | [ style Styles.dropdownInput 57 | , onClick config.clickedMsg 58 | ] 59 | in 60 | div 61 | [ style Styles.dropdownContainer ] 62 | [ p 63 | mainAttr 64 | [ span [ style Styles.dropdownText ] [ text mainText ] 65 | , span [] [ text "▾" ] 66 | ] 67 | , ul 68 | [ style <| displayStyle :: Styles.dropdownList ] 69 | (List.map (viewItem config) data) 70 | ] 71 | 72 | 73 | viewItem : Config msg -> String -> Html msg 74 | viewItem config item = 75 | li 76 | [ style Styles.dropdownListItem 77 | , onClick <| config.itemPickedMsg item 78 | ] 79 | [ text item ] 80 | 81 | 82 | 83 | -- helper to cancel click anywhere 84 | 85 | 86 | onClick : msg -> Attribute msg 87 | onClick message = 88 | onWithOptions 89 | "click" 90 | { stopPropagation = True 91 | , preventDefault = False 92 | } 93 | (Json.succeed message) 94 | -------------------------------------------------------------------------------- /examples/Single/Main.elm: -------------------------------------------------------------------------------- 1 | {- a dropdown 2 | with open/ close state 3 | and blur when clicked outside 4 | -} 5 | import Html exposing (..) 6 | import Html.Attributes exposing (..) 7 | import Html.Events exposing (onWithOptions) 8 | import Dict exposing (Dict) 9 | import Json.Decode as Json 10 | import Mouse 11 | 12 | import Styles.Styles as Styles 13 | 14 | main = 15 | Html.program 16 | { init = init 17 | , view = view 18 | , update = update 19 | , subscriptions = subscriptions 20 | } 21 | 22 | 23 | -- MODEL 24 | 25 | -- our main model, which will change as we use the app 26 | type alias Model = 27 | { pickedCountry : Maybe Country 28 | , isOpen : Bool 29 | } 30 | 31 | -- simple types so we can read the code better 32 | type alias Country = String 33 | type alias City = String 34 | 35 | -- global constants/ config 36 | allCities : Dict Country (List City) 37 | allCities = 38 | Dict.fromList 39 | [ ("Spain",["Barcelona","Madrid","Alicante","Valencia"]) 40 | , ("Germany",["Berlin","München","Bonn","Leipzig"]) 41 | , ("France",["Paris","Lyon","Marseille","Dijon"]) 42 | , ("Italy",["Florence","Rome","Milan"]) 43 | ] 44 | 45 | countries : List Country 46 | countries = 47 | Dict.keys allCities 48 | 49 | 50 | init : ( Model, Cmd Msg ) 51 | init = 52 | { pickedCountry = Nothing 53 | , isOpen = False 54 | } ! [] 55 | 56 | 57 | 58 | -- UPDATE 59 | 60 | 61 | type Msg 62 | = CountryPicked Country 63 | | DropDownClicked 64 | | Blur 65 | 66 | 67 | update : Msg -> Model -> ( Model, Cmd Msg ) 68 | update msg model = 69 | case msg of 70 | CountryPicked country -> 71 | { model 72 | | pickedCountry = Just country 73 | , isOpen = False 74 | } ! [] 75 | 76 | DropDownClicked -> 77 | { model | isOpen = not model.isOpen } ! [] 78 | 79 | Blur -> 80 | { model | isOpen = False } ! [] 81 | 82 | 83 | 84 | -- SUBSCRIPTIONS 85 | 86 | 87 | subscriptions : Model -> Sub Msg 88 | subscriptions model = 89 | if model.isOpen then 90 | Mouse.clicks (always Blur) 91 | else 92 | Sub.none 93 | 94 | 95 | 96 | 97 | -- VIEW 98 | 99 | 100 | 101 | view : Model -> Html Msg 102 | view model = 103 | let 104 | selectedText = 105 | model.pickedCountry 106 | |> Maybe.withDefault "-- pick a country --" 107 | 108 | displayStyle = 109 | if model.isOpen then 110 | ("display", "block") 111 | else 112 | ("display", "none") 113 | 114 | in 115 | div [ style Styles.dropdownContainer ] 116 | [ p 117 | [ style Styles.dropdownInput 118 | , onClick DropDownClicked 119 | ] 120 | [ span [ style Styles.dropdownText ] [ text <| selectedText ] 121 | , span [] [ text " ▾" ] 122 | ] 123 | , ul 124 | [ style <| displayStyle :: Styles.dropdownList ] 125 | (List.map viewCountry countries) 126 | ] 127 | 128 | viewCountry : Country -> Html Msg 129 | viewCountry country = 130 | li 131 | [ style Styles.dropdownListItem 132 | , onClick <| CountryPicked country 133 | ] 134 | [ text country ] 135 | 136 | 137 | -- helper to cancel click anywhere 138 | onClick : msg -> Attribute msg 139 | onClick message = 140 | onWithOptions 141 | "click" 142 | { stopPropagation = True 143 | , preventDefault = False 144 | } 145 | (Json.succeed message) 146 | -------------------------------------------------------------------------------- /examples/Stateful/Dropdown.elm: -------------------------------------------------------------------------------- 1 | module Stateful.Dropdown exposing 2 | (Context, Model, init, selectedFrom, openState, Msg(..), update, view) 3 | {- a Dropdown component that manages its own state 4 | -} 5 | import Html exposing (..) 6 | import Html.Attributes exposing (..) 7 | import Html.Events exposing (onWithOptions) 8 | import Json.Decode as Json 9 | 10 | import Styles.Styles as Styles 11 | 12 | 13 | 14 | -- MODEL 15 | 16 | {- main model, opaque to ensure it can only be updated thru Msg and Update 17 | -} 18 | type Model = 19 | Model 20 | { selectedItem : Maybe String 21 | , isOpen : Bool 22 | } 23 | 24 | 25 | init : Model 26 | init = 27 | Model 28 | { selectedItem = Nothing 29 | , isOpen = False 30 | } 31 | 32 | {- Context type alias 33 | (this is stuff not managed by the dropdown, but passed in from parent) 34 | kind of like props (including callbacks) in react 35 | in our dropdown context is the default text, displayed if no item is selected 36 | -} 37 | type alias Context = String 38 | 39 | -- helpers to enable reading from Model 40 | selectedFrom : Model -> Maybe String 41 | selectedFrom (Model {selectedItem}) = 42 | selectedItem 43 | 44 | openState : Model -> Bool 45 | openState (Model {isOpen}) = 46 | isOpen 47 | 48 | 49 | 50 | 51 | -- UPDATE 52 | 53 | 54 | type Msg 55 | = ItemPicked (Maybe String) 56 | | SetOpenState Bool 57 | 58 | 59 | update : Msg -> Model -> ( Model, Maybe String ) 60 | update msg (Model model) = 61 | case msg of 62 | ItemPicked item -> 63 | ( Model 64 | { model 65 | | selectedItem = item 66 | , isOpen = False 67 | } 68 | , item 69 | ) 70 | 71 | SetOpenState newState -> 72 | ( Model 73 | { model 74 | | isOpen = newState 75 | } 76 | , Nothing 77 | ) 78 | 79 | 80 | 81 | 82 | -- VIEW 83 | 84 | 85 | 86 | view : Context -> Model -> List String -> Html Msg 87 | view context (Model model) data = 88 | let 89 | mainText = 90 | model.selectedItem 91 | |> Maybe.withDefault context 92 | 93 | displayStyle = 94 | if model.isOpen then 95 | ("display", "block") 96 | else 97 | ("display", "none") 98 | 99 | mainAttr = 100 | case data of 101 | [] -> 102 | [ style <| Styles.dropdownDisabled ++ Styles.dropdownInput 103 | ] 104 | 105 | _ -> 106 | [ style Styles.dropdownInput 107 | , onClick <| SetOpenState <| not model.isOpen 108 | ] 109 | 110 | in 111 | div 112 | [ style Styles.dropdownContainer ] 113 | [ p 114 | mainAttr 115 | [ span [ style Styles.dropdownText ] [ text mainText ] 116 | , span [] [ text "▾" ] 117 | ] 118 | , ul 119 | [ style <| displayStyle :: Styles.dropdownList ] 120 | (List.map viewItem data) 121 | ] 122 | 123 | 124 | viewItem : String -> Html Msg 125 | viewItem item = 126 | li 127 | [ style Styles.dropdownListItem 128 | , onClick <| ItemPicked <| Just item 129 | ] 130 | [ text item ] 131 | 132 | 133 | -- helper to cancel click anywhere 134 | onClick : msg -> Attribute msg 135 | onClick message = 136 | onWithOptions 137 | "click" 138 | { stopPropagation = True 139 | , preventDefault = False 140 | } 141 | (Json.succeed message) 142 | -------------------------------------------------------------------------------- /examples/Pure/Main.elm: -------------------------------------------------------------------------------- 1 | {- two dropdowns 2 | with open/ close state 3 | and blur when clicked outside 4 | using a flat approach 5 | importing a module that exposes config, context and view 6 | -} 7 | import Html exposing (..) 8 | import Html.Attributes exposing (style) 9 | import Dict exposing (Dict) 10 | import Json.Decode as Json 11 | import Mouse 12 | 13 | import Styles.Styles as Styles 14 | import Pure.Dropdown as Dropdown 15 | 16 | main = 17 | Html.program 18 | { init = init 19 | , view = view 20 | , update = update 21 | , subscriptions = subscriptions 22 | } 23 | 24 | 25 | 26 | -- MODEL 27 | 28 | -- our main model, which will change as we use the app 29 | type alias Model = 30 | { country : Maybe Country 31 | , city : Maybe City 32 | , openDropDown : OpenDropDown 33 | } 34 | 35 | type OpenDropDown = 36 | AllClosed 37 | | CountryDropDown 38 | | CityDropDown 39 | 40 | 41 | init : ( Model, Cmd Msg ) 42 | init = 43 | { country = Nothing 44 | , city = Nothing 45 | , openDropDown = AllClosed 46 | } ! [] 47 | 48 | -- simple types so we can read the code better 49 | type alias Country = String 50 | type alias City = String 51 | 52 | -- global constants/ config 53 | allCities : Dict Country (List City) 54 | allCities = 55 | Dict.fromList 56 | [ ("Spain",["Barcelona","Madrid","Alicante","Valencia"]) 57 | , ("Germany",["Berlin","München","Bonn","Leipzig"]) 58 | , ("France",["Paris","Lyon","Marseille","Dijon"]) 59 | , ("Italy",["Florence","Rome","Milan"]) 60 | ] 61 | 62 | countries : List Country 63 | countries = 64 | Dict.keys allCities 65 | 66 | 67 | countryConfig : Dropdown.Config Msg 68 | countryConfig = 69 | { defaultText = "-- pick a country --" 70 | , clickedMsg = Toggle CountryDropDown 71 | , itemPickedMsg = CountryPicked 72 | } 73 | 74 | cityConfig : Dropdown.Config Msg 75 | cityConfig = 76 | { defaultText = "-- pick a city --" 77 | , clickedMsg = Toggle CityDropDown 78 | , itemPickedMsg = CityPicked 79 | } 80 | 81 | 82 | 83 | -- UPDATE 84 | 85 | 86 | type Msg = 87 | Toggle OpenDropDown 88 | | CountryPicked Country 89 | | CityPicked City 90 | | Blur 91 | 92 | 93 | update : Msg -> Model -> ( Model, Cmd Msg ) 94 | update msg model = 95 | case msg of 96 | Toggle dropdown -> 97 | let 98 | newOpenDropDown = 99 | if model.openDropDown == dropdown then 100 | AllClosed 101 | else 102 | dropdown 103 | 104 | in 105 | { model 106 | | openDropDown = newOpenDropDown 107 | } ! [] 108 | 109 | CountryPicked pickedCountry -> 110 | let 111 | newCity = 112 | if model.country /= Just pickedCountry then 113 | Nothing 114 | else 115 | model.city 116 | 117 | in 118 | { model 119 | | country = Just pickedCountry 120 | , city = newCity 121 | , openDropDown = AllClosed 122 | } ! [] 123 | 124 | CityPicked pickedCity -> 125 | { model 126 | | city = Just pickedCity 127 | , openDropDown = AllClosed 128 | } ! [] 129 | 130 | Blur -> 131 | { model 132 | | openDropDown = AllClosed 133 | } ! [] 134 | 135 | 136 | 137 | -- SUBSCRIPTIONS 138 | 139 | 140 | subscriptions : Model -> Sub Msg 141 | subscriptions model = 142 | case model.openDropDown of 143 | AllClosed -> 144 | Sub.none 145 | 146 | _ -> 147 | Mouse.clicks (always Blur) 148 | 149 | 150 | 151 | -- VIEW 152 | 153 | 154 | view : Model -> Html Msg 155 | view model = 156 | let 157 | countryContext = 158 | { selectedItem = model.country 159 | , isOpen = model.openDropDown == CountryDropDown 160 | } 161 | 162 | cityContext = 163 | { selectedItem = model.city 164 | , isOpen = model.openDropDown == CityDropDown 165 | } 166 | 167 | cities = 168 | model.country 169 | |> Maybe.andThen (\c -> Dict.get c allCities) 170 | |> Maybe.withDefault [] 171 | 172 | in 173 | div [ style Styles.mainContainer ] 174 | [ Dropdown.view countryConfig countryContext countries 175 | , Dropdown.view cityConfig cityContext cities 176 | ] 177 | -------------------------------------------------------------------------------- /examples/Stateful/Main.elm: -------------------------------------------------------------------------------- 1 | {- two dropdowns 2 | with open/ close state 3 | and blur when clicked outside 4 | using a nested approach 5 | importing a module that manages its own state 6 | -} 7 | import Html exposing (..) 8 | import Html.Attributes exposing (style) 9 | import Dict exposing (Dict) 10 | import Json.Decode as Json 11 | import Mouse 12 | 13 | import Styles.Styles as Styles 14 | import Stateful.Dropdown as Dropdown 15 | 16 | main = 17 | Html.program 18 | { init = init 19 | , view = view 20 | , update = update 21 | , subscriptions = subscriptions 22 | } 23 | 24 | 25 | 26 | -- MODEL 27 | 28 | -- our main model, which will change as we use the app 29 | type alias Model = 30 | { country : Dropdown.Model 31 | , city : Dropdown.Model 32 | } 33 | 34 | -- simple types so we can read the code better 35 | type alias Country = String 36 | type alias City = String 37 | 38 | -- global constants/ config 39 | allCities : Dict Country (List City) 40 | allCities = 41 | Dict.fromList 42 | [ ("Spain",["Barcelona","Madrid","Alicante","Valencia"]) 43 | , ("Germany",["Berlin","München","Bonn","Leipzig"]) 44 | , ("France",["Paris","Lyon","Marseille","Dijon"]) 45 | , ("Italy",["Florence","Rome","Milan"]) 46 | ] 47 | 48 | countries : List Country 49 | countries = 50 | Dict.keys allCities 51 | 52 | 53 | init : ( Model, Cmd Msg ) 54 | init = 55 | { country = Dropdown.init 56 | , city = Dropdown.init 57 | } ! [] 58 | 59 | 60 | 61 | -- UPDATE 62 | 63 | 64 | type Msg = 65 | CountryMsg Dropdown.Msg 66 | | CityMsg Dropdown.Msg 67 | | Blur 68 | 69 | 70 | update : Msg -> Model -> ( Model, Cmd Msg ) 71 | update msg model = 72 | case msg of 73 | CountryMsg countryMsg -> 74 | let 75 | oldCountry = 76 | Dropdown.selectedFrom model.country 77 | 78 | (newCountry, newSelectedCountry) = 79 | Dropdown.update countryMsg model.country 80 | 81 | (newCity, _) = 82 | if newSelectedCountry /= Nothing && newSelectedCountry /= oldCountry then 83 | Dropdown.update (Dropdown.ItemPicked Nothing) model.city 84 | else 85 | (model.city, Nothing) 86 | 87 | (closedNewCity, _) = 88 | Dropdown.update (Dropdown.SetOpenState False) newCity 89 | 90 | 91 | in 92 | { model 93 | | country = newCountry 94 | , city = closedNewCity 95 | } ! [] 96 | 97 | CityMsg cityMsg -> 98 | let 99 | (newCity, _) = 100 | Dropdown.update cityMsg model.city 101 | 102 | (closedCountry, _) = 103 | Dropdown.update (Dropdown.SetOpenState False) model.country 104 | 105 | in 106 | { model 107 | | country = closedCountry 108 | , city = newCity 109 | } ! [] 110 | 111 | Blur -> 112 | let 113 | (closedCountry, _) = 114 | Dropdown.update (Dropdown.SetOpenState False) model.country 115 | 116 | (closedCity, _) = 117 | Dropdown.update (Dropdown.SetOpenState False) model.city 118 | 119 | in 120 | { model 121 | | country = closedCountry 122 | , city = closedCity 123 | } ! [] 124 | 125 | 126 | 127 | -- SUBSCRIPTIONS 128 | 129 | 130 | subscriptions : Model -> Sub Msg 131 | subscriptions model = 132 | if Dropdown.openState model.country || Dropdown.openState model.city then 133 | Mouse.clicks (always Blur) 134 | else 135 | Sub.none 136 | 137 | 138 | 139 | -- VIEW 140 | 141 | 142 | view : Model -> Html Msg 143 | view model = 144 | let 145 | countryText = 146 | Dropdown.selectedFrom model.country 147 | |> Maybe.withDefault "-- pick a country --" 148 | 149 | cityText = 150 | Dropdown.selectedFrom model.city 151 | |> Maybe.withDefault "-- pick a city --" 152 | 153 | cities = 154 | Dropdown.selectedFrom model.country 155 | |> Maybe.andThen (\c -> Dict.get c allCities) 156 | |> Maybe.withDefault [] 157 | 158 | in 159 | div [ style Styles.mainContainer ] 160 | [ Html.map CountryMsg <| Dropdown.view countryText model.country countries 161 | , Html.map CityMsg <| Dropdown.view cityText model.city cities 162 | ] 163 | --------------------------------------------------------------------------------