class Hanami::View

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.

@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)

@api public @since 2.1.0

Constants

DEFAULT_RENDERER_OPTIONS

@api private @since 2.1.0

VERSION

@api public @since 0.1.0

Public Class Methods

cache() click to toggle source

@api private @since 2.1.0

# File lib/hanami/view.rb, line 522
def self.cache
  Cache
end
expose(*names, **options, &block) click to toggle source

@overload expose(name, **options, &block)

Define a value to be passed to the template. The return value of the
block will be decorated by a matching Part and 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`)

@example Accessing input arguments
  expose :article do |slug:|
    article_repo.find_by_slug(slug)
  end

@example Accessing other exposures
  expose :articles do
    article_repo.listing
  end

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

@param name [Symbol] name for the exposure
@macro exposure_options

@overload expose(name, **options)

Define a value to be passed to the template, provided by an instance
method matching the name. The method's return value will be decorated by
a matching Part and 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 entirey, provide a
  keywords splat parameter (i.e. `**input`)

@example Accessing input arguments
  expose :article

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

@example Accessing other exposures
  expose :articles
  expose :featured_articles

  def articles
    article_repo.listing
  end

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

@param name [Symbol] name for the exposure
@macro exposure_options

@overload expose(name, **options)

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

@param name [Symbol] name for the exposure
@macro exposure_options
@option options [Boolean] :default a default value to provide if there is no matching
  input data

@overload expose(*names, **options)

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

The provided options will be applied to all the exposures.

@param names [Symbol] names for the exposures
@macro exposure_options
@option options [Boolean] :default a default value to provide if there is no matching
  input data

@see dry-rb.org/gems/dry-view/exposures/

@api public @since 2.1.0

# File lib/hanami/view.rb, line 396
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
exposures() click to toggle source

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

@return [Exposures] @api private @since 2.1.0

# File lib/hanami/view.rb, line 420
def self.exposures
  @exposures ||= Exposures.new
end
gem_loader() click to toggle source

@api private @since 2.1.0

# File lib/hanami/view.rb, line 26
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",
    )
    loader.inflector = Zeitwerk::GemInflector.new("#{root}/hanami-view.rb")
    loader.inflector.inflect(
      "erb" => "ERB",
      "html" => "HTML",
      "html_safe_string_buffer" => "HTMLSafeStringBuffer",
    )
  end
end
inherited(klass) click to toggle source

@api private @since 2.1.0

Calls superclass method
# File lib/hanami/view.rb, line 277
def self.inherited(klass)
  super

  exposures.each do |name, exposure|
    klass.exposures.import(name, exposure)
  end
end
layout_path(layout) click to toggle source

@api private @since 2.1.0

# File lib/hanami/view.rb, line 516
def self.layout_path(layout)
  File.join(*[config.layouts_dir, layout].compact)
end
new() click to toggle source

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.

@api public @since 2.1.0

# File lib/hanami/view.rb, line 533
def initialize
  self.class.config.finalize!
  ensure_config

  @exposures = self.class.exposures.bind(self)
end
private_expose(*names, **options, &block) click to toggle source

@see expose

@api public @since 2.1.0

# File lib/hanami/view.rb, line 410
def self.private_expose(*names, **options, &block)
  expose(*names, **options, private: true, &block)
end
scope(scope_class = nil, &block) click to toggle source

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.

@param scope [Hanami::View::Scope] the current scope (if any), or the

default base class will be `Hanami::View::Scope`

@param block [Proc] the scope logic definition

@api public

@example 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!"

@example 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!"

@api public @since 2.1.0

# File lib/hanami/view.rb, line 506
def self.scope(scope_class = nil, &block)
  scope_class ||= config.scope || config.scope_class

  config.scope = Class.new(scope_class, &block)
end

Public Instance Methods

call(format: config.default_format, context: config.default_context, layout: config.layout, **input) click to toggle source

Renders the view.

@param format [Symbol] template format to use @param context [Context] context object to use @param layout [String, FalseClass, nil] layout name, or false to indicate no layout @param input input data for preparing exposure values

@return [Rendered] rendered view object

@api public @since 2.1.0

# File lib/hanami/view.rb, line 569
def call(format: config.default_format, context: config.default_context, layout: config.layout, **input)
  rendering = self.rendering(format: format, context: context)

  locals = locals(rendering, input)
  output = rendering.template(config.template, rendering.scope(config.scope, locals))

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

  Rendered.new(output: output, locals: locals)
end
config() click to toggle source

Returns the view’s configuration.

@api public @since 2.1.0

# File lib/hanami/view.rb, line 544
def config
  self.class.config
end
exposures() click to toggle source

Returns the view’s bound exposures.

@return [Exposures]

@api private @since 2.1.0

# File lib/hanami/view.rb, line 554
def exposures
  @exposures
end
rendering(format: config.default_format, context: config.default_context) click to toggle source

@api private @since 2.1.0

# File lib/hanami/view.rb, line 587
def rendering(format: config.default_format, context: config.default_context)
  Rendering.new(config: config, format: format, context: context)
end

Private Instance Methods

ensure_config() click to toggle source
# File lib/hanami/view.rb, line 593
def ensure_config
  raise UndefinedConfigError, :paths unless Array(config.paths).any?
  raise UndefinedConfigError, :template unless config.template
end
layout_locals(locals) click to toggle source
# File lib/hanami/view.rb, line 608
def layout_locals(locals)
  locals.each_with_object({}) do |(key, value), layout_locals|
    layout_locals[key] = value if exposures[key].for_layout?
  end
end
locals(rendering, input) click to toggle source
# File lib/hanami/view.rb, line 598
def locals(rendering, input)
  exposures.(context: rendering.context, **input) do |value, exposure|
    if exposure.decorate? && value
      rendering.part(exposure.name, value, as: exposure.options[:as])
    else
      value
    end
  end
end