Skip to content

Commit

Permalink
Merge ad90f53 into 23413eb
Browse files Browse the repository at this point in the history
  • Loading branch information
TiteiKo committed Jan 23, 2017
2 parents 23413eb + ad90f53 commit b4c35e7
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 8 deletions.
57 changes: 54 additions & 3 deletions lib/hanami/helpers/form_helper/form_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -957,17 +999,26 @@ def csrf_token
# @api private
# @since 0.2.0
def _attributes(type, name, attributes)
{ type: type, name: _input_name(name), id: _input_id(name), value: _value(name) }.merge(attributes)
{ type: type, name: _displayed_input_name(name), id: _input_id(name), value: _value(name) }.merge(attributes)
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
Expand Down
17 changes: 12 additions & 5 deletions lib/hanami/helpers/form_helper/values.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
40 changes: 40 additions & 0 deletions test/fixtures.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
18 changes: 18 additions & 0 deletions test/fixtures/templates/bills/edit.html.erb
Original file line number Diff line number Diff line change
@@ -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
%>
34 changes: 34 additions & 0 deletions test/integration/form_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 b4c35e7

Please sign in to comment.