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 +4 -4
- data/README.md +95 -66
- data/app/javascript/flash_unified/flash_unified.js +120 -133
- data/lib/flash_unified/generators/install/install_generator.rb +107 -37
- data/lib/flash_unified/installer.rb +23 -9
- data/lib/flash_unified/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aea0348851ca5170244e807281bcefa783d5b800f54166546285ec46070d4f6b
|
4
|
+
data.tar.gz: 5d278c86617549ec9dad0e46b7c3167261d4f36dd1188baf81baf6ab9d536263
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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
|
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
|
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.
|
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-
|
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.
|
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
|
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
|
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
|
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.
|
97
|
+
### 1. File placement (only if customization is needed)
|
98
98
|
|
99
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
164
|
+
This ensures Flash messages are rendered when page changes (Turbo events) are detected.
|
172
165
|
|
173
|
-
**Manual (
|
166
|
+
**Manual control (implementing event handlers yourself):**
|
174
167
|
|
175
|
-
|
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
|
-
|
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.
|
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
|
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` —
|
193
|
-
- `flash_container` — the container where
|
194
|
-
- `flash_general_error_messages` —
|
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,
|
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
|
-
|
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
|
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
|
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
|
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`
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
295
|
-
- `installTurboIntegration()` —
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
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
|
-
|
94
|
-
storage.remove();
|
67
|
+
if (!keep) storage.remove();
|
95
68
|
});
|
69
|
+
return messages;
|
70
|
+
}
|
96
71
|
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
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
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
-
//
|
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
|
-
|
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
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
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
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
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
|
7
|
+
desc <<~DESC
|
8
|
+
Copies FlashUnified javascript, view partials, locales and prints setup instructions (Importmap / asset pipeline).
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
def
|
41
|
-
|
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
|
12
|
-
|
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
|
-
|
27
|
-
|
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)
|
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.
|
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-
|
11
|
+
date: 2025-10-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|