Usecase for asynchronous `Task value` instead of synchronous `Task error value`

I recently read Jasper Woudenberg’s article about drag-and-drop and found it really insightful.

To accomplish better drag and drop he needed some external javascript to keep track of where elements currently are.

I had the idea to only check where elements are on the start of a drag action, which has the additional benefit of not having weird side-cases where drag actions impact the width of elements which impacts the drag action again, etc.

Since I would only measure the elements once, it would be possible to use Browser.Dom.getElement (side note: would measureElement be a better name than getElement?).

This turned out to be problematic: It seems tasks are executed in a requestAnimationFrame, and if I Task.map2, the tasks are executed in order, but in individual animation frames.

There was a discussion about something similar previously:

Since it is unfortunately not possible to execute tasks in parallel, measuring e.g. 60 elements for drag-and-drop in a list takes 60 animation frames: in most cases 1 second.

In the same topic, Evan writes this:

My current feeling is that it might want an abstraction of its own, and the starting point there is to get a couple real examples where people feel this might be the best path.

So with the above description, I hoped to have contributed a real-world example, and I asynchronous Task.map2 or similar would be the best solution.

Current alternatives include:

  • Using javascript.
    As I currently only want to get information from the javascript side to the elm side, I need to use a subscription port. But I also want it to only send information, when I start dragging a specific DOM element, but communicating this to a subscription port is difficult (am I correct?).
  • Using a Cmd.batch.
    If I understood this correctly, Cmd.batch would run both tasks in a single animation frame (am I correct?). However, this would mean a lot of additional complexity in my Msg and Model datatype, which would be very hard to abstract out into a library or similar and feels like accidental complexity.

I realize I don’t really have a call-to-action. Maybe you had similar issues in the past? Do you agree with me? Did I miss something?

Thank you!

I don’t understand why drag/drop would require javascript. I’ve never had to do that. Ignore this if it’s offtopic.

Would a custom element work for your use case? You could have it fire events that you listen for on the Elm side and keep a clean separation that way. You can also pass data down through properties and attributes to react to changes, but they will be throttled to the animation frame.

I much prefer it to subscriptions for this sort of thing because it keeps the event logic with the view that will generate them.

Instead of using Browser.Dom.getElement you can use an event decoder to get the coords synchronously:

div [ Events.on "mousedown" <| Decode.map DragStart coordsDecoder ]
...

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


coordsDecoder : Decoder Coords
coordsDecoder =
    Decode.map4
        (\x y offsetX offsetY ->
            Coords (x - offsetX) (y - offsetY)
        )
        (Decode.field "x" Decode.int)
        (Decode.field "y" Decode.int)
        (Decode.field "offsetX" Decode.int)
        (Decode.field "offsetY" Decode.int)

Here I’m getting the position of the element relative to the window by getting the mouse coords and subtracting the mouse offset relative to the element. You can then use these coords together with position: fixed to display the dragged element. This avoids problems with parents that may have position: relative or overflow: hidden.

2 Likes