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 */
}
6 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!

I’m trying to use this to pick up a repeating panel row and drag it to another repeating panel (I’ve created a blank drop_zone in the instance there’s nothing in the DB already). My on_drag_start function is picking up the right item, but I don’t know how to access the data in the LiveObjectProxy to hand it off to the on_drop function (which should find the row in the database and remove the link to where it was, then update the link to where it is now).

Here’s the on_drag_start code:

def on_drag_start(self, event):
    """This method is called when the drag operation is started"""
    # Get the Anvil component associated with the DOM node being dragged
    dragged_component = event.target.anvilComponent
    print('Dragged component:', dragged_component)

    # Check if the dragged component is an instance of ItemTemplate5
    if isinstance(dragged_component, ItemTemplate5):
        # Access the data of the card being dragged
        card_data = dragged_component.item
        print('Card data:', card_data)
        # Print the value of the "Push" column
        print('Push value:', card_data.Push)
        # Store the ID of the Anvil row in the dataTransfer object
        event.dataTransfer.setData('text', card_data.get_id())
        print('Dragged card data:', card_data)

And here’s the errors I’m getting:

Dragged component: <JTBD_App.PushDragDropForm.ItemTemplate5.ItemTemplate5 object>
Card data: <LiveObject: anvil.tables.Row>
AttributeError: 'LiveObjectProxy' object has no attribute 'Push'
at PushDragDropForm, line 107
Drag enter event in PushDragDropForm
Drag enter event in ItemTemplate6
AttributeError: 'DataTransfer' object has no attribute 'get_data

The card_data is a row from a column called Push, and this row is linked to a Global_Push row (the Global_Push column can store multiple Push rows). When I move the row from one panel to another I want to trigger a server side function in the on_drop action to update the database, but I need to be able to pass that function data about both the row that was dropped as well as to pick up data from the DOM on where it has landed.

What am I doing wrong? Is there a guide to help me use these components in the javascript?

You’re trying to access a field in a dictionary, right (or a column in a data table, which is the same syntax)? That would be card_data['Push'], unless I’m missing something.

1 Like

Thanks - and sorry it took a while to respond. This doesn’t work, it throws this error:

SyntaxError: bad input

I wonder whether this is because it’s being called in an anvil.js event?

Instead I was able to get it working using tags:

  def on_drag_start(self, event):
    """This method is called when the drag operation is started"""
    # Get the Anvil component associated with the DOM node being dragged
    dragged_component = event.target.anvilComponent

    # Check if the dragged component is an instance of ItemTemplate5
    if isinstance(dragged_component, ItemTemplate5):
      # Set the ID of the Push row as the data being transferred
      event.dataTransfer.setData('text', dragged_component.push_id)
      # Set the old global push name as the data being transferred
      event.dataTransfer.setData('text/old_global_push_name', 'Ungrouped Pushes')

and:

def on_drop(self, event):
    """This method is called when the dragged data is dropped"""
    # Get the ID of the Push row from the dataTransfer object
    push_id = event.dataTransfer.getData('text')
    # Get the old global push name from the dataTransfer object
    old_global_push_name = event.dataTransfer.getData('text/old_global_push_name')

    # Get the Anvil component associated with the DOM node that the data was dropped on
    dropped_component = event.currentTarget.anvilComponent

    # Check if the dropped component is an instance of ItemTemplate6
    if isinstance(dropped_component, ItemTemplate6):
      # Get the new global push name from the component that the data was dropped on
      new_global_push_name = dropped_component.tag['global_push_name']

      # Get the current project from the open form
      current_project = get_open_form().current_project

      # Call the server function to update the global push and get the moved item
      moved_item = anvil.server.call('update_global_push_pushes', push_id, new_global_push_name, old_global_push_name, current_project)

This might be a clunky way of doing things, and there might be some bad practices here pushing these tags into the client side code, but it seemed to be the best solution I could find on the forum and through web searches.