Referencing dynamically generated components

Hi everyone

I need some help with how to reference a nested dynamically created button so that it runs a function when clicked.

For context, it’s for an idea voting app that people can use for voting ideas up and down. It runs through a data table which contains an ‘Idea’ column, and a ‘Score’ column.

So far, upon init, the app runs through the table and, within a Linear Panel, dynamically generates a Column Panel (card) for each idea, and within each card, dynamically generates labels to show the content of the idea, and buttons for voting up and down. See the image:

What I need help with

I want to set up event handlers to tell the buttons upon being clicked that they should run a function, but I can’t seem to work out how to correctly reference them, most likely following on from them being dynamically generated within other dynamically generated containers.

I have tried getting hold of the buttons using ‘self.linear_panel_1.idea_card.upButton’, which is their nested hierarchy, but that causes this error:

AttributeError: ‘LinearPanel’ object has no attribute ‘idea_card’

So it’s like the linear panel can’t see the Column Panel ‘idea_card’ inside it, even though it has already been dynamically created.

I’ve looked at the docs for event handlers (Anvil Docs | Anvil Components) but can’t seem to apply this here.

Code (simplified to show issue):

class Main_page(Main_pageTemplate):
  def __init__(self, **properties):
    self.init_components(**properties)
    # Voting board
    tableData=anvil.server.call('getideas')

   def vote_button_click(self, **event_args):
     print('test ok') # This is what I want to be run when the upButton is clicked

    for i in range(0,len(tableData)): # This is the loop which displays the ideas
      idea_card = ColumnPanel(role='card')
      self.linear_panel_1.add_component(idea_card) # ColumnPanel added within LinearPanel
      ...
      upButton = Button(text='+1',font_size=12,background='Blue',foreground='white')
      idea_card.add_component(upButton) # Button created

       ### What should this last line be to make the button run vote_button_click upon being clicked?

      self.linear_panel_1.idea_card.upButton.add_event_handler('click', self.vote_button_click)

Some more code which shows the same issue:

  upButton = Button(text='+1',font_size=12,background='Blue',foreground='white')
  idea_card.add_component(upButton)
  idea_card.upButton.add_event_handler('click', self.vote_button_click)

upButton was just added dynamically to idea_card (dynamically generated Column Panel), but the last line causes this error:

'AttributeError: ‘ColumnPanel’ object has no attribute ‘upButton’

https://anvil.works/build#clone:7LXMBIIZSMKQNPY7=FJ5D5GSZK6FHRHMJHHJLJIQ6

When you add a component, that doesn’t automatically create an attribute. If you want an attribute, create it, too:

self.linear_panel_1.add_component(idea_card)
self.linear_panel_1.idea_card = idea_card

This would all be very much simpler with a data grid or repeating panel instead of the dynamic components. There really isn’t any need for those here.

1 Like

I agree with Owen. It’s probably easier to create a template IdeaCard form using the drag-and-drop designer, then use a repeating panel to create one for each idea.

But if you want to do it all in code, to answer your “What should this last line be” question, this should work:

upButton.add_event_handler('click', self.vote_button_click)

You’ll presumably want that function to know which button was clicked–or which idea it corresponds to. But I’ll leave it there for now.

1 Like

Also, looking at your clone, the definition of vote_button_click is in the wrong place. I think you want to put it below, rather than nested within __init__.

A couple other ideas to consider (including a nested function), starting within the __init__ for loop (after initializing self.linear_panel_1.idea_cards = [] just before starting the for loop):

    idea_card.score_label = Label(text=str(tableData[i]["Score"]))
    idea_card.add_component(idea_card.score_label)
    self.linear_panel_1.idea_cards.append(idea_card)
    upButton.add_event_handler('click', self.vote_button_click(i))

def vote_button_click(self, i):
  def inner_click_func(self, **event_args):
    tableData[i]["Score"] += 1
    self.linear_panel_1.idea_cards[i].score_label.text = tableData[i]["Score"]
  return inner_click_func

You can reference a button by using the

events_arg['sender'] .tag

The idea is to set the the button tag as the row and use the above code to get the particular row for that button. Then you can alter it as per your needs.

In the clone link, I have shown upvoting and downvoting. by increasing or decreasing the score.

https://anvil.works/build#clone:AKVMPHE7BWL2R6NZ=64JP3FVUWWZCTPXY7AVPHXKH

1 Like

I went with the repeating panel method in the end, which negated the need to address the complexity of referencing dynamic components in code.

Thanks for everyone for the helpful comments esp. @hugetim and @owen.campbell

4 Likes