logo Red Star Development

woensdag 18 maart 2026

Ant Design's Tree Component: Drag 'n' Drop uitgelegd

Ant Design is een uitgebreide widget toolkit om web applicaties mee te maken. Ze vindt haar oorsprong in React, maar later zijn er ook varianten gemaakt voor integratie met Vue en Angular. Ik werk inmiddels een aantal jaar in Digital-Ink met Ant Design voor React en ik moet zeggen dat ik over het algemeen erg tevreden ben, om de volgende redenen:

  1. uitgebreide verzameling componenten die elk een rijke functionaliteit kent

  2. actieve ontwikkeling, volwassen toolkit

  3. voldoende documentatie en code voorbeelden

Bij elke GUI-toolkit zijn er wel componenten die misschien niet zo goed gedocumenteerd zijn als anderen. En misschien ook minder goed dan dat je had gewild. In Ant Design geldt dat naar mijn mening voor de tree component. Gezien de complexe functionaliteit die dit component biedt, bevat het slechts een handvol voorbeeldcode die een aantal aspecten van het component beschrijven. En met betrekking tot drag-and-drop is er slechts één voorbeeld. Ik heb zelf een tijd geworsteld met het verslepen van nodes binnen de tree component en bij het zoeken naar oplossingen en informatie op internet, vond ik veel lotgenoten die met vergelijkbare vragen rondliepen, maar geen verhelderende antwoorden. Ik ben in deze materie gedoken en ik ga nu proberen die leemte op te vullen met deze technische blogpost.

Het probleem

Ik gebruik de tree component o.a. om de structuur van een roman weer te geven. In mijn applicatie representeert een node een hoofdstuk, of een scène, of zgn. ‘front- of back matter’, om er maar een vakterm uit de uitgeefwereld tegenaan te gooien. Ik wil de gebruiker in staat stellen om nodes binnen de tree te kunnen verslepen, zodat hij de structuur van zijn roman eenvoudig aan kan passen. Niet elk type node mag naar elke willekeurige plek gesleept worden. Om je ‘business rules’ te waarborgen stelt de tree component de ‘allowDrop’ callback beschikbaar, die door de tree component aangeroepen wordt, telkens wanneer de versleepte node een onderliggende node passeert. Deze callback geeft een boolean terug, die aangeeft of de node op de aangegeven plek mag landen of niet. Op het moment dat je de muis loslaat boven een toegestane plek, roept de tree component de ‘onDrop’ callback aan. In deze callback implementeer je de logica om de versleepte node zijn nieuwe plek in de tree component te geven en hem van zijn oude locatie te verwijderen. Verwarring die veroorzaakt wordt door de (te) beknopte - en op een enkele plek onjuiste - documentatie, wordt versterkt door een slechte API. Ik zal dit hieronder toelichten en onderbouwen.

allowDrop

De allowDrop kent de volgende, eenvoudige signatuur: ({ dropNode, dropPosition }) ⇒ boolean. De parameter dropPosition is een integer getal en kan drie waarden aannemen, namelijk -1, 0 of +1. Deze geven de positie aan waar de versleepte node gaat landen als op dat moment de muisknop losgelaten wordt, namelijk: voor (-1), op (0) of na (+1) de node waar je bovenhangt. Wanneer je de versleepte node op de dropNode laat landen, dan wil de gebruiker dat de versleepte node een kind wordt van de dropNode.

Het enige dat je naast de dropNode en dropPosition nog nodig hebt om je business rules te implementeren is de node die wordt versleept. Want als dit bijvoorbeeld een hoofdstuk is, dan kun je die niet op een scène laten landen, omdat een scène geen hoofdstukken kan bevatten. De dragNode wordt helaas door de tree component niet in de callback parameters beschikbaar gemaakt. Dat is naar mijn mening een tekortkoming in de API, maar één waar goed omheen te werken is.

We kunnen dit bijvoorbeeld oplossen door de versleepte node te registreren en beschikbaar te maken in de allowDrop callback, bijvoorbeeld door gebruik te maken van de callbacks onDragStart en onDragEnd. Deze callbacks krijgen de versleepte node door. Je kunt in React de useRef() hook gebruiken om de versleepte node vast te leggen bij onDragStart en deze weer te wissen bij onDragEnd. In de voorbeeldcode op GitHub heb ik deze oplossing gebruikt.

Wanneer allowDrop false teruggeeft, zal de onDrop niet aangeroepen worden wanneer het slepen eindigt. Als allowDrop true teruggeeft, dan wordt dat gevisualiseerd met een horizontale hulplijn die aangeeft waar de versleepte node zal gaan landen (als je op dat moment de muisknop los zou laten). Soms moet je je muis een beetje naar rechts slepen, om aan te geven dat je de versleepte node op de dropNode wil laten vallen, en niet erachter.

Dit brengt ons naar een subtiel en uitzonderlijk probleem in de tree component: om de allowDrop te forceren OP een lege dropNode en niet erachter, moet je je muis naar rechts bewegen tot rond de 12de tot 15de teken van het label van de dropnode of de onderliggende node, anders genereert de tree component niet de verwachte allowDrop. Lege nodes met korte labels zorgen voor problemen, omdat dan het 'hover-gebied' te kort is, met als gevolg dat je in die situatie bijvoorbeeld geen scene aan een leeg hoofdstuk toe kunt voegen. Mijns inziens is dit een bug in de tree component. Bij deze blogpost zit voorbeeld code, waarmee je deze bug kunt naspelen (probeer maar eens een scène aan de lege container ‘Act 4’ toe te voegen).

onDrop

De documentatie geeft de volgende omschrijving bij de onDrop callback:

“Callback function for when the onDrop event occurs: function({event, node, dragNode, dragNodesKeys}).”

Deze omschrijving is veel te vaag en de signatuur van de callback is niet volledig; dit moet zijn:

function({event, node, dragNode, dragNodesKeys, dropToGap, dropPosition})

En juist die twee laatste parameters die niet genoemd worden in de documentatie, zijn cruciaal om de nieuwe positie van het versleepte element te bepalen. Helaas wordt hier de API foeilelijk. Want de parameter dropPosition van deze callback heeft een heel andere betekenis dan die bij de allowDrop callback. Hij had beter dropIndex kunnen heten, want het bevat de index in een array. Maar let op: die index moet je interpreteren, afhankelijk van de waarde van de boolean parameter dropToGap. Een naam die ik niet kan verklaren op basis van de betekenissen die het binnen de onDrop callback heeft.

Als dropToGap false is, geeft dit aan dat de dropPosition de index bevat van de dropNode in zijn parent. Omdat we de dropNode al hebben via de node parameter in de callback aanroep, heeft deze dropPostion in casu weinig waarde. In de praktijk betekent false voor de dropToGap parameter, dat de gebruiker de versleepte node als eerste kind in de dropNode wil invoegen.

Als dropToGap true is, dan betekent het dat de versleepte node op de dropPosition geplaatst moet worden van de parent van de dropNode. Ook hier zijn weer een aantal bijzonderheden te melden, namelijk:

  • is in deze situatie de dropPosition -1, dan moet de versleepte node als eerste kind element aan de root van de tree ingevoegd worden.

  • wanneer je een node binnen een container versleept, bijv. je wilt een scene binnen een hoofdstuk verplaatsen, dan maakt het uit of je de scene omhoog of omlaag sleept. Want de dropPosition bevat de index waar ingevoegd moet worden, voordat de dragNode verwijderd is. Verplaats je omhoog, dan heb je hier geen last van, maar verplaats je omlaag, dan maakt het dus uit of je de dragNode eerst invoegt op zijn nieuwe positie en daarna verwijderd van zijn oude positie, of omgekeerd. (In dit laatste geval moet je de dropPosition corrigeren door het met 1 te verlagen).

“Show me the code”

De theorie hierboven is abstract en complex, daarom heb ik een stukje demo code beschikbaar gemaakt op GitHub GitHub en op Stackblitz, waarin alle bovengenoemde aspecten aan bod komen. In de demo heb ik drie verschillende type nodes gemaakt, die met verschillende achtergrondkleuren op het scherm getoond worden. Dit zijn een scene (grijze achtergrond), front- en back matter (rode achtergrond) en een container (zwarte achtergrond). De regels zijn simpel: een scene kan alleen in een container voorkomen en een container kan een scene of een andere container bevatten. De root van de tree kan een container of back- of front matter bevatten. Probeer het maar uit, en sleep de nodes maar heen en weer. Waar mogen ze geplaatst worden en waar niet?

Het gedrag zoals in deze blog beschreven geldt voor de versies van Ant Design voor React v4.x tot en met v6.3.x. Misschien veranderd de implementatie van de tree component nog in de toekomst. Totdat de genoemde issues opgelost zijn, hoop ik dat deze blogpost je voldoende houvast biedt om jouw eigen business logica in de allowDrop en onDrop callbacks te implementeren.