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

A callback on a custom component #3977

Closed
whitphx opened this issue Oct 27, 2021 · 10 comments · Fixed by #8633
Closed

A callback on a custom component #3977

whitphx opened this issue Oct 27, 2021 · 10 comments · Fixed by #8633
Assignees
Labels
feature:callbacks feature:custom-components type:enhancement Requests for feature enhancements or new features

Comments

@whitphx
Copy link
Contributor

whitphx commented Oct 27, 2021

Problem

I am a developer of a custom component, streamlit-webrtc, which manages its state in Python code, and there is a request to make it possible to detect state changes through the on_change callback.

However, currently a callback on a custom component is not supported.

Solution

  • To introduce a mechanism to allow component developers to register and trigger a callback from a custom component.
  • It must not be an "automatic" trigger reacting to the changes of the frontend component state or session_state, such as a callback automatically invoked when the return value of _component_func or session_state[key] changes.
    • As stated above, my component manages the state on Python side and the component value differs from the returned value from the frontend component. Additionally, a value stored in the session state of its key is a mutable object so that it cannot be used to detect changes. So, I want a way to manually invoke the callback.
    • Ideally, it might be the best if we have a way to set arbitrary values to WidgetMetadata.callback, WidgetMetadata.callback_args, WidgetMetadata.callback_kwargs, and to forcefully call call_callback(wid) for a specific component.

Additional context


Community voting on feature requests enables the Streamlit team to understand which features are most important to our users.

If you'd like the Streamlit team to prioritize this feature request, please use the 👍 (thumbs up emoji) reaction in response to the initial post.

@whitphx whitphx added type:enhancement Requests for feature enhancements or new features status:needs-triage Has not been triaged by the Streamlit team labels Oct 27, 2021
@vdonato
Copy link
Collaborator

vdonato commented Oct 28, 2021

Thanks for filing an issue for this @whitphx, we'll do some investigation to try to gauge how difficult it would be to implement this.

@vdonato vdonato removed the status:needs-triage Has not been triaged by the Streamlit team label Oct 28, 2021
@vdonato vdonato self-assigned this Oct 28, 2021
@okld
Copy link

okld commented Feb 19, 2022

Hello @whitphx,

I managed to get callbacks working on custom components with some hacks. Let me know if that works with you: https://gist.github.com/okld/1a2b2fd2cb9f85fc8c4e92e26c6597d5

@whitphx
Copy link
Contributor Author

whitphx commented Feb 22, 2022

@okld
It seems to work as expected. Thank you so much.
I actually tried to use it in my library: whitphx/streamlit-webrtc#695. I will test it more though, it seems to be working.
BTW, is it OK to use the code in my MIT-licensed library? (I think it's helpful if some LICENSE file is provided in the gist repo :) ) (It seems OK; https://github.community/t/what-license-do-gists-have/144479)


For the core developers:

Through this development, I found okld's implementation was sufficient for my needs
although I have written as below.

It must not be an "automatic" trigger reacting to the changes of the frontend component state or session_state,

Now I think the design of my library streamlit-webrtc is not following the original design principle of Streamlit custom component, as my library does not directly publish the component value from the frontend but provides its wrapper object to the developers instead.
So, the gap derived from that design should be handled on my side, and it was possible.

Then, please ignore that part of my feature request.
okld's implementation is sufficient.

@raethlein
Copy link
Collaborator

Hey @whitphx, it has been some time since this was opened, but we have recently revisited this part and created a draft PR here: #8633
I wanted to ask you if you had a moment to sanity-check the approach and see whether the change would allow your scenario and get rid of the workaround script 🙂

@Socvest
Copy link

Socvest commented May 17, 2024

Hey @whitphx, it has been some time since this was opened, but we have recently revisited this part and created a draft PR here: #8633 I wanted to ask you if you had a moment to sanity-check the approach and see whether the change would allow your scenario and get rid of the workaround script 🙂

I have a query regarding the removal of args and kwargs. In my scenario, I create one function and mount 7 versions of a single custom component. Then I create one on_change function and include in the args a string of the session_state of the particular component in question. If that component is interacted with, then the on_change function is triggered only for the version component that is interacted with.

An example:


if "final_result_of_all_7" not in st.session_state:
     st.session_state["final_result_of_all_7"] = {
           "one_":[].
           "two_":[],
           "three_":[],
            "four_":[].
           "five_":[]
           "six_":[],
           "seven_":[]
}
     

def onChangeFunction(argument=None):
     
      change_this = st.session_state[argument]
       st.session_state["final_result_of_all_7"][argument] = change_this


key_for_component = "one_"
custom_component(value=list_of_dicts, on_change=onChangeFunction, args=(key_for_component,), key=key_for_component)
key_for_component = "two_"
custom_component(value=list_of_dicts, on_change=onChangeFunction, args=(key_for_component,), key=key_for_component)
key_for_component = "three_"
custom_component(value=list_of_dicts, on_change=onChangeFunction, args=(key_for_component,), key=key_for_component)
key_for_component = "four_"
custom_component(value=list_of_dicts, on_change=onChangeFunction, args=(key_for_component,), key=key_for_component)
key_for_component = "five_"
custom_component(value=list_of_dicts, on_change=onChangeFunction, args=(key_for_component,), key=key_for_component)
key_for_component = "six_"
custom_component(value=list_of_dicts, on_change=onChangeFunction, args=(key_for_component,), key=key_for_component)
key_for_component = "seven_"
custom_component(value=list_of_dicts, on_change=onChangeFunction, args=(key_for_component,), key=key_for_component)

I tried implementing this using the current changes you have suggested and it says this:

'st.session_state has no key "one_". Did you forget to initialize it? More info: https://docs.streamlit.io/library/advanced-features/session-state#initialization'

It seems the on_change is triggering before the component mounts.

Taking out the args and kwargs would mean I would have to create 7 different versions of the on_change function and hard code their session_state keys and then pass it to the on_change_handler.

@raethlein
Copy link
Collaborator

raethlein commented May 18, 2024

@Socvest I believe the way how this should work would be like the following:

from functools import partial

keys = ["one_", "two_", ...]
for key in keys:
  custom_component(value=list_of_dicts, on_change=partial(onChangeFunction, key), key=key)

Does this work for you?

@Socvest
Copy link

Socvest commented May 18, 2024

@raethlein it kind of does but it triggers as though I have clicked on all components at once instead of just one at a time. The aim was to change the session_state based on the click of one of the components kind of like a filter component (dynamic filter). I did end up refactoring my function where it loops through all and manually searches for which component changed before making changes to the st.session_state["final_result_of_all_7"] when one of the components is interacted with.

@raethlein
Copy link
Collaborator

@Socvest This sounds like its not related to the new API addition and whether we pass through args or not, am I getting this right? I am trying to understand whether we would need to change the API signature in order to support all intended use-cases 🙂

@Socvest
Copy link

Socvest commented May 21, 2024

@raethlein Yeah, don't worry. It was a coding issue on my end. I pretty much had to add an if else statement to capture the component with a specific key that was interacted with. All is good. Thanks!

@raethlein
Copy link
Collaborator

@Socvest I see, thanks for spending time on testing it and the feedback!

raethlein added a commit that referenced this issue May 22, 2024
## Describe your changes

In the past, some custom components have used a
patch (https://gist.github.com/okld/1a2b2fd2cb9f85fc8c4e92e26c6597d5) to
register an on_change callback. Recently, we have done some refactoring
that broke this workaround. This PR is a suggestion to extend our
official API to make the patch redundant.

Note that we only want to pass the `on_change_callback` and not the
`args` and `kwargs`.
The `register_widget` function today uses `args` and `kwargs` as
keywords to pass to the `on_change callback`. Besides the unfortunate
naming - these are special keywords meant for functions themselves and
not for pass-through arguments - we are thinking about deprecating them
entirely, since you can wrap the callback easily to pass the arguments.

## GitHub Issue Link (if applicable)

Closes #3977
Related to victoryhb/streamlit-option-menu#70

## Testing Plan

- Explanation of why no additional tests are needed
- Unit Tests (JS and/or Python)
- A new unit test is added to make sure the on_change callback is called
when the value changes during a ScriptRun
- E2E Tests
  - prepare `on_change` callback test in the `option_menu` function
- Any manual testing needed?
- I manually tested it on the example of
[streamlit-option-menu](https://github.com/victoryhb/streamlit-option-menu)

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this
project are made under the Apache 2.0 license.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature:callbacks feature:custom-components type:enhancement Requests for feature enhancements or new features
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants