gregbell/active_admin

View on GitHub
lib/active_admin/form_builder.rb

Summary

Maintainability
A
35 mins
Test Coverage
# frozen_string_literal: true
# Provides an intuitive way to build has_many associated records in the same form.
module Formtastic
  module Inputs
    module Base
      def input_wrapping(&block)
        html = super
        template.concat(html) if template.output_buffer && template.assigns[:has_many_block]
        html
      end
    end
  end
end

module ActiveAdmin
  class FormBuilder < ::Formtastic::FormBuilder

    self.input_namespaces = [::Object, ::ActiveAdmin::Inputs, ::Formtastic::Inputs]

    # TODO: remove both class finders after formtastic 4 (where it will be default)
    self.input_class_finder = ::Formtastic::InputClassFinder
    self.action_class_finder = ::Formtastic::ActionClassFinder

    def cancel_link(url = { action: "index" }, html_options = {}, li_attrs = {})
      li_attrs[:class] ||= "action cancel"
      html_options[:class] ||= "cancel-link"
      li_content = template.link_to I18n.t("active_admin.cancel"), url, html_options
      template.content_tag(:li, li_content, li_attrs)
    end

    attr_accessor :already_in_an_inputs_block

    def has_many(assoc, options = {}, &block)
      HasManyBuilder.new(self, assoc, options).render(&block)
    end
  end

  # Decorates a FormBuilder with the additional attributes and methods
  # to build a has_many block.  Nested has_many blocks are handled by
  # nested decorators.
  class HasManyBuilder < SimpleDelegator
    attr_reader :assoc
    attr_reader :options
    attr_reader :heading, :sortable_column, :sortable_start
    attr_reader :new_record, :destroy_option, :remove_record

    def initialize(has_many_form, assoc, options)
      super has_many_form
      @assoc = assoc
      @options = extract_custom_settings!(options.dup)
      @options.reverse_merge!(for: assoc)
      @options[:class] = [options[:class], "inputs has-many-fields"].compact.join(" ")

      if sortable_column
        @options[:for] = [assoc, sorted_children(sortable_column)]
      end
    end

    def render(&block)
      html = "".html_safe
      html << template.content_tag(:h3, class: "has-many-fields-title") { heading } if heading.present?
      html << template.capture { content_has_many(&block) }
      html = wrap_div_or_li(html)
      template.concat(html) if template.output_buffer
      html
    end

    protected

    # remove options that should not render as attributes
    def extract_custom_settings!(options)
      @heading = options.key?(:heading) ? options.delete(:heading) : default_heading
      @sortable_column = options.delete(:sortable)
      @sortable_start = options.delete(:sortable_start) || 0
      @new_record = options.key?(:new_record) ? options.delete(:new_record) : true
      @destroy_option = options.delete(:allow_destroy)
      @remove_record = options.delete(:remove_record)
      options
    end

    def default_heading
      assoc_klass.model_name.human(count: 2.1)
    end

    def assoc_klass
      @assoc_klass ||= __getobj__.object.class.reflect_on_association(assoc).klass
    end

    def content_has_many(&block)
      form_block = proc do |form_builder|
        render_has_many_form(form_builder, options[:parent], &block)
      end

      template.assigns[:has_many_block] = true
      contents = without_wrapper { inputs(options, &form_block) }
      contents ||= "".html_safe

      js = new_record ? js_for_has_many(options[:class], &form_block) : ""
      contents << js
    end

    # Renders the Formtastic inputs then appends ActiveAdmin delete and sort actions.
    def render_has_many_form(form_builder, parent, &block)
      index = parent && form_builder.send(:parent_child_index, parent)
      template.concat template.capture { yield(form_builder, index) }
      template.concat has_many_actions(form_builder, "".html_safe)
    end

    def has_many_actions(form_builder, contents)
      if form_builder.object.new_record?
        contents << template.content_tag(:li, class: "input") do
          remove_text = remove_record.is_a?(String) ? remove_record : I18n.t("active_admin.has_many_remove")
          template.link_to remove_text, "#", class: "has-many-remove"
        end
      elsif allow_destroy?(form_builder.object)
        form_builder.input(
          :_destroy, as: :boolean,
                     wrapper_html: { class: "has-many-delete" },
                     label: I18n.t("active_admin.has_many_delete"))
      end

      if sortable_column
        form_builder.input sortable_column, as: :hidden

        # contents << template.content_tag(:li, class: "handle") do
        #   I18n.t("active_admin.move")
        # end
      end

      contents
    end

    def allow_destroy?(form_object)
      !! case destroy_option
         when Symbol, String
           form_object.public_send destroy_option
         when Proc
           destroy_option.call form_object
         else
           destroy_option
         end
    end

    def sorted_children(column)
      __getobj__.object.public_send(assoc).sort_by do |o|
        attribute = o.public_send column
        [attribute.nil? ? Float::INFINITY : attribute, o.id || Float::INFINITY]
      end
    end

    def without_wrapper
      is_being_wrapped = already_in_an_inputs_block
      self.already_in_an_inputs_block = false

      html = yield

      self.already_in_an_inputs_block = is_being_wrapped
      html
    end

    # Capture the ADD JS
    def js_for_has_many(class_string, &form_block)
      assoc_name = assoc_klass.model_name
      placeholder = "NEW_#{assoc_name.to_s.underscore.upcase.tr('/', '_')}_RECORD"
      opts = {
        for: [assoc, assoc_klass.new],
        class: class_string,
        for_options: { child_index: placeholder }
      }
      html = template.capture { __getobj__.send(:inputs_for_nested_attributes, opts, &form_block) }
      text = new_record.is_a?(String) ? new_record : I18n.t("active_admin.has_many_new", model: assoc_name.human)

      template.link_to text, "#", class: "has-many-add", data: {
        html: CGI.escapeHTML(html).html_safe, placeholder: placeholder
      }
    end

    def wrap_div_or_li(html)
      template.content_tag(
        already_in_an_inputs_block ? :li : :div,
        html,
        class: "has-many-container",
        "data-has-many-association" => assoc,
        "data-sortable" => sortable_column,
        "data-sortable-start" => sortable_start)
    end
  end
end