├── .gitignore ├── .gitattributes ├── examples ├── elm-package.json ├── Slider.elm ├── ComboBox.elm ├── Embedding.elm ├── RadioButton.elm ├── README.md ├── ReadmeExample.elm ├── Validation.elm ├── Custom.elm └── Recursive.elm ├── elm-package.json ├── README.md └── src └── Kintail └── InputWidget.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | docs.json 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /examples/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Examples", 4 | "repository": "https://github.com/kintail/ui.git", 5 | "license": "MPL-2.0", 6 | "source-directories": [ 7 | ".", 8 | "../src" 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 | }, 15 | "elm-version": "0.18.0 <= v < 0.19.0" 16 | } 17 | -------------------------------------------------------------------------------- /examples/Slider.elm: -------------------------------------------------------------------------------- 1 | module Slider exposing (..) 2 | 3 | import Kintail.InputWidget as InputWidget 4 | import Html exposing (Html) 5 | 6 | 7 | view : Float -> Html Float 8 | view value = 9 | Html.div [] 10 | [ InputWidget.slider [] { min = 0, max = 5, step = 0.1 } value 11 | , Html.text (toString value) 12 | ] 13 | 14 | 15 | main : Program Never Float Float 16 | main = 17 | Html.beginnerProgram 18 | { model = 1.5 19 | , update = always 20 | , view = view 21 | } 22 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.6", 3 | "summary": "Unified shorthand for , `, ``, custom event handlers are required. This package handles 11 | those annoying details for you. 12 | - An experiment in developing a common pattern for working with input widgets. 13 | All functions in this library follow the general pattern `a -> Html a`; for 14 | example, the `checkbox` function has the signature 15 | `List (Html.Attribute Bool) -> Bool -> Html Bool`, meaning that in addition 16 | to a list of extra HTML attributes to apply to the resulting element, it 17 | accepts the current `Bool` value to display (`True` for checked, `False` for 18 | unchecked) and produces a message with the new `Bool` value whenever the 19 | checkbox is clicked. Similarly, a `lineEdit` takes a `String` to display and 20 | produces a new `String` as a message whenever that text is edited. This 21 | generally enforces good practice (since you always have to explicitly 22 | supply the current value to display in an input widget, it's much harder to 23 | get a mismatch between your model and your view) but it turns out that 24 | following this pattern also makes it easy to create cool things like 25 | [input widgets for recursive data types](https://github.com/kintail/input-widget/blob/1.0.6/examples/Recursive.elm). 26 | 27 | # How do I use it? 28 | 29 | To install, run 30 | 31 | ``` 32 | elm package install kintail/input-widget 33 | ``` 34 | 35 | or add 36 | 37 | ```json 38 | "kintail/input-widget": "1.0.0 <= v < 2.0.0" 39 | ``` 40 | 41 | to your `elm-package.json`. 42 | 43 | Check out the [module documentation](http://package.elm-lang.org/packages/kintail/input-widget/1.0.6/Kintail-InputWidget) 44 | for usage details. 45 | 46 | # Examples 47 | 48 | Let's walk through a complete example app to prompt someone for their title, 49 | first and last names using functions from this package, and then display a 50 | customized greeting. Start with some module imports: 51 | 52 | ```elm 53 | module ReadmeExample exposing (..) 54 | 55 | import Html exposing (Html) 56 | import Html.App as Html 57 | import Kintail.InputWidget as InputWidget 58 | ``` 59 | 60 | Next, we'll define a simple model to represent a person (with a union type used 61 | for possible titles), along with some associated messages: 62 | 63 | ```elm 64 | type Title 65 | = Dr 66 | | Mrs 67 | | Ms 68 | | Mr 69 | 70 | 71 | type alias Model = 72 | { title : Title 73 | , firstName : String 74 | , lastName : String 75 | } 76 | 77 | 78 | type Msg 79 | = NewTitle Title 80 | | NewFirstName String 81 | | NewLastName String 82 | ``` 83 | 84 | The `update` function is pretty boring: 85 | 86 | ```elm 87 | update : Msg -> Model -> Model 88 | update message model = 89 | case message of 90 | NewTitle newTitle -> 91 | { model | title = newTitle } 92 | 93 | NewFirstName newFirstName -> 94 | { model | firstName = newFirstName } 95 | 96 | NewLastName newLastName -> 97 | { model | lastName = newLastName } 98 | ``` 99 | 100 | The interesting code is all in `view`: 101 | 102 | ```elm 103 | view : Model -> Html Msg 104 | view model = 105 | Html.div [] 106 | [ InputWidget.comboBox [] toString [ Dr, Mrs, Ms, Mr ] model.title 107 | |> Html.map NewTitle 108 | , InputWidget.lineEdit [] model.firstName 109 | |> Html.map NewFirstName 110 | , InputWidget.lineEdit [] model.lastName 111 | |> Html.map NewLastName 112 | , Html.br [] [] 113 | , Html.text 114 | ("Hello " 115 | ++ toString model.title 116 | ++ ". " 117 | ++ model.firstName 118 | ++ " " 119 | ++ model.lastName 120 | ++ "!" 121 | ) 122 | ] 123 | ``` 124 | 125 | As mentioned above, `InputWidget.lineEdit` takes a `String` value to display and 126 | produces a new `String` message whenever the text is edited. 127 | `InputWidget.comboBox` takes a list of values to populate a combo box with, as 128 | well as the value to display as currently selected, and produces a message with 129 | the newly-selected value whenever the selection changes. (You also have to pass 130 | a function to turn those values into strings to display; in many cases, such as 131 | here, you can simply use Elm's built-in `toString` function.) Note how 132 | `Html.map` is used to convert (tag) the 'new value' messages produced by each 133 | widget into proper `Msg` union type values. 134 | 135 | Finally, wrap everything up with `beginnerProgram`: 136 | 137 | ```elm 138 | main : Program Never 139 | main = 140 | Html.beginnerProgram 141 | { model = Model Dr "Albert" "Einstein" 142 | , update = update 143 | , view = view 144 | } 145 | ``` 146 | 147 | Check out the [examples](https://github.com/kintail/input-widget/tree/1.0.6/examples) 148 | directory to see the above example in full as well as more examples of how to 149 | use each input widget, how to define your own, and how to use them within a 150 | standard Elm app. If you clone this repository, you should be able to run all of 151 | the examples by running [elm-reactor](https://github.com/elm-lang/elm-reactor) 152 | inside the `examples` directory. 153 | -------------------------------------------------------------------------------- /examples/Recursive.elm: -------------------------------------------------------------------------------- 1 | module Recursive exposing (..) 2 | 3 | import String 4 | import Html exposing (Html) 5 | import Kintail.InputWidget as InputWidget 6 | 7 | 8 | -- Expression type ('business logic', not tied to the UI) 9 | 10 | 11 | type Expression 12 | = Constant Bool 13 | | And Expression Expression 14 | | Or Expression Expression 15 | | Not Expression 16 | 17 | 18 | evaluate : Expression -> Bool 19 | evaluate expression = 20 | case expression of 21 | Constant value -> 22 | value 23 | 24 | And firstExpression secondExpression -> 25 | evaluate firstExpression && evaluate secondExpression 26 | 27 | Or firstExpression secondExpression -> 28 | evaluate firstExpression || evaluate secondExpression 29 | 30 | Not subExpression -> 31 | not (evaluate subExpression) 32 | 33 | 34 | 35 | -- Expression widget 36 | 37 | 38 | type ExpressionType 39 | = TFalse 40 | | TTrue 41 | | TAnd 42 | | TOr 43 | | TNot 44 | 45 | 46 | {-| Create a 'default'/'dummy' expression of the given type. For example, if 47 | 'Or' is selected in a combo box, this is used to generate a new dummy expression 48 | 'False Or False' that can then be edited further. 49 | -} 50 | defaultExpressionForType : ExpressionType -> Expression 51 | defaultExpressionForType expressionType = 52 | case expressionType of 53 | TFalse -> 54 | Constant False 55 | 56 | TTrue -> 57 | Constant True 58 | 59 | TAnd -> 60 | And (Constant False) (Constant False) 61 | 62 | TOr -> 63 | Or (Constant False) (Constant False) 64 | 65 | TNot -> 66 | Not (Constant False) 67 | 68 | 69 | {-| Convert an `ExpressionType` to a string by using the default `toString` and 70 | then dropping the leading 'T'. 71 | -} 72 | typeString : ExpressionType -> String 73 | typeString = 74 | toString >> String.dropLeft 1 75 | 76 | 77 | {-| Recursively create HTML for editing a given Boolean expression. 78 | -} 79 | expressionWidget : Expression -> Html Expression 80 | expressionWidget expression = 81 | let 82 | -- List of expression types to display in combo boxes. 83 | expressionTypes = 84 | [ TFalse, TTrue, TAnd, TOr, TNot ] 85 | 86 | -- Helper function for creating an expression type combo box: whenever 87 | -- a new expression type is selected, a new dummy expression is created 88 | -- that can then be further edited. 89 | comboBox : ExpressionType -> Html Expression 90 | comboBox expressionType = 91 | InputWidget.comboBox [] typeString expressionTypes expressionType 92 | |> Html.map defaultExpressionForType 93 | in 94 | case expression of 95 | -- The input widget for the constant false value is simply a combo 96 | -- box set to 'False'. 97 | Constant False -> 98 | comboBox TFalse 99 | 100 | -- The input widget for the constant true value is simply a combo 101 | -- box set to 'True'. 102 | Constant True -> 103 | comboBox TTrue 104 | 105 | -- For a negated expression, the input widget is a combo box set to 106 | -- 'Not' and then another expression widget for the negated 107 | -- subexpression. If the subexpression widget is edited, it will 108 | -- emit a message with the new subexpression; `Html.map Not` is used 109 | -- to negate that subexpression to form the updated top-level `Not` 110 | -- expression. (If the combo box is edited instead, the entire 111 | -- top-level expression will be wiped out and replaced by a 'dummy' 112 | -- expression of the newly selected type.) 113 | Not subExpression -> 114 | Html.span [] 115 | [ Html.text "(" 116 | , comboBox TNot 117 | , expressionWidget subExpression |> Html.map Not 118 | , Html.text ")" 119 | ] 120 | 121 | -- Editing an 'And' expression is similar to 'Not' but a bit more 122 | -- complex. Note how `Html.map` is used to combine a new value for 123 | -- one operand with the existing value of the other operand to 124 | -- create a new top-level `And` expression. 125 | And firstExpression secondExpression -> 126 | Html.span [] 127 | [ Html.text "(" 128 | , expressionWidget firstExpression 129 | |> Html.map 130 | (\newFirstExpression -> 131 | And newFirstExpression secondExpression 132 | ) 133 | , comboBox TAnd 134 | , expressionWidget secondExpression 135 | |> Html.map 136 | (\newSecondExpression -> 137 | And firstExpression newSecondExpression 138 | ) 139 | , Html.text ")" 140 | ] 141 | 142 | -- 'Or' is pretty much the same as 'And'. Note that since there is 143 | -- no mapping used on the combo box, editing the expression type 144 | -- completely blows away the current expression and replaces it with 145 | -- a new dummy one of the selected type; a more sophisticated 146 | -- implementation might do something like retain the same left and 147 | -- right hand operands if an 'Or' is switched to an 'And'. 148 | Or firstExpression secondExpression -> 149 | Html.span [] 150 | [ Html.text "(" 151 | , expressionWidget firstExpression 152 | |> Html.map 153 | (\newFirstExpression -> 154 | Or newFirstExpression secondExpression 155 | ) 156 | , comboBox TOr 157 | , expressionWidget secondExpression 158 | |> Html.map 159 | (\newSecondExpression -> 160 | Or firstExpression newSecondExpression 161 | ) 162 | , Html.text ")" 163 | ] 164 | 165 | 166 | 167 | -- Program 168 | 169 | 170 | {-| Show the current expression as well as that expression evaluated to a 171 | Boolean value. 172 | -} 173 | view : Expression -> Html Expression 174 | view expression = 175 | Html.div [] 176 | [ Html.div [] [ expressionWidget expression ] 177 | , Html.div [] [ Html.text (toString (evaluate expression)) ] 178 | ] 179 | 180 | 181 | main : Program Never Expression Expression 182 | main = 183 | Html.beginnerProgram 184 | { model = Or (Constant False) (Constant True) 185 | , view = view 186 | , update = always 187 | } 188 | -------------------------------------------------------------------------------- /src/Kintail/InputWidget.elm: -------------------------------------------------------------------------------- 1 | module Kintail.InputWidget 2 | exposing 3 | ( checkbox 4 | , radioButton 5 | , lineEdit 6 | , comboBox 7 | , slider 8 | , custom 9 | ) 10 | 11 | {-| Functions for creating input widgets of the general form `a -> Html a`. You 12 | should use `Html.map` to convert the produced messages to the message type used 13 | by your app, and the new value should generally be stored in your model and fed 14 | back in to the `view` function. This means that the value emitted from a given 15 | fragment of HTML will generally become the input value used to create that same 16 | fragment of HTML the next time your `view` function is called. 17 | 18 | @docs checkbox, radioButton, lineEdit, comboBox, slider 19 | 20 | ## Advanced 21 | 22 | @docs custom 23 | -} 24 | 25 | import String 26 | import Array exposing (Array) 27 | import Json.Encode as Encode exposing (Value) 28 | import Json.Decode as Decode exposing (Decoder) 29 | import Html exposing (Html) 30 | import Html.Attributes as Attributes 31 | import Html.Events as Events 32 | 33 | 34 | {-| Create a `` element with the given attributes and 35 | current value, and which produces `Bool` messages with the new value whenever 36 | the checkbox is clicked. 37 | 38 | See the [Embedding.elm](https://github.com/kintail/input-widget/blob/1.0.6/examples/Embedding.elm) 39 | example for sample usage. 40 | -} 41 | checkbox : List (Html.Attribute Bool) -> Bool -> Html Bool 42 | checkbox attributes value = 43 | Html.input 44 | (Attributes.type_ "checkbox" 45 | :: Attributes.checked value 46 | :: Events.onCheck identity 47 | :: attributes 48 | ) 49 | [] 50 | 51 | 52 | {-| Create a `` element with the given attributes. When the 53 | radio button is checked, it will send a message equal to the first given value; 54 | it will be displayed as currently checked if the two given values are equal to 55 | each other. 56 | 57 | To create a set of mutually-exclusive radio buttons (the usual case), call this 58 | function multiple times, passing a different first value each time (the value to 59 | be selected if that radio button is clicked) but the same second value (the 60 | currently selected value). This way, only the radio button whose value matches 61 | the currently selected value will be displayed as checked. When any other radio 62 | button is clicked, it will emit a message equal to its specific value, so that 63 | you can update the currently selected value to that value. 64 | 65 | See the [RadioButton.elm](https://github.com/kintail/input-widget/blob/1.0.6/examples/RadioButton.elm) 66 | example for sample usage. 67 | -} 68 | radioButton : List (Html.Attribute a) -> a -> a -> Html a 69 | radioButton attributes value currentValue = 70 | Html.input 71 | (Attributes.type_ "radio" 72 | :: Attributes.checked (value == currentValue) 73 | :: Events.onCheck (always value) 74 | :: attributes 75 | ) 76 | [] 77 | 78 | 79 | {-| Create a simple `` element with the given attributes and text to 80 | display. A message will be sent with the updated text whenever the text is 81 | edited. 82 | 83 | See the [Validation.elm](https://github.com/kintail/input-widget/blob/1.0.6/examples/Validation.elm) 84 | example for sample usage. 85 | -} 86 | lineEdit : List (Html.Attribute String) -> String -> Html String 87 | lineEdit attributes value = 88 | Html.input 89 | (Attributes.value value 90 | :: Events.onInput identity 91 | :: attributes 92 | ) 93 | [] 94 | 95 | 96 | {-| Create a `` 97 | element will be populated by `