11"""
22Author: Sheng Pang
3+ Modifier: Min-Hsueh Chiu
34
45Message Snake - A reusable Dash notification snackbar component.
56
67Provides fixed-position toast notifications callable from any page.
78Supports fade-in/fade-out animations, auto-dismiss, and manual close.
89
910Usage:
10- from mp_web.layouts.message_snake.message_snake import message_snake, register_message_snake_callbacks
11+ from crystal_toolkit.components.error_msg import ErrorMessageAIO
1112
1213 # 1. Include in layout
13- notification = message_snake(
14- message="Operation completed successfully!",
15- msg_type="success",
16- position="bottom-right",
17- snake_id="my-toast",
18- )
14+ ErrorMessageAIO(
15+ "Invalid composition input!",
16+ aio_id=self.id("invalid-comp-alarm"),
17+ msg_type="error",
18+ ).layout(),
1919
20- # 2. Register callbacks (in generate_callbacks or app setup)
21- register_message_snake_callbacks(app, "my-toast")
20+ # 2. Add to callback:
21+ Output(ErrorMessage.ids.visible(self.id("invalid-comp-alarm")), "data"),
22+ # Return True to display the message, and False to hide it.
23+
24+ Note: Do not need to register callbacks as using All-in-one pattern
2225"""
2326
2427from __future__ import annotations
2528
26- from dash import Input , Output , dcc , html
29+ from dash import MATCH , Input , Output , callback , ctx , dcc , html
2730
2831from crystal_toolkit .core .mpcomponent import MPComponent
2932
9194}
9295
9396
94- class ErrorMessage (MPComponent ):
97+ class ErrorMessageAIO (MPComponent ):
98+ class ids :
99+ wrapper = lambda aio_id : {
100+ "component" : "ErrorMessageAIO" ,
101+ "subcomponents" : "wrapper" ,
102+ "aio_id" : aio_id ,
103+ }
104+ close_button = lambda aio_id : {
105+ "component" : "ErrorMessageAIO" ,
106+ "subcomponents" : "close_button" ,
107+ "aio_id" : aio_id ,
108+ }
109+ message = lambda aio_id : {
110+ "component" : "ErrorMessageAIO" ,
111+ "subcomponents" : "message" ,
112+ "aio_id" : aio_id ,
113+ }
114+ div = lambda aio_id : {
115+ "component" : "ErrorMessageAIO" ,
116+ "subcomponents" : "div" ,
117+ "aio_id" : aio_id ,
118+ }
119+ timer = lambda aio_id : {
120+ "component" : "ErrorMessageAIO" ,
121+ "subcomponents" : "timer" ,
122+ "aio_id" : aio_id ,
123+ }
124+ visible = lambda aio_id : {
125+ "component" : "ErrorMessageAIO" ,
126+ "subcomponents" : "visible" ,
127+ "aio_id" : aio_id ,
128+ }
129+
130+ ids = ids
131+
132+ # _callbacks_registered = False # no instance registry
133+
95134 def __init__ (
96135 self ,
97136 message ,
98- id ,
137+ aio_id ,
99138 msg_type ,
100139 position = "bottom-right" ,
101140 style = None ,
@@ -125,13 +164,9 @@ def __init__(
125164 z_index (int): CSS z-index for layering.
126165 auto_dismiss_ms (int): Auto-dismiss delay in milliseconds. Defaults to 50000 (50s).
127166
128- Returns:
129- html.Div: A wrapper containing the notification, CSS animation, and auto-dismiss timer.
130167 """
131- # super().__init__(id=id)
132- self .snake_id = id
133- print ("aaaaa" )
134- print (self .snake_id )
168+
169+ self .snake_id = aio_id
135170 self .show_icon = show_icon
136171 self .msg_type = msg_type
137172 self .message = message
@@ -187,7 +222,9 @@ def _sub_layouts(self):
187222
188223 # Message text
189224 children .append (
190- html .Span (self .message , id = f"{ self .snake_id } -message" , style = {"flex" : "1" })
225+ html .Span (
226+ self .message , id = self .ids .message (self .snake_id ), style = {"flex" : "1" }
227+ )
191228 )
192229
193230 # Close button
@@ -206,21 +243,21 @@ def _sub_layouts(self):
206243 "opacity" : "0.8" ,
207244 "flexShrink" : "0" ,
208245 },
209- id = f" { self .snake_id } -close" ,
246+ id = self .ids . close_button ( self . snake_id ) ,
210247 n_clicks = 0 ,
211248 )
212249 )
213250
214251 # Notification div
215252 notification_div = html .Div (
216253 children = children ,
217- id = f" { self .snake_id } - div" ,
254+ id = self .ids . div ( self . snake_id ) ,
218255 style = self .notification_style ,
219256 )
220257
221258 # Auto-dismiss interval timer (fires once after auto_dismiss_ms)
222259 interval = dcc .Interval (
223- id = f" { self .snake_id } - timer" ,
260+ id = self .ids . timer ( self . snake_id ) ,
224261 interval = self .auto_dismiss_ms ,
225262 n_intervals = 0 ,
226263 max_intervals = 1 ,
@@ -232,55 +269,53 @@ def layout(self) -> html.Div:
232269 sub_layouts = self ._sub_layouts
233270 return html .Div (
234271 [
272+ dcc .Store (id = self .ids .visible (self .snake_id ), data = False ),
235273 sub_layouts ["notification_div" ],
236274 sub_layouts ["interval" ],
237275 ],
238- id = self .snake_id ,
276+ id = self .ids . wrapper ( self . snake_id ) ,
239277 style = {"display" : "none" },
240278 )
241279
242- def generate_callbacks (self , app , cache ) -> None :
243- """Register auto-dismiss and close button callbacks for a message snake.
244-
245- Must be called once per snake_id during app setup (e.g., in generate_callbacks).
246-
247- Args:
248- app: The Dash app instance.
249- snake_id (str): The snake_id used when creating the message_snake.
250- """
280+ @callback (
281+ Output (ids .wrapper (MATCH ), "style" ),
282+ Input (ids .visible (MATCH ), "data" ),
283+ Input (ids .close_button (MATCH ), "n_clicks" ),
284+ prevent_initial_call = True ,
285+ )
286+ def sync_message (command_visible , close_clicks ):
287+ triggered = ctx .triggered_id
251288
252- @app .callback (
253- Output (self .snake_id , "style" ),
254- Input (f"{ self .snake_id } -close" , "n_clicks" ),
255- prevent_initial_call = True ,
256- )
257- def close_message (n_clicks ):
258- print ("what?" )
289+ if (
290+ isinstance (triggered , dict )
291+ and triggered .get ("subcomponents" ) == "close_button"
292+ ):
259293 return {"display" : "none" }
260294
261- """
262- @app.callback(
263- Output(self.id(self.snake_id), "style"),
264- Input(self.id(f"{self.snake_id}-timer"), "n_intervals"),
265- Input(self.id(f"{self.snake_id}-close"), "n_clicks"),
266- State(self.id(self.snake_id), "style"),
267- prevent_initial_call=True,
268- )
269- def _dismiss_message_snake(n_intervals, n_clicks, current_style):
270- # Fade out and hide the message snake on timer or close click.
271- if not current_style:
272- raise PreventUpdate
273-
274- ctx = callback_context
275- if not ctx.triggered:
276- raise PreventUpdate
277-
278- # Apply fade-out: transition opacity to 0, then hide
279- new_style = {**current_style}
280- new_style["transition"] = "opacity 0.4s ease"
281- new_style["opacity"] = "0"
282- new_style["pointerEvents"] = "none"
283- # Override the fade-in animation so it does not reset
284- new_style["animation"] = "none"
285- return new_style
286- """
295+ return {"display" : "block" } if command_visible else {"display" : "none" }
296+
297+ """
298+ @callback(
299+ Output(ids.wrapper(MATCH), "style"),
300+ Input(ids.timer(MATCH), "n_intervals"),
301+ Input(ids.close_button(MATCH), "n_clicks"),
302+ State(ids.wrapper(MATCH), "style"),
303+ prevent_initial_call=True,
304+ )
305+ def _dismiss_message_snake(n_intervals, n_clicks, current_style):
306+ # Fade out and hide the message snake on timer or close click.
307+ if not current_style:
308+ raise PreventUpdate
309+
310+ if not ctx.triggered:
311+ raise PreventUpdate
312+
313+ # Apply fade-out: transition opacity to 0, then hide
314+ new_style = {**current_style}
315+ new_style["transition"] = "opacity 0.4s ease"
316+ new_style["opacity"] = "0"
317+ new_style["pointerEvents"] = "none"
318+ # Override the fade-in animation so it does not reset
319+ new_style["animation"] = "none"
320+ return new_style
321+ """
0 commit comments