Class: Hanami::View Abstract

Inherits:
Object show all
Extended by:
Dry::Configurable
Defined in:
lib/hanami/view.rb,
lib/hanami/view/html.rb,
lib/hanami/view/part.rb,
lib/hanami/view/path.rb,
lib/hanami/view/tilt.rb,
lib/hanami/view/cache.rb,
lib/hanami/view/scope.rb,
lib/hanami/view/errors.rb,
lib/hanami/view/context.rb,
lib/hanami/view/version.rb,
lib/hanami/view/exposure.rb,
lib/hanami/view/rendered.rb,
lib/hanami/view/renderer.rb,
lib/hanami/view/exposures.rb,
lib/hanami/view/rendering.rb,
lib/hanami/view/erb/engine.rb,
lib/hanami/view/erb/parser.rb,
lib/hanami/view/erb/template.rb,
lib/hanami/view/part_builder.rb,
lib/hanami/view/scope_builder.rb,
lib/hanami/view/erb/filters/block.rb,
lib/hanami/view/rendering_missing.rb,
lib/hanami/view/tilt/haml_adapter.rb,
lib/hanami/view/tilt/slim_adapter.rb,
lib/hanami/view/helpers/tag_helper.rb,
lib/hanami/view/decorated_attributes.rb,
lib/hanami/view/erb/filters/trimming.rb,
lib/hanami/view/helpers/escape_helper.rb,
lib/hanami/view/html_safe_string_buffer.rb,
lib/hanami/view/helpers/tag_helper/tag_builder.rb,
lib/hanami/view/helpers/number_formatting_helper.rb

Overview

This class is abstract.

Subclass this and provide your own configuration and exposures to define your own view (along with a custom #initialize if you wish to inject dependencies into your subclass)

A standalone, template-based view rendering system that offers everything you need to write well-factored view code.

This represents a single view, holding the configuration and exposures necessary for rendering its template.

Since:

  • 2.1.0

Defined Under Namespace

Modules: DecoratedAttributes, ERB, HTML, Helpers, Tilt Classes: Cache, Context, Error, Exposure, Exposures, HTMLSafeStringBuffer, Part, PartBuilder, Path, Rendered, Renderer, Rendering, RenderingMissing, RenderingMissingError, Scope, ScopeBuilder, TemplateNotFoundError, UndefinedConfigError

Constant Summary collapse

DEFAULT_RENDERER_OPTIONS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Since:

  • 2.1.0

{default_encoding: "utf-8"}.freeze
VERSION =

Since:

  • 2.1.0

"3.0.1"

Configuration collapse

Exposures collapse

Scope collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeView

Returns an instance of the view. This binds the defined exposures to the view instance.

Subclasses can define their own #initialize to accept injected dependencies, but must call super() to ensure the standard view initialization can proceed.

Since:

  • 2.1.0



556
557
558
559
560
561
562
# File 'lib/hanami/view.rb', line 556

def initialize
  self.class.config.finalize!
  ensure_config

  @config_data = config.to_data
  @exposures = self.class.exposures.bind(self)
end

Class Method Details

.cacheObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 2.1.0



545
546
547
# File 'lib/hanami/view.rb', line 545

def self.cache
  Cache
end

.decorate(*names, **options, &block) ⇒ Object

Defines an exposure that will be decorated with a matching Part.

This is a shorthand for expose(..., decorate: true).

See Also:

Since:

  • 2.1.0



441
442
443
# File 'lib/hanami/view.rb', line 441

def self.decorate(*names, **options, &block)
  expose(*names, **options, decorate: true, &block)
end

.config.decorate_exposures=(value) ⇒ Object

Controls whether exposures are decorated by default.

When set to true, all exposures will be decorated with matching Parts unless explicitly marked with decorate: false.

When set to false (the default), exposures will not be decorated unless explicitly marked with decorate: true, or declared with decorate.

Defaults to false.

Parameters:

  • value (Boolean)

    whether to decorate exposures by default

Since:

  • 3.0.0

Since:

  • 2.1.0



256
# File 'lib/hanami/view.rb', line 256

setting :decorate_exposures, default: false

.config.default_context=(context) ⇒ Object

Set the default context object to use when rendering. This will be used unless another context object is applied at render-time to View#call

Defaults to a frozen instance of Hanami::View::Context.

Parameters:

See Also:

Since:

  • 2.1.0

Since:

  • 2.1.0



155
# File 'lib/hanami/view.rb', line 155

setting :default_context, default: Context.new.freeze

.config.default_format=(format) ⇒ Object

Set the default format to use when rendering.

Defaults to :html.

Parameters:

  • format (Symbol)

Since:

  • 2.1.0

Since:

  • 2.1.0



166
# File 'lib/hanami/view.rb', line 166

setting :default_format, default: :html

.expose(name, **options, &block) ⇒ Object .expose(name, **options) ⇒ Object .expose(name, **options) ⇒ Object .expose(*names, **options) ⇒ Object

Overloads:

  • .expose(name, **options, &block) ⇒ Object

    Define a value to be passed to the template. The return value of the block will be passed to the template.

    The block will be evaluated with the view instance as its self. The block's parameters will determine what it is given:

    • To receive other exposure values, provide positional parameters matching the exposure names. These exposures will already by decorated by their Parts.
    • To receive the view's input arguments (whatever is passed to View#call), provide matching keyword parameters. You can provide default values for these parameters to make the corresponding input keys optional
    • To receive the Context object, provide a context: keyword parameter
    • To receive the view's input arguments in their entirety, provide a keywords splat parameter (i.e. **input)

    Examples:

    Accessing input arguments

    expose :article do |slug:|
      article_repo.find_by_slug(slug)
    end

    Accessing other exposures

    expose :articles do
      article_repo.listing
    end
    
    expose :featured_articles do |articles|
      articles.select(&:featured?)
    end

    Parameters:

    • name (Symbol)

      name for the exposure

    • options (Hash)

      the exposure's options

    Options Hash (**options):

    • :layout (Boolean)

      expose this value to the layout (defaults to false)

    • :decorate (Boolean)

      decorate this value in a matching Part (defaults to false, or the value of config.decorate_exposures)

    • :as (Symbol, Class)

      an alternative name or class to use when finding a matching Part

  • .expose(name, **options) ⇒ Object

    Define a value to be passed to the template, provided by an instance method matching the name. The method's return value will be passed to the template.

    The method's parameters will determine what it is given:

    • To receive other exposure values, provide positional parameters matching the exposure names. These exposures will already by decorated by their Parts.
    • To receive the view's input arguments (whatever is passed to View#call), provide matching keyword parameters. You can provide default values for these parameters to make the corresponding input keys optional
    • To receive the Context object, provide a context: keyword parameter
    • To receive the view's input arguments in their entirety, provide a keywords splat parameter (i.e. **input)

    Examples:

    Accessing input arguments

    expose :article
    
    def article(slug:)
      article_repo.find_by_slug(slug)
    end

    Accessing other exposures

    expose :articles
    expose :featured_articles
    
    def articles
      article_repo.listing
    end
    
    def featured_articles
      articles.select(&:featured?)
    end

    Parameters:

    • name (Symbol)

      name for the exposure

    • options (Hash)

      the exposure's options

    Options Hash (**options):

    • :layout (Boolean)

      expose this value to the layout (defaults to false)

    • :decorate (Boolean)

      decorate this value in a matching Part (defaults to false, or the value of config.decorate_exposures)

    • :as (Symbol, Class)

      an alternative name or class to use when finding a matching Part

  • .expose(name, **options) ⇒ Object

    Define a single value to pass through from the input data (when there is no instance method matching the name). This value will be passed to the template.

    Parameters:

    • name (Symbol)

      name for the exposure

    • options (Hash)

      the exposure's options

    Options Hash (**options):

    • :layout (Boolean)

      expose this value to the layout (defaults to false)

    • :decorate (Boolean)

      decorate this value in a matching Part (defaults to false, or the value of config.decorate_exposures)

    • :as (Symbol, Class)

      an alternative name or class to use when finding a matching Part

    • :default (Boolean)

      a default value to provide if there is no matching input data

  • .expose(*names, **options) ⇒ Object

    Define multiple values to pass through from the input data (when there is no instance methods matching their names). These values will be passed through to the template.

    The provided options will be applied to all the exposures.

    Parameters:

    • names (Symbol)

      names for the exposures

    • options (Hash)

      the exposure's options

    Options Hash (**options):

    • :layout (Boolean)

      expose this value to the layout (defaults to false)

    • :decorate (Boolean)

      decorate this value in a matching Part (defaults to false, or the value of config.decorate_exposures)

    • :as (Symbol, Class)

      an alternative name or class to use when finding a matching Part

    • :default (Boolean)

      a default value to provide if there is no matching input data

See Also:

Since:

  • 2.1.0



415
416
417
418
419
420
421
422
423
# File 'lib/hanami/view.rb', line 415

def self.expose(*names, **options, &block)
  if names.length == 1
    exposures.add(names.first, block, **options)
  else
    names.each do |name|
      exposures.add(name, **options)
    end
  end
end

.exposuresExposures

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the defined exposures. These are unbound, since bound exposures are only created when initializing a View instance.

Returns:

Since:

  • 2.1.0



450
451
452
# File 'lib/hanami/view.rb', line 450

def self.exposures
  @exposures ||= Exposures.new
end

.gem_loaderObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 2.1.0



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/hanami/view.rb', line 25

def self.gem_loader
  @gem_loader ||= Zeitwerk::Loader.new.tap do |loader|
    root = File.expand_path("..", __dir__)
    loader.tag = "hanami-view"
    loader.push_dir(root)
    loader.ignore(
      "#{root}/hanami-view.rb",
      "#{root}/hanami/view/version.rb",
      "#{root}/hanami/view/errors.rb",
      # These adapters require the optional "haml" and "slim" gems. They are loaded lazily by
      # Tilt (see Hanami::View::Tilt) only when their respective template engines are used;
      # ignore them here to allow eager loading without those gems installed.
      "#{root}/hanami/view/tilt/haml_adapter.rb",
      "#{root}/hanami/view/tilt/slim_adapter.rb"
    )
    loader.inflector = Zeitwerk::GemInflector.new("#{root}/hanami-view.rb")
    loader.inflector.inflect(
      "erb" => "ERB",
      "html" => "HTML",
      "html_safe_string_buffer" => "HTMLSafeStringBuffer"
    )
  end
end

.config.inflector=(inflector) ⇒ Object

Set an inflector to provide to the part_builder and scope_builder.

Defaults to Dry::Inflector.new.

Parameters:

  • inflector

Since:

  • 2.1.0

Since:

  • 2.1.0



239
# File 'lib/hanami/view.rb', line 239

setting :inflector, default: Dry::Inflector.new

.inherited(klass) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 2.1.0



296
297
298
299
300
301
302
# File 'lib/hanami/view.rb', line 296

def self.inherited(klass)
  super

  exposures.each do |name, exposure|
    klass.exposures.import(name, exposure)
  end
end

.config.layout=(name) ⇒ Object

Set the name of the layout to render templates within. Layouts will be looked up within the configured layouts_dir, within the configured paths.

A false or nil value will use no layout. Defaults to nil.

Parameters:

  • name (String, FalseClass, nil)

    layout name, or false to indicate no layout

Since:

  • 2.1.0

Since:

  • 2.1.0



117
# File 'lib/hanami/view.rb', line 117

setting :layout, default: false

.config.layouts_dir=(dir) ⇒ Object

Set the name of the directory (within the configured paths) holding the layouts. Defaults to "layouts"

Parameters:

  • dir (String)

    directory name

Since:

  • 2.1.0

Since:

  • 2.1.0



127
# File 'lib/hanami/view.rb', line 127

setting :layouts_dir, default: "layouts"

.config.part_builder=(part_builder) ⇒ Object

Set a custom part builder class

Parameters:

  • part_builder (Class)

Since:

  • 2.1.0

Since:

  • 2.1.0



196
# File 'lib/hanami/view.rb', line 196

setting :part_builder, default: PartBuilder

.config.part_class=(part_class) ⇒ Object

Set a custom default part class.

Parameters:

  • part_class (Class)

Since:

  • 2.1.0

Since:

  • 2.1.0



175
# File 'lib/hanami/view.rb', line 175

setting :part_class, default: Part

.config.scope_namespace=(namespace) ⇒ Object

Set a namespace that will be searched when building scope classes.

Parameters:

  • namespace (Module, Class)

See Also:

Since:

  • 2.1.0

Since:

  • 2.1.0



187
# File 'lib/hanami/view.rb', line 187

setting :part_namespace

.config.paths=(paths) ⇒ Object

Set an array of directories that will be searched for all templates (templates, partials, and layouts).

These will be converted into Path objects and used for template lookup when rendering.

This is a required setting.

Parameters:

Since:

  • 2.1.0

Since:

  • 2.1.0



74
75
76
# File 'lib/hanami/view.rb', line 74

setting :paths, constructor: -> paths {
  Array(paths).map { |path| Path[path] }
}

.private_expose(*names, **options, &block) ⇒ Object

See Also:

Since:

  • 2.1.0



429
430
431
# File 'lib/hanami/view.rb', line 429

def self.private_expose(*names, **options, &block)
  expose(*names, **options, private: true, &block)
end

.config.renderer_engine_mapping=(mapping) ⇒ Object

A hash specifying the (Tilt-compatible) template engine class to use for a given format. Template engine detection is automatic based on format; use this setting only if you want to force a non-preferred engine.

Examples:

config.renderer_engine_mapping = {erb: Tilt::ErubiTemplate}

Parameters:

  • mapping (Hash<Symbol, Class>)

    engine mapping

See Also:

Since:

  • 2.1.0

Since:

  • 2.1.0



291
# File 'lib/hanami/view.rb', line 291

setting :renderer_engine_mapping, default: {}

.config.renderer_options=(options) ⇒ Object

A hash of options to pass to the template engine. Template engines are provided by Tilt; see Tilt's documentation for what options your template engine may support.

Defaults to {default_encoding: "utf-8"}. Any options passed will be merged onto the defaults.

Parameters:

  • options (Hash)

    renderer options

See Also:

Since:

  • 2.1.0

Since:

  • 2.1.0



272
273
274
# File 'lib/hanami/view.rb', line 272

setting :renderer_options, default: DEFAULT_RENDERER_OPTIONS, constructor: -> options {
  DEFAULT_RENDERER_OPTIONS.merge(options.to_h).freeze
}

.scope(scope_class = nil, &block) ⇒ Object

Creates and assigns a scope for the current view.

The newly created scope is useful to add custom logic that is specific to the view.

The scope has access to locals, exposures, and inherited scope (if any)

If the view already has an explicit scope the newly created scope will inherit from the explicit scope.

There are two cases when this may happen:

1. The scope was explicitly assigned (e.g. `config.scope = MyScope`)
2. The scope has been inherited by the view superclass

If the view doesn't have an already existing scope, the newly scope will inherit from Hanami::View::Scope by default.

However, you can specify any base class for it. This is not recommended, unless you know what you're doing.

Examples:

Basic usage

class MyView < Hanami::View
  config.scope = MyScope

  scope do
    def greeting
      _locals[:message].upcase + "!"
    end

    def copyright(time)
      "Copy #{time.year}"
    end
  end
end

# my_view.html.erb
# <%= greeting %>
# <%= copyright(Time.now.utc) %>

MyView.new.(message: "Hello") # => "HELLO!"

Inherited scope

class MyScope < Hanami::View::Scope
  private

  def shout(string)
    string.upcase + "!"
  end
end

class MyView < Hanami::View
  config.scope = MyScope

  scope do
    def greeting
      shout(_locals[:message])
    end

    def copyright(time)
      "Copy #{time.year}"
    end
  end
end

# my_view.html.erb
# <%= greeting %>
# <%= copyright(Time.now.utc) %>

MyView.new.call(message: "Hello") # => "HELLO!"

Parameters:

  • scope (Hanami::View::Scope)

    the current scope (if any), or the default base class will be Hanami::View::Scope

  • block (Proc)

    the scope logic definition

Since:

  • 2.1.0



141
# File 'lib/hanami/view.rb', line 141

setting :scope

.config.scope_builder=(scope_builder) ⇒ Object

Set a custom scope builder class

Parameters:

  • scope_builder (Class)

See Also:

Since:

  • 2.1.0

Since:

  • 2.1.0



228
# File 'lib/hanami/view.rb', line 228

setting :scope_builder, default: ScopeBuilder

.config.scope_class=(scope_class) ⇒ Object

Set a custom default scope class.

Parameters:

  • scope_class (Class)

Since:

  • 2.1.0

Since:

  • 2.1.0



205
# File 'lib/hanami/view.rb', line 205

setting :scope_class, default: Scope

.config.scope_namespace=(namespace) ⇒ Object

Set a namespace that will be searched when building scope classes.

Parameters:

  • namespace (Module, Class)

See Also:

Since:

  • 2.1.0

Since:

  • 2.1.0



217
# File 'lib/hanami/view.rb', line 217

setting :scope_namespace

.config.template=(name) ⇒ Object

Set the name of the template for rendering this view. Template name should be relative to the configured paths.

This is a required setting.

Parameters:

  • name (String)

    template name

Since:

  • 2.1.0

Since:

  • 2.1.0



88
# File 'lib/hanami/view.rb', line 88

setting :template

.config.template_inference_base=(base_path) ⇒ Object

Set the base path to strip away when when inferring a view's template names from its class name.

This setting only applies for views within an Hanami application.

For example, given a view Main::Views::Articles::Index, in the Main slice, and a template_inference_base of "views", the inferred template name will be "articles/index".

Parameters:

  • base_path (String, nil)

    base templates path

Since:

  • 2.1.0

Since:

  • 2.1.0



104
# File 'lib/hanami/view.rb', line 104

setting :template_inference_base

Instance Method Details

#call(format: config_data.default_format, context: config_data.default_context, layout: config_data.layout, **input) ⇒ Rendered

Renders the view.

Parameters:

  • format (Symbol) (defaults to: config_data.default_format)

    template format to use

  • context (Context) (defaults to: config_data.default_context)

    context object to use

  • layout (String, FalseClass, nil) (defaults to: config_data.layout)

    layout name, or false to indicate no layout

  • input

    input data for preparing exposure values

Returns:

Since:

  • 2.1.0



592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
# File 'lib/hanami/view.rb', line 592

def call(format: config_data.default_format,
         context: config_data.default_context,
         layout: config_data.layout,
         **input)
  rendering = self.rendering(format: format, context: context)
  scope_class = config_data.scope

  locals = locals(rendering, input)
  output = rendering.template(config_data.template, rendering.scope(scope_class, locals))

  if layout
    output = rendering.template(
      layout_path(layout),
      rendering.scope(scope_class, layout_locals(locals))
    ) { output }
  end

  Rendered.new(output: output, locals: locals)
end

#configObject

Returns the view's configuration.

Since:

  • 2.1.0



568
569
570
# File 'lib/hanami/view.rb', line 568

def config
  self.class.config
end

#exposuresExposures

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns the view's bound exposures.

Returns:

Since:

  • 2.1.0



577
578
579
# File 'lib/hanami/view.rb', line 577

def exposures # rubocop:disable Style/TrivialAccessors
  @exposures
end

#rendering(format: config_data.default_format, context: config_data.default_context) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Since:

  • 2.1.0



613
614
615
# File 'lib/hanami/view.rb', line 613

def rendering(format: config_data.default_format, context: config_data.default_context)
  Rendering.new(config_data:, format:, context:)
end