Drag and Drop, Anvil Native!

Hopefully a useful method of implementing drag and drop in an Anvil native way (no extra JS libraries to pick through!)

dragdropgif

Step 1:

Create a blank app (I’ve used a Material Design 3 here).

Step 2:

In you main form, set your imports:

import anvil.js

Step 3:

Set up your form’s init to make the drop zones work and give your drop columns an identifier:

class Form1(Form1Template):
  def __init__(self, **properties):
    
    self.init_components(**properties)

    grid1 = anvil.js.get_dom_node(self.grid_panel_1)
    grid1.classList.add('gridnumber1')
    grid2 = anvil.js.get_dom_node(self.grid_panel_2)
    grid2.classList.add('gridnumber2')

     # Add event handlers to grid_panel_1 for dropping items
    drop_node = anvil.js.get_dom_node(self.grid_panel_1)
    drop_node.removeEventListener('dragover', self.on_drag_over)
    drop_node.removeEventListener('dragenter', self.on_drag_enter)
    drop_node.removeEventListener('drop', self.on_drop)
    drop_node.addEventListener('dragover', self.on_drag_over)
    drop_node.addEventListener('dragenter', self.on_drag_enter)
    drop_node.addEventListener('drop', self.on_drop)

    # Add event handlers to grid_panel_2 for dropping items
    drop_node = anvil.js.get_dom_node(self.grid_panel_2)
    drop_node.removeEventListener('dragover', self.on_drag_over)
    drop_node.removeEventListener('dragenter', self.on_drag_enter)
    drop_node.removeEventListener('drop', self.on_drop)
    drop_node.addEventListener('dragover', self.on_drag_over)
    drop_node.addEventListener('dragenter', self.on_drag_enter)
    drop_node.addEventListener('drop', self.on_drop)

    self.make_draggable()

Step 4:

Set up your make_draggable function:

def make_draggable(self,**events):
    # Make the label elements draggable in grid_panel_1
    self.components1 = self.grid_panel_1.get_components()
    
    for node in self.components1:
      drag_node = anvil.js.get_dom_node(node)
      drag_node.setAttribute('draggable', 'true')
      drag_node.removeEventListener('dragstart', self.on_drag_start)
      drag_node.removeEventListener('dragend', self.on_drag_end)
      drag_node.addEventListener('dragstart', self.on_drag_start)
      drag_node.addEventListener('dragend', self.on_drag_end)
    # Make the label elements draggable in grid_panel_2
    self.components2 = self.grid_panel_2.get_components()
    for node in self.components2:
      drag_node = anvil.js.get_dom_node(node)
      drag_node.setAttribute('draggable', 'true')
      drag_node.removeEventListener('dragstart', self.on_drag_start)
      drag_node.removeEventListener('dragend', self.on_drag_end)
      drag_node.addEventListener('dragstart', self.on_drag_start)
      drag_node.addEventListener('dragend', self.on_drag_end)

Step 5:

Set up your drag and drop event functions:

def on_drag_start(self, event):
    # Set the data that will be transferred during the drag operation
    event.dataTransfer.setData('text/html', event.target.outerHTML)

  def on_drag_end(self, event):
    # Remove the item from the source panel
    source_component = event.target
    source_panel = source_component.parentNode
    
    if source_component and source_component.parentNode:
      if source_component.parentNode.contains(source_component):
        source_component.parentNode.removeChild(source_component)

  def on_drag_over(self, event):
    # Allow dropping on the target by preventing the default action
    event.preventDefault()

  def on_drag_enter(self, event):
    # Get a reference to the target panel
    target_panel = anvil.js.get_dom_node(event.target)
    
    # Check that the target panel is a valid drop target
    if target_panel.classList.contains('drop-target'):
        # Add a CSS class to indicate that the panel is a valid drop target
        target_panel.classList.add('drag-over')
        
        # Allow the drop operation
        event.preventDefault()

  def on_drop(self, event):
    # Get the data that was transferred during the drag operation
    data = event.dataTransfer.getData('text/html')

    # Remove the class from the target to unhighlight it
    event.target.classList.remove('drag-over')

    # Add the dropped item to the target panel
    target_panel = anvil.js.get_dom_node(event.target)
    newcomponent = HtmlTemplate()
    newcomponent.html = data
    if target_panel.classList.contains("gridnumber1"):
      oldcomponent = Label(text=data)
      oldcomponent.remove_from_parent()
      self.grid_panel_1.add_component(newcomponent)
    elif target_panel.classList.contains("gridnumber2"):
      oldcomponent = Label(text=data)
      oldcomponent.remove_from_parent()
      self.grid_panel_2.add_component(newcomponent)
    else:
      print("Didn't work")

    self.make_draggable()

Step 6:

Add column panels to your form and call them grid_panel_1 and grid_panel_2.

Drop a few cards in your grid_panel_1 column.

Run your app!

Recap:

Without using additional libraries, you’ve made items in your columns draggable AND droppable.
How you handle the initial creation of the cards and the handling of data is totally open to your imagination!

Goodnight, Jira. Fairwell, Trello.

Anvilistas assemble!

Clone

Updated: to remove a custom html component and just use HtmlTemplate instead, thanks to @jshaffstall

Optional:

I’ve taken the choice to make my grid-item cards “elevated-card” role components, and to give them some interactivity I’ve updated the anvil-role-elevated-card CSS as follows:

.anvil-role-elevated-card {
  overflow: hidden;
  border-radius: 12px;
  background-color: %color:Surface%;
  /* 2dp */  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 2px 6px 2px rgba(0, 0, 0, 0.15);
  padding: 15px;
  transition: all 0.2s ease-in-out;
}

.anvil-role-elevated-card:hover {
  transform: scale(0.98); /* reduce size to 0.97 on hover */
  box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.2); /* add a small box shadow */
  opacity: 0.9; /* reduce opacity to 0.8 on hover */
}
5 Likes

Cool!

Would be great if you could include a link to a live demo and a clone link.

Apols, totally forgot. Have added!

1 Like

Nice!

You can do without the empty Custom HTML form by using the HtmlTemplate built into Anvil : Anvil | Login

1 Like

Awsome, did not know about that at all, lol. Have updated the code and the clone app!