annaghi/dnd-list/6.0.1

DnDList

While dragging and dropping a list item, the mouse events, the ghost element's positioning and the list sorting are handled internally by this module. Here is a basic demo, we will use it as an illustration throughout this page.

The first step is to create a System object which holds all the information related to the drag and drop features. Using this object you can wire up the module's internal model, subscriptions, commands, and update into your model, subscriptions, commands, and update respectively.

Next, when you write your view functions, you will need to bind the drag and drop events to the list items, and also style them according to their current state. The System object gives you access to events and to detailed information about the drag source and drop target items.

Finally, you will need to render a ghost element to be used for dragging display. You can add position styling attributes to this element using theSystem object.

 

Meaningful type aliases

type alias DragIndex =
    Int


type alias DropIndex =
    Int


type alias DragElementId =
    String


type alias DropElementId =
    String


type alias Position =
    { x : Float
    , y : Float
    }

System

type alias System a msg =
{ model : Model
, subscriptions : Model -> Sub msg
, commands : Model -> Cmd msg
, update : Msg -> Model -> List a -> ( Model, List a )
, dragEvents : DragIndex -> DragElementId -> List (Attribute msg)
, dropEvents : DropIndex -> DropElementId -> List (Attribute msg)
, ghostStyles : Model -> List (Attribute msg)
, info : Model -> Maybe Info
}

A System encapsulates:

  • the internal model, subscriptions, commands, and update,

  • the bindable events and styles, and

  • the Info object.

Later we will learn more about the Info object and the System fields.

create : Config a -> (Msg -> msg) -> System a msg

Creates a System object according to the configuration.

Suppose we have a list of fruits:

type alias Fruit =
    String


data : List Fruit
data =
    [ "Apples", "Bananas", "Cherries", "Dates" ]

Now the System is a wrapper type around the list item and our message types:

system : DnDList.System Fruit Msg
system =
    DnDList.create config MyMsg
type Msg

Internal message type. It should be wrapped within our message constructor:

type Msg
    = MyMsg DnDList.Msg

Config

type alias Config a =
{ beforeUpdate : DragIndex -> DropIndex -> List a -> List a
, movement : Movement
, listen : Listen
, operation : Operation
}

Represents the System's configuration.

  • beforeUpdate: This is a hook and gives you access to your list before it will be sorted. The first number is the drag index, the second number is the drop index. The Towers of Hanoi uses this hook to update the disks' tower attribute.

  • movement: The dragging can be constrained to horizontal or vertical axis only, or it can be set to free. This demo config shows the different movements in action.

  • listen: The items can listen for drag events or for drop events. In the first case the list will be sorted again and again while the mouse moves over the different drop target items. In the second case the list will be sorted only once on that drop target where the mouse was finally released.

  • operation: Different kinds of sort operations can be performed on the list. You can start to analyze them with sorting on drag and sorting on drop.

This is our configuration with a void beforeUpdate:

config : DnDList.Config Fruit
config =
    { beforeUpdate = \_ _ list -> list
    , movement = DnDList.Free
    , listen = DnDList.OnDrag
    , operation = DnDList.Rotate
    }
= Free
| Horizontal
| Vertical

Represents the mouse dragging movement. This demo config shows the different movements in action.

  • Free : The ghost element follows the mouse pointer.

  • Horizontal : The ghost element can only move horizontally.

  • Vertical : The ghost element can only move vertically.

type Listen
= OnDrag
| OnDrop

Represents the event for which the list sorting is available.

  • OnDrag: The list will be sorted when the ghost element is being dragged over a drop target item.

  • OnDrop: The list will be sorted when the ghost element is dropped on a drop target item.

= InsertAfter
| InsertBefore
| Rotate
| Swap
| Unaltered

Represents the list sort operation. Detailed comparisons can be found here: sorting on drag and sorting on drop.

  • InsertAfter: The drag source item will be inserted after the drop target item.

  • InsertBefore: The drag source item will be inserted before the drop target item.

  • Rotate: The items between the drag source and the drop target items will be circularly shifted.

  • Swap: The drag source and the drop target items will be swapped.

  • Unaltered: The list items will keep their initial order.

Info

type alias Info =
{ dragIndex : DragIndex
, dropIndex : DropIndex
, dragElementId : DragElementId
, dropElementId : DropElementId
, dragElement : Element
, dropElement : Element
, startPosition : Position
, currentPosition : Position
}

Represents the information about the drag source and the drop target items. It is accessible through the System's info field.

  • dragIndex: The index of the drag source.

  • dropIndex: The index of the drop target.

  • dragElementId: HTML id of the drag source.

  • dropElementId: HTML id of the drop target.

  • dragElement: Information about the drag source as an HTML element, see Browser.Dom.Element.

  • dropElement: Information about the drop target as an HTML element, see Browser.Dom.Element.

  • startPosition: The x, y position of the ghost element when dragging started.

  • currentPosition: The x, y position of the ghost element now.

You can check the Info object to decide what to render when there is an ongoing dragging, and what to render when there is no dragging:

itemView : DnDList.Model -> Int -> Fruit -> Html.Html Msg
itemView dnd index item =
    ...
    case system.info dnd of
        Just _ ->
            -- Render when there is an ongoing dragging.


        Nothing ->
            -- Render when there is no dragging.

Or you can determine the current drag source item using the Info object:

maybeDragItem : DnDList.Model -> List Fruit -> Maybe Fruit
maybeDragItem dnd items =
    system.info dnd
        |> Maybe.andThen
            (\{ dragIndex } ->
                items
                    |> List.drop dragIndex
                    |> List.head
            )

Or you can control over generating styles for the dragged ghost element. For example adding an offset to the position:

type alias Offset =
    { x : Int
    , y : Int
    }


customGhostStyle : DnDList.Model -> DnDList.Info -> Offset -> List (Html.Attribute msg)
customGhostStyle dnd { element } offset =
    let
        px : Int -> String
        px x =
            String.fromInt x ++ "px"


        translate : Int -> Int -> String
        translate x y =
            "translate3d(" ++ px x ++ ", " ++ px y ++ ", 0)"
    in
    case system.info dnd of
        Just { currentPosition, startPosition } ->
            [ Html.Attribute.style "transform" <|
                translate
                    (round element.x + offset.x)
                    (round (currentPosition.y - startPosition.y + element.y) + offset.y)
            ]


        Nothing ->
            []

System fields

model

type Model

Represents the internal model of the current drag and drop features. It will be Nothing if there is no ongoing dragging. You should set it in your model and initialize through the System's model field.

type alias Model =
    { dnd : DnDList.Model
    , items : List Fruit
    }


initialModel : Model
initialModel =
    { dnd = system.model
    , items = data
    }

subscriptions

subscriptions is a function to access the browser events during the dragging.

subscriptions : Model -> Sub Msg
subscriptions model =
    system.subscriptions model.dnd

commands

commands is a function to access the DOM for the drag source and the drop target as HTML elements.

update : Msg -> Model -> ( Model, Cmd Msg )
update message model =
    case message of
        MyMsg msg ->
            let
                updatedModel = ...
            in
            ( updatedModel
            , system.commands updatedModel
            )

update

update is a function which returns an updated internal Model and the sorted list for your model.

update : Msg -> Model -> ( Model, Cmd Msg )
update message model =
    case message of
        MyMsg msg ->
            let
                ( dnd, items ) =
                    system.update msg model.dnd model.items
            in
            ( { model | dnd = dnd, items = items }
            , system.commands dnd
            )

dragEvents

dragEvents is a function which wraps all the events up for the drag source items.

This and the following example will show us how to use auxiliary items and think about them in two different ways:

  • as ordinary list items from the list operation point of view, and
  • as specially styled elements from the HTML design point of view.
  itemView : DnDList.Model -> Int -> Fruit -> Html.Html Msg
  itemView dnd index item =
      let
          itemId : String
          itemId =
              "id-" ++ item
      in
      case system.info dnd of
          Just _ ->
              -- Render when there is an ongoing dragging.

          Nothing ->
              Html.p
                  (Html.Attributes.id itemId
                      :: system.dragEvents index itemId
                  )
                  [ Html.text item ]

dropEvents

dropEvents is a function which wraps all the events up for the drop target items.

itemView : DnDList.Model -> Int -> Fruit -> Html.Html Msg
itemView dnd index item =
    let
        itemId : String
        itemId =
            "id-" ++ item
    in
    case system.info dnd of
        Just { dragIndex } ->
            if dragIndex /= index then
                Html.p
                    (Html.Attributes.id itemId
                        :: system.dropEvents index itemId
                    )
                    [ Html.text item ]


            else
                Html.p
                    [ Html.Attributes.id itemId ]
                    [ Html.text "[---------]" ]


        Nothing ->
            -- Render when there is no dragging.

ghostStyles

ghostStyles is a function which wraps up the positioning styles of the ghost element. The ghost element has absolute position relative to the viewport.

ghostView : DnDList.Model -> List Fruit -> Html.Html Msg
ghostView dnd items =
    let
        maybeDragItem : Maybe Fruit
        maybeDragItem =
            system.info dnd
                |> Maybe.andThen
                    (\{ dragIndex } ->
                        items
                            |> List.drop dragIndex
                            |> List.head
                    )
    in
    case maybeDragItem of
        Just item ->
            Html.div
                (system.ghostStyles dnd)
                [ Html.text item ]


        Nothing ->
            Html.text ""

The following CSS will be added:

{
    position: fixed;
    left: 0;
    top: 0;
    transform: translate3d(the vector is calculated from the dragElement and the mouse position in pixels);
    height: the dragElement's height in pixels;
    width: the dragElement's width in pixels;
    pointer-events: none;
}

info

See Info.