Noneable widgets and labels

I’m trying to create a wrapper widget that extends an existing widget with a checkbox to make it None. The plan is to use these widgets on params when allow_None is set to True.

So far, I’ve managed to create the following CompositeWidget specifically for TextInput although ideally I’d prefer to have a factory function, say MakeOptionalWidget(TextInput) that creates these classes automatically.

In any case, one thing that I can’t figure out is how to get pn.Param to show the label above my new widget, i.e. the following

import panel as pn
import param

pn.extension()

class TextInputOptional(pn.widgets.CompositeWidget):
    value = param.Parameter()

    def __init__(self, **params):
        super().__init__(**params)

        self._textinput_widget = pn.widgets.TextInput(width=240, margin=(0,0))
        self._checkbox_widget = pn.widgets.Checkbox(name="None")
        pn.bind(self._update_text_input_state, self._checkbox_widget, watch=True)
        pn.bind(self._update_value, self._textinput_widget, watch=True)
        self._composite[:] = [
            self._textinput_widget,
            self._checkbox_widget,
        ]

    def _update_text_input_state(self, val):
        self._textinput_widget.disabled = val
        if val:
            self.value = None

    def _update_value(self, val):
        self.value = val


class MyParameterized(param.Parameterized):
    value_not_nullable = param.String()
    value_nullable = param.String(allow_None=True)
    

pn.Param(
    MyParameterized(),
    widgets=dict(value_nullable=TextInputOptional),
    default_layout=pn.Column,
    show_name=True,
    show_labels=True,
)

generates the following (with annotation showing missing label).

image

Alternatively, I suspect Noneable widgets already exist somewhere, so perhaps a pointer to a better implementation than above would be very useful too :slight_smile:

I figured out a solution by overriding the _composite_type attribute. So I now have:

import panel as pn
import param

pn.extension()

def make_optional_widget_class(widget_class):
    class WidgetOptional(pn.widgets.CompositeWidget):
        value = param.Parameter()
    
        def __init__(self, wrapped_widget_params = {}, **params):
            self._widget = widget_class(width=240, margin=(0, 0), **wrapped_widget_params)
            self._checkbox_widget = pn.widgets.Checkbox(name="None")
            pn.bind(self._update_widget_state, self._checkbox_widget, watch=True)
            pn.bind(self._update_value, self._widget, watch=True)
    
            self._composite_type = pn.Column
            super().__init__(**params)
            self._composite[:] = [
                pn.pane.HTML(f'<label for="input">{params["name"]}</label>', margin=(0, 0)),
                pn.Row(self._widget, self._checkbox_widget),
            ]
    
        def _update_widget_state(self, val):
            self._widget.disabled = val
            if val:
                self.value = None
    
        def _update_value(self, val):
            self.value = val

    return WidgetOptional

TextInputOptional = make_optional_widget_class(pn.widgets.TextInput)
IntSliderOptional = make_optional_widget_class(pn.widgets.IntSlider)

class MyParameterized(param.Parameterized):
    string_value = param.String()
    string_value_noneable = param.String(allow_None=True)
    int_value = param.Integer()
    int_value_noneable = param.Integer(allow_None=True)

pn.Param(
    MyParameterized(),
    widgets=dict(
        string_value=pn.widgets.TextInput, 
        string_value_noneable=TextInputOptional,
        int_value_noneable=dict(
            widget_type=IntSliderOptional,
            wrapped_widget_params=dict(start=0, end=8, step=2, value=4)
        ),
    ),
    default_layout=pn.Column,
    show_name=True,
    show_labels=True,
))

Which displays as:

image