Row DnD
Drag rows to reorder the underlying data using HTML5 drag events. Ports examples/react/row-dnd from TanStack Table.
RowDnd.elm
module RowDnd exposing (main)
{-| Row DnD.
Ports `examples/react/row-dnd/src/main.tsx` from TanStack Table. The React
example drives `dnd-kit`; this one uses plain HTML5 drag and drop
(`Shared.Drag`). As in the React version the table state is untouched: the
drop reorders the underlying data list, and `withGetRowId` keeps row ids
stable while the row indexes change.
Drag the 🟰 handle in the "Move" column onto another row.
-}
import Browser
import Html exposing (Html, button, div, pre, span, table, tbody, td, text, th, thead, tr)
import Html.Attributes exposing (class, classList, colspan, draggable, style, title)
import Html.Events exposing (onClick)
import Shared.Drag as Drag
import Shared.People as People exposing (Person)
import Shared.StateJson as StateJson
import Table
import Table.Value as Value
-- COLUMNS
config : Table.Config Person
config =
Table.config
[ -- A dedicated drag handle column, as in the React example.
Table.display "drag-handle"
|> Table.withHeader "Move"
|> Table.withSize 60
, Table.column "firstName" (.firstName >> Value.String)
, Table.column "lastName" (.lastName >> maybeString)
|> Table.withHeader "Last Name"
, Table.column "age" (.age >> toFloat >> Value.Number)
|> Table.withHeader "Age"
, Table.column "visits" (.visits >> maybeNumber)
|> Table.withHeader "Visits"
, Table.column "status" (.status >> People.statusToString >> Value.String)
|> Table.withHeader "Status"
, Table.column "progress" (.progress >> toFloat >> Value.Number)
|> Table.withHeader "Profile Progress"
]
-- required, because the row indexes change on every drop
|> Table.withGetRowId (\person _ _ -> person.id)
maybeString : Maybe String -> Value.Value
maybeString =
Maybe.map Value.String >> Maybe.withDefault Value.Null
maybeNumber : Maybe Int -> Value.Value
maybeNumber =
Maybe.map (toFloat >> Value.Number) >> Maybe.withDefault Value.Null
-- MODEL
type alias Model =
{ state : Table.State
, data : List Person
, seed : Int
, dragging : Maybe String
, over : Maybe String
}
init : Model
init =
{ state = Table.initialState
, data = People.makeData 42 [ 20 ]
, seed = 42
, dragging = Nothing
, over = Nothing
}
type Msg
= RegenerateData
| StressTest
| DragStarted String
| DraggedOver String
| Dropped String
| DragEnded
update : Msg -> Model -> Model
update msg model =
case msg of
RegenerateData ->
{ model | seed = model.seed + 1, data = People.makeData (model.seed + 1) [ 20 ] }
StressTest ->
{ model | seed = model.seed + 1, data = People.makeData (model.seed + 1) [ 1000 ] }
DragStarted rowId ->
{ model | dragging = Just rowId, over = Nothing }
DraggedOver rowId ->
{ model | over = Just rowId }
Dropped rowId ->
case model.dragging of
Just from ->
{ model
| data = Drag.moveBy .id from rowId model.data
, dragging = Nothing
, over = Nothing
}
Nothing ->
{ model | over = Nothing }
DragEnded ->
{ model | dragging = Nothing, over = Nothing }
-- VIEW
view : Model -> Html Msg
view model =
let
rowModel : Table.RowModel Person
rowModel =
Table.coreRowModelFromList config model.state model.data
in
div [ class "demo-root" ]
[ div [ class "spacer-md" ] []
, div [ class "button-row" ]
[ button [ onClick RegenerateData ] [ text "Regenerate Data" ]
, button [ onClick StressTest ] [ text "Stress Test (1k rows)" ]
]
, div [ class "spacer-md" ] []
, table []
[ thead [] (List.map (viewHeaderRow model.state) (Table.headerGroups config model.state))
, tbody [] (List.map (viewRow model) rowModel.rows)
]
, pre [ class "state-dump" ] [ text (StateJson.dump model.state) ]
]
viewHeaderRow : Table.State -> Table.HeaderGroup Person -> Html Msg
viewHeaderRow _ group =
tr []
(List.map
(\header ->
th [ colspan (Table.headerColSpan header) ]
[ span [] [ text (headerLabel header) ] ]
)
group.headers
)
viewRow : Model -> Table.Row Person -> Html Msg
viewRow model row =
let
rowId : String
rowId =
Table.rowId row
in
tr
[ classList
[ ( "dragging", model.dragging == Just rowId )
, ( "drop-target", model.over == Just rowId && model.dragging /= Just rowId )
]
, Drag.onDragOver (DraggedOver rowId)
, Drag.onDrop (Dropped rowId)
]
(List.map (viewCell model.state rowId) (Table.getAllCells config model.state row))
viewCell : Table.State -> String -> Table.Cell -> Html Msg
viewCell state rowId cell =
td [ style "width" (px (columnSize state cell.columnId)) ]
[ if cell.columnId == "drag-handle" then
button
[ draggable "true"
, title "Drag to reorder"
, Drag.onDragStart (DragStarted rowId)
, Drag.onDragEnd DragEnded
]
[ text "🟰" ]
else
text (Value.toString cell.value)
]
columnSize : Table.State -> String -> Float
columnSize state columnId =
Table.findColumn config columnId
|> Maybe.map (Table.getColumnSize config state)
|> Maybe.withDefault 0
px : Float -> String
px n =
String.fromFloat n ++ "px"
headerLabel : Table.Header Person -> String
headerLabel header =
Table.findColumn config (Table.headerColumnId header)
|> Maybe.andThen Table.columnHeader
|> Maybe.withDefault (Table.headerColumnId header)
main : Program () Model Msg
main =
Browser.sandbox { init = init, update = update, view = view }