|
| 1 | +# Modals |
| 2 | + |
| 3 | +Modals are popup dialogs that overlay the content area. They reuse the same component system as toolbars — any `ToolbarItem` works inside a modal — but add overlay behavior, sizing, keyboard handling, and open/close control. |
| 4 | + |
| 5 | +## Creating a Modal |
| 6 | + |
| 7 | +A `Modal` takes a list of toolbar items and is passed alongside your content: |
| 8 | + |
| 9 | +```python |
| 10 | +from pywry import PyWry, Modal, Button, TextInput, Select, Option |
| 11 | + |
| 12 | +app = PyWry() |
| 13 | + |
| 14 | +modal = Modal( |
| 15 | + component_id="settings-modal", |
| 16 | + title="Settings", |
| 17 | + items=[ |
| 18 | + Select( |
| 19 | + label="Language", |
| 20 | + event="settings:language", |
| 21 | + options=[Option(label="Python"), Option(label="JavaScript"), Option(label="Rust")], |
| 22 | + selected="Python", |
| 23 | + ), |
| 24 | + TextInput(label="API Key", event="settings:api-key", placeholder="Enter key..."), |
| 25 | + Button(label="Save", event="settings:save", variant="primary"), |
| 26 | + Button(label="Cancel", event="settings:cancel", variant="ghost"), |
| 27 | + ], |
| 28 | +) |
| 29 | + |
| 30 | +app.show( |
| 31 | + "<h1>Dashboard</h1>", |
| 32 | + modals=[modal], |
| 33 | + callbacks={ |
| 34 | + "settings:save": on_save, |
| 35 | + "settings:cancel": lambda d, e, l: app.emit("modal:close:settings-modal", {}, l), |
| 36 | + }, |
| 37 | +) |
| 38 | +``` |
| 39 | + |
| 40 | +The modal is hidden by default. It renders as an overlay div with a backdrop, and its HTML is injected after the main content. |
| 41 | + |
| 42 | +## Modal Properties |
| 43 | + |
| 44 | +| Property | Type | Default | Description | |
| 45 | +|:---|:---|:---|:---| |
| 46 | +| `component_id` | `str` | auto | Unique identifier — used in open/close commands | |
| 47 | +| `title` | `str` | `"Modal"` | Header text | |
| 48 | +| `items` | `list[ToolbarItem]` | `[]` | Components inside the modal body | |
| 49 | +| `size` | `"sm"` \| `"md"` \| `"lg"` \| `"xl"` \| `"full"` | `"md"` | Preset width | |
| 50 | +| `width` | `str \| None` | `None` | Custom width (overrides `size`) | |
| 51 | +| `max_height` | `str` | `"80vh"` | Maximum height before scrolling | |
| 52 | +| `overlay_opacity` | `float` | `0.5` | Backdrop darkness (0.0–1.0) | |
| 53 | +| `close_on_escape` | `bool` | `True` | ESC key closes the modal | |
| 54 | +| `close_on_overlay_click` | `bool` | `True` | Clicking outside closes the modal | |
| 55 | +| `reset_on_close` | `bool` | `True` | Reset all inputs when closed | |
| 56 | +| `on_close_event` | `str \| None` | `None` | Event emitted when modal closes | |
| 57 | +| `open_on_load` | `bool` | `False` | Open immediately on page load | |
| 58 | +| `style` | `str` | `""` | Inline CSS on the modal container | |
| 59 | +| `script` | `str \| Path \| None` | `None` | JavaScript to inject with the modal | |
| 60 | +| `class_name` | `str` | `""` | Extra CSS class on the modal container | |
| 61 | + |
| 62 | +### Size Presets |
| 63 | + |
| 64 | +| Size | Width | |
| 65 | +|:---|:---| |
| 66 | +| `sm` | Small — compact dialogs, confirmations | |
| 67 | +| `md` | Medium — forms, settings panels | |
| 68 | +| `lg` | Large — data views, multi-column layouts | |
| 69 | +| `xl` | Extra large — dashboards, wide content | |
| 70 | +| `full` | Full viewport width | |
| 71 | + |
| 72 | +## Opening and Closing |
| 73 | + |
| 74 | +### From Python |
| 75 | + |
| 76 | +```python |
| 77 | +# Open |
| 78 | +handle.emit("modal:open:settings-modal", {}) |
| 79 | + |
| 80 | +# Close |
| 81 | +handle.emit("modal:close:settings-modal", {}) |
| 82 | + |
| 83 | +# Toggle |
| 84 | +handle.emit("modal:toggle:settings-modal", {}) |
| 85 | +``` |
| 86 | + |
| 87 | +The event name format is `modal:{action}:{component_id}`. These events are intercepted client-side by the modal handler — they don't round-trip to the server. |
| 88 | + |
| 89 | +### From JavaScript |
| 90 | + |
| 91 | +```javascript |
| 92 | +// Direct API |
| 93 | +pywry.modal.open("settings-modal"); |
| 94 | +pywry.modal.close("settings-modal"); |
| 95 | +pywry.modal.toggle("settings-modal"); |
| 96 | + |
| 97 | +// Or via events (equivalent) |
| 98 | +window.pywry.emit("modal:open:settings-modal", {}); |
| 99 | +``` |
| 100 | + |
| 101 | +### From a Toolbar Button |
| 102 | + |
| 103 | +A common pattern is a toolbar button that opens a modal: |
| 104 | + |
| 105 | +```python |
| 106 | +toolbar = Toolbar( |
| 107 | + position="header", |
| 108 | + items=[Button(label="⚙ Settings", event="modal:open:settings-modal")], |
| 109 | +) |
| 110 | + |
| 111 | +modal = Modal(component_id="settings-modal", title="Settings", items=[...]) |
| 112 | + |
| 113 | +app.show(content, toolbars=[toolbar], modals=[modal]) |
| 114 | +``` |
| 115 | + |
| 116 | +Because the event matches `modal:open:*`, it's handled directly by the frontend — no Python callback needed. |
| 117 | + |
| 118 | +## Close Events |
| 119 | + |
| 120 | +When `on_close_event` is set, PyWry emits that event every time the modal closes (via ESC, overlay click, or programmatic close): |
| 121 | + |
| 122 | +```python |
| 123 | +modal = Modal( |
| 124 | + component_id="confirm-modal", |
| 125 | + title="Confirm Action", |
| 126 | + on_close_event="app:confirm-dismissed", |
| 127 | + items=[ |
| 128 | + Button(label="Confirm", event="app:confirm-yes", variant="primary"), |
| 129 | + Button(label="Cancel", event="modal:close:confirm-modal", variant="ghost"), |
| 130 | + ], |
| 131 | +) |
| 132 | + |
| 133 | +def on_dismissed(data, event_type, label): |
| 134 | + print("User dismissed the confirmation dialog") |
| 135 | + |
| 136 | +app.show(content, modals=[modal], callbacks={"app:confirm-dismissed": on_dismissed}) |
| 137 | +``` |
| 138 | + |
| 139 | +## Reset on Close |
| 140 | + |
| 141 | +When `reset_on_close=True` (the default), all input components inside the modal are reset to their initial values when the modal is closed. This prevents stale form state when reopening. |
| 142 | + |
| 143 | +Set `reset_on_close=False` if you want inputs to preserve their values between open/close cycles: |
| 144 | + |
| 145 | +```python |
| 146 | +modal = Modal( |
| 147 | + component_id="filter-modal", |
| 148 | + title="Filters", |
| 149 | + reset_on_close=False, # Keep selections when reopened |
| 150 | + items=[ |
| 151 | + Select(event="filter:status", options=["Active", "Archived", "All"], selected="Active"), |
| 152 | + Toggle(label="Include drafts", event="filter:drafts", value=False), |
| 153 | + ], |
| 154 | +) |
| 155 | +``` |
| 156 | + |
| 157 | +## Open on Load |
| 158 | + |
| 159 | +Set `open_on_load=True` for modals that should appear immediately — useful for welcome screens, terms acceptance, or first-run configuration: |
| 160 | + |
| 161 | +```python |
| 162 | +modal = Modal( |
| 163 | + component_id="welcome", |
| 164 | + title="Welcome to the Dashboard", |
| 165 | + open_on_load=True, |
| 166 | + close_on_escape=False, |
| 167 | + close_on_overlay_click=False, |
| 168 | + items=[ |
| 169 | + Button(label="Get Started", event="modal:close:welcome", variant="primary"), |
| 170 | + ], |
| 171 | +) |
| 172 | +``` |
| 173 | + |
| 174 | +## Custom Scripts |
| 175 | + |
| 176 | +The `script` parameter lets you inject JavaScript that runs when the modal is loaded. This is useful for custom form validation, dynamic behavior, or third-party integrations: |
| 177 | + |
| 178 | +```python |
| 179 | +modal = Modal( |
| 180 | + component_id="my-modal", |
| 181 | + title="Custom Form", |
| 182 | + items=[ |
| 183 | + TextInput(component_id="email", event="form:email", placeholder="Email"), |
| 184 | + Button(label="Submit", event="form:submit", variant="primary"), |
| 185 | + ], |
| 186 | + script=""" |
| 187 | + document.getElementById('email').addEventListener('input', function(e) { |
| 188 | + const btn = document.querySelector('[data-event="form:submit"]'); |
| 189 | + btn.disabled = !e.target.value.includes('@'); |
| 190 | + }); |
| 191 | + """, |
| 192 | +) |
| 193 | +``` |
| 194 | + |
| 195 | +## Multiple Modals |
| 196 | + |
| 197 | +You can define multiple modals and open them independently: |
| 198 | + |
| 199 | +```python |
| 200 | +app.show( |
| 201 | + content, |
| 202 | + modals=[ |
| 203 | + Modal(component_id="settings", title="Settings", items=[...]), |
| 204 | + Modal(component_id="export", title="Export Data", items=[...]), |
| 205 | + Modal(component_id="help", title="Help", size="lg", items=[...]), |
| 206 | + ], |
| 207 | + toolbars=[ |
| 208 | + Toolbar(position="header", items=[ |
| 209 | + Button(label="⚙", event="modal:open:settings", variant="icon"), |
| 210 | + Button(label="↓", event="modal:open:export", variant="icon"), |
| 211 | + Button(label="?", event="modal:open:help", variant="icon"), |
| 212 | + ]), |
| 213 | + ], |
| 214 | +) |
| 215 | +``` |
| 216 | + |
| 217 | +Only one modal can be visible at a time — opening a new one implicitly closes any currently open modal. |
| 218 | + |
| 219 | +## Styling |
| 220 | + |
| 221 | +Target modals with CSS using the component ID or the `.pywry-modal` class: |
| 222 | + |
| 223 | +```css |
| 224 | +/* All modals */ |
| 225 | +.pywry-modal { |
| 226 | + border-radius: 12px; |
| 227 | +} |
| 228 | + |
| 229 | +/* Specific modal */ |
| 230 | +#settings-modal .pywry-modal-body { |
| 231 | + padding: 24px; |
| 232 | +} |
| 233 | + |
| 234 | +/* Modal overlay */ |
| 235 | +.pywry-modal-overlay { |
| 236 | + backdrop-filter: blur(4px); |
| 237 | +} |
| 238 | +``` |
| 239 | + |
| 240 | +## Next Steps |
| 241 | + |
| 242 | +- **[Toolbar System](toolbars.md)** — Components available inside modals |
| 243 | +- **[Toasts & Alerts](toasts.md)** — Non-blocking notifications |
| 244 | +- **[Theming & CSS](theming.md)** — Customize modal appearance |
| 245 | +- **[Components](../components/index.md)** — Full component API reference |
0 commit comments