flash_unified 0.1.0 → 0.2.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: 7063ee3fc735eb8fcf88ca5562119d948fc68464e2c7be0696b1e7fbc1542a87
4
- data.tar.gz: bdddb50bb03254d0d1ddf07d18dae37d48a4623d02a2f7bed83243e3c6920ca4
3
+ metadata.gz: aea0348851ca5170244e807281bcefa783d5b800f54166546285ec46070d4f6b
4
+ data.tar.gz: 5d278c86617549ec9dad0e46b7c3167261d4f36dd1188baf81baf6ab9d536263
5
5
  SHA512:
6
- metadata.gz: 399bbe669d11e36909ea54d022ea1365c8f69cccad30ae3d2f2d8e1535e927f204dbcc05e32e184553bb718b5496cfec12d72e79c28bc4c4f4d210af1df8777a
7
- data.tar.gz: 133059a7460e3cff955b47a187ce42f27300e87774a2890b174a0597a729d7d2c6ea1b5ccf425fc17853183a3a822c9469978f41894d37005f556fcb8ecb71aa
6
+ metadata.gz: dbcbe847be1227697340e8973f36e9d00cb823cf9acdcf530c7c5e580b3638f1ec746bad376072a905d3bc305b9a2acc26c96d1b5b71a1af0668725c33a9a7f5
7
+ data.tar.gz: a6459a4ae27cbaf5c2078e403d70b42f1cbfe631fedd82252f08522d028c6e726135e56128787a11cfeeddc13a11e5d5c89f4849b16691d9e89c5f4be9c4bddb
data/README.md CHANGED
@@ -2,7 +2,7 @@
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 into the page as data, and a lightweight client-side JavaScript reads those embeddings and renders them onto the page.
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).
6
6
 
7
7
  ## Current status
8
8
 
@@ -10,20 +10,20 @@ This project is considered alpha up to v1.0.0. Public APIs are not stable and ma
10
10
 
11
11
  ## Motivation
12
12
 
13
- Two concerns motivated this work.
13
+ We faced two challenges simultaneously.
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.
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.
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 applications messages are shown outside the frame.
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.
18
18
 
19
19
  ## How it works
20
20
 
21
- The key is that rendering must be done on the JavaScript side. We split responsibilities between server and client into two steps:
21
+ The key insight is that rendering must be done on the JavaScript side. We split responsibilities between server and client into two steps:
22
22
 
23
23
  1. The server embeds the Flash object into the page as hidden DOM elements and returns the rendered page.
24
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
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":
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":
27
27
 
28
28
  ```erb
29
29
  <div data-flash-storage style="display: none;">
@@ -35,11 +35,11 @@ This mechanism is simple; the main requirement is to agree on how to embed data.
35
35
  </div>
36
36
  ```
37
37
 
38
- Because storage is hidden, it can be placed anywhere in the rendered page. For Turbo Frames, place it inside the frame.
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
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.
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
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.
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.
43
43
 
44
44
  ### Controller example
45
45
 
@@ -54,13 +54,13 @@ else
54
54
  end
55
55
  ```
56
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.
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
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.
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
60
 
61
61
  ## Main features
62
62
 
63
- This gem provides rules for embedding data and helper tools for implementation.
63
+ This gem provides the mechanism organized according to defined rules and helper tools to support implementation.
64
64
 
65
65
  Server-side:
66
66
  - View helpers that render DOM fragments expected by the client:
@@ -70,7 +70,7 @@ Server-side:
70
70
  - Localized messages for HTTP status (for advanced usage)
71
71
 
72
72
  Client-side:
73
- - A minimal ES Module in `flash_unified.js`. Configure via Importmap or the asset pipeline.
73
+ - A minimal library in `flash_unified.js` (ES Module). Configure via Importmap or the asset pipeline.
74
74
  - `auto.js` for automatic initialization (optional)
75
75
  - `turbo_helpers.js` for Turbo integration (optional)
76
76
  - `network_helpers.js` for network/HTTP error display (optional)
@@ -94,34 +94,29 @@ bundle install
94
94
 
95
95
  ## Setup
96
96
 
97
- ### 1. Place files
97
+ ### 1. File placement (only if customization is needed)
98
98
 
99
- Run the installer generator:
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
100
 
101
- ```bash
102
- bin/rails generate flash_unified:install
103
- ```
104
-
105
- ### 2. Add JavaScript
101
+ ### 2. JavaScript library setup
106
102
 
107
103
  **Importmap:**
108
104
 
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:
105
+ Pin the JavaScript modules you use to `config/importmap.rb`:
110
106
 
111
107
  ```ruby
112
- pin "flash_unified/auto", to: "flash_unified/auto.js"
113
108
  pin "flash_unified", to: "flash_unified/flash_unified.js"
114
- pin "flash_unified/turbo_helpers", to: "flash_unified/turbo_helpers.js"
115
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"
116
112
  ```
117
113
 
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.
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.
119
115
 
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 optionalpin only the ones you will use.
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.
121
117
 
122
118
  **Asset pipeline (Propshaft / Sprockets):**
123
119
 
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
120
  ```erb
126
121
  <link rel="modulepreload" href="<%= asset_path('flash_unified/flash_unified.js') %>">
127
122
  <link rel="modulepreload" href="<%= asset_path('flash_unified/network_helpers.js') %>">
@@ -146,20 +141,18 @@ This gem's JavaScript is provided as ES modules. Instead of `pin`ning, add the f
146
141
 
147
142
  When using helpers, ensure the initialization that registers event handlers runs on page load.
148
143
 
149
- **Automatic (simple case):**
150
-
151
- Import `auto.js` in your JS entry (e.g. `app/javascript/application.js`):
144
+ **Automatic initialization (simple implementation case):**
152
145
 
146
+ When using `auto.js`, import `auto` in your JavaScript entry point (e.g., `app/javascript/application.js`):
153
147
  ```js
154
148
  import "flash_unified/auto";
155
149
  ```
156
150
 
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):**
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.
160
152
 
161
- When using `turbo_helpers.js`, initialization is not run automatically after import. Call the provided functions:
153
+ **Semi-automatic control (Turbo events are set up automatically):**
162
154
 
155
+ When using `turbo_helpers.js`, initialization is not run automatically after import. Call the methods from the imported module:
163
156
  ```js
164
157
  import { installInitialRenderListener } from "flash_unified";
165
158
  import { installTurboRenderListeners } from "flash_unified/turbo_helpers";
@@ -168,58 +161,65 @@ installTurboRenderListeners();
168
161
  installInitialRenderListener();
169
162
  ```
170
163
 
171
- This ensures Flash messages are rendered when page changes occur (Turbo events).
164
+ This ensures Flash messages are rendered when page changes (Turbo events) are detected.
172
165
 
173
- **Manual (implement your own handlers):**
166
+ **Manual control (implementing event handlers yourself):**
174
167
 
175
- If you implement event registration yourself, at minimum call `renderFlashMessages()` on initial page load. A helper `installInitialRenderListener()` is provided for this purpose:
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:
176
169
 
177
170
  ```js
178
171
  import { installInitialRenderListener } from "flash_unified";
179
172
  installInitialRenderListener();
180
173
  ```
181
174
 
182
- Decide an appropriate timing to call `renderFlashMessages()`—typically within an event handler.
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
+ ```
183
180
 
184
181
  ## Server setup
185
182
 
186
183
  ### Helpers
187
184
 
188
- Server-side view helpers render the DOM fragments the client expects. Most partials do not need changes other than the `flash_templates` partial.
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`.
189
186
 
190
- - `flash_global_storage` — a global storage element (includes `id="flash-storage"`).
187
+ - `flash_global_storage` — a globally placed general-purpose storage element (note: includes `id="flash-storage"`).
191
188
  - `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.
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.
195
192
 
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.
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.
197
194
 
198
195
  ### Minimal layout example
199
196
 
200
- Storage elements can be placed anywhere. Typically they are included near the top of the body:
201
-
197
+ These are hidden elements so they can be placed anywhere. Typically placing them directly under `<body>` is sufficient:
202
198
  ```erb
203
199
  <%= flash_general_error_messages %>
204
200
  <%= flash_global_storage %>
205
201
  <%= flash_templates %>
206
202
  ```
207
203
 
208
- Place the visible container where users should see messages:
209
-
204
+ Place this where you want Flash messages to be displayed:
210
205
  ```erb
211
206
  <%= flash_container %>
212
207
  ```
213
208
 
214
- Embed `flash_storage` inside the response content (for Turbo Frame responses, render it inside the frame):
215
-
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:
216
210
  ```erb
217
211
  <%= flash_storage %>
218
212
  ```
219
213
 
220
214
  ### Template customization
221
215
 
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.
216
+ To customize the appearance and markup of Flash elements, first copy the templates to your host app with:
217
+
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
223
 
224
224
  Here is a partial example:
225
225
 
@@ -236,7 +236,9 @@ Here is a partial example:
236
236
  </template>
237
237
  ```
238
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.
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.
240
242
 
241
243
  ## JavaScript API and extensions
242
244
 
@@ -251,8 +253,10 @@ The JavaScript is split into a core library and optional helpers. Use only what
251
253
  - `installCustomEventListener()` — subscribe to `flash-unified:messages` and process payloads.
252
254
  - `storageHasMessages()` — utility to detect existing messages in storage.
253
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.
254
258
 
255
- Use `appendMessageToStorage()` and `renderFlashMessages()` to produce client-originated Flash messages:
259
+ To display client-generated Flash messages at arbitrary timing, embed the message first and then perform rendering:
256
260
 
257
261
  ```js
258
262
  import { appendMessageToStorage, renderFlashMessages } from "flash_unified";
@@ -261,16 +265,29 @@ appendMessageToStorage("File size too large.", "notice");
261
265
  renderFlashMessages();
262
266
  ```
263
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
+
264
281
  ### Custom event
265
282
 
266
- To use custom events, run `installCustomEventListener()` during initialization:
283
+ When using custom events, run `installCustomEventListener()` during initialization:
267
284
 
268
285
  ```js
269
286
  import { installCustomEventListener } from "flash_unified";
270
287
  installCustomEventListener();
271
288
  ```
272
289
 
273
- Then, at any desired timing, dispatch a `flash-unified:messages` event on the document:
290
+ Then, at any desired timing, dispatch a `flash-unified:messages` event to the document:
274
291
 
275
292
  ```js
276
293
  // Example: passing an array
@@ -289,35 +306,43 @@ document.dispatchEvent(new CustomEvent('flash-unified:messages', {
289
306
 
290
307
  ### Turbo helpers (`flash_unified/turbo_helpers`)
291
308
 
292
- When using Turbo, partial updates require rendering at the appropriate events. Use the helper to register these listeners:
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:
293
310
 
294
- - `installTurboRenderListeners()` — register Turbo lifecycle listeners.
295
- - `installTurboIntegration()` — a convenience that combines `installTurboRenderListeners()` and `installCustomEventListener()` (used by `auto.js`).
311
+ - `installTurboRenderListeners()` — register events for rendering according to Turbo lifecycle.
312
+ - `installTurboIntegration()` — intended for use by `auto.js`, combines `installTurboRenderListeners()` and `installCustomEventListener()`.
296
313
 
297
314
  ```js
298
315
  import { installTurboRenderListeners } from "flash_unified/turbo_helpers";
299
316
  installTurboRenderListeners();
300
317
  ```
301
318
 
302
- ### Network/HTTP helpers (`flash_unified/network_helpers`)
303
-
304
- Use these helpers to set messages for network/HTTP errors:
319
+ ### Network/HTTP error helpers (`flash_unified/network_helpers`)
305
320
 
321
+ When using network/HTTP error helpers:
306
322
  ```js
307
323
  import { notifyNetworkError, notifyHttpError } from "flash_unified/network_helpers";
308
324
 
309
- notifyNetworkError();
310
- notifyHttpError(413);
325
+ notifyNetworkError(); // Set and render generic network error message
326
+ notifyHttpError(413); // Set and render HTTP status-specific message
311
327
  ```
312
328
 
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.
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
+ ```
314
339
 
315
340
  ### Auto initialization entry (`flash_unified/auto`)
316
341
 
317
- Importing `flash_unified/auto` runs initialization after DOM ready. The behavior can be controlled with data attributes on `<html>`:
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>`:
318
343
 
319
344
  - `data-flash-unified-auto-init="false"` — disable automatic initialization.
320
- - `data-flash-unified-enable-network-errors="true"` — also enable network/HTTP error listeners.
345
+ - `data-flash-unified-enable-network-errors="true"` — also enable listeners for network/HTTP errors.
321
346
 
322
347
  ```erb
323
348
  <html data-flash-unified-enable-network-errors="true">
@@ -325,7 +350,11 @@ Importing `flash_unified/auto` runs initialization after DOM ready. The behavior
325
350
 
326
351
  ## Development
327
352
 
328
- See [DEVELOPMENT.md](DEVELOPMENT.md) or [DEVELOPMENT.ja.md](DEVELOPMENT.ja.md) for development and testing instructions.
353
+ For detailed development and testing procedures, see [DEVELOPMENT.md](DEVELOPMENT.md) (English) or [DEVELOPMENT.ja.md](DEVELOPMENT.ja.md) (Japanese).
354
+
355
+ ## Changelog
356
+
357
+ See all release notes on the [GitHub Releases page](https://github.com/hiroaki/flash-unified/releases).
329
358
 
330
359
  ## License
331
360
 
@@ -1,66 +1,20 @@
1
- /*
2
- Flash UnifiedMinimal Core API
1
+ /**
2
+ * flash_unified — Core utilities for reading and rendering embedded flash messages.
3
+ *
4
+ * See README.md for full usage examples and integration notes.
5
+ *
6
+ * @module flash_unified
7
+ */
3
8
 
4
- Purpose
5
- - Provide core utilities for flash message rendering.
6
- - Users control when and how to trigger rendering via their own event handlers.
7
-
8
- Core API:
9
- - renderFlashMessages(): render all messages from storage into containers
10
- - appendMessageToStorage(message, type): add a message to hidden storage
11
- - clearFlashMessages(message?): clear displayed messages
12
- - processMessagePayload(payload): handle message arrays from custom events
13
- - startMutationObserver(): watch for dynamically inserted storage/templates
14
-
15
- Required DOM (no Rails helpers needed)
16
- 1) Display container (required)
17
- <div data-flash-message-container></div>
18
-
19
- 2) Hidden storage (optional; any number; removed after render)
20
- <div data-flash-storage style="display:none;">
21
- <ul>
22
- <li data-type="notice">Saved</li>
23
- <li data-type="alert">Oops</li>
24
- </ul>
25
- </div>
26
-
27
- 3) Message templates, one per type (root should have role="alert" and include
28
- a .flash-message-text node for insertion)
29
- <template id="flash-message-template-notice">
30
- <div class="flash-notice" role="alert"><span class="flash-message-text"></span></div>
31
- </template>
32
- <template id="flash-message-template-alert">
33
- <div class="flash-alert" role="alert"><span class="flash-message-text"></span></div>
34
- </template>
35
-
36
- 4) Global storage (required by appendMessageToStorage)
37
- <div id="flash-storage" style="display:none;"></div>
38
-
39
- Usage Examples:
40
- // Manual control with Stimulus
41
- import { renderFlashMessages, appendMessageToStorage } from "flash_unified";
42
- export default class extends Controller {
43
- connect() { renderFlashMessages(); }
44
- error() {
45
- appendMessageToStorage('Error occurred', 'alert');
46
- renderFlashMessages();
47
- }
48
- }
49
-
50
- // Custom event listener
51
- import { renderFlashMessages } from "flash_unified";
52
- document.addEventListener('turbo:load', renderFlashMessages);
53
- document.addEventListener('my-app:show-message', (event) => {
54
- appendMessageToStorage(event.detail.message, event.detail.type);
55
- renderFlashMessages();
56
- });
57
- */
58
-
59
- /* 初回描画リスナーをセットします。
60
- DOMContentLoaded 時に renderFlashMessages() を一度だけ呼びます。
61
- ---
62
- Install a listener to render flash messages on DOMContentLoaded (once).
63
- */
9
+ /**
10
+ * Install a one-time listener that calls `renderFlashMessages()` on initial page load.
11
+ *
12
+ * @example
13
+ * import { installInitialRenderListener } from 'flash_unified';
14
+ * installInitialRenderListener();
15
+ *
16
+ * @returns {void}
17
+ */
64
18
  function installInitialRenderListener() {
65
19
  if (document.readyState === 'loading') {
66
20
  document.addEventListener('DOMContentLoaded', function() { renderFlashMessages(); }, { once: true });
@@ -69,19 +23,39 @@ function installInitialRenderListener() {
69
23
  }
70
24
  }
71
25
 
72
- /* ストレージにあるメッセージを表示させます。
73
- すべての [data-flash-storage] 内のリスト項目を集約し、各項目ごとにテンプレートを用いて
74
- フラッシュメッセージ要素を生成し、[data-flash-message-container] に追加します。
75
- 処理後は各ストレージ要素を取り除きます。
76
- ---
77
- Render messages found in all [data-flash-storage] lists, create elements via templates,
78
- and append them into [data-flash-message-container]. Each storage is removed after processing.
79
- */
26
+ /**
27
+ * Render messages found in storages into message containers.
28
+ * Delegates message collection to `consumeFlashMessages(false)`, which removes the storage elements.
29
+ *
30
+ * @example
31
+ * import { renderFlashMessages } from 'flash_unified';
32
+ * renderFlashMessages();
33
+ *
34
+ * @returns {void}
35
+ */
80
36
  function renderFlashMessages() {
81
- const storages = document.querySelectorAll('[data-flash-storage]');
82
37
  const containers = document.querySelectorAll('[data-flash-message-container]');
83
38
 
84
- // Aggregated messages list
39
+ const messages = consumeFlashMessages(false);
40
+ containers.forEach(container => {
41
+ messages.forEach(({ type, message }) => {
42
+ if (message) container.appendChild(createFlashMessageNode(type, message));
43
+ });
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Collect messages from all `[data-flash-storage]` elements.
49
+ * By default, removes each storage after reading; pass `keep = true` to preserve them.
50
+ *
51
+ * @param {boolean} [keep=false] - When true, do not remove storage elements after reading.
52
+ * @returns {{type: string, message: string}[]} Array of message objects.
53
+ *
54
+ * @example
55
+ * const msgs = consumeFlashMessages(true);
56
+ */
57
+ function consumeFlashMessages(keep = false) {
58
+ const storages = document.querySelectorAll('[data-flash-storage]');
85
59
  const messages = [];
86
60
  storages.forEach(storage => {
87
61
  const ul = storage.querySelector('ul');
@@ -90,33 +64,38 @@ function renderFlashMessages() {
90
64
  messages.push({ type: li.dataset.type || 'notice', message: li.textContent.trim() });
91
65
  });
92
66
  }
93
- // Remove storage after consuming
94
- storage.remove();
67
+ if (!keep) storage.remove();
95
68
  });
69
+ return messages;
70
+ }
96
71
 
97
- containers.forEach(container => {
98
- messages.forEach(({ type, message }) => {
99
- if (message) container.appendChild(createFlashMessageNode(type, message));
100
- });
101
- });
72
+ /**
73
+ * Return messages without removing the storage elements.
74
+ * Thin wrapper over `consumeFlashMessages(true)`.
75
+ *
76
+ * @returns {{type: string, message: string}[]}
77
+ *
78
+ * @example
79
+ * const msgs = aggregateFlashMessages();
80
+ */
81
+ function aggregateFlashMessages() {
82
+ return consumeFlashMessages(true);
102
83
  }
103
84
 
104
- /* フラッシュ・メッセージ項目として message をデータとして埋め込みます。
105
- 埋め込まれた項目は renderFlashMessages を呼び出すことによって表示されます。
106
- ---
107
- Append a message item into the hidden storage.
108
- Call renderFlashMessages() to display it.
109
- */
85
+ /**
86
+ * Append a message to the global storage element (`#flash-storage`).
87
+ *
88
+ * @param {string} message - The message text to append.
89
+ * @param {string} [type='notice'] - The flash type (e.g. 'notice', 'alert').
90
+ * @returns {void}
91
+ *
92
+ * @example
93
+ * appendMessageToStorage('Saved', 'notice');
94
+ */
110
95
  function appendMessageToStorage(message, type = 'notice') {
111
96
  const storageContainer = document.getElementById("flash-storage");
112
97
  if (!storageContainer) {
113
98
  console.error('[FlashUnified] #flash-storage not found. Define <div id="flash-storage" style="display:none"></div> in layout.');
114
- // TODO: あるいは自動生成して document.body.appendChild しますか?
115
- // ユーザの目に見えない部分で要素が増えることを避けたいと考え、警告に留めています。
116
- // 下で storage を生成する部分は、ユーザが設定するコンテナの中なので問題ありません。
117
- // ---
118
- // Alternatively we could auto-create it on document.body, but we avoid hidden side-effects.
119
- // Creating the inner [data-flash-storage] below is safe since it's inside the user-provided container.
120
99
  return;
121
100
  }
122
101
 
@@ -140,12 +119,17 @@ function appendMessageToStorage(message, type = 'notice') {
140
119
  ul.appendChild(li);
141
120
  }
142
121
 
143
- /* カスタムイベントリスナーを設定します(オプション)。
144
- サーバーや他のJSからのカスタムイベントを受け取ります。
145
- ---
146
- Setup custom event listener for programmatic message dispatch.
147
- Listen for "flash-unified:messages" events from server or other JS.
148
- */
122
+ /**
123
+ * Install a listener for `flash-unified:messages` CustomEvent and process its payload.
124
+ * The event's `detail` should be either an array of message objects or an object with a `messages` array.
125
+ *
126
+ * @example
127
+ * document.dispatchEvent(new CustomEvent('flash-unified:messages', {
128
+ * detail: [{ type: 'notice', message: 'Hi' }]
129
+ * }));
130
+ *
131
+ * @returns {void}
132
+ */
149
133
  function installCustomEventListener() {
150
134
  const root = document.documentElement;
151
135
  if (root.hasAttribute('data-flash-unified-custom-listener')) return; // idempotent
@@ -160,22 +144,20 @@ function installCustomEventListener() {
160
144
  });
161
145
  }
162
146
 
163
- /* フラッシュ・メッセージの表示をクリアします。
164
- message が指定されている場合は、そのメッセージを含んだフラッシュ・メッセージのみを削除します。
165
- 省略された場合はすべてのフラッシュ・メッセージが対象です。
166
- ---
167
- Clear flash messages. If message is provided, remove only matching ones;
168
- otherwise remove all flash message nodes in the containers.
169
- */
147
+ /**
148
+ * Clear rendered flash messages from message containers.
149
+ * If `message` is provided, only remove elements whose text exactly matches it.
150
+ *
151
+ * @param {string} [message] - Exact message text to remove (optional).
152
+ * @returns {void}
153
+ */
170
154
  function clearFlashMessages(message) {
171
155
  document.querySelectorAll('[data-flash-message-container]').forEach(container => {
172
- // メッセージ指定なし: メッセージ要素のみ全削除(コンテナ内の他要素は残す)
173
156
  if (typeof message === 'undefined') {
174
157
  container.querySelectorAll('[data-flash-message]')?.forEach(n => n.remove());
175
158
  return;
176
159
  }
177
160
 
178
- // 指定メッセージに一致する要素だけ削除
179
161
  container.querySelectorAll('[data-flash-message]')?.forEach(n => {
180
162
  const text = n.querySelector('.flash-message-text');
181
163
  if (text && text.textContent.trim() === message) n.remove();
@@ -183,15 +165,14 @@ function clearFlashMessages(message) {
183
165
  });
184
166
  }
185
167
 
186
- // --- ユーティリティ関数 / Utility functions ---
187
-
188
- /* テンプレートからフラッシュ・メッセージ要素を生成します。
189
- type に対応する <template id="flash-message-template-<type>"> を利用し、
190
- .flash-message-text に文言を挿入します。テンプレートが無い場合は簡易的な要素を生成します。
191
- ---
192
- Create a flash message DOM node using <template id="flash-message-template-<type>">.
193
- Inserts the message into .flash-message-text. Falls back to a minimal element when template is missing.
194
- */
168
+ /**
169
+ * Create a DOM node for a flash message using the `flash-message-template-<type>` template.
170
+ * Falls back to a minimal element when the template is missing.
171
+ *
172
+ * @param {string} type
173
+ * @param {string} message
174
+ * @returns {Element}
175
+ */
195
176
  function createFlashMessageNode(type, message) {
196
177
  const templateId = `flash-message-template-${type}`;
197
178
  const template = document.getElementById(templateId);
@@ -212,7 +193,7 @@ function createFlashMessageNode(type, message) {
212
193
  return root;
213
194
  } else {
214
195
  console.error(`[FlashUnified] No template found for type: ${type}`);
215
- // テンプレートがない場合は生成 / Fallback element when template is missing
196
+ // Fallback element when template is missing
216
197
  const node = document.createElement('div');
217
198
  node.setAttribute('role', 'alert');
218
199
  node.setAttribute('data-flash-message', 'true');
@@ -224,10 +205,11 @@ function createFlashMessageNode(type, message) {
224
205
  }
225
206
  }
226
207
 
227
- /* 何らかのストレージにメッセージが存在するかを判定します。
228
- ---
229
- Return true if any [data-flash-storage] contains at least one <li> item.
230
- */
208
+ /**
209
+ * Return true if any `[data-flash-storage]` contains at least one `<li>`.
210
+ *
211
+ * @returns {boolean}
212
+ */
231
213
  function storageHasMessages() {
232
214
  const storages = document.querySelectorAll('[data-flash-storage]');
233
215
  for (const storage of storages) {
@@ -239,11 +221,15 @@ function storageHasMessages() {
239
221
  return false;
240
222
  }
241
223
 
242
- /* メッセージの配列(または { messages: [...] })を受け取り、ストレージに追加して描画します。
243
- ---
244
- Handle a payload of messages and render them.
245
- Accepts either an array of { type, message } or an object { messages: [...] }.
246
- */
224
+ /**
225
+ * Accept either:
226
+ * - an array of message objects [{ type, message }, ...], or
227
+ * - an object { messages: [...] } where messages is such an array.
228
+ * Append each message to storage and trigger rendering.
229
+ *
230
+ * @param {Array|Object} payload
231
+ * @returns {void}
232
+ */
247
233
  function processMessagePayload(payload) {
248
234
  if (!payload) return;
249
235
  const list = Array.isArray(payload)
@@ -257,13 +243,12 @@ function processMessagePayload(payload) {
257
243
  renderFlashMessages();
258
244
  }
259
245
 
260
- /* 任意: MutationObserver を有効化し、動的に挿入されたストレージ/テンプレートを検出して描画します。
261
- サーバーレスポンス側でカスタムイベントを発火できない場合の代替となります。
262
- ---
263
- Optional: Enable a MutationObserver that watches for dynamically inserted
264
- flash storage or templates and triggers rendering. Useful when you cannot
265
- or do not want to dispatch a custom event from server responses.
266
- */
246
+ /**
247
+ * Enable a MutationObserver that watches for dynamically inserted storages, templates,
248
+ * or message containers and triggers rendering. Useful when server responses cannot dispatch events.
249
+ *
250
+ * @returns {void}
251
+ */
267
252
  function startMutationObserver() {
268
253
  const root = document.documentElement;
269
254
  if (root.hasAttribute('data-flash-unified-observer-enabled')) return;
@@ -303,5 +288,7 @@ export {
303
288
  startMutationObserver,
304
289
  installCustomEventListener,
305
290
  installInitialRenderListener,
306
- storageHasMessages
291
+ storageHasMessages,
292
+ consumeFlashMessages,
293
+ aggregateFlashMessages
307
294
  };
@@ -4,45 +4,25 @@ require "rails/generators/base"
4
4
  module FlashUnified
5
5
  module Generators
6
6
  class InstallGenerator < Rails::Generators::Base
7
- desc "Copies FlashUnified javascript, view partials, locales and prints setup instructions (Importmap / asset pipeline)."
7
+ desc <<~DESC
8
+ Copies FlashUnified javascript, view partials, locales and prints setup instructions (Importmap / asset pipeline).
8
9
 
9
- class_option :force, type: :boolean, default: false, desc: "Overwrite existing files"
10
-
11
- # Print a clear start message so users see the generator run boundary.
12
- # Using `say_status :run` follows the Rails generator convention (colored label).
13
- # Print a start message once per generator run. An optional `note` will be
14
- # appended to the message to provide context (e.g. "copy javascript").
15
- def start_message(note = nil)
16
- return if @flash_unified_started
17
- message = "Installing FlashUnified"
18
- message += " — #{note}" if note
19
- say_status :run, message, :blue
20
- @flash_unified_started = true
21
- end
22
-
23
- def copy_javascript
24
- start_message("copy javascript")
25
- installer = FlashUnified::Installer.new(source_root: File.expand_path('../../../../', __dir__), target_root: Dir.pwd, force: options[:force])
26
- installer.copy_javascript do |status, path|
27
- say_status status, display_path(path)
28
- end
29
- end
10
+ By default (no options), only templates and locales are copied.
11
+ Use --all to copy all groups (javascript, templates, views, locales, helpers).
12
+ Use --templates or --locales for fine-grained control.
13
+ DESC
30
14
 
31
- # View partials are copied into your host app so you can customize them.
32
- def copy_view_partials
33
- start_message("copy view partials")
34
- installer = FlashUnified::Installer.new(source_root: File.expand_path('../../../../', __dir__), target_root: Dir.pwd, force: options[:force])
35
- installer.copy_views do |status, path|
36
- say_status status, display_path(path)
37
- end
38
- end
39
-
40
- def copy_locales
41
- start_message("copy locales")
42
- installer = FlashUnified::Installer.new(source_root: File.expand_path('../../../../', __dir__), target_root: Dir.pwd, force: options[:force])
43
- installer.copy_locales do |status, path|
44
- say_status status, display_path(path)
45
- end
15
+ class_option :force, type: :boolean, default: false, desc: "Overwrite existing files"
16
+ class_option :all, type: :boolean, default: false, desc: "Install all files"
17
+ class_option :templates, type: :boolean, default: false, desc: "Install only _templates.html.erb partial"
18
+ class_option :views, type: :boolean, default: false, desc: "Install all view partials (views/flash_unified/*)"
19
+ class_option :javascript, type: :boolean, default: false, desc: "Install only JavaScript files"
20
+ class_option :locales, type: :boolean, default: false, desc: "Install only locale files"
21
+ class_option :helpers, type: :boolean, default: false, desc: "Install only helper files"
22
+
23
+ # Rails generator entrypoint (only task that performs copying)
24
+ def install
25
+ handle_installation
46
26
  end
47
27
 
48
28
  def show_importmap_instructions
@@ -128,6 +108,96 @@ module FlashUnified
128
108
 
129
109
  private
130
110
 
111
+ no_tasks do
112
+ # Print a clear start message so users see the generator run boundary.
113
+ # Using `say_status :run` follows the Rails generator convention (colored label).
114
+ # Print a start message once per generator run. An optional `note` will be
115
+ # appended to the message to provide context (e.g. "copy javascript").
116
+ def start_message(note = nil)
117
+ return if @flash_unified_started
118
+ message = "Installing FlashUnified"
119
+ message += " — #{note}" if note
120
+ say_status :run, message, :blue
121
+ @flash_unified_started = true
122
+ end
123
+
124
+ # Resolve gem root robustly by walking up until we find the gemspec
125
+ def gem_root
126
+ return @gem_root if defined?(@gem_root)
127
+ path = Pathname.new(__dir__)
128
+ path.ascend do |p|
129
+ if (p + 'flash_unified.gemspec').exist?
130
+ @gem_root = p
131
+ break
132
+ end
133
+ end
134
+ @gem_root ||= Pathname.new(File.expand_path('../../../../', __dir__))
135
+ end
136
+
137
+ def installer
138
+ @installer ||= FlashUnified::Installer.new(source_root: gem_root.to_s, target_root: destination_root, force: options[:force])
139
+ end
140
+
141
+ def handle_installation
142
+ # Determine which groups to install
143
+ groups = []
144
+ groups << :javascript if options[:javascript]
145
+ groups << :templates if options[:templates]
146
+ groups << :views if options[:views]
147
+ groups << :locales if options[:locales]
148
+ groups << :helpers if options[:helpers]
149
+ # If --all was provided, install every group. Otherwise, when no
150
+ # explicit groups are requested, install a sensible minimal set:
151
+ # templates + locales. This avoids unexpectedly copying JavaScript,
152
+ # helpers or full view partial sets into the host app when the user
153
+ # runs the generator without options.
154
+ if options[:all]
155
+ groups = [:javascript, :templates, :views, :locales, :helpers]
156
+ elsif groups.empty?
157
+ groups = [:templates, :locales]
158
+ end
159
+
160
+ groups.each do |group|
161
+ send("copy_#{group}")
162
+ end
163
+ end
164
+
165
+ def copy_javascript
166
+ start_message("copy javascript")
167
+ installer.copy_javascript do |status, path|
168
+ say_status status, display_path(path)
169
+ end
170
+ end
171
+
172
+ def copy_templates
173
+ start_message("copy _templates.html.erb")
174
+ installer.copy_templates do |status, path|
175
+ say_status status, display_path(path)
176
+ end
177
+ end
178
+
179
+ def copy_views
180
+ start_message("copy all view partials")
181
+ installer.copy_views do |status, path|
182
+ say_status status, display_path(path)
183
+ end
184
+ end
185
+
186
+ def copy_locales
187
+ start_message("copy locales")
188
+ installer.copy_locales do |status, path|
189
+ say_status status, display_path(path)
190
+ end
191
+ end
192
+
193
+ def copy_helpers
194
+ start_message("copy helpers")
195
+ installer.copy_helpers do |status, path|
196
+ say_status status, display_path(path)
197
+ end
198
+ end
199
+ end
200
+
131
201
  # Return a user-friendly path for display in generator output. If the
132
202
  # provided path is under the current working directory (Rails root), show
133
203
  # it as a relative path; otherwise show the original path.
@@ -8,8 +8,9 @@ module FlashUnified
8
8
  class Installer
9
9
  attr_reader :source_root, :target_root, :force
10
10
 
11
- def initialize(source_root:, target_root:, force: false)
12
- @source_root = Pathname.new(source_root)
11
+ def initialize(source_root: nil, target_root:, force: false)
12
+ # When source_root is not given, assume the gem root two levels up from this file (lib/flash_unified)
13
+ @source_root = Pathname.new(source_root || File.expand_path('../..', __dir__))
13
14
  @target_root = Pathname.new(target_root)
14
15
  @force = !!force
15
16
  end
@@ -20,16 +21,20 @@ module FlashUnified
20
21
  copy_tree(src, dst, &block)
21
22
  end
22
23
 
24
+ # Copy only _templates.html.erb
25
+ def copy_templates(&block)
26
+ src_dir = source_root.join('app', 'views', 'flash_unified')
27
+ dst_dir = target_root.join('app', 'views', 'flash_unified')
28
+ files = %w[_templates.html.erb]
29
+ copy_files(files, src_dir, dst_dir, &block)
30
+ end
31
+
32
+ # Copy all files in views/flash_unified/ directory
23
33
  def copy_views(&block)
24
34
  src_dir = source_root.join('app', 'views', 'flash_unified')
25
35
  dst_dir = target_root.join('app', 'views', 'flash_unified')
26
- files = %w[
27
- _templates.html.erb
28
- _storage.html.erb
29
- _global_storage.html.erb
30
- _container.html.erb
31
- _general_error_messages.html.erb
32
- ]
36
+ raise "source missing: #{src_dir}" unless src_dir.directory?
37
+ files = Dir.children(src_dir).select { |f| File.file?(src_dir.join(f)) }
33
38
  copy_files(files, src_dir, dst_dir, &block)
34
39
  end
35
40
 
@@ -41,6 +46,15 @@ module FlashUnified
41
46
  copy_files(files, src_dir, dst_dir, &block)
42
47
  end
43
48
 
49
+ def copy_helpers(&block)
50
+ src_dir = source_root.join('app', 'helpers', 'flash_unified')
51
+ dst_dir = target_root.join('app', 'helpers', 'flash_unified')
52
+ files = %w[
53
+ view_helper.rb
54
+ ]
55
+ copy_files(files, src_dir, dst_dir, &block)
56
+ end
57
+
44
58
  private
45
59
 
46
60
  def copy_tree(src, dst, &block)
@@ -1,3 +1,3 @@
1
1
  module FlashUnified
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.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.1.0
4
+ version: 0.2.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-13 00:00:00.000000000 Z
11
+ date: 2025-10-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails