drag and drop library

597 views
Skip to first unread message

Janis Voigtländer

unread,
Sep 17, 2014, 3:49:24 AM9/17/14
to elm-d...@googlegroups.com
Hi, I've written this small library:

https://github.com/jvoigtlaender/drag-and-drop-elm

It allows you to easily do something like this:

http://share-elm.com/sprout/54193b54e4b0d19703e9773b

Yes, I am aware that http://library.elm-lang.org/catalog/echaozh-elm-drag/0.1 exists. But I was aiming for a more high-level API. I don't think something like my example on share-elm above would be easily doable with the existing elm-drag library.

Conrad Parker

unread,
Sep 17, 2014, 9:25:42 AM9/17/14
to elm-d...@googlegroups.com
Hi Janis,

this works well. I'm not so familiar with html5 drag-n-drop, and I didn't dig into the different APIs enough to tell: what exactly is the design difference between the two libraries, and why can't this example be done easily in elm-drag?

Conrad.

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Janis Voigtländer

unread,
Sep 17, 2014, 9:56:07 AM9/17/14
to elm-d...@googlegroups.com
Oh, I'm not using any specific html5 functionality, rather the drag-and-dop behavior is defined in terms of the standard Elm  Mouse  signals (which might or might not be a good idea). Same holds for the other library.

The main API difference between the two libraries is that the earlier one limits itself to providing signals directly reflecting mouse movements. Translation of those movements/events into something related to objects in the application (which object was picked up, moved where, etc.) is left to the client program. That's similar to what I provide with the DragAndDrop.mouseEvents signal. The higher-level part of my API is the trackSingle/trackMultiple-functions. They each take as input a signal which tells when an application object is under the mouse (and which one, if that is relevant; as well as, when the mouse leaves an object etc.). That would typically be a signal that comes from an application or applications of Graphics.Input.hoverable. Based on that input signal, the track-functions abstract from the mouseEvents and instead give the client a signal expressed in terms of actions (lift, moveBy, release) on the hovered/tracked objects.

Riccardo

unread,
Nov 25, 2015, 5:48:53 AM11/25/15
to Elm Discuss
Hi Janis,
I am playing with the library, the API looks really nice!
I am trying to use it together with the start-app module, but I am having troubles with that.. any chance you could post an example on how to do it?
(sorry, beginner question, I started with Elm a few days ago!)

Thanks!
Riccardo

Janis Voigtländer

unread,
Nov 25, 2015, 6:41:20 AM11/25/15
to elm-d...@googlegroups.com

Sure.

Try this program:

import Html exposing (div, button, text, fromElement)
import Html.Events exposing (onClick)
import StartApp
import Effects
import DragAndDrop exposing (..)
import Graphics.Input
import Text exposing (fromString)
import Graphics.Element exposing (layers, leftAligned, sizeOf)
import Graphics.Collage exposing (collage, outlined, rect, solid, toForm)
import Color exposing (black)

hover = Signal.mailbox False

box = Graphics.Input.hoverable (Signal.message hover.address)
                               (putInBox (leftAligned (fromString "drag-and-drop me")))

putInBox e =
  let (sx,sy) = sizeOf e
  in layers [e, collage sx sy [outlined (solid black) (rect (toFloat sx) (toFloat sy))]]

moveBy (dx,dy) (x,y) = (x + toFloat dx, y - toFloat dy)

main =
  (StartApp.start { init = (model, Effects.none)
                  , update = update
                  , view = view
                  , inputs = [ Signal.map DnD (track False hover.signal) ] }).html

model = (0,0)

type Action = DnD (Maybe DragAndDrop.Action) | ToOrigin

update action model =
  case action of
    DnD (Just (MoveBy (dx,dy))) -> (moveBy (dx,dy) model, Effects.none)
    ToOrigin                    -> ((0,0), Effects.none)
    _                           -> (model, Effects.none)

view address model =
  div []
    [ fromElement (collage 200 200 [Graphics.Collage.move model (toForm box)])
    , button [ onClick address ToOrigin ] [ text "back to center" ] ]

with this elm-package.json:

{
    "version": "1.0.0",
    "summary": "helpful summary of your project, less than 80 characters",
    "repository": "https://github.com/user/project.git",
    "license": "BSD3",
    "source-directories": [
        "."
    ],
    "exposed-modules": [],
    "dependencies": {
        "elm-lang/core": "3.0.0 <= v < 4.0.0",
        "evancz/elm-effects": "2.0.1 <= v < 3.0.0",
        "evancz/elm-html": "4.0.2 <= v < 5.0.0",
        "evancz/start-app": "2.0.2 <= v < 3.0.0",
        "jvoigtlaender/elm-drag-and-drop": "1.0.3 <= v < 2.0.0"
    },
    "elm-version": "0.16.0 <= v < 0.17.0"
}

Riccardo

unread,
Nov 25, 2015, 6:54:54 AM11/25/15
to Elm Discuss
Thanks Janis, this really helps!

Jesse Levine

unread,
Feb 13, 2016, 2:34:52 AM2/13/16
to Elm Discuss
Hi Janis,

Like Riccardo, I am trying to figure out how to use elm-drag in the context of a start-app project using the elm architecture. Your example above is very helpful, but there is something I still don't understand:

Let's say I have a medium-sized app with a big tree of elements being displayed, and the drag-n-drop feature only occurs in one little UI component somewhere in this tree, as a grandchild, let's say, of the Main view:

             Main
            /       \
           /         \
    Thing       SomethingElse
   /      \                         \
  /        \                         \
        MyWidget

In this scenario, how can I get my MyWidget component to "dispatch" its drag events up the hierarchy, so that its parent is notified whenever a drag event occurs? 

Say I'm using the Drag.track function, which gives me back a Signal (Maybe Drag.Action). I know I can do some mapping and finagle this Signal into a Signal MyWidget.Action, which is seemingly great, because that's what MyWiget.update needs. But I can't just have MyWidget call its own update function, because that's not how the architecture is supposed to be wired up. I need to somehow pipe this Signal back up to the parent, who forwards its own stuff up to its own parent ... and then everyone waits for an `update` call to trickle back down from the top. 

And I know the normal way to do this is to use the Address that was passed into MyWidget.view, since that address was probably created using Signal.forwardTo, and so any messages it receives will be forwarded up. So I've got this Signal, and I've got this Address, and I just want the output of the Signal to send messages to the Address. But I've looked through the Signal library and I don't see any way to do this.

In your example, your main function simply "reaches down" and hooks into hover.signal:

```elm
main = (StartApp.start { init = (model, Effects.none) , update = update , view = view , inputs = [ Signal.map DnD (track False hover.signal) ] }).html
```

But this seems to be feasible only because the hover mailbox happens to nearby. In my scenario, hover is going to be initialized within the scope of the MyWidget module, about which the Main module should not even know or care. 

At least I think that's how it would work..? I am still trying to learn the elm-architecture and haven't fully grasped it (especially in the territory of mailboxes, addresses, and the rest of "post-office" land). 

I'm clearly missing something-- I feel like this should be doable, right?

Any pointers? Thanks!

Jesse

Janis Voigtländer

unread,
Feb 13, 2016, 5:39:37 PM2/13/16
to elm-d...@googlegroups.com

Maybe something like below would work for you. Some notes:

  • That code is not using the track-functions. Instead, the mouseEvents/automaton version of the API is used.
  • Consequently, the Main module does not “reach down” for a hover-signal.
  • The Main does need to bring in the mouseEvents signal, though.
  • But that signal does not depend on the MyWidget module in any way. Moreover, it needs to be connected only once, even if there are several subwidgets that use mouse dragging.
  • The way the Main module uses MyWidget is essentially following the usual architecture (so not depending on internals of MyWidget), except that Main needs to explicitly “notify” a MyWidget component of mouse events (see the line with MyWidget.notifyMouseEvent). That would have to happen for each subcomponent that uses mouse dragging. It might be the main weakness of this approach, in particular if MyWidget is a grandchild as in your scenario, not a direct child as in my simplified example.
  • In MyWidget, the model needs to be enriched by a “drag state”. The way that is handled is rather canonical (see the updateDragState function) and would repeat itself across other widget types.
module Main (main) where

import MyWidget

import Html exposing (div, button, text, fromElement)
import Html.Events exposing (onClick)
import StartApp
import Effects
import Drag

main =
  (StartApp.start { init = (model, Effects
.none)
                  , update = update
                  , view = view
                  , inputs = [ Signal.map MouseEvent Drag.mouseEvents ] }).html

model = { counter = 0, widget = MyWidget.model }

type Action = Increase | Widget MyWidget.Action | MouseEvent Drag.MouseEvent

update action ({ counter, widget } as model) =
  case action of
    Increase         -> ({ model | counter = counter + 1 }, Effects.none)
    Widget action    -> ({ model | widget = MyWidget.update action widget }, Effects.none)
    MouseEvent event -> ({ model | widget = MyWidget.notifyMouseEvent event widget }, Effects.none)

view address { counter, widget } =
  div []
    [ fromElement (MyWidget.view (Signal.forwardTo address Widget) widget)
    , text (toString counter)
    , button [ onClick address Increase ] [ text "increase counter" ] ]
module MyWidget (model, Action, update, notifyMouseEvent, view) where

import Drag
import Automaton

import Graphics.Input
import Text exposing (fromString)
import Graphics.Element exposing (layers, leftAligned, sizeOf)
import Graphics.Collage exposing (collage, outlined, rect, solid, toForm)
import Color exposing (black)

box address = Graphics.Input.hoverable (\b -> Signal.message address (Hover (if b then Just () else Nothing)))
                                       (putInBox (leftAligned (fromString "drag-and-drop me")))

putInBox e =
  let (sx,sy) = sizeOf e
  in layers [e, collage sx sy [outlined (solid black) (rect (toFloat sx) (toFloat sy))]]

moveBy (dx,dy) (x,y) = (x + toFloat dx, y - toFloat dy)

model = { pos = (0,0), dragState = Drag.automaton Nothing }

type Action = Drag (Maybe ((), Drag.Action)) | Hover (Maybe ())

update action ({ pos } as model) =
  case action of
    Drag (Just (_, Drag.MoveBy (dx,dy))) -> { model | pos = moveBy (dx,dy) pos }
    Hover which                          -> updateDragState (Drag.Hover which) model
    _                                    -> model

updateDragState input ({ dragState } as model) = let ( dragState', output ) = Automaton.step input dragState
                                                 in update (Drag output) { model | dragState = dragState' }

notifyMouseEvent = updateDragState << Drag.Mouse

view address { pos } = collage 200 200 [Graphics.Collage.move pos (toForm (box address))]
{
    "version": "1.0.0",
    "summary": "helpful summary of your project, less than 80 characters",
    "repository": "https://github.com/user/project.git",
    "license": "BSD3",
    "source-directories": [
        "."
    ],
    "exposed-modules": [],
    "dependencies": 
{
        "elm-lang/core": "3.0.0 <= v < 4.0.0",
        "evancz/automaton": "1.1.0 <= v < 2.0.0",
        "evancz/elm-effects": "2.0.1 <= v < 3.0.0",
        "evancz/elm-html": "4.0.2 <= v < 5.0.0",
        "evancz/start-app": "2.0.2 <= v < 3.0.0",
        "jvoigtlaender/elm-drag": "1.0.0 <= v < 2.0.0"
    },
    "elm-version": "0.16.0 <= v < 0.17.0"
}
Reply all
Reply to author
Forward
0 new messages