Permalink
Browse files

add form support for arrays of nested fields (#74)

Add form support for arrays of nested fields
  • Loading branch information...
TiteiKo authored and AlfonsoUceda committed Feb 3, 2017
1 parent a328552 commit 16917b2c5e5e266f94ab658bc38372087f7a7649
@@ -198,7 +198,49 @@ def to_s
def fields_for(name)
current_name = @name
@name = _input_name(name)
yield
yield(name)
ensure
@name = current_name
end

# Nested collections
#
# Supports nesting for collections, with infinite
# levels of nesting.
#
# @param name [Symbol] the nested name, it's used to generate input
# names, ids, and to lookup params to fill values.
#
# @example Basic usage
# <%=
# form_for :delivery, routes.deliveries_path do
# text_field :customer_name
#
# fields_for_collection :addresses do
# text_field :street
# end
#
# submit 'Create'
# end
# %>
#
# Output:
# # <form action="/deliveries" method="POST" accept-charset="utf-8" id="delivery-form">
# # <input type="text" name="delivery[customer_name]" id="delivery-customer-name" value="">
# # <input type="text" name="delivery[addresses][][street]" id="delivery-address-0-street" value="">
# # <input type="text" name="delivery[addresses][][street]" id="delivery-address-1-street" value="">
# #
# # <button type="submit">Create</button>
# # </form>
#
def fields_for_collection(name, &block)
current_name = @name
base_value = _value(name)
@name = _input_name(name)

base_value.count.times do |index|
fields_for(index, &block)
end
ensure
@name = current_name
end
@@ -957,19 +999,29 @@ def csrf_token
# @api private
# @since 0.2.0
def _attributes(type, name, attributes)
attrs = { type: type, name: _input_name(name), id: _input_id(name), value: _value(name) }.merge!(attributes)
attrs = { type: type, name: _displayed_input_name(name), id: _input_id(name), value: _value(name) }
attrs.merge!(attributes)
attrs[:value] = Hanami::Utils::Escape.html(attrs[:value])
attrs
end

# Input <tt>name</tt> HTML attribute
# Full input name, used to construct the input
# attributes.
#
# @api private
# @since 0.2.0
def _input_name(name)
"#{@name}[#{name}]"
end

# Input <tt>name</tt> HTML attribute
#
# @api private
# @since x.x.x
def _displayed_input_name(name)
_input_name(name).gsub(/\[\d+\]/, '[]')
end

# Input <tt>id</tt> HTML attribute
#
# @api private
@@ -36,6 +36,7 @@ def get(*keys)
# @since 0.5.0
# @api private
def _get_from_params(*keys)
keys.map! { |key| key.to_s =~ /\A\d+\z/ ? key.to_s.to_i : key }
@params.dig(*keys)
end

@@ -47,15 +48,21 @@ def _get_from_values(*keys)

tail.each do |k|
break if result.nil?

result = case result
when Utils::Hash, ::Hash then result[k]
when ->(r) { r.respond_to?(k) } then result.public_send(k)
end
result = _dig(result, k)
end

result
end

# @since x.x.x
# @api private
def _dig(base, key)
case base
when Utils::Hash, ::Hash then base[key]
when Array then base[key.to_s.to_i]
when ->(r) { r.respond_to?(key) } then base.public_send(key)
end
end
end
end
end
@@ -384,6 +384,15 @@ def initialize(attributes = {})
end
end

class Bill
attr_reader :id, :addresses

def initialize(attributes = {})
@id = attributes[:id]
@addresses = attributes[:addresses]
end
end

class DeliveryParams < Hanami::Action::Params
params do
required(:delivery).schema do
@@ -395,6 +404,18 @@ class DeliveryParams < Hanami::Action::Params
end
end

class BillParams < Hanami::Action::Params
params do
required(:bill).schema do
required(:addresses).each do
schema do
required(:street, :string).filled
end
end
end
end
end

class Session
def initialize(values)
@values = values.to_h
@@ -423,6 +444,10 @@ def delivery_path(attrs = {})
_escape "/deliveries/#{attrs.fetch(:id)}"
end

def bill_path(attrs = {})
_escape "/bills/#{attrs.fetch(:id)}"
end

private

def _escape(string)
@@ -494,6 +519,21 @@ def submit_label
end
end
end

module Bills
class Edit
include TestView
template 'bills/edit'

def form
Form.new(:bill, routes.bill_path(id: bill.id), { bill: bill }, method: :patch)
end

def submit_label
'Update'
end
end
end
end
end

@@ -0,0 +1,18 @@
<%=
form_for(form, class: 'form-horizontal') do
fieldset do
legend 'Addresses'
fields_for_collection :addresses do |i|
div class: 'form-group' do
label :street
input_text :street, class: 'form-control', placeholder: 'Street', 'data-funky': "id-#{i}"
end
end
label :ensure_names
end
submit submit_label, class: 'btn btn-default'
end
%>
@@ -89,4 +89,38 @@
end
end
end

describe 'form with nested structures' do
describe 'first page load' do
before do
@address1 = Address.new(street: '5th Ave')
@address2 = Address.new(street: '4th Ave')
@bill = Bill.new(id: 1, addresses: [@address1, @address2])
@params = BillParams.new({})
@session = Session.new(_csrf_token: 's14')

@actual = FullStack::Views::Bills::Edit.render(format: :html, bill: @bill, params: @params, session: @session)
end

it 'renders the form' do
@actual.must_include %(<form action="/bills/#{@bill.id}" method="POST" accept-charset="utf-8" id="bill-form" class="form-horizontal">\n<input type="hidden" name="_method" value="PATCH">\n<input type="hidden" name="_csrf_token" value="#{@session[:_csrf_token]}">\n<fieldset>\n<legend>Addresses</legend>\n<div class="form-group">\n<label for="bill-addresses-0-street">Street</label>\n<input type="text" name="bill[addresses][][street]" id="bill-addresses-0-street" value="#{@address1.street}" class="form-control" placeholder="Street" data-funky="id-0">\n</div>\n<div class="form-group">\n<label for="bill-addresses-1-street">Street</label>\n<input type="text" name="bill[addresses][][street]" id="bill-addresses-1-street" value="#{@address2.street}" class="form-control" placeholder="Street" data-funky="id-1">\n</div>\n<label for="bill-ensure-names">Ensure names</label>\n</fieldset>\n<button type="submit" class="btn btn-default">Update</button>\n</form>\n)
end
end

describe 'after a failed submission' do
before do
@address1 = Address.new(street: '5th Ave')
@address2 = Address.new(street: '4th Ave')
@bill = Bill.new(id: 1, addresses: [@address1, @address2])
@params = BillParams.new(bill: { addresses: [{ street: 'Mulholland Drive' }, { street: 'Quaint Edge' }] })
@session = Session.new(_csrf_token: 's14')

@actual = FullStack::Views::Bills::Edit.render(format: :html, bill: @bill, params: @params, session: @session)
end

it 'renders the form' do
@actual.must_include %(<form action="/bills/#{@bill.id}" method="POST" accept-charset="utf-8" id="bill-form" class="form-horizontal">\n<input type="hidden" name="_method" value="PATCH">\n<input type="hidden" name="_csrf_token" value="#{@session[:_csrf_token]}">\n<fieldset>\n<legend>Addresses</legend>\n<div class="form-group">\n<label for="bill-addresses-0-street">Street</label>\n<input type="text" name="bill[addresses][][street]" id="bill-addresses-0-street" value="#{@params[:bill][:addresses][0][:street]}" class="form-control" placeholder="Street" data-funky="id-0">\n</div>\n<div class="form-group">\n<label for="bill-addresses-1-street">Street</label>\n<input type="text" name="bill[addresses][][street]" id="bill-addresses-1-street" value="#{@params[:bill][:addresses][1][:street]}" class="form-control" placeholder="Street" data-funky="id-1">\n</div>\n<label for="bill-ensure-names">Ensure names</label>\n</fieldset>\n<button type="submit" class="btn btn-default">Update</button>\n</form>\n)
end
end
end
end

0 comments on commit 16917b2

Please sign in to comment.