Skip to content

Commit

Permalink
Merge branch 'master' into 1.0.x
Browse files Browse the repository at this point in the history
  • Loading branch information
jodosha committed Feb 8, 2017
2 parents 0b8c077 + 315f587 commit 0d0dd29
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 27 deletions.
5 changes: 5 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
language: ruby
sudo: false
cache: bundler
before_install:
- gem update --system
- rvm @global do gem uninstall bundler -a -x
- rvm @global do gem install bundler -v 1.13.7
script: 'bundle exec rake test:coverage --trace && bundle exec rubocop'
rvm:
- 2.3.3
- 2.4.0
- ruby-head
- jruby-9.1.6.0
- jruby-head
Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# Hanami::Helpers
View helpers for Ruby web applications

## v1.0.0.beta1 (unreleased)
### Added
- [Luca Guidi] Official support for Ruby: MRI 2.4
- [Marion Duprey] Introduced Form helper `fields_for_collection` to support arrays of nested fields

## Fixed
- [Ksenia Zalesnaya] Ensure radio buttons and selects to coerce the value to boolean before to decide if they should be checked or not.
- [Anton Davydov] Escape form values to prevent XSS attacks

## v0.5.1 - 2016-12-19
### Fixed
- [Alex Coles] Ensure `#form_for`'s `values:` to accept `Hanami::Entity` instances
Expand Down
64 changes: 59 additions & 5 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 @@ -678,7 +720,7 @@ def text_field(name, attributes = {})
# # <input type="radio" name="book[category]" value="Non-Fiction" checked="checked">
def radio_button(name, value, attributes = {})
attributes = { type: :radio, name: _input_name(name), value: value }.merge(attributes)
attributes[:checked] = CHECKED if _value(name) == value
attributes[:checked] = CHECKED if _value(name).to_s == value.to_s
input(attributes)
end

Expand Down Expand Up @@ -957,17 +999,29 @@ 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)
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 1.0.0.beta1
def _displayed_input_name(name)
_input_name(name).gsub(/\[\d+\]/, '[]')
end

# Input <tt>id</tt> HTML attribute
#
# @api private
Expand Down Expand Up @@ -1047,7 +1101,7 @@ def _select_input_name(name, multiple)
# rubocop:disable Metrics/PerceivedComplexity
def _select_option_selected?(value, selected, input_value, multiple)
value == selected || (multiple && (selected.is_a?(Array) && selected.include?(value))) ||
value == input_value || (multiple && (input_value.is_a?(Array) && input_value.include?(value)))
value.to_s == input_value.to_s || (multiple && (input_value.is_a?(Array) && input_value.include?(value)))
end
# rubocop:enable Metrics/PerceivedComplexity
# rubocop:enable Metrics/CyclomaticComplexity
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 1.0.0.beta1
# @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
%>
95 changes: 80 additions & 15 deletions test/form_helper_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,18 @@
actual.must_include %(<input type="checkbox" name="book[free_shipping]" id="book-free-shipping" value="true" checked="checked">)
end
end

describe 'checked_value is boolean' do
let(:params) { Hash[book: { free_shipping: 'true' }] }

it "renders with 'checked' attribute" do
actual = view.form_for(:book, action) do
check_box :free_shipping, checked_value: true
end.to_s

actual.must_include %(<input type="checkbox" name="book[free_shipping]" id="book-free-shipping" value="true" checked="checked">)
end
end
end

describe 'automatic values' do
Expand Down Expand Up @@ -1011,6 +1023,27 @@
end
end

describe 'without values' do
let(:book) { Book.new(title: val) }
let(:val) { '"DDD" Book' }

it 'renders with value' do
actual = view.form_for(:book, action) do
text_field :title
end.to_s

actual.must_include %(<input type="text" name="book[title]" id="book-title" value="">)
end

it "allows to override 'value' attribute" do
actual = view.form_for(:book, action) do
text_field :title, value: book.title
end.to_s

actual.must_include %(<input type="text" name="book[title]" id="book-title" value="&quot;DDD&quot; Book">)
end
end

describe 'with filled params' do
let(:params) { Hash[book: { percent_read: val }] }
let(:val) { 95 }
Expand Down Expand Up @@ -1398,16 +1431,32 @@
end

describe 'with filled params' do
let(:params) { Hash[book: { category: val }] }
let(:val) { 'Non-Fiction' }
describe 'string value' do
let(:params) { Hash[book: { category: val }] }
let(:val) { 'Non-Fiction' }

it 'renders with value' do
actual = view.form_for(:book, action) do
radio_button :category, 'Fiction'
radio_button :category, 'Non-Fiction'
end.to_s
it 'renders with value' do
actual = view.form_for(:book, action) do
radio_button :category, 'Fiction'
radio_button :category, 'Non-Fiction'
end.to_s

actual.must_include %(<input type="radio" name="book[category]" value="Fiction">\n<input type="radio" name="book[category]" value="Non-Fiction" checked="checked">)
actual.must_include %(<input type="radio" name="book[category]" value="Fiction">\n<input type="radio" name="book[category]" value="Non-Fiction" checked="checked">)
end
end

describe 'decimal value' do
let(:params) { Hash[book: { price: val }] }
let(:val) { '20.0' }

it 'renders with value' do
actual = view.form_for(:book, action) do
radio_button :price, 10.0
radio_button :price, 20.0
end.to_s

actual.must_include %(<input type="radio" name="book[price]" value="10.0">\n<input type="radio" name="book[price]" value="20.0" checked="checked">)
end
end
end
end
Expand Down Expand Up @@ -1580,15 +1629,31 @@
end

describe 'with filled params' do
let(:params) { Hash[book: { store: val }] }
let(:val) { 'it' }
describe 'string values' do
let(:params) { Hash[book: { store: val }] }
let(:val) { 'it' }

it 'renders with value' do
actual = view.form_for(:book, action) do
select :store, option_values, options: { prompt: 'Select a store' }
end.to_s
it 'renders with value' do
actual = view.form_for(:book, action) do
select :store, option_values, options: { prompt: 'Select a store' }
end.to_s

actual.must_include %(<select name="book[store]" id="book-store">\n<option>Select a store</option>\n<option value="it" selected="selected">Italy</option>\n<option value="us">United States</option>\n</select>)
actual.must_include %(<select name="book[store]" id="book-store">\n<option>Select a store</option>\n<option value="it" selected="selected">Italy</option>\n<option value="us">United States</option>\n</select>)
end
end

describe 'integer values' do
let(:values) { Hash['Brave new world' => 1, 'Solaris' => 2] }
let(:params) { Hash[bookshelf: { book: val }] }
let(:val) { '1' }

it 'renders' do
actual = view.form_for(:bookshelf, action) do
select :book, values
end.to_s

actual.must_include %(<select name="bookshelf[book]" id="bookshelf-book">\n<option value="1" selected="selected">Brave new world</option>\n<option value="2">Solaris</option>\n</select>)
end
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions test/html_helper/html_builder_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@
end

it 'renders multiple attributes' do
result = @builder.input('required' => true, 'something' => 'bar').to_s
result.must_equal('<input required="required" something="bar">')
result = @builder.input('required' => true, 'value' => 'Title "book"', 'something' => 'bar').to_s
result.must_equal('<input required="required" value="Title "book"" something="bar">')
end
end

Expand Down
Loading

0 comments on commit 0d0dd29

Please sign in to comment.