hanami-view 2.0.0.alpha5 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec11d28fb1ab17f2de3bfa384fce768648a6975f6f5261795b239069733a5df2
4
- data.tar.gz: 6f631fa7bdd6528d406534f074ec7df1823e46a4ae692a0c4b83e6c8ba30efbf
3
+ metadata.gz: d49c664876b44d1be9f65e7d9c472a3fe50ab40d42002b170a4a2121ddf2f095
4
+ data.tar.gz: 51508250526dd6af93f2f8d1af2031e37c39109896a0b92fac038b99a65e1512
5
5
  SHA512:
6
- metadata.gz: 6ca038a05ffb5361ca9e61820af0487498f3a6dee0842f33f5f51577127e36fd6f97eea13df0984a64cb66d093a0c331a943736856589062310f1b0c2d40eada
7
- data.tar.gz: 3468190981d001cbb87618779abae17ce19700cb7855ac70db09db5895f23cea4056ea580aa967b7a59f1168d6801fc93129184aa4afcfb871682e1386feb0ed
6
+ metadata.gz: e08aff9c75a5c412922f6e7669078e0a88258d8aa6d80b7b56569ce3732a15e703861e7a713381c1e928cbe5e5a67f1ac51e2473a9de85868ca9c895184e838e
7
+ data.tar.gz: cdad93f677870279dad0fb101dbfc03b4da79d78f78f3f1d6de5fdc50b7e7b0f4e69aef6b9b360049aaa2ff45d2e15beb24b2afe6116a87c07a17eb9eb0219eb
data/CHANGELOG.md CHANGED
@@ -1,6 +1,34 @@
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
+
12
+ ## v2.0.0.alpha7 - 2022-03-08
13
+
14
+ ### Added
15
+ - [Luca Guidi] Automatically inject the app's `settings` and `assets` components (if present) into instances of `Hanami::View::ApplicationContext`
16
+ - [Luca Guidi] Temporarily added to `Hanami::View::ApplicationContext` the `#content_for`, `#current_path` `#csrf_token` helpers, ported from the hanami-2-application-template. Some of those helpers will be moved to `hanami-helpers` gem in a later release.
17
+
18
+ ### Changed
19
+ - [Sean Collins] For views within an Hanami application, changed default location for templates from "web/templates" to "templates"
20
+ - [Luca Guidi] For views within an Hanami application, the default `part_namespace` is now `"view/parts"` (previously `"views/parts"`)
21
+
22
+ ## Fixed
23
+ - [Luca Guidi] Application-level configuration is now applied to `Hanami::View` subclasses, no matter how deep their inheritance chain (e.g. app base view -> slice base view -> slice view)
24
+
25
+ ## v2.0.0.alpha6 - 2022-02-10
26
+ ### Added
27
+ - [Luca Guidi] Official support for Ruby: MRI 3.0 and 3.1
28
+
29
+ ### Changed
30
+ - [Luca Guidi] Drop support for Ruby: MRI 2.3, 2.4, 2.5, 2.6, and 2.7.
31
+
4
32
  ## v2.0.0.alpha5 - 2022-01-12
5
33
  ### Added
6
34
  - [Marc Busqué] Automatically provide access to Hanami application routes helper as `routes` in default application view context (`Hanami::View::ApplicationContext`)
data/README.md CHANGED
@@ -26,7 +26,7 @@ A view layer for [Hanami](http://hanamirb.org)
26
26
 
27
27
  ## Rubies
28
28
 
29
- __Hanami::view__ supports Ruby (MRI) 2.6+
29
+ __Hanami::view__ supports Ruby (MRI) 3.0+
30
30
 
31
31
  ## Installation
32
32
 
data/hanami-view.gemspec CHANGED
@@ -18,13 +18,14 @@ Gem::Specification.new do |spec|
18
18
  spec.bindir = 'bin'
19
19
  spec.executables = []
20
20
  spec.require_paths = ['lib']
21
+ spec.metadata["rubygems_mfa_required"] = "true"
21
22
 
22
23
  spec.metadata['allowed_push_host'] = 'https://rubygems.org'
23
24
  spec.metadata['changelog_uri'] = 'https://github.com/hanami/view/blob/main/CHANGELOG.md'
24
25
  spec.metadata['source_code_uri'] = 'https://github.com/hanami/view'
25
26
  spec.metadata['bug_tracker_uri'] = 'https://github.com/hanami/view/issues'
26
27
 
27
- spec.required_ruby_version = ">= 2.4.0"
28
+ spec.required_ruby_version = ">= 3.0"
28
29
 
29
30
  spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
30
31
  spec.add_runtime_dependency "dry-configurable", "~> 0.13", ">= 0.13.0"
@@ -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
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Hanami
4
4
  class View
5
+ # @since 2.0.0
6
+ # @api public
7
+ class Error < StandardError
8
+ end
9
+
5
10
  # Error raised when critical settings are not configured
6
11
  #
7
12
  # @api private
@@ -46,7 +46,7 @@ module Hanami
46
46
  end
47
47
 
48
48
  def render(path, scope, &block)
49
- tilt(path).render(scope, &block)
49
+ tilt(path).render(scope, {locals: scope._locals}, &block)
50
50
  end
51
51
 
52
52
  def chdir(dirname)
@@ -35,7 +35,7 @@ module Hanami
35
35
  # @overload _locals
36
36
  # Returns the locals
37
37
  # @overload locals
38
- # A convenience alias for `#_format.` Is available unless there is a
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>]
@@ -3,6 +3,6 @@
3
3
  module Hanami
4
4
  class View
5
5
  # @api private
6
- VERSION = "2.0.0.alpha5"
6
+ VERSION = "2.0.0.alpha8"
7
7
  end
8
8
  end
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,23 +229,388 @@ module Hanami
230
229
 
231
230
  # @!endgroup
232
231
 
233
- include StandaloneView
234
-
235
- def self.inherited(subclass)
232
+ # @api private
233
+ def self.inherited(klass)
236
234
  super
237
235
 
238
- # If inheriting directly from Hanami::View within an Hanami app, configure
239
- # the view for the application
240
- if subclass.superclass == View && (provider = application_provider(subclass))
241
- subclass.include ApplicationView.new(provider)
236
+ exposures.each do |name, exposure|
237
+ klass.exposures.import(name, exposure)
242
238
  end
243
239
  end
244
240
 
245
- def self.application_provider(subclass)
246
- if Hanami.respond_to?(:application?) && Hanami.application?
247
- Hanami.application.component_provider(subclass)
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
248
358
  end
249
359
  end
250
- private_class_method :application_provider
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
251
615
  end
252
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.alpha5
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-01-12 00:00:00.000000000 Z
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
@@ -184,6 +181,7 @@ homepage: https://dry-rb.org/gems/hanami-view
184
181
  licenses:
185
182
  - MIT
186
183
  metadata:
184
+ rubygems_mfa_required: 'true'
187
185
  allowed_push_host: https://rubygems.org
188
186
  changelog_uri: https://github.com/hanami/view/blob/main/CHANGELOG.md
189
187
  source_code_uri: https://github.com/hanami/view
@@ -196,14 +194,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
196
194
  requirements:
197
195
  - - ">="
198
196
  - !ruby/object:Gem::Version
199
- version: 2.4.0
197
+ version: '3.0'
200
198
  required_rubygems_version: !ruby/object:Gem::Requirement
201
199
  requirements:
202
200
  - - ">"
203
201
  - !ruby/object:Gem::Version
204
202
  version: 1.3.1
205
203
  requirements: []
206
- rubygems_version: 3.2.3
204
+ rubygems_version: 3.3.7
207
205
  signing_key:
208
206
  specification_version: 4
209
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: "views/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 = ["web/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,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Hanami
4
- class View
5
- class ApplicationContext < Module
6
- attr_reader :provider
7
- attr_reader :application
8
-
9
- def initialize(provider)
10
- @provider = provider
11
- @application = provider.respond_to?(:application) ? provider.application : Hanami.application
12
- end
13
-
14
- def included(context_class)
15
- define_initialize
16
- context_class.include(InstanceMethods)
17
- end
18
-
19
- private
20
-
21
- def define_initialize
22
- inflector = application.inflector
23
- routes = application[:routes_helper] if application.key?(:routes_helper)
24
-
25
- define_method :initialize do |**options|
26
- @inflector = options[:inflector] || inflector
27
- @routes = options[:routes] || routes
28
- super(**options)
29
- end
30
- end
31
-
32
- module InstanceMethods
33
- attr_reader :inflector
34
- attr_reader :routes
35
-
36
- def request
37
- _options.fetch(:request)
38
- end
39
-
40
- def session
41
- request.session
42
- end
43
-
44
- def flash
45
- response.flash
46
- end
47
-
48
- private
49
-
50
- # TODO: create `Request#flash` so we no longer need the `response`
51
- def response
52
- _options.fetch(:response)
53
- end
54
- end
55
- end
56
- end
57
- 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