├── .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 `
` 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 | -------------------------------------------------------------------------------- |