Can I disable a button while callback is running?

I’m playing with Panel and Param from Holoviz, and I’m looking for a way to temporarily disable the button while the triggered callback is running.

From the documentation (https://param.holoviz.org/_modules/param/__init__.html#Event) it does read like it is possible. But I can’t seem to make it work…

Here’s a short piece of skeleton code:

import time
import panel as pn
pn.extension()
import param

class TestButton(param.Parameterized):

    action = param.Action(lambda x: x._print_something())
    event = param.Event()

    @param.depends('event', watch = True)
    def _print_something(self):
        print(f'event: {self.event}, action: {self.action}')
        time.sleep(1)
        print(f'event: {self.event}, action: {self.action}')
        
testbutton = TestButton()

dash = pn.Column(
    pn.Param(
        testbutton.param,
    )
)

pn.panel(dash)

Help much appreciated!

Hi @Pill-GZ

Welcome to the community. Great question.

Yes you can disable the buttons while they are executing the associated function.

import time
import panel as pn
pn.extension()
import param

class TestButton(param.Parameterized):

    action = param.Action(lambda x: x._print_something())
    event = param.Event()

    updating = param.Boolean()

    @param.depends('event', watch = True)
    def _print_something(self):
        if self.updating:
            return

        self.updating = True
        print(f'event: {self.event}, action: {self.action}')
        time.sleep(1)
        self.updating = False

testbutton = TestButton()

widgets=pn.Param(testbutton.param, parameters=["action", "event", "updating"])
action_button = widgets[1]
event_button = widgets[2]
updating_checkbox = widgets[3]

action_button.button_type="primary"
event_button.button_type="primary"
updating_checkbox.disabled=True

@param.depends(testbutton.param.updating, watch=True)
def toggle_loading(updating):
    action_button.disabled = updating
    event_button.disabled = updating

dash = pn.Column(action_button, event_button, updating_checkbox)

pn.panel(dash).servable()
1 Like

… and with the upcoming Panel 0.11 release you can easily do something a little bit more awesome.

import time
import panel as pn
pn.extension()
import param

class TestButton(param.Parameterized):

    action = param.Action(lambda x: x._print_something())
    event = param.Event()

    updating = param.Boolean()

    @param.depends('event', watch = True)
    def _print_something(self):
        if self.updating:
            return

        self.updating = True
        print(f'event: {self.event}, action: {self.action}')
        time.sleep(1)
        self.updating = False

testbutton = TestButton()

widgets=pn.Param(testbutton.param, parameters=["action", "event", "updating"])
action_button = widgets[1]
event_button = widgets[2]
updating_checkbox = widgets[3]

action_button.button_type="primary"
event_button.button_type="primary"
updating_checkbox.disabled=True

@param.depends(testbutton.param.updating, watch=True)
def toggle_loading(updating):
    # action_button.disabled = updating
    # event_button.disabled = updating
    action_button.loading = updating
    event_button.loading = updating

template = pn.template.FastListTemplate(
    title="How to disable buttons in Panel?", theme="dark"
)
template.main[:]=[
    pn.Column(
        action_button,
        event_button,
        updating_checkbox
    )
]

template.servable()
2 Likes

Hi @Marc

Thank you! This worked great!

A follow-up question: Why can’t we – or why shouldn’t we – move the toggle_loading function inside the TestButton class?

Is it because disabling the button only takes place on the widget event_button level, and the parameterized class only handles the parameters and their relationships?

What are Parameterized classes designed to do, in general?

(As you can probably tell from my question I’m new to Holoviz and UI-related concepts)

Again, really appreciate your help!
Zheng

1 Like

I’ll try to answer my own questions above (just to share with other newbies like me, since I feel the documentation/examples for the package is a bit inadequate).

  • Is it because disabling the button only takes place on the widget event_button level, and the parameterized class only handles the parameters and their relationships?

Yes. The UI button widget is external to the Parameterized class, and it has not been defined when class or instance were created. Therefore it is not possible to disable the button inside the class or with the instance.

  • What are Parameterized classes designed to do, in general?

To the best of my understanding a Parameterized class has two uses:

  1. To provide attribute validation. E.g. if we have a class called Book, with a price attribute, it is a good idea to perform validation whenever user tries to set the attribute value. Specifically, actions like a_book.price = 'abc' on a_book = Book() object should be prevented. This can be achieved with a Parameterized class by defining price = param.Number().

  2. To define relationships among parameters. E.g. if we want to keep track of both the book price in USD and GBP with two attributes Book.price_USD and Book.price_GBP, then it is a good idea to alter the other price whenever one price changes. Specifically, setting a_book.price_USD = 10 should automatically update the a_book.price_GBP attribute to the corresponding numerical value accounting for exchange rates. This can be achieved in a Parameterized class by defining callbacks via the @param.depends decorator.

@Marc Please correct me if I said anything crazy. And please add to the list if you see other important/interesting uses of the param package.

Cheers

1 Like

Hi @Pill-GZ

THANKS so much for sharing your thoughts and insights. It helps other users in the same situation and it helps the developers of Panel to improve it.

Panel can be a mouthful. It’s still young, it already has so much powerful functionality and the documentation do need some iterations before its here.

If you ever feel like contributing to improving the documentation its just a git clone of https://github.com/holoviz/panel away. The documentation is developed in Jupyter Notebooks that are straightforward to create or update. There is a community of contributors on https://gitter.im/pyviz/pyviz that loves to help other contributors get started. So reach out there.


The validation (1) you get from Parameterized classes is one advantage. The encapsulation of the domain or business knowledge (2) into a Parameterized class is another advantage. Personally I like the Parameterized approach because

  1. It helps med define, model, encapsulate and focus on the domain or problem to solve.
  2. You end up with a self contained component that can be used and shared as such.
  3. You get a more reactive approach. You have some parameters and can react when they change.

But I experience a lot of friends and colleagues who have difficulties with that approach. Either because the don’t have a lot experience with Classes or because they actually favor a functional approach without classes. Most data science coding I see is functional and thus it is a change of mindset to suddenly start writing classes.

Functional Approach

The below is an attempt to solve your problem @Pill-GZ using a functional approach.

For me I really lack having for example the updating parameter to store whether or not my app is updating. I think that is simpler and more general to think about.

Combining the state of updating with the checkbox widget that is shown to the user is not a good idea I think. The updating value could just as well be shown to the user via a CheckButton, String, an Indicator or something else.

import time

import panel as pn

pn.extension()

# Create Domain Functions. I.e. the functions that solves your problem.
# For example extracting, transforming or loading data
# For example creating visualizations

def print_something():
    print('something')
    time.sleep(1)

# Create Widgets and wire them together
updating_checkbox = pn.widgets.Checkbox(name="Updating", disabled=True, )
action_button = pn.widgets.Button(name="Action", button_type="primary")
event_button = pn.widgets.Button(name="Event", button_type="success")

def start_updating():
    updating_checkbox.value = True
    action_button.disabled = True
    event_button.disabled = True

def stop_updating():
    action_button.disabled = False
    event_button.disabled = False
    updating_checkbox.value = False

def click_handler(event):
    if updating_checkbox.value:
        return

    start_updating()
    print_something()
    stop_updating()

action_button.on_click(click_handler)
event_button.on_click(click_handler)

# Create the Layout and serve the app

template = pn.template.FastListTemplate(
    title="How to disable buttons in Panel? Functional Approach", theme="dark"
)
template.main[:]=[
    pn.Column(
        action_button,
        event_button,
        updating_checkbox
    )
]

template.servable()

1 Like

Parameterized Approach

For me the below is an attempt to solve your use case @Pill-GZ using a Parameterized approach.

This is one attempt. There are other ways of doing this that would also work. And you could also mix the two approaches.

But for me this is better. I can model the problem using a Parameterized class, lay it out in a view and end up with a class/ component that makes sense to encapsulate in a library/ package and share with other users.

import time

import panel as pn
import param

pn.extension()



class TestButton(param.Parameterized):

    action = param.Action(lambda x: x._print_something())
    event = param.Event()

    updating = param.Boolean()
    view = param.Parameter(precedence=-1)

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

        self.view = self._create_view()

    @param.depends("event", watch=True)
    def _print_something(self):
        if self.updating:
            return

        self.updating = True
        print(f"event: {self.event}, action: {self.action}")
        time.sleep(1)
        self.updating = False

    @param.depends("updating", watch=True)
    def toggle_loading(self):
        self._action_button.loading = self.updating
        self._event_button.loading = self.updating

    def _create_view(self):
        widgets = pn.Param(
            self,
            parameters=["action", "event", "updating"],
            widgets={
                "action": {"button_type": "primary"},
                "event": {"button_type": "success"},
                "updating": {"disabled": True},
            },
        )
        self._action_button = widgets[1]
        self._event_button = widgets[2]
        self._updating_checkbox = widgets[3]
        return widgets


test_button = TestButton()
template = pn.template.FastListTemplate(
    title="How to disable buttons in Panel? Parameterized Approach", theme="dark"
)
template.main[:] = [test_button.view]

template.servable()

You might event refactor the toggle_loading and _create_view functions into a separate Function or Class that takes the TestButton instance as input.

Hi @Marc,

Thank you!

The second example (with Parameterized class trying to create view) did not disable the buttons on clicking, and I couldn’t figure out why.

1 Like

Hi @Pill-GZ

I believe It’s because you have to wait for Panel 0.11 to be released. The .loading indicator is released in that one. It will be any day now.

Hi @Marc Thank you. I should’ve caught that.

Looking forward to the update!

1 Like

But you should be able to use disabled instead of loading today.

Yes, disabled worked for me. Thanks again!

1 Like

Thank you, @Marc, for this example. I wish examples in #panel would present in two forms - functional and class(ic).
Personally I love functional approach but I’m mathematician by background so biased a bit.
P.S. I do read your comments and posts for inspiration - and so far you’ve been a great help! Thank you!

2 Likes

Wanted to add, with batch_watch, both buttons are disabled/enabled at the same time rather than one by one.

    def toggle_loading(self):
        with param.batch_watch(self):
            self._action_button.loading = self.updating
            self._event_button.loading = self.updating

However, I wonder if there’s a way to speed up disabling it with Javascript because I can spam press the “Action” button a few times before it gets disabled, triggering it multiple times.

Here’s an example with plain widgets

1 Like