phlex-hanami 0.2.0.pre.alpha.1 → 0.2.0.pre.alpha.3
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 +56 -2
- data/lib/phlex/hanami/component.rb +41 -0
- data/lib/phlex/hanami/context.rb +21 -1
- data/lib/phlex/hanami/contextual.rb +294 -0
- data/lib/phlex/hanami/errors/mailer_view_error.rb +31 -0
- data/lib/phlex/hanami/errors/missing_context_error.rb +1 -0
- data/lib/phlex/hanami/errors/relative_path_error.rb +27 -0
- data/lib/phlex/hanami/extensions/mailer.rb +108 -0
- data/lib/phlex/hanami/extensions/slice.rb +32 -5
- data/lib/phlex/hanami/helpers.rb +3 -2
- data/lib/phlex/hanami/mailer/layout.rb +44 -0
- data/lib/phlex/hanami/mailer/renderable.rb +135 -0
- data/lib/phlex/hanami/mailer/text.rb +137 -0
- data/lib/phlex/hanami/mailer/view.rb +38 -0
- data/lib/phlex/hanami/mailer.rb +47 -0
- data/lib/phlex/hanami/renderable.rb +14 -231
- data/lib/phlex/hanami/rspec.rb +14 -0
- data/lib/phlex/hanami/{slice_configured_view.rb → slice_configured.rb} +11 -7
- data/lib/phlex/hanami/slice_configured_context.rb +9 -1
- data/lib/phlex/hanami/testing.rb +156 -0
- data/lib/phlex/hanami.rb +7 -1
- data/sig/external/dry.rbs +9 -0
- data/sig/external/hanami.rbs +77 -0
- data/sig/external/hanami_view.rbs +9 -0
- data/sig/external/phlex.rbs +16 -0
- data/sig/phlex/hanami/component.rbs +9 -0
- data/sig/phlex/hanami/context.rbs +39 -0
- data/sig/phlex/hanami/contextual.rbs +57 -0
- data/sig/phlex/hanami/errors/error.rbs +8 -0
- data/sig/phlex/hanami/errors/mailer_view_error.rbs +9 -0
- data/sig/phlex/hanami/errors/missing_context_error.rbs +9 -0
- data/sig/phlex/hanami/errors/relative_path_error.rbs +9 -0
- data/sig/phlex/hanami/extensions/mailer.rbs +25 -0
- data/sig/phlex/hanami/extensions/slice.rbs +21 -0
- data/sig/phlex/hanami/helpers.rbs +13 -0
- data/sig/phlex/hanami/layout.rbs +9 -0
- data/sig/phlex/hanami/mailer/layout.rbs +11 -0
- data/sig/phlex/hanami/mailer/renderable.rbs +27 -0
- data/sig/phlex/hanami/mailer/text.rbs +47 -0
- data/sig/phlex/hanami/mailer/view.rbs +11 -0
- data/sig/phlex/hanami/mailer.rbs +8 -0
- data/sig/phlex/hanami/renderable.rbs +27 -0
- data/sig/phlex/hanami/rspec.rbs +1 -0
- data/sig/phlex/hanami/slice_configured.rbs +19 -0
- data/sig/phlex/hanami/slice_configured_context.rbs +27 -0
- data/sig/phlex/hanami/testing.rbs +19 -0
- data/sig/phlex/hanami/view.rbs +9 -0
- data/sig/phlex/hanami.rbs +7 -0
- data/sig/phlex-hanami.rbs +1 -2
- metadata +41 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4adab6a5900c422ff7da10ee21be828ba77c1d7523da39173433ff7749516e73
|
|
4
|
+
data.tar.gz: d44d102a14b331b1262f692f8479418d8f08557815830899c3fb9be093f34e25
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 151ad52da0f7b28599a61385974233e109be3a195710f3d2d971783c25202d830cc04b7d85fb8f19c90c6058c2798f0f8e85bc4bc3a81d86003ec242884fb939
|
|
7
|
+
data.tar.gz: 85e34fbfb79b7cb58352575c5d1cdb18066f4034a427a3d55753992b7f5653d3a348e27c98f593b5db2f56f711fc90755965d5c0470a74f0b467192f6368c833
|
data/CHANGELOG.md
CHANGED
|
@@ -7,7 +7,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [v0.2.0-alpha.
|
|
10
|
+
## [v0.2.0-alpha.3] - 2026-09-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `Phlex::Hanami::Component`, the base class for everything below a view. It reads the same view context a view
|
|
15
|
+
does, and has neither a layout nor the `call` contract Hanami renders a view through, so Hanami cannot render one
|
|
16
|
+
by accident.
|
|
17
|
+
- `Phlex::Hanami::Contextual`, the view context and the slice on their own, for a component base class you already
|
|
18
|
+
have. `Phlex::Hanami::Renderable` includes it and adds the view half.
|
|
19
|
+
- Support for [Phlex kits]. Extend a component namespace with `Phlex::Kit`, include it in a base view, and a view
|
|
20
|
+
calls its components as methods. A kit registers in the slice container as the module, so `slice["components"]`
|
|
21
|
+
returns the kit rather than raising `NoMethodError`.
|
|
22
|
+
- `Phlex::Hanami::Testing::ViewHelpers`, for testing a view or a component without a request. `render` renders the
|
|
23
|
+
instance you give it, `view_context` builds the context an action would have, and `view_request` fakes a request
|
|
24
|
+
for the session, the flash and the CSRF token.
|
|
25
|
+
- `require "phlex/hanami/rspec"` includes those helpers into every RSpec example group tagged `type: :view`. Under
|
|
26
|
+
any other framework, require `phlex/hanami/testing` and include the module where your view tests run.
|
|
27
|
+
- RBS signatures for the public API, shipped in `sig`.
|
|
28
|
+
- Documentation for [components](docs/usage/components.md), [testing](docs/usage/testing.md) and
|
|
29
|
+
[responses](docs/usage/responses.md).
|
|
30
|
+
|
|
31
|
+
[Phlex kits]: https://www.phlex.fun/components/kits.html
|
|
32
|
+
|
|
33
|
+
## [v0.2.0-alpha.2] - 2026-09-05
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Phlex views for [hanami-mailer]. A mailer renders the Phlex class whose container key matches its own, so
|
|
38
|
+
`MyApp::Mailers::Welcome` renders `MyApp::Views::Mailers::Welcome` with no configuration.
|
|
39
|
+
- `Phlex::Hanami::Mailer::View` and `Phlex::Hanami::Mailer::Layout`, and `Phlex::Hanami::Mailer::Renderable` for an
|
|
40
|
+
existing `Phlex::HTML` base class.
|
|
41
|
+
- A plain text alternative part, converted from the rendered HTML by `Phlex::Hanami::Mailer::Text`. Override
|
|
42
|
+
`text_body` on the view to write it by hand.
|
|
43
|
+
- A mail layout convention: `Views::Mailers::Layout` in a slice wraps that slice's mail views.
|
|
44
|
+
- Usage documentation under [docs/usage](docs/usage/README.md), covering views, layouts, the view context,
|
|
45
|
+
helpers, mailers and how the gem hooks into Hanami.
|
|
46
|
+
- `Phlex::Hanami::MailerViewError`, raised when a mailer is paired with a Phlex view that is not a mail view,
|
|
47
|
+
rather than sending a message whose plain text part is markup.
|
|
48
|
+
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- `url` in a view returns a String. Hanami's routes helper returns a `URI`, which Phlex rejects as an attribute
|
|
52
|
+
value, so `a(href: url(:posts))` raised.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- `path` in a mail view raises `Phlex::Hanami::RelativePathError` instead of writing a relative link an email client
|
|
57
|
+
cannot follow.
|
|
58
|
+
|
|
59
|
+
[hanami-mailer]: https://github.com/hanami/mailer
|
|
60
|
+
|
|
61
|
+
## [v0.2.0-alpha.1] - 2026-09-04
|
|
11
62
|
|
|
12
63
|
Initial alpha release by the new maintainer [@aaronmallen](https://github.com/aaronmallen).
|
|
13
64
|
|
|
@@ -15,5 +66,8 @@ Initial alpha release by the new maintainer [@aaronmallen](https://github.com/aa
|
|
|
15
66
|
|
|
16
67
|
Initial release, by the previous maintainer [@stephannv](https://github.com/stephannv).
|
|
17
68
|
|
|
18
|
-
[Unreleased]: https://github.com/aaronmallen/phlex-hanami/compare/0.
|
|
69
|
+
[Unreleased]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.3...HEAD
|
|
70
|
+
[v0.2.0-alpha.3]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.2...0.2.0-alpha.3
|
|
71
|
+
[v0.2.0-alpha.2]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.1...0.2.0-alpha.2
|
|
72
|
+
[v0.2.0-alpha.1]: https://github.com/aaronmallen/phlex-hanami/releases/tag/0.2.0-alpha.1
|
|
19
73
|
[v0.1.0]: https://github.com/stephannv/phlex-hanami/releases/tag/v0.1.0
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Hanami
|
|
5
|
+
# The base class for everything below a view.
|
|
6
|
+
#
|
|
7
|
+
# A component is an ordinary Phlex class you render with `render`. It reads the same context a
|
|
8
|
+
# view does — `path`, `assets`, `content_for`, `flash`, translations — because Phlex shares that
|
|
9
|
+
# context with every component in the tree, however deep. What it does not have is the half of a
|
|
10
|
+
# view that belongs to Hanami: no layout, and no `call(context:, **input)` entry point, because
|
|
11
|
+
# Hanami never calls a component.
|
|
12
|
+
#
|
|
13
|
+
# Subclass this, or include {Contextual} into a base class you already have.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# # app/components/card.rb
|
|
17
|
+
# module MyApp
|
|
18
|
+
# module Components
|
|
19
|
+
# class Card < Phlex::Hanami::Component
|
|
20
|
+
# def initialize(post:) = @post = post
|
|
21
|
+
#
|
|
22
|
+
# def view_template
|
|
23
|
+
# article do
|
|
24
|
+
# h2 { a(href: path(:post, id: @post.id)) { @post.title } }
|
|
25
|
+
# yield if block_given?
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# A component takes the context of whoever renders it, not its own slice's, so one written in
|
|
33
|
+
# the app namespace works unchanged inside a slice's view.
|
|
34
|
+
#
|
|
35
|
+
# @api public
|
|
36
|
+
# @since 0.2.0
|
|
37
|
+
class Component < Phlex::HTML
|
|
38
|
+
include Contextual
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/phlex/hanami/context.rb
CHANGED
|
@@ -17,6 +17,7 @@ module Phlex
|
|
|
17
17
|
|
|
18
18
|
# @api private
|
|
19
19
|
# @since 0.2.0
|
|
20
|
+
#: (singleton(::Hanami::Slice)) -> void
|
|
20
21
|
def self.configure_for_slice(slice)
|
|
21
22
|
extend SliceConfiguredContext.new(slice)
|
|
22
23
|
end
|
|
@@ -27,12 +28,20 @@ module Phlex
|
|
|
27
28
|
#
|
|
28
29
|
# @api public
|
|
29
30
|
# @since 0.2.0
|
|
30
|
-
attr_reader :inflector
|
|
31
|
+
attr_reader :inflector #: Dry::Inflector
|
|
31
32
|
|
|
32
33
|
# @see SliceConfiguredContext#define_new
|
|
33
34
|
#
|
|
34
35
|
# @api private
|
|
35
36
|
# @since 0.2.0
|
|
37
|
+
#: (
|
|
38
|
+
# ?inflector: Dry::Inflector?,
|
|
39
|
+
# ?routes: ::Hanami::Slice::RoutesHelper?,
|
|
40
|
+
# ?assets: ::Hanami::Assets?,
|
|
41
|
+
# ?request: ::Hanami::Action::Request?,
|
|
42
|
+
# ?i18n: ::Hanami::Providers::I18n::Backend?,
|
|
43
|
+
# **untyped
|
|
44
|
+
# ) -> void
|
|
36
45
|
def initialize(inflector: nil, routes: nil, assets: nil, request: nil, i18n: nil, **)
|
|
37
46
|
@inflector = inflector
|
|
38
47
|
@routes = routes
|
|
@@ -50,6 +59,7 @@ module Phlex
|
|
|
50
59
|
#
|
|
51
60
|
# @api public
|
|
52
61
|
# @since 0.2.0
|
|
62
|
+
#: () -> ::Hanami::Assets
|
|
53
63
|
def assets
|
|
54
64
|
raise ::Hanami::ComponentLoadError, "Assets not available. #{assets_hint}" unless @assets
|
|
55
65
|
|
|
@@ -65,6 +75,7 @@ module Phlex
|
|
|
65
75
|
#
|
|
66
76
|
# @api public
|
|
67
77
|
# @since 0.2.0
|
|
78
|
+
#: (Symbol, ?String?) ?{ () -> String } -> String?
|
|
68
79
|
def content_for(key, value = nil)
|
|
69
80
|
if block_given?
|
|
70
81
|
@content_for[key] = yield
|
|
@@ -85,6 +96,7 @@ module Phlex
|
|
|
85
96
|
#
|
|
86
97
|
# @api public
|
|
87
98
|
# @since 0.2.0
|
|
99
|
+
#: () -> String?
|
|
88
100
|
def csrf_token
|
|
89
101
|
request.session[::Hanami::Action::CSRFProtection::CSRF_TOKEN]
|
|
90
102
|
end
|
|
@@ -95,6 +107,7 @@ module Phlex
|
|
|
95
107
|
#
|
|
96
108
|
# @api public
|
|
97
109
|
# @since 0.2.0
|
|
110
|
+
#: () -> ::Hanami::Action::Flash
|
|
98
111
|
def flash
|
|
99
112
|
request.flash
|
|
100
113
|
end
|
|
@@ -105,6 +118,7 @@ module Phlex
|
|
|
105
118
|
#
|
|
106
119
|
# @api public
|
|
107
120
|
# @since 0.2.0
|
|
121
|
+
#: () -> ::Hanami::Providers::I18n::Backend
|
|
108
122
|
def i18n
|
|
109
123
|
raise ::Hanami::ComponentLoadError, "the i18n gem is required to access translations" unless @i18n
|
|
110
124
|
|
|
@@ -113,6 +127,7 @@ module Phlex
|
|
|
113
127
|
|
|
114
128
|
# @api private
|
|
115
129
|
# @since 0.2.0
|
|
130
|
+
#: (self) -> void
|
|
116
131
|
def initialize_copy(source)
|
|
117
132
|
super
|
|
118
133
|
@content_for = source.instance_variable_get(:@content_for).dup
|
|
@@ -126,6 +141,7 @@ module Phlex
|
|
|
126
141
|
#
|
|
127
142
|
# @api public
|
|
128
143
|
# @since 0.2.0
|
|
144
|
+
#: () -> ::Hanami::Action::Request
|
|
129
145
|
def request
|
|
130
146
|
unless @request
|
|
131
147
|
raise ::Hanami::ComponentLoadError, <<~MESSAGE
|
|
@@ -142,6 +158,7 @@ module Phlex
|
|
|
142
158
|
#
|
|
143
159
|
# @api public
|
|
144
160
|
# @since 0.2.0
|
|
161
|
+
#: () -> bool
|
|
145
162
|
def request?
|
|
146
163
|
!!@request
|
|
147
164
|
end
|
|
@@ -154,6 +171,7 @@ module Phlex
|
|
|
154
171
|
#
|
|
155
172
|
# @api public
|
|
156
173
|
# @since 0.2.0
|
|
174
|
+
#: () -> ::Hanami::Slice::RoutesHelper
|
|
157
175
|
def routes
|
|
158
176
|
raise ::Hanami::ComponentLoadError, "the hanami-router gem is required to access routes" unless @routes
|
|
159
177
|
|
|
@@ -166,12 +184,14 @@ module Phlex
|
|
|
166
184
|
#
|
|
167
185
|
# @api public
|
|
168
186
|
# @since 0.2.0
|
|
187
|
+
#: () -> ::Hanami::Action::Request::Session
|
|
169
188
|
def session
|
|
170
189
|
request.session
|
|
171
190
|
end
|
|
172
191
|
|
|
173
192
|
private
|
|
174
193
|
|
|
194
|
+
#: () -> String
|
|
175
195
|
def assets_hint
|
|
176
196
|
if ::Hanami.bundled?("hanami-assets")
|
|
177
197
|
"Have you put files into your assets directory?"
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Hanami
|
|
5
|
+
# Gives a Phlex class the Hanami view context and the slice it was defined in.
|
|
6
|
+
#
|
|
7
|
+
# This is the half of the integration a component needs: `path`, `assets`, `content_for`,
|
|
8
|
+
# `flash`, translations and the rest, read off the context Hanami builds for the request.
|
|
9
|
+
# {Renderable} includes it and adds the `call` contract Hanami's `Response#render` expects;
|
|
10
|
+
# include this on its own for a class Hanami never calls directly.
|
|
11
|
+
#
|
|
12
|
+
# {Component} subclasses it already. Include this instead when you have your own component base
|
|
13
|
+
# class.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# module MyApp
|
|
17
|
+
# class Component < Phlex::HTML
|
|
18
|
+
# include Phlex::Hanami::Contextual
|
|
19
|
+
# end
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @api public
|
|
23
|
+
# @since 0.2.0
|
|
24
|
+
module Contextual
|
|
25
|
+
# @api private
|
|
26
|
+
# @since 0.2.0
|
|
27
|
+
#: (singleton(::Phlex::SGML)) -> void
|
|
28
|
+
def self.included(view_class)
|
|
29
|
+
view_class.extend(::Hanami::SliceConfigurable)
|
|
30
|
+
view_class.extend(ClassMethods)
|
|
31
|
+
return unless defined?(::Hanami::Helpers::I18nHelper)
|
|
32
|
+
|
|
33
|
+
# Order matters: a module included later sits higher in the lookup chain, so the overrides
|
|
34
|
+
# have to go in after Hanami's helper, not alongside it in this module.
|
|
35
|
+
view_class.include(::Hanami::Helpers::I18nHelper)
|
|
36
|
+
view_class.include(I18nOverrides)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Relative-key resolution for Hanami's i18n helper.
|
|
40
|
+
#
|
|
41
|
+
# A Phlex view has no template, so Hanami's implementation — which reads
|
|
42
|
+
# `_context.current_template_name` — cannot work. Included after `Hanami::Helpers::I18nHelper`
|
|
43
|
+
# so that it wins: a module included later sits higher in the lookup chain.
|
|
44
|
+
#
|
|
45
|
+
# @api private
|
|
46
|
+
# @since 0.2.0
|
|
47
|
+
module I18nOverrides
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Resolves a relative translation key (`t(".title")`) against the view's own container key, so
|
|
51
|
+
# `MyApp::Views::Posts::Index` looks up `posts.index.title`.
|
|
52
|
+
#
|
|
53
|
+
# Hanami's own implementation uses `_context.current_template_name`, which is hanami-view's
|
|
54
|
+
# template stack and has no meaning for a Phlex view.
|
|
55
|
+
#: (String | Symbol) -> (String | Symbol)
|
|
56
|
+
def _resolve_i18n_key(key)
|
|
57
|
+
return key unless key.to_s.start_with?(".")
|
|
58
|
+
|
|
59
|
+
base = i18n_key_base
|
|
60
|
+
unless base
|
|
61
|
+
raise ::I18n::ArgumentError,
|
|
62
|
+
"Cannot resolve the relative translation key #{key.inspect} for #{self.class}. " \
|
|
63
|
+
"Give the view a name inside a slice, or use an absolute key (without a leading dot)."
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
"#{base}#{key}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The view's container key, dotted: `MyApp::Views::Posts::Index` in the app slice becomes
|
|
70
|
+
# `posts.index`. Nil for an anonymous view, or one outside any slice.
|
|
71
|
+
#: () -> String?
|
|
72
|
+
def i18n_key_base
|
|
73
|
+
view_slice = self.class.slice
|
|
74
|
+
name = self.class.name
|
|
75
|
+
return nil unless view_slice && name
|
|
76
|
+
|
|
77
|
+
view_slice.inflector
|
|
78
|
+
.underscore(name)
|
|
79
|
+
.sub(%r{^#{view_slice.slice_name.path}/}, "")
|
|
80
|
+
.sub(%r{^views/}, "")
|
|
81
|
+
.tr("/", ".")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @api public
|
|
86
|
+
# @since 0.2.0
|
|
87
|
+
module ClassMethods
|
|
88
|
+
# @api private
|
|
89
|
+
# @since 0.2.0
|
|
90
|
+
#: (singleton(::Hanami::Slice)) -> void
|
|
91
|
+
def configure_for_slice(slice)
|
|
92
|
+
extend SliceConfigured.new(slice)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# The slice this class belongs to, or nil when it is defined outside a slice namespace.
|
|
96
|
+
#
|
|
97
|
+
# A class with no slice still renders: the context comes from whoever is rendering it, not
|
|
98
|
+
# from the slice. What it loses is the slice-relative half — `t(".title")` has nothing to
|
|
99
|
+
# resolve against and raises.
|
|
100
|
+
#
|
|
101
|
+
# @return [Hanami::Slice, nil]
|
|
102
|
+
#
|
|
103
|
+
# @api public
|
|
104
|
+
# @since 0.2.0
|
|
105
|
+
#: () -> singleton(::Hanami::Slice)?
|
|
106
|
+
def slice
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# The URL for an asset.
|
|
112
|
+
#
|
|
113
|
+
# @example
|
|
114
|
+
# script(src: asset_url("app.js"))
|
|
115
|
+
#
|
|
116
|
+
# @param source [String] the asset's source path
|
|
117
|
+
#
|
|
118
|
+
# @return [String] the URL
|
|
119
|
+
#
|
|
120
|
+
# @api public
|
|
121
|
+
# @since 0.2.0
|
|
122
|
+
#: (String) -> String
|
|
123
|
+
def asset_url(source)
|
|
124
|
+
assets[source].url
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# The slice's assets.
|
|
128
|
+
#
|
|
129
|
+
# @api public
|
|
130
|
+
# @since 0.2.0
|
|
131
|
+
#: () -> ::Hanami::Assets
|
|
132
|
+
def assets
|
|
133
|
+
hanami_context.assets
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Stores a string of markup for later use, or reads back what was stored.
|
|
137
|
+
#
|
|
138
|
+
# @api public
|
|
139
|
+
# @since 0.2.0
|
|
140
|
+
#: (Symbol, ?String?) ?{ () -> String } -> String?
|
|
141
|
+
def content_for(...)
|
|
142
|
+
hanami_context.content_for(...)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The current request's CSRF token.
|
|
146
|
+
#
|
|
147
|
+
# @return [String]
|
|
148
|
+
#
|
|
149
|
+
# @api public
|
|
150
|
+
# @since 0.2.0
|
|
151
|
+
#: () -> String?
|
|
152
|
+
def csrf_token
|
|
153
|
+
hanami_context.csrf_token
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The flash hash for the current request.
|
|
157
|
+
#
|
|
158
|
+
# @api public
|
|
159
|
+
# @since 0.2.0
|
|
160
|
+
#: () -> ::Hanami::Action::Flash
|
|
161
|
+
def flash
|
|
162
|
+
hanami_context.flash
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The Hanami view context for this render.
|
|
166
|
+
#
|
|
167
|
+
# Read from Phlex's user context, which is shared with every component in the render tree, so
|
|
168
|
+
# a component nested any number of levels deep sees the same context.
|
|
169
|
+
#
|
|
170
|
+
# @return [Object] a `Hanami::View::Context` when hanami-view is bundled, otherwise a {Context}
|
|
171
|
+
#
|
|
172
|
+
# @raise [MissingContextError] when the view was rendered without a context
|
|
173
|
+
#
|
|
174
|
+
# @api public
|
|
175
|
+
# @since 0.2.0
|
|
176
|
+
#: () -> (::Hanami::View::Context | Context)
|
|
177
|
+
def hanami_context
|
|
178
|
+
context[CONTEXT_KEY] || raise(MissingContextError, self.class)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Whether a Hanami view context is available.
|
|
182
|
+
#
|
|
183
|
+
# @return [Boolean]
|
|
184
|
+
#
|
|
185
|
+
# @api public
|
|
186
|
+
# @since 0.2.0
|
|
187
|
+
#: () -> bool
|
|
188
|
+
def hanami_context?
|
|
189
|
+
!context[CONTEXT_KEY].nil?
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# The slice's i18n backend.
|
|
193
|
+
#
|
|
194
|
+
# @api public
|
|
195
|
+
# @since 0.2.0
|
|
196
|
+
#: () -> ::Hanami::Providers::I18n::Backend
|
|
197
|
+
def i18n
|
|
198
|
+
hanami_context.i18n
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# The path for a named route.
|
|
202
|
+
#
|
|
203
|
+
# @example
|
|
204
|
+
# a(href: path(:posts)) { "Posts" }
|
|
205
|
+
#
|
|
206
|
+
# @return [String]
|
|
207
|
+
#
|
|
208
|
+
# @api public
|
|
209
|
+
# @since 0.2.0
|
|
210
|
+
#: (*untyped, **untyped) -> String
|
|
211
|
+
def path(...)
|
|
212
|
+
routes.path(...)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# The current request.
|
|
216
|
+
#
|
|
217
|
+
# @return [Hanami::Action::Request]
|
|
218
|
+
#
|
|
219
|
+
# @api public
|
|
220
|
+
# @since 0.2.0
|
|
221
|
+
#: () -> ::Hanami::Action::Request
|
|
222
|
+
def request
|
|
223
|
+
hanami_context.request
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Whether the view is being rendered from within a request.
|
|
227
|
+
#
|
|
228
|
+
# @return [Boolean]
|
|
229
|
+
#
|
|
230
|
+
# @api public
|
|
231
|
+
# @since 0.2.0
|
|
232
|
+
#: () -> bool
|
|
233
|
+
def request?
|
|
234
|
+
hanami_context? && hanami_context.request?
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# The app's routes helper.
|
|
238
|
+
#
|
|
239
|
+
# @return [Hanami::Slice::RoutesHelper]
|
|
240
|
+
#
|
|
241
|
+
# @api public
|
|
242
|
+
# @since 0.2.0
|
|
243
|
+
#: () -> ::Hanami::Slice::RoutesHelper
|
|
244
|
+
def routes
|
|
245
|
+
hanami_context.routes
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# The session for the current request.
|
|
249
|
+
#
|
|
250
|
+
# @api public
|
|
251
|
+
# @since 0.2.0
|
|
252
|
+
#: () -> ::Hanami::Action::Request::Session
|
|
253
|
+
def session
|
|
254
|
+
hanami_context.session
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# The slice this class belongs to.
|
|
258
|
+
#
|
|
259
|
+
# @return [Hanami::Slice, nil]
|
|
260
|
+
#
|
|
261
|
+
# @api public
|
|
262
|
+
# @since 0.2.0
|
|
263
|
+
#: () -> singleton(::Hanami::Slice)?
|
|
264
|
+
def slice
|
|
265
|
+
self.class.slice
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# The full URL for a named route.
|
|
269
|
+
#
|
|
270
|
+
# Hanami's routes helper returns a `URI`; Phlex writes strings, and rejects anything else as
|
|
271
|
+
# an attribute value, so this hands back the string.
|
|
272
|
+
#
|
|
273
|
+
# @example
|
|
274
|
+
# a(href: url(:posts)) { "Posts" }
|
|
275
|
+
#
|
|
276
|
+
# @return [String]
|
|
277
|
+
#
|
|
278
|
+
# @api public
|
|
279
|
+
# @since 0.2.0
|
|
280
|
+
#: (*untyped, **untyped) -> String
|
|
281
|
+
def url(...)
|
|
282
|
+
routes.url(...).to_s
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
# Hanami's helper modules reach for the view context under this name.
|
|
288
|
+
#: () -> (::Hanami::View::Context | Context)
|
|
289
|
+
def _context
|
|
290
|
+
hanami_context
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
#: (singleton(::Hanami::Mailer), singleton(::Phlex::SGML)) -> void
|
|
16
|
+
def initialize(mailer_class, view_class)
|
|
17
|
+
super(<<~MESSAGE)
|
|
18
|
+
#{mailer_class} is paired with #{view_class}, which is not a mail view.
|
|
19
|
+
|
|
20
|
+
A mail view renders both parts of the message. Subclass the mail base class:
|
|
21
|
+
|
|
22
|
+
class #{view_class} < Phlex::Hanami::Mailer::View
|
|
23
|
+
|
|
24
|
+
or include the module into the base class you already have:
|
|
25
|
+
|
|
26
|
+
include Phlex::Hanami::Mailer::Renderable
|
|
27
|
+
MESSAGE
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
#: (singleton(::Phlex::SGML)) -> void
|
|
16
|
+
def initialize(view_class)
|
|
17
|
+
super(<<~MESSAGE)
|
|
18
|
+
#{view_class} asked for a relative path, and an email needs a full URL.
|
|
19
|
+
|
|
20
|
+
Use `url` instead:
|
|
21
|
+
|
|
22
|
+
a(href: url(:post, id: @post.id)) { @post.title }
|
|
23
|
+
MESSAGE
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|