What triggers component re-rendering (i.e. call __panel__)?

I’m making a custom component that manages a list of files the user has selected:

import param
import panel as pn
from panel.viewable import Viewer
import logging
import os
# from src.frontend.components.file_selector_modal import FileSelectorModal
pn.extension()
logger = logging.getLogger(name="app")

class FileManager(Viewer):
    
    files = param.List(["File1.txt","File2.csv"],item_type=str,doc="A list of filenames")
    
    def __init__(self, **params):
        # not sure if init should come first, but I tried it both ways with no luck
        self._refresh()
        super().__init__(**params)

    @param.depends('files', watch=True)
    def _refresh(self):
        logger.info(f"Refreshing FileManager with files: {self.files}")
        new_layout = pn.Column("# Uploaded Files")
        if len(self.files) == 0:
            ## this message refers to a button in another part of the app
            new_layout.append(pn.pane.Markdown(f"**Click \"Upload file\" to upload a File**"))
        else:            
            for filename in self.files:
                new_layout.append(self._generate_file_management_widgets(filename))        
        self._layout = new_layout

    def _generate_file_management_widgets(self,filename:str):
        """Helper method to generate the file management widgets for each file"""
        ## multi-select checkbox, view, and delete are not yet implemented
        select_checkbox = pn.widgets.Checkbox(align='center')        
        view_button = pn.widgets.ButtonIcon(icon='eye', description='View the contents of the File',align='center')
        delete_button = pn.widgets.ButtonIcon(icon='trash', description='Remove File from conversation',align='center')
        return pn.Row(
                select_checkbox,
                pn.pane.Markdown(os.path.basename(filename), styles={'margin': "0px"}),
                view_button,
                delete_button
        )

    def __panel__(self):
        logger.info("Rendering FileManager", level="INFO")
        return self._layout

Then my app basically works like this:

from sidebar_widgets import upload_button, uploaded_files

template = pn.template.MaterialTemplate(
        sidebar=[upload_button], 
        site = 'File Manager'
    )

upload_file_button = pn.widgets.Button(name="Upload File", button_type="primary")
upload_file_button.on_click(lambda event: file_manager.files.append(uploaded_files))

file_manager = FileManager()
template.main=[pn.Row(file_manager)]

When the app starts, “Rerendering FileManager” prints to log and it renders as expected with File1.txt, File2.csv and their respective widgets. (Works as expected.)

When I click my upload button, “Refreshing FileManager with files” prints to log as expected, but “Rerendering FileManager” is not printed. It appears __panel__ is never being called after the initial render. What am I missing?

I based this code off of this example in the docs: Combine Existing Components — Panel v1.6.2

Welcome to the community @jonwetzel

You are missing that by design __panel__ only gets called once. I.e. it will return the pn.Column value originally hold by self._layout.

I.e. you need to return something that will be updated.

For example you could set self._layout=pn.Column() and update the contents of that Column in the _refresh function.

Or in __panel__ function you could return a function that depends on 'files' and returns the updated contents. I.e. refactor _refresh into a function like that and return it in the __panel__ function.

I tried the first suggestion and it worked. Thank you very much!

1 Like