Skip to content

Commit 5dde752

Browse files
committed
optimize messageAIO usage
1 parent 33c2c77 commit 5dde752

2 files changed

Lines changed: 72 additions & 68 deletions

File tree

crystal_toolkit/components/messageAIO.py

Lines changed: 62 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,39 @@
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+
from dash import MATCH, Input, Output, State, callback, dcc, html, no_update
3037

3138
from crystal_toolkit.core.mpcomponent import MPComponent
3239

@@ -106,11 +113,6 @@ class ids:
106113
"subcomponents": "close_button",
107114
"aio_id": aio_id,
108115
}
109-
message = lambda aio_id: {
110-
"component": "MessageAIO",
111-
"subcomponents": "message",
112-
"aio_id": aio_id,
113-
}
114116
div = lambda aio_id: {
115117
"component": "MessageAIO",
116118
"subcomponents": "div",
@@ -121,9 +123,14 @@ class ids:
121123
"subcomponents": "timer",
122124
"aio_id": aio_id,
123125
}
124-
visible = lambda aio_id: {
126+
data = lambda aio_id: {
127+
"component": "MessageAIO",
128+
"subcomponents": "data",
129+
"aio_id": aio_id,
130+
}
131+
message = lambda aio_id: {
125132
"component": "MessageAIO",
126-
"subcomponents": "visible",
133+
"subcomponents": "message",
127134
"aio_id": aio_id,
128135
}
129136

@@ -133,9 +140,7 @@ class ids:
133140

134141
def __init__(
135142
self,
136-
message,
137143
aio_id,
138-
msg_type,
139144
position="bottom-right",
140145
style=None,
141146
show_icon=False,
@@ -151,30 +156,23 @@ def __init__(
151156
to enable auto-dismiss and close functionality.
152157
153158
Args:
154-
message (str): The notification message to display.
155159
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:
160+
position (str, optional): Fixed position on screen. One of:
158161
'top', 'bottom', 'center', 'top-right', 'top-left',
159162
'bottom-right', 'bottom-left'.
160163
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).
164+
show_icon (bool, optional): Whether to show a type-specific icon.
165+
min_width (str, optional): Minimum width of the notification.
166+
max_width (str, optional): Maximum width of the notification.
167+
z_index (int, optional): CSS z-index for layering.
168+
auto_dismiss_ms (int, optional): Auto-dismiss delay in milliseconds. Defaults to 50000 (50s).
166169
167170
"""
168171

169172
self.snake_id = aio_id
170173
self.show_icon = show_icon
171-
self.msg_type = msg_type
172-
self.message = message
173174
self.auto_dismiss_ms = auto_dismiss_ms
174175

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

@@ -197,7 +195,6 @@ def __init__(
197195
# Visible on mount; fade-out handled by callback via transition
198196
"opacity": "1",
199197
"transition": "opacity 0.4s ease",
200-
**type_style,
201198
**pos_style,
202199
}
203200

@@ -210,7 +207,10 @@ def __init__(
210207
sub_layouts = self._sub_layouts
211208
super().__init__(
212209
[ # Equivalent to `html.Div([...])`
213-
dcc.Store(id=self.ids.visible(self.snake_id), data=False),
210+
dcc.Store(
211+
id=self.ids.data(self.snake_id),
212+
data={"message": None, "msg_type": "info"},
213+
),
214214
sub_layouts["notification_div"],
215215
sub_layouts["interval"],
216216
],
@@ -235,9 +235,7 @@ def _sub_layouts(self):
235235

236236
# Message text
237237
children.append(
238-
html.Span(
239-
self.message, id=self.ids.message(self.snake_id), style={"flex": "1"}
240-
)
238+
html.Span(id=self.ids.message(self.snake_id), style={"flex": "1"})
241239
)
242240

243241
# Close button
@@ -293,21 +291,35 @@ def layout(self) -> html.Div:
293291
"""
294292

295293
@callback(
296-
Output(ids.wrapper(MATCH), "style"),
297-
Input(ids.visible(MATCH), "data"),
294+
Output(ids.wrapper(MATCH), "style", allow_duplicate=True),
298295
Input(ids.close_button(MATCH), "n_clicks"),
299296
prevent_initial_call=True,
300297
)
301-
def sync_message(command_visible, close_clicks):
302-
triggered = ctx.triggered_id
298+
def sync_message(close_clicks):
299+
return {"display": "none"}
303300

304-
if (
305-
isinstance(triggered, dict)
306-
and triggered.get("subcomponents") == "close_button"
307-
):
308-
return {"display": "none"}
301+
@callback(
302+
Output(ids.message(MATCH), "children"),
303+
Output(ids.wrapper(MATCH), "style", allow_duplicate=True),
304+
Output(ids.div(MATCH), "style"),
305+
Input(ids.data(MATCH), "data"),
306+
Input(ids.wrapper(MATCH), "style"),
307+
State(ids.div(MATCH), "style"),
308+
prevent_initial_call=True,
309+
)
310+
def update_messages(input_data, cur_wrapper_style, cur_style):
311+
print(input_data)
312+
if not input_data:
313+
return no_update, {"display": "none"}, cur_style
314+
315+
message = input_data.get("message", None)
316+
msg_type = input_data.get("msg_type", "info")
317+
318+
# Resolve type colors
319+
type_style = _TYPE_COLORS.get(msg_type, _TYPE_COLORS["info"])
320+
cur_style.update(type_style)
309321

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

312324
"""
313325
@callback(

crystal_toolkit/components/pourbaix.py

Lines changed: 10 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,8 @@ 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"),
793+
# Output(MessageAIO.ids.visible(self.id("invalid-conc-alarm")), "data"),
801794
Output(self.id("display-composition"), "children"),
802795
Input(self.id(), "data"),
803796
Input(self.id("display-composition"), "children"),
@@ -836,8 +829,7 @@ def make_figure(
836829
logger.error("Invalid composition input!")
837830
return (
838831
self.get_figure_div(),
839-
True,
840-
False,
832+
{"message": "Invalid composition input!", "msg_type": "error"},
841833
"",
842834
)
843835
try:
@@ -853,8 +845,7 @@ def make_figure(
853845
logger.error("Invalid composition input!")
854846
return (
855847
self.get_figure_div(),
856-
True,
857-
False,
848+
{"message": "Invalid composition input!", "msg_type": "error"},
858849
"",
859850
)
860851

@@ -889,8 +880,10 @@ def make_figure(
889880
# if the input is out of pre-defined range, Input will get None
890881
return (
891882
self.get_figure_div(),
892-
False,
893-
True,
883+
{
884+
"message": "Invalid concentration input!",
885+
"msg_type": "error",
886+
},
894887
"",
895888
)
896889

@@ -919,7 +912,6 @@ def make_figure(
919912

920913
return (
921914
self.get_figure_div(figure=figure),
922-
False,
923-
False,
915+
{},
924916
html.Small(f"Pourbaix composition set to {unicodeify(formula)}."),
925917
)

0 commit comments

Comments
 (0)