-
-
Notifications
You must be signed in to change notification settings - Fork 556
Description
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
)