swal_rails 0.3.1.beta1
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 +7 -0
- data/Appraisals +43 -0
- data/CHANGELOG.md +73 -0
- data/LICENSE.txt +21 -0
- data/README.md +973 -0
- data/Rakefile +12 -0
- data/app/assets/javascripts/swal_rails/chain.js +38 -0
- data/app/assets/javascripts/swal_rails/confirm.js +93 -0
- data/app/assets/javascripts/swal_rails/controllers/swal_controller.js +54 -0
- data/app/assets/javascripts/swal_rails/flash.js +24 -0
- data/app/assets/javascripts/swal_rails/index.js +62 -0
- data/app/assets/stylesheets/swal_rails/index.css +5 -0
- data/config/importmap.rb +9 -0
- data/config/locales/swal_rails.en.yml +19 -0
- data/config/locales/swal_rails.fr.yml +19 -0
- data/gemfiles/rails_7_2.gemfile +25 -0
- data/gemfiles/rails_8_0.gemfile +25 -0
- data/gemfiles/rails_8_1.gemfile +25 -0
- data/gemfiles/rails_8_1_sprockets.gemfile +25 -0
- data/lib/generators/swal_rails/install/install_generator.rb +138 -0
- data/lib/generators/swal_rails/install/templates/initializer.rb +33 -0
- data/lib/generators/swal_rails/locales/locales_generator.rb +19 -0
- data/lib/swal_rails/configuration.rb +88 -0
- data/lib/swal_rails/engine.rb +55 -0
- data/lib/swal_rails/helpers.rb +96 -0
- data/lib/swal_rails/version.rb +6 -0
- data/lib/swal_rails.rb +25 -0
- data/vendor/javascript/sweetalert2/LICENSE +22 -0
- data/vendor/javascript/sweetalert2/sweetalert2.all.js +4814 -0
- data/vendor/javascript/sweetalert2/sweetalert2.all.min.js +6 -0
- data/vendor/javascript/sweetalert2/sweetalert2.esm.all.js +4805 -0
- data/vendor/javascript/sweetalert2/sweetalert2.esm.all.min.js +6 -0
- data/vendor/javascript/sweetalert2/sweetalert2.esm.js +4804 -0
- data/vendor/javascript/sweetalert2/sweetalert2.esm.min.js +5 -0
- data/vendor/javascript/sweetalert2/sweetalert2.js +4813 -0
- data/vendor/javascript/sweetalert2/sweetalert2.min.js +5 -0
- data/vendor/stylesheets/sweetalert2/LICENSE +22 -0
- data/vendor/stylesheets/sweetalert2/sweetalert2.css +1233 -0
- data/vendor/stylesheets/sweetalert2/sweetalert2.min.css +1 -0
- data/vendor/stylesheets/sweetalert2/themes/bootstrap-4.css +167 -0
- data/vendor/stylesheets/sweetalert2/themes/bootstrap-5.css +173 -0
- data/vendor/stylesheets/sweetalert2/themes/borderless.css +46 -0
- data/vendor/stylesheets/sweetalert2/themes/bulma.css +94 -0
- data/vendor/stylesheets/sweetalert2/themes/material-ui.css +183 -0
- data/vendor/stylesheets/sweetalert2/themes/minimal.css +40 -0
- metadata +124 -0
data/README.md
ADDED
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# ๐ฌ swal_rails
|
|
4
|
+
|
|
5
|
+
**SweetAlert2 v11 for Rails 7+ โ batteries included.**
|
|
6
|
+
|
|
7
|
+
First-class support for **Importmap**, **jsbundling**, and **Sprockets**, with a
|
|
8
|
+
Stimulus controller, auto-wired flash messages, Turbo confirm replacement,
|
|
9
|
+
Ruby view helpers, and full I18n. Everything is configurable.
|
|
10
|
+
|
|
11
|
+
[](https://github.com/Metalzoid/swal_rails/actions)
|
|
12
|
+
[](https://rubygems.org/gems/swal_rails)
|
|
13
|
+
[](https://www.ruby-lang.org/)
|
|
14
|
+
[](https://rubyonrails.org/)
|
|
15
|
+
[](https://sweetalert2.github.io/)
|
|
16
|
+
[](LICENSE.txt)
|
|
17
|
+
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## ๐ Table of contents
|
|
23
|
+
|
|
24
|
+
- [Why swal_rails?](#-why-swal_rails)
|
|
25
|
+
- [Features](#-features)
|
|
26
|
+
- [Compatibility](#-compatibility)
|
|
27
|
+
- [Installation](#-installation)
|
|
28
|
+
- [Quick start](#-quick-start)
|
|
29
|
+
- [Configuration](#%EF%B8%8F-configuration)
|
|
30
|
+
- [Usage](#-usage)
|
|
31
|
+
- [Flash messages](#flash-messages)
|
|
32
|
+
- [Turbo confirmations](#turbo-confirmations)
|
|
33
|
+
- [`data-swal-confirm` attribute](#data-swal-confirm-attribute)
|
|
34
|
+
- [Multi-step confirmations](#multi-step-confirmations)
|
|
35
|
+
- [Stimulus controller](#stimulus-controller)
|
|
36
|
+
- [Ruby view helpers](#ruby-view-helpers)
|
|
37
|
+
- [Programmatic JS](#programmatic-js)
|
|
38
|
+
- [Reference](#-reference)
|
|
39
|
+
- [`SwalRails.configure`](#swalrailsconfigure)
|
|
40
|
+
- [View helpers](#view-helpers)
|
|
41
|
+
- [Data attributes](#data-attributes)
|
|
42
|
+
- [Stimulus controller reference](#stimulus-controller-reference)
|
|
43
|
+
- [JS runtime](#js-runtime)
|
|
44
|
+
- [Generators](#generators)
|
|
45
|
+
- [Flash value shapes](#flash-value-shapes)
|
|
46
|
+
- [Chain semantics](#chain-semantics)
|
|
47
|
+
- [I18n](#-i18n)
|
|
48
|
+
- [Accessibility](#-accessibility)
|
|
49
|
+
- [Security & CSP](#-security--csp)
|
|
50
|
+
- [Themes](#-themes)
|
|
51
|
+
- [Asset pipelines](#-asset-pipelines)
|
|
52
|
+
- [Development](#-development)
|
|
53
|
+
- [Contributing](#-contributing)
|
|
54
|
+
- [Credits & license](#-credits--license)
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## ๐ค Why swal_rails?
|
|
59
|
+
|
|
60
|
+
The existing gems haven't shipped a release since 2019 (SA2 was on **v7** back
|
|
61
|
+
then โ it's on **v11** now) and were built for the Rails 5 / UJS era.
|
|
62
|
+
`swal_rails` is the modern replacement:
|
|
63
|
+
|
|
64
|
+
| | `sweetalert2-rails` | `sweetify` | **`swal_rails`** |
|
|
65
|
+
| --------------------------------------- | :-----------------: | :------------: | :------------------: |
|
|
66
|
+
| SweetAlert2 v11.x | โ | โ | โ
|
|
|
67
|
+
| Rails 7+ / Turbo-native | โ | โ | โ
|
|
|
68
|
+
| Importmap | โ | โ | โ
|
|
|
69
|
+
| jsbundling (esbuild / vite / rollup) | โ | โ | โ
|
|
|
70
|
+
| Sprockets | โ
| โ | โ
|
|
|
71
|
+
| Stimulus controller | โ | โ | โ
|
|
|
72
|
+
| Flash auto-wire, map per key | โ | partial | โ
|
|
|
73
|
+
| Turbo `setConfirmMethod` override | โ | โ | โ
|
|
|
74
|
+
| `data-swal-confirm` attribute | โ | โ | โ
|
|
|
75
|
+
| Ruby view helpers (`swal_tag`) | โ | โ | โ
|
|
|
76
|
+
| I18n Rails (fr/en shipped) | โ | โ | โ
|
|
|
77
|
+
| a11y (reduced-motion, ARIA) | โ | โ | โ
|
|
|
78
|
+
| Last release | 2019 | 2019 | **maintained** |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## โจ Features
|
|
83
|
+
|
|
84
|
+
- ๐จ **SweetAlert2 v11** vendored and pinned โ no CDN, no surprise upgrades.
|
|
85
|
+
- โก **Three asset pipelines**: Importmap (default), jsbundling, Sprockets.
|
|
86
|
+
- ๐ **Auto-wired flash** โ `flash[:notice]` โ toast, `flash[:alert]` โ modal, fully mappable per key.
|
|
87
|
+
- ๐ก๏ธ **Turbo confirmations** โ replace the native `confirm()` globally **or** opt-in per element.
|
|
88
|
+
- ๐ฎ **Stimulus controller** (`data-controller="swal"`) for declarative popups.
|
|
89
|
+
- ๐งฑ **Ruby view helpers** โ `swal_tag`, `swal_config_meta_tag`, `swal_flash_meta_tag`.
|
|
90
|
+
- ๐ **CSP-friendly** โ `swal_tag(..., nonce: true)` propagates the per-request nonce.
|
|
91
|
+
- ๐ **I18n ready** โ `en` / `fr` locales shipped, override freely.
|
|
92
|
+
- โฟ **Accessibility** โ honors `prefers-reduced-motion`, preserves ARIA & focus trap.
|
|
93
|
+
- ๐งฉ **Engine-based** โ zero monkey-patching, follows Rails Engine conventions.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## ๐ง Compatibility
|
|
98
|
+
|
|
99
|
+
Tested on every combination of Ruby and Rails listed below via the
|
|
100
|
+
[Appraisal](https://github.com/thoughtbot/appraisal) gem:
|
|
101
|
+
|
|
102
|
+
| | Rails 7.2 | Rails 8.0 | Rails 8.1.3 | Rails 8.1.3 + Sprockets |
|
|
103
|
+
| ------- | :-------: | :-------: | :---------: | :---------------------: |
|
|
104
|
+
| Ruby 3.2| โ
| โ
| โ
| โ
|
|
|
105
|
+
| Ruby 3.3| โ
| โ
| โ
| โ
|
|
|
106
|
+
| Ruby 3.4| โ
| โ
| โ
| โ
|
|
|
107
|
+
| Ruby 3.5| โ
| โ
| โ
| โ
|
|
|
108
|
+
| Ruby 4.0| โ | โ
| โ
| โ
|
|
|
109
|
+
|
|
110
|
+
> **Requirements:** Ruby **โฅ 3.2** (tested up to **4.0**), Rails **โฅ 7.2** (tested up to **8.1.3**), Turbo recommended.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## ๐ฆ Installation
|
|
115
|
+
|
|
116
|
+
Add this line to your application's `Gemfile`:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
gem "swal_rails"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
> **During the beta**, pin the prerelease explicitly:
|
|
123
|
+
> ```ruby
|
|
124
|
+
> gem "swal_rails", "0.3.1.beta1"
|
|
125
|
+
> ```
|
|
126
|
+
> or install globally with `gem install swal_rails --pre`. Bundler ignores prereleases unless you ask for one.
|
|
127
|
+
|
|
128
|
+
Then install and run the generator:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
$ bundle install
|
|
132
|
+
$ bin/rails g swal_rails:install
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The generator **autodetects** your asset pipeline. You can force it:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
$ bin/rails g swal_rails:install --mode=importmap # default for new Rails apps
|
|
139
|
+
$ bin/rails g swal_rails:install --mode=jsbundling # esbuild, vite, rollup
|
|
140
|
+
$ bin/rails g swal_rails:install --mode=sprockets # legacy apps
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### What the generator does
|
|
144
|
+
|
|
145
|
+
- ๐ creates `config/initializers/swal_rails.rb`
|
|
146
|
+
- ๐ pins (Importmap) or adds (jsbundling/sprockets) `sweetalert2` and `swal_rails`
|
|
147
|
+
- ๐ injects `<%= swal_rails_meta_tags %>` into `app/views/layouts/application.html.erb`
|
|
148
|
+
- ๐ loads the `en` / `fr` locales
|
|
149
|
+
|
|
150
|
+
Finally, in your JS entrypoint (e.g. `app/javascript/application.js`):
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
import "swal_rails"
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## ๐ Quick start
|
|
159
|
+
|
|
160
|
+
Thirty seconds, tops. After running the installer:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
# app/controllers/posts_controller.rb
|
|
164
|
+
def create
|
|
165
|
+
@post = Post.create!(post_params)
|
|
166
|
+
redirect_to @post, notice: "Post created!"
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```erb
|
|
171
|
+
<%# app/views/posts/show.html.erb %>
|
|
172
|
+
<%= button_to "Delete", @post, method: :delete,
|
|
173
|
+
data: { turbo_confirm: "Really delete this post?" } %>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
That's it. `notice` renders as a **toast**, the delete button opens a
|
|
177
|
+
**SweetAlert2 modal** instead of the browser's ugly native `confirm()`.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## โ๏ธ Configuration
|
|
182
|
+
|
|
183
|
+
Everything lives in `config/initializers/swal_rails.rb`:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
SwalRails.configure do |config|
|
|
187
|
+
# โโโ Confirm behavior โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
188
|
+
# :off โ don't touch Turbo, no data-attribute listener
|
|
189
|
+
# :data_attribute โ only intercept [data-swal-confirm] clicks (default)
|
|
190
|
+
# :turbo_override โ replace Turbo.setConfirmMethod globally
|
|
191
|
+
# :both โ data-attribute + Turbo override
|
|
192
|
+
config.confirm_mode = :data_attribute
|
|
193
|
+
|
|
194
|
+
# โโโ UX โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
195
|
+
config.respect_reduced_motion = true # disable animations when OS asks
|
|
196
|
+
config.expose_window_swal = true # window.Swal for console hacking
|
|
197
|
+
|
|
198
|
+
# โโโ Defaults passed to every Swal.fire call โโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
199
|
+
config.default_options = {
|
|
200
|
+
buttonsStyling: true,
|
|
201
|
+
reverseButtons: false,
|
|
202
|
+
focusConfirm: true,
|
|
203
|
+
returnFocus: true
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# โโโ Flash โ Swal mapping (per key) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
207
|
+
config.flash_map[:notice] = { icon: "success", toast: true, position: "top-end", timer: 3000 }
|
|
208
|
+
config.flash_map[:success] = { icon: "success", toast: true, position: "top-end", timer: 3000 }
|
|
209
|
+
config.flash_map[:alert] = { icon: "error", toast: false }
|
|
210
|
+
config.flash_map[:error] = { icon: "error", toast: false }
|
|
211
|
+
config.flash_map[:warning] = { icon: "warning", toast: true, position: "top-end", timer: 4000 }
|
|
212
|
+
config.flash_map[:info] = { icon: "info", toast: true, position: "top-end", timer: 3000 }
|
|
213
|
+
|
|
214
|
+
# โโโ I18n scope (for button labels) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
215
|
+
config.i18n_scope = "swal_rails"
|
|
216
|
+
end
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
> ๐ก **Per-key override tip:** to disable the toast for a single key without
|
|
220
|
+
> rewriting the whole map: `config.flash_map[:alert] = { icon: "error", toast: false }`.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## ๐ฏ Usage
|
|
225
|
+
|
|
226
|
+
### Flash messages
|
|
227
|
+
|
|
228
|
+
Any flash set from a controller is rendered automatically on page load:
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
flash[:notice] = "Profile updated" # โ toast top-right
|
|
232
|
+
flash[:alert] = "Could not save" # โ modal
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Arrays are expanded into one popup per message โ handy for model errors:
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
flash[:alert] = @post.errors.full_messages # ["Title can't be blank", "Body is too short"]
|
|
239
|
+
# โ two separate Swals, one per message
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Need to override the per-key defaults for a single request? Assign a **Hash**
|
|
243
|
+
instead of a String โ its keys flow straight into `Swal.fire` and shadow
|
|
244
|
+
`flash_map[key]`:
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
flash[:notice] = { text: "Deployed!", icon: "rocket", timer: 5000, toast: true }
|
|
248
|
+
# โ ignores flash_map[:notice], fires a 5-second rocket toast
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Behind the scenes, the engine serializes the flash into a meta tag
|
|
252
|
+
(`<meta name="swal-flash" content="...">`) and the JS runtime reads it and
|
|
253
|
+
calls `Swal.fire(...)` with your per-key options.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
### Turbo confirmations
|
|
258
|
+
|
|
259
|
+
Works with standard Rails / Turbo syntax:
|
|
260
|
+
|
|
261
|
+
```erb
|
|
262
|
+
<%= button_to "Delete", post_path(@post), method: :delete,
|
|
263
|
+
data: { turbo_confirm: "Really delete?" } %>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
When `confirm_mode` is `:turbo_override` or `:both`, `swal_rails` replaces
|
|
267
|
+
`Turbo.setConfirmMethod` with a SweetAlert2-backed implementation โ the same
|
|
268
|
+
`data-turbo-confirm` attribute now shows a proper modal.
|
|
269
|
+
|
|
270
|
+
Pass a **Hash** instead of a string to carry full SA2 options:
|
|
271
|
+
|
|
272
|
+
```erb
|
|
273
|
+
<%= button_to "Delete", post_path(@post), method: :delete, data: {
|
|
274
|
+
turbo_confirm: { icon: "error", title: "Really?", confirmButtonText: "Nuke" }
|
|
275
|
+
} %>
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Rails JSON-encodes the Hash into the attribute; the runtime parses it back
|
|
279
|
+
and treats it as a full options object (same thing works with
|
|
280
|
+
`data-swal-confirm`).
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
### `data-swal-confirm` attribute
|
|
285
|
+
|
|
286
|
+
If you don't want to override Turbo globally, opt-in per element:
|
|
287
|
+
|
|
288
|
+
```erb
|
|
289
|
+
<%= link_to "Archive", archive_path,
|
|
290
|
+
data: { swal_confirm: "Archive this item?", swal_icon: "warning" } %>
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Supported data attributes:
|
|
294
|
+
|
|
295
|
+
| Attribute | Maps to |
|
|
296
|
+
| ---------------------------------- | ---------------------- |
|
|
297
|
+
| `data-swal-confirm` | `text` / title prompt |
|
|
298
|
+
| `data-swal-title` | `title` |
|
|
299
|
+
| `data-swal-text` | `text` |
|
|
300
|
+
| `data-swal-icon` | `icon` |
|
|
301
|
+
| `data-swal-confirm-text` | `confirmButtonText` |
|
|
302
|
+
| `data-swal-cancel-text` | `cancelButtonText` |
|
|
303
|
+
| `data-swal-options` *(JSON)* | full SA2 options (wins over the above) |
|
|
304
|
+
|
|
305
|
+
Use `data-swal-options` when you need anything beyond the shortcuts โ it
|
|
306
|
+
accepts any SweetAlert2 option:
|
|
307
|
+
|
|
308
|
+
```erb
|
|
309
|
+
<%= button_to "Delete", post_path(@post), method: :delete, data: {
|
|
310
|
+
swal_confirm: "Danger",
|
|
311
|
+
swal_options: { icon: "error", iconColor: "#ff0000", confirmButtonText: "Nuke" }.to_json
|
|
312
|
+
} %>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
### Multi-step confirmations
|
|
318
|
+
|
|
319
|
+
For destructive flows (account deletion, legal opt-ins, irreversible
|
|
320
|
+
actions), chain several popups via `data-swal-steps`. Each step only
|
|
321
|
+
fires if the previous one was **confirmed** โ any Cancel or Esc aborts
|
|
322
|
+
the whole cascade, and the original click/submit never reaches the
|
|
323
|
+
server:
|
|
324
|
+
|
|
325
|
+
```erb
|
|
326
|
+
<%= button_to "Delete account", account_path, method: :delete, data: {
|
|
327
|
+
swal_steps: [
|
|
328
|
+
{ title: "Delete your account?", icon: "warning" },
|
|
329
|
+
{ title: "This cannot be undone", icon: "error" },
|
|
330
|
+
{ title: "Type DELETE to confirm", input: "text" }
|
|
331
|
+
].to_json
|
|
332
|
+
} %>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Every step is a full SweetAlert2 options Hash โ override the default
|
|
336
|
+
icon, buttons, timer, `input:` type, anything SA2 accepts. The per-step
|
|
337
|
+
defaults (`showCancelButton: true`, `focusCancel: true`, `icon: "warning"`)
|
|
338
|
+
are merged in first and can be replaced key-by-key.
|
|
339
|
+
|
|
340
|
+
#### Conditional branching (`onConfirmed` / `onDenied`)
|
|
341
|
+
|
|
342
|
+
Add a Deny button (`showDenyButton: true`) to get a three-way choice, and
|
|
343
|
+
attach a nested sub-chain to either outcome:
|
|
344
|
+
|
|
345
|
+
```erb
|
|
346
|
+
<%= button_to "Delete or disable?", account_path, method: :delete, data: {
|
|
347
|
+
swal_steps: [
|
|
348
|
+
{
|
|
349
|
+
title: "Delete or just disable?",
|
|
350
|
+
icon: "question",
|
|
351
|
+
showDenyButton: true,
|
|
352
|
+
confirmButtonText: "Delete forever",
|
|
353
|
+
denyButtonText: "Disable for 30 days",
|
|
354
|
+
onDenied: [
|
|
355
|
+
{ title: "Confirm disable", icon: "info" }
|
|
356
|
+
]
|
|
357
|
+
}
|
|
358
|
+
].to_json
|
|
359
|
+
} %>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Semantic rules, per step:
|
|
363
|
+
|
|
364
|
+
| SA2 result | Behavior |
|
|
365
|
+
| ------------- | -------- |
|
|
366
|
+
| `isDismissed` | Abort the entire chain; action does not fire |
|
|
367
|
+
| `isConfirmed` | Run `onConfirmed` sub-chain if present (replaces remainder); else continue linearly |
|
|
368
|
+
| `isDenied` | Run `onDenied` sub-chain if present (its result decides); else abort |
|
|
369
|
+
|
|
370
|
+
Sub-chains are recursive โ they're just nested arrays of steps.
|
|
371
|
+
|
|
372
|
+
Under `confirm_mode = :turbo_override` (or `:both`), passing a JSON array
|
|
373
|
+
to `data-turbo-confirm` works the same way:
|
|
374
|
+
|
|
375
|
+
```erb
|
|
376
|
+
<%= button_to "Delete", account_path, method: :delete, data: {
|
|
377
|
+
turbo_confirm: [
|
|
378
|
+
{ title: "Really?" },
|
|
379
|
+
{ title: "Really really?" }
|
|
380
|
+
]
|
|
381
|
+
} %>
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
From Ruby, the view helper `swal_chain_tag` fires a chain inline on page
|
|
385
|
+
load (same CSP nonce and XSS hardening as `swal_tag`):
|
|
386
|
+
|
|
387
|
+
```erb
|
|
388
|
+
<%= swal_chain_tag([
|
|
389
|
+
{ title: "Welcome back" },
|
|
390
|
+
{ title: "Accept updated terms?" }
|
|
391
|
+
]) %>
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
### Stimulus controller
|
|
397
|
+
|
|
398
|
+
For fully declarative popups without touching JS:
|
|
399
|
+
|
|
400
|
+
```erb
|
|
401
|
+
<button data-controller="swal"
|
|
402
|
+
data-action="click->swal#fire"
|
|
403
|
+
data-swal-options-value='{"title":"Hello","icon":"success"}'>
|
|
404
|
+
Ping
|
|
405
|
+
</button>
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
Available actions: `fire`, `confirm`, `chain`. The `chain` action reads
|
|
409
|
+
`data-swal-steps-value` (same shape as `data-swal-steps`) and submits the
|
|
410
|
+
enclosing form if every step resolves confirmed.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
### Ruby view helpers
|
|
415
|
+
|
|
416
|
+
Fire a one-shot popup directly from a view:
|
|
417
|
+
|
|
418
|
+
```erb
|
|
419
|
+
<%= swal_tag(title: "Welcome back!", icon: "info", timer: 2000) %>
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Under a strict Content Security Policy, pass `nonce: true` โ Rails fills in
|
|
423
|
+
the per-request nonce so the inline `<script>` survives the policy:
|
|
424
|
+
|
|
425
|
+
```erb
|
|
426
|
+
<%= swal_tag({ title: "Welcome back!" }, nonce: true) %>
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
> **Heads-up:** the emitted tag is `<script type="module">` with a bare
|
|
430
|
+
> `import Swal from "sweetalert2"`. That resolves via Importmap (or any
|
|
431
|
+
> shim that processes import maps). On a pure esbuild/webpack setup with
|
|
432
|
+
> no importmap tag on the page, prefer the Stimulus controller or call
|
|
433
|
+
> `window.Swal.fire(...)` from your bundle instead.
|
|
434
|
+
|
|
435
|
+
Lower-level helpers (injected by the generator into your layout):
|
|
436
|
+
|
|
437
|
+
```erb
|
|
438
|
+
<%= swal_rails_meta_tags %>
|
|
439
|
+
<%# expands to: %>
|
|
440
|
+
<%= swal_config_meta_tag %> <%# serializes SwalRails.configuration %>
|
|
441
|
+
<%= swal_flash_meta_tag %> <%# serializes current flash, if any %>
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
---
|
|
445
|
+
|
|
446
|
+
### Programmatic JS
|
|
447
|
+
|
|
448
|
+
`Swal` is re-exported from the gem's JS runtime:
|
|
449
|
+
|
|
450
|
+
```js
|
|
451
|
+
import Swal from "sweetalert2"
|
|
452
|
+
|
|
453
|
+
Swal.fire({
|
|
454
|
+
title: "Saved!",
|
|
455
|
+
icon: "success",
|
|
456
|
+
toast: true,
|
|
457
|
+
position: "top-end",
|
|
458
|
+
timer: 3000
|
|
459
|
+
})
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
If `config.expose_window_swal = true`, `window.Swal` is also available for
|
|
463
|
+
quick console debugging.
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## ๐ Reference
|
|
468
|
+
|
|
469
|
+
Complete, at-a-glance specification of every public surface the gem
|
|
470
|
+
exposes. The sections above give narrative walk-throughs โ this section
|
|
471
|
+
is the lookup table.
|
|
472
|
+
|
|
473
|
+
### `SwalRails.configure`
|
|
474
|
+
|
|
475
|
+
```ruby
|
|
476
|
+
SwalRails.configure { |config| ... } # block form, yields Configuration
|
|
477
|
+
SwalRails.configuration # reader โ memoized, safe to mutate
|
|
478
|
+
SwalRails.reset_configuration! # resets to defaults (test fixture helper)
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
#### Configuration attributes
|
|
482
|
+
|
|
483
|
+
| Attribute | Type | Default | Description |
|
|
484
|
+
| ------------------------ | ------- | ------------------ | ----------- |
|
|
485
|
+
| `confirm_mode` | Symbol | `:data_attribute` | Routing of confirm dialogs โ see values below. Validated: assignment with any other value raises `ArgumentError`. Strings are coerced to symbols. |
|
|
486
|
+
| `flash_keys_as_meta` | Boolean | `true` | When `false`, `swal_flash_meta_tag` returns `nil` โ useful to opt out without removing the `swal_rails_meta_tags` call from the layout. |
|
|
487
|
+
| `respect_reduced_motion` | Boolean | `true` | When the OS reports `prefers-reduced-motion: reduce`, the gem empties SA2's `showClass` / `hideClass` to suppress animations. |
|
|
488
|
+
| `expose_window_swal` | Boolean | `true` | When `true`, `window.Swal` is set to the mixed-in `Swal` instance after boot (useful for console debugging and inline scripts). |
|
|
489
|
+
| `default_options` | Hash | see below | Merged into **every** `Swal.fire(...)` call via `Swal.mixin(...)`. |
|
|
490
|
+
| `flash_map` | Hash | see below | Flash-key โ SA2 options mapping. Keys normalized to symbols. Non-Hash assignment raises `ArgumentError`. |
|
|
491
|
+
| `i18n_scope` | String | `"swal_rails"` | I18n scope used to look up `confirm_button_text`, `cancel_button_text`, `deny_button_text`, `close_button_aria_label`. Non-string values are coerced. |
|
|
492
|
+
|
|
493
|
+
`confirm_mode` accepted values:
|
|
494
|
+
|
|
495
|
+
| Value | Behavior |
|
|
496
|
+
| ------------------ | -------- |
|
|
497
|
+
| `:off` | No Turbo override, no data-attribute listener. Use `Swal` manually. |
|
|
498
|
+
| `:data_attribute` | **(default)** intercept clicks/submits on `[data-swal-confirm]` and `[data-swal-steps]`. Does not touch Turbo. |
|
|
499
|
+
| `:turbo_override` | Replaces `Turbo.setConfirmMethod` globally. `data-turbo-confirm` attributes open SA2 modals. |
|
|
500
|
+
| `:both` | Enables both mechanisms โ useful for mixed codebases migrating over. |
|
|
501
|
+
|
|
502
|
+
`default_options` default:
|
|
503
|
+
|
|
504
|
+
```ruby
|
|
505
|
+
{ buttonsStyling: true, reverseButtons: false, focusConfirm: true, returnFocus: true }
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
`flash_map` default (per key, all overridable):
|
|
509
|
+
|
|
510
|
+
```ruby
|
|
511
|
+
{
|
|
512
|
+
notice: { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false },
|
|
513
|
+
success: { icon: "success", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false },
|
|
514
|
+
alert: { icon: "error", toast: false, timer: nil },
|
|
515
|
+
error: { icon: "error", toast: false, timer: nil },
|
|
516
|
+
warning: { icon: "warning", toast: true, position: "top-end", timer: 4000, timerProgressBar: true, showConfirmButton: false },
|
|
517
|
+
info: { icon: "info", toast: true, position: "top-end", timer: 3000, timerProgressBar: true, showConfirmButton: false }
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
#### `to_client_payload` (internal, read-only)
|
|
522
|
+
|
|
523
|
+
Serialization contract consumed by the JS runtime via the
|
|
524
|
+
`<meta name="swal-config">` tag. Returns:
|
|
525
|
+
|
|
526
|
+
```ruby
|
|
527
|
+
{
|
|
528
|
+
confirmMode: Symbol,
|
|
529
|
+
respectReducedMotion: Boolean,
|
|
530
|
+
exposeWindowSwal: Boolean,
|
|
531
|
+
defaultOptions: Hash,
|
|
532
|
+
flashMap: Hash,
|
|
533
|
+
i18n: Hash # only keys whose translation is present
|
|
534
|
+
}
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
### View helpers
|
|
540
|
+
|
|
541
|
+
All helpers are injected into both `ActionController::Base` and
|
|
542
|
+
`ActionView::Base` by the engine; they are available in every view and
|
|
543
|
+
in every controller-side rendering context.
|
|
544
|
+
|
|
545
|
+
| Helper | Returns | Description |
|
|
546
|
+
| ---------------------------------- | ----------------------------------------- | ----------- |
|
|
547
|
+
| `swal_rails_meta_tags` | `ActiveSupport::SafeBuffer` | Emits `swal_config_meta_tag` + `swal_flash_meta_tag` joined with `"\n"`. Call once, in `<head>`. |
|
|
548
|
+
| `swal_config_meta_tag` | `ActiveSupport::SafeBuffer` | Emits `<meta name="swal-config" content="โฆJSONโฆ">` with the serialized `SwalRails.configuration.to_client_payload`. |
|
|
549
|
+
| `swal_flash_meta_tag` | `ActiveSupport::SafeBuffer` or `nil` | Emits `<meta name="swal-flash" content="โฆJSONโฆ">` if `flash_keys_as_meta` is enabled **and** the current `flash` is non-empty; otherwise returns `nil`. |
|
|
550
|
+
| `swal_tag(options, html_options)` | `ActiveSupport::SafeBuffer` | Inline `<script type="module">` firing `Swal.fire(options)` once. Options are JSON-escaped to neutralize `</script>`, `<!--`, U+2028, U+2029. |
|
|
551
|
+
| `swal_chain_tag(steps, html_options)` | `ActiveSupport::SafeBuffer` | Inline `<script type="module">` firing `chainDialogs(Swal, steps)`. `steps` is an Array of Hashes (single Hash is auto-wrapped). Same escape hardening as `swal_tag`. |
|
|
552
|
+
|
|
553
|
+
#### `html_options` (for `swal_tag` / `swal_chain_tag`)
|
|
554
|
+
|
|
555
|
+
- `nonce: true` โ propagates the per-request CSP nonce (when Rails
|
|
556
|
+
exposes `content_security_policy_nonce`). Silently dropped when no CSP
|
|
557
|
+
helper is configured, so the helper stays safe in apps without CSP.
|
|
558
|
+
- Any other key is forwarded to `javascript_tag` as `<script>`
|
|
559
|
+
attributes.
|
|
560
|
+
|
|
561
|
+
The default `type` is `"module"` โ override with `html_options = { type: "text/javascript" }` if you need a classic script.
|
|
562
|
+
|
|
563
|
+
> โ ๏ธ The emitted `<script type="module">` contains bare imports
|
|
564
|
+
> (`import Swal from "sweetalert2"`). These resolve via Importmap; in a
|
|
565
|
+
> pure esbuild/webpack setup with no importmap tag on the page, call
|
|
566
|
+
> `window.Swal.fire(...)` from your bundle instead.
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
### Data attributes
|
|
571
|
+
|
|
572
|
+
All attributes are read from the element's `dataset`. Values carry through `button_to`, `link_to`, `form_with`, and raw HTML equally.
|
|
573
|
+
|
|
574
|
+
#### Single-step confirm (`data-swal-confirm`)
|
|
575
|
+
|
|
576
|
+
| Attribute | Accepts | Maps to (SA2 option) |
|
|
577
|
+
| ------------------------- | ---------------------------- | -------------------- |
|
|
578
|
+
| `data-swal-confirm` | String **or** JSON object *or* JSON array | message (String) โ `text`; Object โ full SA2 options (overrides all shortcuts); Array โ multi-step chain (see below) |
|
|
579
|
+
| `data-swal-title` | String | `title` |
|
|
580
|
+
| `data-swal-text` | String | `text` |
|
|
581
|
+
| `data-swal-icon` | String (`"warning"`, `"error"`, โฆ) | `icon` (default `"warning"`) |
|
|
582
|
+
| `data-swal-confirm-text` | String | `confirmButtonText` |
|
|
583
|
+
| `data-swal-cancel-text` | String | `cancelButtonText` |
|
|
584
|
+
| `data-swal-options` | JSON Object (stringified) | Full SA2 options โ **wins over** all shortcuts and over the JSON object form of `data-swal-confirm` |
|
|
585
|
+
|
|
586
|
+
Merge order (later wins):
|
|
587
|
+
|
|
588
|
+
```
|
|
589
|
+
defaults โ data-swal-* shortcuts โ JSON object in data-swal-confirm โ data-swal-options
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
#### Multi-step chain (`data-swal-steps`)
|
|
593
|
+
|
|
594
|
+
| Attribute | Accepts | Behavior |
|
|
595
|
+
| ------------------------- | -------------------------------------- | -------- |
|
|
596
|
+
| `data-swal-steps` | JSON Array of step Hashes (stringified) | Runs the chain. Per-step defaults `{ showCancelButton: true, focusCancel: true, icon: "warning" }` are merged first and can be overridden key-by-key. Every step is a full SA2 options Hash plus the optional `onConfirmed` / `onDenied` sub-chain keys. |
|
|
597
|
+
|
|
598
|
+
Both attributes coexist โ if `data-swal-steps` is present and non-empty, it takes precedence over `data-swal-confirm`.
|
|
599
|
+
|
|
600
|
+
#### Turbo `data-turbo-confirm`
|
|
601
|
+
|
|
602
|
+
When `confirm_mode` is `:turbo_override` or `:both`, `data-turbo-confirm` accepts:
|
|
603
|
+
|
|
604
|
+
| Form | Behavior |
|
|
605
|
+
| ------- | -------- |
|
|
606
|
+
| String | SA2 popup with the string as `text`. |
|
|
607
|
+
| Hash (JSON-encoded by Rails) | Full SA2 options. |
|
|
608
|
+
| Array (JSON-encoded by Rails) | Multi-step chain โ same shape as `data-swal-steps`. |
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
### Stimulus controller reference
|
|
613
|
+
|
|
614
|
+
Registered under the identifier `"swal"`.
|
|
615
|
+
|
|
616
|
+
#### Values
|
|
617
|
+
|
|
618
|
+
| Value | Type | Default | Used by |
|
|
619
|
+
| ------------- | ------ | ------- | ------- |
|
|
620
|
+
| `optionsValue` | Object | `{}` | `fire`, `confirm` |
|
|
621
|
+
| `stepsValue` | Array | `[]` | `chain` |
|
|
622
|
+
|
|
623
|
+
#### Actions
|
|
624
|
+
|
|
625
|
+
| Action | Target behavior |
|
|
626
|
+
| --------- | --------------- |
|
|
627
|
+
| `fire` | Fires `Swal.fire(optionsValue)`. Calls `preventDefault()` on the event if the element is `<a>` or `<button>`. Returns the SA2 promise. |
|
|
628
|
+
| `confirm` | Fires `Swal.fire({ showCancelButton: true, focusCancel: true, ...optionsValue })`. On `isConfirmed`, calls `requestSubmit()` (fallback `submit()`) on the enclosing form. |
|
|
629
|
+
| `chain` | Runs `chainDialogs(Swal, stepsValue)`. On resolved `true`, calls `requestSubmit()` / `submit()` on the enclosing form. Returns the boolean. |
|
|
630
|
+
|
|
631
|
+
Example:
|
|
632
|
+
|
|
633
|
+
```erb
|
|
634
|
+
<button data-controller="swal"
|
|
635
|
+
data-action="click->swal#chain"
|
|
636
|
+
data-swal-steps-value='[{"title":"Sure?"},{"title":"Really?"}]'>
|
|
637
|
+
Proceed
|
|
638
|
+
</button>
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
### JS runtime
|
|
644
|
+
|
|
645
|
+
#### Entry point
|
|
646
|
+
|
|
647
|
+
```js
|
|
648
|
+
import "swal_rails" // installs confirm + flash handlers
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
Expected in your JS entrypoint (e.g. `app/javascript/application.js`).
|
|
652
|
+
|
|
653
|
+
#### Re-exports
|
|
654
|
+
|
|
655
|
+
```js
|
|
656
|
+
import Swal from "sweetalert2" // SA2, re-exported as default from swal_rails
|
|
657
|
+
import { Swal } from "swal_rails" // named re-export (same instance)
|
|
658
|
+
import { chainDialogs, CHAIN_DEFAULTS } from "swal_rails/chain"
|
|
659
|
+
import { installConfirm } from "swal_rails/confirm" // for custom boot sequences
|
|
660
|
+
import { installFlash } from "swal_rails/flash" // same
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
#### `chainDialogs(Swal, steps)`
|
|
664
|
+
|
|
665
|
+
```ts
|
|
666
|
+
chainDialogs(Swal: typeof import("sweetalert2"), steps: Array<StepOptions>): Promise<boolean>
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
Returns `true` iff a complete path through the chain was confirmed. `steps` may be empty (resolves `true` immediately). Non-array input resolves `true` as well.
|
|
670
|
+
|
|
671
|
+
`StepOptions` is any valid SA2 options Hash, plus the two chain-only keys:
|
|
672
|
+
|
|
673
|
+
| Key | Type | Effect |
|
|
674
|
+
| -------------- | ------------------- | ------ |
|
|
675
|
+
| `onConfirmed` | `Array<StepOptions>` | On `isConfirmed`, run this sub-chain and adopt its boolean result (replaces the remainder of the current chain). |
|
|
676
|
+
| `onDenied` | `Array<StepOptions>` | On `isDenied` (requires `showDenyButton: true`), run this sub-chain and adopt its boolean result. Without this key, `isDenied` aborts the chain. |
|
|
677
|
+
|
|
678
|
+
#### Events
|
|
679
|
+
|
|
680
|
+
| Event name | Target | `event.detail` | When |
|
|
681
|
+
| ------------------ | ---------- | -------------- | ---- |
|
|
682
|
+
| `swal-rails:ready` | `document` | `{ Swal, config }` | Fired once per page lifetime after the first successful boot (not per Turbo navigation). |
|
|
683
|
+
|
|
684
|
+
#### Meta-tag contract
|
|
685
|
+
|
|
686
|
+
| Meta name | Payload | Read by |
|
|
687
|
+
| ------------- | ------- | ------- |
|
|
688
|
+
| `swal-config` | `to_client_payload` JSON | Runtime boot โ mixin, confirm handler, flash handler. Once per page. |
|
|
689
|
+
| `swal-flash` | Array of `{ key, options }` | Flash runtime โ re-read on every `turbo:load`. |
|
|
690
|
+
|
|
691
|
+
Flash entries are `{ key: "notice", options: { text: "..." } }` for string values, or `{ key: "notice", options: {...user hash...} }` for Hash values. Arrays in `flash[key]` are expanded into one entry per element.
|
|
692
|
+
|
|
693
|
+
---
|
|
694
|
+
|
|
695
|
+
### Generators
|
|
696
|
+
|
|
697
|
+
#### `bin/rails g swal_rails:install`
|
|
698
|
+
|
|
699
|
+
| Flag | Type | Default | Values |
|
|
700
|
+
| ------------------------- | ------- | ----------------- | ------ |
|
|
701
|
+
| `--mode` | String | `auto` | `auto`, `importmap`, `jsbundling`, `sprockets` |
|
|
702
|
+
| `--confirm_mode` | String | `data_attribute` | `off`, `data_attribute`, `turbo_override`, `both` โ baked into the generated initializer |
|
|
703
|
+
| `--skip_layout` | Boolean | `false` | When set, does not inject `<%= swal_rails_meta_tags %>` into `app/views/layouts/application.html.erb` |
|
|
704
|
+
|
|
705
|
+
`--mode=auto` detection order:
|
|
706
|
+
1. `config/importmap.rb` present โ `importmap`
|
|
707
|
+
2. `package.json` present โ `jsbundling`
|
|
708
|
+
3. fallback โ `sprockets`
|
|
709
|
+
|
|
710
|
+
Per-mode side effects:
|
|
711
|
+
|
|
712
|
+
- **importmap**: appends `pin "sweetalert2", to: "sweetalert2.esm.all.js"` and `pin "swal_rails", to: "swal_rails/index.js"` to `config/importmap.rb`; appends `import "swal_rails"` to `app/javascript/application.js`.
|
|
713
|
+
- **jsbundling**: runs `yarn add sweetalert2@<pinned>` or `npm install sweetalert2@<pinned>` (based on the lockfile present); appends `import "swal_rails"` to `app/javascript/application.js`.
|
|
714
|
+
- **sprockets**: appends `//= link sweetalert2.js` and `//= link sweetalert2.css` to `app/assets/config/manifest.js`.
|
|
715
|
+
|
|
716
|
+
All append operations are idempotent โ running the generator twice is safe.
|
|
717
|
+
|
|
718
|
+
#### `bin/rails g swal_rails:locales`
|
|
719
|
+
|
|
720
|
+
No flags. Copies `config/locales/swal_rails.en.yml` and `swal_rails.fr.yml` from the gem into your app's `config/locales/`.
|
|
721
|
+
|
|
722
|
+
---
|
|
723
|
+
|
|
724
|
+
### Flash value shapes
|
|
725
|
+
|
|
726
|
+
| Value type | Rendered as |
|
|
727
|
+
| ---------- | ----------- |
|
|
728
|
+
| `String` | `{ text: value }` โ safe by default (SA2 renders via `text:`, no HTML injection). |
|
|
729
|
+
| `Hash` | Full SA2 options, **shadows** `flash_map[key]`. Any SA2 key is accepted (icon, timer, input, html, iconHtml, โฆ). |
|
|
730
|
+
| `Array` | Expanded into one entry per element. Strings become `{ text: elem }`, Hashes pass through verbatim. |
|
|
731
|
+
| `nil` / `""` / `blank?` | Skipped. |
|
|
732
|
+
|
|
733
|
+
Key normalization: Hash keys are `symbolize_keys`-ed before serialization, so `flash[:notice] = { "text" => "..." }` and `flash[:notice] = { text: "..." }` are equivalent.
|
|
734
|
+
|
|
735
|
+
> โ ๏ธ **Hash overrides bypass the `text:` safety net.** Using `html:`, `iconHtml:`, or `footer:` with untrusted input is an XSS โ SweetAlert2 renders those as raw HTML by design. Rule of thumb: if the value is user-controlled, keep the String form.
|
|
736
|
+
|
|
737
|
+
---
|
|
738
|
+
|
|
739
|
+
### Chain semantics
|
|
740
|
+
|
|
741
|
+
Single source of truth for every chain-aware entry point (`data-swal-steps`, `data-turbo-confirm` with an array, Stimulus `chain` action, `swal_chain_tag`, direct `chainDialogs` call).
|
|
742
|
+
|
|
743
|
+
Per step:
|
|
744
|
+
|
|
745
|
+
| SA2 result | Behavior |
|
|
746
|
+
| ------------- | -------- |
|
|
747
|
+
| `isDismissed` (ร, Esc, backdrop) | Abort the current chain โ `false`. Outer chain (if this was a sub-chain) also terminates with `false`. |
|
|
748
|
+
| `isConfirmed` | If `onConfirmed: [...]` is defined, run it recursively and **replace** the remainder of the current chain with its result. Else continue linearly. |
|
|
749
|
+
| `isDenied` (requires `showDenyButton: true`) | If `onDenied: [...]` is defined, run it recursively and adopt its result. Else abort โ `false`. |
|
|
750
|
+
|
|
751
|
+
Return rules:
|
|
752
|
+
|
|
753
|
+
- An empty or non-array `steps` input resolves `true` immediately.
|
|
754
|
+
- A chain resolves `true` iff it ran to completion along a path without any dismiss or unbranched deny.
|
|
755
|
+
- Sub-chains inherit the same per-step defaults (`showCancelButton: true`, `focusCancel: true`, `icon: "warning"`).
|
|
756
|
+
- `onConfirmed` / `onDenied` keys are stripped before being passed to `Swal.fire`, so they never leak into the SA2 popup options.
|
|
757
|
+
|
|
758
|
+
Callback contract for Turbo override: the chain's final `Promise<boolean>` is what `Turbo.setConfirmMethod` receives โ `false` cancels the navigation / form submit, `true` proceeds.
|
|
759
|
+
|
|
760
|
+
---
|
|
761
|
+
|
|
762
|
+
## ๐ I18n
|
|
763
|
+
|
|
764
|
+
Locales `en` and `fr` ship with the gem. To copy them into your app for customization:
|
|
765
|
+
|
|
766
|
+
```bash
|
|
767
|
+
$ bin/rails g swal_rails:locales
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
Generated keys:
|
|
771
|
+
|
|
772
|
+
```yaml
|
|
773
|
+
en:
|
|
774
|
+
swal_rails:
|
|
775
|
+
confirm_button_text: "OK"
|
|
776
|
+
cancel_button_text: "Cancel"
|
|
777
|
+
deny_button_text: "No"
|
|
778
|
+
close_button_aria_label: "Close this dialog"
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
The current `I18n.locale` is read on every request and injected into the
|
|
782
|
+
client payload โ change languages, button labels follow.
|
|
783
|
+
|
|
784
|
+
---
|
|
785
|
+
|
|
786
|
+
## โฟ Accessibility
|
|
787
|
+
|
|
788
|
+
- **Reduced motion** โ when `prefers-reduced-motion: reduce` is set, animations
|
|
789
|
+
are disabled (`showClass`/`hideClass` emptied).
|
|
790
|
+
- **Focus trap & ARIA** โ SweetAlert2's built-in focus management and ARIA
|
|
791
|
+
roles are preserved; `returnFocus: true` brings focus back to the trigger.
|
|
792
|
+
- **Translatable labels** โ all button / aria labels go through I18n.
|
|
793
|
+
|
|
794
|
+
---
|
|
795
|
+
|
|
796
|
+
## ๐ Security & CSP
|
|
797
|
+
|
|
798
|
+
### XSS safety
|
|
799
|
+
|
|
800
|
+
All strings flowing through the Ruby helpers are hardened against the usual
|
|
801
|
+
breakout sequences:
|
|
802
|
+
|
|
803
|
+
- **`swal_tag`** runs the serialized options through `ERB::Util.json_escape`,
|
|
804
|
+
which neutralizes `</script>`, `<!--`, U+2028 and U+2029 before they reach
|
|
805
|
+
the inline `<script>` body.
|
|
806
|
+
- **`swal_config_meta_tag`** and **`swal_flash_meta_tag`** emit `<meta>`
|
|
807
|
+
attributes, which Rails HTML-escapes automatically; the JS runtime feeds
|
|
808
|
+
messages to SweetAlert2's `text:` option (not `html:`), so flash payloads
|
|
809
|
+
are rendered as text even if they contain HTML.
|
|
810
|
+
|
|
811
|
+
> โ ๏ธ **Hash-form overrides bypass the `text:` safety net.** When you pass a
|
|
812
|
+
> Hash to `flash[key]`, `data-turbo-confirm`, `data-swal-confirm`, or
|
|
813
|
+
> `data-swal-options`, its keys flow straight into `Swal.fire`. Using
|
|
814
|
+
> `html:`, `iconHtml:`, or `footer:` with untrusted input is an XSS โ
|
|
815
|
+
> SweetAlert2 renders those as raw HTML by design. Rule of thumb: if the
|
|
816
|
+
> value is user-controlled, keep the String form (or the `text:` key).
|
|
817
|
+
|
|
818
|
+
### Content Security Policy
|
|
819
|
+
|
|
820
|
+
Meta tags carry no script and need no nonce. For the inline helper:
|
|
821
|
+
|
|
822
|
+
```erb
|
|
823
|
+
<%= swal_tag({ title: "Saved" }, nonce: true) %>
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
When ActionView's CSP helper is available, Rails substitutes the per-request
|
|
827
|
+
nonce; otherwise `nonce: true` is silently dropped so the tag stays valid on
|
|
828
|
+
apps without a configured CSP.
|
|
829
|
+
|
|
830
|
+
> **SweetAlert2 + strict `style-src`** โ SA2 injects styles via JavaScript.
|
|
831
|
+
> Under `style-src 'self'` with no `'unsafe-inline'`, the popups won't be
|
|
832
|
+
> styled. Either ship SA2's CSS via your normal stylesheet (the gem vendors
|
|
833
|
+
> `sweetalert2.css`) or allow a style nonce for the inserted tags.
|
|
834
|
+
|
|
835
|
+
---
|
|
836
|
+
|
|
837
|
+
## ๐ญ Themes
|
|
838
|
+
|
|
839
|
+
The six official SweetAlert2 themes are vendored alongside the default
|
|
840
|
+
stylesheet โ pick one, load it in your layout, and set the
|
|
841
|
+
`data-swal2-theme` attribute to activate it.
|
|
842
|
+
|
|
843
|
+
| Theme | File |
|
|
844
|
+
| --- | --- |
|
|
845
|
+
| Bootstrap 4 | `sweetalert2/themes/bootstrap-4.css` |
|
|
846
|
+
| Bootstrap 5 | `sweetalert2/themes/bootstrap-5.css` |
|
|
847
|
+
| Borderless | `sweetalert2/themes/borderless.css` |
|
|
848
|
+
| Bulma | `sweetalert2/themes/bulma.css` |
|
|
849
|
+
| Material UI | `sweetalert2/themes/material-ui.css` |
|
|
850
|
+
| Minimal | `sweetalert2/themes/minimal.css` |
|
|
851
|
+
|
|
852
|
+
### Importmap / jsbundling (apps that load their own CSS)
|
|
853
|
+
|
|
854
|
+
```erb
|
|
855
|
+
<%# app/views/layouts/application.html.erb %>
|
|
856
|
+
<%= stylesheet_link_tag "sweetalert2/themes/bootstrap-5" %>
|
|
857
|
+
<body data-swal2-theme="bootstrap-5">
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
### Sprockets
|
|
861
|
+
|
|
862
|
+
```css
|
|
863
|
+
/* app/assets/stylesheets/application.css */
|
|
864
|
+
*= require sweetalert2/themes/bootstrap-5
|
|
865
|
+
```
|
|
866
|
+
|
|
867
|
+
`bootstrap-5` and `material-ui` ship both a light and a dark variant
|
|
868
|
+
baked into the same file โ set `data-swal2-theme="bootstrap-5-dark"` or
|
|
869
|
+
`"material-ui-dark"` to opt into dark mode.
|
|
870
|
+
|
|
871
|
+
> **OS-driven dark mode?** Hook the attribute to `prefers-color-scheme`
|
|
872
|
+
> with a one-liner:
|
|
873
|
+
> ```js
|
|
874
|
+
> document.body.dataset.swal2Theme =
|
|
875
|
+
> matchMedia("(prefers-color-scheme: dark)").matches
|
|
876
|
+
> ? "bootstrap-5-dark" : "bootstrap-5"
|
|
877
|
+
> ```
|
|
878
|
+
|
|
879
|
+
---
|
|
880
|
+
|
|
881
|
+
## ๐จ Asset pipelines
|
|
882
|
+
|
|
883
|
+
`swal_rails` adapts to whichever pipeline you use โ the generator picks the
|
|
884
|
+
right template automatically.
|
|
885
|
+
|
|
886
|
+
```
|
|
887
|
+
โโ Importmap (Rails 7+ default) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
888
|
+
โ config/importmap.rb โ
|
|
889
|
+
โ pin "sweetalert2", to: "sweetalert2.js" โ
|
|
890
|
+
โ pin "swal_rails", to: "swal_rails.js" โ
|
|
891
|
+
โ app/javascript/application.js โ
|
|
892
|
+
โ import "swal_rails" โ
|
|
893
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
894
|
+
|
|
895
|
+
โโ jsbundling (esbuild / vite / rollup) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
896
|
+
โ package.json โ "sweetalert2": "^11" (your bundler resolves it) โ
|
|
897
|
+
โ app/javascript/application.js โ
|
|
898
|
+
โ import "swal_rails" โ
|
|
899
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
900
|
+
|
|
901
|
+
โโ Sprockets (legacy) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
902
|
+
โ app/assets/javascripts/application.js โ
|
|
903
|
+
โ //= require sweetalert2 โ
|
|
904
|
+
โ //= require swal_rails โ
|
|
905
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
---
|
|
909
|
+
|
|
910
|
+
## ๐งช Development
|
|
911
|
+
|
|
912
|
+
```bash
|
|
913
|
+
$ git clone https://github.com/Metalzoid/swal_rails.git
|
|
914
|
+
$ cd swal_rails
|
|
915
|
+
$ bundle install
|
|
916
|
+
$ bundle exec rspec # 47 examples, real headless Chromium (Cuprite)
|
|
917
|
+
$ bundle exec rubocop # style
|
|
918
|
+
```
|
|
919
|
+
|
|
920
|
+
Test against a specific Rails version:
|
|
921
|
+
|
|
922
|
+
```bash
|
|
923
|
+
$ bundle exec appraisal install
|
|
924
|
+
$ BUNDLE_GEMFILE=gemfiles/rails_7_2.gemfile bundle exec rspec
|
|
925
|
+
$ BUNDLE_GEMFILE=gemfiles/rails_8_1_sprockets.gemfile bundle exec rspec
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
### Repo layout
|
|
929
|
+
|
|
930
|
+
```
|
|
931
|
+
swal_rails/
|
|
932
|
+
โโโ app/ # Engine: helpers, views, assets
|
|
933
|
+
โโโ lib/
|
|
934
|
+
โ โโโ swal_rails.rb # entrypoint + Engine + Railtie
|
|
935
|
+
โ โโโ swal_rails/configuration.rb
|
|
936
|
+
โ โโโ generators/
|
|
937
|
+
โ โโโ install/ # bin/rails g swal_rails:install
|
|
938
|
+
โ โโโ locales/ # bin/rails g swal_rails:locales
|
|
939
|
+
โโโ vendor/javascript/sweetalert2/ # pinned SA2 v11.26.24 (MIT)
|
|
940
|
+
โโโ spec/
|
|
941
|
+
โ โโโ dummy/ # minimal Rails app for system tests
|
|
942
|
+
โโโ Appraisals # Rails 7.2 โ 8.1 + sprockets variant
|
|
943
|
+
โโโ gemfiles/ # per-version lockfiles (generated)
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
---
|
|
947
|
+
|
|
948
|
+
## ๐ค Contributing
|
|
949
|
+
|
|
950
|
+
Bug reports and pull requests welcome on GitHub at
|
|
951
|
+
<https://github.com/Metalzoid/swal_rails>.
|
|
952
|
+
|
|
953
|
+
1. Fork it
|
|
954
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
|
955
|
+
3. Make sure tests and RuboCop pass (`bundle exec rspec && bundle exec rubocop`)
|
|
956
|
+
4. Push (`git push origin my-new-feature`)
|
|
957
|
+
5. Open a Pull Request
|
|
958
|
+
|
|
959
|
+
---
|
|
960
|
+
|
|
961
|
+
## ๐ Credits & license
|
|
962
|
+
|
|
963
|
+
This gem bundles [SweetAlert2](https://sweetalert2.github.io/) by Tristan
|
|
964
|
+
Edwards, Limon Monte and contributors, distributed under the MIT License. The
|
|
965
|
+
full license is included at `vendor/javascript/sweetalert2/LICENSE`.
|
|
966
|
+
|
|
967
|
+
`swal_rails` itself is released under the [MIT License](LICENSE.txt).
|
|
968
|
+
|
|
969
|
+
<div align="center">
|
|
970
|
+
|
|
971
|
+
Built with ๐ฌ by [Metalzoid](https://github.com/Metalzoid)
|
|
972
|
+
|
|
973
|
+
</div>
|