projectblacklight/blacklight

View on GitHub
app/components/blacklight/document_component.rb

Summary

Maintainability
A
1 hr
Test Coverage
# frozen_string_literal: true

require 'view_component/version'

module Blacklight
  ##
  # A component for rendering a single document
  #
  # @note when subclassing this component, if you override the initializer,
  #    you must explicitly specify the counter variable `document_counter` even if you don't use it.
  #    Otherwise view_component will not provide the count value when calling the component.
  #
  # @see https://viewcomponent.org/guide/collections.html#collection-counter
  #
  # @example
  #  class MyDocumentComponent < Blacklight::DocumentComponent
  #    def initialize(document_counter: nil, **kwargs)
  #      super
  #      ... custom code ...
  #    end
  #  end
  class DocumentComponent < Blacklight::Component
    with_collection_parameter :document

    # ViewComponent 3 changes iteration counters to begin at 0 rather than 1
    COLLECTION_INDEX_OFFSET = ViewComponent::VERSION::MAJOR < 3 ? 0 : 1

    # Content appearing before the document
    renders_one :header

    # Content appearing after the document
    renders_one :footer

    # Content appearing instead of the regularly rendered document; its use is discouraged, but is a stop-gap until
    # the ecosystem fully adopts view components.
    renders_one :body

    # The document title with some reasonable default behavior
    renders_one :title, (lambda do |*args, component: nil, **kwargs|
      component ||= @presenter&.view_config&.title_component || Blacklight::DocumentTitleComponent

      component.new(*args, counter: @counter, document: @document, presenter: @presenter, as: @title_component, actions: !@show, link_to_document: !@show, document_component: self, **kwargs)
    end)

    renders_one :embed, (lambda do |static_content = nil, *args, component: nil, **kwargs|
      next static_content if static_content.present?

      component ||= @presenter.view_config&.embed_component

      next unless component

      component.new(*args, document: @document, presenter: @presenter, document_counter: @document_counter, **kwargs)
    end)

    # The primary metadata section
    renders_one :metadata, (lambda do |static_content = nil, *args, component: nil, fields: nil, **kwargs|
      next static_content if static_content.present?

      component ||= @presenter&.view_config&.metadata_component || Blacklight::DocumentMetadataComponent

      component.new(*args, fields: fields || @presenter&.field_presenters || [], **kwargs)
    end)

    # Additional metadata sections
    renders_many :metadata_sections

    renders_one :thumbnail, (lambda do |image_options_or_static_content = {}, *args, component: nil, **kwargs|
      next image_options_or_static_content if image_options_or_static_content.is_a? String

      component ||= @presenter&.view_config&.thumbnail_component || Blacklight::Document::ThumbnailComponent

      component.new(*args, document: @document, presenter: @presenter, counter: @counter, image_options: image_options_or_static_content, **kwargs)
    end)

    # A container for partials rendered using the view config partials configuration. Its use is discouraged, but necessary until
    # the ecosystem fully adopts view components.
    renders_many :partials

    # Backwards compatibility
    renders_one :actions

    # rubocop:disable Metrics/ParameterLists
    # @param document [Blacklight::DocumentPresenter]
    # @param presenter [Blacklight::DocumentPresenter] alias for document
    # @param partials [Array, nil] view partial names that should be used to provide content for the `partials` slot
    # @param id [String] HTML id for the root element
    # @param classes [Array, String] additional HTML classes for the root element
    # @param component [Symbol, String] HTML tag type to use for the root element
    # @param title_component [Symbol, String] HTML tag type to use for the title element
    # @param counter [Number, nil] a pre-computed counter for the position of this document in a search result set
    # @param document_counter [Number, nil] provided by ViewComponent collection iteration
    # @param counter_offset [Number] the offset of the start of the collection counter parameter for the component to the overall result set
    # @param show [Boolean] are we showing only a single document (vs a list of search results); used for backwards-compatibility
    def initialize(document: nil, presenter: nil, partials: nil,
                   id: nil, classes: [], component: :article, title_component: nil,
                   counter: nil, document_counter: nil, counter_offset: 0,
                   show: false, **args)
      Blacklight.deprecation.warn('the `presenter` argument to DocumentComponent#initialize is deprecated; pass the `presenter` in as document instead') if presenter

      @presenter = presenter || document || args[self.class.collection_parameter]
      @document = @presenter.document
      @view_partials = partials || []

      @component = component
      @title_component = title_component
      @id = id || ('document' if show)
      @classes = classes

      @counter = counter
      @document_counter = document_counter || args.fetch(self.class.collection_counter_parameter, nil)
      @counter ||= @document_counter + COLLECTION_INDEX_OFFSET + counter_offset if @document_counter.present?

      @show = show
    end
    # rubocop:enable Metrics/ParameterLists

    # HTML classes to apply to the root element
    def classes
      [
        @classes,
        helpers.render_document_class(@document),
        'document',
        ("document-position-#{@counter}" if @counter)
      ].compact.flatten
    end

    def before_render
      set_slot(:title, nil) unless title
      set_slot(:thumbnail, nil) unless thumbnail || show?
      set_slot(:metadata, nil, fields: presenter.field_presenters) unless metadata
      set_slot(:embed, nil) unless embed
      if view_partials.present?
        view_partials.each do |view_partial|
          with_partial(view_partial) do
            helpers.render_document_partial @document, view_partial, component: self, document_counter: @counter
          end
        end
      else
        set_slot(:partials, nil)
      end
    end

    private

    attr_reader :document_counter, :presenter, :view_partials

    def show?
      @show
    end
  end
end