Simple form 添加 array input 组件

PG支持array类型,但simple form中却没有 array input 组件,试着custom一个,效果还不错。

效果图

最终效果:

array_input

具体步骤

这里以诗词Poem的content类型为array为例,基于rails5.2,webpacker4.0,给出具体的实现步骤,

  • 新增component array_input

    添加app/inputs/array_input.rb文件,内容如下:

    class ArrayInput < SimpleForm::Inputs::Base
      def input(wrapper_options = nil)
        existing_values = Array(object.public_send(attribute_name))
        template.content_tag(:div) do
          existing_values.map do |array_item|
            template.concat builder_input_with_remove_btn(array_item)
          end
    
          if existing_values.empty?
            template.concat builder_input_with_remove_btn(nil)
          end
    
          template.concat add_item_btn
        end
      end
    
      def input_html_options
        super.merge({ class: 'form-control rounded', type: :text })
      end
    
      def builder_input_with_remove_btn(element)
        template.content_tag(:div, class: 'input-group mb-3') do
          template.concat @builder.text_field(nil, input_html_options.merge(value: element, name: "#{object_name}[#{attribute_name}][]"))
                 template.concat remove_item_btn
        end
      end
    
      def add_item_btn
        content = <<~HTML
          <button type="button" class="btn btn-success btn-sm array-add-item">新增 +</button>
        HTML
        content.html_safe
      end
    
      def remove_item_btn
        content = <<~HTML
        <div class="input-group-append ml-2">
          <button type="button" class="btn btn-secondary btn-sm rounded array-remove-item">移除 -</button>
        </div>
        HTML
        content.html_safe
      end
    end
    

    这里,需要注意的是,当content 为空时,只会有一行input,此时的移除按钮应该是disabled才合适。

    我这里给出的解决方法有点蠢,直接在build_input_with_remove_btn中,添加了参数来控制,可以看到效果图中,第一个移除按钮是disabled。

    修改builder_input_with_remove_btn 这个method:

    ## 给 builder_input_with_remove_btn 添加 disabled 参数
        def builder_input_with_remove_btn(element, disabled = false)
        template.content_tag(:div, class: 'input-group mb-3') do
          template.concat @builder.text_field(nil, input_html_options.merge(value: element, name: "#{object_name}[#{attribute_name}][]"))
          disabled ? template.concat(disabled_remove_item_btn) : template.concat(remove_item_btn)
        end
      end
    
    ## 添加新的 disabled_remove_item_btn
      def disabled_remove_item_btn
        content = <<~HTML
        <div class="input-group-append ml-2">
          <button type="button" class="btn btn-secondary btn-sm rounded array-remove-item" disabled>移除 -</button>
        </div>
        HTML
        content.html_safe
      end
    
    ## 修改 input ,调用新的 builder_input_with_remove_btn
      def input(wrapper_options = nil)
        existing_values = Array(object.public_send(attribute_name))
        template.content_tag(:div) do
          existing_values.each_with_index do |array_el, index|
            disabled = index.zero? ? true : false
            template.concat builder_input_with_remove_btn(array_el, disabled)
          end
    
          if existing_values.empty?
            template.concat builder_input_with_remove_btn(nil, true)
          end
    
          template.concat add_item_btn
        end
      end
    
  • 给移除和新增按钮添加click监听事件

    在app/javascripts中,添加文件src/array_input.jsx:

    import $ from 'jquery';
    
    $(document).on('content:loaded', function(event) {
      $(event.target).find('.array-add-item').each(function() {
        $(this).on('click', e => {
          e.preventDefault()
          const inputList = this.parentElement.querySelectorAll('.input-group')
          const lastLineField = inputList[inputList.length - 1]
          const $cloneField = $(lastLineField).clone()
          $cloneField.find('input').val("")
          $cloneField.find('button').removeAttr('disabled')
          $(lastLineField).after($cloneField)
        })
      })
    
      $(event.target).on('click', '.array-remove-item', e => {
        e.preventDefault()
        e.currentTarget.parentElement.parentElement.remove()
      })
    })
    

    这里针对移除按钮的 click 事件,需要使用on('click', '.array-remove-item',e => {....})来全局监听,因为新增事件中,clone出来的input-group 并没有将事件监听也一并clone了。「踩过这个坑的都懂」

    在packs/application.js中,引入刚刚新增的文件:

    import '../src/array_input.jsx';
    
  • 修改使用content的地方,确保值正确传递

    修改文件的表单部分:

    <%= f.input :content, as: :array %>
    

    在controller中,针对传过来的params,设定content为array:

    params.require(:poem).permit(:title, :dynasty, :author, content: [])
    

    在poem.rb中,清除content中的空行:

    before_save :remove_blank_item
    def remove_blank_item
      self.content.reject!(&:blank?)
    end
    

OK,收工。

参考

Integrating an array column with Simple Form in Rails

Simple form array text input

Custom inputs examples