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.

module Main exposing (..) 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 . h 4 [] [ Html . text "Draggable" ] ] , Html . div [ Attributes . class "col-sm-6" ] [ Html . h 4 [] [ 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 . h 4 [] [ Html . text "Draggable" ])) , Html . div [ Attributes . class "col-sm-6" ] <| ( List . map itemView model . items |> (::) ( Html . h 4 [] [ 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 . h 4 [] [ Html . text "Draggable" ])) , Html . div [ Attributes . class "col-sm-6" , onDragOver DragOver , onDrop Drop ] <| ( List . map itemView model . items |> (::) ( Html . h 4 [] [ 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!