├── .envrc ├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── PUBLISHING.md ├── README.md ├── docs ├── eagle.svg ├── giraffe.svg ├── index.html └── monkey.svg ├── elm.json ├── examples ├── .gitignore ├── AnimalExample.elm ├── DropboxExample.elm ├── LICENSE.md ├── Main.elm ├── RecursionExample.elm ├── eagle.svg ├── elm.json ├── giraffe.svg └── monkey.svg ├── package.json ├── screenshot.gif ├── shell.nix ├── src └── Debug │ └── Control.elm └── tests ├── .gitignore └── Controls ├── ComplexChoiceTest.elm ├── DateTest.elm ├── ListTest.elm ├── RecursionTest.elm ├── SimpleChoiceTest.elm └── StringTest.elm /.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - 'release/*' 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: current 21 | - run: npm install 22 | - run: npm test 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff/ 2 | /node_modules/ 3 | /package-lock.json 4 | /documentation.json 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | node_js: "12" 5 | 6 | cache: 7 | directories: 8 | - node_modules 9 | 10 | install: 11 | - npm install 12 | 13 | script: 14 | - npm test 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.2.3 2 | 3 | - `datetimepicker-legacy` is replaced with [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local) [#16](https://github.com/avh4/elm-debug-controls/pull/16) 4 | - Record views no longer show an extra leading comma [#15](https://github.com/avh4/elm-debug-controls/pull/15) 5 | 6 | 7 | ## 2.2.2 8 | 9 | - Bump version bounds for elm-css dependency 10 | 11 | 12 | ## 2.2.1 13 | 14 | - Record field inputs now have labels (for accessibility) 15 | 16 | 17 | ## 2.2.0 18 | 19 | - Added `stringTextarea` control 20 | 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Dev environment 3 | 4 | 1. One of: 5 | - If you use [`nix-direnv`](https://github.com/nix-community/nix-direnv), simply `direnv allow` 6 | - Else if you use [nix](https://nixos.org/), `nix-shell` 7 | - Else install a stable version of [nodejs](https://nodejs.org/) via whatever means you prefer 8 | 2. `npm install` 9 | 3. `npm test` 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016-2019 Aaron VonderHaar 2 | Copyright 2019 Tessa Kelly 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /PUBLISHING.md: -------------------------------------------------------------------------------- 1 | - `git fetch` 2 | - `git switch -c release/$VERSION origin/main` 3 | - `npx elm bump` 4 | - review `CHANGELOG.md` 5 | - `npm run-script build-site` 6 | - Stage the changes 7 | - `git commit -m "Prepare $VERSION release"` 8 | - `git push -u origin HEAD` 9 | - Wait for CI, and approve the PR 10 | - `git tag $VERSION` 11 | - `git push origin $VERSION` 12 | - `npx elm publish` 13 | - `Merge the PR` 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/avh4/elm-debug-controls.svg?branch=master)](https://travis-ci.org/avh4/elm-debug-controls) 2 | [![Latest Version](https://img.shields.io/elm-package/v/avh4/elm-debug-controls.svg?label=version)](https://package.elm-lang.org/packages/avh4/elm-debug-controls/latest/) 3 | 4 | # elm-debug-controls 5 | 6 | This package helps you easily build interactive UIs for complex data structures. 7 | The resulting controls are not meant for building end-user UIs, 8 | but they are useful for quickly building debugging consoles, documentation, and style guides. 9 | 10 | ## Demo 11 | 12 | https://avh4.github.io/elm-debug-controls/ 13 | 14 | ## Usage 15 | 16 | Suppose we have an Elm data structure like the following and want to create a simple debugging tool to experiment with different values: 17 | 18 | ```elm 19 | import Time 20 | 21 | type alias UploadRequest = 22 | { path : String 23 | , mode : WriteMode 24 | , autorename : Bool 25 | , clientModified : Maybe Time.Posix 26 | , mute : Bool 27 | , content : String 28 | } 29 | 30 | type WriteMode 31 | = Add 32 | | Overwrite 33 | | Update String 34 | ``` 35 | 36 | Using `elm-debug-controls`, we can quickly create an interactive UI to create `UploadRequest` values: 37 | 38 | ```sh 39 | elm install avh4/elm-debug-controls 40 | ``` 41 | 42 | ```elm 43 | import Debug.Control exposing (bool, choice, field, map, record, string, value) 44 | 45 | type alias Model = 46 | { ... 47 | , uploadRequest : Debug.Control.Control UploadRequest 48 | } 49 | 50 | init : Model 51 | init = 52 | { ... 53 | , uploadRequest = 54 | record UploadRequest 55 | |> field "path" (string "/demo.txt") 56 | |> field "mode" 57 | (choice 58 | [ ( "Add", value Add ) 59 | , ( "Overwrite", value Overwrite ) 60 | , ( "Update rev", map Update <| string "123abcdef" ) 61 | ] 62 | ) 63 | |> field "autorename" (bool False) 64 | |> field "clientModified" 65 | (maybe False <| date Time.utc <| Time.millisToPosix 0) 66 | |> field "mute" (bool False) 67 | |> field "content" (string "HELLO.") 68 | } 69 | ``` 70 | 71 | 72 | Now we can hook the control up to our view: 73 | 74 | ```elm 75 | type Msg 76 | = ... 77 | | ChangeUploadRequest (Debug.Control.Control UploadRequest) 78 | 79 | update : Msg -> Model -> Model 80 | update msg model = 81 | case msg of 82 | ... 83 | ChangeUploadRequest uploadRequest -> 84 | { model | uploadRequest = uploadRequest } 85 | 86 | view : Model -> Html Msg 87 | view model = 88 | ... 89 | Debug.Control.view ChangeUploadRequest model.uploadRequest 90 | ``` 91 | 92 | We now have an interactive UI that looks like this: 93 | 94 | ![Screen capture of the interactive UI](https://github.com/avh4/elm-debug-controls/raw/master/screenshot.gif) 95 | 96 | Finally, we can use the `UploadResponse` value elsewhere in our program with: 97 | 98 | ```elm 99 | Debug.Control.currentValue model.uploadRequest 100 | ``` 101 | -------------------------------------------------------------------------------- /docs/giraffe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | imagebot_2 19 | 20 | image/svg+xmlOpenclipartCartoon giraffe2008-07-07T11:40:13Cartoon giraffe sitting, front view. Nice 20 minute tutorial video for drawing the giraffe in Inkscape <a target="_blank" href="https://www.youtube.com/watch?v=qZt9J_AUYZI">https://www.youtube.com/watch?v=qZt9J_AUYZI</a>https://openclipart.org/detail/17628/cartoon-giraffe-by-lemmlinglemmlinganimalcartooncolourfunnygiraffemammal 21 | -------------------------------------------------------------------------------- /docs/monkey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 23 | 26 | 31 | 36 | 38 | 47 | 52 | 57 | 59 | 69 | 79 | 89 | 91 | 108 | 113 | 117 | 123 | 129 | 134 | 140 | 146 | 152 | 154 | 156 | 158 | 160 | 162 | image/svg+xml 165 | 168 | 171 | 173 | 176 | Openclipart 179 | 181 | 183 | Monkey head 186 | 2008-02-19T12:16:18 189 | A funny monkey head 192 | https://openclipart.org/detail/14513/monkey-head-by-nicubunu 195 | 197 | 199 | nicubunu 202 | 204 | 206 | 208 | 210 | animal 213 | funny 216 | mammal 219 | monkey 222 | 224 | 226 | 228 | 231 | 234 | 237 | 240 | 242 | 244 | 246 | 248 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "avh4/elm-debug-controls", 4 | "summary": "Easily create interactive UIs for complex data structures", 5 | "license": "BSD-3-Clause", 6 | "version": "2.2.3", 7 | "exposed-modules": [ 8 | "Debug.Control" 9 | ], 10 | "elm-version": "0.19.0 <= v < 0.20.0", 11 | "dependencies": { 12 | "elm/core": "1.0.0 <= v < 2.0.0", 13 | "elm/html": "1.0.0 <= v < 2.0.0", 14 | "elm/json": "1.1.3 <= v < 2.0.0", 15 | "elm/time": "1.0.0 <= v < 2.0.0", 16 | "justinmimbs/time-extra": "1.1.0 <= v < 2.0.0" 17 | }, 18 | "test-dependencies": { 19 | "elm-explorations/test": "2.0.0 <= v < 3.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff/ 2 | /index.html 3 | -------------------------------------------------------------------------------- /examples/AnimalExample.elm: -------------------------------------------------------------------------------- 1 | module AnimalExample exposing (Animal(..), debugControl, view, viewAnimal) 2 | 3 | import Debug.Control exposing (Control, choice, list, map, string, value, values) 4 | import Html exposing (Html) 5 | import Html.Attributes as Html exposing (style) 6 | import String 7 | 8 | 9 | type Animal 10 | = Monkey 11 | | Giraffe 12 | | Eagle 13 | | Chimera (List Animal) 14 | | CustomAnimal String 15 | 16 | 17 | debugControl : Control (Maybe Animal) 18 | debugControl = 19 | let 20 | basicAnimal = 21 | values Debug.toString [ Monkey, Giraffe, Eagle ] 22 | in 23 | choice 24 | [ ( "Animal", map Just basicAnimal ) 25 | , ( "Chimera", map (Just << Chimera) (list basicAnimal) ) 26 | , ( "Custom", map (Just << CustomAnimal) (string "Zebra") ) 27 | , ( "Nothing", value Nothing ) 28 | ] 29 | 30 | 31 | viewAnimal : Int -> Maybe Animal -> Html msg 32 | viewAnimal size animal = 33 | let 34 | svg url = 35 | Html.img 36 | [ Html.style "width" (String.fromInt size ++ "px") 37 | , Html.style "height" (String.fromInt size ++ "px") 38 | , Html.style "overflow" "hidden" 39 | , Html.style "vertical-align" "bottom" 40 | , Html.src url 41 | , Html.width size 42 | ] 43 | [] 44 | 45 | letters background color string = 46 | Html.div 47 | [ Html.style "width" (String.fromInt size ++ "px") 48 | , Html.style "height" (String.fromInt size ++ "px") 49 | , Html.style "background-color" background 50 | , Html.style "color" color 51 | , Html.style "overflow" "hidden" 52 | , Html.style "text-overflow" "ellipsis" 53 | , Html.style "line-height" (String.fromInt size ++ "px") 54 | , Html.style "text-align" "center" 55 | , Html.style "font-family" "sans-serif" 56 | ] 57 | [ Html.text string ] 58 | in 59 | case animal of 60 | Just Monkey -> 61 | svg "monkey.svg" 62 | 63 | Just Giraffe -> 64 | svg "giraffe.svg" 65 | 66 | Just Eagle -> 67 | svg "eagle.svg" 68 | 69 | Just (CustomAnimal name) -> 70 | name 71 | |> String.split " " 72 | |> List.map (String.left 1) 73 | |> String.join "" 74 | |> String.toUpper 75 | |> letters "pink" "black" 76 | 77 | Just (Chimera parts) -> 78 | let 79 | scale = 80 | List.length parts 81 | |> toFloat 82 | |> sqrt 83 | |> ceiling 84 | in 85 | Html.div 86 | [ Html.style "width" (String.fromInt size ++ "px") 87 | , Html.style "height" (String.fromInt size ++ "px") 88 | , Html.style "background-color" "lightgreen" 89 | , Html.style "line-height" "0" 90 | ] 91 | (List.map (Just >> viewAnimal (size // scale)) parts) 92 | 93 | Nothing -> 94 | letters "lightgray" "gray" "N/A" 95 | 96 | 97 | view : Control (Maybe Animal) -> Html (Control (Maybe Animal)) 98 | view control = 99 | let 100 | h title = 101 | Html.h2 [] [ Html.text title ] 102 | 103 | showData data = 104 | Html.table [] 105 | [ Html.tr [] 106 | [ Html.td [] [ viewAnimal 50 data ] 107 | , Html.td [] 108 | [ Html.code 109 | [ style "word-break" "break-all" ] 110 | [ Html.text (Debug.toString data) ] 111 | ] 112 | ] 113 | ] 114 | in 115 | Html.div [] 116 | [ h "Example data structure" 117 | , Html.pre [] 118 | [ Html.text """type Animal 119 | = Monkey 120 | | Giraffe 121 | | Eagle 122 | | Chimera (List Animal) 123 | | CustomAnimal String""" 124 | ] 125 | , h "Interactive control" 126 | , Debug.Control.view identity control 127 | , showData (Debug.Control.currentValue control) 128 | , h "All possible values" 129 | , List.map showData (Debug.Control.allValues control) 130 | |> Html.div [] 131 | , Html.hr [] [] 132 | , Html.a [ Html.href "https://github.com/avh4/elm-debug-controls/blob/master/examples/LICENSE.md#images" ] 133 | [ Html.text "Image credits" ] 134 | ] 135 | -------------------------------------------------------------------------------- /examples/DropboxExample.elm: -------------------------------------------------------------------------------- 1 | module DropboxExample exposing (Model, Msg, init, update, view) 2 | 3 | import Debug.Control as Control exposing (Control) 4 | import Html exposing (..) 5 | import Time 6 | 7 | 8 | type alias Model = 9 | { download : Control DownloadRequest 10 | , upload : Control UploadRequest 11 | } 12 | 13 | 14 | type alias DownloadRequest = 15 | { path : String 16 | } 17 | 18 | 19 | type WriteMode 20 | = Add 21 | | Overwrite 22 | | Update String 23 | 24 | 25 | type alias UploadRequest = 26 | { path : String 27 | , mode : WriteMode 28 | , autorename : Bool 29 | , clientModified : Maybe Time.Posix 30 | , mute : Bool 31 | , content : String 32 | } 33 | 34 | 35 | init : Model 36 | init = 37 | { download = 38 | Control.record DownloadRequest 39 | |> Control.field "path" (Control.string "/demo.txt") 40 | , upload = 41 | Control.record UploadRequest 42 | |> Control.field "path" (Control.string "/demo.txt") 43 | |> Control.field "mode" 44 | (Control.choice 45 | [ ( "Add", Control.value Add ) 46 | , ( "Overwrite", Control.value Overwrite ) 47 | , ( "Update rev", Control.map Update <| Control.string "123abcdef" ) 48 | ] 49 | ) 50 | |> Control.field "autorename" (Control.bool False) 51 | |> Control.field "clientModified" 52 | (Control.maybe False <| Control.date Time.utc <| Time.millisToPosix 0) 53 | |> Control.field "mute" (Control.bool False) 54 | |> Control.field "content" 55 | (Control.stringTextarea 56 | """I do much wonder that one man, seeing how much 57 | another man is a fool when he dedicates his 58 | behaviors to love, will, after he hath laughed at 59 | such shallow follies in others, become the argument 60 | of his own scorn by failing in love: and such a man 61 | is Claudio. I have known when there was no music 62 | with him but the drum and the fife; and now had he 63 | rather hear the tabour and the pipe: I have known 64 | when he would have walked ten mile a-foot to see a 65 | good armour; and now will he lie ten nights awake, 66 | carving the fashion of a new doublet. 67 | """ 68 | ) 69 | } 70 | 71 | 72 | type Msg 73 | = DownloadChange (Control DownloadRequest) 74 | | UploadChange (Control UploadRequest) 75 | 76 | 77 | update : Msg -> Model -> Model 78 | update msg model = 79 | case msg of 80 | DownloadChange download -> 81 | { model | download = download } 82 | 83 | UploadChange upload -> 84 | { model | upload = upload } 85 | 86 | 87 | view : Model -> Html Msg 88 | view model = 89 | Html.div [] 90 | [ h2 [] [ text "Download" ] 91 | , p [] 92 | [ text "We have the following " 93 | , code [] [ text "DownloadRequest" ] 94 | , text " data type:" 95 | ] 96 | , pre [] 97 | [ text """type alias DownloadRequest = 98 | { path : String 99 | }""" 100 | ] 101 | , p [] [ text "An interactive control can be created with the following code:" ] 102 | , pre [] [ text """import Debug.Control exposing (field, record, string) 103 | 104 | record DownloadRequest 105 | |> field "path" (string "/demo.txt")""" ] 106 | , Control.view DownloadChange model.download 107 | , h2 [] [ text "Upload" ] 108 | , p [] 109 | [ text "We have the following " 110 | , code [] [ text "DownloadRequest" ] 111 | , text " data type:" 112 | ] 113 | , pre [] 114 | [ text """type alias UploadRequest = 115 | { path : String 116 | , mode : WriteMode 117 | , autorename : Bool 118 | , clientModified : Maybe Time.Posix 119 | , mute : Bool 120 | , content : String 121 | } 122 | 123 | type WriteMode 124 | = Add 125 | | Overwrite 126 | | Update String""" 127 | ] 128 | , p [] [ text "An interactive control can be created with the following code:" ] 129 | , pre [] [ text """import Debug.Control exposing (bool, choice, field, map, record, string, value) 130 | 131 | record UploadRequest 132 | |> field "path" (string "/demo.txt") 133 | |> field "mode" 134 | (choice 135 | [ ( "Add", value Add ) 136 | , ( "Overwrite", value Overwrite ) 137 | , ( "Update rev", map Update <| string "123abcdef" ) 138 | ] 139 | ) 140 | |> field "autorename" (bool False) 141 | |> field "clientModified" 142 | (maybe False <| date Tim.utc <| Time.millisToPosix 0) 143 | |> field "mute" (bool False) 144 | |> field "content" 145 | (Control.stringTextarea 146 | \"\"\"I do much wonder that one man, seeing how much 147 | another man is a fool when he dedicates his 148 | behaviors to love, will, after he hath laughed at 149 | such shallow follies in others, become the argument 150 | of his own scorn by failing in love: and such a man 151 | is Claudio. I have known when there was no music 152 | with him but the drum and the fife; and now had he 153 | rather hear the tabour and the pipe: I have known 154 | when he would have walked ten mile a-foot to see a 155 | good armour; and now will he lie ten nights awake, 156 | carving the fashion of a new doublet. 157 | \"\"\" 158 | ) 159 | """ ] 160 | , Control.view UploadChange model.upload 161 | ] 162 | -------------------------------------------------------------------------------- /examples/LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | ## Images 3 | 4 | License: https://openclipart.org/share 5 | 6 | Unlimited Commercial Use 7 | 8 | We try to make it clear that you may use all clipart from Openclipart even for [unlimited commercial use](https://openclipart.org/unlimited-commercial-use-clipart). We believe that giving away our images is a great way to share with the world our talents and that will come back around in a better form. 9 | 10 | May I Use Openclipart for? 11 | 12 | We put together a [small chart of as many possibilities and questions](https://openclipart.org/may-clipart-be-used-comparison) we have heard from people asking how they may use Openclipart. If you have an additional question, please email love@openclipart.org. 13 | 14 | All Clipart are Released into the Public Domain. 15 | 16 | Each artist at Openclipart [releases all rights](http://creativecommons.org/publicdomain/zero/1.0/) to the images they share at Openclipart. The reason is so that there is [no friction](http://creativecommons.org/publicdomain/zero/1.0/) in using and sharing images authors make available at this website so that each artist might also receive the same benefit in using other artists clipart totally for any possible reason. 17 | 18 | Images from: 19 | - giraffe.svg https://openclipart.org/detail/17628/cartoon-giraffe 20 | - eagle.svg https://openclipart.org/detail/98821/eagle 21 | - monkey.svg: https://openclipart.org/detail/14513/monkey-head 22 | -------------------------------------------------------------------------------- /examples/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (Model, Msg(..), WhichExample(..), choiceControl, initialModel, main, stringControl, update, view) 2 | 3 | import AnimalExample 4 | import BeautifulExample 5 | import Color 6 | import Debug.Control exposing (Control, choice, list, map, string, value, values) 7 | import DropboxExample 8 | import Html exposing (..) 9 | import Html.Events exposing (onClick) 10 | import RecursionExample 11 | 12 | 13 | stringControl : Control String 14 | stringControl = 15 | string "default value" 16 | 17 | 18 | choiceControl : Control Bool 19 | choiceControl = 20 | choice 21 | [ ( "YES", value True ) 22 | , ( "NO", value False ) 23 | ] 24 | 25 | 26 | type WhichExample 27 | = Animal 28 | | Dropbox 29 | | SimpleControls 30 | | RecursionExample 31 | 32 | 33 | type alias Model = 34 | { which : WhichExample 35 | , animal : Control (Maybe AnimalExample.Animal) 36 | , dropbox : DropboxExample.Model 37 | , choice : Control Bool 38 | , string : Control String 39 | , recursionExample : Control RecursionExample.RecursiveType 40 | } 41 | 42 | 43 | initialModel : Model 44 | initialModel = 45 | { which = Dropbox 46 | , animal = AnimalExample.debugControl 47 | , dropbox = DropboxExample.init 48 | , choice = choiceControl 49 | , string = stringControl 50 | , recursionExample = RecursionExample.init 51 | } 52 | 53 | 54 | type Msg 55 | = SwitchTo WhichExample 56 | | ChangeAnimal (Control (Maybe AnimalExample.Animal)) 57 | | DropboxMsg DropboxExample.Msg 58 | | ChangeChoice (Control Bool) 59 | | ChangeString (Control String) 60 | | ChangeRecursiveChoice (Control RecursionExample.RecursiveType) 61 | 62 | 63 | update : Msg -> Model -> Model 64 | update msg model = 65 | case msg of 66 | SwitchTo which -> 67 | { model | which = which } 68 | 69 | ChangeAnimal animal -> 70 | { model | animal = animal } 71 | 72 | DropboxMsg dropboxMsg -> 73 | { model | dropbox = DropboxExample.update dropboxMsg model.dropbox } 74 | 75 | ChangeChoice choice -> 76 | { model | choice = choice } 77 | 78 | ChangeString string -> 79 | { model | string = string } 80 | 81 | ChangeRecursiveChoice choice -> 82 | { model | recursionExample = choice } 83 | 84 | 85 | view : Model -> Html Msg 86 | view model = 87 | div [] 88 | [ button [ onClick (SwitchTo Dropbox) ] [ text "Records example (Upload/Download)" ] 89 | , br [] [] 90 | , button [ onClick (SwitchTo Animal) ] [ text "Union type example (Animal)" ] 91 | , br [] [] 92 | , button [ onClick (SwitchTo SimpleControls) ] [ text "Simple controls" ] 93 | , br [] [] 94 | , button [ onClick (SwitchTo RecursionExample) ] [ text "Recursion example" ] 95 | , br [] [] 96 | , case model.which of 97 | Animal -> 98 | AnimalExample.view model.animal 99 | |> Html.map ChangeAnimal 100 | 101 | Dropbox -> 102 | DropboxExample.view model.dropbox 103 | |> Html.map DropboxMsg 104 | 105 | SimpleControls -> 106 | div [] 107 | [ h3 [] [ text "choice" ] 108 | , Debug.Control.view ChangeChoice model.choice 109 | , h3 [] [ text "string" ] 110 | , Debug.Control.view ChangeString model.string 111 | ] 112 | 113 | RecursionExample -> 114 | RecursionExample.view model.recursionExample 115 | |> Html.map ChangeRecursiveChoice 116 | ] 117 | 118 | 119 | main : Program () Model Msg 120 | main = 121 | BeautifulExample.sandbox 122 | { title = "elm-debug-controls" 123 | , details = Just """This package helps you easily create interactive and exhaustive views of complex data structures.""" 124 | , color = Just Color.brown 125 | , maxWidth = 600 126 | , githubUrl = Just "https://github.com/avh4/elm-debug-controls" 127 | , documentationUrl = Just "http://package.elm-lang.org/packages/avh4/elm-debug-controls/latest" 128 | } 129 | { init = initialModel 130 | , update = update 131 | , view = view 132 | } 133 | -------------------------------------------------------------------------------- /examples/RecursionExample.elm: -------------------------------------------------------------------------------- 1 | module RecursionExample exposing (RecursiveType, init, view) 2 | 3 | import Debug.Control exposing (Control, choice, lazy, list, map, string, value, values) 4 | import Html exposing (Html) 5 | import Html.Attributes as Html exposing (style) 6 | import String 7 | 8 | 9 | type RecursiveType 10 | = RecursiveType (Maybe RecursiveType) 11 | 12 | 13 | init : Control RecursiveType 14 | init = 15 | choice 16 | [ ( "No child", value Nothing ) 17 | , ( "child", lazy (\() -> init) |> map Just ) 18 | ] 19 | |> map RecursiveType 20 | 21 | 22 | view : Control RecursiveType -> Html (Control RecursiveType) 23 | view control = 24 | let 25 | h title = 26 | Html.h2 [] [ Html.text title ] 27 | in 28 | Html.div [] 29 | [ h "Recursively-defined values" 30 | , Debug.Control.view identity control 31 | ] 32 | -------------------------------------------------------------------------------- /examples/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | ".", 5 | "../src" 6 | ], 7 | "elm-version": "0.19.1", 8 | "dependencies": { 9 | "direct": { 10 | "avh4/elm-beautiful-example": "2.0.1", 11 | "avh4/elm-color": "1.0.0", 12 | "elm/core": "1.0.5", 13 | "elm/html": "1.0.0", 14 | "elm/json": "1.1.3", 15 | "elm/time": "1.0.0", 16 | "justinmimbs/time-extra": "1.1.1" 17 | }, 18 | "indirect": { 19 | "elm/browser": "1.0.2", 20 | "elm/parser": "1.1.0", 21 | "elm/svg": "1.0.1", 22 | "elm/url": "1.0.0", 23 | "elm/virtual-dom": "1.0.3", 24 | "justinmimbs/date": "4.0.1" 25 | } 26 | }, 27 | "test-dependencies": { 28 | "direct": {}, 29 | "indirect": {} 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/giraffe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | imagebot_2 19 | 20 | image/svg+xmlOpenclipartCartoon giraffe2008-07-07T11:40:13Cartoon giraffe sitting, front view. Nice 20 minute tutorial video for drawing the giraffe in Inkscape <a target="_blank" href="https://www.youtube.com/watch?v=qZt9J_AUYZI">https://www.youtube.com/watch?v=qZt9J_AUYZI</a>https://openclipart.org/detail/17628/cartoon-giraffe-by-lemmlinglemmlinganimalcartooncolourfunnygiraffemammal 21 | -------------------------------------------------------------------------------- /examples/monkey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 23 | 26 | 31 | 36 | 38 | 47 | 52 | 57 | 59 | 69 | 79 | 89 | 91 | 108 | 113 | 117 | 123 | 129 | 134 | 140 | 146 | 152 | 154 | 156 | 158 | 160 | 162 | image/svg+xml 165 | 168 | 171 | 173 | 176 | Openclipart 179 | 181 | 183 | Monkey head 186 | 2008-02-19T12:16:18 189 | A funny monkey head 192 | https://openclipart.org/detail/14513/monkey-head-by-nicubunu 195 | 197 | 199 | nicubunu 202 | 204 | 206 | 208 | 210 | animal 213 | funny 216 | mammal 219 | monkey 222 | 224 | 226 | 228 | 231 | 234 | 237 | 240 | 242 | 244 | 246 | 248 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "elm make && elm-test && npm run-script build-example && elm make --docs=documentation.json && npm run-script check && elm diff", 4 | "build-example": "(cd examples && elm make Main.elm)", 5 | "check": "elm-format --validate .", 6 | "build-site": "mkdir -p docs && cp examples/*.svg docs/ && npm run-script build-example && cp examples/index.html docs/" 7 | }, 8 | "devDependencies": { 9 | "elm": "^0.19.1-5", 10 | "elm-format": "^0.8.5", 11 | "elm-test": "^0.19.1-revision10" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avh4/elm-debug-controls/a0245e506f9bdce81e4313d9503b1b972a54cae3/screenshot.gif -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | 3 | pkgs.mkShell { 4 | buildInputs = with pkgs; [ nodejs nodePackages.npm-check-updates nixfmt ]; 5 | } 6 | -------------------------------------------------------------------------------- /src/Debug/Control.elm: -------------------------------------------------------------------------------- 1 | module Debug.Control exposing 2 | ( Control 3 | , value 4 | , bool, string, stringTextarea, date 5 | , values, maybe, choice, list, record, field 6 | , map 7 | , view, currentValue, allValues 8 | , lazy 9 | ) 10 | 11 | {-| Create interactive controls for complex data structures. 12 | 13 | @docs Control 14 | @docs value 15 | @docs bool, string, stringTextarea, date 16 | @docs values, maybe, choice, list, record, field 17 | @docs map 18 | 19 | @docs view, currentValue, allValues 20 | @docs lazy 21 | 22 | -} 23 | 24 | import Html exposing (Html) 25 | import Html.Attributes 26 | import Html.Events 27 | import Json.Decode 28 | import String 29 | import Time 30 | import Time.Extra 31 | 32 | 33 | {-| An interactive control that produces a value `a`. 34 | -} 35 | type Control a 36 | = Control 37 | { currentValue : () -> a 38 | , allValues : () -> List a 39 | , view : () -> ControlView a 40 | } 41 | 42 | 43 | type ControlView a 44 | = NoView 45 | | SingleView (Html (Control a)) 46 | | FieldViews (List ( String, Html (Control a) )) 47 | 48 | 49 | {-| A `Control` that has a static value (and no UI). 50 | -} 51 | value : a -> Control a 52 | value initial = 53 | Control 54 | { currentValue = \() -> initial 55 | , allValues = \() -> [ initial ] 56 | , view = \() -> NoView 57 | } 58 | 59 | 60 | {-| A `Control` that chooses between a list of values with a dropdown UI. 61 | 62 | The first value will be the initial value. 63 | 64 | -} 65 | values : (a -> String) -> List a -> Control a 66 | values toString choices = 67 | choice (List.map (\x -> ( toString x, value x )) choices) 68 | 69 | 70 | {-| A `Control` that wraps another control in a `Maybe`, which a checkbox UI. 71 | 72 | The `Bool` parameter is the initial value, where `False` is `Nothing`, 73 | and `True` is `Just` with the value of the nested control. 74 | 75 | -} 76 | maybe : Bool -> Control a -> Control (Maybe a) 77 | maybe isJust (Control control) = 78 | Control 79 | { currentValue = 80 | \() -> 81 | if isJust then 82 | Just (control.currentValue ()) 83 | 84 | else 85 | Nothing 86 | , allValues = 87 | \() -> 88 | Nothing 89 | :: List.map Just (control.allValues ()) 90 | , view = 91 | \() -> 92 | SingleView <| 93 | Html.span 94 | [ Html.Attributes.style "white-space" "nowrap" 95 | ] 96 | [ Html.input 97 | [ Html.Attributes.type_ "checkbox" 98 | , Html.Events.onCheck (\a -> maybe a (Control control)) 99 | , Html.Attributes.checked isJust 100 | ] 101 | [] 102 | , Html.text " " 103 | , if isJust then 104 | view_ (maybe isJust) (Control control) 105 | 106 | else 107 | Html.text "Nothing" 108 | ] 109 | } 110 | 111 | 112 | {-| A `Control` that toggles a `Bool` with a checkbox UI. 113 | -} 114 | bool : Bool -> Control Bool 115 | bool initialValue = 116 | Control 117 | { currentValue = \() -> initialValue 118 | , allValues = 119 | \() -> 120 | [ initialValue 121 | , not initialValue 122 | ] 123 | , view = 124 | \() -> 125 | SingleView <| 126 | Html.span [] 127 | [ Html.input 128 | [ Html.Attributes.type_ "checkbox" 129 | , Html.Events.onCheck bool 130 | , Html.Attributes.checked initialValue 131 | ] 132 | [] 133 | , Html.text " " 134 | , case initialValue of 135 | True -> 136 | Html.text "True" 137 | 138 | False -> 139 | Html.text "False" 140 | ] 141 | } 142 | 143 | 144 | {-| A `Control` that allows text input. 145 | -} 146 | string : String -> Control String 147 | string initialValue = 148 | Control 149 | { currentValue = \() -> initialValue 150 | , allValues = 151 | \() -> 152 | [ initialValue 153 | , "" 154 | , "short" 155 | , "Longwordyesverylongwithnospacessupercalifragilisticexpialidocious" 156 | , "Long text lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum." 157 | ] 158 | , view = 159 | \() -> 160 | SingleView <| 161 | Html.input 162 | [ Html.Attributes.value initialValue 163 | , Html.Events.onInput string 164 | ] 165 | [] 166 | } 167 | 168 | 169 | {-| A `Control` that allows multiline text input. 170 | -} 171 | stringTextarea : String -> Control String 172 | stringTextarea initialValue = 173 | Control 174 | { currentValue = \() -> initialValue 175 | , allValues = 176 | \() -> 177 | [ initialValue 178 | , "" 179 | , "short" 180 | , "Longwordyesverylongwithnospacessupercalifragilisticexpialidocious" 181 | , """ 182 | Long text lorem ipsum dolor sit amet, consectetur adipiscing elit, 183 | sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 184 | 185 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris 186 | nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in 187 | reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. 188 | Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. 189 | """ 190 | ] 191 | , view = 192 | \() -> 193 | SingleView <| 194 | Html.textarea 195 | [ Html.Attributes.value initialValue 196 | , Html.Events.onInput stringTextarea 197 | ] 198 | [] 199 | } 200 | 201 | 202 | {-| A `Control` that allows a date and time input using the browser's date picker UI. 203 | -} 204 | date : Time.Zone -> Time.Posix -> Control Time.Posix 205 | date zone initialValue = 206 | let 207 | initial = 208 | Time.Extra.posixToParts zone initialValue 209 | 210 | initialDateTime : String 211 | initialDateTime = 212 | String.fromInt initial.year 213 | ++ "-" 214 | ++ twoDigitMonth initial.month 215 | ++ "-" 216 | ++ twoDigit initial.day 217 | ++ "T" 218 | ++ twoDigit initial.hour 219 | ++ ":" 220 | ++ twoDigit initial.minute 221 | in 222 | dateInputField initialDateTime 223 | |> map (toPosix zone >> Maybe.withDefault initialValue) 224 | 225 | 226 | toPosix : Time.Zone -> String -> Maybe Time.Posix 227 | toPosix zone time = 228 | case String.split "-" time of 229 | yearStr :: monthStr :: remainder :: [] -> 230 | case String.split "T" remainder of 231 | dayStr :: rem :: [] -> 232 | case String.split ":" "rem" of 233 | hourStr :: minuteStr :: [] -> 234 | Maybe.map5 235 | (\year month day hour minute -> 236 | Time.Extra.partsToPosix zone 237 | { year = year 238 | , month = month 239 | , day = day 240 | , hour = hour 241 | , minute = minute 242 | , second = 0 243 | , millisecond = 0 244 | } 245 | ) 246 | (String.toInt yearStr) 247 | (monthFromStr monthStr) 248 | (String.toInt dayStr) 249 | (String.toInt hourStr) 250 | (String.toInt minuteStr) 251 | 252 | _ -> 253 | Nothing 254 | 255 | _ -> 256 | Nothing 257 | 258 | _ -> 259 | Nothing 260 | 261 | 262 | monthFromStr : String -> Maybe Time.Month 263 | monthFromStr monthStr = 264 | case monthStr of 265 | "01" -> 266 | Just Time.Jan 267 | 268 | "02" -> 269 | Just Time.Feb 270 | 271 | "03" -> 272 | Just Time.Mar 273 | 274 | "04" -> 275 | Just Time.Apr 276 | 277 | "05" -> 278 | Just Time.May 279 | 280 | "06" -> 281 | Just Time.Jun 282 | 283 | "07" -> 284 | Just Time.Jul 285 | 286 | "08" -> 287 | Just Time.Aug 288 | 289 | "09" -> 290 | Just Time.Sep 291 | 292 | "10" -> 293 | Just Time.Oct 294 | 295 | "11" -> 296 | Just Time.Nov 297 | 298 | "12" -> 299 | Just Time.Dec 300 | 301 | _ -> 302 | Nothing 303 | 304 | 305 | twoDigitMonth : Time.Month -> String 306 | twoDigitMonth month = 307 | case month of 308 | Time.Jan -> 309 | "01" 310 | 311 | Time.Feb -> 312 | "02" 313 | 314 | Time.Mar -> 315 | "03" 316 | 317 | Time.Apr -> 318 | "04" 319 | 320 | Time.May -> 321 | "05" 322 | 323 | Time.Jun -> 324 | "06" 325 | 326 | Time.Jul -> 327 | "07" 328 | 329 | Time.Aug -> 330 | "08" 331 | 332 | Time.Sep -> 333 | "09" 334 | 335 | Time.Oct -> 336 | "10" 337 | 338 | Time.Nov -> 339 | "11" 340 | 341 | Time.Dec -> 342 | "12" 343 | 344 | 345 | twoDigit : Int -> String 346 | twoDigit val = 347 | if val < 10 then 348 | "0" ++ String.fromInt val 349 | 350 | else 351 | String.fromInt val 352 | 353 | 354 | dateInputField : String -> Control String 355 | dateInputField initialValue = 356 | Control 357 | { currentValue = \() -> initialValue 358 | , allValues = \() -> [ initialValue ] -- TODO 359 | , view = 360 | \() -> 361 | SingleView <| 362 | Html.input 363 | [ Html.Attributes.type_ "datetime-local" 364 | , Html.Attributes.pattern "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}" 365 | , Html.Attributes.attribute "value" initialValue 366 | , Html.Events.onInput dateInputField 367 | ] 368 | [] 369 | } 370 | 371 | 372 | {-| A `Control` that chooses between a list of nested controls. 373 | 374 | This will crash if you provide an empty list. 375 | 376 | The first entry will be the initial value. 377 | 378 | -} 379 | choice : List ( String, Control a ) -> Control a 380 | choice choices = 381 | case choices of 382 | [] -> 383 | -- Debug.crash "No choices given" 384 | choice choices 385 | 386 | first :: rest -> 387 | choice_ [] first rest 388 | 389 | 390 | choice_ : 391 | List ( String, Control a ) 392 | -> ( String, Control a ) 393 | -> List ( String, Control a ) 394 | -> Control a 395 | choice_ left current right = 396 | Control 397 | { currentValue = \() -> current |> Tuple.second |> currentValue 398 | , allValues = 399 | \() -> 400 | (List.reverse left ++ [ current ] ++ right) 401 | |> List.map (Tuple.second >> allValues) 402 | |> List.concat 403 | , view = 404 | \() -> 405 | SingleView <| 406 | let 407 | option selected ( label, _ ) = 408 | Html.option 409 | [ Html.Attributes.selected selected ] 410 | [ Html.text label ] 411 | 412 | selectNew i = 413 | let 414 | all = 415 | List.reverse left 416 | ++ [ current ] 417 | ++ right 418 | 419 | left_ = 420 | all 421 | |> List.take i 422 | |> List.reverse 423 | 424 | current_ = 425 | all 426 | |> List.drop i 427 | |> List.head 428 | |> Maybe.withDefault current 429 | 430 | right_ = 431 | all 432 | |> List.drop (i + 1) 433 | in 434 | choice_ left_ current_ right_ 435 | 436 | updateChild new = 437 | choice_ left ( Tuple.first current, new ) right 438 | in 439 | Html.div [] 440 | [ Html.map selectNew <| 441 | Html.select 442 | [ Html.Events.on "change" (Json.Decode.at [ "target", "selectedIndex" ] Json.Decode.int) 443 | ] 444 | <| 445 | List.concat 446 | [ List.map (option False) <| List.reverse left 447 | , [ option True current ] 448 | , List.map (option False) right 449 | ] 450 | , view_ updateChild (Tuple.second current) 451 | ] 452 | } 453 | 454 | 455 | {-| A `Control` that provides a list of selected length. 456 | -} 457 | list : Control a -> Control (List a) 458 | list itemControl = 459 | list_ itemControl 1 0 10 460 | 461 | 462 | list_ : Control a -> Int -> Int -> Int -> Control (List a) 463 | list_ itemControl current min max = 464 | let 465 | makeList n = 466 | allValues itemControl 467 | |> List.repeat n 468 | |> List.concat 469 | |> List.take n 470 | in 471 | Control 472 | { currentValue = \() -> makeList current 473 | , allValues = 474 | \() -> 475 | [ 1, 0, 3 ] 476 | |> List.filter (\x -> x > min && x < max) 477 | |> (\a -> List.append a [ min, max ]) 478 | |> List.map makeList 479 | , view = 480 | \() -> 481 | SingleView <| 482 | let 483 | selectNew new = 484 | list_ itemControl new min max 485 | in 486 | Html.map 487 | (String.toInt 488 | >> Maybe.withDefault current 489 | >> selectNew 490 | ) 491 | <| 492 | Html.label [] 493 | [ Html.text "" 494 | , Html.input 495 | [ Html.Attributes.type_ "range" 496 | , Html.Attributes.min <| String.fromInt min 497 | , Html.Attributes.max <| String.fromInt max 498 | , Html.Attributes.step <| String.fromInt 1 499 | , Html.Attributes.attribute "value" <| String.fromInt current 500 | , Html.Events.on "input" Html.Events.targetValue 501 | ] 502 | [] 503 | ] 504 | } 505 | 506 | 507 | {-| Create a `Control` representing a record with multiple fields. 508 | 509 | This uses an API similar to [elm-decode-pipeline](http://package.elm-lang.org/packages/NoRedInk/elm-decode-pipeline/latest). 510 | 511 | You will use this with `field`. 512 | 513 | import Debug.Control exposing (field, record, string) 514 | 515 | type alias Point = 516 | { x : String 517 | , y : String 518 | } 519 | 520 | pointControl : Control Point 521 | pointControl = 522 | record Point 523 | |> field "x" (string "initial x value") 524 | |> field "y" (string "initial y value") 525 | 526 | -} 527 | record : a -> Control a 528 | record fn = 529 | Control 530 | { currentValue = \() -> fn 531 | , allValues = \() -> [ fn ] 532 | , view = \() -> FieldViews [] 533 | } 534 | 535 | 536 | {-| Used with `record` to create a `Control` representing a record. 537 | 538 | See [`record`](#record). 539 | 540 | -} 541 | field : String -> Control a -> Control (a -> b) -> Control b 542 | field name (Control control) (Control pipeline) = 543 | Control 544 | { currentValue = \() -> pipeline.currentValue () (control.currentValue ()) 545 | , allValues = 546 | \() -> 547 | control.allValues () 548 | |> List.concatMap 549 | (\v -> 550 | List.map (\p -> p v) 551 | (pipeline.allValues ()) 552 | ) 553 | , view = 554 | \() -> 555 | let 556 | otherFields = 557 | case pipeline.view () of 558 | FieldViews fs -> 559 | List.map (Tuple.mapSecond (\x -> Html.map (field name (Control control)) x)) 560 | fs 561 | 562 | _ -> 563 | [] 564 | 565 | newView = 566 | view_ (\v -> field name v (Control pipeline)) (Control control) 567 | in 568 | FieldViews (( name, newView ) :: otherFields) 569 | } 570 | 571 | 572 | {-| Transform the value produced by a `Control`. 573 | -} 574 | map : (a -> b) -> Control a -> Control b 575 | map fn (Control a) = 576 | Control 577 | { currentValue = \() -> fn (a.currentValue ()) 578 | , allValues = mapAllValues fn a.allValues 579 | , view = \() -> mapView fn (a.view ()) 580 | } 581 | 582 | 583 | {-| Use lazy when working with recursive types: 584 | 585 | import Debug.Control as Control exposing (Control) 586 | 587 | type RecursiveType 588 | = RecursiveType (Maybe RecursiveType) 589 | 590 | recursiveTypeControl : Control RecursiveType 591 | recursiveTypeControl = 592 | Control.choice 593 | [ ( "No child", Control.value Nothing ) 594 | , ( "child", Control.lazy (\() -> recursiveTypeControl) |> Control.map Just ) 595 | ] 596 | |> Control.map RecursiveType 597 | 598 | -} 599 | lazy : (() -> Control a) -> Control a 600 | lazy fn = 601 | let 602 | unwrap (Control v) = 603 | v 604 | in 605 | Control 606 | { currentValue = \() -> (unwrap (fn ())).currentValue () 607 | , allValues = \() -> (unwrap (fn ())).allValues () 608 | , view = \() -> (unwrap (fn ())).view () 609 | } 610 | 611 | 612 | mapAllValues : (a -> b) -> (() -> List a) -> (() -> List b) 613 | mapAllValues fn allValues_ = 614 | \() -> List.map fn (allValues_ ()) 615 | 616 | 617 | mapView : (a -> b) -> ControlView a -> ControlView b 618 | mapView fn controlView = 619 | case controlView of 620 | NoView -> 621 | NoView 622 | 623 | SingleView v -> 624 | SingleView (Html.map (map fn) v) 625 | 626 | FieldViews fs -> 627 | FieldViews 628 | (List.map (Tuple.mapSecond (Html.map (map fn))) fs) 629 | 630 | 631 | {-| Gets the current value of a `Control`. 632 | -} 633 | currentValue : Control a -> a 634 | currentValue (Control c) = 635 | c.currentValue () 636 | 637 | 638 | {-| TODO: revise API 639 | -} 640 | allValues : Control a -> List a 641 | allValues (Control c) = 642 | c.allValues () 643 | 644 | 645 | {-| Renders the interactive UI for a `Control`. 646 | -} 647 | view : (Control a -> msg) -> Control a -> Html msg 648 | view msg (Control c) = 649 | Html.div [] 650 | [ view_ msg (Control c) 651 | ] 652 | 653 | 654 | view_ : (Control a -> msg) -> Control a -> Html msg 655 | view_ msg (Control c) = 656 | case c.view () of 657 | NoView -> 658 | Html.text "" 659 | 660 | SingleView v -> 661 | Html.map msg v 662 | 663 | FieldViews fs -> 664 | let 665 | fieldRow index ( name, fieldView ) = 666 | Html.label 667 | [ Html.Attributes.style "display" "table-row" 668 | , Html.Attributes.style "vertical-align" "text-top" 669 | ] 670 | [ Html.span 671 | [ Html.Attributes.style "display" "table-cell" 672 | ] 673 | [ Html.text 674 | (if index == 0 then 675 | "{" 676 | 677 | else 678 | "," 679 | ) 680 | ] 681 | , Html.span 682 | [ Html.Attributes.style "display" "table-cell" 683 | , Html.Attributes.style "text-align" "right" 684 | ] 685 | [ Html.text name ] 686 | , Html.span 687 | [ Html.Attributes.style "display" "table-cell" 688 | ] 689 | [ Html.text " = " ] 690 | , Html.div 691 | [ Html.Attributes.style "display" "table-cell" 692 | ] 693 | [ fieldView ] 694 | ] 695 | in 696 | List.concat 697 | [ fs 698 | |> List.reverse 699 | |> List.indexedMap fieldRow 700 | , [ Html.div 701 | [ Html.Attributes.style "display" "table-row" 702 | ] 703 | [ Html.div 704 | [ Html.Attributes.style "display" "table-cell" 705 | ] 706 | [ Html.text "}" ] 707 | ] 708 | ] 709 | ] 710 | |> Html.div 711 | [ Html.Attributes.style "display" "table" 712 | , Html.Attributes.style "border-spacing" "2px" 713 | ] 714 | |> Html.map msg 715 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff/ 2 | -------------------------------------------------------------------------------- /tests/Controls/ComplexChoiceTest.elm: -------------------------------------------------------------------------------- 1 | module Controls.ComplexChoiceTest exposing (all) 2 | 3 | import Debug.Control as Control 4 | import Expect 5 | import Html 6 | import Html.Attributes as Html 7 | import Test exposing (..) 8 | import Test.Html.Query as Query 9 | import Test.Html.Selector exposing (..) 10 | 11 | 12 | type Animal 13 | = Monkey 14 | | Giraffe 15 | 16 | 17 | maybeControls = 18 | Control.choice 19 | [ ( "Animal" 20 | , Control.map Just <| 21 | Control.choice 22 | [ ( "Monkey", Control.value Monkey ) 23 | , ( "Giraffe", Control.value Giraffe ) 24 | ] 25 | ) 26 | , ( "---", Control.value Nothing ) 27 | ] 28 | 29 | 30 | all : Test 31 | all = 32 | describe "Control.choice (complex)" 33 | [ test "initial value is the first choice" <| 34 | \() -> 35 | maybeControls 36 | |> Control.currentValue 37 | |> Expect.equal (Just Monkey) 38 | , test "Renders all options" <| 39 | \() -> 40 | maybeControls 41 | |> Control.view identity 42 | |> Query.fromHtml 43 | |> Expect.all 44 | [ Query.has [ tag "option", text "Animal" ] 45 | , Query.has [ tag "option", text "---" ] 46 | , Query.has [ tag "option", text "Monkey" ] 47 | , Query.has [ tag "option", text "Giraffe" ] 48 | ] 49 | , test "allValues" <| 50 | \() -> 51 | maybeControls 52 | |> Control.allValues 53 | |> Expect.equal 54 | [ Just Monkey 55 | , Just Giraffe 56 | , Nothing 57 | ] 58 | ] 59 | -------------------------------------------------------------------------------- /tests/Controls/DateTest.elm: -------------------------------------------------------------------------------- 1 | module Controls.DateTest exposing (all) 2 | 3 | import Debug.Control as Control 4 | import Expect 5 | import Html 6 | import Html.Attributes as Html 7 | import Test exposing (..) 8 | import Test.Html.Query as Query 9 | import Test.Html.Selector exposing (..) 10 | import Time 11 | import Time.Extra 12 | 13 | 14 | all : Test 15 | all = 16 | describe "Control.date" 17 | [ test "epoch initial value" <| 18 | \() -> 19 | Control.date Time.utc (Time.millisToPosix 0) 20 | |> Control.currentValue 21 | |> Expect.equal (Time.millisToPosix 0) 22 | , test "other initial value" <| 23 | \() -> 24 | Control.date Time.utc (Time.millisToPosix 1425744000000) 25 | |> Control.currentValue 26 | |> Expect.equal (Time.millisToPosix 1425744000000) 27 | ] 28 | -------------------------------------------------------------------------------- /tests/Controls/ListTest.elm: -------------------------------------------------------------------------------- 1 | module Controls.ListTest exposing (all) 2 | 3 | import Debug.Control as Control 4 | import Expect 5 | import Html 6 | import Html.Attributes as Html 7 | import Test exposing (..) 8 | 9 | 10 | listControl = 11 | Control.list <| Control.values Debug.toString [ "A", "B" ] 12 | 13 | 14 | all : Test 15 | all = 16 | describe "Control.list" 17 | [ test "initial value" <| 18 | \() -> 19 | listControl 20 | |> Control.currentValue 21 | |> Expect.equal [ "A" ] 22 | ] 23 | -------------------------------------------------------------------------------- /tests/Controls/RecursionTest.elm: -------------------------------------------------------------------------------- 1 | module Controls.RecursionTest exposing (all) 2 | 3 | import Debug.Control as Control exposing (Control) 4 | import Expect 5 | import Html 6 | import Html.Attributes as Html 7 | import Test exposing (..) 8 | import Test.Html.Query as Query 9 | import Test.Html.Selector exposing (..) 10 | 11 | 12 | type RecursiveType 13 | = RecursiveType (Maybe RecursiveType) 14 | 15 | 16 | controls : Control RecursiveType 17 | controls = 18 | Control.choice 19 | [ ( "No child", Control.value Nothing ) 20 | , ( "child", Control.lazy (\() -> controls) |> Control.map Just ) 21 | ] 22 | |> Control.map RecursiveType 23 | 24 | 25 | all : Test 26 | all = 27 | describe "Control.choice (complex)" 28 | [ test "Initially there is no child" <| 29 | \() -> 30 | controls 31 | |> Control.currentValue 32 | |> Expect.equal (RecursiveType Nothing) 33 | , test "Renders all options" <| 34 | \() -> 35 | controls 36 | |> Control.view identity 37 | |> Query.fromHtml 38 | |> Expect.all 39 | [ Query.has [ tag "option", text "No child" ] 40 | , Query.has [ tag "option", text "child" ] 41 | ] 42 | ] 43 | -------------------------------------------------------------------------------- /tests/Controls/SimpleChoiceTest.elm: -------------------------------------------------------------------------------- 1 | module Controls.SimpleChoiceTest exposing (all) 2 | 3 | import Debug.Control as Control 4 | import Expect 5 | import Html 6 | import Html.Attributes as Html 7 | import Test exposing (..) 8 | import Test.Html.Query as Query 9 | import Test.Html.Selector exposing (..) 10 | 11 | 12 | yesNoControls = 13 | Control.choice 14 | [ ( "YES", Control.value True ) 15 | , ( "NO", Control.value False ) 16 | ] 17 | 18 | 19 | all : Test 20 | all = 21 | describe "Control.choice" 22 | [ test "initial value is the first choice" <| 23 | \() -> 24 | yesNoControls 25 | |> Control.currentValue 26 | |> Expect.equal True 27 | , test "Renders all options" <| 28 | \() -> 29 | yesNoControls 30 | |> Control.view identity 31 | |> Query.fromHtml 32 | |> Expect.all 33 | [ Query.has [ tag "option", text "YES" ] 34 | , Query.has [ tag "option", text "NO" ] 35 | ] 36 | ] 37 | -------------------------------------------------------------------------------- /tests/Controls/StringTest.elm: -------------------------------------------------------------------------------- 1 | module Controls.StringTest exposing (all) 2 | 3 | import Debug.Control as Control 4 | import Expect 5 | import Html 6 | import Html.Attributes as Html 7 | import Test exposing (..) 8 | import Test.Html.Query as Query 9 | import Test.Html.Selector exposing (..) 10 | 11 | 12 | all : Test 13 | all = 14 | describe "Text" 15 | [ describe "Control.string" 16 | [ test "initial value" <| 17 | \() -> 18 | Control.string "default" 19 | |> Control.currentValue 20 | |> Expect.equal "default" 21 | , test "Renders all options" <| 22 | \() -> 23 | Control.string "default" 24 | |> Control.view identity 25 | |> Query.fromHtml 26 | |> Query.has [ tag "input", attribute (Html.value "default") ] 27 | ] 28 | , describe "Control.stringTextarea" 29 | [ test "initial value" <| 30 | \() -> 31 | Control.stringTextarea "long default" 32 | |> Control.currentValue 33 | |> Expect.equal "long default" 34 | , test "Renders all options" <| 35 | \() -> 36 | Control.stringTextarea "long default" 37 | |> Control.view identity 38 | |> Query.fromHtml 39 | |> Query.has [ tag "textarea", attribute (Html.value "long default") ] 40 | ] 41 | ] 42 | --------------------------------------------------------------------------------