React Complex Tree
Module and widget to use React Complex Tree in Mendix: Tree view with lazy load and drag/drop
Try the demo to get a running start!
Typical usage scenario
- Show data in a tree structure
- Data structure can have unknown number of levels
- Drag items to move or reorder them
Features and limitations
- Show data in a tree
- Lazy loading
- No fixed number of levels in the tree
- Drag items to move or reorder them
- Allow users to rename an item in the tree
- React to click and selection events
- Signal the widget to expand a node into view
- Buttons to collapse and expand all nodes
- Uses a REST service to get data
- Datasource is not supported
- Mendix 9.24.10
Download the module from the Marketplace and place the widget on a page.
Especially the advanced cases like drag/drop support and lazy loading can be challenging. Refer to the demo app to get a running start.
In fact, one way to start fast is to download this module from the Marketplace, so it appears as Marketplace module in your app. Then take module ReactComplexTreeUtils from the demo app and customize it to your needs.
Several options to choose from:
- Make your own entities a specialization of the entities in the ReactComplexTreeUtils module. Especially useful if you intend to create several trees in separate use cases which use different entities. This also allows more finegrained control over the entity access.
- Create associations from your entities to the entities in the ReactComplexTreeUtils module. Note that this results in an all-or-nothing scenario for the access to the node data. Use specializations to restrict access
- Use the entities as is and extend them with the attributes and associations you require
Note that the last option makes updating the ReactComplexTreeUtils later more difficult as you would loose your changes. Using specializations is the advisable option.
The widget needs a context object.
The attributes as they are defined in the demo app:
- DataChangedDate: Whenever you want the widget to retrieve node data, set the data changed date to the current date/time.
- SelectionChangedEventNodeIDs: The ID(s) of the selected node(s). Multiple values are separated using a comma.
- MissingNodesEventNodeIDs: The IDs of nodes that need to be loaded into the tree. Used with lazy loading when the user expands a node that has not been fully loaded yet. Multiple values are separated using a comma.
- RenamedNodeID: The ID of the renamed node
- NewName: The new name of the renamed node
- DraggedNodeIDs: The ID(s) of the dragged node(s). Multiple values are separated using a comma.
- DropInfo: A JSON structure containing the drop information. It is explained in detail below.
Loading or updating data
The widget uses a REST service to load or update the tree. This allows fine-grained control on the data returned.
The REST service
The module contains the entities and resources to implement the REST service. The demo app has a fully working implementation you can use as starting point.
The service must allow authentication using the active session. The widget can only make service calls to the Mendix runtime, not outside the app.
There is no input parameter, as the service uses the active session, you can retrieve data from session. The demo does this: The UserContext entity is tied to the session so it can easily be retrieved from the service microflow.
- action: Enum TreeWidgetAction. Usage is described below.
- nodeCount: The number of nodes returned
- deletedNodeIDs: When data has been deleted, set the node IDs separated using a comma, use action update to remove them from the tree
- focusNodeID: When set, the node with this ID is expanded and focused.
- expandItemIDs: Node ID(s) to expand. Separate multiple values using a comma. When set, the nodes are expanded.
- resetExpandedItems: When true, only the nodes in are expandItemIDs are expanded. Any other nodes will be collapsed.
- nodes: Information about each node:
- name: The visible name in the tree
- index: The node ID. This must be a valid name: Starts with a-z or A-Z, followed by a-z, A-Z, 0-9, _ or - and no whitespace in it
- children: The ID(s) of the child node(s), if any. Multiple values are separated using a comma.
- parentID: The ID of the parent
- canMove: Whether the node may be dragged
- canRename: Whether the node can be renamed.
- dragType: For use with drag/drop, see below
- acceptDragTypes: For use with drag/drop, see below
Unfortunately, the canRename flag currently has no effect in the library. The demo handles this by resetting the name in the microflow and updating the node.
Important: a tree must have one and only one root node. The ID must be 'root'. It is the only node for which parentID is empty/null. This node is not visible in the tree.
There is no lazy loading flag, just return nodes for as many levels you want available directly. When the user expands a node for which the child nodes are not yet available, it will set the required node IDs on the Missing node IDs property. In the demo, this is linked to attribute MissingNodesEventNodeIDs. The widget will then call the On missing nodes action. Link this to a microflow that splits the ID attribute into separate node IDs and retrieves the requested nodes. Then return these nodes with action update.
Take care when reloading the tree, discarding earlier expanded nodes. Be sure to set resetExpandedItems in the response! Otherwise, a node might appear as expanded but no child nodes will be shown.
Setting focus to a node and making it visible
When a node is already loaded into the tree you can focus it from your application logic. If that is the only action to take you can use action focus in the service response.
When using lazy loading the node and its parent might not be loaded in the tree. In that case, you need to use the update action with the node and its parent as update list and set focusNodeID. The widget will load the new nodes and expand it. To make it visible, any parent needs to be expanded as well. That is why you will need to pass the parent IDs as well in expandItemIDs to expand the parents. With lazy loading, be sure to return the parents in your update set as well. This can be a little challenging to get right. Again, check the demo, it has this logic in place.
Reacting to events
The widget uses normal actions to respond to events:
- Renaming a node, if allowed in the widget property
- Selection changed, this also includes clicking a node as that selects it.
- Missing nodes, used for lazy loading
- Node dropped elsewhere in the tree.
Set the action to call a microflow. In each microflow, deal with the event data and update the DataChangedDate in the context object to make the widget call the service. In the service microflow return the appropriate response.
Try the demo! Getting drag/drop to work properly really is an advanced usecase.
When allowing users to reorder nodes by dragging them between other nodes, you will need to keep track of the node sequence in a parent.
The drop info data
The user may drop one or more nodes on another node or between nodes. Dropping a node onto another node makes it a child node. Dropping nodes between other nodes makes them siblings.
The information provided by the library varies on where the user dropped the node(s). The library returns a JSON structure, for which ReactComplexTree.IMM_DropInfo is available. It parses it into entity ReactComplexTree.DropInfo so you can access the info.
When dropping nodes onto another node, only the TargetItem is used, this is the ID of the drop target node. In the demo, the nodes are added as new nodes to the target, at the end of the child list if the node already had child nodes.
When dropping nodes between other nodes, it gets more complicated as the user wanted the nodes to be placed between other nodes. We now need to use ParentItem of the DropInfo to get the parent where the new nodes need to be inserted as child nodes. If the user dropped the nodes at the top of the child list, LinePosition in the DropInfo will be set to top.
The demo handles this by keeping track of a ChildSeqNbr value for each node. When adding nodes to the top of the list, the other nodes get a new sequence number. When dropping between other nodes, the nodes that need to move out of the way get a higher sequence number to make room for the dropped nodes.
This was one of the most complicated cases to get right. Check microflow ACT_UserContext_OnDrop and its subflows in the demo app to get the hang of this.
Restricting where to drop
When enabling drag/drop on the widget, by default any node can be dropped on or between any other node. Usually that is not desirable.
In the service response, for each node the dragType and the acceptDragTypes can be returned. The drag type indicates which type a node is, this depends on your application logic. In the demo, Environment is used for the Client and Server nodes. The child nodes all have drag type Tool. The root node only accepts nodes with Environment, the Environment nodes only accept Tool nodes. Now the user cannot drag a Tool node to the root.
Nodes without a value for acceptDragTypes accept any node. To prevent a node from receiving any drops, set a value for acceptDragTypes that is not used anywhere. The value None would work nicely, as long as you really don't use that for a drag type!
In the demo the drag types are just strings to allow testing. In real life situations, you can use an enumeration for the drag types. This prevents typo's. Use getKey to put them in the service response.
The widget does not include any CSS. Styling is done using the styling in the ReactComplexTree module.
Frequently Asked Questions
- Q: Why does the widget only support a REST service for data retrieval?
- A: The service call from the widget allows full control when data gets retrieved. Also loading large datasets through a datasource can cause performance issues in the client.