flash_unified 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7063ee3fc735eb8fcf88ca5562119d948fc68464e2c7be0696b1e7fbc1542a87
4
+ data.tar.gz: bdddb50bb03254d0d1ddf07d18dae37d48a4623d02a2f7bed83243e3c6920ca4
5
+ SHA512:
6
+ metadata.gz: 399bbe669d11e36909ea54d022ea1365c8f69cccad30ae3d2f2d8e1535e927f204dbcc05e32e184553bb718b5496cfec12d72e79c28bc4c4f4d210af1df8777a
7
+ data.tar.gz: 133059a7460e3cff955b47a187ce42f27300e87774a2890b174a0597a729d7d2c6ea1b5ccf425fc17853183a3a822c9469978f41894d37005f556fcb8ecb71aa
data/LICENSE ADDED
@@ -0,0 +1,5 @@
1
+ Copyright (C) 2025
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted.
4
+
5
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,332 @@
1
+ # FlashUnified
2
+
3
+ FlashUnified provides a unified Flash message rendering mechanism for Rails applications that can be used from both server-side and client-side code.
4
+
5
+ Server-side view helpers embed Flash messages into the page as data, and a lightweight client-side JavaScript reads those embeddings and renders them onto the page.
6
+
7
+ ## Current status
8
+
9
+ This project is considered alpha up to v1.0.0. Public APIs are not stable and may change in future releases.
10
+
11
+ ## Motivation
12
+
13
+ Two concerns motivated this work.
14
+
15
+ One is to be able to present client-originated messages using the same UI as server-side Flash messages. For example, when a large request is blocked by a proxy and a 413 error occurs, the client must handle it because the request does not reach the Rails server; nevertheless we want to display it using the same Flash UI.
16
+
17
+ The other is to support showing Flash messages that originate from Turbo Frames. Displaying Flash inside a frame is straightforward, but in most applications messages are shown outside the frame.
18
+
19
+ ## How it works
20
+
21
+ The key is that rendering must be done on the JavaScript side. We split responsibilities between server and client into two steps:
22
+
23
+ 1. The server embeds the Flash object into the page as hidden DOM elements and returns the rendered page.
24
+ 2. The client-side JavaScript detects page changes, scans those elements, reads the embedded messages, formats them using templates, and inserts them into the specified container element. After rendering, the message elements are removed from the DOM to avoid duplicate displays.
25
+
26
+ This mechanism is simple; the main requirement is to agree on how to embed data. This gem defines a DOM structure for the embedding, which we refer to as "storage":
27
+
28
+ ```erb
29
+ <div data-flash-storage style="display: none;">
30
+ <ul>
31
+ <% flash.each do |type, message| %>
32
+ <li data-type="<%= type %>"><%= message %></li>
33
+ <% end %>
34
+ </ul>
35
+ </div>
36
+ ```
37
+
38
+ Because storage is hidden, it can be placed anywhere in the rendered page. For Turbo Frames, place it inside the frame.
39
+
40
+ The container (where Flash messages are displayed) and the templates used for formatting are independent of the storage location. In other words, even when storage is inside a Turbo Frame, the rendering can target a container outside the frame.
41
+
42
+ For client-detected cases (for example, when a proxy returns 413 on form submission), instead of rendering an error message directly from JavaScript, embed the message into a container element and let the same templates and flow render it as a Flash message.
43
+
44
+ ### Controller example
45
+
46
+ Controller-side procedures for setting Flash are unchanged:
47
+
48
+ ```ruby
49
+ if @user.save
50
+ redirect_to @user, notice: "Created successfully."
51
+ else
52
+ flash.now[:alert] = "Could not create."
53
+ render :new, status: :unprocessable_content
54
+ end
55
+ ```
56
+
57
+ Introducing this gem does not require changes to existing controllers. Layout changes are minimal since storage elements are hidden; usually you only need to adjust the container area for Flash messages.
58
+
59
+ The main implementation task when using this gem is deciding when the embedded data should be rendered as Flash messages. Typically this is done with events. The gem provides helpers that automatically set up event handlers, but you may call rendering methods directly where appropriate.
60
+
61
+ ## Main features
62
+
63
+ This gem provides rules for embedding data and helper tools for implementation.
64
+
65
+ Server-side:
66
+ - View helpers that render DOM fragments expected by the client:
67
+ - Hidden storage elements for temporarily saving messages in the page
68
+ - Templates for the actual display elements
69
+ - A container element indicating where templates should be inserted
70
+ - Localized messages for HTTP status (for advanced usage)
71
+
72
+ Client-side:
73
+ - A minimal ES Module in `flash_unified.js`. Configure via Importmap or the asset pipeline.
74
+ - `auto.js` for automatic initialization (optional)
75
+ - `turbo_helpers.js` for Turbo integration (optional)
76
+ - `network_helpers.js` for network/HTTP error display (optional)
77
+
78
+ Generator:
79
+ - An installer generator that copies the above files into the host application.
80
+
81
+ ## Installation
82
+
83
+ Add the following to your application's `Gemfile`:
84
+
85
+ ```ruby
86
+ gem 'flash_unified'
87
+ ```
88
+
89
+ Then run:
90
+
91
+ ```bash
92
+ bundle install
93
+ ```
94
+
95
+ ## Setup
96
+
97
+ ### 1. Place files
98
+
99
+ Run the installer generator:
100
+
101
+ ```bash
102
+ bin/rails generate flash_unified:install
103
+ ```
104
+
105
+ ### 2. Add JavaScript
106
+
107
+ **Importmap:**
108
+
109
+ `config/importmap.rb` should pin the JavaScript modules you use. The installer generator and sandbox templates pin all shipped modules, but if you manage pins manually, include at least the following:
110
+
111
+ ```ruby
112
+ pin "flash_unified/auto", to: "flash_unified/auto.js"
113
+ pin "flash_unified", to: "flash_unified/flash_unified.js"
114
+ pin "flash_unified/turbo_helpers", to: "flash_unified/turbo_helpers.js"
115
+ pin "flash_unified/network_helpers", to: "flash_unified/network_helpers.js"
116
+ ```
117
+
118
+ If you want the library to set up rendering timing automatically, use `auto.js`. `auto.js` will register Turbo integration events, custom event listeners, and perform initial render handling automatically.
119
+
120
+ If you prefer to control those events yourself, import the core `flash_unified` module and call the provided helpers (for example, `installInitialRenderListener()` for initial render, `installTurboRenderListeners()` from `flash_unified/turbo_helpers` for Turbo lifecycle hooks, and `installCustomEventListener()` to subscribe to `flash-unified:messages`). `turbo_helpers.js` and `network_helpers.js` are optional—pin only the ones you will use.
121
+
122
+ **Asset pipeline (Propshaft / Sprockets):**
123
+
124
+ This gem's JavaScript is provided as ES modules. Instead of `pin`ning, add the following to an appropriate place in your layout:
125
+ ```erb
126
+ <link rel="modulepreload" href="<%= asset_path('flash_unified/flash_unified.js') %>">
127
+ <link rel="modulepreload" href="<%= asset_path('flash_unified/network_helpers.js') %>">
128
+ <link rel="modulepreload" href="<%= asset_path('flash_unified/turbo_helpers.js') %>">
129
+ <link rel="modulepreload" href="<%= asset_path('flash_unified/auto.js') %>">
130
+ <script type="importmap">
131
+ {
132
+ "imports": {
133
+ "flash_unified": "<%= asset_path('flash_unified/flash_unified.js') %>",
134
+ "flash_unified/auto": "<%= asset_path('flash_unified/auto.js') %>",
135
+ "flash_unified/turbo_helpers": "<%= asset_path('flash_unified/turbo_helpers.js') %>",
136
+ "flash_unified/network_helpers": "<%= asset_path('flash_unified/network_helpers.js') %>"
137
+ }
138
+ }
139
+ </script>
140
+ <script type="module">
141
+ import "flash_unified/auto";
142
+ </script>
143
+ ```
144
+
145
+ ### 3. JavaScript initialization
146
+
147
+ When using helpers, ensure the initialization that registers event handlers runs on page load.
148
+
149
+ **Automatic (simple case):**
150
+
151
+ Import `auto.js` in your JS entry (e.g. `app/javascript/application.js`):
152
+
153
+ ```js
154
+ import "flash_unified/auto";
155
+ ```
156
+
157
+ `auto.js` runs initialization when loaded. Its behavior can be controlled with data attributes on `<html>` (described below).
158
+
159
+ **Semi-automatic (Turbo events are set automatically):**
160
+
161
+ When using `turbo_helpers.js`, initialization is not run automatically after import. Call the provided functions:
162
+
163
+ ```js
164
+ import { installInitialRenderListener } from "flash_unified";
165
+ import { installTurboRenderListeners } from "flash_unified/turbo_helpers";
166
+
167
+ installTurboRenderListeners();
168
+ installInitialRenderListener();
169
+ ```
170
+
171
+ This ensures Flash messages are rendered when page changes occur (Turbo events).
172
+
173
+ **Manual (implement your own handlers):**
174
+
175
+ If you implement event registration yourself, at minimum call `renderFlashMessages()` on initial page load. A helper `installInitialRenderListener()` is provided for this purpose:
176
+
177
+ ```js
178
+ import { installInitialRenderListener } from "flash_unified";
179
+ installInitialRenderListener();
180
+ ```
181
+
182
+ Decide an appropriate timing to call `renderFlashMessages()`—typically within an event handler.
183
+
184
+ ## Server setup
185
+
186
+ ### Helpers
187
+
188
+ Server-side view helpers render the DOM fragments the client expects. Most partials do not need changes other than the `flash_templates` partial.
189
+
190
+ - `flash_global_storage` — a global storage element (includes `id="flash-storage"`).
191
+ - `flash_storage` — a storage element; include it inside the content you return.
192
+ - `flash_templates` — templates (`<template>` elements) used by the client.
193
+ - `flash_container` — the container where Flash messages are displayed.
194
+ - `flash_general_error_messages` — a node with messages for HTTP status codes.
195
+
196
+ Important: the JavaScript relies on specific DOM contracts (for example, a global storage element with `id="flash-storage"` and template IDs in the form `flash-message-template-<type>`). Changing these IDs/selectors without updating the JavaScript will break integration.
197
+
198
+ ### Minimal layout example
199
+
200
+ Storage elements can be placed anywhere. Typically they are included near the top of the body:
201
+
202
+ ```erb
203
+ <%= flash_general_error_messages %>
204
+ <%= flash_global_storage %>
205
+ <%= flash_templates %>
206
+ ```
207
+
208
+ Place the visible container where users should see messages:
209
+
210
+ ```erb
211
+ <%= flash_container %>
212
+ ```
213
+
214
+ Embed `flash_storage` inside the response content (for Turbo Frame responses, render it inside the frame):
215
+
216
+ ```erb
217
+ <%= flash_storage %>
218
+ ```
219
+
220
+ ### Template customization
221
+
222
+ To customize the appearance and markup of Flash messages, edit the partial template `app/views/flash_unified/_templates.html.erb` that is copied into your host Rails application by the installer generator.
223
+
224
+ Here is a partial example:
225
+
226
+ ```erb
227
+ <template id="flash-message-template-notice">
228
+ <div class="flash-notice" role="alert">
229
+ <span class="flash-message-text"></span>
230
+ </div>
231
+ </template>
232
+ <template id="flash-message-template-warning">
233
+ <div class="flash-warning" role="alert">
234
+ <span class="flash-message-text"></span>
235
+ </div>
236
+ </template>
237
+ ```
238
+
239
+ Template IDs like `flash-message-template-notice` map to Flash types (`:notice`, `:alert`, `:warning`). The client inserts the message string into `.flash-message-text`. Otherwise the templates are free-form; add elements such as dismiss buttons as needed.
240
+
241
+ ## JavaScript API and extensions
242
+
243
+ The JavaScript is split into a core library and optional helpers. Use only what you need.
244
+
245
+ ### Core (`flash_unified`)
246
+
247
+ - `renderFlashMessages()` — scan storages, render to containers, and remove storages.
248
+ - `appendMessageToStorage(message, type = 'notice')` — append to the global storage.
249
+ - `clearFlashMessages(message?)` — remove rendered messages (all or exact-match only).
250
+ - `processMessagePayload(payload)` — accept `{ type, message }[]` or `{ messages: [...] }`.
251
+ - `installCustomEventListener()` — subscribe to `flash-unified:messages` and process payloads.
252
+ - `storageHasMessages()` — utility to detect existing messages in storage.
253
+ - `startMutationObserver()` — (optional / experimental) monitor insertion of storages/templates and render them.
254
+
255
+ Use `appendMessageToStorage()` and `renderFlashMessages()` to produce client-originated Flash messages:
256
+
257
+ ```js
258
+ import { appendMessageToStorage, renderFlashMessages } from "flash_unified";
259
+
260
+ appendMessageToStorage("File size too large.", "notice");
261
+ renderFlashMessages();
262
+ ```
263
+
264
+ ### Custom event
265
+
266
+ To use custom events, run `installCustomEventListener()` during initialization:
267
+
268
+ ```js
269
+ import { installCustomEventListener } from "flash_unified";
270
+ installCustomEventListener();
271
+ ```
272
+
273
+ Then, at any desired timing, dispatch a `flash-unified:messages` event on the document:
274
+
275
+ ```js
276
+ // Example: passing an array
277
+ document.dispatchEvent(new CustomEvent('flash-unified:messages', {
278
+ detail: [
279
+ { type: 'notice', message: 'Sent successfully.' },
280
+ { type: 'warning', message: 'Expires in one week.' }
281
+ ]
282
+ }));
283
+
284
+ // Example: passing an object
285
+ document.dispatchEvent(new CustomEvent('flash-unified:messages', {
286
+ detail: { messages: [ { type: 'alert', message: 'Operation was cancelled.' } ] }
287
+ }));
288
+ ```
289
+
290
+ ### Turbo helpers (`flash_unified/turbo_helpers`)
291
+
292
+ When using Turbo, partial updates require rendering at the appropriate events. Use the helper to register these listeners:
293
+
294
+ - `installTurboRenderListeners()` — register Turbo lifecycle listeners.
295
+ - `installTurboIntegration()` — a convenience that combines `installTurboRenderListeners()` and `installCustomEventListener()` (used by `auto.js`).
296
+
297
+ ```js
298
+ import { installTurboRenderListeners } from "flash_unified/turbo_helpers";
299
+ installTurboRenderListeners();
300
+ ```
301
+
302
+ ### Network/HTTP helpers (`flash_unified/network_helpers`)
303
+
304
+ Use these helpers to set messages for network/HTTP errors:
305
+
306
+ ```js
307
+ import { notifyNetworkError, notifyHttpError } from "flash_unified/network_helpers";
308
+
309
+ notifyNetworkError();
310
+ notifyHttpError(413);
311
+ ```
312
+
313
+ The messages used here are output as hidden elements by the server-side view helper `flash_general_error_messages`. The original message strings are installed as I18n translation files in `config/locales` by the generator. To change these messages, edit the translations in the corresponding locale file.
314
+
315
+ ### Auto initialization entry (`flash_unified/auto`)
316
+
317
+ Importing `flash_unified/auto` runs initialization after DOM ready. The behavior can be controlled with data attributes on `<html>`:
318
+
319
+ - `data-flash-unified-auto-init="false"` — disable automatic initialization.
320
+ - `data-flash-unified-enable-network-errors="true"` — also enable network/HTTP error listeners.
321
+
322
+ ```erb
323
+ <html data-flash-unified-enable-network-errors="true">
324
+ ```
325
+
326
+ ## Development
327
+
328
+ See [DEVELOPMENT.md](DEVELOPMENT.md) or [DEVELOPMENT.ja.md](DEVELOPMENT.ja.md) for development and testing instructions.
329
+
330
+ ## License
331
+
332
+ This project is licensed under 0BSD (Zero-Clause BSD). See `LICENSE` for details.
@@ -0,0 +1,32 @@
1
+ module FlashUnified
2
+ module ViewHelper
3
+ # General flash-storage using by Turbo Stream
4
+ def flash_global_storage
5
+ render partial: "flash_unified/global_storage"
6
+ end
7
+
8
+ # flash message storage
9
+ def flash_storage
10
+ render partial: "flash_unified/storage"
11
+ end
12
+
13
+ # Render templates partial (the <template> tags consumed by client-side JS).
14
+ # The generator copies `_templates.html.erb` by default; if it's not
15
+ # available, fall back to an inline set of templates so the helper always
16
+ # returns usable markup.
17
+ def flash_templates
18
+ render partial: "flash_unified/templates"
19
+ end
20
+
21
+ # flash message display container
22
+ def flash_container
23
+ render partial: "flash_unified/container"
24
+ end
25
+
26
+ # General error messages
27
+ def flash_general_error_messages
28
+ render partial: "flash_unified/general_error_messages"
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,54 @@
1
+ /*
2
+ Flash Unified Auto-Initialize Entry Point
3
+
4
+ This module automatically initializes the flash message system when imported
5
+ to simplify setup for common cases.
6
+
7
+ Usage patterns:
8
+
9
+ 1) Auto-initialize with Turbo integration (default behavior):
10
+ import "flash_unified/auto";
11
+ // Automatically sets up Turbo listeners and custom events
12
+
13
+ 2) Opt-out via HTML attribute:
14
+ <html data-flash-unified-auto-init="false">
15
+ import "flash_unified/auto";
16
+ // No initialization occurs
17
+
18
+ 3) Enable network error handling:
19
+ <html data-flash-unified-enable-network-errors="true">
20
+ import "flash_unified/auto";
21
+ // Also installs network error listeners
22
+
23
+ Opt-out/Opt-in options:
24
+ - Set <html data-flash-unified-auto-init="false"> to disable auto-initialization
25
+ - Set <html data-flash-unified-enable-network-errors="true"> to enable network error handling
26
+ */
27
+
28
+ import { installTurboIntegration, installNetworkErrorListeners } from 'flash_unified/turbo_helpers';
29
+ import { installInitialRenderListener } from 'flash_unified';
30
+
31
+ if (typeof document !== 'undefined') {
32
+ const root = document.documentElement;
33
+ const autoInit = root.getAttribute('data-flash-unified-auto-init');
34
+
35
+ // Only proceed if not explicitly disabled
36
+ if (autoInit !== 'false') {
37
+ const enableNetworkErrors = root.getAttribute('data-flash-unified-enable-network-errors') === 'true';
38
+
39
+ const init = async () => {
40
+ // Set up Turbo integration and custom event handling
41
+ installTurboIntegration();
42
+ installInitialRenderListener();
43
+ // Optionally install network error helpers
44
+ if (enableNetworkErrors) {
45
+ installNetworkErrorListeners();
46
+ }
47
+ };
48
+ if (document.readyState === 'loading') {
49
+ document.addEventListener('DOMContentLoaded', init, { once: true });
50
+ } else {
51
+ init();
52
+ }
53
+ }
54
+ }