Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
MarcSkovMadsen opened this issue Jul 12, 2022 · 2 comments
Open

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

MarcSkovMadsen opened this issue Jul 12, 2022 · 2 comments
Labels
type: docs Related to the Panel documentation and examples type: enhancement Minor feature or improvement to an existing feature
Milestone

Comments

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Jul 12, 2022

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 MarcSkovMadsen added the type: enhancement Minor feature or improvement to an existing feature label Jul 12, 2022
@MarcSkovMadsen
Copy link
Collaborator Author

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.

@maximlt
Copy link
Member

maximlt commented Sep 28, 2022

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: docs Related to the Panel documentation and examples type: enhancement Minor feature or improvement to an existing feature
Projects
None yet
Development

No branches or pull requests

2 participants