flash_unified 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aea0348851ca5170244e807281bcefa783d5b800f54166546285ec46070d4f6b
4
- data.tar.gz: 5d278c86617549ec9dad0e46b7c3167261d4f36dd1188baf81baf6ab9d536263
3
+ metadata.gz: 1ff9e758b822e2e06c38f1543fc5e2be3b4aa101879bdd1137ab70f01b0cc350
4
+ data.tar.gz: 1df992f44cd7c2db0992b4130d9047af5c7017a26e3367aa395597a884adbddb
5
5
  SHA512:
6
- metadata.gz: dbcbe847be1227697340e8973f36e9d00cb823cf9acdcf530c7c5e580b3638f1ec746bad376072a905d3bc305b9a2acc26c96d1b5b71a1af0668725c33a9a7f5
7
- data.tar.gz: a6459a4ae27cbaf5c2078e403d70b42f1cbfe631fedd82252f08522d028c6e726135e56128787a11cfeeddc13a11e5d5c89f4849b16691d9e89c5f4be9c4bddb
6
+ metadata.gz: 1f327a2f3b87d9974b81b1031f5d787cc6b3f03927398030e0e89fb39e8c0e94232cc6ec86195dbe6e56d53a1cb140d07bb5c52d803a34d1f7930c60584aeacc
7
+ data.tar.gz: fcd91ea10bdeccefc552f5c66e6c6e8e4ddc053acb4f3e638b1c3b8e04db51efd6463478464cc7e308e38bbc7db54fb266ccf21ee7df3782569ffe6664abc575
data/README.md CHANGED
@@ -2,29 +2,28 @@
2
2
 
3
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
4
 
5
- Server-side view helpers embed Flash messages as hidden DOM elements (`data-flash-storage`), and client-side JavaScript scans, formats, and renders them into containers. This enables consistent Flash UI for server messages, Turbo Frame responses, and client-detected errors (e.g., 413 proxy errors).
5
+ Server-side view helpers embed Flash messages as data in the page, and a lightweight client-side JavaScript library reads those storages and renders messages into visible containers using templates.
6
6
 
7
7
  ## Current status
8
8
 
9
- This project is considered alpha up to v1.0.0. Public APIs are not stable and may change in future releases.
9
+ This project is considered alpha through v1.0.0. Public APIs are not yet stable and may change in future releases.
10
10
 
11
11
  ## Motivation
12
12
 
13
- We faced two challenges simultaneously.
13
+ We had two challenges at the same time.
14
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 logic.
15
+ One was to display messages originating from the client-side with the same UI representation as Flash from the server-side. For example, when a large request is blocked by a proxy, we want to display a 413 error as a Flash message. Since the request never reaches the Rails server, this must be handled on the client-side, but we want to display it with the same UI logic as normal Flash.
16
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 cases you want to display them outside the frame.
17
+ The other was to display Flash messages from Turbo Frames as well. It's not a problem if Flash is displayed within the frame, but in most cases it will be displayed outside the frame.
18
18
 
19
19
  ## How it works
20
20
 
21
- The key insight is that rendering must be done on the JavaScript side. We split responsibilities between server and client into two steps:
21
+ The key point to solving these challenges is that rendering needs to be performed on the JavaScript side. Therefore, we devised a two-stage process that divides responsibilities between the server-side and client-side:
22
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 define rules for how to embed data. This gem defines a DOM structure for the embedding, which we call "storage":
23
+ 1. The server embeds the Flash object as a hidden DOM element within the page, renders the page, and returns it.
24
+ 2. When the client-side JavaScript detects a page change, it scans those elements, reads the embedded messages, formats them using templates, and inserts (renders) them into the specified container elements. At that time, message elements are removed from the DOM to avoid duplicate displays.
27
25
 
26
+ The mechanism is simple, and to implement it, we only need to decide on the rules for how to embed. In this gem, we define the embedded DOM structure as follows and call it "storage":
28
27
  ```erb
29
28
  <div data-flash-storage style="display: none;">
30
29
  <ul>
@@ -35,16 +34,13 @@ This mechanism is simple; the main requirement is to define rules for how to emb
35
34
  </div>
36
35
  ```
37
36
 
38
- Because storage is a hidden element, 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 and can be placed anywhere. This means that even when storage is inside a Turbo Frame, the rendering can target a Flash display area outside the frame.
41
-
42
- For client-side handling of cases like proxy errors on form submission, instead of rendering an error message directly from JavaScript, embed the message into a container element first and let the same templates and processing flow render the Flash message.
37
+ Since storage is a hidden element, it can be placed anywhere in the page rendered by the server. For Turbo Frames, place it inside the frame.
43
38
 
44
- ### Controller example
39
+ The "container" where Flash messages are displayed and the "templates" for formatting can be placed anywhere regardless of the storage. This means that even with Turbo Frames, it works with Flash rendering areas placed outside the frame.
45
40
 
46
- Controller-side procedures for setting Flash are unchanged:
41
+ When handling cases on the client-side where a proxy returns an error when a form is submitted, instead of displaying the error message directly from JavaScript, you can render Flash in the same way (using the same templates and processing flow) by temporarily embedding the message as a storage element.
47
42
 
43
+ On the other hand, in controllers that set Flash, there is no difference from the normal Flash message display procedure:
48
44
  ```ruby
49
45
  if @user.save
50
46
  redirect_to @user, notice: "Created successfully."
@@ -54,308 +50,71 @@ else
54
50
  end
55
51
  ```
56
52
 
57
- Introducing this gem does not require changes to existing controllers. There are almost no changes needed to existing page layouts either. The DOM elements to be set in views are hidden elements, and you'll mostly just need to slightly adjust the container area for Flash message display.
58
-
59
- The main implementation task when using this gem is determining when the embedded data should be rendered as Flash messages. Typically this is done with events. Specific handling is left to the implementer, but helpers for automatic event setup are also provided. You can also explicitly call display methods within arbitrary processing.
60
-
61
- ## Main features
62
-
63
- This gem provides the mechanism organized according to defined rules and helper tools to support implementation.
53
+ In other words, when introducing this gem, **no changes are required to existing controllers**. Also, there is almost no need to change existing page layouts. The DOM elements to be set in views are hidden elements, and you only need to slightly adjust the container area where Flash messages are displayed.
64
54
 
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)
55
+ The main thing to implement when introducing this gem is the timing to display embedded data as Flash messages. Normally, you will use events. While the specific implementation is left to the implementer, helpers are provided to automatically set up events. You can also explicitly call methods for display within arbitrary processes.
71
56
 
72
- Client-side:
73
- - A minimal library in `flash_unified.js` (ES Module). 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)
57
+ ## Quick Start
77
58
 
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`:
59
+ ### 1. Installation
84
60
 
61
+ When using Bundler, add the gem entry to your Gemfile:
85
62
  ```ruby
86
63
  gem 'flash_unified'
87
64
  ```
88
65
 
89
- Then run:
90
-
66
+ Run the command:
91
67
  ```bash
92
68
  bundle install
93
69
  ```
94
70
 
95
- ## Setup
96
-
97
- ### 1. File placement (only if customization is needed)
98
-
99
- This gem provides JavaScript, template, and locale translation files from within the engine. Only copy files using the generator if you want to customize them. Details are described below.
100
-
101
- ### 2. JavaScript library setup
102
-
103
- **Importmap:**
104
-
105
- Pin the JavaScript modules you use to `config/importmap.rb`:
106
-
107
- ```ruby
108
- pin "flash_unified", to: "flash_unified/flash_unified.js"
109
- pin "flash_unified/network_helpers", to: "flash_unified/network_helpers.js"
110
- pin "flash_unified/turbo_helpers", to: "flash_unified/turbo_helpers.js"
111
- pin "flash_unified/auto", to: "flash_unified/auto.js"
112
- ```
113
-
114
- Use `auto.js` to set up rendering timing automatically. `auto.js` automatically handles Turbo integration event registration, custom event registration, and initial page rendering.
115
-
116
- If you want to control or implement such events yourself, use the core library `flash_unified.js` to implement rendering processing independently. In that case, `auto.js` is not needed. The helpers `turbo_helpers.js` and `network_helpers.js` are optional, so pin only the ones you will use.
117
-
118
- **Asset pipeline (Propshaft / Sprockets):**
119
-
120
- ```erb
121
- <link rel="modulepreload" href="<%= asset_path('flash_unified/flash_unified.js') %>">
122
- <link rel="modulepreload" href="<%= asset_path('flash_unified/network_helpers.js') %>">
123
- <link rel="modulepreload" href="<%= asset_path('flash_unified/turbo_helpers.js') %>">
124
- <link rel="modulepreload" href="<%= asset_path('flash_unified/auto.js') %>">
125
- <script type="importmap">
126
- {
127
- "imports": {
128
- "flash_unified": "<%= asset_path('flash_unified/flash_unified.js') %>",
129
- "flash_unified/auto": "<%= asset_path('flash_unified/auto.js') %>",
130
- "flash_unified/turbo_helpers": "<%= asset_path('flash_unified/turbo_helpers.js') %>",
131
- "flash_unified/network_helpers": "<%= asset_path('flash_unified/network_helpers.js') %>"
132
- }
133
- }
134
- </script>
135
- <script type="module">
136
- import "flash_unified/auto";
137
- </script>
138
- ```
139
-
140
- ### 3. JavaScript initialization
141
-
142
- When using helpers, ensure the initialization that registers event handlers runs on page load.
143
-
144
- **Automatic initialization (simple implementation case):**
145
-
146
- When using `auto.js`, import `auto` in your JavaScript entry point (e.g., `app/javascript/application.js`):
147
- ```js
148
- import "flash_unified/auto";
71
+ Or install it directly:
72
+ ```bash
73
+ gem install flash_unified
149
74
  ```
150
75
 
151
- Initialization processing is executed simultaneously with import. The behavior at that time can be controlled with data attributes on `<html>`. Details are described below.
152
-
153
- **Semi-automatic control (Turbo events are set up automatically):**
154
-
155
- When using `turbo_helpers.js`, initialization is not run automatically after import. Call the methods from the imported module:
156
- ```js
157
- import { installInitialRenderListener } from "flash_unified";
158
- import { installTurboRenderListeners } from "flash_unified/turbo_helpers";
76
+ ### 2. Client-side setup (Importmap)
159
77
 
160
- installTurboRenderListeners();
161
- installInitialRenderListener();
78
+ Add to `config/importmap.rb`:
79
+ ```ruby
80
+ pin "flash_unified/all", to: "flash_unified/all.bundle.js"
162
81
  ```
163
82
 
164
- This ensures Flash messages are rendered when page changes (Turbo events) are detected.
165
-
166
- **Manual control (implementing event handlers yourself):**
167
-
168
- When implementing event registration and other aspects yourself, you'll typically need to call `renderFlashMessages()` at least on initial page load to process messages that may have been embedded by the server. This has been prepared as `installInitialRenderListener()` since it's a standard procedure:
169
-
170
- ```js
171
- import { installInitialRenderListener } from "flash_unified";
172
- installInitialRenderListener();
83
+ Import in your JavaScript entry point (e.g., `app/javascript/application.js`):
84
+ ```javascript
85
+ import "flash_unified/all";
173
86
  ```
174
87
 
175
- Set up calls to rendering processing at appropriate timing to handle storage elements containing Flash messages embedded by the server. You'll probably write calls to `renderFlashMessages()` within some event handler:
176
-
177
- ```js
178
- renderFlashMessages();
179
- ```
88
+ ### 3. Server-side setup
180
89
 
181
- ## Server setup
182
-
183
- ### Helpers
184
-
185
- Server-side view helpers render the DOM fragments (templates, storage, containers, etc.) that the client expects. There are corresponding partial templates for each helper, but generally you don't need to change anything except the partial template for `flash_templates`.
186
-
187
- - `flash_global_storage` — a globally placed general-purpose storage element (note: includes `id="flash-storage"`).
188
- - `flash_storage` — a storage element; include it inside the content you return.
189
- - `flash_templates` — display element templates used by the client (`<template>` elements).
190
- - `flash_container` — the container element to place at the target location where users will actually see messages.
191
- - `flash_general_error_messages` — an element that defines messages for HTTP status codes.
192
-
193
- Important: the JavaScript relies on specific DOM contracts defined by the gem (for example, adding `id="flash-storage"` to global storage elements and template IDs in the form `flash-message-template-<type>`). Changing these IDs or selectors will break integration, so if you make changes, you must also update the corresponding JavaScript code.
194
-
195
- ### Minimal layout example
196
-
197
- These are hidden elements so they can be placed anywhere. Typically placing them directly under `<body>` is sufficient:
90
+ Place the "sources" with the helper right after `<body>` in your layout:
198
91
  ```erb
199
- <%= flash_general_error_messages %>
200
- <%= flash_global_storage %>
201
- <%= flash_templates %>
92
+ <body>
93
+ <%= flash_unified_sources %>
94
+ ...
202
95
  ```
203
96
 
204
- Place this where you want Flash messages to be displayed:
97
+ Place the "container" with the helper at the location where you want to display messages:
205
98
  ```erb
206
- <%= flash_container %>
99
+ <div class="notify">
100
+ <%= flash_container %>
101
+ ...
207
102
  ```
208
103
 
209
- Embed the Flash message content in the response content. Since this is a hidden element, it can be placed anywhere within that content. If responding to a Turbo Frame, place it so it renders within the target frame:
210
- ```erb
211
- <%= flash_storage %>
212
- ```
213
-
214
- ### Template customization
104
+ That's it event handlers that monitor page changes will scan storages and render messages into containers.
215
105
 
216
- To customize the appearance and markup of Flash elements, first copy the templates to your host app with:
106
+ As mentioned earlier, no changes are required in your controllers for setting Flash messages.
217
107
 
218
- ```bash
219
- bin/rails generate flash_unified:install --templates
220
- ```
221
-
222
- You can freely customize by editing the copied `app/views/flash_unified/_templates.html.erb`.
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` correspond to Flash "types" (e.g., `:notice`, `:alert`, `:warning`). The client references the type contained in messages to select the corresponding template.
240
-
241
- The client inserts the message string into the `.flash-message-text` element within the template. Otherwise there are no constraints. Feel free to add additional elements (e.g., dismiss buttons) as needed.
242
-
243
- ## JavaScript API and extensions
244
-
245
- The JavaScript is split into a core library and optional helpers. Use only what you need.
246
-
247
- ### Core (`flash_unified`)
248
-
249
- - `renderFlashMessages()` — scan storages, render to containers, and remove storages.
250
- - `appendMessageToStorage(message, type = 'notice')` — append to the global storage.
251
- - `clearFlashMessages(message?)` — remove rendered messages (all or exact-match only).
252
- - `processMessagePayload(payload)` — accept `{ type, message }[]` or `{ messages: [...] }`.
253
- - `installCustomEventListener()` — subscribe to `flash-unified:messages` and process payloads.
254
- - `storageHasMessages()` — utility to detect existing messages in storage.
255
- - `startMutationObserver()` — (optional / experimental) monitor insertion of storages/templates and render them.
256
- - `consumeFlashMessages(keep = false)` — scan all `[data-flash-storage]` elements on the current page and return an array of messages ({ type, message }[]). By default this operation is destructive and removes the storage elements; pass `keep = true` to read without removing.
257
- - `aggregateFlashMessages()` — a thin wrapper over `consumeFlashMessages(true)` that returns the aggregated messages without removing storage elements. Useful for forwarding messages to external notifier libraries.
258
-
259
- To display client-generated Flash messages at arbitrary timing, embed the message first and then perform rendering:
260
-
261
- ```js
262
- import { appendMessageToStorage, renderFlashMessages } from "flash_unified";
263
-
264
- appendMessageToStorage("File size too large.", "notice");
265
- renderFlashMessages();
266
- ```
267
-
268
- To pass server-embedded messages to external libraries like toast instead of rendering them in the page, use `aggregateFlashMessages()` to get messages without destroying storage and pass them to your notification library:
269
-
270
- ```js
271
- import { aggregateFlashMessages } from "flash_unified";
272
-
273
- document.addEventListener('turbo:load', () => {
274
- const msgs = aggregateFlashMessages();
275
- msgs.forEach(({ type, message }) => {
276
- YourNotifier[type](message); // like toastr.info(message)
277
- });
278
- });
279
- ```
280
-
281
- ### Custom event
282
-
283
- When using custom events, run `installCustomEventListener()` during initialization:
284
-
285
- ```js
286
- import { installCustomEventListener } from "flash_unified";
287
- installCustomEventListener();
288
- ```
289
-
290
- Then, at any desired timing, dispatch a `flash-unified:messages` event to the document:
291
-
292
- ```js
293
- // Example: passing an array
294
- document.dispatchEvent(new CustomEvent('flash-unified:messages', {
295
- detail: [
296
- { type: 'notice', message: 'Sent successfully.' },
297
- { type: 'warning', message: 'Expires in one week.' }
298
- ]
299
- }));
300
-
301
- // Example: passing an object
302
- document.dispatchEvent(new CustomEvent('flash-unified:messages', {
303
- detail: { messages: [ { type: 'alert', message: 'Operation was cancelled.' } ] }
304
- }));
305
- ```
306
-
307
- ### Turbo helpers (`flash_unified/turbo_helpers`)
308
-
309
- When using Turbo for partial page updates, you need to perform rendering processing triggered by partial update events. A helper is provided to register those event listeners in bulk:
310
-
311
- - `installTurboRenderListeners()` — register events for rendering according to Turbo lifecycle.
312
- - `installTurboIntegration()` — intended for use by `auto.js`, combines `installTurboRenderListeners()` and `installCustomEventListener()`.
313
-
314
- ```js
315
- import { installTurboRenderListeners } from "flash_unified/turbo_helpers";
316
- installTurboRenderListeners();
317
- ```
318
-
319
- ### Network/HTTP error helpers (`flash_unified/network_helpers`)
320
-
321
- When using network/HTTP error helpers:
322
- ```js
323
- import { notifyNetworkError, notifyHttpError } from "flash_unified/network_helpers";
324
-
325
- notifyNetworkError(); // Set and render generic network error message
326
- notifyHttpError(413); // Set and render HTTP status-specific message
327
- ```
328
-
329
- - `notifyNetworkError()` — uses generic network error text from `#general-error-messages` for rendering.
330
- - `notifyHttpError(status)` — similarly uses HTTP status-specific text for rendering.
331
-
332
- The text used here is written as hidden elements by the server-side view helper `flash_general_error_messages`, and the original text is placed as I18n translation files in `config/locales/http_status_messages.*.yml`.
333
-
334
- To customize the default translation content, copy translation files to your host app with the following command and edit them:
335
-
336
- ```bash
337
- bin/rails generate flash_unified:install --locales
338
- ```
339
-
340
- ### Auto initialization entry (`flash_unified/auto`)
341
-
342
- Importing `flash_unified/auto` automatically runs Turbo integration initialization after DOM ready. The behavior at that time can be controlled with data attributes on `<html>`:
343
-
344
- - `data-flash-unified-auto-init="false"` — disable automatic initialization.
345
- - `data-flash-unified-enable-network-errors="true"` — also enable listeners for network/HTTP errors.
346
-
347
- ```erb
348
- <html data-flash-unified-enable-network-errors="true">
349
- ```
108
+ **For detailed usage** (customization, API reference, Turbo/network helpers, templates, locales, generator usage, and examples), see [`ADVANCED.md`](ADVANCED.md). Examples for using asset pipelines like Sprockets are also provided.
350
109
 
351
110
  ## Development
352
111
 
353
- For detailed development and testing procedures, see [DEVELOPMENT.md](DEVELOPMENT.md) (English) or [DEVELOPMENT.ja.md](DEVELOPMENT.ja.md) (Japanese).
112
+ For detailed development and testing procedures, see [DEVELOPMENT.md](DEVELOPMENT.md).
354
113
 
355
114
  ## Changelog
356
115
 
357
- See all release notes on the [GitHub Releases page](https://github.com/hiroaki/flash-unified/releases).
116
+ See the [GitHub Releases page](https://github.com/hiroaki/flash-unified/releases).
358
117
 
359
118
  ## License
360
119
 
361
- This project is licensed under 0BSD (Zero-Clause BSD). See `LICENSE` for details.
120
+ This project is released under the 0BSD (Zero-Clause BSD) license. For details, see [LICENSE](LICENSE).
@@ -27,6 +27,28 @@ module FlashUnified
27
27
  def flash_general_error_messages
28
28
  render partial: "flash_unified/general_error_messages"
29
29
  end
30
+
31
+ # Wrapper helper that renders the common flash-unified pieces in a single
32
+ # call. This is a non-destructive convenience helper which calls the
33
+ # existing partial-rendering helpers in a sensible default order. Pass a
34
+ # hash to disable parts, e.g. `flash_unified_sources(container: false)`.
35
+ def flash_unified_sources(options = {})
36
+ opts = {
37
+ global_storage: true,
38
+ templates: true,
39
+ general_errors: true,
40
+ storage: true,
41
+ container: false
42
+ }.merge(options.transform_keys(&:to_sym))
43
+
44
+ parts = []
45
+ parts << flash_global_storage if opts[:global_storage]
46
+ parts << flash_templates if opts[:templates]
47
+ parts << flash_general_error_messages if opts[:general_errors]
48
+ parts << flash_storage if opts[:storage]
49
+ parts << flash_container if opts[:container]
50
+
51
+ safe_join(parts, "\n")
52
+ end
30
53
  end
31
54
  end
32
-
@@ -0,0 +1 @@
1
+ var d=null;function x(e){if(e!==null&&typeof e!="function")throw new TypeError("Renderer must be a function or null");d=e}function S(e){if(!e||!e.isConnected)return!1;let r=window.getComputedStyle(e);return r&&r.display!=="none"&&r.visibility!=="hidden"&&parseFloat(r.opacity)>0}function N(e={}){let{primaryOnly:r=!1,visibleOnly:n=!1,sortByPriority:t=!1,firstOnly:s=!1,filter:o}=e,i=Array.from(document.querySelectorAll("[data-flash-message-container]"));return r&&(i=i.filter(l=>l.hasAttribute("data-flash-primary")&&l.getAttribute("data-flash-primary")!=="false")),n&&(i=i.filter(S)),typeof o=="function"&&(i=i.filter(o)),t&&i.sort((l,v)=>{let c=Number(l.getAttribute("data-flash-message-container-priority")),m=Number(v.getAttribute("data-flash-message-container-priority")),A=Number.isFinite(c)?c:Number.POSITIVE_INFINITY,C=Number.isFinite(m)?m:Number.POSITIVE_INFINITY;return A-C}),s?i.length>0?[i[0]]:[]:i}function L(){let e=document.documentElement,r=n=>{let t=e.getAttribute(n);if(t!==null)return t===""||t.toLowerCase()==="true"||t==="1"?!0:!(t.toLowerCase()==="false"||t==="0")};return{primaryOnly:r("data-flash-unified-container-primary-only"),visibleOnly:r("data-flash-unified-container-visible-only"),sortByPriority:r("data-flash-unified-container-sort-by-priority"),firstOnly:r("data-flash-unified-container-first-only")}}function F(e){N(L()).forEach(n=>{e.forEach(({type:t,message:s})=>{s&&n.appendChild(I(t,s))})})}function h(){document.readyState==="loading"?document.addEventListener("DOMContentLoaded",function(){a()},{once:!0}):a()}function a(){let e=g(!1);typeof d=="function"?d(e):F(e)}function g(e=!1){let r=document.querySelectorAll("[data-flash-storage]"),n=[];return r.forEach(t=>{let s=t.querySelector("ul");s&&s.children.length>0&&s.querySelectorAll("li").forEach(o=>{n.push({type:o.dataset.type||"notice",message:o.textContent.trim()})}),e||t.remove()}),n}function O(){return g(!0)}function f(e,r="notice"){let n=document.getElementById("flash-storage");if(!n){console.error('[FlashUnified] #flash-storage not found. Define <div id="flash-storage" style="display:none"></div> in layout.');return}let t=n.querySelector("[data-flash-storage]");t||(t=document.createElement("div"),t.setAttribute("data-flash-storage","true"),t.style.display="none",n.appendChild(t));let s=t.querySelector("ul");s||(s=document.createElement("ul"),t.appendChild(s));let o=document.createElement("li");o.dataset.type=r,o.textContent=e,s.appendChild(o)}function y(){let e=document.documentElement;e.hasAttribute("data-flash-unified-custom-listener")||(e.setAttribute("data-flash-unified-custom-listener","true"),document.addEventListener("flash-unified:messages",function(r){try{M(r.detail)}catch(n){console.error("[FlashUnified] Failed to handle custom payload",n)}}))}function T(e){document.querySelectorAll("[data-flash-message-container]").forEach(r=>{if(typeof e>"u"){r.querySelectorAll("[data-flash-message]")?.forEach(n=>n.remove());return}r.querySelectorAll("[data-flash-message]")?.forEach(n=>{let t=n.querySelector(".flash-message-text");t&&t.textContent.trim()===e&&n.remove()})})}function I(e,r){let n=`flash-message-template-${e}`,t=document.getElementById(n);if(t&&t.content){let s=t.content.firstElementChild;if(!s){console.error(`[FlashUnified] Template #${n} has no root element`);let l=document.createElement("div");return l.setAttribute("role","alert"),l.setAttribute("data-flash-message","true"),l.textContent=r,l}let o=s.cloneNode(!0);o.setAttribute("data-flash-message","true");let i=o.querySelector(".flash-message-text");return i&&(i.textContent=r),o}else{console.error(`[FlashUnified] No template found for type: ${e}`);let s=document.createElement("div");s.setAttribute("role","alert"),s.setAttribute("data-flash-message","true");let o=document.createElement("span");return o.className="flash-message-text",o.textContent=r,s.appendChild(o),s}}function p(){let e=document.querySelectorAll("[data-flash-storage]");for(let r of e){let n=r.querySelector("ul");if(n&&n.children.length>0)return!0}return!1}function M(e){if(!e)return;let r=Array.isArray(e)?e:Array.isArray(e.messages)?e.messages:[];r.length!==0&&(r.forEach(({type:n,message:t})=>{t&&f(String(t),n)}),a())}function R(){let e=document.documentElement;if(e.hasAttribute("data-flash-unified-observer-enabled"))return;e.setAttribute("data-flash-unified-observer-enabled","true"),new MutationObserver(n=>{let t=!1;for(let s of n)s.type==="childList"&&s.addedNodes.forEach(o=>{o instanceof Element&&(o.matches('[data-flash-storage], [data-flash-message-container], template[id^="flash-message-template-"]')&&(t=!0),o.querySelector&&o.querySelector("[data-flash-storage]")&&(t=!0))});t&&a()}).observe(document.body,{childList:!0,subtree:!0})}function u(e){if(p())return;let r=Number(e);if(isNaN(r)||r<0||r>0&&r<400)return;let n;r===0?n="network":n=String(r);let t=document.querySelector("[data-flash-message-container]");if(t&&t.querySelector("[data-flash-message]"))return;let s=document.getElementById("general-error-messages");if(!s){console.error("[FlashUnified] No general error messages element found");return}let o=s.querySelector(`li[data-status="${n}"]`);o?f(o.textContent.trim(),"alert"):console.error(`[FlashUnified] No error message defined for status: ${e}`)}function P(){u(0),a()}function B(e){u(e),a()}function q(){let e=document.documentElement;e.hasAttribute("data-flash-unified-turbo-listeners")||(e.setAttribute("data-flash-unified-turbo-listeners","true"),document.addEventListener("turbo:load",function(){a()}),document.addEventListener("turbo:frame-load",function(){a()}),document.addEventListener("turbo:render",function(){a()}),w())}function w(){let e=new Event("turbo:after-stream-render");document.addEventListener("turbo:before-stream-render",r=>{let n=r.detail.render;r.detail.render=async function(t){await n(t),document.dispatchEvent(e)}}),document.addEventListener("turbo:after-stream-render",function(){a()})}function b(){let e=document.documentElement;e.hasAttribute("data-flash-unified-initialized")||(e.setAttribute("data-flash-unified-initialized","true"),q(),y())}function E(){let e=document.documentElement;e.hasAttribute("data-flash-unified-network-listeners")||(e.setAttribute("data-flash-unified-network-listeners","true"),document.addEventListener("turbo:submit-end",function(r){let n=r.detail.fetchResponse,t;n===void 0?(t=0,console.warn("[FlashUnified] No response received from server. Possible network or proxy error.")):t=n.statusCode,u(t),a()}),document.addEventListener("turbo:fetch-request-error",function(r){u(0),a()}))}if(typeof document<"u"){let e=document.documentElement;if(e.getAttribute("data-flash-unified-auto-init")!=="false"){let n=e.getAttribute("data-flash-unified-enable-network-errors")==="true",t=async()=>{b(),h(),n&&E()};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",t,{once:!0}):t()}}export{O as aggregateFlashMessages,f as appendMessageToStorage,T as clearFlashMessages,g as consumeFlashMessages,N as getFlashMessageContainers,L as getHtmlContainerOptions,y as installCustomEventListener,h as installInitialRenderListener,E as installNetworkErrorListeners,b as installTurboIntegration,q as installTurboRenderListeners,B as notifyHttpError,P as notifyNetworkError,M as processMessagePayload,a as renderFlashMessages,u as resolveAndAppendErrorMessage,x as setFlashMessageRenderer,R as startMutationObserver,p as storageHasMessages};
@@ -6,6 +6,150 @@
6
6
  * @module flash_unified
7
7
  */
8
8
 
9
+ /**
10
+ * Custom renderer function set by user. When null, defaultRenderer is used.
11
+ * @type {Function|null}
12
+ */
13
+ let customRenderer = null;
14
+
15
+ /**
16
+ * Set a custom renderer function to replace the default DOM-based rendering.
17
+ * Pass `null` to reset to default behavior.
18
+ *
19
+ * @param {Function|null} fn - A function that receives an array of message objects: [{type, message}, ...]
20
+ * @returns {void}
21
+ * @throws {TypeError} If fn is neither a function nor null
22
+ *
23
+ * @example
24
+ * import { setFlashMessageRenderer } from 'flash_unified';
25
+ * // Use toastr for notifications
26
+ * setFlashMessageRenderer((messages) => {
27
+ * messages.forEach(({ type, message }) => {
28
+ * toastr[type === 'alert' ? 'error' : 'info'](message);
29
+ * });
30
+ * });
31
+ */
32
+ function setFlashMessageRenderer(fn) {
33
+ if (fn !== null && typeof fn !== 'function') {
34
+ throw new TypeError('Renderer must be a function or null');
35
+ }
36
+ customRenderer = fn;
37
+ }
38
+
39
+ /**
40
+ * Return whether an element is visible (basic heuristic).
41
+ * Considers display/visibility and DOM connection; does not use IntersectionObserver.
42
+ *
43
+ * @param {Element} el
44
+ * @returns {boolean}
45
+ */
46
+ function isVisible(el) {
47
+ if (!el || !el.isConnected) return false;
48
+ const style = window.getComputedStyle(el);
49
+ return style && style.display !== 'none' && style.visibility !== 'hidden' && parseFloat(style.opacity) > 0;
50
+ }
51
+
52
+ /**
53
+ * Collect flash message containers with optional filtering/sorting.
54
+ * By default, returns all elements matching `[data-flash-message-container]`.
55
+ * This function is intended for custom renderers to choose target containers.
56
+ *
57
+ * Options:
58
+ * - primaryOnly?: boolean — If true, only include elements with `data-flash-primary` present or set to "true".
59
+ * - visibleOnly?: boolean — If true, include only elements considered visible.
60
+ * - sortByPriority?: boolean — If true, sort by numeric `data-flash-message-container-priority` ascending (missing treated as Infinity).
61
+ * - firstOnly?: boolean — If true, return at most one element after filtering/sorting.
62
+ * - filter?: (el: Element) => boolean — Additional predicate to include elements.
63
+ *
64
+ * @param {Object} [options]
65
+ * @returns {Element[]} Array of container elements
66
+ */
67
+ function getFlashMessageContainers(options = {}) {
68
+ const {
69
+ primaryOnly = false,
70
+ visibleOnly = false,
71
+ sortByPriority = false,
72
+ firstOnly = false,
73
+ filter
74
+ } = options;
75
+
76
+ // Fixed selector by gem convention
77
+ let list = Array.from(document.querySelectorAll('[data-flash-message-container]'));
78
+
79
+ if (primaryOnly) {
80
+ list = list.filter(el => el.hasAttribute('data-flash-primary') && (el.getAttribute('data-flash-primary') !== 'false'));
81
+ }
82
+ if (visibleOnly) {
83
+ list = list.filter(isVisible);
84
+ }
85
+ if (typeof filter === 'function') {
86
+ list = list.filter(filter);
87
+ }
88
+ if (sortByPriority) {
89
+ list.sort((a, b) => {
90
+ const pa = Number(a.getAttribute('data-flash-message-container-priority'));
91
+ const pb = Number(b.getAttribute('data-flash-message-container-priority'));
92
+ const va = Number.isFinite(pa) ? pa : Number.POSITIVE_INFINITY;
93
+ const vb = Number.isFinite(pb) ? pb : Number.POSITIVE_INFINITY;
94
+ return va - vb;
95
+ });
96
+ }
97
+ if (firstOnly) {
98
+ return list.length > 0 ? [list[0]] : [];
99
+ }
100
+ return list;
101
+ }
102
+
103
+ /**
104
+ * Read default container selection options from <html> data-attributes.
105
+ * Supported attributes (all optional):
106
+ * - data-flash-unified-container-primary-only
107
+ * - data-flash-unified-container-visible-only
108
+ * - data-flash-unified-container-sort-by-priority
109
+ * - data-flash-unified-container-first-only
110
+ *
111
+ * Each attribute accepts:
112
+ * - presence with no value → true
113
+ * - "true"/"1" → true
114
+ * - "false"/"0" → false
115
+ * Missing attribute yields undefined (does not override defaults).
116
+ *
117
+ * @returns {{ primaryOnly?: boolean, visibleOnly?: boolean, sortByPriority?: boolean, firstOnly?: boolean }}
118
+ */
119
+ function getHtmlContainerOptions() {
120
+ const root = document.documentElement;
121
+ const parse = (name) => {
122
+ const val = root.getAttribute(name);
123
+ if (val === null) return undefined;
124
+ if (val === '' || val.toLowerCase() === 'true' || val === '1') return true;
125
+ if (val.toLowerCase() === 'false' || val === '0') return false;
126
+ // Any other non-empty value: treat as true for convenience
127
+ return true;
128
+ };
129
+ return {
130
+ primaryOnly: parse('data-flash-unified-container-primary-only'),
131
+ visibleOnly: parse('data-flash-unified-container-visible-only'),
132
+ sortByPriority: parse('data-flash-unified-container-sort-by-priority'),
133
+ firstOnly: parse('data-flash-unified-container-first-only')
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Default renderer: renders messages into DOM containers using templates.
139
+ *
140
+ * @param {{type: string, message: string}[]} messages - Array of message objects
141
+ * @returns {void}
142
+ */
143
+ function defaultRenderer(messages) {
144
+ // Allow page-wide defaults via <html> data-attributes
145
+ const containers = getFlashMessageContainers(getHtmlContainerOptions());
146
+ containers.forEach(container => {
147
+ messages.forEach(({ type, message }) => {
148
+ if (message) container.appendChild(createFlashMessageNode(type, message));
149
+ });
150
+ });
151
+ }
152
+
9
153
  /**
10
154
  * Install a one-time listener that calls `renderFlashMessages()` on initial page load.
11
155
  *
@@ -26,6 +170,7 @@ function installInitialRenderListener() {
26
170
  /**
27
171
  * Render messages found in storages into message containers.
28
172
  * Delegates message collection to `consumeFlashMessages(false)`, which removes the storage elements.
173
+ * Uses custom renderer if set, otherwise uses default DOM-based rendering.
29
174
  *
30
175
  * @example
31
176
  * import { renderFlashMessages } from 'flash_unified';
@@ -34,14 +179,13 @@ function installInitialRenderListener() {
34
179
  * @returns {void}
35
180
  */
36
181
  function renderFlashMessages() {
37
- const containers = document.querySelectorAll('[data-flash-message-container]');
38
-
39
182
  const messages = consumeFlashMessages(false);
40
- containers.forEach(container => {
41
- messages.forEach(({ type, message }) => {
42
- if (message) container.appendChild(createFlashMessageNode(type, message));
43
- });
44
- });
183
+
184
+ if (typeof customRenderer === 'function') {
185
+ customRenderer(messages);
186
+ } else {
187
+ defaultRenderer(messages);
188
+ }
45
189
  }
46
190
 
47
191
  /**
@@ -282,6 +426,9 @@ function startMutationObserver() {
282
426
 
283
427
  export {
284
428
  renderFlashMessages,
429
+ setFlashMessageRenderer,
430
+ getFlashMessageContainers,
431
+ getHtmlContainerOptions,
285
432
  appendMessageToStorage,
286
433
  clearFlashMessages,
287
434
  processMessagePayload,
@@ -35,7 +35,9 @@ Gem::Specification.new do |spec|
35
35
  "README.md",
36
36
  "CHANGELOG.md",
37
37
  "flash_unified.gemspec"
38
- ].reject { |f| File.directory?(f) }
38
+ ].reject do |f|
39
+ File.directory?(f) || f == "app/javascript/flash_unified/all.entry.js"
40
+ end
39
41
  # Ensure version file is included
40
42
  files << "lib/flash_unified/version.rb" unless files.include?("lib/flash_unified/version.rb")
41
43
  files.uniq
@@ -27,6 +27,7 @@ module FlashUnified
27
27
  flash_unified/auto.js
28
28
  flash_unified/turbo_helpers.js
29
29
  flash_unified/network_helpers.js
30
+ flash_unified/all.bundle.js
30
31
  ]
31
32
  else
32
33
  # Fallback: still add to assets paths if available
@@ -30,76 +30,63 @@ module FlashUnified
30
30
 
31
31
  === FlashUnified installation instructions ===
32
32
 
33
- Importing the JavaScript
34
- - Importmap: add to `config/importmap.rb`:
33
+ Quick start (Importmap)
34
+ 1) Add to `config/importmap.rb`:
35
35
 
36
- pin "flash_unified", to: "flash_unified/flash_unified.js"
37
- pin "flash_unified/auto", to: "flash_unified/auto.js"
38
- pin "flash_unified/turbo_helpers", to: "flash_unified/turbo_helpers.js"
39
- pin "flash_unified/network_helpers", to: "flash_unified/network_helpers.js"
36
+ pin "flash_unified/all", to: "flash_unified/all.bundle.js"
40
37
 
41
- Quick start (auto init):
38
+ 2) Import once in your JavaScript entry point (e.g. `app/javascript/application.js`):
42
39
 
43
- Importing `flash_unified/auto` sets up Turbo listeners and triggers an initial render.
40
+ import "flash_unified/all";
44
41
 
45
- import "flash_unified/auto";
46
- // Configure via <html data-flash-unified-*>:
47
- // data-flash-unified-auto-init="false" (opt-out)
48
- // data-flash-unified-enable-network-errors="true" (install Turbo network error listeners)
42
+ 3) In your layout (inside `<body>`):
49
43
 
44
+ <%= flash_unified_sources %>
45
+ <%= flash_container %>
46
+
47
+ Auto-initialization is enabled by default. Control it via `<html>` attributes:
48
+ - `data-flash-unified-auto-init="false"` to disable all automatic wiring.
49
+ - `data-flash-unified-enable-network-errors="true"` to enable network error listeners.
50
+
51
+ Advanced usage (optional)
50
52
  Manual control:
51
53
 
52
54
  import { renderFlashMessages, appendMessageToStorage } from "flash_unified";
53
55
  import { installTurboRenderListeners } from "flash_unified/turbo_helpers";
54
56
  installTurboRenderListeners();
55
57
 
56
- Network helpers (optional, framework-agnostic):
58
+ Network helpers:
57
59
 
58
60
  import { notifyNetworkError, notifyHttpError } from "flash_unified/network_helpers";
59
61
  // notifyNetworkError();
60
62
  // notifyHttpError(413);
61
63
 
62
- - Asset pipeline (Propshaft / Sprockets): the engine adds its `app/javascript` to the asset paths; add modulepreload links and an inline importmap in your layout's `<head>` and import the bare specifier.
64
+ Propshaft / Sprockets quick start
65
+ Place in `<head>`:
63
66
 
64
- <link rel="modulepreload" href="<%= asset_path('flash_unified/flash_unified.js') %>">
65
- <link rel="modulepreload" href="<%= asset_path('flash_unified/network_helpers.js') %>">
66
- <link rel="modulepreload" href="<%= asset_path('flash_unified/turbo_helpers.js') %>">
67
- <link rel="modulepreload" href="<%= asset_path('flash_unified/auto.js') %>">
67
+ <link rel="modulepreload" href="<%= asset_path('flash_unified/all.bundle.js') %>">
68
68
  <script type="importmap">
69
69
  {
70
70
  "imports": {
71
- "flash_unified": "<%= asset_path('flash_unified/flash_unified.js') %>",
72
- "flash_unified/auto": "<%= asset_path('flash_unified/auto.js') %>",
73
- "flash_unified/turbo_helpers": "<%= asset_path('flash_unified/turbo_helpers.js') %>",
74
- "flash_unified/network_helpers": "<%= asset_path('flash_unified/network_helpers.js') %>"
71
+ "flash_unified/all": "<%= asset_path('flash_unified/all.bundle.js') %>"
75
72
  }
76
73
  }
77
74
  </script>
78
75
  <script type="module">
79
- import "flash_unified/auto";
76
+ import "flash_unified/all";
80
77
  </script>
81
78
 
82
- Remove the `import "flash_unified/auto";` line if you don't want automatic initialization.
83
-
84
- How to place partials in your layout
85
- - The gem's view helpers render engine partials. After running this generator you'll have the partials available under `app/views/flash_unified` and can customize them as needed.
86
-
87
- Recommended layout snippet (inside `<body>`, global helpers):
79
+ (Optionally map `flash_unified` or other modules if you need manual control APIs.)
88
80
 
81
+ Layout helpers
89
82
  <%= flash_general_error_messages %>
90
83
  <%= flash_global_storage %>
91
84
  <%= flash_templates %>
92
-
93
- Place the visible container wherever messages should appear:
94
-
95
85
  <%= flash_container %>
96
-
97
- Embed per-response storage inside content (e.g. Turbo Frame responses):
98
-
99
86
  <%= flash_storage %>
100
87
 
101
88
  Documentation
102
- - For full details and customization guidance, see README.md / README.ja.md in the gem.
89
+ - See README.md / README.ja.md for customization guidance and advanced scenarios.
103
90
 
104
91
  MSG
105
92
 
@@ -1,3 +1,3 @@
1
1
  module FlashUnified
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flash_unified
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - hiroaki
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-10-20 00:00:00.000000000 Z
11
+ date: 2025-10-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -161,6 +161,7 @@ files:
161
161
  - LICENSE
162
162
  - README.md
163
163
  - app/helpers/flash_unified/view_helper.rb
164
+ - app/javascript/flash_unified/all.bundle.js
164
165
  - app/javascript/flash_unified/auto.js
165
166
  - app/javascript/flash_unified/flash_unified.js
166
167
  - app/javascript/flash_unified/network_helpers.js