okld / streamlit-elements

Create a draggable and resizable dashboard in Streamlit, featuring Material UI widgets, Monaco editor (Visual Studio Code), Nivo charts, and more!
MIT License
730 stars 82 forks source link

Problems accessing values from RadioGroup and Slider #2

Open pwsanders7 opened 2 years ago

pwsanders7 commented 2 years ago

Thank you for creating such an amazing component, it is exactly what I had been hoping for. I have been playing around with it a bit and having difficulty accessing values from the event callbacks for the RadioGroup Below is my code for the radio group:

` with elements('RadioGroup'): if "radio_value" not in st.session_state: st.session_state.radio_value = None

  if st.session_state.radio_value is not None:
      selection = st.session_state.radio_value
  else:
      selection = []
  with mui.FormControl:
      mui.Typography('Radio Test')
      mui.Typography(f'Event Valuue: {selection}')
      with mui.RadioGroup(name = 'RadioTest',defaultValue = 'Never', onChange = sync('radio_value')):
          mui.FormControlLabel(value='Never', control = mui.Radio(), label = 'Never')
          mui.FormControlLabel(value='Always', control = mui.Radio(), label = 'Always')`

When I do this, the "radio_value" attribute in session_state is equal to a dictionary with the following values: {'type': 'change', 'nativeEvent': {'isTrusted': True}, 'target': {'checked': True}, 'eventPhase': 3, 'bubbles': True, 'cancelable': True, 'timeStamp': 3176.4000000059605, 'defaultPrevented': False, 'isTrusted': True}

From this I would have expected to have gotten target.value, but there is only target.checked, which in this case is always true since something is checked.

It is highly possible this is an error in my code but curious if you had any input or if this is just one of the components that still has problems as you described in the Readme.

okld commented 2 years ago

Hello @pwsanders7,

Actually the issue comes from Streamlit Elements. It is related to how I serialize DOM objects to JSON. Fortunately in your case, RadioGroup's onChange callback can still get the selected value with its second argument:

Signature: function(event: React.ChangeEvent<HTMLInputElement>, value: string) => void event: The event source of the callback. value: The value of the selected radio button. You can pull out the new value by accessing event.target.value (string).

Therefore, just changing sync() arguments should fix your code.

# We don't want the first argument anymore (None).
# We want to sync the second argument with st.session_state.radio_value.
with mui.RadioGroup(name='RadioTest', defaultValue='Never', onChange=sync(None, 'radio_value')):
   # ...

Regardless, this is an issue I'll address in an upcoming release.

pwsanders7 commented 2 years ago

That did the trick, thank you so much! This also worked for the slider, where I was having a similar issue, putting none for the first parameter solved it. Looking forward to working more fully with the library now!

pwsanders7 commented 2 years ago

On a related note, how would I handle this if I wanted to create a callback function instead of using sync? In the examples provided in the readme the callbacks all assume the single parameter of event, and I am note sure how to access the value prop this way.

Thank you again!

okld commented 2 years ago

You can create a function with any number of parameter to match callback signatures. In your case, you can replace sync(None, 'radio_value') with:

def handle_change(event, value):
    # event is not used
    st.session_state.radio_value = value

# ...
with mui.RadioGroup(name='RadioTest', defaultValue='Never', onChange=handle_change):
   # ...

But you're right, I'll add another callback example with more than one parameter.

pwsanders7 commented 2 years ago

Great thank you so much!

pwsanders7 commented 2 years ago

Hello again,

On a related note, can I add parameters that are not a part of the callback signature?

For example, I have a text field and I want to create a callback function, where I include a dynamic value for the key of the session state.

For example: def text_to_response(event, state_variable): st.session_state[state_var] = event.target.value mui.TextField(label = label, onChange = text_to_response, multiline = multiline, fullWidth=fullWidth)

In this example, is there any way to add the state_variable argument in the mui.TextField? I have tried using the "props" argument but it does not work for me, and I have tried args.

Thank you again!

okld commented 2 years ago

Hey @pwsanders7,

You can partially apply arguments to a function using functools's partial function. This is something I haven't documented yet because I'd like to find a better alternative, but in the meantime this does what you want.

from streamlit_elements import partial  # or: from functools import partial

def text_to_response(event, state_var):
    st.session_state[state_var] = event.target.value

mui.TextField(
    label=label,
    # This will apply state_var argument to text_to_response,
    # resulting in a function that only takes an event parameter.
    onChange=partial(text_to_response, state_var="my_var"),
    multiline=multiline,
    fullWidth=fullWidth
)
pwsanders7 commented 2 years ago

This is perfect thank you! I think this should cover my use case with no problem. I had recently done a small work around of using a lambda function and using setattr function on st.session_state. Your solution is much better as it will allow for more complex operations, thank you!