flash_unified 0.3.0 → 0.4.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 +33 -10
- data/app/helpers/flash_unified/view_helper.rb +13 -0
- data/app/javascript/flash_unified/all.bundle.js +1 -1
- data/app/javascript/flash_unified/flash_unified.js +23 -0
- data/app/views/flash_unified/_storage.html.erb +1 -1
- 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: ab78476991976297386a29f8497c4a6cdfa3653de8ee8cb5d1ab66c9b0d6f552
|
|
4
|
+
data.tar.gz: f1033c6796f66de87d6c4d274a71e0d107ff4010584b09ca691e7ec7bcb987f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f1e25694b830f1ab50023cc811a7e24a898c6a6f15e5b4acbde4eb19f19e0a2c77e35f3ccdb2c8ca360bfcc91dc5865eae0e69aea9e130509a699f8d8be29c6c
|
|
7
|
+
data.tar.gz: b29cdfe43b56d2baf9d073a48bcd4453dc63b2b8aa69f3cc6cbe4b42c8638ec0d320cc56a9f83a897aeb480b53393e0ab56ecaf91aef990015400e8da220fbd6
|
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 as data in the page, and a lightweight client-side JavaScript library reads those
|
|
5
|
+
Server-side view helpers embed Flash messages as data in the page, and a lightweight client-side JavaScript library reads those storage elements and renders messages into visible containers using templates.
|
|
6
6
|
|
|
7
7
|
## Current status
|
|
8
8
|
|
|
@@ -23,7 +23,7 @@ The key point to solving these challenges is that rendering needs to be performe
|
|
|
23
23
|
1. The server embeds the Flash object as a hidden DOM element within the page, renders the page, and returns it.
|
|
24
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.
|
|
25
25
|
|
|
26
|
-
The mechanism is simple
|
|
26
|
+
The mechanism is simple; to implement it, we only need to define the rules for embedding. In this gem, we define the embedded DOM structure below and refer to it as a "storage element":
|
|
27
27
|
```erb
|
|
28
28
|
<div data-flash-storage style="display: none;">
|
|
29
29
|
<ul>
|
|
@@ -34,9 +34,9 @@ The mechanism is simple, and to implement it, we only need to decide on the rule
|
|
|
34
34
|
</div>
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Since storage is
|
|
37
|
+
Since a storage element is hidden, it can be placed anywhere in the page rendered by the server. For Turbo Frames, place a storage element inside the frame.
|
|
38
38
|
|
|
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.
|
|
39
|
+
The "container" where Flash messages are displayed and the "templates" for formatting can be placed anywhere, regardless of the storage element. This means that even with Turbo Frames, it works with Flash rendering areas placed outside the frame.
|
|
40
40
|
|
|
41
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.
|
|
42
42
|
|
|
@@ -87,29 +87,52 @@ import "flash_unified/all";
|
|
|
87
87
|
|
|
88
88
|
### 3. Server-side setup
|
|
89
89
|
|
|
90
|
-
Place the "sources"
|
|
90
|
+
Place the "sources" helper right after `<body>` in your layout. This emits hidden elements and therefore does not affect your layout:
|
|
91
91
|
```erb
|
|
92
92
|
<body>
|
|
93
93
|
<%= flash_unified_sources %>
|
|
94
94
|
...
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
-
Place the "container"
|
|
97
|
+
Place the "container" helper at the location where you want to display messages:
|
|
98
98
|
```erb
|
|
99
99
|
<div class="notify">
|
|
100
100
|
<%= flash_container %>
|
|
101
101
|
...
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
When using Turbo, you need to place storage elements (hidden elements) within the content that updates.
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
**Turbo Frame**
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
Place a storage element inside the frame:
|
|
109
|
+
```erb
|
|
110
|
+
<turbo-frame id="foo">
|
|
111
|
+
<%= flash_storage %>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Turbo Stream**
|
|
115
|
+
|
|
116
|
+
Add a stream to append a storage element to the global storage:
|
|
117
|
+
```erb
|
|
118
|
+
<%= flash_turbo_stream %>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
Or in a controller:
|
|
123
|
+
```ruby
|
|
124
|
+
render turbo_stream: helpers.flash_turbo_stream
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
That's it. Event handlers that monitor page changes will scan storage elements and render messages into containers.
|
|
128
|
+
|
|
129
|
+
## Detailed usage
|
|
130
|
+
|
|
131
|
+
For customization options, API references, Turbo/network helpers, templates, locales, generators, and more, see [`ADVANCED.md`](ADVANCED.md). Examples for using asset pipelines like Sprockets are also provided.
|
|
109
132
|
|
|
110
133
|
## Development
|
|
111
134
|
|
|
112
|
-
For detailed development and testing procedures, see [DEVELOPMENT.md](DEVELOPMENT.md).
|
|
135
|
+
For detailed development and testing procedures, see [`DEVELOPMENT.md`](DEVELOPMENT.md).
|
|
113
136
|
|
|
114
137
|
## Changelog
|
|
115
138
|
|
|
@@ -28,6 +28,19 @@ module FlashUnified
|
|
|
28
28
|
render partial: "flash_unified/general_error_messages"
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Turbo Stream helper that appends flash storage to the global storage element.
|
|
32
|
+
# This is a convenience helper for the common pattern of appending flash messages
|
|
33
|
+
# via Turbo Stream to the global flash-storage container.
|
|
34
|
+
#
|
|
35
|
+
# Usage in views:
|
|
36
|
+
# <%= flash_turbo_stream %>
|
|
37
|
+
#
|
|
38
|
+
# Usage in controllers:
|
|
39
|
+
# render turbo_stream: helpers.flash_turbo_stream
|
|
40
|
+
def flash_turbo_stream
|
|
41
|
+
turbo_stream.append("flash-storage", partial: "flash_unified/storage")
|
|
42
|
+
end
|
|
43
|
+
|
|
31
44
|
# Wrapper helper that renders the common flash-unified pieces in a single
|
|
32
45
|
# call. This is a non-destructive convenience helper which calls the
|
|
33
46
|
# existing partial-rendering helpers in a sensible default order. Pass a
|
|
@@ -1 +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
|
|
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 C(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,a=Array.from(document.querySelectorAll("[data-flash-message-container]"));return r&&(a=a.filter(l=>l.hasAttribute("data-flash-primary")&&l.getAttribute("data-flash-primary")!=="false")),n&&(a=a.filter(C)),typeof o=="function"&&(a=a.filter(o)),t&&a.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,S=Number.isFinite(m)?m:Number.POSITIVE_INFINITY;return A-S}),s?a.length>0?[a[0]]:[]:a}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(){i()},{once:!0}):i()}function i(){let e=g(!1);typeof d=="function"?d(e):F(e)}function g(e=!1){let r=document.querySelectorAll("[data-flash-storage]"),n=new Set,t=[];return r.forEach(s=>{let o=s.getAttribute("data-object-id");if(o&&n.has(o)){e||s.remove();return}o&&n.add(o);let a=s.querySelector("ul");a&&a.children.length>0&&a.querySelectorAll("li").forEach(l=>{t.push({type:l.dataset.type||"notice",message:l.textContent.trim()})}),e||s.remove()}),t}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{w(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 a=o.querySelector(".flash-message-text");return a&&(a.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 b(){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 w(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)}),i())}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&&i()}).observe(document.body,{childList:!0,subtree:!0})}function u(e){if(b())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),i()}function B(e){u(e),i()}function M(){let e=document.documentElement;e.hasAttribute("data-flash-unified-turbo-listeners")||(e.setAttribute("data-flash-unified-turbo-listeners","true"),document.addEventListener("turbo:load",function(){i()}),document.addEventListener("turbo:frame-load",function(){i()}),document.addEventListener("turbo:render",function(){i()}),q())}function q(){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(){i()})}function p(){let e=document.documentElement;e.hasAttribute("data-flash-unified-initialized")||(e.setAttribute("data-flash-unified-initialized","true"),M(),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),i()}),document.addEventListener("turbo:fetch-request-error",function(r){u(0),i()}))}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()=>{p(),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,p as installTurboIntegration,M as installTurboRenderListeners,B as notifyHttpError,P as notifyNetworkError,w as processMessagePayload,i as renderFlashMessages,u as resolveAndAppendErrorMessage,x as setFlashMessageRenderer,R as startMutationObserver,b as storageHasMessages};
|
|
@@ -192,6 +192,21 @@ function renderFlashMessages() {
|
|
|
192
192
|
* Collect messages from all `[data-flash-storage]` elements.
|
|
193
193
|
* By default, removes each storage after reading; pass `keep = true` to preserve them.
|
|
194
194
|
*
|
|
195
|
+
* Notes about deduplication using object_id:
|
|
196
|
+
* - Each storage may include a `data-object-id` attribute populated server-side
|
|
197
|
+
* (the Rails `flash.object_id` in `_storage.html.erb`). This value is used to
|
|
198
|
+
* deduplicate storages that originate from the same FlashHash instance.
|
|
199
|
+
* - This is useful for the common case where the same `flash` object is rendered
|
|
200
|
+
* both in the layout and inside a Turbo Frame during a single full-page render:
|
|
201
|
+
* those storages will share the same `data-object-id` and only one will be processed.
|
|
202
|
+
* - `flash.object_id` is scoped to the Ruby object instance for the current request.
|
|
203
|
+
* Storages coming from separate requests will have different object ids and
|
|
204
|
+
* therefore will not be deduplicated (this is intentional — separate requests
|
|
205
|
+
* should be allowed to show their messages).
|
|
206
|
+
* - If a storage does not provide `data-object-id`, it is treated as independent.
|
|
207
|
+
* Consumers may want to ensure the server partial emits `data-object-id` when
|
|
208
|
+
* appropriate to enable robust deduplication.
|
|
209
|
+
*
|
|
195
210
|
* @param {boolean} [keep=false] - When true, do not remove storage elements after reading.
|
|
196
211
|
* @returns {{type: string, message: string}[]} Array of message objects.
|
|
197
212
|
*
|
|
@@ -200,8 +215,16 @@ function renderFlashMessages() {
|
|
|
200
215
|
*/
|
|
201
216
|
function consumeFlashMessages(keep = false) {
|
|
202
217
|
const storages = document.querySelectorAll('[data-flash-storage]');
|
|
218
|
+
const seen = new Set();
|
|
203
219
|
const messages = [];
|
|
204
220
|
storages.forEach(storage => {
|
|
221
|
+
const objectId = storage.getAttribute('data-object-id');
|
|
222
|
+
if (objectId && seen.has(objectId)) {
|
|
223
|
+
if (!keep) storage.remove();
|
|
224
|
+
return; // skip duplicate
|
|
225
|
+
}
|
|
226
|
+
if (objectId) seen.add(objectId);
|
|
227
|
+
|
|
205
228
|
const ul = storage.querySelector('ul');
|
|
206
229
|
if (ul && ul.children.length > 0) {
|
|
207
230
|
ul.querySelectorAll('li').forEach(li => {
|
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.4.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-
|
|
11
|
+
date: 2025-11-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|