hanami-view 2.0.0.alpha7 → 2.0.0.alpha8
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 +8 -0
- data/lib/hanami/view/context.rb +0 -18
- data/lib/hanami/view/errors.rb +0 -8
- data/lib/hanami/view/renderer.rb +1 -1
- data/lib/hanami/view/scope.rb +1 -1
- data/lib/hanami/view/version.rb +1 -1
- data/lib/hanami/view.rb +376 -13
- metadata +3 -6
- data/lib/hanami/view/application_configuration.rb +0 -77
- data/lib/hanami/view/application_context.rb +0 -98
- data/lib/hanami/view/standalone_view.rb +0 -400
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d49c664876b44d1be9f65e7d9c472a3fe50ab40d42002b170a4a2121ddf2f095
|
4
|
+
data.tar.gz: 51508250526dd6af93f2f8d1af2031e37c39109896a0b92fac038b99a65e1512
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e08aff9c75a5c412922f6e7669078e0a88258d8aa6d80b7b56569ce3732a15e703861e7a713381c1e928cbe5e5a67f1ac51e2473a9de85868ca9c895184e838e
|
7
|
+
data.tar.gz: cdad93f677870279dad0fb101dbfc03b4da79d78f78f3f1d6de5fdc50b7e7b0f4e69aef6b9b360049aaa2ff45d2e15beb24b2afe6116a87c07a17eb9eb0219eb
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
# Hanami::View
|
2
2
|
View layer for Hanami
|
3
3
|
|
4
|
+
## v2.0.0.alpha8 - 2022-05-19
|
5
|
+
|
6
|
+
### Added
|
7
|
+
- [Tim Riley] Access a hash of template locals via accessing `locals` inside any template
|
8
|
+
|
9
|
+
### Changed
|
10
|
+
- [Tim Riley] Removed automatic integration of `Hanami::View` subclasses with their surrounding Hanami application. View base classes within Hanami apps should inherit from `Hanami::Application::View` instead.
|
11
|
+
|
4
12
|
## v2.0.0.alpha7 - 2022-03-08
|
5
13
|
|
6
14
|
### Added
|
data/lib/hanami/view/context.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "dry/core/equalizer"
|
4
|
-
require_relative "application_context"
|
5
4
|
require_relative "decorated_attributes"
|
6
5
|
|
7
6
|
module Hanami
|
@@ -19,23 +18,6 @@ module Hanami
|
|
19
18
|
|
20
19
|
attr_reader :_render_env, :_options
|
21
20
|
|
22
|
-
def self.inherited(subclass)
|
23
|
-
super
|
24
|
-
|
25
|
-
# When inheriting within an Hanami app, add application context behavior
|
26
|
-
provider = application_provider(subclass)
|
27
|
-
if provider
|
28
|
-
subclass.include ApplicationContext.new(provider)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.application_provider(subclass)
|
33
|
-
if Hanami.respond_to?(:application?) && Hanami.application?
|
34
|
-
Hanami.application.component_provider(subclass)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
private_class_method :application_provider
|
38
|
-
|
39
21
|
# Returns a new instance of Context
|
40
22
|
#
|
41
23
|
# In subclasses, you should include an `**options` parameter and pass _all
|
data/lib/hanami/view/errors.rb
CHANGED
data/lib/hanami/view/renderer.rb
CHANGED
data/lib/hanami/view/scope.rb
CHANGED
@@ -35,7 +35,7 @@ module Hanami
|
|
35
35
|
# @overload _locals
|
36
36
|
# Returns the locals
|
37
37
|
# @overload locals
|
38
|
-
# A convenience alias for `#
|
38
|
+
# A convenience alias for `#_locals.` Is available unless there is a
|
39
39
|
# local named `locals`
|
40
40
|
#
|
41
41
|
# @return [Hash[<Symbol, Object>]
|
data/lib/hanami/view/version.rb
CHANGED
data/lib/hanami/view.rb
CHANGED
@@ -15,7 +15,6 @@ require_relative "view/render_environment"
|
|
15
15
|
require_relative "view/rendered"
|
16
16
|
require_relative "view/renderer"
|
17
17
|
require_relative "view/scope_builder"
|
18
|
-
require_relative "view/standalone_view"
|
19
18
|
|
20
19
|
module Hanami
|
21
20
|
# A standalone, template-based view rendering system that offers everything
|
@@ -230,24 +229,388 @@ module Hanami
|
|
230
229
|
|
231
230
|
# @!endgroup
|
232
231
|
|
233
|
-
|
234
|
-
|
235
|
-
def self.inherited(subclass)
|
232
|
+
# @api private
|
233
|
+
def self.inherited(klass)
|
236
234
|
super
|
237
235
|
|
238
|
-
|
239
|
-
|
240
|
-
# i.e. for the slice and/or the application itself
|
241
|
-
if (provider = application_provider(subclass)) && provider != application_provider(subclass.superclass)
|
242
|
-
subclass.include ApplicationView.new(provider)
|
236
|
+
exposures.each do |name, exposure|
|
237
|
+
klass.exposures.import(name, exposure)
|
243
238
|
end
|
244
239
|
end
|
245
240
|
|
246
|
-
|
247
|
-
|
248
|
-
|
241
|
+
# @!group Exposures
|
242
|
+
|
243
|
+
# @!macro [new] exposure_options
|
244
|
+
# @param options [Hash] the exposure's options
|
245
|
+
# @option options [Boolean] :layout expose this value to the layout (defaults to false)
|
246
|
+
# @option options [Boolean] :decorate decorate this value in a matching Part (defaults to
|
247
|
+
# true)
|
248
|
+
# @option options [Symbol, Class] :as an alternative name or class to use when finding a
|
249
|
+
# matching Part
|
250
|
+
|
251
|
+
# @overload expose(name, **options, &block)
|
252
|
+
# Define a value to be passed to the template. The return value of the
|
253
|
+
# block will be decorated by a matching Part and passed to the template.
|
254
|
+
#
|
255
|
+
# The block will be evaluated with the view instance as its `self`. The
|
256
|
+
# block's parameters will determine what it is given:
|
257
|
+
#
|
258
|
+
# - To receive other exposure values, provide positional parameters
|
259
|
+
# matching the exposure names. These exposures will already by decorated
|
260
|
+
# by their Parts.
|
261
|
+
# - To receive the view's input arguments (whatever is passed to
|
262
|
+
# `View#call`), provide matching keyword parameters. You can provide
|
263
|
+
# default values for these parameters to make the corresponding input
|
264
|
+
# keys optional
|
265
|
+
# - To receive the Context object, provide a `context:` keyword parameter
|
266
|
+
# - To receive the view's input arguments in their entirety, provide a
|
267
|
+
# keywords splat parameter (i.e. `**input`)
|
268
|
+
#
|
269
|
+
# @example Accessing input arguments
|
270
|
+
# expose :article do |slug:|
|
271
|
+
# article_repo.find_by_slug(slug)
|
272
|
+
# end
|
273
|
+
#
|
274
|
+
# @example Accessing other exposures
|
275
|
+
# expose :articles do
|
276
|
+
# article_repo.listing
|
277
|
+
# end
|
278
|
+
#
|
279
|
+
# expose :featured_articles do |articles|
|
280
|
+
# articles.select(&:featured?)
|
281
|
+
# end
|
282
|
+
#
|
283
|
+
# @param name [Symbol] name for the exposure
|
284
|
+
# @macro exposure_options
|
285
|
+
#
|
286
|
+
# @overload expose(name, **options)
|
287
|
+
# Define a value to be passed to the template, provided by an instance
|
288
|
+
# method matching the name. The method's return value will be decorated by
|
289
|
+
# a matching Part and passed to the template.
|
290
|
+
#
|
291
|
+
# The method's parameters will determine what it is given:
|
292
|
+
#
|
293
|
+
# - To receive other exposure values, provide positional parameters
|
294
|
+
# matching the exposure names. These exposures will already by decorated
|
295
|
+
# by their Parts.
|
296
|
+
# - To receive the view's input arguments (whatever is passed to
|
297
|
+
# `View#call`), provide matching keyword parameters. You can provide
|
298
|
+
# default values for these parameters to make the corresponding input
|
299
|
+
# keys optional
|
300
|
+
# - To receive the Context object, provide a `context:` keyword parameter
|
301
|
+
# - To receive the view's input arguments in their entirey, provide a
|
302
|
+
# keywords splat parameter (i.e. `**input`)
|
303
|
+
#
|
304
|
+
# @example Accessing input arguments
|
305
|
+
# expose :article
|
306
|
+
#
|
307
|
+
# def article(slug:)
|
308
|
+
# article_repo.find_by_slug(slug)
|
309
|
+
# end
|
310
|
+
#
|
311
|
+
# @example Accessing other exposures
|
312
|
+
# expose :articles
|
313
|
+
# expose :featured_articles
|
314
|
+
#
|
315
|
+
# def articles
|
316
|
+
# article_repo.listing
|
317
|
+
# end
|
318
|
+
#
|
319
|
+
# def featured_articles
|
320
|
+
# articles.select(&:featured?)
|
321
|
+
# end
|
322
|
+
#
|
323
|
+
# @param name [Symbol] name for the exposure
|
324
|
+
# @macro exposure_options
|
325
|
+
#
|
326
|
+
# @overload expose(name, **options)
|
327
|
+
# Define a single value to pass through from the input data (when there is
|
328
|
+
# no instance method matching the `name`). This value will be decorated by
|
329
|
+
# a matching Part and passed to the template.
|
330
|
+
#
|
331
|
+
# @param name [Symbol] name for the exposure
|
332
|
+
# @macro exposure_options
|
333
|
+
# @option options [Boolean] :default a default value to provide if there is no matching
|
334
|
+
# input data
|
335
|
+
#
|
336
|
+
# @overload expose(*names, **options)
|
337
|
+
# Define multiple values to pass through from the input data (when there
|
338
|
+
# is no instance methods matching their names). These values will be
|
339
|
+
# decorated by matching Parts and passed through to the template.
|
340
|
+
#
|
341
|
+
# The provided options will be applied to all the exposures.
|
342
|
+
#
|
343
|
+
# @param names [Symbol] names for the exposures
|
344
|
+
# @macro exposure_options
|
345
|
+
# @option options [Boolean] :default a default value to provide if there is no matching
|
346
|
+
# input data
|
347
|
+
#
|
348
|
+
# @see https://dry-rb.org/gems/dry-view/exposures/
|
349
|
+
#
|
350
|
+
# @api public
|
351
|
+
def self.expose(*names, **options, &block)
|
352
|
+
if names.length == 1
|
353
|
+
exposures.add(names.first, block, **options)
|
354
|
+
else
|
355
|
+
names.each do |name|
|
356
|
+
exposures.add(name, **options)
|
357
|
+
end
|
249
358
|
end
|
250
359
|
end
|
251
|
-
|
360
|
+
|
361
|
+
# @api public
|
362
|
+
def self.private_expose(*names, **options, &block)
|
363
|
+
expose(*names, **options, private: true, &block)
|
364
|
+
end
|
365
|
+
|
366
|
+
# Returns the defined exposures. These are unbound, since bound exposures
|
367
|
+
# are only created when initializing a View instance.
|
368
|
+
#
|
369
|
+
# @return [Exposures]
|
370
|
+
# @api private
|
371
|
+
def self.exposures
|
372
|
+
@exposures ||= Exposures.new
|
373
|
+
end
|
374
|
+
|
375
|
+
# @!endgroup
|
376
|
+
|
377
|
+
# @!group Scope
|
378
|
+
|
379
|
+
# Creates and assigns a scope for the current view.
|
380
|
+
#
|
381
|
+
# The newly created scope is useful to add custom logic that is specific
|
382
|
+
# to the view.
|
383
|
+
#
|
384
|
+
# The scope has access to locals, exposures, and inherited scope (if any)
|
385
|
+
#
|
386
|
+
# If the view already has an explicit scope the newly created scope will
|
387
|
+
# inherit from the explicit scope.
|
388
|
+
#
|
389
|
+
# There are two cases when this may happen:
|
390
|
+
# 1. The scope was explicitly assigned (e.g. `config.scope = MyScope`)
|
391
|
+
# 2. The scope has been inherited by the view superclass
|
392
|
+
#
|
393
|
+
# If the view doesn't have an already existing scope, the newly scope
|
394
|
+
# will inherit from `Hanami::View::Scope` by default.
|
395
|
+
#
|
396
|
+
# However, you can specify any base class for it. This is not
|
397
|
+
# recommended, unless you know what you're doing.
|
398
|
+
#
|
399
|
+
# @param scope [Hanami::View::Scope] the current scope (if any), or the
|
400
|
+
# default base class will be `Hanami::View::Scope`
|
401
|
+
# @param block [Proc] the scope logic definition
|
402
|
+
#
|
403
|
+
# @api public
|
404
|
+
#
|
405
|
+
# @example Basic usage
|
406
|
+
# class MyView < Hanami::View
|
407
|
+
# config.scope = MyScope
|
408
|
+
#
|
409
|
+
# scope do
|
410
|
+
# def greeting
|
411
|
+
# _locals[:message].upcase + "!"
|
412
|
+
# end
|
413
|
+
#
|
414
|
+
# def copyright(time)
|
415
|
+
# "Copy #{time.year}"
|
416
|
+
# end
|
417
|
+
# end
|
418
|
+
# end
|
419
|
+
#
|
420
|
+
# # my_view.html.erb
|
421
|
+
# # <%= greeting %>
|
422
|
+
# # <%= copyright(Time.now.utc) %>
|
423
|
+
#
|
424
|
+
# MyView.new.(message: "Hello") # => "HELLO!"
|
425
|
+
#
|
426
|
+
# @example Inherited scope
|
427
|
+
# class MyScope < Hanami::View::Scope
|
428
|
+
# private
|
429
|
+
#
|
430
|
+
# def shout(string)
|
431
|
+
# string.upcase + "!"
|
432
|
+
# end
|
433
|
+
# end
|
434
|
+
#
|
435
|
+
# class MyView < Hanami::View
|
436
|
+
# config.scope = MyScope
|
437
|
+
#
|
438
|
+
# scope do
|
439
|
+
# def greeting
|
440
|
+
# shout(_locals[:message])
|
441
|
+
# end
|
442
|
+
#
|
443
|
+
# def copyright(time)
|
444
|
+
# "Copy #{time.year}"
|
445
|
+
# end
|
446
|
+
# end
|
447
|
+
# end
|
448
|
+
#
|
449
|
+
# # my_view.html.erb
|
450
|
+
# # <%= greeting %>
|
451
|
+
# # <%= copyright(Time.now.utc) %>
|
452
|
+
#
|
453
|
+
# MyView.new.(message: "Hello") # => "HELLO!"
|
454
|
+
def self.scope(base: config.scope || Hanami::View::Scope, &block)
|
455
|
+
config.scope = Class.new(base, &block)
|
456
|
+
end
|
457
|
+
|
458
|
+
# @!endgroup
|
459
|
+
|
460
|
+
# @!group Render environment
|
461
|
+
|
462
|
+
# Returns a render environment for the view and the given options. This
|
463
|
+
# environment isn't chdir'ed into any particular directory.
|
464
|
+
#
|
465
|
+
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
466
|
+
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
467
|
+
#
|
468
|
+
# @see View.template_env render environment for the view's template
|
469
|
+
# @see View.layout_env render environment for the view's layout
|
470
|
+
#
|
471
|
+
# @return [RenderEnvironment]
|
472
|
+
# @api public
|
473
|
+
def self.render_env(format: config.default_format, context: config.default_context)
|
474
|
+
RenderEnvironment.prepare(renderer(format), config, context)
|
475
|
+
end
|
476
|
+
|
477
|
+
# @overload template_env(format: config.default_format, context: config.default_context)
|
478
|
+
# Returns a render environment for the view and the given options,
|
479
|
+
# chdir'ed into the view's template directory. This is the environment
|
480
|
+
# used when rendering the template, and is useful to to fetch
|
481
|
+
# independently when unit testing Parts and Scopes.
|
482
|
+
#
|
483
|
+
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
484
|
+
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
485
|
+
#
|
486
|
+
# @return [RenderEnvironment]
|
487
|
+
# @api public
|
488
|
+
def self.template_env(**args)
|
489
|
+
render_env(**args).chdir(config.template)
|
490
|
+
end
|
491
|
+
|
492
|
+
# @overload layout_env(format: config.default_format, context: config.default_context)
|
493
|
+
# Returns a render environment for the view and the given options,
|
494
|
+
# chdir'ed into the view's layout directory. This is the environment used
|
495
|
+
# when rendering the view's layout.
|
496
|
+
#
|
497
|
+
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
498
|
+
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
499
|
+
#
|
500
|
+
# @return [RenderEnvironment] @api public
|
501
|
+
def self.layout_env(**args)
|
502
|
+
render_env(**args).chdir(layout_path)
|
503
|
+
end
|
504
|
+
|
505
|
+
# Returns renderer for the view and provided format
|
506
|
+
#
|
507
|
+
# @api private
|
508
|
+
def self.renderer(format)
|
509
|
+
fetch_or_store(:renderer, config, format) {
|
510
|
+
Renderer.new(
|
511
|
+
config.paths,
|
512
|
+
format: format,
|
513
|
+
engine_mapping: config.renderer_engine_mapping,
|
514
|
+
**config.renderer_options
|
515
|
+
)
|
516
|
+
}
|
517
|
+
end
|
518
|
+
|
519
|
+
# @api private
|
520
|
+
def self.layout_path
|
521
|
+
File.join(*[config.layouts_dir, config.layout].compact)
|
522
|
+
end
|
523
|
+
|
524
|
+
# @!endgroup
|
525
|
+
|
526
|
+
# Returns an instance of the view. This binds the defined exposures to the
|
527
|
+
# view instance.
|
528
|
+
#
|
529
|
+
# Subclasses can define their own `#initialize` to accept injected
|
530
|
+
# dependencies, but must call `super()` to ensure the standard view
|
531
|
+
# initialization can proceed.
|
532
|
+
#
|
533
|
+
# @api public
|
534
|
+
def initialize
|
535
|
+
@exposures = self.class.exposures.bind(self)
|
536
|
+
end
|
537
|
+
|
538
|
+
# The view's configuration
|
539
|
+
#
|
540
|
+
# @api private
|
541
|
+
def config
|
542
|
+
self.class.config
|
543
|
+
end
|
544
|
+
|
545
|
+
# The view's bound exposures
|
546
|
+
#
|
547
|
+
# @return [Exposures]
|
548
|
+
# @api private
|
549
|
+
def exposures
|
550
|
+
@exposures
|
551
|
+
end
|
552
|
+
|
553
|
+
# Render the view
|
554
|
+
#
|
555
|
+
# @param format [Symbol] template format to use
|
556
|
+
# @param context [Context] context object to use
|
557
|
+
# @param input input data for preparing exposure values
|
558
|
+
#
|
559
|
+
# @return [Rendered] rendered view object
|
560
|
+
# @api public
|
561
|
+
def call(format: config.default_format, context: config.default_context, **input)
|
562
|
+
ensure_config
|
563
|
+
|
564
|
+
env = self.class.render_env(format: format, context: context)
|
565
|
+
template_env = self.class.template_env(format: format, context: context)
|
566
|
+
|
567
|
+
locals = locals(template_env, input)
|
568
|
+
output = env.template(config.template, template_env.scope(config.scope, locals))
|
569
|
+
|
570
|
+
if layout?
|
571
|
+
layout_env = self.class.layout_env(format: format, context: context)
|
572
|
+
begin
|
573
|
+
output = env.template(
|
574
|
+
self.class.layout_path,
|
575
|
+
layout_env.scope(config.scope, layout_locals(locals))
|
576
|
+
) { output }
|
577
|
+
rescue TemplateNotFoundError
|
578
|
+
raise LayoutNotFoundError.new(config.layout, config.paths)
|
579
|
+
end
|
580
|
+
end
|
581
|
+
|
582
|
+
Rendered.new(output: output, locals: locals)
|
583
|
+
end
|
584
|
+
|
585
|
+
private
|
586
|
+
|
587
|
+
# @api private
|
588
|
+
def ensure_config
|
589
|
+
raise UndefinedConfigError, :paths unless Array(config.paths).any?
|
590
|
+
raise UndefinedConfigError, :template unless config.template
|
591
|
+
end
|
592
|
+
|
593
|
+
# @api private
|
594
|
+
def locals(render_env, input)
|
595
|
+
exposures.(context: render_env.context, **input) do |value, exposure|
|
596
|
+
if exposure.decorate? && value
|
597
|
+
render_env.part(exposure.name, value, **exposure.options)
|
598
|
+
else
|
599
|
+
value
|
600
|
+
end
|
601
|
+
end
|
602
|
+
end
|
603
|
+
|
604
|
+
# @api private
|
605
|
+
def layout_locals(locals)
|
606
|
+
locals.each_with_object({}) do |(key, value), layout_locals|
|
607
|
+
layout_locals[key] = value if exposures[key].for_layout?
|
608
|
+
end
|
609
|
+
end
|
610
|
+
|
611
|
+
# @api private
|
612
|
+
def layout?
|
613
|
+
!!config.layout # rubocop:disable Style/DoubleNegation
|
614
|
+
end
|
252
615
|
end
|
253
616
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hanami-view
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.0.
|
4
|
+
version: 2.0.0.alpha8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tim Riley
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2022-
|
12
|
+
date: 2022-05-19 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: concurrent-ruby
|
@@ -156,8 +156,6 @@ files:
|
|
156
156
|
- hanami-view.gemspec
|
157
157
|
- lib/hanami-view.rb
|
158
158
|
- lib/hanami/view.rb
|
159
|
-
- lib/hanami/view/application_configuration.rb
|
160
|
-
- lib/hanami/view/application_context.rb
|
161
159
|
- lib/hanami/view/application_view.rb
|
162
160
|
- lib/hanami/view/context.rb
|
163
161
|
- lib/hanami/view/context_helpers/content_helpers.rb
|
@@ -174,7 +172,6 @@ files:
|
|
174
172
|
- lib/hanami/view/renderer.rb
|
175
173
|
- lib/hanami/view/scope.rb
|
176
174
|
- lib/hanami/view/scope_builder.rb
|
177
|
-
- lib/hanami/view/standalone_view.rb
|
178
175
|
- lib/hanami/view/tilt.rb
|
179
176
|
- lib/hanami/view/tilt/erb.rb
|
180
177
|
- lib/hanami/view/tilt/erbse.rb
|
@@ -204,7 +201,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
204
201
|
- !ruby/object:Gem::Version
|
205
202
|
version: 1.3.1
|
206
203
|
requirements: []
|
207
|
-
rubygems_version: 3.3.
|
204
|
+
rubygems_version: 3.3.7
|
208
205
|
signing_key:
|
209
206
|
specification_version: 4
|
210
207
|
summary: A complete, standalone view rendering system that gives you everything you
|
@@ -1,77 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "dry/configurable"
|
4
|
-
require_relative "../view"
|
5
|
-
|
6
|
-
module Hanami
|
7
|
-
class View
|
8
|
-
class ApplicationConfiguration
|
9
|
-
include Dry::Configurable
|
10
|
-
|
11
|
-
setting :parts_path, default: "view/parts"
|
12
|
-
|
13
|
-
def initialize(*)
|
14
|
-
super
|
15
|
-
|
16
|
-
@base_config = View.config.dup
|
17
|
-
|
18
|
-
configure_defaults
|
19
|
-
end
|
20
|
-
|
21
|
-
# Returns the list of available settings
|
22
|
-
#
|
23
|
-
# @return [Set]
|
24
|
-
#
|
25
|
-
# @since 2.0.0
|
26
|
-
# @api private
|
27
|
-
def settings
|
28
|
-
self.class.settings + View.settings - NON_FORWARDABLE_METHODS
|
29
|
-
end
|
30
|
-
|
31
|
-
def finalize!
|
32
|
-
return self if frozen?
|
33
|
-
|
34
|
-
base_config.finalize!
|
35
|
-
|
36
|
-
super
|
37
|
-
end
|
38
|
-
|
39
|
-
private
|
40
|
-
|
41
|
-
attr_reader :base_config
|
42
|
-
|
43
|
-
def configure_defaults
|
44
|
-
self.paths = ["templates"]
|
45
|
-
self.template_inference_base = "views"
|
46
|
-
self.layout = "application"
|
47
|
-
end
|
48
|
-
|
49
|
-
# An inflector for views is not configurable via `config.views.inflector` on an
|
50
|
-
# `Hanami::Application`. The application-wide inflector is already configurable
|
51
|
-
# there as `config.inflector` and will be used as the default inflector for views.
|
52
|
-
#
|
53
|
-
# A custom inflector may still be provided in an `Hanami::View` subclass, via
|
54
|
-
# `config.inflector=`.
|
55
|
-
NON_FORWARDABLE_METHODS = [:inflector, :inflector=].freeze
|
56
|
-
private_constant :NON_FORWARDABLE_METHODS
|
57
|
-
|
58
|
-
def method_missing(name, *args, &block)
|
59
|
-
return super if NON_FORWARDABLE_METHODS.include?(name)
|
60
|
-
|
61
|
-
if config.respond_to?(name)
|
62
|
-
config.public_send(name, *args, &block)
|
63
|
-
elsif base_config.respond_to?(name)
|
64
|
-
base_config.public_send(name, *args, &block)
|
65
|
-
else
|
66
|
-
super
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def respond_to_missing?(name, _include_all = false)
|
71
|
-
return false if NON_FORWARDABLE_METHODS.include?(name)
|
72
|
-
|
73
|
-
config.respond_to?(name) || base_config.respond_to?(name) || super
|
74
|
-
end
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
@@ -1,98 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "hanami/view/errors"
|
4
|
-
|
5
|
-
module Hanami
|
6
|
-
class View
|
7
|
-
class ApplicationContext < Module
|
8
|
-
attr_reader :provider
|
9
|
-
attr_reader :application
|
10
|
-
|
11
|
-
def initialize(provider)
|
12
|
-
@provider = provider
|
13
|
-
@application = provider.respond_to?(:application) ? provider.application : Hanami.application
|
14
|
-
end
|
15
|
-
|
16
|
-
def included(context_class)
|
17
|
-
define_initialize
|
18
|
-
context_class.include(InstanceMethods)
|
19
|
-
end
|
20
|
-
|
21
|
-
private
|
22
|
-
|
23
|
-
def define_initialize
|
24
|
-
inflector = application.inflector
|
25
|
-
settings = application[:settings] if application.key?(:settings)
|
26
|
-
routes = application[:routes_helper] if application.key?(:routes_helper)
|
27
|
-
assets = application[:assets] if application.key?(:assets)
|
28
|
-
|
29
|
-
define_method :initialize do |**options|
|
30
|
-
@inflector = options[:inflector] || inflector
|
31
|
-
@settings = options[:settings] || settings
|
32
|
-
@routes = options[:routes] || routes
|
33
|
-
@assets = options[:assets] || assets
|
34
|
-
super(**options)
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
module InstanceMethods
|
39
|
-
attr_reader :inflector
|
40
|
-
attr_reader :routes
|
41
|
-
attr_reader :settings
|
42
|
-
|
43
|
-
def initialize(**args)
|
44
|
-
defaults = {content: {}}
|
45
|
-
|
46
|
-
super(**defaults.merge(args))
|
47
|
-
end
|
48
|
-
|
49
|
-
def content_for(key, value = nil, &block)
|
50
|
-
content = _options[:content]
|
51
|
-
output = nil
|
52
|
-
|
53
|
-
if block
|
54
|
-
content[key] = yield
|
55
|
-
elsif value
|
56
|
-
content[key] = value
|
57
|
-
else
|
58
|
-
output = content[key]
|
59
|
-
end
|
60
|
-
|
61
|
-
output
|
62
|
-
end
|
63
|
-
|
64
|
-
def current_path
|
65
|
-
request.fullpath
|
66
|
-
end
|
67
|
-
|
68
|
-
def csrf_token
|
69
|
-
request.session[Hanami::Action::CSRFProtection::CSRF_TOKEN]
|
70
|
-
end
|
71
|
-
|
72
|
-
def request
|
73
|
-
_options.fetch(:request)
|
74
|
-
end
|
75
|
-
|
76
|
-
def session
|
77
|
-
request.session
|
78
|
-
end
|
79
|
-
|
80
|
-
def flash
|
81
|
-
response.flash
|
82
|
-
end
|
83
|
-
|
84
|
-
def assets
|
85
|
-
@assets or
|
86
|
-
raise Hanami::View::MissingProviderError.new("hanami-assets")
|
87
|
-
end
|
88
|
-
|
89
|
-
private
|
90
|
-
|
91
|
-
# TODO: create `Request#flash` so we no longer need the `response`
|
92
|
-
def response
|
93
|
-
_options.fetch(:response)
|
94
|
-
end
|
95
|
-
end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|
@@ -1,400 +0,0 @@
|
|
1
|
-
require_relative "scope"
|
2
|
-
|
3
|
-
module Hanami
|
4
|
-
class View
|
5
|
-
module StandaloneView
|
6
|
-
def self.included(klass)
|
7
|
-
klass.extend ClassMethods
|
8
|
-
klass.include InstanceMethods
|
9
|
-
end
|
10
|
-
|
11
|
-
module ClassMethods
|
12
|
-
# @api private
|
13
|
-
def inherited(klass)
|
14
|
-
super
|
15
|
-
|
16
|
-
exposures.each do |name, exposure|
|
17
|
-
klass.exposures.import(name, exposure)
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
# @!group Exposures
|
22
|
-
|
23
|
-
# @!macro [new] exposure_options
|
24
|
-
# @param options [Hash] the exposure's options
|
25
|
-
# @option options [Boolean] :layout expose this value to the layout (defaults to false)
|
26
|
-
# @option options [Boolean] :decorate decorate this value in a matching Part (defaults to
|
27
|
-
# true)
|
28
|
-
# @option options [Symbol, Class] :as an alternative name or class to use when finding a
|
29
|
-
# matching Part
|
30
|
-
|
31
|
-
# @overload expose(name, **options, &block)
|
32
|
-
# Define a value to be passed to the template. The return value of the
|
33
|
-
# block will be decorated by a matching Part and passed to the template.
|
34
|
-
#
|
35
|
-
# The block will be evaluated with the view instance as its `self`. The
|
36
|
-
# block's parameters will determine what it is given:
|
37
|
-
#
|
38
|
-
# - To receive other exposure values, provide positional parameters
|
39
|
-
# matching the exposure names. These exposures will already by decorated
|
40
|
-
# by their Parts.
|
41
|
-
# - To receive the view's input arguments (whatever is passed to
|
42
|
-
# `View#call`), provide matching keyword parameters. You can provide
|
43
|
-
# default values for these parameters to make the corresponding input
|
44
|
-
# keys optional
|
45
|
-
# - To receive the Context object, provide a `context:` keyword parameter
|
46
|
-
# - To receive the view's input arguments in their entirety, provide a
|
47
|
-
# keywords splat parameter (i.e. `**input`)
|
48
|
-
#
|
49
|
-
# @example Accessing input arguments
|
50
|
-
# expose :article do |slug:|
|
51
|
-
# article_repo.find_by_slug(slug)
|
52
|
-
# end
|
53
|
-
#
|
54
|
-
# @example Accessing other exposures
|
55
|
-
# expose :articles do
|
56
|
-
# article_repo.listing
|
57
|
-
# end
|
58
|
-
#
|
59
|
-
# expose :featured_articles do |articles|
|
60
|
-
# articles.select(&:featured?)
|
61
|
-
# end
|
62
|
-
#
|
63
|
-
# @param name [Symbol] name for the exposure
|
64
|
-
# @macro exposure_options
|
65
|
-
#
|
66
|
-
# @overload expose(name, **options)
|
67
|
-
# Define a value to be passed to the template, provided by an instance
|
68
|
-
# method matching the name. The method's return value will be decorated by
|
69
|
-
# a matching Part and passed to the template.
|
70
|
-
#
|
71
|
-
# The method's parameters will determine what it is given:
|
72
|
-
#
|
73
|
-
# - To receive other exposure values, provide positional parameters
|
74
|
-
# matching the exposure names. These exposures will already by decorated
|
75
|
-
# by their Parts.
|
76
|
-
# - To receive the view's input arguments (whatever is passed to
|
77
|
-
# `View#call`), provide matching keyword parameters. You can provide
|
78
|
-
# default values for these parameters to make the corresponding input
|
79
|
-
# keys optional
|
80
|
-
# - To receive the Context object, provide a `context:` keyword parameter
|
81
|
-
# - To receive the view's input arguments in their entirey, provide a
|
82
|
-
# keywords splat parameter (i.e. `**input`)
|
83
|
-
#
|
84
|
-
# @example Accessing input arguments
|
85
|
-
# expose :article
|
86
|
-
#
|
87
|
-
# def article(slug:)
|
88
|
-
# article_repo.find_by_slug(slug)
|
89
|
-
# end
|
90
|
-
#
|
91
|
-
# @example Accessing other exposures
|
92
|
-
# expose :articles
|
93
|
-
# expose :featured_articles
|
94
|
-
#
|
95
|
-
# def articles
|
96
|
-
# article_repo.listing
|
97
|
-
# end
|
98
|
-
#
|
99
|
-
# def featured_articles
|
100
|
-
# articles.select(&:featured?)
|
101
|
-
# end
|
102
|
-
#
|
103
|
-
# @param name [Symbol] name for the exposure
|
104
|
-
# @macro exposure_options
|
105
|
-
#
|
106
|
-
# @overload expose(name, **options)
|
107
|
-
# Define a single value to pass through from the input data (when there is
|
108
|
-
# no instance method matching the `name`). This value will be decorated by
|
109
|
-
# a matching Part and passed to the template.
|
110
|
-
#
|
111
|
-
# @param name [Symbol] name for the exposure
|
112
|
-
# @macro exposure_options
|
113
|
-
# @option options [Boolean] :default a default value to provide if there is no matching
|
114
|
-
# input data
|
115
|
-
#
|
116
|
-
# @overload expose(*names, **options)
|
117
|
-
# Define multiple values to pass through from the input data (when there
|
118
|
-
# is no instance methods matching their names). These values will be
|
119
|
-
# decorated by matching Parts and passed through to the template.
|
120
|
-
#
|
121
|
-
# The provided options will be applied to all the exposures.
|
122
|
-
#
|
123
|
-
# @param names [Symbol] names for the exposures
|
124
|
-
# @macro exposure_options
|
125
|
-
# @option options [Boolean] :default a default value to provide if there is no matching
|
126
|
-
# input data
|
127
|
-
#
|
128
|
-
# @see https://dry-rb.org/gems/dry-view/exposures/
|
129
|
-
#
|
130
|
-
# @api public
|
131
|
-
def expose(*names, **options, &block)
|
132
|
-
if names.length == 1
|
133
|
-
exposures.add(names.first, block, **options)
|
134
|
-
else
|
135
|
-
names.each do |name|
|
136
|
-
exposures.add(name, **options)
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
# @api public
|
142
|
-
def private_expose(*names, **options, &block)
|
143
|
-
expose(*names, **options, private: true, &block)
|
144
|
-
end
|
145
|
-
|
146
|
-
# Returns the defined exposures. These are unbound, since bound exposures
|
147
|
-
# are only created when initializing a View instance.
|
148
|
-
#
|
149
|
-
# @return [Exposures]
|
150
|
-
# @api private
|
151
|
-
def exposures
|
152
|
-
@exposures ||= Exposures.new
|
153
|
-
end
|
154
|
-
|
155
|
-
# @!endgroup
|
156
|
-
|
157
|
-
# @!group Scope
|
158
|
-
|
159
|
-
# Creates and assigns a scope for the current view.
|
160
|
-
#
|
161
|
-
# The newly created scope is useful to add custom logic that is specific
|
162
|
-
# to the view.
|
163
|
-
#
|
164
|
-
# The scope has access to locals, exposures, and inherited scope (if any)
|
165
|
-
#
|
166
|
-
# If the view already has an explicit scope the newly created scope will
|
167
|
-
# inherit from the explicit scope.
|
168
|
-
#
|
169
|
-
# There are two cases when this may happen:
|
170
|
-
# 1. The scope was explicitly assigned (e.g. `config.scope = MyScope`)
|
171
|
-
# 2. The scope has been inherited by the view superclass
|
172
|
-
#
|
173
|
-
# If the view doesn't have an already existing scope, the newly scope
|
174
|
-
# will inherit from `Hanami::View::Scope` by default.
|
175
|
-
#
|
176
|
-
# However, you can specify any base class for it. This is not
|
177
|
-
# recommended, unless you know what you're doing.
|
178
|
-
#
|
179
|
-
# @param scope [Hanami::View::Scope] the current scope (if any), or the
|
180
|
-
# default base class will be `Hanami::View::Scope`
|
181
|
-
# @param block [Proc] the scope logic definition
|
182
|
-
#
|
183
|
-
# @api public
|
184
|
-
#
|
185
|
-
# @example Basic usage
|
186
|
-
# class MyView < Hanami::View
|
187
|
-
# config.scope = MyScope
|
188
|
-
#
|
189
|
-
# scope do
|
190
|
-
# def greeting
|
191
|
-
# _locals[:message].upcase + "!"
|
192
|
-
# end
|
193
|
-
#
|
194
|
-
# def copyright(time)
|
195
|
-
# "Copy #{time.year}"
|
196
|
-
# end
|
197
|
-
# end
|
198
|
-
# end
|
199
|
-
#
|
200
|
-
# # my_view.html.erb
|
201
|
-
# # <%= greeting %>
|
202
|
-
# # <%= copyright(Time.now.utc) %>
|
203
|
-
#
|
204
|
-
# MyView.new.(message: "Hello") # => "HELLO!"
|
205
|
-
#
|
206
|
-
# @example Inherited scope
|
207
|
-
# class MyScope < Hanami::View::Scope
|
208
|
-
# private
|
209
|
-
#
|
210
|
-
# def shout(string)
|
211
|
-
# string.upcase + "!"
|
212
|
-
# end
|
213
|
-
# end
|
214
|
-
#
|
215
|
-
# class MyView < Hanami::View
|
216
|
-
# config.scope = MyScope
|
217
|
-
#
|
218
|
-
# scope do
|
219
|
-
# def greeting
|
220
|
-
# shout(_locals[:message])
|
221
|
-
# end
|
222
|
-
#
|
223
|
-
# def copyright(time)
|
224
|
-
# "Copy #{time.year}"
|
225
|
-
# end
|
226
|
-
# end
|
227
|
-
# end
|
228
|
-
#
|
229
|
-
# # my_view.html.erb
|
230
|
-
# # <%= greeting %>
|
231
|
-
# # <%= copyright(Time.now.utc) %>
|
232
|
-
#
|
233
|
-
# MyView.new.(message: "Hello") # => "HELLO!"
|
234
|
-
def scope(base: config.scope || Hanami::View::Scope, &block)
|
235
|
-
config.scope = Class.new(base, &block)
|
236
|
-
end
|
237
|
-
|
238
|
-
# @!endgroup
|
239
|
-
|
240
|
-
# @!group Render environment
|
241
|
-
|
242
|
-
# Returns a render environment for the view and the given options. This
|
243
|
-
# environment isn't chdir'ed into any particular directory.
|
244
|
-
#
|
245
|
-
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
246
|
-
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
247
|
-
#
|
248
|
-
# @see View.template_env render environment for the view's template
|
249
|
-
# @see View.layout_env render environment for the view's layout
|
250
|
-
#
|
251
|
-
# @return [RenderEnvironment]
|
252
|
-
# @api public
|
253
|
-
def render_env(format: config.default_format, context: config.default_context)
|
254
|
-
RenderEnvironment.prepare(renderer(format), config, context)
|
255
|
-
end
|
256
|
-
|
257
|
-
# @overload template_env(format: config.default_format, context: config.default_context)
|
258
|
-
# Returns a render environment for the view and the given options,
|
259
|
-
# chdir'ed into the view's template directory. This is the environment
|
260
|
-
# used when rendering the template, and is useful to to fetch
|
261
|
-
# independently when unit testing Parts and Scopes.
|
262
|
-
#
|
263
|
-
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
264
|
-
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
265
|
-
#
|
266
|
-
# @return [RenderEnvironment]
|
267
|
-
# @api public
|
268
|
-
def template_env(**args)
|
269
|
-
render_env(**args).chdir(config.template)
|
270
|
-
end
|
271
|
-
|
272
|
-
# @overload layout_env(format: config.default_format, context: config.default_context)
|
273
|
-
# Returns a render environment for the view and the given options,
|
274
|
-
# chdir'ed into the view's layout directory. This is the environment used
|
275
|
-
# when rendering the view's layout.
|
276
|
-
#
|
277
|
-
# @param format [Symbol] template format to use (defaults to the `default_format` setting)
|
278
|
-
# @param context [Context] context object to use (defaults to the `default_context` setting)
|
279
|
-
#
|
280
|
-
# @return [RenderEnvironment] @api public
|
281
|
-
def layout_env(**args)
|
282
|
-
render_env(**args).chdir(layout_path)
|
283
|
-
end
|
284
|
-
|
285
|
-
# Returns renderer for the view and provided format
|
286
|
-
#
|
287
|
-
# @api private
|
288
|
-
def renderer(format)
|
289
|
-
fetch_or_store(:renderer, config, format) {
|
290
|
-
Renderer.new(
|
291
|
-
config.paths,
|
292
|
-
format: format,
|
293
|
-
engine_mapping: config.renderer_engine_mapping,
|
294
|
-
**config.renderer_options
|
295
|
-
)
|
296
|
-
}
|
297
|
-
end
|
298
|
-
|
299
|
-
# @api private
|
300
|
-
def layout_path
|
301
|
-
File.join(*[config.layouts_dir, config.layout].compact)
|
302
|
-
end
|
303
|
-
|
304
|
-
# @!endgroup
|
305
|
-
end
|
306
|
-
|
307
|
-
module InstanceMethods
|
308
|
-
# Returns an instance of the view. This binds the defined exposures to the
|
309
|
-
# view instance.
|
310
|
-
#
|
311
|
-
# Subclasses can define their own `#initialize` to accept injected
|
312
|
-
# dependencies, but must call `super()` to ensure the standard view
|
313
|
-
# initialization can proceed.
|
314
|
-
#
|
315
|
-
# @api public
|
316
|
-
def initialize
|
317
|
-
@exposures = self.class.exposures.bind(self)
|
318
|
-
end
|
319
|
-
|
320
|
-
# The view's configuration
|
321
|
-
#
|
322
|
-
# @api private
|
323
|
-
def config
|
324
|
-
self.class.config
|
325
|
-
end
|
326
|
-
|
327
|
-
# The view's bound exposures
|
328
|
-
#
|
329
|
-
# @return [Exposures]
|
330
|
-
# @api private
|
331
|
-
def exposures
|
332
|
-
@exposures
|
333
|
-
end
|
334
|
-
|
335
|
-
# Render the view
|
336
|
-
#
|
337
|
-
# @param format [Symbol] template format to use
|
338
|
-
# @param context [Context] context object to use
|
339
|
-
# @param input input data for preparing exposure values
|
340
|
-
#
|
341
|
-
# @return [Rendered] rendered view object
|
342
|
-
# @api public
|
343
|
-
def call(format: config.default_format, context: config.default_context, **input)
|
344
|
-
ensure_config
|
345
|
-
|
346
|
-
env = self.class.render_env(format: format, context: context)
|
347
|
-
template_env = self.class.template_env(format: format, context: context)
|
348
|
-
|
349
|
-
locals = locals(template_env, input)
|
350
|
-
output = env.template(config.template, template_env.scope(config.scope, locals))
|
351
|
-
|
352
|
-
if layout?
|
353
|
-
layout_env = self.class.layout_env(format: format, context: context)
|
354
|
-
begin
|
355
|
-
output = env.template(
|
356
|
-
self.class.layout_path,
|
357
|
-
layout_env.scope(config.scope, layout_locals(locals))
|
358
|
-
) { output }
|
359
|
-
rescue TemplateNotFoundError
|
360
|
-
raise LayoutNotFoundError.new(config.layout, config.paths)
|
361
|
-
end
|
362
|
-
end
|
363
|
-
|
364
|
-
Rendered.new(output: output, locals: locals)
|
365
|
-
end
|
366
|
-
|
367
|
-
private
|
368
|
-
|
369
|
-
# @api private
|
370
|
-
def ensure_config
|
371
|
-
raise UndefinedConfigError, :paths unless Array(config.paths).any?
|
372
|
-
raise UndefinedConfigError, :template unless config.template
|
373
|
-
end
|
374
|
-
|
375
|
-
# @api private
|
376
|
-
def locals(render_env, input)
|
377
|
-
exposures.(context: render_env.context, **input) do |value, exposure|
|
378
|
-
if exposure.decorate? && value
|
379
|
-
render_env.part(exposure.name, value, **exposure.options)
|
380
|
-
else
|
381
|
-
value
|
382
|
-
end
|
383
|
-
end
|
384
|
-
end
|
385
|
-
|
386
|
-
# @api private
|
387
|
-
def layout_locals(locals)
|
388
|
-
locals.each_with_object({}) do |(key, value), layout_locals|
|
389
|
-
layout_locals[key] = value if exposures[key].for_layout?
|
390
|
-
end
|
391
|
-
end
|
392
|
-
|
393
|
-
# @api private
|
394
|
-
def layout?
|
395
|
-
!!config.layout # rubocop:disable Style/DoubleNegation
|
396
|
-
end
|
397
|
-
end
|
398
|
-
end
|
399
|
-
end
|
400
|
-
end
|