Repository: gradio-app/gradio

Issue:
[Blocks] Implement other event triggers besides `click` & support event triggers across multiple components
Currently, the only event trigger we support is `click`. 

1. Can we add support additional event triggers? Some of these will be component specific, but something like an "on change" event would be useful for many different components. For example, in my Block below, I would like to run the function any time the input sliders change value

```python
block = gr.Blocks()

with block:
    gr.Markdown("Let's do some kinematics! Choose the speed and angle to see the trajectory.")
    
    with gr.Row():
        speed = gr.inputs.Slider(1, 30, default=25)
        angle = gr.inputs.Slider(0, 90, default=45)
    output = gr.outputs.Image(type="plot")
    btn = gr.Button("Run")
    btn.click(plot, [speed, angle], output)

block.launch()
```

2. It would be nice if I could easily assign a trigger to "if any of these two sliders change" without having to write two separate event trigger lines

3. It would be _really_ nice if I could just say something like "if anything changes in this Block" re-run all of the function(s). I would assume that many of our users want this functionality for their blocks. Could we have a parameter in `gr.Blocks()` like `live=True`. In other words, if you run `gr.Blocks(live=True)`, then you have an automatically responsive Block in which if any of the input components change, all of the functions are re-run and the outputs automatically refresh. 


Discussion:
Just came across this one when trying to add [Quickdraw](https://gradio.app/building_a_pictionary_app/) as a tab! +1 to this. 
**Proposed Component Events**

| Component           | Event     | When?                             | Notes                         |
|---------------------|-----------|-----------------------------------|-------------------------------|
| **Audio**           | `change`  | audio source changes              | should this be `on:data`?     |
|               | `edit`  | audio is edited (trimmed)         |      |
|                     | `clear`   | audio source is reset to nothing  |                               |
|                     | `play`   | audio is played  |                               |
|                     | `pause`   | audio is paused  |                               |
|                     | `stop`   | audio is stopped  |                               |
| **Button**          | `click`   | button is clicked                 |                               |
| **Carousel**        | `change`  | current item changes              |                               |
| **ChatBot**         | `change` | chatbot posts a message           |                               |
| **Checkbox**        | `change`  | checkbox is checked/ unchecked    |                               |
| **CheckboxGroup**   | `change`  | checkbox is checked/ unchecked    |                               |
| **Column**          |           |                                   | static layout component (nyi) |
| **DataFrame**       | `change`  | data changes                      | should this be `on:data`?     |
| **Dropdown**        | `change`  | selected option changes           |                               |
| **File**            | `change`  | file data changes                 | should this be `on:data`?     |
|                     | `clear`   | video source is reset to no value |                               |
| **Grid**            |           |                                   | static layout component (nyi) |
| **HighlightedText** | `change`  | content is passed in              | output only                   |
| **HTML**            | `change`  | content is passed in              | output only                   |
| **Image**           | `change`  | image source changes              | should this be `on:data`?     |
|               | `edit`  | image is edited         |      |
|                     | `clear`   | image source is reset to nothing  |                               |
| **JSON**            | `change`  | content is passed in              | output only                   |
| **KeyValue**            | `change`  | content is passed in              | output only                   |
| **Label**           | `change`  | content is passed in              | output only                   |
| **Number**          | `change`  | number value changes              |                               |
|                               | `submit`  | form is submitted              |                               |
| **Radio**           | `change`  | selected option changes           |                               |
| **Row**             |           |                                   | static layout component       |
| **Slider**          | `change`  | current value changes             |                               |
| **Tabs**            | `change`  | selected tab changes              |                               |
| **Textbox**         | `change`  | text value changes                |                               |
|                               | `submit`  | form is submitted              |                               |
| **Timeseries**      | `change`  | chart data changes                | should this `on:data`?        |
| **Video**           | `change`  | video source changes              | should this be `on:data`?     |
|                     | `clear`   | video source is reset to no value |                               |
|                     | `play`   | video is played  |                               |
|                     | `pause`   | video is paused  |                               |
|                     | `stop`   | video is stopped  |                               |

I think `change` is better than `data` thinking about it.`data` kind of makes sense the first time something is loaded but for edits, it makes less sense.
@pngwn could you elaborate on this, how can user reset the video source?
**video source is reset to no value**
Click a button and clear the value / set it to null. The reason for the separate event from `change` is because you typically won’t want to run a prediction on a null value. 
> I think change is better than data thinking about it.data kind of makes sense the first time something is loaded but for edits, it makes less sense.

I find `change()` more intuitive as well

A few suggestions:

* For the `textbox`, can we support an event that detects if the user presses the "enter" button? We've heard from users directly and in issues (#681) that they want to submit the data by pressing enter
* I don't see `Image` here but I imagine it should support: `change()`, `clear()`, and perhaps `edit()` if the image is edited using the editor tools
* A couple of currently-output components are missing: [KeyValues](https://gradio.app/docs/#o_keyvalues), [HighlightedText](https://gradio.app/docs/#o_highlightedtext), which should support `change()`
* What about supporting a `.play()` event for `Audio` and `Video`?
* Nit: consider renaming `Chatbot.message()` to `Chatbot.change()` for consistency

- A`submit` even would be fine, I'll add that to `Number` and `Textbox`.
- Yeah I missed `Image`, I think it would be the same as Files, etc. `change` would cover edits but we can add the more specific `edit` event too.
- We can add all of the audio control events for `Audio` and `Video`. play/pause/stop. `Audio` will need `edit` as well (if we are adding it to Image)
- `HighlightedText` is in the table, `KeyValues` is not, I did n't know about that one.
- I'll change `Chatbot` from `message` to `change`.

We have a sketch component, would drawing update be `change `as well?
@FarukOzderim yeah it would. Technically that is just an update to the image source as Sketch is a variant of the image component rather than a component in its own right. 
This is great stuff, love it. 
Shouldn't "submit" apply to chatbot too? When a user presses enter. And "change" if a user types a single letter? 
No because the `Chatbot` component only displays messages like this: 
![image](https://user-images.githubusercontent.com/1778297/158446896-6d4ed9fe-3972-4c11-8b98-535f28804936.png)

(Doesn't include the input textbox)
Updated the table and implemented all of the events on the frontend in #807.

We can use the table above as a source of truth for syncing front and backend but also for documentation. We should just keep editing it as anything changes.
> Updated the table and implemented all of the events on the frontend in #807.
> 
> We can use the table above as a source of truth for syncing front and backend but also for documentation. We should just keep editing it as anything changes.

Let's reference the changes if we edit the table in the future as it would make further updates easier.

Resolving patch:
diff --git a/gradio/blocks.py b/gradio/blocks.py
--- a/gradio/blocks.py
+++ b/gradio/blocks.py
@@ -19,77 +19,33 @@ def __init__(self):
             Context.root_block.blocks[self._id] = self
         self.events = []
 
-    def click(self, fn, inputs, outputs):
-        if not isinstance(inputs, list):
-            inputs = [inputs]
-        if not isinstance(outputs, list):
-            outputs = [outputs]
-        Context.root_block.fns.append(fn)
-        Context.root_block.dependencies.append(
-            {
-                "id": len(Context.root_block.dependencies),
-                "targets": [self._id],
-                "trigger": "click",
-                "inputs": [block._id for block in inputs],
-                "outputs": [block._id for block in outputs],
-            }
-        )
-
-    def change(
-        self, fn: str, inputs: List["Component"], outputs: List["Component"]
+    def set_event_trigger(
+        self,
+        event_name: str,
+        fn: Callable,
+        inputs: List[Component],
+        outputs: List[Component],
     ) -> None:
         """
-        Adds change event to the component's dependencies.
-
-        Whenever the component changes the function is triggered.
-
+        Adds an event to the component's dependencies.
         Parameters:
-            fn: function name
+            event_name: event name
+            fn: Callable function
             inputs: input list
             outputs: output list
-
         Returns: None
-
         """
+        # Support for singular parameter
         if not isinstance(inputs, list):
             inputs = [inputs]
         if not isinstance(outputs, list):
             outputs = [outputs]
-        Context.root_block.fns.append(fn)
-        Context.root_block.dependencies.append(
-            {
-                "targets": [self._id],
-                "trigger": "change",
-                "inputs": [block._id for block in inputs],
-                "outputs": [block._id for block in outputs],
-            }
-        )
 
-    def save(
-        self, fn: str, inputs: List["Component"], outputs: List["Component"]
-    ) -> None:
-        """
-        Adds save event to the component's dependencies.
-
-        Whenever the component is saved the function is triggered.
-
-        Parameters:
-            fn: function name
-            inputs: input list
-            outputs: output list
-
-        Returns: None
-
-        """
-        if not isinstance(inputs, list):
-            inputs = [inputs]
-        if not isinstance(outputs, list):
-            outputs = [outputs]
         Context.root_block.fns.append(fn)
         Context.root_block.dependencies.append(
             {
                 "targets": [self._id],
-                "trigger": "save",
+                "trigger": event_name,
                 "inputs": [block._id for block in inputs],
                 "outputs": [block._id for block in outputs],
             }
@@ -120,7 +76,15 @@ def get_template_context(self):
 
 
 class Tabs(BlockContext):
-    pass
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
 
 
 class TabItem(BlockContext):
@@ -131,6 +95,16 @@ def __init__(self, label):
     def get_template_context(self):
         return {"label": self.label}
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
 
 class Blocks(Launchable, BlockContext):
     def __init__(self, theme="default"):
diff --git a/gradio/components.py b/gradio/components.py
--- a/gradio/components.py
+++ b/gradio/components.py
@@ -9,7 +9,7 @@
 import tempfile
 import warnings
 from types import ModuleType
-from typing import Any, Dict, List, Optional, Tuple
+from typing import Any, Callable, Dict, List, Optional, Tuple
 
 import numpy as np
 import pandas as pd
@@ -358,6 +358,26 @@ def deserialize(self, x):
         """
         return x
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
+    def submit(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("submit", fn, inputs, outputs)
+
 
 class Number(Component):
     """
@@ -473,6 +493,26 @@ def deserialize(self, y):
         """
         return y
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
+    def submit(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("submit", fn, inputs, outputs)
+
 
 class Slider(Component):
     """
@@ -584,6 +624,16 @@ def deserialize(self, y):
         """
         return y
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
 
 class Checkbox(Component):
     """
@@ -667,6 +717,16 @@ def deserialize(self, x):
         """
         return x
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
 
 class CheckboxGroup(Component):
     """
@@ -784,6 +844,16 @@ def deserialize(self, x):
         """
         return x
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
 
 class Radio(Component):
     """
@@ -876,6 +946,16 @@ def deserialize(self, x):
         """
         return x
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
 
 class Dropdown(Radio):
     """
@@ -1211,6 +1291,36 @@ def deserialize(self, x):
         y = processing_utils.decode_base64_to_file(x).name
         return y
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
+    def edit(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("edit", fn, inputs, outputs)
+
+    def clear(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("clear", fn, inputs, outputs)
+
 
 class Video(Component):
     """
@@ -1323,6 +1433,56 @@ def postprocess(self, y):
     def deserialize(self, x):
         return processing_utils.decode_base64_to_file(x).name
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
+    def clear(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("clear", fn, inputs, outputs)
+
+    def play(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("play", fn, inputs, outputs)
+
+    def pause(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("pause", fn, inputs, outputs)
+
+    def stop(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("stop", fn, inputs, outputs)
+
 
 class Audio(Component):
     """
@@ -1565,6 +1725,66 @@ def postprocess(self, y):
     def deserialize(self, x):
         return processing_utils.decode_base64_to_file(x).name
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
+    def edit(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("edit", fn, inputs, outputs)
+
+    def clear(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("clear", fn, inputs, outputs)
+
+    def play(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("play", fn, inputs, outputs)
+
+    def pause(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("pause", fn, inputs, outputs)
+
+    def stop(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("stop", fn, inputs, outputs)
+
 
 class File(Component):
     """
@@ -1683,6 +1903,26 @@ def postprocess(self, y):
             "data": processing_utils.encode_file_to_base64(y),
         }
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
+    def clear(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("clear", fn, inputs, outputs)
+
 
 class Dataframe(Component):
     """
@@ -1848,6 +2088,16 @@ def postprocess(self, y):
                 + ". Please choose from: 'pandas', 'numpy', 'array'."
             )
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
 
 class Timeseries(Component):
     """
@@ -1937,6 +2187,16 @@ def postprocess(self, y):
         """
         return {"headers": y.columns.values.tolist(), "data": y.values.tolist()}
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
 
 class State(Component):
     """
@@ -2071,6 +2331,35 @@ def restore_flagged(self, dir, data, encryption_key):
         except ValueError:
             return data
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
+
+class KeyValues(Component):
+    """
+    Component displays a table representing values for multiple fields.
+    Output type: Union[Dict, List[Tuple[str, Union[str, int, float]]]]
+    Demos: text_analysis
+    """
+
+    def __init__(self, default: str = " ", *, label: Optional[str] = None, **kwargs):
+        """
+        Parameters:
+        default (str): IGNORED
+        label (str): component name in interface.
+        """
+        raise DeprecationWarning(
+            "The KeyValues component is deprecated. Please use the DataFrame or JSON "
+            "components instead."
+        )
+
 
 class HighlightedText(Component):
     """
@@ -2129,6 +2418,16 @@ def save_flagged(self, dir, label, data, encryption_key):
     def restore_flagged(self, dir, data, encryption_key):
         return json.loads(data)
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
 
 class JSON(Component):
     """
@@ -2171,6 +2470,16 @@ def save_flagged(self, dir, label, data, encryption_key):
     def restore_flagged(self, dir, data, encryption_key):
         return json.loads(data)
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
 
 class HTML(Component):
     """
@@ -2202,6 +2511,16 @@ def get_shortcut_implementations(cls):
             "html": {},
         }
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
 
 class Carousel(Component):
     """
@@ -2281,6 +2600,16 @@ def restore_flagged(self, dir, data, encryption_key):
             for sample_set in json.loads(data)
         ]
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
 
 class Chatbot(Component):
     """
@@ -2316,6 +2645,16 @@ def postprocess(self, y):
         """
         return y
 
+    def change(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("change", fn, inputs, outputs)
+
 
 # Static Components
 class Markdown(Component):
@@ -2327,6 +2666,16 @@ class Button(Component):
     def __init__(self, default_value: str = "", *, label: str, **kwargs):
         super().__init__(label=label, **kwargs)
 
+    def click(self, fn: Callable, inputs: List[Component], outputs: List[Component]):
+        """
+        Parameters:
+            fn: Callable function
+            inputs: List of inputs
+            outputs: List of outputs
+        Returns: None
+        """
+        self.set_event_trigger("click", fn, inputs, outputs)
+
 
 # TODO: (faruk) does this take component or interface as a input?
 # see this line in Carousel


Test patch: