Custom component that is both a Viewer and Reactive

I am endeavouring to build some custom components that I then want to link (or allow other to link) together via the link method. I found being able to just inherit from panel.viewable.Viewer very effective, but then I wanted to add the extra linking functionality. Inheriting from panel.reactive.Reactive made all of that work perfectly. The catch is that the __panel__ override no longer seems to produce the same results. I have an example case below, taken from the Custom Component docs.

import panel as pn
pn.extension()

class CustomReactivePane(pn.viewable.Viewer, pn.reactive.Reactive):
    
    value = param.Range(doc="A numeric range.")
    width = param.Integer(default=300)
    
    def __init__(self, **params):
        self._start_input = pn.widgets.FloatInput()
        self._end_input = pn.widgets.FloatInput(align='end')
        super().__init__(**params)
        self._layout = pn.Row(self._start_input, self._end_input)
        self._sync_widgets()
    
    def __panel__(self):
        return self._layout
    
    @param.depends('value', 'width', watch=True)
    def _sync_widgets(self):
        self._start_input.name = self.name
        self._start_input.value = self.value[0]
        self._end_input.value = self.value[1]
        self._start_input.width = self.width//2
        self._end_input.width = self.width//2
        
    @param.depends('_start_input.value', '_end_input.value', watch=True)
    def _sync_params(self):
        self.value = (self._start_input.value, self._end_input.value)

If I then have

range_widget = CustomReactivePane(name='Range', value=(0, 10))

pn.Column(
    '## This is a custom widget',
    range_widget
)

what I get as a result in a notebook is:

Column
    [0] Markdown(str)
    [1] CustomReactivePane(name='Range', value=(0, 10))

which contrasts with the nice actual widget panel I get if I either use range_widget._layout or remove the pn.reactive.Reactive from the inheritance. I’ve tried a few things such as explicitly calling the Viewer init etc. but to no success. Is there an easy way to have a custom panel class that is both easily displayed and reactive?

Definitely need to document how to extend some of the core components. By inheriting from Reactive you are sending it down a very different codepath which expects calling the standard rendering machinery that invokes ._get_model(). You can simply forward the args and kwargs to the _layout and it’ll work:

import panel as pn
pn.extension()

class CustomReactivePane(pn.reactive.Reactive):
    
    value = param.Range(doc="A numeric range.")
    width = param.Integer(default=300)
    
    def __init__(self, **params):
        self._start_input = pn.widgets.FloatInput()
        self._end_input = pn.widgets.FloatInput(align='end')
        super().__init__(**params)
        self._layout = pn.Row(self._start_input, self._end_input)
        self._sync_widgets()
    
    def _get_model(self, *args, **kwargs):
        return self._layout._get_model(*args, **kwargs)
    
    @param.depends('value', 'width', watch=True)
    def _sync_widgets(self):
        self._start_input.name = self.name
        self._start_input.value = self.value[0]
        self._end_input.value = self.value[1]
        self._start_input.width = self.width//2
        self._end_input.width = self.width//2
        
    @param.depends('_start_input.value', '_end_input.value', watch=True)
    def _sync_params(self):
        self.value = (self._start_input.value, self._end_input.value)

you might also want to extend pn.widgets.base.CompositeWidget, which directly forwards layout parameters to the container and handles a bit of other housekeeping around making the subcomponents discoverable to Panel:


class CustomReactivePane(pn.widgets.base.CompositeWidget):
    
    value = param.Range(doc="A numeric range.")
    
    _composite_type = pn.Row
    
    def __init__(self, **params):
        self._start_input = pn.widgets.FloatInput()
        self._end_input = pn.widgets.FloatInput(align='end')
        super().__init__(**params)
        self._composite[:] = [self._start_input, self._end_input]
        self._sync_widgets()

    @param.depends('value', 'width', watch=True)
    def _sync_widgets(self):
        self._start_input.name = self.name
        self._start_input.value = self.value[0]
        self._end_input.value = self.value[1]
        self._start_input.width = self.width//2
        self._end_input.width = self.width//2
        
    @param.depends('_start_input.value', '_end_input.value', watch=True)
    def _sync_params(self):
        self.value = (self._start_input.value, self._end_input.value)
        
CustomReactivePane(value=(0, 0), name='Test')
1 Like

Thanks, option one worked well in my actual use case. I’ll have to look into the CompositeWidget a little more. I am pretty new to Panel so this has been quite a learning curve for me, but it really is very powerful. The fact that there is a lot more power not currently explained in the docs is … both awesome (because it does what I want – honestly being able to just use link on a few of my custom components and have everything work as I intended was magical) and understandable (there is a lot to the library, and only so much one can document).

2 Likes