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.
75Provides 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
1021Usage:
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
2734from __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
3140from 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(
0 commit comments