Skip to content

Commit 44be623

Browse files
authored
Merge pull request #529 from materialsproject/messageaio-v2
optimize messageAIO usage
2 parents 33c2c77 + 7bccff6 commit 44be623

2 files changed

Lines changed: 80 additions & 66 deletions

File tree

crystal_toolkit/components/messageAIO.py

Lines changed: 71 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,46 @@
11
"""
2-
Author: Sheng Pang
3-
Modifier: Min-Hsueh Chiu
4-
5-
Message Snake - A reusable Dash notification snackbar component.
2+
Authors: Sheng Pang & Min-Hsueh Chiu
63
4+
messageAIO - A reusable Dash notification snackbar component.
75
Provides fixed-position toast notifications callable from any page.
8-
Supports fade-in/fade-out animations, auto-dismiss, and manual close.
6+
Supports fade-in/fade-out animations (not implemented), auto-dismiss (not implemented), and manual close.
7+
8+
9+
V1: The instantiated messageAIO is limited to one single `msg_type`.
10+
11+
So it becomes difficult to use when an app has multiple msg_types.
12+
Developers would need to instantiate multiple MessageAIO components and
13+
define multiple Outputs in the callback.
14+
15+
V2 improvement:
16+
- use a single instantiation and only one Output per callback.
17+
- If `ids.data` is change, then it should be visible, this logic should be handled here.
18+
19+
920
1021
Usage:
1122
from crystal_toolkit.components.error_msg import MessageAIO
1223
1324
# 1. Include in layout
14-
MessageAIO(
15-
"Invalid composition input!",
16-
aio_id=self.id("invalid-comp-alarm"),
17-
msg_type="error",
18-
),
25+
MessageAIO(aio_id=self.id(<COMPONENT_ID>)),
1926
2027
# 2. Add to callback:
21-
Output(MessageAIO.ids.visible(self.id("invalid-comp-alarm")), "data"),
22-
# Return True to display the message, and False to hide it.
28+
Output(MessageAIO.ids.data(self.id(<COMPONENT_ID>)), "data"),
29+
Return {"message": <DISPLAY MSG>, "msg_type": <MSG_TYPE>}
2330
2431
Note: Do not need to register callbacks as using All-in-one pattern
2532
"""
2633

2734
from __future__ import annotations
2835

29-
from dash import MATCH, Input, Output, callback, ctx, dcc, html
36+
import logging
37+
38+
from dash import MATCH, Input, Output, State, callback, ctx, dcc, html, no_update
3039

3140
from crystal_toolkit.core.mpcomponent import MPComponent
3241

42+
logger = logging.getLogger(__name__)
43+
3344
# Bulma-inspired color scheme for notification types
3445
_TYPE_COLORS = {
3546
"info": {
@@ -106,11 +117,6 @@ class ids:
106117
"subcomponents": "close_button",
107118
"aio_id": aio_id,
108119
}
109-
message = lambda aio_id: {
110-
"component": "MessageAIO",
111-
"subcomponents": "message",
112-
"aio_id": aio_id,
113-
}
114120
div = lambda aio_id: {
115121
"component": "MessageAIO",
116122
"subcomponents": "div",
@@ -121,9 +127,14 @@ class ids:
121127
"subcomponents": "timer",
122128
"aio_id": aio_id,
123129
}
124-
visible = lambda aio_id: {
130+
data = lambda aio_id: {
131+
"component": "MessageAIO",
132+
"subcomponents": "data",
133+
"aio_id": aio_id,
134+
}
135+
message = lambda aio_id: {
125136
"component": "MessageAIO",
126-
"subcomponents": "visible",
137+
"subcomponents": "message",
127138
"aio_id": aio_id,
128139
}
129140

@@ -133,9 +144,7 @@ class ids:
133144

134145
def __init__(
135146
self,
136-
message,
137147
aio_id,
138-
msg_type,
139148
position="bottom-right",
140149
style=None,
141150
show_icon=False,
@@ -151,30 +160,23 @@ def __init__(
151160
to enable auto-dismiss and close functionality.
152161
153162
Args:
154-
message (str): The notification message to display.
155163
id (str): Unique HTML id for the notification container.
156-
msg_type (str): Notification type - 'info', 'warning', 'error', 'success'.
157-
position (str): Fixed position on screen. One of:
164+
position (str, optional): Fixed position on screen. One of:
158165
'top', 'bottom', 'center', 'top-right', 'top-left',
159166
'bottom-right', 'bottom-left'.
160167
style (dict, optional): Additional CSS style overrides.
161-
show_icon (bool): Whether to show a type-specific icon.
162-
min_width (str): Minimum width of the notification.
163-
max_width (str): Maximum width of the notification.
164-
z_index (int): CSS z-index for layering.
165-
auto_dismiss_ms (int): Auto-dismiss delay in milliseconds. Defaults to 50000 (50s).
168+
show_icon (bool, optional): Whether to show a type-specific icon.
169+
min_width (str, optional): Minimum width of the notification.
170+
max_width (str, optional): Maximum width of the notification.
171+
z_index (int, optional): CSS z-index for layering.
172+
auto_dismiss_ms (int, optional): Auto-dismiss delay in milliseconds. Defaults to 50000 (50s).
166173
167174
"""
168175

169176
self.snake_id = aio_id
170177
self.show_icon = show_icon
171-
self.msg_type = msg_type
172-
self.message = message
173178
self.auto_dismiss_ms = auto_dismiss_ms
174179

175-
# Resolve type colors
176-
type_style = _TYPE_COLORS.get(msg_type, _TYPE_COLORS["info"])
177-
178180
# Resolve position
179181
pos_style = _POSITION_STYLES.get(position, _POSITION_STYLES["bottom-right"])
180182

@@ -197,7 +199,6 @@ def __init__(
197199
# Visible on mount; fade-out handled by callback via transition
198200
"opacity": "1",
199201
"transition": "opacity 0.4s ease",
200-
**type_style,
201202
**pos_style,
202203
}
203204

@@ -210,7 +211,10 @@ def __init__(
210211
sub_layouts = self._sub_layouts
211212
super().__init__(
212213
[ # Equivalent to `html.Div([...])`
213-
dcc.Store(id=self.ids.visible(self.snake_id), data=False),
214+
dcc.Store(
215+
id=self.ids.data(self.snake_id),
216+
data={"message": None, "msg_type": "info"},
217+
),
214218
sub_layouts["notification_div"],
215219
sub_layouts["interval"],
216220
],
@@ -235,9 +239,7 @@ def _sub_layouts(self):
235239

236240
# Message text
237241
children.append(
238-
html.Span(
239-
self.message, id=self.ids.message(self.snake_id), style={"flex": "1"}
240-
)
242+
html.Span(id=self.ids.message(self.snake_id), style={"flex": "1"})
241243
)
242244

243245
# Close button
@@ -293,21 +295,42 @@ def layout(self) -> html.Div:
293295
"""
294296

295297
@callback(
296-
Output(ids.wrapper(MATCH), "style"),
297-
Input(ids.visible(MATCH), "data"),
298+
Output(ids.message(MATCH), "children"),
299+
Output(ids.wrapper(MATCH), "style", allow_duplicate=True),
300+
Output(ids.div(MATCH), "style"),
301+
Input(ids.data(MATCH), "data"),
298302
Input(ids.close_button(MATCH), "n_clicks"),
303+
State(ids.div(MATCH), "style"),
299304
prevent_initial_call=True,
300305
)
301-
def sync_message(command_visible, close_clicks):
302-
triggered = ctx.triggered_id
306+
def update_messages(input_data, close_clicks, cur_style):
307+
if not isinstance(input_data, dict):
308+
raise ValueError("`input_data` must be a dictionary for MessageAIO")
303309

304310
if (
305-
isinstance(triggered, dict)
306-
and triggered.get("subcomponents") == "close_button"
311+
isinstance(ctx.triggered_id, dict)
312+
and ctx.triggered_id.get("subcomponents") == "close_button"
307313
):
308-
return {"display": "none"}
314+
return no_update, {"display": "none"}, cur_style
315+
316+
if not input_data:
317+
return no_update, {"display": "none"}, cur_style
318+
319+
message = input_data.get("message", None)
320+
msg_type = input_data.get("msg_type", None)
321+
322+
if not message:
323+
raise ValueError("`message` field is required for MessageAIO")
324+
325+
if not msg_type:
326+
logger.warning("No `msg_type`. Falling back to 'info'")
327+
msg_type = "info"
328+
329+
# Resolve type colors
330+
type_style = _TYPE_COLORS.get(msg_type, {})
331+
cur_style.update(type_style)
309332

310-
return {"display": "block"} if command_visible else {"display": "none"}
333+
return message, {"display": "block"}, cur_style
311334

312335
"""
313336
@callback(

crystal_toolkit/components/pourbaix.py

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -460,14 +460,7 @@ def _sub_layouts(self) -> dict[str, Component]:
460460
html.Div(
461461
[
462462
MessageAIO(
463-
"Invalid composition input!",
464-
aio_id=self.id("invalid-comp-alarm"),
465-
msg_type="error",
466-
),
467-
MessageAIO(
468-
"Invalid concentration input!",
469-
aio_id=self.id("invalid-conc-alarm"),
470-
msg_type="error",
463+
aio_id=self.id("outputConsole"),
471464
),
472465
html.Div(
473466
[
@@ -796,8 +789,7 @@ def get_pourbaix_diagram(pourbaix_entries, **kwargs):
796789

797790
@app.callback(
798791
Output(self.id("graph-panel"), "children"),
799-
Output(MessageAIO.ids.visible(self.id("invalid-comp-alarm")), "data"),
800-
Output(MessageAIO.ids.visible(self.id("invalid-conc-alarm")), "data"),
792+
Output(MessageAIO.ids.data(self.id("outputConsole")), "data"),
801793
Output(self.id("display-composition"), "children"),
802794
Input(self.id(), "data"),
803795
Input(self.id("display-composition"), "children"),
@@ -836,8 +828,7 @@ def make_figure(
836828
logger.error("Invalid composition input!")
837829
return (
838830
self.get_figure_div(),
839-
True,
840-
False,
831+
{"message": "Invalid composition input!", "msg_type": "error"},
841832
"",
842833
)
843834
try:
@@ -853,8 +844,7 @@ def make_figure(
853844
logger.error("Invalid composition input!")
854845
return (
855846
self.get_figure_div(),
856-
True,
857-
False,
847+
{"message": "Invalid composition input!", "msg_type": "error"},
858848
"",
859849
)
860850

@@ -889,8 +879,10 @@ def make_figure(
889879
# if the input is out of pre-defined range, Input will get None
890880
return (
891881
self.get_figure_div(),
892-
False,
893-
True,
882+
{
883+
"message": "Invalid concentration input!",
884+
"msg_type": "error",
885+
},
894886
"",
895887
)
896888

@@ -919,7 +911,6 @@ def make_figure(
919911

920912
return (
921913
self.get_figure_div(figure=figure),
922-
False,
923-
False,
914+
{},
924915
html.Small(f"Pourbaix composition set to {unicodeify(formula)}."),
925916
)

0 commit comments

Comments
 (0)