phlex-hanami 0.1.0 → 0.2.0.pre.alpha.2
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 +46 -2
- data/{LICENSE.txt → LICENSE} +6 -6
- data/lib/phlex/hanami/context.rb +184 -0
- data/lib/phlex/hanami/errors/error.rb +12 -0
- data/lib/phlex/hanami/errors/mailer_view_error.rb +30 -0
- data/lib/phlex/hanami/errors/missing_context_error.rb +24 -0
- data/lib/phlex/hanami/errors/relative_path_error.rb +26 -0
- data/lib/phlex/hanami/extensions/mailer.rb +102 -0
- data/lib/phlex/hanami/extensions/slice.rb +117 -0
- data/lib/phlex/hanami/helpers.rb +62 -3
- data/lib/phlex/hanami/layout.rb +39 -0
- data/lib/phlex/hanami/mailer/layout.rb +44 -0
- data/lib/phlex/hanami/mailer/renderable.rb +129 -0
- data/lib/phlex/hanami/mailer/text.rb +124 -0
- data/lib/phlex/hanami/mailer/view.rb +38 -0
- data/lib/phlex/hanami/mailer.rb +47 -0
- data/lib/phlex/hanami/renderable.rb +379 -0
- data/lib/phlex/hanami/slice_configured.rb +50 -0
- data/lib/phlex/hanami/slice_configured_context.rb +70 -0
- data/lib/phlex/hanami/view.rb +39 -0
- data/lib/phlex/hanami.rb +40 -6
- data/lib/phlex-hanami.rb +3 -0
- data/sig/phlex-hanami.rbs +2 -0
- metadata +58 -29
- data/.rspec +0 -3
- data/.standard.yml +0 -5
- data/CODE_OF_CONDUCT.md +0 -132
- data/README.md +0 -43
- data/Rakefile +0 -10
- data/lib/phlex/hanami/assets_helper.rb +0 -23
- data/lib/phlex/hanami/context_helper.rb +0 -21
- data/lib/phlex/hanami/version.rb +0 -7
- data/sig/phlex/hanami.rbs +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eb2ded5cb201e8636daeba76f21fc25b297899eba517ce428d6fcc41450460f9
|
|
4
|
+
data.tar.gz: f8935646735d5d5e52faf6c48b5713dae2f4169c02e8162fc4356c32e5cf09bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 396360b1808f053b39c29047103c6f860c46ac6bf436890a483848c63639c1e382fc0ad34eefb61f58335a4d158783349670c2b0ee30a421473737c5111e2abf
|
|
7
|
+
data.tar.gz: 84fbc91b5c45c6ece40da01070cd64bd9b2702cbc19c10c1ba8eeb3d27cc7aab57e3e60434305b18021dc980cc0a1fff983a051d5cce33b09b200fd87667db52
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
1
8
|
## [Unreleased]
|
|
2
9
|
|
|
3
|
-
## [
|
|
10
|
+
## [v0.2.0-alpha.2] - 2026-09-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Phlex views for [hanami-mailer]. A mailer renders the Phlex class whose container key matches its own, so
|
|
15
|
+
`MyApp::Mailers::Welcome` renders `MyApp::Views::Mailers::Welcome` with no configuration.
|
|
16
|
+
- `Phlex::Hanami::Mailer::View` and `Phlex::Hanami::Mailer::Layout`, and `Phlex::Hanami::Mailer::Renderable` for an
|
|
17
|
+
existing `Phlex::HTML` base class.
|
|
18
|
+
- A plain text alternative part, converted from the rendered HTML by `Phlex::Hanami::Mailer::Text`. Override
|
|
19
|
+
`text_body` on the view to write it by hand.
|
|
20
|
+
- A mail layout convention: `Views::Mailers::Layout` in a slice wraps that slice's mail views.
|
|
21
|
+
- Usage documentation under [docs/usage](docs/usage/README.md), covering views, layouts, the view context,
|
|
22
|
+
helpers, mailers and how the gem hooks into Hanami.
|
|
23
|
+
- `Phlex::Hanami::MailerViewError`, raised when a mailer is paired with a Phlex view that is not a mail view,
|
|
24
|
+
rather than sending a message whose plain text part is markup.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- `url` in a view returns a String. Hanami's routes helper returns a `URI`, which Phlex rejects as an attribute
|
|
29
|
+
value, so `a(href: url(:posts))` raised.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- `path` in a mail view raises `Phlex::Hanami::RelativePathError` instead of writing a relative link an email client
|
|
34
|
+
cannot follow.
|
|
35
|
+
|
|
36
|
+
[hanami-mailer]: https://github.com/hanami/mailer
|
|
37
|
+
|
|
38
|
+
## [v0.2.0-alpha.1] - 2026-09-04
|
|
39
|
+
|
|
40
|
+
Initial alpha release by the new maintainer [@aaronmallen](https://github.com/aaronmallen).
|
|
41
|
+
|
|
42
|
+
## [v0.1.0] - 2024-12-07
|
|
43
|
+
|
|
44
|
+
Initial release, by the previous maintainer [@stephannv](https://github.com/stephannv).
|
|
4
45
|
|
|
5
|
-
-
|
|
46
|
+
[Unreleased]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.2...HEAD
|
|
47
|
+
[v0.2.0-alpha.2]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.1...0.2.0-alpha.2
|
|
48
|
+
[v0.2.0-alpha.1]: https://github.com/aaronmallen/phlex-hanami/releases/tag/0.2.0-alpha.1
|
|
49
|
+
[v0.1.0]: https://github.com/stephannv/phlex-hanami/releases/tag/v0.1.0
|
data/{LICENSE.txt → LICENSE}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c)
|
|
3
|
+
Copyright (c) 2026 Aaron Allen
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
9
9
|
copies of the Software, and to permit persons to whom the Software is
|
|
10
10
|
furnished to do so, subject to the following conditions:
|
|
11
11
|
|
|
12
|
-
The above copyright notice and this permission notice shall be included in
|
|
13
|
-
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
14
|
|
|
15
15
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
16
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
17
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
18
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
19
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
-
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Hanami
|
|
5
|
+
# The view context used when the hanami-view gem is not bundled.
|
|
6
|
+
#
|
|
7
|
+
# Hanami builds a context once per request and hands it to the view as `context:`. When
|
|
8
|
+
# hanami-view is bundled that context is a `Hanami::View::Context` and this class is unused;
|
|
9
|
+
# otherwise phlex-hanami defines a `Views::Context` subclass of this in every slice.
|
|
10
|
+
#
|
|
11
|
+
# The public surface mirrors `Hanami::View::Context` so that a view reads the same either way.
|
|
12
|
+
#
|
|
13
|
+
# @api public
|
|
14
|
+
# @since 0.2.0
|
|
15
|
+
class Context
|
|
16
|
+
extend ::Hanami::SliceConfigurable
|
|
17
|
+
|
|
18
|
+
# @api private
|
|
19
|
+
# @since 0.2.0
|
|
20
|
+
def self.configure_for_slice(slice)
|
|
21
|
+
extend SliceConfiguredContext.new(slice)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The app's inflector.
|
|
25
|
+
#
|
|
26
|
+
# @return [Dry::Inflector]
|
|
27
|
+
#
|
|
28
|
+
# @api public
|
|
29
|
+
# @since 0.2.0
|
|
30
|
+
attr_reader :inflector
|
|
31
|
+
|
|
32
|
+
# @see SliceConfiguredContext#define_new
|
|
33
|
+
#
|
|
34
|
+
# @api private
|
|
35
|
+
# @since 0.2.0
|
|
36
|
+
def initialize(inflector: nil, routes: nil, assets: nil, request: nil, i18n: nil, **)
|
|
37
|
+
@inflector = inflector
|
|
38
|
+
@routes = routes
|
|
39
|
+
@assets = assets
|
|
40
|
+
@request = request
|
|
41
|
+
@i18n = i18n
|
|
42
|
+
@content_for = {}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The slice's assets.
|
|
46
|
+
#
|
|
47
|
+
# @return [Hanami::Assets]
|
|
48
|
+
#
|
|
49
|
+
# @raise [Hanami::ComponentLoadError] when hanami-assets is not bundled, or no assets exist
|
|
50
|
+
#
|
|
51
|
+
# @api public
|
|
52
|
+
# @since 0.2.0
|
|
53
|
+
def assets
|
|
54
|
+
raise ::Hanami::ComponentLoadError, "Assets not available. #{assets_hint}" unless @assets
|
|
55
|
+
|
|
56
|
+
@assets
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Store a string of markup for later use, or read back what was stored.
|
|
60
|
+
#
|
|
61
|
+
# @param key [Symbol] the content key
|
|
62
|
+
# @param value [String, nil] the content, when no block is given
|
|
63
|
+
#
|
|
64
|
+
# @return [String, nil]
|
|
65
|
+
#
|
|
66
|
+
# @api public
|
|
67
|
+
# @since 0.2.0
|
|
68
|
+
def content_for(key, value = nil)
|
|
69
|
+
if block_given?
|
|
70
|
+
@content_for[key] = yield
|
|
71
|
+
nil
|
|
72
|
+
elsif value
|
|
73
|
+
@content_for[key] = value
|
|
74
|
+
nil
|
|
75
|
+
else
|
|
76
|
+
@content_for[key]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The current request's CSRF token.
|
|
81
|
+
#
|
|
82
|
+
# @return [String]
|
|
83
|
+
#
|
|
84
|
+
# @raise [Hanami::ComponentLoadError] when there is no request
|
|
85
|
+
#
|
|
86
|
+
# @api public
|
|
87
|
+
# @since 0.2.0
|
|
88
|
+
def csrf_token
|
|
89
|
+
request.session[::Hanami::Action::CSRFProtection::CSRF_TOKEN]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The flash hash for the current request.
|
|
93
|
+
#
|
|
94
|
+
# @raise [Hanami::ComponentLoadError] when there is no request
|
|
95
|
+
#
|
|
96
|
+
# @api public
|
|
97
|
+
# @since 0.2.0
|
|
98
|
+
def flash
|
|
99
|
+
request.flash
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# The slice's i18n backend.
|
|
103
|
+
#
|
|
104
|
+
# @raise [Hanami::ComponentLoadError] when the i18n gem is not bundled
|
|
105
|
+
#
|
|
106
|
+
# @api public
|
|
107
|
+
# @since 0.2.0
|
|
108
|
+
def i18n
|
|
109
|
+
raise ::Hanami::ComponentLoadError, "the i18n gem is required to access translations" unless @i18n
|
|
110
|
+
|
|
111
|
+
@i18n
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @api private
|
|
115
|
+
# @since 0.2.0
|
|
116
|
+
def initialize_copy(source)
|
|
117
|
+
super
|
|
118
|
+
@content_for = source.instance_variable_get(:@content_for).dup
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The current request, when the view is rendered from an action.
|
|
122
|
+
#
|
|
123
|
+
# @return [Hanami::Action::Request]
|
|
124
|
+
#
|
|
125
|
+
# @raise [Hanami::ComponentLoadError] when the view is not rendered from a request
|
|
126
|
+
#
|
|
127
|
+
# @api public
|
|
128
|
+
# @since 0.2.0
|
|
129
|
+
def request
|
|
130
|
+
unless @request
|
|
131
|
+
raise ::Hanami::ComponentLoadError, <<~MESSAGE
|
|
132
|
+
Request not available. Only views rendered from Hanami::Action instances have a request.
|
|
133
|
+
MESSAGE
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
@request
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Whether a request is available.
|
|
140
|
+
#
|
|
141
|
+
# @return [Boolean]
|
|
142
|
+
#
|
|
143
|
+
# @api public
|
|
144
|
+
# @since 0.2.0
|
|
145
|
+
def request?
|
|
146
|
+
!!@request
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# The app's routes helper.
|
|
150
|
+
#
|
|
151
|
+
# @return [Hanami::Slice::RoutesHelper]
|
|
152
|
+
#
|
|
153
|
+
# @raise [Hanami::ComponentLoadError] when hanami-router is not bundled or routes are undefined
|
|
154
|
+
#
|
|
155
|
+
# @api public
|
|
156
|
+
# @since 0.2.0
|
|
157
|
+
def routes
|
|
158
|
+
raise ::Hanami::ComponentLoadError, "the hanami-router gem is required to access routes" unless @routes
|
|
159
|
+
|
|
160
|
+
@routes
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# The session for the current request.
|
|
164
|
+
#
|
|
165
|
+
# @raise [Hanami::ComponentLoadError] when there is no request
|
|
166
|
+
#
|
|
167
|
+
# @api public
|
|
168
|
+
# @since 0.2.0
|
|
169
|
+
def session
|
|
170
|
+
request.session
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private
|
|
174
|
+
|
|
175
|
+
def assets_hint
|
|
176
|
+
if ::Hanami.bundled?("hanami-assets")
|
|
177
|
+
"Have you put files into your assets directory?"
|
|
178
|
+
else
|
|
179
|
+
"The hanami-assets gem is required to access assets."
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Hanami
|
|
5
|
+
# Raised when a mailer's paired Phlex view cannot render a mail message.
|
|
6
|
+
#
|
|
7
|
+
# A mailer asks its view for two parts, and an ordinary view answers both with markup — which
|
|
8
|
+
# would ship an email whose plain text part is HTML. Better to say so than to send it.
|
|
9
|
+
#
|
|
10
|
+
# @api public
|
|
11
|
+
# @since 0.2.0
|
|
12
|
+
class MailerViewError < Error
|
|
13
|
+
# @api private
|
|
14
|
+
# @since 0.2.0
|
|
15
|
+
def initialize(mailer_class, view_class)
|
|
16
|
+
super(<<~MESSAGE)
|
|
17
|
+
#{mailer_class} is paired with #{view_class}, which is not a mail view.
|
|
18
|
+
|
|
19
|
+
A mail view renders both parts of the message. Subclass the mail base class:
|
|
20
|
+
|
|
21
|
+
class #{view_class} < Phlex::Hanami::Mailer::View
|
|
22
|
+
|
|
23
|
+
or include the module into the base class you already have:
|
|
24
|
+
|
|
25
|
+
include Phlex::Hanami::Mailer::Renderable
|
|
26
|
+
MESSAGE
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Hanami
|
|
5
|
+
# Raised when a view reaches for the Hanami view context but was rendered without one.
|
|
6
|
+
#
|
|
7
|
+
# @api public
|
|
8
|
+
# @since 0.2.0
|
|
9
|
+
class MissingContextError < Error
|
|
10
|
+
# @api private
|
|
11
|
+
# @since 0.2.0
|
|
12
|
+
def initialize(view_class)
|
|
13
|
+
super(<<~MESSAGE)
|
|
14
|
+
#{view_class} was rendered without a Hanami view context.
|
|
15
|
+
|
|
16
|
+
Views rendered from an action are given one automatically. When rendering a view yourself,
|
|
17
|
+
pass one:
|
|
18
|
+
|
|
19
|
+
#{view_class}.call(context: MyApp::Views::Context.new)
|
|
20
|
+
MESSAGE
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Hanami
|
|
5
|
+
# Raised when a mail view asks for a relative path.
|
|
6
|
+
#
|
|
7
|
+
# An email is read outside the app, so a relative path in one is a dead link. Mail views raise
|
|
8
|
+
# rather than render one.
|
|
9
|
+
#
|
|
10
|
+
# @api public
|
|
11
|
+
# @since 0.2.0
|
|
12
|
+
class RelativePathError < Error
|
|
13
|
+
# @api private
|
|
14
|
+
# @since 0.2.0
|
|
15
|
+
def initialize(view_class)
|
|
16
|
+
super(<<~MESSAGE)
|
|
17
|
+
#{view_class} asked for a relative path, and an email needs a full URL.
|
|
18
|
+
|
|
19
|
+
Use `url` instead:
|
|
20
|
+
|
|
21
|
+
a(href: url(:post, id: @post.id)) { @post.title }
|
|
22
|
+
MESSAGE
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Hanami
|
|
5
|
+
module Extensions
|
|
6
|
+
# Pairs a Hanami mailer with a Phlex view.
|
|
7
|
+
#
|
|
8
|
+
# Prepended onto `Hanami::Mailer`, so it sits in front of hanami-mailer's own view handling
|
|
9
|
+
# and answers first. A mailer's view is any object taking `call(format:, **input)`, which a
|
|
10
|
+
# {Phlex::Hanami::Mailer::View} already is, so pairing is the whole of the integration:
|
|
11
|
+
# nothing here teaches hanami-mailer what Phlex is.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
# @since 0.2.0
|
|
15
|
+
module Mailer
|
|
16
|
+
# Prepends this onto `Hanami::Mailer`.
|
|
17
|
+
#
|
|
18
|
+
# Called from `phlex/hanami.rb` when hanami-mailer is bundled. Zeitwerk keeps the mail
|
|
19
|
+
# classes unloaded until something names one, but a prepend needs the constant, so the
|
|
20
|
+
# check belongs at the call site.
|
|
21
|
+
#
|
|
22
|
+
# @api private
|
|
23
|
+
# @since 0.2.0
|
|
24
|
+
def self.install
|
|
25
|
+
::Hanami::Mailer.prepend(self)
|
|
26
|
+
::Hanami::Mailer.singleton_class.prepend(ClassMethods)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @api private
|
|
30
|
+
# @since 0.2.0
|
|
31
|
+
module ClassMethods
|
|
32
|
+
# @api private
|
|
33
|
+
# @since 0.2.0
|
|
34
|
+
def configure_for_slice(slice)
|
|
35
|
+
super
|
|
36
|
+
extend SliceConfigured.new(slice)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The mailer's paired Phlex view, or nil when it has none.
|
|
40
|
+
#
|
|
41
|
+
# The key is derived the way hanami-mailer derives a template name: underscore the class
|
|
42
|
+
# name and drop the slice's own segment, so `MyApp::Mailers::Welcome` looks up
|
|
43
|
+
# `views.mailers.welcome`. Resolved through the container on every call rather than
|
|
44
|
+
# memoized, so code reloading is not defeated.
|
|
45
|
+
#
|
|
46
|
+
# @return [Class, nil]
|
|
47
|
+
#
|
|
48
|
+
# @raise [MailerViewError] when the paired view is a Phlex class that cannot render mail
|
|
49
|
+
#
|
|
50
|
+
# @api private
|
|
51
|
+
# @since 0.2.0
|
|
52
|
+
def phlex_view
|
|
53
|
+
mailer_slice = slice
|
|
54
|
+
return nil unless mailer_slice && name
|
|
55
|
+
|
|
56
|
+
key = "views.#{view_name(mailer_slice)}"
|
|
57
|
+
return nil unless mailer_slice.key?(key)
|
|
58
|
+
|
|
59
|
+
view = mailer_slice[key]
|
|
60
|
+
return nil unless view.is_a?(::Class) && view < ::Phlex::SGML
|
|
61
|
+
raise MailerViewError.new(self, view) unless view < ::Phlex::Hanami::Mailer::Renderable
|
|
62
|
+
|
|
63
|
+
view
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The slice this mailer belongs to, or nil when it is defined outside a slice namespace.
|
|
67
|
+
#
|
|
68
|
+
# @return [Hanami::Slice, nil]
|
|
69
|
+
#
|
|
70
|
+
# @api private
|
|
71
|
+
# @since 0.2.0
|
|
72
|
+
def slice
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def view_name(mailer_slice)
|
|
79
|
+
mailer_slice.inflector
|
|
80
|
+
.underscore(name)
|
|
81
|
+
.sub(%r{^#{mailer_slice.slice_name.path}/}, "")
|
|
82
|
+
.tr("/", ".")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The view this mailer renders.
|
|
87
|
+
#
|
|
88
|
+
# A view passed to the constructor wins, then the paired Phlex view, then whatever
|
|
89
|
+
# hanami-mailer would have used on its own — so an app can render some of its mail with
|
|
90
|
+
# Phlex and the rest with templates.
|
|
91
|
+
#
|
|
92
|
+
# @return [Object, nil]
|
|
93
|
+
#
|
|
94
|
+
# @api private
|
|
95
|
+
# @since 0.2.0
|
|
96
|
+
def view
|
|
97
|
+
@view || self.class.phlex_view || super
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Hanami
|
|
5
|
+
module Extensions
|
|
6
|
+
# Installs phlex-hanami into every slice, including the app.
|
|
7
|
+
#
|
|
8
|
+
# Prepended onto `Hanami::Slice::ClassMethods` so that it runs for the app and each of its
|
|
9
|
+
# slices as they are prepared. Two things happen:
|
|
10
|
+
#
|
|
11
|
+
# 1. Phlex classes are registered in the slice container as classes rather than instances.
|
|
12
|
+
# `Dry::System` would otherwise call `new` on them while resolving, which raises for any
|
|
13
|
+
# view with a real initializer, and a memoized Phlex instance can only render once.
|
|
14
|
+
# 2. A `Views::Context` is defined for the slice when hanami-view is not bundled, filling the
|
|
15
|
+
# third-party slot `Hanami::Extensions::Action::SliceConfiguredAction#view_context_class`
|
|
16
|
+
# looks in.
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
# @since 0.2.0
|
|
20
|
+
module Slice
|
|
21
|
+
# Returns Phlex classes as classes, and everything else as the loader would.
|
|
22
|
+
#
|
|
23
|
+
# `component.loader.constant` is what the default autoloading loader already calls, so no
|
|
24
|
+
# extra loading happens here and a constant that fails to load still raises
|
|
25
|
+
# `Dry::System::ComponentNotLoadableError`.
|
|
26
|
+
#
|
|
27
|
+
# @api private
|
|
28
|
+
# @since 0.2.0
|
|
29
|
+
COMPONENT_INSTANCE = proc { |component, *args, **kwargs|
|
|
30
|
+
constant = component.loader.constant(component)
|
|
31
|
+
|
|
32
|
+
if constant.is_a?(Class) && constant < Phlex::SGML
|
|
33
|
+
constant
|
|
34
|
+
else
|
|
35
|
+
component.loader.call(component, *args, **kwargs)
|
|
36
|
+
end
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# @api private
|
|
40
|
+
# @since 0.2.0
|
|
41
|
+
def prepare(provider_name = nil)
|
|
42
|
+
result = super
|
|
43
|
+
|
|
44
|
+
unless provider_name
|
|
45
|
+
Slice.register_phlex_components(self)
|
|
46
|
+
Slice.define_view_context(self)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
result
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class << self
|
|
53
|
+
# Defines `Views::Context` for the slice when nothing else has.
|
|
54
|
+
#
|
|
55
|
+
# Does nothing when hanami-view is bundled, because Hanami defines its own richer context
|
|
56
|
+
# there, and returns the existing one when the user has defined it themselves.
|
|
57
|
+
#
|
|
58
|
+
# @return [Class, nil] the slice's context class
|
|
59
|
+
#
|
|
60
|
+
# @api private
|
|
61
|
+
# @since 0.2.0
|
|
62
|
+
def define_view_context(slice)
|
|
63
|
+
return if ::Hanami.bundled?("hanami-view")
|
|
64
|
+
|
|
65
|
+
namespace = views_namespace(slice)
|
|
66
|
+
return namespace.const_get(:Context, false) if namespace.const_defined?(:Context, false)
|
|
67
|
+
|
|
68
|
+
# The class is anonymous at this point, so the slice cannot be inferred from its name
|
|
69
|
+
# the way `Hanami::SliceConfigurable` would; configure it explicitly instead.
|
|
70
|
+
namespace.const_set(:Context, ::Class.new(Context).tap { |klass| klass.configure_for_slice(slice) })
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Installs {COMPONENT_INSTANCE} as the default for the slice container's component dirs.
|
|
74
|
+
#
|
|
75
|
+
# `Dry::System::Config::ComponentDirs` re-applies its defaults every time the dirs are
|
|
76
|
+
# read, and only where a dir has not configured the setting itself, so setting this after
|
|
77
|
+
# the dirs have been added covers all of them and clobbers nothing.
|
|
78
|
+
#
|
|
79
|
+
# @api private
|
|
80
|
+
# @since 0.2.0
|
|
81
|
+
def register_phlex_components(slice)
|
|
82
|
+
component_dirs = slice.container.config.component_dirs
|
|
83
|
+
return if component_dirs.configured?(:instance)
|
|
84
|
+
|
|
85
|
+
component_dirs.instance = COMPONENT_INSTANCE
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The class an action's view context is built from, for anything rendering outside a
|
|
89
|
+
# request that needs the same one.
|
|
90
|
+
#
|
|
91
|
+
# Reads the third-party slot `Hanami::Extensions::Action::SliceConfiguredAction` reads,
|
|
92
|
+
# so a mail view and a request view are given the same class.
|
|
93
|
+
#
|
|
94
|
+
# @return [Class]
|
|
95
|
+
#
|
|
96
|
+
# @api private
|
|
97
|
+
# @since 0.2.0
|
|
98
|
+
def view_context_class(slice)
|
|
99
|
+
return ::Hanami::Extensions::View::Context.context_class(slice) if ::Hanami.bundled?("hanami-view")
|
|
100
|
+
|
|
101
|
+
define_view_context(slice)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def views_namespace(slice)
|
|
107
|
+
if slice.namespace.const_defined?(:Views, false)
|
|
108
|
+
slice.namespace.const_get(:Views, false)
|
|
109
|
+
else
|
|
110
|
+
slice.namespace.const_set(:Views, ::Module.new)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
data/lib/phlex/hanami/helpers.rb
CHANGED
|
@@ -1,9 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "hanami/view"
|
|
4
|
+
require "hanami/extensions/view/standard_helpers"
|
|
5
|
+
|
|
6
|
+
# Hanami's helpers return `SafeString`s meant to be interpolated into a template. Marking the class
|
|
7
|
+
# as a Phlex safe object lets Phlex emit them without escaping — both through `raw` and, more often,
|
|
8
|
+
# as the return value of a block.
|
|
9
|
+
Hanami::View::HTML::SafeString.include(Phlex::SGML::SafeObject)
|
|
10
|
+
|
|
1
11
|
module Phlex
|
|
2
12
|
module Hanami
|
|
13
|
+
# Hanami's standard helper library, for Phlex views that want it.
|
|
14
|
+
#
|
|
15
|
+
# Opt in per base class; it is not included in {View}, because it adds around twenty methods and
|
|
16
|
+
# needs the hanami-view gem, and neither should depend on what happens to be in your Gemfile.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# module MyApp
|
|
20
|
+
# class View < Phlex::Hanami::View
|
|
21
|
+
# include Phlex::Hanami::Helpers
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# class Edit < MyApp::View
|
|
26
|
+
# def view_template
|
|
27
|
+
# raw form_for("post", path(:posts)) { |f| f.text_field(:title) + f.submit("Save") }
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
#
|
|
31
|
+
# Most of Hanami's helpers duplicate something Phlex already does better — Phlex escapes by
|
|
32
|
+
# default and is itself a tag builder — so reach for `form_for`, `format_number` and the asset
|
|
33
|
+
# helpers, and write everything else as ordinary Phlex.
|
|
34
|
+
#
|
|
35
|
+
# @api public
|
|
36
|
+
# @since 0.2.0
|
|
3
37
|
module Helpers
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
38
|
+
# Hanami's helper library defines two methods Phlex already owns.
|
|
39
|
+
#
|
|
40
|
+
# Both of Hanami's return strings to interpolate; Phlex's write into the output buffer.
|
|
41
|
+
# Letting Hanami's win breaks `raw` silently — the helper's output simply never appears — and
|
|
42
|
+
# `raw` is the method you need in order to emit a helper's result at all.
|
|
43
|
+
#
|
|
44
|
+
# @api private
|
|
45
|
+
# @since 0.2.0
|
|
46
|
+
OVERRIDDEN_METHODS = {
|
|
47
|
+
raw: Phlex::SGML.instance_method(:raw),
|
|
48
|
+
tag: Phlex::HTML.instance_method(:tag),
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
51
|
+
# Phlex's implementations, mixed back in over Hanami's.
|
|
52
|
+
#
|
|
53
|
+
# @api private
|
|
54
|
+
# @since 0.2.0
|
|
55
|
+
PhlexMethods = ::Module.new do
|
|
56
|
+
OVERRIDDEN_METHODS.each do |name, method|
|
|
57
|
+
define_method(name) { |*args, **options, &block| method.bind_call(self, *args, **options, &block) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @api private
|
|
62
|
+
# @since 0.2.0
|
|
63
|
+
def self.included(view_class)
|
|
64
|
+
view_class.include(::Hanami::Extensions::View::StandardHelpers)
|
|
65
|
+
view_class.include(PhlexMethods)
|
|
7
66
|
end
|
|
8
67
|
end
|
|
9
68
|
end
|