Here’s an example, in case someone can spot an obvious mistake.
The idea is that:
- There is a ControlCardAIO with a toggle switch
- which can take any children
On interactions:
-
- Any sliders in the children should get disabled if the card is disabled
-
- I can determine (from outside the AIO) if it has been disabled, and what the value of the slider(s) are
Here’s my test code:
from dash import dash, dcc, html, MATCH, ALL, callback
from dash.dependencies import Input, Output
import dash_daq as daq
import uuid
app = dash.Dash(
__name__,
meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}],
)
app.title = "Testing ControlCardAIO"
server = app.server
app.config["suppress_callback_exceptions"] = False
APP_PATH = str(pathlib.Path(__file__).parent.resolve())
control_color = "#FEC037"
class ControlCardAIO(html.Div):
# A set of functions that create pattern-matching callbacks of the subcomponents
class ids:
toggle = lambda aio_id: {
'component': 'ControlCardAIO',
'subcomponent': 'toggle',
'aio_id': aio_id
}
# Make the ids class a public class
ids = ids
# Define the arguments of the All-in-One component
def __init__(
self,
disabled=False,
content=None,
aio_id=None
):
# Allow developers to pass in their own `aio_id` if they're
# binding their own callback to a particular component.
if aio_id is None:
# Otherwise use a uuid that has virtually no chance of collision.
# Uuids are safe in dash deployments with processes
# because this component's callbacks
# use a stateless pattern-matching callback:
# The actual ID does not matter as long as its unique and matches
# the PMC `MATCH` pattern..
aio_id = str(uuid.uuid4())
# Alter the sub-component identifiers
enrich_id(content, aio_id)
# Define the component's layout
super().__init__([ # Equivalent to `html.Div([...])`
html.Div(
children=[
# main switch, should disable the controls within the card
html.Div(daq.BooleanSwitch(on=True, id=self.ids.toggle(aio_id))),
html.Br(),
html.Div(children=content)
],
style=dict(border='1px solid blue', width='300px')
)
])
# Define this component's stateless pattern-matching callback
# that will apply to every instance of this component.
# Disable any slider if card's main toggle is off
@callback(
# will not work if `'id': ALL` is not added
Output({'aio_id': MATCH, 'component_type': 'Slider', 'id': ALL}, 'disabled'),
Input(ids.toggle(MATCH), 'on')
)
def toggle_slider_state(toggle_state):
return [not toggle_state]
def enrich_id(element, aio_id):
if hasattr(element, 'id'):
setattr(element, 'id', {
'id': getattr(element, 'id'),
'aio_id': aio_id,
'component_type': element.__class__.__name__
})
if isinstance(element, list):
for el in element:
enrich_id(el, aio_id)
if hasattr(element, 'children') and isinstance(getattr(element, 'children'), list):
enrich_id(getattr(element, 'children'), aio_id)
app.layout = html.Div([
ControlCardAIO(aio_id='card1',
content=html.Div(
children=[
html.P("ID = card1"),
dcc.Slider(min=0, max=10, value=5, id='slider1'),
# dcc.Checklist(options=['normal', 'elevated', 'critical'], id='check1')
])
),
html.P([
"- Is Card1 disabled? ",
html.Span("n/a", id='control1')
]),
html.P([
"- The value of the slider in Card1 is: ",
html.Span("n/a", id='control2')
])
])
@app.callback(
Output('control1', 'children'),
# Not adding `'component': ALL` prevents pattern matching
Input({'aio_id': 'card1', 'subcomponent': 'toggle', 'component': ALL}, 'on')
)
def react_to_toggle(state):
return [str(state)]
@app.callback(
Output('control2', 'children'),
# Not adding `'component_type': ALL` prevents pattern matching
Input({'aio_id': 'card1', 'id': 'slider1', 'component_type': ALL}, 'value'),
)
def react_to_inner_component(value2):
return [str(value2)]
if __name__ == "__main__":
app.run_server(debug=True, port=8051)
Observations:
-
- only works if I add the ‘id’ component in the callback inside the AIO (see inline comments), even though it may or may not actually exist
-
- only works if I provide a “complete” complex id to the app-level callbacks’ Inputs, stating all components that may have been set (see inline comments)
In both cases, I seem to have to over-qualify the composite identifiers with things I don’t want to have to define. In doing so, I also force the input to be an array of values, instead of a single one, which makes the logic unnecessarily complex…
I am a bit surprised by having to do that. In the documentation on pattern-matching there is this line:
- In fact, in this example, we didn’t actually need
'type': 'filter-dropdown'
. The same callback would have worked with Input({'index': ALL}, 'value')
. We included 'type': 'filter-dropdown'
as an extra specifier in case you create multiple sets of dynamic components.
Did I misunderstand what that means? I took that as meaning "you don’t need to provide all specifiers in a composite ID, if some of the specifier’s values are not relevant in the determination of the components used in the callback…