|
1 | 1 | import functools
|
2 | 2 | import warnings
|
3 |
| - |
| 3 | +import json |
| 4 | +from copy import deepcopy |
4 | 5 | import flask
|
5 | 6 |
|
6 | 7 | from . import exceptions
|
| 8 | +from ._utils import stringify_id, AttributeDict |
7 | 9 |
|
8 | 10 |
|
9 | 11 | def has_context(func):
|
@@ -46,16 +48,158 @@ def states(self):
|
46 | 48 | @property
|
47 | 49 | @has_context
|
48 | 50 | def triggered(self):
|
| 51 | + """ |
| 52 | + Returns a list of all the Input props that changed and caused the callback to execute. It is empty when the |
| 53 | + callback is called on initial load, unless an Input prop got its value from another initial callback. |
| 54 | + Callbacks triggered by user actions typically have one item in triggered, unless the same action changes |
| 55 | + two props at once or the callback has several Input props that are all modified by another callback based on |
| 56 | + a single user action. |
| 57 | +
|
| 58 | + Example: To get the id of the component that triggered the callback: |
| 59 | + `component_id = ctx.triggered[0]['prop_id'].split('.')[0]` |
| 60 | +
|
| 61 | + Example: To detect initial call, empty triggered is not really empty, it's falsy so that you can use: |
| 62 | + `if ctx.triggered:` |
| 63 | + """ |
49 | 64 | # For backward compatibility: previously `triggered` always had a
|
50 | 65 | # value - to avoid breaking existing apps, add a dummy item but
|
51 | 66 | # make the list still look falsy. So `if ctx.triggered` will make it
|
52 | 67 | # look empty, but you can still do `triggered[0]["prop_id"].split(".")`
|
53 | 68 | return getattr(flask.g, "triggered_inputs", []) or falsy_triggered
|
54 | 69 |
|
| 70 | + @property |
| 71 | + @has_context |
| 72 | + def triggered_prop_ids(self): |
| 73 | + """ |
| 74 | + Returns a dictionary of all the Input props that changed and caused the callback to execute. It is empty when |
| 75 | + the callback is called on initial load, unless an Input prop got its value from another initial callback. |
| 76 | + Callbacks triggered by user actions typically have one item in triggered, unless the same action changes |
| 77 | + two props at once or the callback has several Input props that are all modified by another callback based |
| 78 | + on a single user action. |
| 79 | +
|
| 80 | + triggered_prop_ids (dict): |
| 81 | + - keys (str) : the triggered "prop_id" composed of "component_id.component_property" |
| 82 | + - values (str or dict): the id of the component that triggered the callback. Will be the dict id for pattern matching callbacks |
| 83 | +
|
| 84 | + Example - regular callback |
| 85 | + {"btn-1.n_clicks": "btn-1"} |
| 86 | +
|
| 87 | + Example - pattern matching callbacks: |
| 88 | + {'{"index":0,"type":"filter-dropdown"}.value': {"index":0,"type":"filter-dropdown"}} |
| 89 | +
|
| 90 | + Example usage: |
| 91 | + `if "btn-1.n_clicks" in ctx.triggered_prop_ids: |
| 92 | + do_something()` |
| 93 | + """ |
| 94 | + triggered = getattr(flask.g, "triggered_inputs", []) |
| 95 | + ids = AttributeDict({}) |
| 96 | + for item in triggered: |
| 97 | + component_id, _, _ = item["prop_id"].rpartition(".") |
| 98 | + ids[item["prop_id"]] = component_id |
| 99 | + if component_id.startswith("{"): |
| 100 | + ids[item["prop_id"]] = AttributeDict(json.loads(component_id)) |
| 101 | + return ids |
| 102 | + |
| 103 | + @property |
| 104 | + @has_context |
| 105 | + def triggered_id(self): |
| 106 | + """ |
| 107 | + Returns the component id (str or dict) of the Input component that triggered the callback. |
| 108 | +
|
| 109 | + Note - use `triggered_prop_ids` if you need both the component id and the prop that triggered the callback or if |
| 110 | + multiple Inputs triggered the callback. |
| 111 | +
|
| 112 | + Example usage: |
| 113 | + `if "btn-1" == ctx.triggered_id: |
| 114 | + do_something()` |
| 115 | +
|
| 116 | + """ |
| 117 | + component_id = None |
| 118 | + if self.triggered: |
| 119 | + prop_id = self.triggered_prop_ids.first() |
| 120 | + component_id = self.triggered_prop_ids[prop_id] |
| 121 | + return component_id |
| 122 | + |
55 | 123 | @property
|
56 | 124 | @has_context
|
57 | 125 | def args_grouping(self):
|
58 |
| - return getattr(flask.g, "args_grouping", []) |
| 126 | + """ |
| 127 | + args_grouping is a dict of the inputs used with flexible callback signatures. The keys are the variable names |
| 128 | + and the values are dictionaries containing: |
| 129 | + - “id”: (string or dict) the component id. If it’s a pattern matching id, it will be a dict. |
| 130 | + - “id_str”: (str) for pattern matching ids, it’s the strigified dict id with no white spaces. |
| 131 | + - “property”: (str) The component property used in the callback. |
| 132 | + - “value”: the value of the component property at the time the callback was fired. |
| 133 | + - “triggered”: (bool)Whether this input triggered the callback. |
| 134 | +
|
| 135 | + Example usage: |
| 136 | + @app.callback( |
| 137 | + Output("container", "children"), |
| 138 | + inputs=dict(btn1=Input("btn-1", "n_clicks"), btn2=Input("btn-2", "n_clicks")), |
| 139 | + ) |
| 140 | + def display(btn1, btn2): |
| 141 | + c = ctx.args_grouping |
| 142 | + if c.btn1.triggered: |
| 143 | + return f"Button 1 clicked {btn1} times" |
| 144 | + elif c.btn2.triggered: |
| 145 | + return f"Button 2 clicked {btn2} times" |
| 146 | + else: |
| 147 | + return "No clicks yet" |
| 148 | +
|
| 149 | + """ |
| 150 | + triggered = getattr(flask.g, "triggered_inputs", []) |
| 151 | + triggered = [item["prop_id"] for item in triggered] |
| 152 | + grouping = getattr(flask.g, "args_grouping", {}) |
| 153 | + |
| 154 | + def update_args_grouping(g): |
| 155 | + if isinstance(g, dict) and "id" in g: |
| 156 | + str_id = stringify_id(g["id"]) |
| 157 | + prop_id = f"{str_id}.{g['property']}" |
| 158 | + |
| 159 | + new_values = { |
| 160 | + "value": g.get("value"), |
| 161 | + "str_id": str_id, |
| 162 | + "triggered": prop_id in triggered, |
| 163 | + "id": AttributeDict(g["id"]) |
| 164 | + if isinstance(g["id"], dict) |
| 165 | + else g["id"], |
| 166 | + } |
| 167 | + g.update(new_values) |
| 168 | + |
| 169 | + def recursive_update(g): |
| 170 | + if isinstance(g, (tuple, list)): |
| 171 | + for i in g: |
| 172 | + update_args_grouping(i) |
| 173 | + recursive_update(i) |
| 174 | + if isinstance(g, dict): |
| 175 | + for i in g.values(): |
| 176 | + update_args_grouping(i) |
| 177 | + recursive_update(i) |
| 178 | + |
| 179 | + recursive_update(grouping) |
| 180 | + |
| 181 | + return grouping |
| 182 | + |
| 183 | + # todo not sure whether we need this, but it removes a level of nesting so |
| 184 | + # you don't need to use `.value` to get the value. |
| 185 | + @property |
| 186 | + @has_context |
| 187 | + def args_grouping_values(self): |
| 188 | + grouping = getattr(flask.g, "args_grouping", {}) |
| 189 | + grouping = deepcopy(grouping) |
| 190 | + |
| 191 | + def recursive_update(g): |
| 192 | + if isinstance(g, (tuple, list)): |
| 193 | + for i in g: |
| 194 | + recursive_update(i) |
| 195 | + if isinstance(g, dict): |
| 196 | + for k, v in g.items(): |
| 197 | + if isinstance(v, dict) and "id" in v: |
| 198 | + g[k] = v["value"] |
| 199 | + recursive_update(v) |
| 200 | + |
| 201 | + recursive_update(grouping) |
| 202 | + return grouping |
59 | 203 |
|
60 | 204 | @property
|
61 | 205 | @has_context
|
|
0 commit comments