As promised in the previous blog on Drag & Drop with the Ignite UI Tree– some tips and tricks coming right up. If you haven’t already looked into the control’s features, now’s your chance – overview with main ones listed in the Documentation and live in the Samples. Looking at those, while each of them is rather nice, how do you think some of them would work alongside each other…and is something missing? The first one is always a potential trouble maker and the latter.. well extending functionality is fun, but as you know there are always new stuff coming with each release – might be just the thing you are missing. We do take feedback on everything through socials (links always below), there’s an active community at the Forums and you can always submit a feature request( must be logged in). Enough with the shameless plugs, on to the discussion!
Play nice?
As you’ve already seen in my previous post there are some quirks for example when you have nodes moving around with Drag and Drop and checkboxes at the same time you will loose the checkbox state and when a tri-state parent is fully checked a dropped node will respect the valid-for-all rule.
It’s merely the fact that while drag and drop is a nice interaction from a user standpoint, underneath it involves removing and adding nodes. While that is simple in itself, it does present a number of situations where it can be really tricky to preserve whatever state the node may have applied by multiple other features. There’s a similar scenario with selection when you move the selected node around – you still keep the selection data, but the visual is gone as the node is re-created.
Then you have another major feature of the Ignite UI Tree – the Load On Demand. This is an extra tricky one actually. While the moving of nodes happens entirely on the client and there’s little that can come in it’s way.. it’s really hard to add a node to a parent one that hasn’t yet populated its children. Yes, you can easily supply the UI, but the underlying data source would still contain no array of child items for you to add to. Of course, one the node populates it’s all good and cool, but as you get told everywhere users are notoriously impatient and in case they do not wait for the node to load it can get quite messy.
Do keep in mind thought, the Ignite UI toolset keeps evolving with not only new features, but general improvements as well and it’s certain Drag&Drop will become more in tune with the rest. For that reason, what I do provide for now would possible become obsolete, but it’s more of a testimony to the fact that you can enhance and extend Ignite UI controls with very little effort!
Play nice.. or I’ll make you!
So let’s get on with the modifications. The Tree’s events will be our heroes of the day as many if not all major changes are represented with them. I have one very easy fix for maintaining proper selection UI with truly minor effort – as I said it’s all really a matter of picking the right time to hook your code. I figured it’s mildly pointless to update a second selection variable on each change, so instead I will check when a drag is starting if the node in question is selected. Then when the said node is dropped I would simply use the API method provided for selection. This will in essence go and set in the new path for the node:
- $('#tree').igTree({
- dragAndDrop: true,
- dragAndDropSettings: {
- dragAndDropMode: 'default',
- dragStartDelay: 100,
- revert: true,
- //simple icon-only tooltip
- invalidMoveToMarkup: '<div><span></span><p> </p></div>',
- containment: $("#tree")
- },
- dragStart: function (evt, ui) {
- // check if element is the one selected
- selectionDirty = $('#tree').igTree('isSelected', ui.element);
- },
- nodeDropped: function(evt, ui) {
- if (selectionDirty) {
- //if it was selected, re-select when dropped in new place
- var newNode = $("#tree").igTree("nodeByPath", ui.path + '_' + ui.draggable.attr('data-path').split('_').pop());
- $('#tree').igTree('select', newNode);
- }
- selectionDirty = false;
- },
- dataSource: northwindCategories,
- bindings: {
- textKey: 'CategoryName',
- primaryKey: 'CategoryID',
- valueKey: 'CategoryID',
- childDataProperty: 'Products',
- bindings: {
- textKey: 'ProductName',
- valueKey: 'ProductID',
- primaryKey: 'ProductID'
- }
- }
- });
The difference from what you have seen before as code for the tree are the dragStart and nodeDropped events. Because those come in line of the jQuery UI Draggable & Droppable events and they can generally handle interaction between two trees, you won’t see the familiar ‘ui.owner‘ to access the tree as it’s kind of hard to tell which tree should be an owner really :) However, since I’m adding handlers separately that’s okay – I know which tree to call. Also, the tree is helpful enough to give nodes by paths – the latter you can get by combining the event provided path (the parent one) and the ID of the dropped element. The way this handles it right now should be suitable for deeper hierarchy that two as well.
Note: Remember you have the same event arguments as in jQuery UI in addition to the Tree adding some stuff (the element for example), but in the case of the drop the element is the actual target and the draggable from the original arguments is the node being dropped. The results:
You can take a very similar approach with the checkboxes in bi-state mode Trees to complement that feature, however, the tri-state limitation I feel might be the right design (respecting the parent global state). Up to you really, the option is there.
Load on Demand
As I mentioned it’s nothing that complicated stopping this from being flawless, except the very nature of loading data on demand..it’s not there yet! And if the user doesn’t want to wait things go bad. One very simple way is to decrease the expand delay (time till nodes automatically expand when dragged over) to mere nothing. This might make it work just fine if the service you use is fast enough to get ahead of the unsuspecting user. However, go too low and you risk the user triggering a whole bunch of requests on the drag path and actually making the application slower rather than faster. The solution lies in not letting the user drop on non-populated node. However, that would mean that at some point the node would expand and drop would be possible, but the helper will still be showing invalid location. It’s not such a nice experience to make the user shake the node around to force a drag and re-evaluation of the drop target. If you want just the perfect experience, you can have it with one trick – move the mouse for the user! Yup, basically listen for when the node is populated, check if the user is on top of it, and trigger a mouse move so the validation will be re-done:
- var stateHelper = { dropTarget: null, populatingNode: null, draggedNode: null };
- $("#tree").igTree({
- dragAndDrop: true,
- parentNodeImageClass: "parent",
- dragAndDropSettings: {
- dragAndDropMode: "move",
- customDropValidation: function (element) {
- // Validates the drop target:
- // Nodes will properly cause targets to expand (and therefore load)
- // But we must not allow actual drop before the data is loaded
- // because we have nothing to add this node to.
- stateHelper.dropTarget = $(this);
- stateHelper.draggedNode = $(element);
- var childNodes = stateHelper.dropTarget.closest('li[data-role=node]').children('ul');
- if (childNodes.length > 0 && JSON.parse(childNodes.attr('data-populated')) === false) {
- returnfalse;
- }
- returntrue;
- },
- expandDelay: 500
- },
- dragStop: function(evt, ui) {
- stateHelper.dropTarget = undefined;
- },
- nodePopulated: function(evt, ui) {
- if (stateHelper.dropTarget && $.contains(ui.element, stateHelper.dropTarget)) {
- stateHelper.populatingNode = ui.element;
- }
- },
- rendered: function(evt, ui) {
- if (stateHelper.populatingNode) {
- // forse re-handling of the drag in case the user manages to
- // shomehow keep his mouse steady when the node expands
- // this will evaluate the target again
- stateHelper.populatingNode.children('a').trigger({
- type: 'mousemove',
- pageX: stateHelper.draggedNode.data('draggable').position.left,
- pageY: stateHelper.draggedNode.data('draggable').position.top
- });
- stateHelper.populatingNode = undefined;
- }
- },
- dataSourceType: 'remoteUrl',
- dataSource: 'http://services.odata.org/OData/OData.svc/Categories?$format=json&$callback=?',
- responseDataKey: "d",
- loadOnDemand: true,
- bindings: {
- textKey: 'Name',
- valueKey: 'ID',
- primaryKey: 'ID',
- childDataProperty: 'Products',
- bindings: {
- textKey: 'Name',
- valueKey: 'ID',
- primaryKey: 'ID',
- childDataProperty: 'Supplier'
- }
- }
- });
As you can see the Tree is bound to the oData Northwind service making calls only when needed, we listen for populated nodes and save them in the ‘stateHelper’ to later trigger the mouse move on them when the UI has been rendered.
A word on the custom validation function
The thing about this functions is that it will be overridden by the internal tree validation, which makes it the last last chance for you as a developer to return false and prevent a node drop on that target. What this also should tell you is that the internal validation will try to validate actual Ignite UI Tree as a target, so your function is not to enable something the feature won’t allow – no call will be made to it when the target is foreign. This begs the question, can you still use other targets? Yes, the drop events still fire on the drop targets! Take the Simple File Manager as an example – the trick is in the usage of the droppable events instead!
And as you have already seen form above the context of the function is the droppable element and the parameter is the draggable.
Last but not least, if you are wondering how this JavaScript function is being used with the ASP.NET MVC wrappers, the MVC side property accepts a string with the function’s name. What that means is that such functions need to be discoverable on the global scope (on window really), so be careful where you define it:
- //rest is omitted
- }).DragAndDropSettings(d =>
- {
- d.ExpandDelay(500);
- d.CustomDropValidation("customValidation");
- })
And then:
- <script>
- function customValidation(element) {
- // omitted
- }
- </script>
- <script>
- function customValidation(element) {
- // omitted
- }
- </script>
Resources
First let me again remind of the elaborate Ignite UI Tree Documentation and jQuery API Reference. Then don’t forget the awesome samples!
This time I’ll add in the mix a solid ASP.NET MVC demo project ( with everything from here included in with both MVC helper and script-only demos, plus some basic stuff from the previous post). Make sure to check that one out, the animations above don’t do it justice! You would need at least a trial version of Ignite UI so hit the banner below if you are still lacking one.
Also you can go for fiddling with two demos on JSFiddle:
- Basic settings and events + enhanced selection demo
- Modified Drag&Drop + Load On Demand behavior with Custom drop validation function.
Stay tuned, for there is still more awesome tricks I have for you – next time we’ll do some updating support!
Summary
This was the second part of the in-depth look at the Ignite UI Tree’s Drag and Drop feature. We covered some of the challenges such functionality presents and how you can overcome them. We improved the selection handling of moved nodes, we made the experience a true joy with Load on Demand. We also gave a little attention to the custom validation function – what is it for, what to expect and what you can do with it. At the end of this, I would like to remind you that this has been a proof of the concept that the controls are flexible enough to let you mold them to your needs with little code and great impact!
And as always, you can follow us on Twitter @DamyanPetev and @Infragistics and stay in touch on Facebook, Google+ and LinkedIn!