Initial commit with draft API
This commit is contained in:
commit
5de13f7561
|
@ -0,0 +1 @@
|
||||||
|
elm-stuff
|
|
@ -0,0 +1,30 @@
|
||||||
|
Copyright (c) 2016-present, Evan Czaplicki
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
* Neither the name of Evan Czaplicki nor the names of other
|
||||||
|
contributors may be used to endorse or promote products derived
|
||||||
|
from this software without specific prior written permission.
|
||||||
|
|
||||||
|
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
|
||||||
|
OWNER 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.
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"summary": "Sortable tables for data of any shape.",
|
||||||
|
"repository": "https://github.com/evancz/elm-table.git",
|
||||||
|
"license": "BSD3",
|
||||||
|
"source-directories": [
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"exposed-modules": [
|
||||||
|
"Table"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"elm-lang/core": "4.0.0 <= v < 5.0.0",
|
||||||
|
"elm-lang/html": "1.1.0 <= v < 2.0.0"
|
||||||
|
},
|
||||||
|
"elm-version": "0.17.0 <= v < 0.18.0"
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
import Html exposing (Html, div, h1, text)
|
||||||
|
import Html.App as App
|
||||||
|
import Table
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
main =
|
||||||
|
App.program
|
||||||
|
{ init = init presidents
|
||||||
|
, update = update
|
||||||
|
, view = view
|
||||||
|
, subscriptions = \_ -> Sub.none
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- MODEL
|
||||||
|
|
||||||
|
|
||||||
|
type alias Model =
|
||||||
|
{ people : List Person
|
||||||
|
, tableState : Table.State
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
init : List Person -> ( Model, Cmd Msg )
|
||||||
|
init people =
|
||||||
|
( Model people (Table.ascending "Year")
|
||||||
|
, Cmd.none
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- UPDATE
|
||||||
|
|
||||||
|
|
||||||
|
type Msg
|
||||||
|
= UpdateTableState Table.State
|
||||||
|
|
||||||
|
|
||||||
|
update : Msg -> Model -> ( Model, Cmd Msg )
|
||||||
|
update msg model =
|
||||||
|
case msg of
|
||||||
|
UpdateTableState newState ->
|
||||||
|
( Model model.people newState, Cmd.none )
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- VIEW
|
||||||
|
|
||||||
|
|
||||||
|
view : Model -> Html Msg
|
||||||
|
view {people, tableState} =
|
||||||
|
div []
|
||||||
|
[ h1 [] [ text "Birthplaces of U.S. Presidents" ]
|
||||||
|
, Table.view config tableState people
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
config : Table.Config Person Msg
|
||||||
|
config =
|
||||||
|
Table.config
|
||||||
|
{ toId = .name
|
||||||
|
, toMsg = UpdateTableState
|
||||||
|
, columns =
|
||||||
|
[ Table.stringColumn "Name" .name
|
||||||
|
, Table.intColumn "Year" .year
|
||||||
|
, Table.stringColumn "City" .city
|
||||||
|
, Table.stringColumn "State" .state
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- PEOPLE
|
||||||
|
|
||||||
|
|
||||||
|
type alias Person =
|
||||||
|
{ name : String
|
||||||
|
, year : Int
|
||||||
|
, city : String
|
||||||
|
, state : String
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
presidents : List Person
|
||||||
|
presidents =
|
||||||
|
[ Person "George Washington" 1732 "Westmoreland County" "Virginia"
|
||||||
|
, Person "John Adams" 1735 "Braintree" "Massachusetts"
|
||||||
|
, Person "Thomas Jefferson" 1743 "Shadwell" "Virginia"
|
||||||
|
, Person "James Madison" 1751 "Port Conway" "Virginia"
|
||||||
|
, Person "James Monroe" 1758 "Monroe Hall" "Virginia"
|
||||||
|
, Person "Andrew Jackson" 1767 "Waxhaws Region" "South/North Carolina"
|
||||||
|
, Person "John Quincy Adams" 1767 "Braintree" "Massachusetts"
|
||||||
|
, Person "William Henry Harrison" 1773 "Charles City County" "Virginia"
|
||||||
|
, Person "Martin Van Buren" 1782 "Kinderhook" "New York"
|
||||||
|
, Person "Zachary Taylor" 1784 "Barboursville" "Virginia"
|
||||||
|
, Person "John Tyler" 1790 "Charles City County" "Virginia"
|
||||||
|
, Person "James Buchanan" 1791 "Cove Gap" "Pennsylvania"
|
||||||
|
, Person "James K. Polk" 1795 "Pineville" "North Carolina"
|
||||||
|
, Person "Millard Fillmore" 1800 "Summerhill" "New York"
|
||||||
|
, Person "Franklin Pierce" 1804 "Hillsborough" "New Hampshire"
|
||||||
|
, Person "Andrew Johnson" 1808 "Raleigh" "North Carolina"
|
||||||
|
, Person "Abraham Lincoln" 1809 "Sinking spring" "Kentucky"
|
||||||
|
, Person "Ulysses S. Grant" 1822 "Point Pleasant" "Ohio"
|
||||||
|
, Person "Rutherford B. Hayes" 1822 "Delaware" "Ohio"
|
||||||
|
, Person "Chester A. Arthur" 1829 "Fairfield" "Vermont"
|
||||||
|
, Person "James A. Garfield" 1831 "Moreland Hills" "Ohio"
|
||||||
|
, Person "Benjamin Harrison" 1833 "North Bend" "Ohio"
|
||||||
|
, Person "Grover Cleveland" 1837 "Caldwell" "New Jersey"
|
||||||
|
, Person "William McKinley" 1843 "Niles" "Ohio"
|
||||||
|
, Person "Woodrow Wilson" 1856 "Staunton" "Virginia"
|
||||||
|
, Person "William Howard Taft" 1857 "Cincinnati" "Ohio"
|
||||||
|
, Person "Theodore Roosevelt" 1858 "New York City" "New York"
|
||||||
|
, Person "Warren G. Harding" 1865 "Blooming Grove" "Ohio"
|
||||||
|
, Person "Calvin Coolidge" 1872 "Plymouth" "Vermont"
|
||||||
|
, Person "Herbert Hoover" 1874 "West Branch" "Iowa"
|
||||||
|
, Person "Franklin D. Roosevelt" 1882 "Hyde Park" "New York"
|
||||||
|
, Person "Harry S. Truman" 1884 "Lamar" "Missouri"
|
||||||
|
, Person "Dwight D. Eisenhower" 1890 "Denison" "Texas"
|
||||||
|
, Person "Lyndon B. Johnson" 1908 "Stonewall" "Texas"
|
||||||
|
, Person "Ronald Reagan" 1911 "Tampico" "Illinois"
|
||||||
|
, Person "Richard M. Nixon" 1913 "Yorba Linda" "California"
|
||||||
|
, Person "Gerald R. Ford" 1913 "Omaha" "Nebraska"
|
||||||
|
, Person "John F. Kennedy" 1917 "Brookline" "Massachusetts"
|
||||||
|
, Person "George H. W. Bush" 1924 "Milton" "Massachusetts"
|
||||||
|
, Person "Jimmy Carter" 1924 "Plains" "Georgia"
|
||||||
|
, Person "George W. Bush" 1946 "New Haven" "Connecticut"
|
||||||
|
, Person "Bill Clinton" 1946 "Hope" "Arkansas"
|
||||||
|
, Person "Barack Obama" 1961 "Honolulu" "Hawaii"
|
||||||
|
]
|
|
@ -0,0 +1,591 @@
|
||||||
|
module Table exposing
|
||||||
|
( view
|
||||||
|
, State, initialSort
|
||||||
|
, Config, config
|
||||||
|
, Column, stringColumn, intColumn, floatColumn, column
|
||||||
|
, Sorter, unsortable, increasingBy, decreasingBy
|
||||||
|
, increasingOrDecreasingBy, decreasingOrIncreasingBy
|
||||||
|
)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
This library helps you create sortable tables. The crucial feature is that it
|
||||||
|
lets you own your data separately and keep it in whatever format is best for
|
||||||
|
you. This way you are free to change your data without worrying about the table
|
||||||
|
“getting out of sync” with the data. Having a single source of
|
||||||
|
truth is pretty great!
|
||||||
|
|
||||||
|
I recommend checking out the [examples][] to get a feel for how it works.
|
||||||
|
|
||||||
|
[examples]: https://github.com/evancz/elm-tables/tree/master/examples
|
||||||
|
|
||||||
|
# View
|
||||||
|
@docs view
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
@docs config, stringColumn, intColumn, floatColumn
|
||||||
|
|
||||||
|
# State
|
||||||
|
@docs State, initialSort
|
||||||
|
|
||||||
|
|
||||||
|
# Crazy Customization
|
||||||
|
|
||||||
|
If you are new to this library, you can probably stop reading here. After this
|
||||||
|
point are a bunch of ways to customize your table further. If it does not
|
||||||
|
provide what you need, you may just want to write a custom table yourself. It
|
||||||
|
is not that crazy.
|
||||||
|
|
||||||
|
## Custom Columns
|
||||||
|
|
||||||
|
@docs Column, customColumn, veryCustomColumn,
|
||||||
|
Sorter, unsortable, increasingBy, decreasingBy,
|
||||||
|
increasingOrDecreasingBy, decreasingOrIncreasingBy
|
||||||
|
|
||||||
|
## Custom Tables
|
||||||
|
|
||||||
|
@docs Config, customConfig,
|
||||||
|
Customizations, HtmlDetails, Status, defaultCustomizations
|
||||||
|
-}
|
||||||
|
|
||||||
|
import Html exposing (Html, Attribute)
|
||||||
|
import Html.Attributes as Attr
|
||||||
|
import Html.Events as E
|
||||||
|
import Html.Keyed as Keyed
|
||||||
|
import Html.Lazy exposing (lazy2, lazy3)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- STATE
|
||||||
|
|
||||||
|
|
||||||
|
{-| Tracks which column to sort by.
|
||||||
|
-}
|
||||||
|
type State =
|
||||||
|
State String Bool
|
||||||
|
|
||||||
|
|
||||||
|
{-| Create a table state. By providing a column name, you determine which
|
||||||
|
column should be used for sorting by default. So if you want your table of
|
||||||
|
yachts to be sorted by length by default, you might say:
|
||||||
|
|
||||||
|
import Table
|
||||||
|
|
||||||
|
Table.initialSort "Length"
|
||||||
|
-}
|
||||||
|
initialSort : String -> State
|
||||||
|
initialSort header =
|
||||||
|
State header False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
{-| Configuration for your table, describing your columns.
|
||||||
|
|
||||||
|
**Note:** Your `Config` should *never* be held in your model.
|
||||||
|
It should only appear in `view` code.
|
||||||
|
-}
|
||||||
|
type Config data msg =
|
||||||
|
Config
|
||||||
|
{ toId : data -> String
|
||||||
|
, toMsg : State -> msg
|
||||||
|
, columns : List (Column data msg)
|
||||||
|
, customizations : Customizations data msg
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Create the `Config` for your `view` function. Everything you need to
|
||||||
|
render your columns efficiently and handle selection of columns.
|
||||||
|
|
||||||
|
Say we have a `List Person` that we want to show as a table. The table should
|
||||||
|
have a column for name and age. We would create a `Config` like this:
|
||||||
|
|
||||||
|
import Table
|
||||||
|
|
||||||
|
type Msg = NewTableState State | ...
|
||||||
|
|
||||||
|
config : Table.Config Person Msg
|
||||||
|
config =
|
||||||
|
Table.config
|
||||||
|
{ toId = .name
|
||||||
|
, toMsg = NewTableState
|
||||||
|
, columns =
|
||||||
|
[ Table.stringColumn "Name" .name
|
||||||
|
, Table.intColumn "Age" .age
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
You provide the following information in your table configuration:
|
||||||
|
|
||||||
|
- `toId` — turn a `Person` into a unique ID. This lets us use
|
||||||
|
[`Html.Keyed`][keyed] under the hood to make resorts faster.
|
||||||
|
- `columns` — specify some columns to show.
|
||||||
|
- `toMsg` — a way send new table states to your app as messages.
|
||||||
|
|
||||||
|
See the [examples][] to get a better feel for this!
|
||||||
|
|
||||||
|
[keyed]: http://package.elm-lang.org/packages/elm-lang/html/latest/Html-Keyed
|
||||||
|
[examples]: https://github.com/evancz/elm-tables/tree/master/examples
|
||||||
|
-}
|
||||||
|
config
|
||||||
|
: { toId : data -> String
|
||||||
|
, toMsg : State -> msg
|
||||||
|
, columns : List (Column data msg)
|
||||||
|
}
|
||||||
|
-> Config data msg
|
||||||
|
config { toId, toMsg, columns } =
|
||||||
|
Config
|
||||||
|
{ toId = toId
|
||||||
|
, toMsg = toMsg
|
||||||
|
, columns = List.map (\(Column cData) -> cData) columns
|
||||||
|
, customizations = defaultCustomizations
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
customConfig
|
||||||
|
: { toId : data -> String
|
||||||
|
, toMsg : State -> msg
|
||||||
|
, columns : List (Column data msg)
|
||||||
|
, customizations : Customizations data msg
|
||||||
|
}
|
||||||
|
-> Config data msg
|
||||||
|
customConfig { toId, toMsg, columns, customizations } =
|
||||||
|
Config
|
||||||
|
{ toId = toId
|
||||||
|
, toMsg = toMsg
|
||||||
|
, columns = List.map (\(Column cData) -> cData) columns
|
||||||
|
, customizations = customizations
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| There are quite a lot of ways to customize the `<table>` tag. You can add
|
||||||
|
a `<caption>` which can be styled via CSS. You can do crazy stuff with
|
||||||
|
`<thead>` to group columns in weird ways. You can have a `<tfoot>` tag for
|
||||||
|
summaries of various columns. And maybe you want to put attributes on `<tbody>`
|
||||||
|
or on particular rows in the body. All these customizations are available to you.
|
||||||
|
|
||||||
|
**Note:** The level of craziness possible in `<thead>` and `<tfoot>` are so
|
||||||
|
high that I could not see how to provide the full functionality *and* make it
|
||||||
|
impossible to do bad stuff. So just be aware of that, and share any stories
|
||||||
|
you have. Stories make it possible to design better!
|
||||||
|
-}
|
||||||
|
type alias Cusomizations data msg =
|
||||||
|
{ tableAttrs : List (Attribute msg)
|
||||||
|
, caption : Maybe (HtmlDetails msg)
|
||||||
|
, thead : List (String, Status, Attribute msg) -> HtmlDetails msg
|
||||||
|
, tfoot : Maybe (HtmlDetails msg)
|
||||||
|
, tbodyAttrs : List (Attribute msg)
|
||||||
|
, rowAttrs : data -> List (Attribute msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Sometimes you must use a `<td>` tag, but the attributes and children are up
|
||||||
|
to you. This type lets you specify all the details of an HTML node except the
|
||||||
|
tag name.
|
||||||
|
-}
|
||||||
|
type alias HtmlDetails msg =
|
||||||
|
{ attributes : List (Attribute msg)
|
||||||
|
, children : List (Html msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
defaultCustomizations : Customizations data msg
|
||||||
|
defaultCustomizations =
|
||||||
|
{ tableAttrs = []
|
||||||
|
, caption = Nothing
|
||||||
|
, thead = simpleThead
|
||||||
|
, tfoot = Nothing
|
||||||
|
, tbodyAttrs = []
|
||||||
|
, rowAttrs = simpleRowAttrs
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
simpleThead : List (String, Status, Attribute msg) -> HtmlDetails msg
|
||||||
|
simpleThead headers =
|
||||||
|
HtmlDetails [] (List.map simpleTheadHelp headers)
|
||||||
|
|
||||||
|
|
||||||
|
simpleTheadHelp : ( String, Status, Attribute msg ) -> Html msg
|
||||||
|
simpleTheadHelp (name, status, onClick) =
|
||||||
|
let
|
||||||
|
content =
|
||||||
|
case status of
|
||||||
|
Unsortable ->
|
||||||
|
[ text name ]
|
||||||
|
|
||||||
|
Sortable selected ->
|
||||||
|
[ text name
|
||||||
|
, if selected then darkGrey "↓" else lightGrey "↓"
|
||||||
|
]
|
||||||
|
|
||||||
|
Reversable Nothing ->
|
||||||
|
[ text name
|
||||||
|
, lightGrey "↕"
|
||||||
|
]
|
||||||
|
|
||||||
|
Reversable (Just isReversed) ->
|
||||||
|
[ text name
|
||||||
|
, darkGrey (if isReversed then "↑" else "↓")
|
||||||
|
]
|
||||||
|
in
|
||||||
|
Html.th [ onClick ] content
|
||||||
|
|
||||||
|
|
||||||
|
darkGrey : String -> Html msg
|
||||||
|
darkGrey symbol =
|
||||||
|
Html.span [ Attr.style [("color", "#ccc")] ] [ Html.text (" " ++ symbol) ]
|
||||||
|
|
||||||
|
|
||||||
|
lightGrey : String -> Html msg
|
||||||
|
lightGrey symbol =
|
||||||
|
Html.span [ Attr.style [("color", "#999")] ] [ Html.text (" " ++ symbol) ]
|
||||||
|
|
||||||
|
|
||||||
|
simpleRowAttrs : data -> Attribute msg
|
||||||
|
simpleRowAttrs _ =
|
||||||
|
[]
|
||||||
|
|
||||||
|
|
||||||
|
type Status
|
||||||
|
= Unsortable
|
||||||
|
| Sortable Bool
|
||||||
|
| Reversable (Maybe Bool)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- COLUMNS
|
||||||
|
|
||||||
|
|
||||||
|
{-| Describes how to turn `data` into a column in your table.
|
||||||
|
-}
|
||||||
|
type Column data msg =
|
||||||
|
Column (ColumnData data msg)
|
||||||
|
|
||||||
|
|
||||||
|
type alias ColumnData data msg =
|
||||||
|
{ name : String
|
||||||
|
, viewData : data -> HtmlDetails msg
|
||||||
|
, sorter : Sorter data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-|-}
|
||||||
|
stringColumn : String -> (data -> String) -> Column data msg
|
||||||
|
stringColumn name toStr =
|
||||||
|
Column
|
||||||
|
{ name = name
|
||||||
|
, viewData = textDetails << toStr
|
||||||
|
, sorter = increasingOrDecreasingBy toStr
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-|-}
|
||||||
|
intColumn : String -> (data -> Int) -> Column data msg
|
||||||
|
intColumn name toInt =
|
||||||
|
Column
|
||||||
|
{ name = name
|
||||||
|
, viewData = textDetails << toString << toInt
|
||||||
|
, sorter = increasingOrDecreasingBy toInt
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-|-}
|
||||||
|
floatColumn : String -> (data -> Float) -> Column data msg
|
||||||
|
floatColumn name toFloat =
|
||||||
|
Column
|
||||||
|
{ name = name
|
||||||
|
, viewData = textDetails << toString << toFloat
|
||||||
|
, sorter = increasingOrDecreasingBy toFloat
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
textDetails : String -> HtmlDetails msg
|
||||||
|
textDetails str =
|
||||||
|
HtmlDetails [] [ Html.text str ]
|
||||||
|
|
||||||
|
|
||||||
|
{-| Perhaps the basic columns are not quite what you want. Maybe you want to
|
||||||
|
display monetary values in thousands of dollars, and `floatColumn` does not
|
||||||
|
quite cut it. You could define a custom column like this:
|
||||||
|
|
||||||
|
import Table
|
||||||
|
|
||||||
|
dollarColumn : String -> (data -> Float) -> Column data msg
|
||||||
|
dollarColumn name toDollars =
|
||||||
|
Table.customColumn
|
||||||
|
{ name = name
|
||||||
|
, viewData = \data -> viewDollars (toDollars data)
|
||||||
|
, sorter = Table.decreasingBy toDollars
|
||||||
|
}
|
||||||
|
|
||||||
|
viewDollars : Float -> String
|
||||||
|
viewDollars dollars =
|
||||||
|
"$" ++ toString (round (dollars / 1000)) ++ "k"
|
||||||
|
|
||||||
|
The `viewData` field means we will displays the number `12345.67` as `$12k`.
|
||||||
|
|
||||||
|
The `sorter` field specifies how the column can be sorted. In `dollarColumn` we
|
||||||
|
are saying that it can *only* be shown from highest-to-lowest monetary value.
|
||||||
|
More about sorters soon!
|
||||||
|
-}
|
||||||
|
customColumn
|
||||||
|
: { name : String
|
||||||
|
, viewData : data -> String
|
||||||
|
, sorter : Sorter data
|
||||||
|
}
|
||||||
|
-> Column data msg
|
||||||
|
customColumn { name, viewData, sorter } =
|
||||||
|
Column <|
|
||||||
|
ColumnData name (textDetails << viewData) sorter
|
||||||
|
|
||||||
|
|
||||||
|
{-| It is *possible* that you want something crazier than `customColumn`. In
|
||||||
|
that unlikely scenario, this function lets you have full control over the
|
||||||
|
attributes and children of each `<td>` cell in this column.
|
||||||
|
|
||||||
|
So maybe you want to a dollars column, and the dollar signs should be green.
|
||||||
|
|
||||||
|
import Html exposing (Html, Attribute, span, text)
|
||||||
|
import Html.Attributes exposing (style)
|
||||||
|
import Table
|
||||||
|
|
||||||
|
dollarColumn : String -> (data -> Float) -> Column data msg
|
||||||
|
dollarColumn name toDollars =
|
||||||
|
Table.veryCustomColumn
|
||||||
|
{ name = name
|
||||||
|
, viewData = \data -> viewDollars (toDollars data)
|
||||||
|
, sorter = Table.decreasingBy toDollars
|
||||||
|
}
|
||||||
|
|
||||||
|
viewDollars : Float -> Table.HtmlDetails msg
|
||||||
|
viewDollars dollars =
|
||||||
|
Table.HtmlDetails []
|
||||||
|
[ span [ style [("color","green")] ] [ text "$" ]
|
||||||
|
, text (toString (round (dollars / 1000)) ++ "k")
|
||||||
|
]
|
||||||
|
-}
|
||||||
|
veryCustomColumn
|
||||||
|
: { name : String
|
||||||
|
, viewData : data -> HtmlDetails msg
|
||||||
|
, sorter : Sorter data
|
||||||
|
}
|
||||||
|
-> Column data msg
|
||||||
|
veryCustomColumn =
|
||||||
|
Column
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- VIEW
|
||||||
|
|
||||||
|
|
||||||
|
{-| Take a list of data and turn it into a table. The `Config` argument is the
|
||||||
|
configuration for the table. It describes the columns that we want to show. The
|
||||||
|
`State` argument describes which column we are sorting by at the moment.
|
||||||
|
|
||||||
|
**Note:** The `State` and `List data` should live in your `Model`. The `Config`
|
||||||
|
for the table belongs in your `view` code. I very strongly recommend against
|
||||||
|
putting `Config` in your model. Describe any potential table configurations
|
||||||
|
statically, and look for a different library if you need something crazier than
|
||||||
|
that.
|
||||||
|
-}
|
||||||
|
view : Config data msg -> State -> List data -> Html msg
|
||||||
|
view (Config { toId, toMsg, columns, customizations }) state data =
|
||||||
|
let
|
||||||
|
sortedData =
|
||||||
|
sort state columns data
|
||||||
|
|
||||||
|
theadDetails =
|
||||||
|
customizations.thead (List.map Debug.crash columns)
|
||||||
|
|
||||||
|
thead =
|
||||||
|
Html.thead theadDetails.attributes theadDetails.children
|
||||||
|
|
||||||
|
tbody =
|
||||||
|
List.map (viewRow toId columns) sortedData
|
||||||
|
|
||||||
|
withFoot =
|
||||||
|
case customizations.tfoot of
|
||||||
|
Nothing ->
|
||||||
|
tbody
|
||||||
|
|
||||||
|
Just { attributes, children } ->
|
||||||
|
Html.tfoot attributes children :: tbody
|
||||||
|
in
|
||||||
|
Html.table customizations.tableAttrs <|
|
||||||
|
case customizations.caption of
|
||||||
|
Nothing ->
|
||||||
|
thead :: withFoot
|
||||||
|
|
||||||
|
Just { attributes, children } ->
|
||||||
|
Html.caption attributes children :: thead :: withFoot
|
||||||
|
|
||||||
|
|
||||||
|
viewHeader : List (ColumnData a msg) -> (State -> msg) -> State -> Html msg
|
||||||
|
viewHeader columnData toMsg state =
|
||||||
|
Html.tr [] (List.map (lazy3 viewHeaderHelp toMsg state) columnData)
|
||||||
|
|
||||||
|
|
||||||
|
viewHeaderHelp : (State -> msg) -> State -> ColumnData a msg -> Html msg
|
||||||
|
viewHeaderHelp toMsg state ({name} as column) =
|
||||||
|
let
|
||||||
|
|
||||||
|
Html.th
|
||||||
|
[ class (String.join " " classes)
|
||||||
|
, onClick name state toMsg
|
||||||
|
]
|
||||||
|
[ html
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
onClick : String -> State -> (State -> msg) -> Attribute msg
|
||||||
|
onClick name (State selectedColumn isReversed) toMsg =
|
||||||
|
E.on "click" <| Json.map toMsg <|
|
||||||
|
Json.object2
|
||||||
|
State
|
||||||
|
(Json.succed name)
|
||||||
|
(Json.succed (name == selectedColumn && not isReversed)
|
||||||
|
|
||||||
|
|
||||||
|
descendingClass : Attribute msg
|
||||||
|
descendingClass =
|
||||||
|
class "elm-table-selected elm-table-descending"
|
||||||
|
|
||||||
|
|
||||||
|
ascendingClass : Attribute msg
|
||||||
|
ascendingClass =
|
||||||
|
class "elm-table-selected elm-table-ascending"
|
||||||
|
|
||||||
|
|
||||||
|
viewRow : (a -> String) -> List (ColumnData a msg) -> a -> ( String, Html msg )
|
||||||
|
viewRow toId columnData entry =
|
||||||
|
( toId entry
|
||||||
|
, lazy2 viewRowHelp columnData entry
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
viewRowHelp : List (ColumnData a msg) -> a -> Html msg
|
||||||
|
viewRowHelp columnData entry =
|
||||||
|
Html.tr [] (List.map (\{toCell} -> Html.td [] [ toCell entry ]) columnData)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- SORTING
|
||||||
|
|
||||||
|
|
||||||
|
sort : State -> List (ColumnData data msg) -> List data -> List data
|
||||||
|
sort (State selectedColumn isReversed) columnData data =
|
||||||
|
case findSorter selectedColumn columnData of
|
||||||
|
Nothing ->
|
||||||
|
data
|
||||||
|
|
||||||
|
Just sorter ->
|
||||||
|
applySorter isReversed sorter data
|
||||||
|
|
||||||
|
|
||||||
|
applySorter : Bool -> Sorter data -> List data -> List data
|
||||||
|
applySorter isReversed sorter data =
|
||||||
|
case sorter of
|
||||||
|
None ->
|
||||||
|
data
|
||||||
|
|
||||||
|
Increasing sort ->
|
||||||
|
sort data
|
||||||
|
|
||||||
|
Decreasing sort ->
|
||||||
|
List.reverse (sort data)
|
||||||
|
|
||||||
|
IncOrDec sort ->
|
||||||
|
if isReversed then List.reverse (sort data) else sort data
|
||||||
|
|
||||||
|
DecOrInc sort ->
|
||||||
|
if isReversed then sort data else List.reverse (sort data)
|
||||||
|
|
||||||
|
|
||||||
|
findSorter : String -> List (ColumnData data msg) -> Maybe (Sorter data)
|
||||||
|
findSorter selectedColumn columnData =
|
||||||
|
case columnData of
|
||||||
|
[] ->
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
{name, sorter} :: remainingColumnData ->
|
||||||
|
if name == selectedColumn then
|
||||||
|
sorter
|
||||||
|
else
|
||||||
|
findSorter selectedColumn remainingColumnData
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- SORTERS
|
||||||
|
|
||||||
|
|
||||||
|
{-| Specifies a particular way of sorting data.
|
||||||
|
-}
|
||||||
|
type Sorter data
|
||||||
|
= None
|
||||||
|
| Increasing (List data -> List data)
|
||||||
|
| Decreasing (List data -> List data)
|
||||||
|
| IncOrDec (List data -> List data)
|
||||||
|
| DecOrInc (List data -> List data)
|
||||||
|
|
||||||
|
|
||||||
|
{-| A sorter for columns that are unsortable. Maybe you have a column in your
|
||||||
|
table for delete buttons that delete the row. It would not make any sense to
|
||||||
|
sort based on that column.
|
||||||
|
-}
|
||||||
|
unsortable : Sorter data
|
||||||
|
unsortable =
|
||||||
|
None
|
||||||
|
|
||||||
|
|
||||||
|
{-| Create a sorter that can only display the data in increasing order. If we
|
||||||
|
want a table of people, sorted alphabetically by name, we would say this:
|
||||||
|
|
||||||
|
sorter : Sorter { a | name : comparable }
|
||||||
|
sorter =
|
||||||
|
increasingBy .name
|
||||||
|
-}
|
||||||
|
increasingBy : (data -> comparable) -> Sorter data
|
||||||
|
increasingBy toComparable =
|
||||||
|
Increasing (List.sortBy toComparable)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Create a sorter that can only display the data in decreasing order. If we
|
||||||
|
want a table of countries, sorted by population from highest to lowest, we
|
||||||
|
would say this:
|
||||||
|
|
||||||
|
sorter : Sorter { a | population : comparable }
|
||||||
|
sorter =
|
||||||
|
decreasingBy .population
|
||||||
|
-}
|
||||||
|
decreasingBy : (data -> comparable) -> Sorter data
|
||||||
|
decreasingBy toComparable =
|
||||||
|
Decreasing (List.sortBy toComparable)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Sometimes you want to be able to sort data in increasing *or* decreasing
|
||||||
|
order. Maybe you have a bunch of data about orange juice, and you want to know
|
||||||
|
both which has the most sugar, and which has the least sugar. Both interesting!
|
||||||
|
This function lets you see both, starting with decreasing order.
|
||||||
|
|
||||||
|
sorter : Sorter { a | sugar : comparable }
|
||||||
|
sorter =
|
||||||
|
decreasingOrIncreasingBy .sugar
|
||||||
|
-}
|
||||||
|
decreasingOrIncreasingBy : (data -> comparable) -> Sorter data
|
||||||
|
decreasingOrIncreasingBy toComparable =
|
||||||
|
DecOrInc (List.sortBy toComparable)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Sometimes you want to be able to sort data in increasing *or* decreasing
|
||||||
|
order. Maybe you have race times for the 100 meter sprint. This function lets
|
||||||
|
sort by best time by default, but also see the other order.
|
||||||
|
|
||||||
|
sorter : Sorter { a | time : comparable }
|
||||||
|
sorter =
|
||||||
|
increasingOrDecreasingBy .time
|
||||||
|
-}
|
||||||
|
increasingOrDecreasingBy : (data -> comparable) -> Sorter data
|
||||||
|
increasingOrDecreasingBy toComparable =
|
||||||
|
IncOrDec (List.sortBy toComparable)
|
Loading…
Reference in New Issue