react-email-rails 0.4.1 → 0.5.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/CHANGELOG.md +5 -0
- data/README.md +84 -15
- data/lib/react_email_rails/action_mailer.rb +38 -0
- data/lib/react_email_rails/configuration.rb +2 -0
- data/lib/react_email_rails/shared_props.rb +44 -0
- data/lib/react_email_rails/version.rb +1 -1
- data/lib/react_email_rails.rb +2 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4bbfbb40c2044856c9061ba2a3c134d07dbd5aa9b58b6cfe3d576d25fe45b4d2
|
|
4
|
+
data.tar.gz: 7c272a65e71044f813c813b248a48bc69e3a043494ad9f6133ae8c69ab006b24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5b434f0983a8c1280d622203869f535a0c98fb293bcfff3d51d39a8676d730d000b901cb9bcc6b3b478fe633904974798131482e44982140f40e12951ac1423
|
|
7
|
+
data.tar.gz: 21652f85cc0ee448ec90ffd7b24c4eaf05b892dbe0e83d487777ae57bbcda7a34c46e1b9becad9174838edd0d33919b11aa9efb56cbd520433803c8a3fd82385
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
- Add `react_email_share` for sharing props across every `react:` email a mailer renders, mirroring inertia-rails' `inertia_share`. Supports static values, lazy lambdas and blocks (evaluated in the mailer instance), `only`/`except`/`if`/`unless` filters, and subclass inheritance. Per-mail props win over shared props.
|
|
6
|
+
- Shared props merge shallowly by default. Pass `deep_merge: true` to `mail` to merge nested hashes, or set `config.deep_merge_shared_props = true` to make it the default.
|
|
7
|
+
|
|
3
8
|
## 0.4.1
|
|
4
9
|
|
|
5
10
|
- `ReactEmailRails.parse` now neutralizes unsafe link/button URI schemes: hrefs whose scheme is not `http`, `https`, `mailto`, or `tel` (e.g. `javascript:`/`data:`) are blanked before they reach the document `Hash`. Scheme detection ignores the whitespace and control characters browsers strip when resolving a scheme, so case- and whitespace-obfuscated payloads are caught too.
|
data/README.md
CHANGED
|
@@ -33,7 +33,7 @@ react-email-rails automatically renders both HTML and plain-text versions from t
|
|
|
33
33
|
|
|
34
34
|
## Status
|
|
35
35
|
|
|
36
|
-
**react-email-rails is pre-1.0.** It
|
|
36
|
+
**react-email-rails is pre-1.0.** It was extracted from [XOXO](https://xoxo.email) which is pre-launch, so it hasn't been battle-tested in high-volume production environments yet. It's tested in CI across the supported Ruby, Rails, Node, and Vite versions, but the API may still change before 1.0. Please [share feedback and report issues](https://github.com/heysupertape/react-email-rails/issues).
|
|
37
37
|
|
|
38
38
|
## Requirements
|
|
39
39
|
|
|
@@ -160,7 +160,7 @@ That's it. It now renders and delivers like any other Action Mailer email.
|
|
|
160
160
|
|
|
161
161
|
## Usage
|
|
162
162
|
|
|
163
|
-
Pass data from your mailer
|
|
163
|
+
Pass data from your mailer and each top-level key becomes a prop on the component's default export. Our API mirrors [inertia-rails](https://inertia-rails.dev), making the two libraries feel consistent when used together.
|
|
164
164
|
|
|
165
165
|
```ruby
|
|
166
166
|
mail react: { foo: "bar" }, ...
|
|
@@ -172,9 +172,11 @@ export default function Email({ foo }: { foo: string }) {
|
|
|
172
172
|
}
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
### Props
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
Choose the level of inference you want for your props:
|
|
178
|
+
|
|
179
|
+
#### Implicit Component, Instance Props
|
|
178
180
|
|
|
179
181
|
```ruby
|
|
180
182
|
class AccountMailer < ApplicationMailer
|
|
@@ -191,7 +193,7 @@ Action Mailer's framework assigns (including `params` and `rendered_format`) are
|
|
|
191
193
|
|
|
192
194
|
Without `use_react_instance_props`, `react: true` still infers the component and renders it with no props, which is handy for emails that take no props at all.
|
|
193
195
|
|
|
194
|
-
|
|
196
|
+
#### Implicit Component, Explicit Props
|
|
195
197
|
|
|
196
198
|
```ruby
|
|
197
199
|
mail(
|
|
@@ -204,7 +206,7 @@ mail(
|
|
|
204
206
|
)
|
|
205
207
|
```
|
|
206
208
|
|
|
207
|
-
|
|
209
|
+
#### Explicit Component, Explicit Props
|
|
208
210
|
|
|
209
211
|
```ruby
|
|
210
212
|
mail(
|
|
@@ -218,7 +220,81 @@ mail(
|
|
|
218
220
|
)
|
|
219
221
|
```
|
|
220
222
|
|
|
221
|
-
|
|
223
|
+
### Shared Props
|
|
224
|
+
|
|
225
|
+
Use `react_email_share` to share data with all of a mailer's emails and subclasses, and it'll be automatically merged with any inline props.
|
|
226
|
+
|
|
227
|
+
```ruby
|
|
228
|
+
class MarketingMailer < ApplicationMailer
|
|
229
|
+
# Static value
|
|
230
|
+
react_email_share app_name: "Acme"
|
|
231
|
+
|
|
232
|
+
# Lambda value, evaluated lazily in the mailer instance
|
|
233
|
+
react_email_share unread_count: -> { current_user&.unread_count }
|
|
234
|
+
|
|
235
|
+
# Block, evaluated lazily in the mailer instance
|
|
236
|
+
react_email_share do
|
|
237
|
+
{ brand: { name: "Acme", url: marketing_url } }
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Per-mail props win over shared props of the same name, so a mailer can always override what it inherits:
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
mail react: { app_name: "Acme Pro" }, ... # overrides the shared app_name
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Shared props apply to all three forms above (`react:` hash, `react: "component", props:`, and `react: true`).
|
|
249
|
+
|
|
250
|
+
#### Conditional Sharing
|
|
251
|
+
|
|
252
|
+
Pass any `before_action` filter (`only`, `except`, `if`, `unless`) to scope a share to certain actions:
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
react_email_share only: [:welcome, :reactivation] do
|
|
256
|
+
{ promotion: current_promotion }
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
react_email_share if: :user_signed_in? do
|
|
260
|
+
{ user: { name: current_user.name } }
|
|
261
|
+
end
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
You can also share from inside an action, before calling `mail`:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
def welcome
|
|
268
|
+
react_email_share notice: "Thanks for joining!"
|
|
269
|
+
mail react: { account: }, to: account.email, subject: "Welcome"
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
#### Deep Merging
|
|
274
|
+
|
|
275
|
+
By default shared props are merged shallowly, so a per-mail prop replaces a shared one of the same name outright. Pass `deep_merge: true` to merge nested hashes instead:
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
react_email_share do
|
|
279
|
+
{ settings: { theme: "light", locale: "en" } }
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Shallow (default): settings => { theme: "dark" }
|
|
283
|
+
mail react: { settings: { theme: "dark" } }, ...
|
|
284
|
+
|
|
285
|
+
# Deep: settings => { theme: "dark", locale: "en" }
|
|
286
|
+
mail react: { settings: { theme: "dark" } }, deep_merge: true, ...
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
Set `config.deep_merge_shared_props = true` to make deep merging the default for every email. (See [Configuration](#configuration))
|
|
290
|
+
|
|
291
|
+
### Prop Serialization
|
|
292
|
+
|
|
293
|
+
Like `render json:`, `mail react:` accepts any object that responds to `as_json`, including hashes, Active Model objects, and serializers such as [Alba](https://github.com/okuramasafumi/alba) or [ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers).
|
|
294
|
+
|
|
295
|
+
### Prop Transformation
|
|
296
|
+
|
|
297
|
+
Prop keys are camelized by default, so `account.plan_name` arrives as `account.planName`. Override `transform_props` in your [configuration](#configuration).
|
|
222
298
|
|
|
223
299
|
### Component Names
|
|
224
300
|
|
|
@@ -241,14 +317,6 @@ mail react: "users/welcome", props: { user: }, to:, subject:
|
|
|
241
317
|
|
|
242
318
|
Or override `component_path_resolver` globally in your [configuration](#configuration).
|
|
243
319
|
|
|
244
|
-
### Prop Serialization
|
|
245
|
-
|
|
246
|
-
Like `render json:`, `mail react:` accepts any object that responds to `as_json`, including hashes, Active Model objects, and serializers such as [Alba](https://github.com/okuramasafumi/alba) or [ActiveModel::Serializer](https://github.com/rails-api/active_model_serializers).
|
|
247
|
-
|
|
248
|
-
### Prop Transformation
|
|
249
|
-
|
|
250
|
-
Prop keys are camelized by default, so `account.plan_name` arrives as `account.planName`. Override `transform_props` in your [configuration](#configuration).
|
|
251
|
-
|
|
252
320
|
### Layouts
|
|
253
321
|
|
|
254
322
|
Action Mailer layouts aren't applied to `react:` emails. React Email treats layouts like any other component, so share structure with normal React composition instead:
|
|
@@ -314,6 +382,7 @@ If the defaults don't fit, override them in `config/initializers/react_email_rai
|
|
|
314
382
|
| `render_timeout` | `10` seconds |
|
|
315
383
|
| `render_process_max_requests` | `1_000` |
|
|
316
384
|
| `on_render_error` | `nil` |
|
|
385
|
+
| `deep_merge_shared_props` | `false` |
|
|
317
386
|
|
|
318
387
|
#### Prop Transformation
|
|
319
388
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
module ReactEmailRails::ActionMailer
|
|
2
2
|
extend(ActiveSupport::Concern)
|
|
3
3
|
|
|
4
|
+
# `react_email_share` kwargs reserved for `before_action` filtering, not prop data.
|
|
5
|
+
SHARED_FILTER_OPTIONS = [:if, :unless, :only, :except].freeze
|
|
6
|
+
|
|
4
7
|
prepended do
|
|
5
8
|
class_attribute(:react_email_use_instance_props, default: false)
|
|
6
9
|
end
|
|
@@ -9,6 +12,22 @@ module ReactEmailRails::ActionMailer
|
|
|
9
12
|
def use_react_instance_props
|
|
10
13
|
self.react_email_use_instance_props = true
|
|
11
14
|
end
|
|
15
|
+
|
|
16
|
+
# Share props with every `react:` email this mailer and its subclasses render.
|
|
17
|
+
# Mirrors inertia-rails `inertia_share`; `only`/`except`/`if`/`unless` go to `before_action`.
|
|
18
|
+
def react_email_share(hash = nil, **props, &block)
|
|
19
|
+
options = props.slice(*SHARED_FILTER_OPTIONS)
|
|
20
|
+
data = hash || props.except(*SHARED_FILTER_OPTIONS)
|
|
21
|
+
|
|
22
|
+
before_action(**options) do
|
|
23
|
+
react_email_append_shared(data, block)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Share props from within an action, e.g. conditionally before calling `mail`.
|
|
29
|
+
def react_email_share(hash = nil, **props, &block)
|
|
30
|
+
react_email_append_shared(hash || props, block)
|
|
12
31
|
end
|
|
13
32
|
|
|
14
33
|
def mail(headers = {}, &block)
|
|
@@ -17,8 +36,13 @@ module ReactEmailRails::ActionMailer
|
|
|
17
36
|
headers = headers.dup
|
|
18
37
|
react = headers.delete(:react)
|
|
19
38
|
props = headers.delete(:props) if headers.key?(:props)
|
|
39
|
+
deep_merge = headers.delete(:deep_merge) if headers.key?(:deep_merge)
|
|
20
40
|
|
|
21
41
|
component, resolved_props = ReactEmailRails::PropsResolver.new(self).resolve(react, props)
|
|
42
|
+
resolved_props = ReactEmailRails::SharedProps.new(self).merge_into(
|
|
43
|
+
resolved_props,
|
|
44
|
+
deep_merge: react_email_deep_merge?(deep_merge),
|
|
45
|
+
)
|
|
22
46
|
render_options = ReactEmailRails.configuration.resolve_render_options(self)
|
|
23
47
|
rendered = ReactEmailRails.render(component:, props: resolved_props, render_options:)
|
|
24
48
|
|
|
@@ -28,4 +52,18 @@ module ReactEmailRails::ActionMailer
|
|
|
28
52
|
yield(format) if block
|
|
29
53
|
end
|
|
30
54
|
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def react_email_append_shared(data, block)
|
|
59
|
+
store = (@_react_email_shared ||= [])
|
|
60
|
+
store << data.dup.freeze if data.present?
|
|
61
|
+
store << block if block
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def react_email_deep_merge?(override)
|
|
65
|
+
return ReactEmailRails.configuration.deep_merge_shared_props if override.nil?
|
|
66
|
+
|
|
67
|
+
override
|
|
68
|
+
end
|
|
31
69
|
end
|
|
@@ -34,6 +34,7 @@ class ReactEmailRails::Configuration
|
|
|
34
34
|
:render_options,
|
|
35
35
|
:transform_props,
|
|
36
36
|
:on_render_error,
|
|
37
|
+
:deep_merge_shared_props,
|
|
37
38
|
)
|
|
38
39
|
|
|
39
40
|
attr_reader(
|
|
@@ -52,6 +53,7 @@ class ReactEmailRails::Configuration
|
|
|
52
53
|
config.render_process_max_requests = DEFAULT_RENDER_PROCESS_MAX_REQUESTS
|
|
53
54
|
config.transform_props = :lower_camel
|
|
54
55
|
config.on_render_error = nil
|
|
56
|
+
config.deep_merge_shared_props = false
|
|
55
57
|
end
|
|
56
58
|
end
|
|
57
59
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Collects props registered with `react_email_share` and merges them beneath the
|
|
2
|
+
# per-mail props, which win on conflict. Mirrors inertia-rails shared data.
|
|
3
|
+
class ReactEmailRails::SharedProps
|
|
4
|
+
IVAR = :@_react_email_shared
|
|
5
|
+
|
|
6
|
+
def initialize(mailer)
|
|
7
|
+
@mailer = mailer
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Returns `props` untouched when nothing is shared, so non-Hash inputs (e.g.
|
|
11
|
+
# serializers) still flow straight through to serialization.
|
|
12
|
+
def merge_into(props, deep_merge:)
|
|
13
|
+
shared = to_h
|
|
14
|
+
return props if shared.empty?
|
|
15
|
+
|
|
16
|
+
base = shared.as_json
|
|
17
|
+
incoming = props.as_json
|
|
18
|
+
deep_merge ? base.deep_merge(incoming) : base.merge(incoming)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_h
|
|
22
|
+
entries.each_with_object({}) do |entry, props|
|
|
23
|
+
props.merge!(resolve(entry))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader(:mailer)
|
|
30
|
+
|
|
31
|
+
def entries
|
|
32
|
+
mailer.instance_variable_get(IVAR) || []
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# An entry is either a static Hash or a block evaluated at render time. Callable
|
|
36
|
+
# values inside a Hash are evaluated too, so `unread_count: -> { ... }` works.
|
|
37
|
+
def resolve(entry)
|
|
38
|
+
hash = entry.respond_to?(:call) ? mailer.instance_exec(&entry) : entry
|
|
39
|
+
|
|
40
|
+
(hash || {}).transform_values do |value|
|
|
41
|
+
value.respond_to?(:call) ? mailer.instance_exec(&value) : value
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/react_email_rails.rb
CHANGED
|
@@ -5,6 +5,7 @@ require("active_support/concern")
|
|
|
5
5
|
require("active_support/notifications")
|
|
6
6
|
require("active_support/core_ext/object/blank")
|
|
7
7
|
require("active_support/core_ext/object/json")
|
|
8
|
+
require("active_support/core_ext/hash/deep_merge")
|
|
8
9
|
require("active_support/inflector")
|
|
9
10
|
require("rails/railtie")
|
|
10
11
|
|
|
@@ -24,6 +25,7 @@ require_relative("react_email_rails/render_modes/persistent/command_runner")
|
|
|
24
25
|
require_relative("react_email_rails/configuration")
|
|
25
26
|
require_relative("react_email_rails/tasks")
|
|
26
27
|
require_relative("react_email_rails/props_resolver")
|
|
28
|
+
require_relative("react_email_rails/shared_props")
|
|
27
29
|
require_relative("react_email_rails/railtie")
|
|
28
30
|
|
|
29
31
|
module ReactEmailRails
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: react-email-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Supertape
|
|
@@ -164,6 +164,7 @@ files:
|
|
|
164
164
|
- lib/react_email_rails/render_modes/subprocess/command_runner.rb
|
|
165
165
|
- lib/react_email_rails/render_protocol.rb
|
|
166
166
|
- lib/react_email_rails/rendered_email.rb
|
|
167
|
+
- lib/react_email_rails/shared_props.rb
|
|
167
168
|
- lib/react_email_rails/tasks.rb
|
|
168
169
|
- lib/react_email_rails/version.rb
|
|
169
170
|
- lib/tasks/react_email_rails/build.rake
|