Skip to content

Instantly share code, notes, and snippets.

@estum
Created September 2, 2016 08:21
Show Gist options
  • Save estum/998811f032a652a9659ed75f425b6b80 to your computer and use it in GitHub Desktop.
Save estum/998811f032a652a9659ed75f425b6b80 to your computer and use it in GitHub Desktop.
SimpleForm: Array Input
@Forms ?= {}
class Forms.ArrayInput
REMOVE_BUTTON_HTML = """<button type="button" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button>"""
ADD_ITEM_SELECTOR = "a.array-input-add-item"
getTargetFrom = (element) ->
el = ($ element)
el.closest(el.data("target"))
addItemHandler = (e) ->
e.preventDefault()
getTargetFrom(@).data("instance").buildNewItem()
off
@init: (e) =>
($ ADD_ITEM_SELECTOR).each(@build)
return
@build: ->
wrapper = getTargetFrom(@)
unless wrapper.data("instance") instanceof Forms.ArrayInput
array_input = new Forms.ArrayInput(wrapper.get(0), ($ @))
wrapper.data "instance", array_input
return
constructor: (@wrapper, addItemButton) ->
@buildAddItemButton(addItemButton ? ($ ADD_ITEM_SELECTOR, @wrapper))
inputs = @container().children()
@total = inputs.length
inputs.each (index, el) =>
($ el).data("index", index)
@buildRemoveButtonFor(el)
($ @wrapper).on "click", "button.close", (e) =>
e.preventDefault()
@removeItem(($ e.currentTarget).data("target"))
off
container: ->
($ @groupSelector, @wrapper)
buildAddItemButton: (addItemButton) ->
@groupSelector = addItemButton.data("groupTarget")
@itemSelector = addItemButton.data("itemTarget")
@input = addItemButton.data("input")
addItemButton.
removeData("input").
removeAttr("dataInput").
on("click", addItemHandler)
buildRemoveButtonFor: (item) ->
($ REMOVE_BUTTON_HTML).appendTo(item).data('target', "##{($ ":input", item).attr("id")}")
setItemID: (_, value) =>
value.replace(/%d$/, @last_idx)
newIndex: ->
if @total == 0 then 0 else parseInt(@container().children().last().data("index"))+1
buildNewItem: ->
@last_idx = @newIndex()
item = ($ @input).data("index", @last_idx)
item.find(":input").attr("id", @setItemID)
@buildRemoveButtonFor(item)
@total++
item.appendTo(@container())
removeItem: (sel) ->
item = @container().find(sel)
if item.length > 0
item.closest(@itemSelector).remove()
@total--
item
jQuery ->
if Turbolinks?
($ document).on("turbolinks:load", Forms.ArrayInput.init)
else
Forms.ArrayInput.init()
# = SimpleForm: Array Input
#
# Example:
#
# f.input :alias_names, :as => :array,
# :subwrapper_html => { class: "row" },
# :group_html => { tag: :ol, class: "list-inline col-lg-11" },
# :item_html => { tag: :li, class: "col-md-6 col-lg-3" },
# :button_html => { class: "btn btn-info", text: fa_icon(:plus) },
# :button_wrapper_html => { class: "col-lg-1 text-right" }
#
class ArrayInput < SimpleForm::Inputs::StringInput
GROUP_CLASS = "array-input-items-group"
ITEM_CLASS = "array-input-item"
def input(wrapper_options = nil)
input_html_classes << "form-control"
unless input_type == :string
input_html_classes.unshift("string")
input_html_options[:type] ||= input_type if html5?
end
type = wrapper_options.fetch(:type) { input_type }
@merged_input_options = merge_wrapper_options(input_html_options, wrapper_options)
@merged_input_options[:name] = "#{object_name}[#{attribute_name}][]"
@merged_input_options[:type] = type == :array ? :string : type
if options.key?(:item_html)
@item_options = html_options_for(:item, [ITEM_CLASS]).dup
@item_tag = @item_options.delete(:tag) { :div }
end
template.content_tag :div, html_options_for(:subwrapper, []) do
template.concat(build_inputs_group)
template.concat(build_add_item_button)
end
ensure
remove_instance_variable :@merged_input_options
remove_instance_variable :@item_options
remove_instance_variable :@item_tag
end
private
def build_inputs_group
opts = html_options_for(:group, [GROUP_CLASS])
tag = opts.delete(:tag) { :div }
values = Array.wrap(object.public_send(attribute_name))
template.content_tag(tag, opts) do
values.each_with_index do |value, idx|
template.concat(build_item_input(value: value, id: "#{input_class}_#{idx}"))
end
end
end
def build_item_input(input_options)
wrap_item_input { @builder.text_field(nil, @merged_input_options.merge(input_options)) }
end
def build_add_item_button
opts = html_options_for(:button, []).reverse_merge(class: SimpleForm.button_class)
opts[:class] = ["array-input-add-item", *opts[:class]].compact
opts[:data] = {
:input => template.send(:html_escape_once, build_item_input(value: nil, id: "#{input_class}_%d")),
:target => ".array.#{input_class}",
"item-target" => ".#{ITEM_CLASS}",
"group-target" => ".#{GROUP_CLASS}"
}
text = opts.delete(:text) { translate_from_namespace(:add_item, 'Add item') }
template.content_tag(:div, html_options_for(:button_wrapper, [])) do
template.link_to(text.html_safe, "#add-item", opts)
end
end
def wrap_item_input # :yields:
defined?(@item_tag) && defined?(@item_options) ? template.content_tag(@item_tag, @item_options) { yield } : yield
end
end
.array {
.array-input-items-group.list-inline {
margin-left: 0;
}
.array-input-items-group > .array-input-item {
padding: 0;
margin-bottom: 10px;
& > button.close {
position: absolute;
top: 7px;
right: 10px;
display: none;
}
&:hover > button.close {
display: block;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment