├── .gitignore ├── LICENSE ├── README.md ├── elm-package.json ├── examples ├── 1-presidents.elm ├── 2-travel.elm ├── README.md ├── elm-package.json └── index.html └── src └── Table.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-present, Evan Czaplicki 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Evan Czaplicki nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | **This package is not published in 0.19 and above.** In an effort to prune my responsibilities, we suggested that community members fork it and take it in the direction that makes sense to them. Maybe that means adding more features. Maybe it means removing things to make it even simpler. Maybe it means rewriting it from scratch. Point is, **please search for packages with the same or similar name, and look for an author you trust.** 4 | 5 |
6 | 7 | * * * 8 | What follows is the README from before in case you are curious about the history. 9 | * * * 10 | 11 |
12 | 13 | # Sortable Tables 14 | 15 | Create sortable tables for data of any shape. 16 | 17 | This library also lets you customize ``, ``, ``, etc. for your particular needs. So it is pretty easy to do whatever crazy CSS trickery is needed to get the exact table you want. 18 | 19 | 20 | ## Examples 21 | 22 | 1. [U.S. Presidents by Birth Place](https://evancz.github.io/elm-sortable-table/presidents.html) / [Code](https://github.com/evancz/elm-sortable-table/blob/master/examples/1-presidents.elm) 23 | 2. [Travel Planner for the Mission District in San Francisco](https://evancz.github.io/elm-sortable-table/travel.html) / [Code](https://github.com/evancz/elm-sortable-table/blob/master/examples/2-travel.elm) 24 | 25 | 26 | ## Usage Rules 27 | 28 | - Always put `Table.State` in your model. 29 | - Never put `Table.Config` in your model. 30 | 31 | One of the core rules of The Elm Architecture is **never put functions in your `Model` or `Msg` types**. It may cost a little bit of extra code to model everything as data, but the architecture and debugging benefits are worth it. Point is, a `Table.Config` value is really just a bunch of `view` functions, so it does not belong in your model. It goes in your `view`! 32 | 33 | Furthermore, you do not want to be creating table configurations dynamically, partly because it is harder to optimize. If you need multiple table configurations, it is best to create multiple top-level definitions and switch between them in your `view` based on other data in your `Model`. If your use case is so complex that this is not possible, please open an issue explaining your situation! 34 | 35 | 36 | ## About API Design 37 | 38 | This library is one of the first “reusable view” packages that also manages some state, so I want to point out some design considerations that will be helpful in general. 39 | 40 | 41 | ### The Elm Architecture 42 | 43 | It may not be obvious at first glance, but this library follows The Elm Architecture: 44 | 45 | - `Model` — There is a model named `Table.State`. 46 | 47 | - `init` — You initialize the model with `Table.initialSort`. 48 | 49 | - `view` — You turn the current state into HTML with `Table.view`. 50 | 51 | - `update` — This is a little hidden, but it is there. When you create a `Table.Config`, you provide a function `Table.State -> msg` so that whoever is rendering the table has a chance to update the table state. 52 | 53 | I took some minor liberties with `update` to make the API a bit simpler. It would be more legit if `Table.Config` took a `Table.Msg -> msg` argument and you needed to use `Table.update : Table.Msg -> Table.State -> Table.State` to deal with it. I decided not to go this route because `Table.Msg` and `Table.State` would both allocate the same amount of memory and one version the overall API a bit tougher. As we learn from how people use this, we may see that the explicit `update` function is actually a better way to go though! 54 | 55 | 56 | ### Single Source of Truth 57 | 58 | The data displayed in the table is given as an argument to `view`. To put that another way, the `Table.State` value only tracks the details specific to *displaying* a sorted table, not the actual data to appear in the table. **This is the most important decision in this whole library.** This choice means you can change your data without any risk of the table getting out of sync. You may be adding things, changing entries, or whatever else; the table will never “get stuck” and display out of date information. 59 | 60 | To make this more clear, let’s imagine the alternate choice: instead of giving `List data` to `view`, we have it live in `Table.State`. Now say we want to update the dataset. We grab a copy of the data, make the changes we want, and put it back. But what if we forget to put it back? What if we hold on to that second copy in our `Model`? Which one is the *real* data now? 61 | 62 | Point is, **when creating an API like this, own as little state as possible.** Having multiple copies of “the same” value in your `Model` is a sure way to create synchronization errors. Elm is built on the idea that there should be a single source of truth, but if you design your API poorly, you can force your users to make duplicates and open themselves up to bugs for no reason. Do not do that to them! 63 | 64 | 65 | ### Simple By Default 66 | 67 | I designed this library to have a very smooth learning curve. As you read the docs, you start with the simplest functions. Predefined columns, and very little customization. This makes it easier for the reader to build a basic intuition for how things work. 68 | 69 | The trick is that all these simple functions are defined in terms of crazier ones that allow for more customization. As the user **NEEDS** that complexity, they can read on and gradually use the parts that are relevant to them. This means the user never finds themselves in a situation where they have to learn a bunch of stuff that does not actually matter to them. At the same time, that stuff is there when they need it. 70 | 71 | To turn this into advice about API design, **helper functions can make a library simpler to learn and use.** Ultimately, people may not use `Table.floatColumn` very often in real stuff, but that function is crucial for learning. So when you find yourself with a tough API, one way to ramp people up is to create specialized helper functions that let you get common functionality without confronting people with all the details. 72 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.1", 3 | "summary": "Sortable tables for data of any shape.", 4 | "repository": "https://github.com/evancz/elm-sortable-table.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [ 10 | "Table" 11 | ], 12 | "dependencies": { 13 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 14 | "elm-lang/html": "2.0.0 <= v < 3.0.0" 15 | }, 16 | "elm-version": "0.18.0 <= v < 0.19.0" 17 | } 18 | -------------------------------------------------------------------------------- /examples/1-presidents.elm: -------------------------------------------------------------------------------- 1 | import Html exposing (Html, div, h1, input, text) 2 | import Html.Attributes exposing (placeholder) 3 | import Html.Events exposing (onInput) 4 | import Table 5 | 6 | 7 | 8 | main = 9 | Html.program 10 | { init = init presidents 11 | , update = update 12 | , view = view 13 | , subscriptions = \_ -> Sub.none 14 | } 15 | 16 | 17 | 18 | -- MODEL 19 | 20 | 21 | type alias Model = 22 | { people : List Person 23 | , tableState : Table.State 24 | , query : String 25 | } 26 | 27 | 28 | init : List Person -> ( Model, Cmd Msg ) 29 | init people = 30 | let 31 | model = 32 | { people = people 33 | , tableState = Table.initialSort "Year" 34 | , query = "" 35 | } 36 | in 37 | ( model, Cmd.none ) 38 | 39 | 40 | 41 | -- UPDATE 42 | 43 | 44 | type Msg 45 | = SetQuery String 46 | | SetTableState Table.State 47 | 48 | 49 | update : Msg -> Model -> ( Model, Cmd Msg ) 50 | update msg model = 51 | case msg of 52 | SetQuery newQuery -> 53 | ( { model | query = newQuery } 54 | , Cmd.none 55 | ) 56 | 57 | SetTableState newState -> 58 | ( { model | tableState = newState } 59 | , Cmd.none 60 | ) 61 | 62 | 63 | 64 | -- VIEW 65 | 66 | 67 | view : Model -> Html Msg 68 | view { people, tableState, query } = 69 | let 70 | lowerQuery = 71 | String.toLower query 72 | 73 | acceptablePeople = 74 | List.filter (String.contains lowerQuery << String.toLower << .name) people 75 | in 76 | div [] 77 | [ h1 [] [ text "Birthplaces of U.S. Presidents" ] 78 | , input [ placeholder "Search by Name", onInput SetQuery ] [] 79 | , Table.view config tableState acceptablePeople 80 | ] 81 | 82 | 83 | config : Table.Config Person Msg 84 | config = 85 | Table.config 86 | { toId = .name 87 | , toMsg = SetTableState 88 | , columns = 89 | [ Table.stringColumn "Name" .name 90 | , Table.intColumn "Year" .year 91 | , Table.stringColumn "City" .city 92 | , Table.stringColumn "State" .state 93 | ] 94 | } 95 | 96 | 97 | 98 | -- PEOPLE 99 | 100 | 101 | type alias Person = 102 | { name : String 103 | , year : Int 104 | , city : String 105 | , state : String 106 | } 107 | 108 | 109 | presidents : List Person 110 | presidents = 111 | [ Person "George Washington" 1732 "Westmoreland County" "Virginia" 112 | , Person "John Adams" 1735 "Braintree" "Massachusetts" 113 | , Person "Thomas Jefferson" 1743 "Shadwell" "Virginia" 114 | , Person "James Madison" 1751 "Port Conway" "Virginia" 115 | , Person "James Monroe" 1758 "Monroe Hall" "Virginia" 116 | , Person "Andrew Jackson" 1767 "Waxhaws Region" "South/North Carolina" 117 | , Person "John Quincy Adams" 1767 "Braintree" "Massachusetts" 118 | , Person "William Henry Harrison" 1773 "Charles City County" "Virginia" 119 | , Person "Martin Van Buren" 1782 "Kinderhook" "New York" 120 | , Person "Zachary Taylor" 1784 "Barboursville" "Virginia" 121 | , Person "John Tyler" 1790 "Charles City County" "Virginia" 122 | , Person "James Buchanan" 1791 "Cove Gap" "Pennsylvania" 123 | , Person "James K. Polk" 1795 "Pineville" "North Carolina" 124 | , Person "Millard Fillmore" 1800 "Summerhill" "New York" 125 | , Person "Franklin Pierce" 1804 "Hillsborough" "New Hampshire" 126 | , Person "Andrew Johnson" 1808 "Raleigh" "North Carolina" 127 | , Person "Abraham Lincoln" 1809 "Sinking spring" "Kentucky" 128 | , Person "Ulysses S. Grant" 1822 "Point Pleasant" "Ohio" 129 | , Person "Rutherford B. Hayes" 1822 "Delaware" "Ohio" 130 | , Person "Chester A. Arthur" 1829 "Fairfield" "Vermont" 131 | , Person "James A. Garfield" 1831 "Moreland Hills" "Ohio" 132 | , Person "Benjamin Harrison" 1833 "North Bend" "Ohio" 133 | , Person "Grover Cleveland" 1837 "Caldwell" "New Jersey" 134 | , Person "William McKinley" 1843 "Niles" "Ohio" 135 | , Person "Woodrow Wilson" 1856 "Staunton" "Virginia" 136 | , Person "William Howard Taft" 1857 "Cincinnati" "Ohio" 137 | , Person "Theodore Roosevelt" 1858 "New York City" "New York" 138 | , Person "Warren G. Harding" 1865 "Blooming Grove" "Ohio" 139 | , Person "Calvin Coolidge" 1872 "Plymouth" "Vermont" 140 | , Person "Herbert Hoover" 1874 "West Branch" "Iowa" 141 | , Person "Franklin D. Roosevelt" 1882 "Hyde Park" "New York" 142 | , Person "Harry S. Truman" 1884 "Lamar" "Missouri" 143 | , Person "Dwight D. Eisenhower" 1890 "Denison" "Texas" 144 | , Person "Lyndon B. Johnson" 1908 "Stonewall" "Texas" 145 | , Person "Ronald Reagan" 1911 "Tampico" "Illinois" 146 | , Person "Richard M. Nixon" 1913 "Yorba Linda" "California" 147 | , Person "Gerald R. Ford" 1913 "Omaha" "Nebraska" 148 | , Person "John F. Kennedy" 1917 "Brookline" "Massachusetts" 149 | , Person "George H. W. Bush" 1924 "Milton" "Massachusetts" 150 | , Person "Jimmy Carter" 1924 "Plains" "Georgia" 151 | , Person "George W. Bush" 1946 "New Haven" "Connecticut" 152 | , Person "Bill Clinton" 1946 "Hope" "Arkansas" 153 | , Person "Barack Obama" 1961 "Honolulu" "Hawaii" 154 | , Person "Donald Trump" 1946 "New York City" "New York" 155 | ] 156 | -------------------------------------------------------------------------------- /examples/2-travel.elm: -------------------------------------------------------------------------------- 1 | import Html exposing (Html, Attribute, div, h1, input, p, text) 2 | import Html.Attributes exposing (checked, style, type_) 3 | import Html.Events exposing (onClick) 4 | import Html.Lazy exposing (lazy) 5 | import Table exposing (defaultCustomizations) 6 | import Time exposing (Time) 7 | 8 | 9 | 10 | main = 11 | Html.program 12 | { init = init missionSights 13 | , update = update 14 | , view = view 15 | , subscriptions = \_ -> Sub.none 16 | } 17 | 18 | 19 | 20 | -- MODEL 21 | 22 | 23 | type alias Model = 24 | { sights : List Sight 25 | , tableState : Table.State 26 | } 27 | 28 | 29 | init : List Sight -> ( Model, Cmd Msg ) 30 | init sights = 31 | let 32 | model = 33 | { sights = sights 34 | , tableState = Table.initialSort "Year" 35 | } 36 | in 37 | ( model, Cmd.none ) 38 | 39 | 40 | 41 | -- UPDATE 42 | 43 | 44 | type Msg 45 | = ToggleSelected String 46 | | SetTableState Table.State 47 | 48 | 49 | update : Msg -> Model -> ( Model, Cmd Msg ) 50 | update msg model = 51 | case msg of 52 | ToggleSelected name -> 53 | ( { model | sights = List.map (toggle name) model.sights } 54 | , Cmd.none 55 | ) 56 | 57 | SetTableState newState -> 58 | ( { model | tableState = newState } 59 | , Cmd.none 60 | ) 61 | 62 | 63 | toggle : String -> Sight -> Sight 64 | toggle name sight = 65 | if sight.name == name then 66 | { sight | selected = not sight.selected } 67 | 68 | else 69 | sight 70 | 71 | 72 | 73 | -- VIEW 74 | 75 | 76 | view : Model -> Html Msg 77 | view { sights, tableState } = 78 | div [] 79 | [ h1 [] [ text "Trip Planner" ] 80 | , lazy viewSummary sights 81 | , Table.view config tableState sights 82 | ] 83 | 84 | 85 | viewSummary : List Sight -> Html msg 86 | viewSummary allSights = 87 | case List.filter .selected allSights of 88 | [] -> 89 | p [] [ text "Click the sights you want to see on your trip!" ] 90 | 91 | sights -> 92 | let 93 | time = 94 | List.sum (List.map .time sights) 95 | 96 | price = 97 | List.sum (List.map .price sights) 98 | 99 | summary = 100 | "That is " ++ timeToString time ++ " of fun, costing $" ++ toString price 101 | in 102 | p [] [ text summary ] 103 | 104 | 105 | timeToString : Time -> String 106 | timeToString time = 107 | let 108 | hours = 109 | case floor (Time.inHours time) of 110 | 0 -> "" 111 | 1 -> "1 hour" 112 | n -> toString n ++ " hours" 113 | 114 | minutes = 115 | case rem (round (Time.inMinutes time)) 60 of 116 | 0 -> "" 117 | 1 -> "1 minute" 118 | n -> toString n ++ " minutes" 119 | in 120 | hours ++ " " ++ minutes 121 | 122 | 123 | 124 | -- TABLE CONFIGURATION 125 | 126 | 127 | config : Table.Config Sight Msg 128 | config = 129 | Table.customConfig 130 | { toId = .name 131 | , toMsg = SetTableState 132 | , columns = 133 | [ checkboxColumn 134 | , Table.stringColumn "Name" .name 135 | , timeColumn 136 | , Table.floatColumn "Price" .price 137 | , Table.floatColumn "Rating" .rating 138 | ] 139 | , customizations = 140 | { defaultCustomizations | rowAttrs = toRowAttrs } 141 | } 142 | 143 | 144 | toRowAttrs : Sight -> List (Attribute Msg) 145 | toRowAttrs sight = 146 | [ onClick (ToggleSelected sight.name) 147 | , style [ ("background", if sight.selected then "#CEFAF8" else "white") ] 148 | ] 149 | 150 | 151 | timeColumn : Table.Column Sight Msg 152 | timeColumn = 153 | Table.customColumn 154 | { name = "Time" 155 | , viewData = timeToString << .time 156 | , sorter = Table.increasingOrDecreasingBy .time 157 | } 158 | 159 | 160 | checkboxColumn : Table.Column Sight Msg 161 | checkboxColumn = 162 | Table.veryCustomColumn 163 | { name = "" 164 | , viewData = viewCheckbox 165 | , sorter = Table.unsortable 166 | } 167 | 168 | 169 | viewCheckbox : Sight -> Table.HtmlDetails Msg 170 | viewCheckbox {selected} = 171 | Table.HtmlDetails [] 172 | [ input [ type_ "checkbox", checked selected ] [] 173 | ] 174 | 175 | 176 | 177 | -- SIGHTS 178 | 179 | 180 | type alias Sight = 181 | { name : String 182 | , time : Time 183 | , price : Float 184 | , rating : Float 185 | , selected : Bool 186 | } 187 | 188 | 189 | missionSights : List Sight 190 | missionSights = 191 | [ Sight "Eat a Burrito" (30 * Time.minute) 7 4.6 False 192 | , Sight "Buy drugs in Dolores park" Time.hour 20 4.8 False 193 | , Sight "Armory Tour" (1.5 * Time.hour) 27 4.5 False 194 | , Sight "Tartine Bakery" Time.hour 10 4.1 False 195 | , Sight "Have Brunch" (2 * Time.hour) 25 4.2 False 196 | , Sight "Get catcalled at BART" (5 * Time.minute) 0 1.6 False 197 | , Sight "Buy a painting at \"Stuff\"" (45 * Time.minute) 400 4.7 False 198 | , Sight "McDonalds at 24th" (20 * Time.minute) 5 2.8 False 199 | ] 200 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | 1. [U.S. Presidents by Birth Place](https://evancz.github.io/elm-sortable-table/presidents.html) 4 | 2. [Travel Planner for the Mission District in San Francisco](https://evancz.github.io/elm-sortable-table/travel.html) 5 | 6 | 7 | ## Build Instructions 8 | 9 | To see the examples *without* CSS, run the following commands: 10 | 11 | ```bash 12 | git clone https://github.com/evancz/elm-sortable-table.git 13 | cd elm-sortable-table 14 | cd examples 15 | elm-reactor 16 | ``` 17 | 18 | Then navigate to `1-presidents.elm` or `2-travel.elm` from [localhost:8000](http://localhost:8000/). When using `elm-reactor`, refreshing a page that ends with `.elm` will recompile the code in that file and show you the new result. 19 | 20 | 21 | ## Build Instructions with CSS 22 | 23 | To see the examples *with* CSS, run the following commands: 24 | 25 | ```bash 26 | git clone https://github.com/evancz/elm-sortable-table.git 27 | cd elm-sortable-table 28 | cd examples 29 | elm-make 1-presidents.elm --yes --output=elm.js 30 | elm-reactor 31 | ``` 32 | 33 | Then open [localhost:8000/index.html](http://localhost:8000/index.html) in your browser. That HTML file loads in some CSS and whatever code is in `elm.js`. So if you want to see the second example with CSS, you can compile it like this: 34 | 35 | ```bash 36 | elm-make 2-travel.elm --yes --output=elm.js 37 | ``` 38 | 39 | As you make changes, you will want to recompile the Elm code with `elm-make`. -------------------------------------------------------------------------------- /examples/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.1", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "." 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 12 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 13 | "evancz/elm-sortable-table": "1.0.1 <= v < 2.0.0" 14 | }, 15 | "elm-version": "0.18.0 <= v < 0.19.0" 16 | } 17 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sortable Tables 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Table.elm: -------------------------------------------------------------------------------- 1 | module Table exposing 2 | ( view 3 | , config, stringColumn, intColumn, floatColumn 4 | , State, initialSort 5 | , Column, customColumn, veryCustomColumn 6 | , Sorter, unsortable, increasingBy, decreasingBy 7 | , increasingOrDecreasingBy, decreasingOrIncreasingBy 8 | , Config, customConfig 9 | , Customizations, HtmlDetails, Status(..), defaultCustomizations 10 | ) 11 | 12 | {-| 13 | 14 | This library helps you create sortable tables. The crucial feature is that it 15 | lets you own your data separately and keep it in whatever format is best for 16 | you. This way you are free to change your data without worrying about the table 17 | “getting out of sync” with the data. Having a single source of 18 | truth is pretty great! 19 | 20 | I recommend checking out the [examples][] to get a feel for how it works. 21 | 22 | [examples]: https://github.com/evancz/elm-sortable-table/tree/master/examples 23 | 24 | # View 25 | 26 | @docs view 27 | 28 | # Configuration 29 | 30 | @docs config, stringColumn, intColumn, floatColumn 31 | 32 | # State 33 | 34 | @docs State, initialSort 35 | 36 | 37 | # Crazy Customization 38 | 39 | If you are new to this library, you can probably stop reading here. After this 40 | point are a bunch of ways to customize your table further. If it does not 41 | provide what you need, you may just want to write a custom table yourself. It 42 | is not that crazy. 43 | 44 | ## Custom Columns 45 | 46 | @docs Column, customColumn, veryCustomColumn, 47 | Sorter, unsortable, increasingBy, decreasingBy, 48 | increasingOrDecreasingBy, decreasingOrIncreasingBy 49 | 50 | ## Custom Tables 51 | 52 | @docs Config, customConfig, Customizations, HtmlDetails, Status, 53 | defaultCustomizations 54 | -} 55 | 56 | import Html exposing (Html, Attribute) 57 | import Html.Attributes as Attr 58 | import Html.Events as E 59 | import Html.Keyed as Keyed 60 | import Html.Lazy exposing (lazy2, lazy3) 61 | import Json.Decode as Json 62 | 63 | 64 | 65 | -- STATE 66 | 67 | 68 | {-| Tracks which column to sort by. 69 | -} 70 | type State = 71 | State String Bool 72 | 73 | 74 | {-| Create a table state. By providing a column name, you determine which 75 | column should be used for sorting by default. So if you want your table of 76 | yachts to be sorted by length by default, you might say: 77 | 78 | import Table 79 | 80 | Table.initialSort "Length" 81 | -} 82 | initialSort : String -> State 83 | initialSort header = 84 | State header False 85 | 86 | 87 | 88 | -- CONFIG 89 | 90 | 91 | {-| Configuration for your table, describing your columns. 92 | 93 | **Note:** Your `Config` should *never* be held in your model. 94 | It should only appear in `view` code. 95 | -} 96 | type Config data msg = 97 | Config 98 | { toId : data -> String 99 | , toMsg : State -> msg 100 | , columns : List (ColumnData data msg) 101 | , customizations : Customizations data msg 102 | } 103 | 104 | 105 | {-| Create the `Config` for your `view` function. Everything you need to 106 | render your columns efficiently and handle selection of columns. 107 | 108 | Say we have a `List Person` that we want to show as a table. The table should 109 | have a column for name and age. We would create a `Config` like this: 110 | 111 | import Table 112 | 113 | type Msg = NewTableState State | ... 114 | 115 | config : Table.Config Person Msg 116 | config = 117 | Table.config 118 | { toId = .name 119 | , toMsg = NewTableState 120 | , columns = 121 | [ Table.stringColumn "Name" .name 122 | , Table.intColumn "Age" .age 123 | ] 124 | } 125 | 126 | You provide the following information in your table configuration: 127 | 128 | - `toId` — turn a `Person` into a unique ID. This lets us use 129 | [`Html.Keyed`][keyed] under the hood to make resorts faster. 130 | - `columns` — specify some columns to show. 131 | - `toMsg` — a way to send new table states to your app as messages. 132 | 133 | See the [examples][] to get a better feel for this! 134 | 135 | [keyed]: http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Keyed 136 | [examples]: https://github.com/evancz/elm-sortable-table/tree/master/examples 137 | -} 138 | config 139 | : { toId : data -> String 140 | , toMsg : State -> msg 141 | , columns : List (Column data msg) 142 | } 143 | -> Config data msg 144 | config { toId, toMsg, columns } = 145 | Config 146 | { toId = toId 147 | , toMsg = toMsg 148 | , columns = List.map (\(Column cData) -> cData) columns 149 | , customizations = defaultCustomizations 150 | } 151 | 152 | 153 | {-| Just like `config` but you can specify a bunch of table customizations. 154 | -} 155 | customConfig 156 | : { toId : data -> String 157 | , toMsg : State -> msg 158 | , columns : List (Column data msg) 159 | , customizations : Customizations data msg 160 | } 161 | -> Config data msg 162 | customConfig { toId, toMsg, columns, customizations } = 163 | Config 164 | { toId = toId 165 | , toMsg = toMsg 166 | , columns = List.map (\(Column cData) -> cData) columns 167 | , customizations = customizations 168 | } 169 | 170 | 171 | {-| There are quite a lot of ways to customize the `` tag. You can add 172 | a `` to group columns in weird ways. You can have a `` tag for 174 | summaries of various columns. And maybe you want to put attributes on `` 175 | or on particular rows in the body. All these customizations are available to you. 176 | 177 | **Note:** The level of craziness possible in `` and `` are so 178 | high that I could not see how to provide the full functionality *and* make it 179 | impossible to do bad stuff. So just be aware of that, and share any stories 180 | you have. Stories make it possible to design better! 181 | -} 182 | type alias Customizations data msg = 183 | { tableAttrs : List (Attribute msg) 184 | , caption : Maybe (HtmlDetails msg) 185 | , thead : List (String, Status, Attribute msg) -> HtmlDetails msg 186 | , tfoot : Maybe (HtmlDetails msg) 187 | , tbodyAttrs : List (Attribute msg) 188 | , rowAttrs : data -> List (Attribute msg) 189 | } 190 | 191 | 192 | {-| Sometimes you must use a `
` which can be styled via CSS. You can do crazy stuff with 173 | `
` tag, but the attributes and children are up 193 | to you. This type lets you specify all the details of an HTML node except the 194 | tag name. 195 | -} 196 | type alias HtmlDetails msg = 197 | { attributes : List (Attribute msg) 198 | , children : List (Html msg) 199 | } 200 | 201 | 202 | {-| The customizations used in `config` by default. 203 | -} 204 | defaultCustomizations : Customizations data msg 205 | defaultCustomizations = 206 | { tableAttrs = [] 207 | , caption = Nothing 208 | , thead = simpleThead 209 | , tfoot = Nothing 210 | , tbodyAttrs = [] 211 | , rowAttrs = simpleRowAttrs 212 | } 213 | 214 | 215 | simpleThead : List (String, Status, Attribute msg) -> HtmlDetails msg 216 | simpleThead headers = 217 | HtmlDetails [] (List.map simpleTheadHelp headers) 218 | 219 | 220 | simpleTheadHelp : ( String, Status, Attribute msg ) -> Html msg 221 | simpleTheadHelp (name, status, onClick) = 222 | let 223 | content = 224 | case status of 225 | Unsortable -> 226 | [ Html.text name ] 227 | 228 | Sortable selected -> 229 | [ Html.text name 230 | , if selected then darkGrey "↓" else lightGrey "↓" 231 | ] 232 | 233 | Reversible Nothing -> 234 | [ Html.text name 235 | , lightGrey "↕" 236 | ] 237 | 238 | Reversible (Just isReversed) -> 239 | [ Html.text name 240 | , darkGrey (if isReversed then "↑" else "↓") 241 | ] 242 | in 243 | Html.th [ onClick ] content 244 | 245 | 246 | darkGrey : String -> Html msg 247 | darkGrey symbol = 248 | Html.span [ Attr.style [("color", "#555")] ] [ Html.text (" " ++ symbol) ] 249 | 250 | 251 | lightGrey : String -> Html msg 252 | lightGrey symbol = 253 | Html.span [ Attr.style [("color", "#ccc")] ] [ Html.text (" " ++ symbol) ] 254 | 255 | 256 | simpleRowAttrs : data -> List (Attribute msg) 257 | simpleRowAttrs _ = 258 | [] 259 | 260 | 261 | {-| The status of a particular column, for use in the `thead` field of your 262 | `Customizations`. 263 | 264 | - If the column is unsortable, the status will always be `Unsortable`. 265 | - If the column can be sorted in one direction, the status will be `Sortable`. 266 | The associated boolean represents whether this column is selected. So it is 267 | `True` if the table is currently sorted by this column, and `False` otherwise. 268 | - If the column can be sorted in either direction, the status will be `Reversible`. 269 | The associated maybe tells you whether this column is selected. It is 270 | `Just isReversed` if the table is currently sorted by this column, and 271 | `Nothing` otherwise. The `isReversed` boolean lets you know which way it 272 | is sorted. 273 | 274 | This information lets you do custom header decorations for each scenario. 275 | -} 276 | type Status 277 | = Unsortable 278 | | Sortable Bool 279 | | Reversible (Maybe Bool) 280 | 281 | 282 | 283 | -- COLUMNS 284 | 285 | 286 | {-| Describes how to turn `data` into a column in your table. 287 | -} 288 | type Column data msg = 289 | Column (ColumnData data msg) 290 | 291 | 292 | type alias ColumnData data msg = 293 | { name : String 294 | , viewData : data -> HtmlDetails msg 295 | , sorter : Sorter data 296 | } 297 | 298 | 299 | {-|-} 300 | stringColumn : String -> (data -> String) -> Column data msg 301 | stringColumn name toStr = 302 | Column 303 | { name = name 304 | , viewData = textDetails << toStr 305 | , sorter = increasingOrDecreasingBy toStr 306 | } 307 | 308 | 309 | {-|-} 310 | intColumn : String -> (data -> Int) -> Column data msg 311 | intColumn name toInt = 312 | Column 313 | { name = name 314 | , viewData = textDetails << toString << toInt 315 | , sorter = increasingOrDecreasingBy toInt 316 | } 317 | 318 | 319 | {-|-} 320 | floatColumn : String -> (data -> Float) -> Column data msg 321 | floatColumn name toFloat = 322 | Column 323 | { name = name 324 | , viewData = textDetails << toString << toFloat 325 | , sorter = increasingOrDecreasingBy toFloat 326 | } 327 | 328 | 329 | textDetails : String -> HtmlDetails msg 330 | textDetails str = 331 | HtmlDetails [] [ Html.text str ] 332 | 333 | 334 | {-| Perhaps the basic columns are not quite what you want. Maybe you want to 335 | display monetary values in thousands of dollars, and `floatColumn` does not 336 | quite cut it. You could define a custom column like this: 337 | 338 | import Table 339 | 340 | dollarColumn : String -> (data -> Float) -> Column data msg 341 | dollarColumn name toDollars = 342 | Table.customColumn 343 | { name = name 344 | , viewData = \data -> viewDollars (toDollars data) 345 | , sorter = Table.decreasingBy toDollars 346 | } 347 | 348 | viewDollars : Float -> String 349 | viewDollars dollars = 350 | "$" ++ toString (round (dollars / 1000)) ++ "k" 351 | 352 | The `viewData` field means we will displays the number `12345.67` as `$12k`. 353 | 354 | The `sorter` field specifies how the column can be sorted. In `dollarColumn` we 355 | are saying that it can *only* be shown from highest-to-lowest monetary value. 356 | More about sorters soon! 357 | -} 358 | customColumn 359 | : { name : String 360 | , viewData : data -> String 361 | , sorter : Sorter data 362 | } 363 | -> Column data msg 364 | customColumn { name, viewData, sorter } = 365 | Column <| 366 | ColumnData name (textDetails << viewData) sorter 367 | 368 | 369 | {-| It is *possible* that you want something crazier than `customColumn`. In 370 | that unlikely scenario, this function lets you have full control over the 371 | attributes and children of each `` cell in this column. 372 | 373 | So maybe you want to a dollars column, and the dollar signs should be green. 374 | 375 | import Html exposing (Html, Attribute, span, text) 376 | import Html.Attributes exposing (style) 377 | import Table 378 | 379 | dollarColumn : String -> (data -> Float) -> Column data msg 380 | dollarColumn name toDollars = 381 | Table.veryCustomColumn 382 | { name = name 383 | , viewData = \data -> viewDollars (toDollars data) 384 | , sorter = Table.decreasingBy toDollars 385 | } 386 | 387 | viewDollars : Float -> Table.HtmlDetails msg 388 | viewDollars dollars = 389 | Table.HtmlDetails [] 390 | [ span [ style [("color","green")] ] [ text "$" ] 391 | , text (toString (round (dollars / 1000)) ++ "k") 392 | ] 393 | -} 394 | veryCustomColumn 395 | : { name : String 396 | , viewData : data -> HtmlDetails msg 397 | , sorter : Sorter data 398 | } 399 | -> Column data msg 400 | veryCustomColumn = 401 | Column 402 | 403 | 404 | 405 | -- VIEW 406 | 407 | 408 | {-| Take a list of data and turn it into a table. The `Config` argument is the 409 | configuration for the table. It describes the columns that we want to show. The 410 | `State` argument describes which column we are sorting by at the moment. 411 | 412 | **Note:** The `State` and `List data` should live in your `Model`. The `Config` 413 | for the table belongs in your `view` code. I very strongly recommend against 414 | putting `Config` in your model. Describe any potential table configurations 415 | statically, and look for a different library if you need something crazier than 416 | that. 417 | -} 418 | view : Config data msg -> State -> List data -> Html msg 419 | view (Config { toId, toMsg, columns, customizations }) state data = 420 | let 421 | sortedData = 422 | sort state columns data 423 | 424 | theadDetails = 425 | customizations.thead (List.map (toHeaderInfo state toMsg) columns) 426 | 427 | thead = 428 | Html.thead theadDetails.attributes theadDetails.children 429 | 430 | tbody = 431 | Keyed.node "tbody" customizations.tbodyAttrs <| 432 | List.map (viewRow toId columns customizations.rowAttrs) sortedData 433 | 434 | withFoot = 435 | case customizations.tfoot of 436 | Nothing -> 437 | tbody :: [] 438 | 439 | Just { attributes, children } -> 440 | Html.tfoot attributes children :: tbody :: [] 441 | in 442 | Html.table customizations.tableAttrs <| 443 | case customizations.caption of 444 | Nothing -> 445 | thead :: withFoot 446 | 447 | Just { attributes, children } -> 448 | Html.caption attributes children :: thead :: withFoot 449 | 450 | 451 | toHeaderInfo : State -> (State -> msg) -> ColumnData data msg -> ( String, Status, Attribute msg ) 452 | toHeaderInfo (State sortName isReversed) toMsg { name, sorter } = 453 | case sorter of 454 | None -> 455 | ( name, Unsortable, onClick sortName isReversed toMsg ) 456 | 457 | Increasing _ -> 458 | ( name, Sortable (name == sortName), onClick name False toMsg ) 459 | 460 | Decreasing _ -> 461 | ( name, Sortable (name == sortName), onClick name False toMsg ) 462 | 463 | IncOrDec _ -> 464 | if name == sortName then 465 | ( name, Reversible (Just isReversed), onClick name (not isReversed) toMsg ) 466 | else 467 | ( name, Reversible Nothing, onClick name False toMsg ) 468 | 469 | DecOrInc _ -> 470 | if name == sortName then 471 | ( name, Reversible (Just isReversed), onClick name (not isReversed) toMsg ) 472 | else 473 | ( name, Reversible Nothing, onClick name False toMsg ) 474 | 475 | 476 | onClick : String -> Bool -> (State -> msg) -> Attribute msg 477 | onClick name isReversed toMsg = 478 | E.on "click" <| Json.map toMsg <| 479 | Json.map2 State (Json.succeed name) (Json.succeed isReversed) 480 | 481 | 482 | viewRow : (data -> String) -> List (ColumnData data msg) -> (data -> List (Attribute msg)) -> data -> ( String, Html msg ) 483 | viewRow toId columns toRowAttrs data = 484 | ( toId data 485 | , lazy3 viewRowHelp columns toRowAttrs data 486 | ) 487 | 488 | 489 | viewRowHelp : List (ColumnData data msg) -> (data -> List (Attribute msg)) -> data -> Html msg 490 | viewRowHelp columns toRowAttrs data = 491 | Html.tr (toRowAttrs data) (List.map (viewCell data) columns) 492 | 493 | 494 | viewCell : data -> ColumnData data msg -> Html msg 495 | viewCell data {viewData} = 496 | let 497 | details = 498 | viewData data 499 | in 500 | Html.td details.attributes details.children 501 | 502 | 503 | 504 | -- SORTING 505 | 506 | 507 | sort : State -> List (ColumnData data msg) -> List data -> List data 508 | sort (State selectedColumn isReversed) columnData data = 509 | case findSorter selectedColumn columnData of 510 | Nothing -> 511 | data 512 | 513 | Just sorter -> 514 | applySorter isReversed sorter data 515 | 516 | 517 | applySorter : Bool -> Sorter data -> List data -> List data 518 | applySorter isReversed sorter data = 519 | case sorter of 520 | None -> 521 | data 522 | 523 | Increasing sort -> 524 | sort data 525 | 526 | Decreasing sort -> 527 | List.reverse (sort data) 528 | 529 | IncOrDec sort -> 530 | if isReversed then List.reverse (sort data) else sort data 531 | 532 | DecOrInc sort -> 533 | if isReversed then sort data else List.reverse (sort data) 534 | 535 | 536 | findSorter : String -> List (ColumnData data msg) -> Maybe (Sorter data) 537 | findSorter selectedColumn columnData = 538 | case columnData of 539 | [] -> 540 | Nothing 541 | 542 | {name, sorter} :: remainingColumnData -> 543 | if name == selectedColumn then 544 | Just sorter 545 | else 546 | findSorter selectedColumn remainingColumnData 547 | 548 | 549 | 550 | -- SORTERS 551 | 552 | 553 | {-| Specifies a particular way of sorting data. 554 | -} 555 | type Sorter data 556 | = None 557 | | Increasing (List data -> List data) 558 | | Decreasing (List data -> List data) 559 | | IncOrDec (List data -> List data) 560 | | DecOrInc (List data -> List data) 561 | 562 | 563 | {-| A sorter for columns that are unsortable. Maybe you have a column in your 564 | table for delete buttons that delete the row. It would not make any sense to 565 | sort based on that column. 566 | -} 567 | unsortable : Sorter data 568 | unsortable = 569 | None 570 | 571 | 572 | {-| Create a sorter that can only display the data in increasing order. If we 573 | want a table of people, sorted alphabetically by name, we would say this: 574 | 575 | sorter : Sorter { a | name : comparable } 576 | sorter = 577 | increasingBy .name 578 | -} 579 | increasingBy : (data -> comparable) -> Sorter data 580 | increasingBy toComparable = 581 | Increasing (List.sortBy toComparable) 582 | 583 | 584 | {-| Create a sorter that can only display the data in decreasing order. If we 585 | want a table of countries, sorted by population from highest to lowest, we 586 | would say this: 587 | 588 | sorter : Sorter { a | population : comparable } 589 | sorter = 590 | decreasingBy .population 591 | -} 592 | decreasingBy : (data -> comparable) -> Sorter data 593 | decreasingBy toComparable = 594 | Decreasing (List.sortBy toComparable) 595 | 596 | 597 | {-| Sometimes you want to be able to sort data in increasing *or* decreasing 598 | order. Maybe you have a bunch of data about orange juice, and you want to know 599 | both which has the most sugar, and which has the least sugar. Both interesting! 600 | This function lets you see both, starting with decreasing order. 601 | 602 | sorter : Sorter { a | sugar : comparable } 603 | sorter = 604 | decreasingOrIncreasingBy .sugar 605 | -} 606 | decreasingOrIncreasingBy : (data -> comparable) -> Sorter data 607 | decreasingOrIncreasingBy toComparable = 608 | DecOrInc (List.sortBy toComparable) 609 | 610 | 611 | {-| Sometimes you want to be able to sort data in increasing *or* decreasing 612 | order. Maybe you have race times for the 100 meter sprint. This function lets 613 | sort by best time by default, but also see the other order. 614 | 615 | sorter : Sorter { a | time : comparable } 616 | sorter = 617 | increasingOrDecreasingBy .time 618 | -} 619 | increasingOrDecreasingBy : (data -> comparable) -> Sorter data 620 | increasingOrDecreasingBy toComparable = 621 | IncOrDec (List.sortBy toComparable) 622 | --------------------------------------------------------------------------------