React Drag/Drop

Category: Widgets
Subcategory: Display widgets

Overview

Wrapper for react-dnd (https://react-dnd.github.io/react-dnd/about) to provide drag/drop functionality using the pluggable widget architecture. Drag/Drop items, also image DnD/rotate

Documentation

MendixReactDnD

Wrapper for react-dnd to provide drag/drop functionality using the pluggable widget architecture.

Features

  • Drag items from lists and drop them on other page elements.
  • Reorder items in a list by dragging them.
  • Configure for each item whether it is draggable, or a drop target, or both.
  • For drop targets, configure which items may be dropped onto it.
  • Position images on a floorplan or other background.
  • Scroll the background to a giving position to move an image into view.
  • Rotate images by dragging a rotation handle.
  • Default styling to indicate valid and invalid drop targets.
  • Optionally apply a shadow effect to items while they are dragged.

Not for the faint hearted!

This is not an easy widget to work with! Take your time when you want to use it. The result is well worth the effort!

Testdrive the sample/demo project! It is a great starting point and contains a lot of stuff you can copy and adjust to your needs.

Read the docs!

What's with all the datasource containers?

This has to do with how React works, the framework underneath the Mendix client and the modern pluggable widgets. React does not allow you to just target any element on the page. There are tricks to achieve that but that breaks several rules and conventions. First and foremost, a component has control on the elements it renders and any components it renders. That is why React DnD libraries always come with some top level component that provides the drag/drop canvas and needs to contain the draggables and drop targets. Of course, these can be nested in any other element and component.

As a result, the Mendix widget must take a similar approach. It renders the top level component and in that all the datasource containers you define in Mendix.

Usage

Take the sample project as a starting point!

  • Define IDs for the various containers using an enumeration.
  • The widget needs a context object, described in more detail separately
  • Datasource containers currently only work with lists. However, nothing stops you from creating a datasource microflow that returns a single object in a list, mimicking a single object datasource.
  • Create a microflow that handles the drop.
  • Optionally configure an onClick action.

Drag/drop backend

The react-dnd library can use the HTML5 and touch backends for drag/drop operations. The default is to use the HTML5 backend.

The basic rule: Use the HTML5 backend for laptops and desktops, use the touch backend for tablets and smartphones.

However, there are many devices that can be used in different ways:

  • Surface devices that can be used with a mouse or as tablet.
  • Many laptops have a touch screen
  • Separate touch screen attached to a laptop or desktop
  • Windows has a tablet mode.

The widget makes no attempt to guess the device type. It is up to you to choose the right backend for your application.

Be sure to test this with the actual devices!

The context object

All event handling is done by setting values on the context object.

AttributeTypeDropDrop with positionRemarksContainer IDEnumerationReq.Req.Container ID of the dragged itemClient XIntegerReq.Client (mouse) X position for the event.Client YIntegerReq.Client (mouse) Y position for the event.Offset XIntegerReq.Offset X position for the event. Relative from the element.Offset YIntegerReq.Offset Y position for the event. Relative from the element.Dragged difference XIntegerOpt.Dragged difference for the X offset.Dragged difference YIntegerOpt.Dragged difference for the Y offset.Object GUIDStringReq.Req.When an object was clicked or dragged, contains the GUID of the dragged objectDrop target IDEnumerationReq.Req.When an object was dragged onto another object, contains the container ID of the drop targetGUID of drop targetStringReq.Req.When an object was dragged onto another object, contains the GUID of the drop targetNew rotationIntegerReq.New rotation for the object. Required when allowing users to rotate items.Data change dateDateReq.Req.Update the date in your logic to make the widget update the table. (Pluggable widgets are rendered VERY often!)Use touch backendBooleanWhen set to true, the touch backend is used for drag/drop operations. For tablets and smartphones.Zoom %IntegerZoom percentage, when set, will be used to correct X/Y offset for the zoom percentageAdjust offsetBooleanAdjust offset for zoom position. Turn off if you want to make the calculation yourself.Snap to gridBooleanSnap to grid while draggingSnap to sizeIntegerStep for the snap to grid feature, can be configured separately from the visible grid size, configured for each container.Snap to rotateBooleanRotate along rotation drag degreesRotation drag degreesIntegerNumber of degrees to rotate while dragging the rotation handle.Rotation button degreesIntegerNumber of degrees to rotate when clicking a rotation button.Add to rotationBooleanWhen true, add the rotation button degrees value to the current rotation. When false, rotate to the next or previous value.shift keyBooleanWhether the shift key was held during an onclick event. (Not applicable for drop events)ctrl keyBooleanWhether the ctrl key was held during an onclick event. (Not applicable for drop events)alt keyBooleanWhether the alt key was held during an onclick event. (Not applicable for drop events)Is right clickBooleanWhether the click event is a right click. (Not applicable for drop events)Selected marker GUIDsStringOpt.The selected marker(s). Comma separated list of GUIDs.Selected marker countIntegerOpt.Optional. Number of selected markers. Only relevant when multiselect of markers is allowed at one or more containers.Scroll to rowIntegerOpt.Set the row number of the cell to scroll. See belowScroll to columnIntegerOpt.Set the column number of the cell to scrollScroll to XIntegerOpt.The X position to scroll toScroll to YIntegerOpt.The Y position to scroll to

Events

ActionDropDrop with positionRemarksOn click actionOpt.Action to call after an item was clicked. See below.On drop actionReq.Req.Action to call after an item was dropped.On rotate actionOpt.On rotate action. Required when allowing users to rotate items.On drag to selectOpt.Action to call after user selects multiple markers by dragging a selection area. Use this to update the list of selected markers.

The Data changed date attribute

Pluggable widgets are rendered really often due to the way React and Mendix work. Clicking buttons, conditional visibility elsewhere on the page, changing the context object or opening a popup are examples.

To improve performance, the widget will only reload the data from the datasource when the value of the data changed date attribute changes. So whenever you want the widget to refresh, set the attribute to current date/time in your microflow. When the date did not change, the widget will just render the data loaded in a previous render.

Be sure to only commit the context object with the data changed date with refresh in client. The datasources will get the other data. Refreshing objects retrieved through a datasource can trigger additional server roundtrips.

Dragged difference

When dragging a parent marker, you will need to adjust any related markers as well if you want to keep them together. The widget will do this while dragging the parent around but you will need to persist the new position for the child markers yourself.

The same applies to multi selection. When multiple markers are selected, the widget will report the new position for the marker that was dragged. The GUIDs of the other selected markers are passed in the selection context attribute.

Return on click events?

Returning on click events from the widget only makes sense when positioning items on a background. When dropping without position, a container with an onClick action is easier. The on click event of the widget allows you to capture the exact click coordinate on the container.

Inspecting the contents

Because the widget captures right-click events, inspecting an element by right-clicking it no longer works. The browser inspector can also directly inspect elements. The example is for Chrome. The little arrow icon in the top left of the inspector pane allows you to target elements on the page.

Inspect element 1

Inspect element 2

Containers

The widget allows you to choose where to put each container by setting a row and column number. The result is a grid with your containers. Note that the widget does not contain additional empty cells when you leave gaps in the column numbers. Note that it is perfectly fine to place multiple containers in the same row/column combination. This also allows a header and the list to be styled as one.

General container configuration

PropertyTypeRemarksContainer IDStringExpression, usually toString() of an enumeration value you defined earlier. This value will be used in the eventsRow numberIntegerRow number, numbering starts at 1Column numberIntegerColumn number, numbering starts at 1DnD typeEnumerationDrag/drop type.Allow selectionEnumerationWhether markers may be selected. For single selection, the marker will be selected and the onClick action will be called.Return OnClickBooleanWhether OnClick events should be returned for the container.Allow drag to selectBooleanWhether the user can drag a selection area to select multiple markers. Only makes sense when multi-select is allowed on items in the same row/column combination.Accepts IDsStringFor drop targets. The IDs of containers for which items may be dropped onto this container. Usually toString() of an enumeration value you defined earlier. Separate multiple values using a comma (,).

The datasource for the container

PropertyTypeReq.RemarksData sourceYThe datasource for the item(s) in the container.ContentYThe content to render for each item. Optional! The widget can render images, see belowDisable dragBooleanWhen dragging is possible, use this to (temporarily) disable dragging of this item.NameStringName attribute. When set, will render a data-name attribute with the value on the item element.Marker classStringOptional, value will be added to the CSS class of the markerChild IDsStringSee below

Be careful when using attributes over long XPaths in a datasource. This could cause performance issues.

IMPORTANT Make sure any datasource microflow has the context object as parameter and any XPath ties to the context object as well. Otherwise, the contents may not refresh properly.

Child IDs

Datasource items can be linked to a parent item. When dragging the parent, any child items will move along too. Note that an item ID is just the GUID.

Rendering items as an image

The widget can render items as image, allowing the user to rotate the image. Optionally, the image can resize along with the zoom percentage or keep its size. Examples: A marker on a floorplan should probably not scale so it remains visible when zooming out. Furniture placed on the floorplan should scale with the zoom percentage.

When X and Y offset are set, position absolute is also set on the item.

Use CommunityCommons.GetImageDimensions to save the image dimensions on your image objects.

To create a sidebar with templates, use a separate container to show the template items. Mark these as template using the Is template item property and set the maximum template width property on them. This ensures that the template bar does not scale with the zoom factor and the items are not wider than the sidebar. If the item you create from the template will scale with the floorplan or background, set the value on the template as well. As a result, while dragging the template onto the background, the template will appear correctly sized for the current zoom level so it may be placed on the background correctly.

PropertyTypeReq.RemarksOffset XIntegerOffset X position for the item.Offset YIntegerOffset Y position for the item.Image URLStringYImage URL. Can be absolute, concatenated with Mendix remote URL when it does not start with http, should not start with / in that case.Image heightIntegerYImage height, in pixelsImage widthIntegerYImage width, in pixelsScale imageBooleanScale image with the floorplan.RotationIntegerThe current rotation of the image. Required when the user is allowed to rotate the imageAllow rotateBooleanWhether user may rotate the image. Only when image is rendered. If true, a rotation handle will be shown when the user hovers over the image.Show gridBooleanShow a grid over this itemGrid sizeIntegerSize of a grid cell. Note that the snap to grid feature has a separate snap to sizeIs template itemBooleanIs this a template to create items by dropping this one on the background?Max. template widthIntegerMaximum width of the template item as shown in the sidebar.

Styling the container and its items

The widget comes with defaults for the required classes but you're welcome to use your own. Just be sure you know what you're doing!

PropertyTypeReq.RemarksContainer classStringAdditional class(es) to put on the container.Draggable classStringYClass to place on the draggable containerDragging classStringYClass to place on the draggable container while dragging is active.Drop target classStringYClass to place on the drop target containerCan drop classStringYClass to place on the drop target container when a valid item is hovered over itInvalid drop classStringYClass to place on the drop target container when an invalid item is hovered over it

The widget always drags a copy. Note that the draggable class applies to the original item, not the dragged copy. If you want to move an item on a canvas, the original item should probably be hidden. Use class dragging-container-hidden in such use cases.

Retrieving an object using its GUID

The widget uses GUIDs to indicate which object was dropped and where it is dropped. The demo project has Java action MainModule.RetrieveObjectByGUID which allows easy retrieval of an object by the GUID.

Do not place the widget in a popup

This will not work very well and is not supported.

Positioning images on a background.

The sample project has an example of positioning items on a floorplan.

Take care when creating a palette from which the user can drag items onto the floorplan or canvas. If the item on the palette has different dimensions than the item on the floorplan it might not be positioned correctly.

If you allow items to be positioned on top of each other or overlapping in any way, be sure to make the items a drop target as well. Without this, items cannot be dropped close to each other or on top of each other.

Prevent dropping outside the background image

The background image will resize according to the zoom level, so it may be smaller than the available space. The drop target is just a div so by default it will take all available space, allowing markers to be dropped outside the backgroud image. To prevent this, put a class similar to this one on your background container:

    div.widget-cell-content-container.floorplanContainer {
        display: inline-block;
    }

The demo app has this class applied to the background.

Only put this class on the background container! If this class is put on draggable items, these will not be positioned correctly anymore!

Marker selection

The styling tab has additional options for marker selection, intended only for positioning items on a floorplan.

The selected marker GUIDs attribute can be used to set and get the selected markers.

As the markers are images with a position and size, drawing a border around them affects the layout. The default uses a 2 pixel border, so the border width property is set to 2. This is used when calculating the marker image properties.

Snap to grid

The widget has a snap to grid feature, to prevent the user from having to drag and drop at an exact pixel position.

If it seems impossible to snap drop an item at the top-left corner of your background, make sure your background is positioned at a multiple of the snap to size used. So if your snap to size is 10 but the background is positoned at left=85, the snap to function will be off by 5. Note that layout grid and other containers may add padding as well. Inspect in the browser where necessary. This can be seen in the demo project: snap to sizes 5 and 10 work correctly but 15 and 20 make it impossible to position an item in the top/left corner or at the top of the background.

Show grid

For each datasource can be specified whether a grid should be shown. This is done at datasource level otherwise the grid would be shown across all containers where it should only be shown over the background.

Scroll/Zoom to marker

Set the zoom percentage, scroll to row/column and X/Y position to scroll one of more markers into view. Be sure to set the values to empty, not zero, to prevent unwanted scroll actions from the widget. Best place to do that is when opening the page with the widget. The sample project does this.

Microflow SUB_EditFloorplan_ZoomTo in the sample project accepts a list of markers as parameter. It then calculates the zoom percentage and scroll to X/Y. You can copy this microflow to your own project and call it. Be sure to commit the context object yourself. The microflow does not commit the object in case other changes need to be made to the context object as well.

With complex pages, state update issuesmay occur, in that case commit the context object. This can be done using the On scroll to handled action. A simple Save changes is sufficient.

The scroll to can come too soon, in that case the content has not yet been rendered. This may happen when the page is opened with scroll to already set. So a delay is available, which can be configured using the Scroll to delay property. The default is zero.

Styling

The widget is easy enough to style by setting a class on it in the widget properties and then apply styling by targeting the main widget using your class and overruling defaults. Also, the classes used for each container can be changed at the container definition.

An example of a page where available products can be dragged to include them. Screen copy

This is what the structure looks like: Widget DOM structure

Note that some details are left out for brevity, inspect the sample app in the browser for full details

The elements and classes involved that you can overrule to apply styling:

ElementClassesDescriptionWidget containerwidget-containerThe widget container. Any classes you put on the widget end up heredrag-shadowAdd this class to the widget container to show a shadow while draggingRowwidget-row widget-row-xRow container, row xCellwidget-cell widget-cell-rx-cyCell at row x and column yCell content containerwidget-cell-content-containerContains the contents rendered for a containerDrop target containerdroptarget-containerIf container is a drop targetDraggable containerdraggable-containerIf container is draggableItem containerwidget-cell-content-container-itemContains rendered content for one datasource itemdraggableItemAdded if draggabledropTargetAdded if drop target

Additional marker classes

When you want to put additional classes on a marker, without actually updating it, you can use this feature. The value of the property is a JSON array as a string:

[
    {
        "itemID": "<itemID1>",
        "classes": "myspecialclass"
    },
    {
        "itemID": "<itemID2>",
        "classes": "myspecialclass otherclass"
    }
]

Separate multiple clases using a space. Note that an item ID is just the GUID. So you target any marker for that specific Mendix object. Usually just the one marker of course.

The demo project has an export mapping available to create the JSON data, you can of course also just piece it together as a string, choose what suits you best.

The custom drag layer

The custom drag layer is a layer above the normal render layer. Nothing fancy, just a div with a z-index spanning the viewport.

Any dragged item is rendered here, this is the copy that is being dragged. It is wrapped in a div with class custom-draglayer-item.

ElementClassesDescriptionCustom drag layercustom-draglayerDragged items are rendered hereCustom drag layer itemcustom-draglayer-itemContains dragged item

To change the appearance of your item while it is dragged, use class custom-draglayer-item in your SASS file. For example, to remove the bottom border while the item is being dragged:

div.parent-child {
    div.custom-draglayer div.widget-cell-content-container-item {
        border-bottom: none;
    }
}

In this example class parent-child would be put on the widget in Studio Pro, to make sure only items dragged in that particular page are affected

Demo project

Separate demo project in the appstore

Issues, suggestions and feature requests

link to GitHub issues

Releases

Version: 3.1.1
Framework Version: 9.12.5
Release Notes: For positioning images only: - Fix drop offset when background is scrolled - Fix dragging a selection area after zooming and scrolling
Version: 3.1.0
Framework Version: 9.12.5
Release Notes: Added scroll to functionality
Version: 3.0.0
Framework Version: 9.12.2
Release Notes: - Mendix 9.12 - Big performance improvement, especially when positioning a large number of items on a background. - Upgraded the react-dnd library used in the widget. - Refactored the widget code to take advantage of new React and react-dnd optimizations
Version: 2.4.0
Framework Version: 9.6.9
Release Notes: Added touch backend to support tablets and smartphones
Version: 2.3.0
Framework Version: 9.6.5
Release Notes: Drag a selection area across the background to select markers.