panel icon indicating copy to clipboard operation
panel copied to clipboard

Bind Api: Make it easy to create forms with .bind like api.

Open MarcSkovMadsen opened this issue 1 year ago • 1 comments

Request

Make it simple to create forms with .bind like api.

Motivation

I'm trying to create an example for lighting.ai. One of the pages is a form.

image

I would like to

  • use the pn.bind api as that is the recommended one.
  • not nest functions inside functions as that makes the logic hard to reason about and test.
  • write simple and readable code

But I found it takes thinking and wrapper functions to support this very common workflow with the pn.bind api.

Form code: As Is

import panel as pn

def execute_business_logic(input1, input2):
    print(input1, input2)

# Now I want to make a form to execute this 

input1 = pn.widgets.TextInput(value="1")
input2 = pn.widgets.TextInput(value="2")
submit_button = pn.widgets.Button(name="Submit")

def submit(_):
    # Lots of users don't know _. If I use something else linters will complain about unused arguments.
    # It takes mental bandwidth to figure out you need a wrapper function
    execute_business_logic(input1.value, input2.value)

pn.bind(submit, submit_button, watch=True)

# Create the form
pn.Column(
    input1, input2, submit_button
).servable()

I would like to avoid the submit wrapper function to make things simpler and more readable. I think this is a very common pattern and should be supported.

Form Code: To Be

We could introduce bind_as_form

import panel as pn

def _to_value(value):
    if hasattr(value, "value"):
        return value.value
    return value

def bind_as_form(function, *args, submit, watch=False, **kwargs):
    """Extends pn.bind to support "Forms" like binding. I.e. triggering only when a Submit button is clicked,
    but using the dynamic values of widgets or Parameters as inputs.
    
    Args:
        function (_type_): The function to execute
        submit (_type_): The Submit widget or parameter to bind to
        watch (bool, optional): Defaults to False.

    Returns:
        _type_: A Reactive Function
    """
    if not args:
        args = []
    if not kwargs:
        kwargs = {}

    def function_wrapper(_, args=args, kwargs=kwargs):
        args=[_to_value[value] for value in args]
        kwargs={key: _to_value(value) for key, value in kwargs.items()}
        return function(*args, **kwargs)
    return pn.bind(function_wrapper, submit, watch=watch)

This would make the api much simpler

def execute_business_logic(input1, input2):
    print(input1, input2)

# Now I want to make a form to execute this 

input1 = pn.widgets.TextInput(value="1")
input2 = pn.widgets.TextInput(value="2")
submit_button = pn.widgets.Button(name="Submit")

bind_as_form(execute_business_logic, input1=input1, input2=input2, submit=submit_button, watch=True)

# Create the form
pn.Column(
    input1, input2, submit_button
).servable()

Optionally submit could be a list such that multiple widgets could trigger a reexecution of the execute_business_logic function.

Additional Context

I've tried to consider the other apis. But I don't want to use interact or Parameterized classes here as pn.bind is the text book api to use. With watch you still need to create a wrapper function.

Abstraction

Analyzing bind_as_form a bit more we can see that it really does several things

  • Wraps the function to run with the values of some widgets
  • Wraps the function to run when events of some widgets are triggered.

In principle bind_as_form could be replaced by a two step process generalized process

bind_events(
    bind_values(execute_business_logic, input1=input1, input2=input2) # functions.partial would not work here as we want to provide `.value` as argument.
    submit_button, watch=True # This could in principle take multiple arguments
)

MarcSkovMadsen avatar Jul 12 '22 18:07 MarcSkovMadsen

After a long summer holiday reflecting on this, I still think this functionality is key missing piece making Panel harder to use with the pn.bind api than it has to be.

bind_events could also be called bind_trigger or trigger. bind_values could also be called bind_partial or just partial.

MarcSkovMadsen avatar Jul 29 '22 18:07 MarcSkovMadsen

Creating a form with a submit button is certainly a common pattern. I've had to add many forms to an app and we came up with a component that customizes that a little bit more. What you often find in a form is a * symbol - some times in red - that indicates that this field/setting is mandatory. So that component allows to declare that upfront.

I don't know if the solution to that is in a new API. For sure a first good step would be to document how to create a form with a submit button, with pn.bind but also with a Parameterized class.

maximlt avatar Sep 28 '22 22:09 maximlt