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
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Hanami
|
|
5
|
+
# Makes a Phlex class renderable by Hanami.
|
|
6
|
+
#
|
|
7
|
+
# Include this into an existing `Phlex::SGML` subclass, or subclass {View}, which includes it
|
|
8
|
+
# already. It gives the class the `call` contract Hanami's `Response#render` expects, per-slice
|
|
9
|
+
# configuration, and access to the request's view context.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# module MyApp
|
|
13
|
+
# class View < Phlex::HTML
|
|
14
|
+
# include Phlex::Hanami::Renderable
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# @api public
|
|
19
|
+
# @since 0.2.0
|
|
20
|
+
module Renderable
|
|
21
|
+
# The `Method#parameters` types that name a keyword argument.
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
24
|
+
# @since 0.2.0
|
|
25
|
+
KEYWORD_TYPES = %i[key keyreq].freeze
|
|
26
|
+
|
|
27
|
+
# Distinguishes "no layout was configured" from "a layout of `nil` was configured", which is
|
|
28
|
+
# how a view opts out.
|
|
29
|
+
#
|
|
30
|
+
# @api private
|
|
31
|
+
# @since 0.2.0
|
|
32
|
+
UNSET = ::Object.new.freeze
|
|
33
|
+
|
|
34
|
+
# @api private
|
|
35
|
+
# @since 0.2.0
|
|
36
|
+
def self.included(view_class)
|
|
37
|
+
view_class.extend(::Hanami::SliceConfigurable)
|
|
38
|
+
view_class.extend(ClassMethods)
|
|
39
|
+
return unless defined?(::Hanami::Helpers::I18nHelper)
|
|
40
|
+
|
|
41
|
+
# Order matters: a module included later sits higher in the lookup chain, so the overrides
|
|
42
|
+
# have to go in after Hanami's helper, not alongside it in this module.
|
|
43
|
+
view_class.include(::Hanami::Helpers::I18nHelper)
|
|
44
|
+
view_class.include(I18nOverrides)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Relative-key resolution for Hanami's i18n helper.
|
|
48
|
+
#
|
|
49
|
+
# A Phlex view has no template, so Hanami's implementation — which reads
|
|
50
|
+
# `_context.current_template_name` — cannot work. Included after `Hanami::Helpers::I18nHelper`
|
|
51
|
+
# so that it wins: a module included later sits higher in the lookup chain.
|
|
52
|
+
#
|
|
53
|
+
# @api private
|
|
54
|
+
# @since 0.2.0
|
|
55
|
+
module I18nOverrides
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
# Resolves a relative translation key (`t(".title")`) against the view's own container key, so
|
|
59
|
+
# `MyApp::Views::Posts::Index` looks up `posts.index.title`.
|
|
60
|
+
#
|
|
61
|
+
# Hanami's own implementation uses `_context.current_template_name`, which is hanami-view's
|
|
62
|
+
# template stack and has no meaning for a Phlex view.
|
|
63
|
+
def _resolve_i18n_key(key)
|
|
64
|
+
return key unless key.to_s.start_with?(".")
|
|
65
|
+
|
|
66
|
+
base = i18n_key_base
|
|
67
|
+
unless base
|
|
68
|
+
raise ::I18n::ArgumentError,
|
|
69
|
+
"Cannot resolve the relative translation key #{key.inspect} for #{self.class}. " \
|
|
70
|
+
"Give the view a name inside a slice, or use an absolute key (without a leading dot)."
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
"#{base}#{key}"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# The view's container key, dotted: `MyApp::Views::Posts::Index` in the app slice becomes
|
|
77
|
+
# `posts.index`. Nil for an anonymous view, or one outside any slice.
|
|
78
|
+
def i18n_key_base
|
|
79
|
+
view_slice = self.class.slice
|
|
80
|
+
name = self.class.name
|
|
81
|
+
return nil unless view_slice && name
|
|
82
|
+
|
|
83
|
+
view_slice.inflector
|
|
84
|
+
.underscore(name)
|
|
85
|
+
.sub(%r{^#{view_slice.slice_name.path}/}, "")
|
|
86
|
+
.sub(%r{^views/}, "")
|
|
87
|
+
.tr("/", ".")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @api public
|
|
92
|
+
# @since 0.2.0
|
|
93
|
+
module ClassMethods
|
|
94
|
+
# Renders the view to a String.
|
|
95
|
+
#
|
|
96
|
+
# This is the entry point Hanami's `Hanami::Action::Response#render` calls. Hanami passes
|
|
97
|
+
# the request's view context as `context:` alongside every response exposure and request
|
|
98
|
+
# param; `input` is filtered down to the keywords this view's `initialize` accepts, so an
|
|
99
|
+
# unexpected param is dropped rather than raising.
|
|
100
|
+
#
|
|
101
|
+
# Exposures take precedence over params, which is Hanami's own merge order.
|
|
102
|
+
#
|
|
103
|
+
# When the view has a layout, the view's body is rendered first and then handed to the
|
|
104
|
+
# layout. That ordering is what makes `content_for` set in the view visible in the layout's
|
|
105
|
+
# `head`; Hanami assigns the whole response body at once, so nothing is streamed either way.
|
|
106
|
+
#
|
|
107
|
+
# @param context [Object, nil] the Hanami view context
|
|
108
|
+
# @param input [Hash] exposures and params
|
|
109
|
+
#
|
|
110
|
+
# @return [String] the rendered markup
|
|
111
|
+
#
|
|
112
|
+
# @api public
|
|
113
|
+
# @since 0.2.0
|
|
114
|
+
def call(context: nil, **input)
|
|
115
|
+
phlex_context = { CONTEXT_KEY => context }
|
|
116
|
+
body = new(**accepted_input(input)).call(context: phlex_context)
|
|
117
|
+
|
|
118
|
+
layout_class = layout
|
|
119
|
+
return body unless layout_class
|
|
120
|
+
|
|
121
|
+
layout_class.new.call(context: phlex_context) { |layout| layout.raw(layout.safe(body)) }
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# @api private
|
|
125
|
+
# @since 0.2.0
|
|
126
|
+
def configure_for_slice(slice)
|
|
127
|
+
extend SliceConfigured.new(slice)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# The layout configured on this class, or inherited from a superclass. {UNSET} when none has
|
|
131
|
+
# been configured anywhere in the chain.
|
|
132
|
+
#
|
|
133
|
+
# @api private
|
|
134
|
+
# @since 0.2.0
|
|
135
|
+
def configured_layout
|
|
136
|
+
return @layout if defined?(@layout)
|
|
137
|
+
return superclass.configured_layout if superclass.respond_to?(:configured_layout)
|
|
138
|
+
|
|
139
|
+
UNSET
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Reads or sets the layout this view renders inside.
|
|
143
|
+
#
|
|
144
|
+
# Called with no argument it reads: an explicit setting on this class, otherwise the
|
|
145
|
+
# nearest superclass's, otherwise the slice's conventional `Views::Layout`.
|
|
146
|
+
#
|
|
147
|
+
# Layouts only apply at this entry point, so a component rendered with `render` is never
|
|
148
|
+
# wrapped — only the view Hanami calls is.
|
|
149
|
+
#
|
|
150
|
+
# @example Set a layout for a whole slice, from its base view
|
|
151
|
+
# module MyApp
|
|
152
|
+
# class View < Phlex::Hanami::View
|
|
153
|
+
# layout MyApp::Views::AdminLayout
|
|
154
|
+
# end
|
|
155
|
+
# end
|
|
156
|
+
#
|
|
157
|
+
# @example Opt out, for a turbo frame or a partial response
|
|
158
|
+
# class Frame < MyApp::View
|
|
159
|
+
# layout nil
|
|
160
|
+
# end
|
|
161
|
+
#
|
|
162
|
+
# @param layout_class [Class, nil] the layout to render inside, or nil for none
|
|
163
|
+
#
|
|
164
|
+
# @return [Class, nil] the layout
|
|
165
|
+
#
|
|
166
|
+
# @api public
|
|
167
|
+
# @since 0.2.0
|
|
168
|
+
def layout(layout_class = UNSET)
|
|
169
|
+
unless UNSET.equal?(layout_class)
|
|
170
|
+
@layout = layout_class
|
|
171
|
+
return layout_class
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
configured = configured_layout
|
|
175
|
+
UNSET.equal?(configured) ? default_layout : configured
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# The slice this view belongs to, or nil when it is defined outside a slice namespace.
|
|
179
|
+
#
|
|
180
|
+
# @return [Hanami::Slice, nil]
|
|
181
|
+
#
|
|
182
|
+
# @api public
|
|
183
|
+
# @since 0.2.0
|
|
184
|
+
def slice
|
|
185
|
+
nil
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
private
|
|
189
|
+
|
|
190
|
+
# Keeps only the keywords `initialize` declares.
|
|
191
|
+
#
|
|
192
|
+
# A view whose initializer takes `**` opts out and receives everything, request params
|
|
193
|
+
# included.
|
|
194
|
+
def accepted_input(input)
|
|
195
|
+
parameters = instance_method(:initialize).parameters
|
|
196
|
+
return input if parameters.any? { |type, _| type == :keyrest }
|
|
197
|
+
|
|
198
|
+
input.slice(*parameters.filter_map { |type, name| name if KEYWORD_TYPES.include?(type) })
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# The slice's conventional layout: `Views::Layout` in the slice's namespace, if it defines
|
|
202
|
+
# one. Resolved on each call rather than memoized, so code reloading is not defeated.
|
|
203
|
+
def default_layout
|
|
204
|
+
return nil unless slice
|
|
205
|
+
return nil unless slice.namespace.const_defined?(:Views, false)
|
|
206
|
+
|
|
207
|
+
views = slice.namespace.const_get(:Views, false)
|
|
208
|
+
views.const_get(:Layout, false) if views.const_defined?(:Layout, false)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# The URL for an asset.
|
|
213
|
+
#
|
|
214
|
+
# @example
|
|
215
|
+
# script(src: asset_url("app.js"))
|
|
216
|
+
#
|
|
217
|
+
# @param source [String] the asset's source path
|
|
218
|
+
#
|
|
219
|
+
# @return [String] the URL
|
|
220
|
+
#
|
|
221
|
+
# @api public
|
|
222
|
+
# @since 0.2.0
|
|
223
|
+
def asset_url(source)
|
|
224
|
+
assets[source].url
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# The slice's assets.
|
|
228
|
+
#
|
|
229
|
+
# @api public
|
|
230
|
+
# @since 0.2.0
|
|
231
|
+
def assets
|
|
232
|
+
hanami_context.assets
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Stores a string of markup for later use, or reads back what was stored.
|
|
236
|
+
#
|
|
237
|
+
# @api public
|
|
238
|
+
# @since 0.2.0
|
|
239
|
+
def content_for(...)
|
|
240
|
+
hanami_context.content_for(...)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# The current request's CSRF token.
|
|
244
|
+
#
|
|
245
|
+
# @return [String]
|
|
246
|
+
#
|
|
247
|
+
# @api public
|
|
248
|
+
# @since 0.2.0
|
|
249
|
+
def csrf_token
|
|
250
|
+
hanami_context.csrf_token
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# The flash hash for the current request.
|
|
254
|
+
#
|
|
255
|
+
# @api public
|
|
256
|
+
# @since 0.2.0
|
|
257
|
+
def flash
|
|
258
|
+
hanami_context.flash
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# The Hanami view context for this render.
|
|
262
|
+
#
|
|
263
|
+
# Read from Phlex's user context, which is shared with every component in the render tree, so
|
|
264
|
+
# a component nested any number of levels deep sees the same context.
|
|
265
|
+
#
|
|
266
|
+
# @return [Object] a `Hanami::View::Context` when hanami-view is bundled, otherwise a {Context}
|
|
267
|
+
#
|
|
268
|
+
# @raise [MissingContextError] when the view was rendered without a context
|
|
269
|
+
#
|
|
270
|
+
# @api public
|
|
271
|
+
# @since 0.2.0
|
|
272
|
+
def hanami_context
|
|
273
|
+
context[CONTEXT_KEY] || raise(MissingContextError, self.class)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Whether a Hanami view context is available.
|
|
277
|
+
#
|
|
278
|
+
# @return [Boolean]
|
|
279
|
+
#
|
|
280
|
+
# @api public
|
|
281
|
+
# @since 0.2.0
|
|
282
|
+
def hanami_context?
|
|
283
|
+
!context[CONTEXT_KEY].nil?
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# The slice's i18n backend.
|
|
287
|
+
#
|
|
288
|
+
# @api public
|
|
289
|
+
# @since 0.2.0
|
|
290
|
+
def i18n
|
|
291
|
+
hanami_context.i18n
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# The path for a named route.
|
|
295
|
+
#
|
|
296
|
+
# @example
|
|
297
|
+
# a(href: path(:posts)) { "Posts" }
|
|
298
|
+
#
|
|
299
|
+
# @return [String]
|
|
300
|
+
#
|
|
301
|
+
# @api public
|
|
302
|
+
# @since 0.2.0
|
|
303
|
+
def path(...)
|
|
304
|
+
routes.path(...)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# The current request.
|
|
308
|
+
#
|
|
309
|
+
# @return [Hanami::Action::Request]
|
|
310
|
+
#
|
|
311
|
+
# @api public
|
|
312
|
+
# @since 0.2.0
|
|
313
|
+
def request
|
|
314
|
+
hanami_context.request
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Whether the view is being rendered from within a request.
|
|
318
|
+
#
|
|
319
|
+
# @return [Boolean]
|
|
320
|
+
#
|
|
321
|
+
# @api public
|
|
322
|
+
# @since 0.2.0
|
|
323
|
+
def request?
|
|
324
|
+
hanami_context? && hanami_context.request?
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# The app's routes helper.
|
|
328
|
+
#
|
|
329
|
+
# @return [Hanami::Slice::RoutesHelper]
|
|
330
|
+
#
|
|
331
|
+
# @api public
|
|
332
|
+
# @since 0.2.0
|
|
333
|
+
def routes
|
|
334
|
+
hanami_context.routes
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# The session for the current request.
|
|
338
|
+
#
|
|
339
|
+
# @api public
|
|
340
|
+
# @since 0.2.0
|
|
341
|
+
def session
|
|
342
|
+
hanami_context.session
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# The slice this view belongs to.
|
|
346
|
+
#
|
|
347
|
+
# @return [Hanami::Slice, nil]
|
|
348
|
+
#
|
|
349
|
+
# @api public
|
|
350
|
+
# @since 0.2.0
|
|
351
|
+
def slice
|
|
352
|
+
self.class.slice
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# The full URL for a named route.
|
|
356
|
+
#
|
|
357
|
+
# Hanami's routes helper returns a `URI`; Phlex writes strings, and rejects anything else as
|
|
358
|
+
# an attribute value, so this hands back the string.
|
|
359
|
+
#
|
|
360
|
+
# @example
|
|
361
|
+
# a(href: url(:posts)) { "Posts" }
|
|
362
|
+
#
|
|
363
|
+
# @return [String]
|
|
364
|
+
#
|
|
365
|
+
# @api public
|
|
366
|
+
# @since 0.2.0
|
|
367
|
+
def url(...)
|
|
368
|
+
routes.url(...).to_s
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
private
|
|
372
|
+
|
|
373
|
+
# Hanami's helper modules reach for the view context under this name.
|
|
374
|
+
def _context
|
|
375
|
+
hanami_context
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Hanami
|
|
5
|
+
# Tells a class which slice it belongs to.
|
|
6
|
+
#
|
|
7
|
+
# `Hanami::SliceConfigurable` calls `configure_for_slice` once per slice as classes are defined,
|
|
8
|
+
# so a class in the app namespace is configured for the app and one in a slice namespace is
|
|
9
|
+
# configured for that slice. {Renderable} views and Hanami mailers both extend one of these.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
# @since 0.2.0
|
|
13
|
+
class SliceConfigured < Module
|
|
14
|
+
# The slice this module configures for.
|
|
15
|
+
#
|
|
16
|
+
# @return [Hanami::Slice]
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
# @since 0.2.0
|
|
20
|
+
attr_reader :slice
|
|
21
|
+
|
|
22
|
+
# @api private
|
|
23
|
+
# @since 0.2.0
|
|
24
|
+
def initialize(slice)
|
|
25
|
+
super()
|
|
26
|
+
@slice = slice
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @api private
|
|
30
|
+
# @since 0.2.0
|
|
31
|
+
def extended(_klass)
|
|
32
|
+
define_slice
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @api private
|
|
36
|
+
# @since 0.2.0
|
|
37
|
+
def inspect
|
|
38
|
+
"#<#{self.class.name}[#{slice.name}]>"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def define_slice
|
|
44
|
+
slice = @slice
|
|
45
|
+
|
|
46
|
+
define_method(:slice) { slice }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Hanami
|
|
5
|
+
# Provides slice-specific dependencies to a {Context} subclass defined in a slice's namespace.
|
|
6
|
+
#
|
|
7
|
+
# Mirrors `Hanami::Extensions::View::SliceConfiguredContext`, so a context built without
|
|
8
|
+
# hanami-view is injected with the same components Hanami would have injected with it.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
# @since 0.2.0
|
|
12
|
+
class SliceConfiguredContext < Module
|
|
13
|
+
# The slice this module configures for.
|
|
14
|
+
#
|
|
15
|
+
# @return [Hanami::Slice]
|
|
16
|
+
#
|
|
17
|
+
# @api private
|
|
18
|
+
# @since 0.2.0
|
|
19
|
+
attr_reader :slice
|
|
20
|
+
|
|
21
|
+
# @api private
|
|
22
|
+
# @since 0.2.0
|
|
23
|
+
def initialize(slice)
|
|
24
|
+
super()
|
|
25
|
+
@slice = slice
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @api private
|
|
29
|
+
# @since 0.2.0
|
|
30
|
+
def extended(_context_class)
|
|
31
|
+
define_new
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @api private
|
|
35
|
+
# @since 0.2.0
|
|
36
|
+
def inspect
|
|
37
|
+
"#<#{self.class.name}[#{slice.name}]>"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Defines a `.new` that resolves the slice's components and passes them to `#initialize`.
|
|
43
|
+
def define_new
|
|
44
|
+
dependencies = method(:dependencies)
|
|
45
|
+
|
|
46
|
+
define_method(:new) do |**kwargs|
|
|
47
|
+
super(**dependencies.call.merge(kwargs))
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# The slice components a context is injected with, resolved fresh for each context so that
|
|
52
|
+
# a container which does not memoize still hands out working objects.
|
|
53
|
+
def dependencies
|
|
54
|
+
{ assets: resolve_assets, i18n: resolve_i18n, inflector: slice.inflector, routes: resolve_routes }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def resolve_assets
|
|
58
|
+
slice["assets"] if slice.key?("assets")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def resolve_i18n
|
|
62
|
+
slice["i18n"] if slice.key?("i18n")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def resolve_routes
|
|
66
|
+
slice["routes"] if slice.key?("routes")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Hanami
|
|
5
|
+
# The base class for Phlex views rendered by Hanami.
|
|
6
|
+
#
|
|
7
|
+
# Subclass this for an app-level base view. If you already have your own `Phlex::HTML` base
|
|
8
|
+
# class, include {Renderable} into it instead — the two are equivalent.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # app/view.rb
|
|
12
|
+
# module MyApp
|
|
13
|
+
# class View < Phlex::Hanami::View
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # app/views/posts/index.rb
|
|
18
|
+
# module MyApp
|
|
19
|
+
# module Views
|
|
20
|
+
# module Posts
|
|
21
|
+
# class Index < MyApp::View
|
|
22
|
+
# def initialize(posts:) = @posts = posts
|
|
23
|
+
#
|
|
24
|
+
# def view_template
|
|
25
|
+
# h1 { "Posts" }
|
|
26
|
+
# ul { @posts.each { |post| li { post.title } } }
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# @api public
|
|
34
|
+
# @since 0.2.0
|
|
35
|
+
class View < Phlex::HTML
|
|
36
|
+
include Renderable
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/phlex/hanami.rb
CHANGED
|
@@ -1,14 +1,48 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "hanami"
|
|
3
4
|
require "phlex"
|
|
4
|
-
|
|
5
|
+
require "zeitwerk"
|
|
5
6
|
|
|
6
7
|
module Phlex
|
|
8
|
+
# An object oriented view layer for Hanami.
|
|
9
|
+
#
|
|
10
|
+
# Requiring this file installs the integration: Phlex classes register in slice containers as
|
|
11
|
+
# classes rather than instances, and a view context is made available to every slice.
|
|
12
|
+
#
|
|
13
|
+
# @api public
|
|
14
|
+
# @since 0.2.0
|
|
7
15
|
module Hanami
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
# The key the Hanami view context is stored under in Phlex's user context.
|
|
17
|
+
#
|
|
18
|
+
# Phlex shares its user context with every component in a render tree, so nesting a component
|
|
19
|
+
# any number of levels deep keeps the context reachable.
|
|
20
|
+
#
|
|
21
|
+
# @api private
|
|
22
|
+
# @since 0.2.0
|
|
23
|
+
CONTEXT_KEY = :__hanami_context__
|
|
13
24
|
end
|
|
14
25
|
end
|
|
26
|
+
|
|
27
|
+
# Hanami only requires its i18n helper as part of its hanami-view extensions, but the helper itself
|
|
28
|
+
# is written to work without hanami-view, so pull it in directly when the i18n gem is there.
|
|
29
|
+
if Hanami.bundled?("i18n")
|
|
30
|
+
require "i18n"
|
|
31
|
+
require "hanami/helpers/i18n_helper"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Zeitwerk::Loader.new.tap do |loader|
|
|
35
|
+
lib_dir = File.join(File.dirname(__FILE__), "hanami")
|
|
36
|
+
loader.tag = "phlex-hanami"
|
|
37
|
+
loader.push_dir(lib_dir, namespace: Phlex::Hanami)
|
|
38
|
+
loader.collapse(File.join(lib_dir, "errors"))
|
|
39
|
+
loader.ignore(__FILE__)
|
|
40
|
+
end.setup
|
|
41
|
+
|
|
42
|
+
# Installs the integration. Referencing the constant is what autoloads it, so this cannot move into
|
|
43
|
+
# the file itself — nothing else would ever refer to it.
|
|
44
|
+
Hanami::Slice::ClassMethods.prepend(Phlex::Hanami::Extensions::Slice)
|
|
45
|
+
|
|
46
|
+
# Mail is a second entry point, and an optional one. Zeitwerk keeps the mail classes unloaded until
|
|
47
|
+
# something names one, but prepending onto `Hanami::Mailer` needs the constant to exist.
|
|
48
|
+
Phlex::Hanami::Extensions::Mailer.install if Hanami.bundled?("hanami-mailer")
|
data/lib/phlex-hanami.rb
ADDED