(λx.x) Ben Hanna

Software Engineer and Functional Programming Enthusiast

Basic HTML5 Drag and Drop with Elm


  • Mon 18 June 2018
  • Blog

In doing some research for a potential project I decided to see how drag and drop functionality can be implemented in Elm.

Thankfully, it looks like it's not too hard to achieve since drag and drop is now part of the HTML5 standard. To demonstrate this I'll show you how to build an application that allows you to construct a list by dragging items onto it.

To follow along with this tutorial I recommend building an application with Ellie.

Getting Started

To get things started we'll start with a Browser.sandbox since we we're not doing anything fancy.

import Browser
import Html exposing (Html)
import Html.Attributes as Attributes


main : Program () Model Msg
main =
    Browser.sandbox
        { init = initialModel
        , update = update
        , view = view
        }


type alias Model =
    { items : List String
    }


initialModel : Model
initialModel = Model []


type Msg 
    = Noop


update : Msg -> Model -> Model
update msg model =
    case msg of
        Noop -> 
            model


view : Model -> Html Msg
view model =
    Html.div
        [ Attributes.class "container" ]
        [ Html.div 
            [ Attributes.class "row" ] 
            [ Html.div 
                [ Attributes.class "col-sm-6" ]
                [ Html.h4 [] [ Html.text "Draggable" ] ]
            , Html.div 
                [ Attributes.class "col-sm-6" ]
                [ Html.h4 [] [ Html.text "Drop Zone" ] ]
            ]
        ]

For styling I'm using the mini.css so you'll want to add the stylesheet for that.

<head>
    <link rel="stylesheet" href="https://unpkg.com/mini.css@3.0.0/dist/mini-default.min.css">
</head>

Now there's a potential issue in that Firefox requires event.dataTransfer.setData() to be called during the dragstart event in order to drag an element. To address this we'll add an event listener on the body element.

<script>
    document.body.addEventListener('dragstart', function (event) {
      event.dataTransfer.setData('text/plain', null);
    });

    var app = Elm.Main.init({ node: document.querySelector('main') });
</script>

At this point running the application should display the shell that we've setup. Now we can move onto adding in the fun stuff.

Create a list of draggable items

First we need to add a new property to our Model for tracking the item that is being dragged. I'm also going to add a list of draggable items to the model so that we can compose our list of different items.

type alias Model =
    { beingDragged : Maybe String
    , draggableItems: List String
    , items : List String
    }


initialModel : Model
initialModel =
    { beingDragged = Nothing
    , draggableItems =
        List.range 1 5
            |> List.map Debug.toString
    , items = []
    }

Now let's update our view to render the draggable items.

draggableItemView : String -> Html Msg
draggableItemView item =
    Html.div
        [ Attributes.class "card fluid warning"
        ] 
        [ Html.div 
            [ Attributes.class "section" ] 
            [ Html.text item ] 
        ]


itemView : String -> Html Msg
itemView item =
    Html.div
        [ Attributes.class "card fluid error" ] 
        [ Html.div 
            [ Attributes.class "section" ] 
            [ Html.text item ]
        ]


view : Model -> Html Msg
view model =
    Html.div
        [ Attributes.class "container" ]
        [ Html.div 
            [ Attributes.class "row" ] 
            [ Html.div 
                [ Attributes.class "col-sm-6" ]
                <| (List.map draggableItemView model.draggableItems
                    |> (::) (Html.h4 [] [ Html.text "Draggable" ]))
            , Html.div 
                [ Attributes.class "col-sm-6"
                ]
                <| (List.map itemView model.items
                    |> (::) (Html.h4 [] [ Html.text "Drop Zone" ]))
            ]
        ]

Add update messages

Next we need to setup some messages for handling actions in the application. These messages will be passed by the event handlers we're going to setup in the next section.

type Msg
    = Drag String
    | DragEnd
    | DragOver
    | Drop


update : Msg -> Model -> Model
update msg model =
    case msg of
        Drag item ->
            { model | beingDragged = Just item }

        DragEnd ->
            { model | beingDragged = Nothing }

        DragOver ->
            model

        Drop ->
            case model.beingDragged of
                Nothing ->
                    model

                Just item ->
                    { model
                        | beingDragged = Nothing
                        , items = item :: model.items 
                    }

Add event handlers

Next we need to leverage the following events to achieve drag and drop functionality.

Event Description
dragstart Fires when an element starts being dragged.
dragend Fires when dragging stops without being dropped on a dropzone.
dragover Fires when an dragging over an element. Cancelling this event allows an element to be a dropzone.
drop Fires when dragging stops over a dropzone.


Handlers for these events aren't provided in Html.Events. Thankfully, these are fairly trivial to setup.

import Html.Events as Events
import Json.Decode as Decode


onDragStart msg =
    Events.on "dragstart" 
        <| Decode.succeed msg


onDragEnd msg =
    Events.on "dragend"
        <| Decode.succeed msg


onDragOver msg =
    Events.preventDefaultOn "dragover"
        <| Decode.succeed (msg, True)


onDrop msg =
    Events.preventDefaultOn "drop"
        <| Decode.succeed (msg, True)

Then all that's left now is for us to wire up the events in our view.

draggableItemView : String -> Html Msg
draggableItemView item =
    Html.div
        [ Attributes.class "card fluid warning"
        , Attributes.draggable "true"
        , onDragStart <| Drag item
        , onDragEnd DragEnd 
        ] 
        [ Html.div 
            [ Attributes.class "section" ] 
            [ Html.text item ] 
        ]


itemView : String -> Html Msg
itemView item =
    Html.div
        [ Attributes.class "card fluid error" ] 
        [ Html.div 
            [ Attributes.class "section" ] 
            [ Html.text item ]
        ]


view : Model -> Html Msg
view model =
    Html.div
        [ Attributes.class "container" ]
        [ Html.div 
            [ Attributes.class "row" ] 
            [ Html.div 
                [ Attributes.class "col-sm-6" ]
                <| (List.map draggableItemView model.draggableItems
                    |> (::) (Html.h4 [] [ Html.text "Draggable" ]))
            , Html.div 
                [ Attributes.class "col-sm-6"
                , onDragOver DragOver
                , onDrop Drop
                ]
                <| (List.map itemView model.items
                    |> (::) (Html.h4 [] [ Html.text "Drop Zone" ]))
            ]
        ]

Putting it all together

With everything in place now, you should have a solution like below that allows you to drag items from the left list onto the right list.

And that's all it takes to implement basic HTML5 drag and drop in Elm. Happy hacking!