panel
panel copied to clipboard
Bind Api: Make it easy to create forms with .bind like api.
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.
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
)
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
.
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.