Skip to content

Commit

Permalink
Support emit: false option for non-rails template forms allowing bloc…
Browse files Browse the repository at this point in the history
…k based form use without appending to template

This is useful when calling methods that need to call form with
blocks, but where the block is not yielding control back to the
template.  This will allow support for CSRF tokens and forme_set
metadata for such forms, which did not work correctly before as
as mostly empty form would be appened to the template.
  • Loading branch information
jeremyevans committed Dec 29, 2023
1 parent de2b07c commit b0453b1
Show file tree
Hide file tree
Showing 8 changed files with 73 additions and 7 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
=== master

* Support emit: false option for non-rails template forms allowing block based form use without appending to template (jeremyevans)

=== 2.4.1 (2023-09-19)

* Add dependency on bigdecimal, as bigdecimal is moving from standard library to bundled gem in Ruby 3.4 (jeremyevans)
Expand Down
20 changes: 18 additions & 2 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -800,7 +800,7 @@ For new code, it is recommended to use forme_route_csrf, as that uses Roda's rou
plugin, which supports more secure request-specific CSRF tokens. In both cases, usage in ERB
templates is the same:

<% form(@obj, :action=>'/foo') do |f| %>
<% form(@obj, action: '/foo') do |f| %>
<%= f.input(:field) %>
<% f.tag(:fieldset) do %>
<%= f.input(:field_two) %>
Expand All @@ -810,6 +810,8 @@ templates is the same:
The forme_route_csrf plugin's +form+ method supports the following options
in addition to the default +Forme.form+ options:

:emit :: Set to false to not emit implicit tags into template. This should only be
used if you are not modifying the template inside the block.
:csrf :: Set to force whether a CSRF tag should be included. By default, a CSRF
tag is included if the form's method is one of the request methods checked
by the Roda route_csrf plugin.
Expand All @@ -818,6 +820,19 @@ in addition to the default +Forme.form+ options:
CSRF token unless the Roda route_csrf plugin has been
configured to support non-request specific tokens.

The <tt>emit: false</tt> option allows you to do:

<%= form(@obj, {action: '/foo'}, emit: false) do |f|
f.input(:field)
f.tag(:fieldset) do
f.input(:field_two)
end
end %>

This is useful if you are calling some method that calls +form+ with a block,
where the resulting entire Forme::Forme object will be literalized into the
template. The form will include the CSRF token and forme_set metadata as appropriate.

The forme plugin does not require any csrf plugin, but will transparently use
Rack::Csrf if it is available. If Rack::Csrf is available a CSRF tag if the form's
method is +POST+, with no configuration ability.
Expand Down Expand Up @@ -856,7 +871,8 @@ It allows you to use the following API in your erb templates:
<% end %>

In order to this to work transparently, the ERB outvar needs to be <tt>@_out_buf</tt> (this is the
default in Sinatra).
default in Sinatra). The Sinatra extension also supports the <tt>emit: false</tt> option to not
directly modify the related template (see example in the Roda section for usage).

= Rails Support

Expand Down
5 changes: 3 additions & 2 deletions lib/forme/template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def method_missing(*a, &block)
%w'inputs tag subform'.each do |meth|
class_eval(<<-END, __FILE__, __LINE__+1)
def #{meth}(*a, &block)
return @form.#{meth}(*a) unless block
return @form.#{meth}(*a) if !block || @form.opts[:emit] == false
buffer = @form.to_s
offset = buffer.length
Expand All @@ -40,6 +40,7 @@ def #{meth}(*a, &block)

# Serialize the tag and inject it into the output.
def emit(tag)
return if @form.opts[:emit] == false
return unless output = output()
output << tag
end
Expand Down Expand Up @@ -72,7 +73,7 @@ def form(obj=nil, attr={}, opts={}, &block)
private

def _forme_form(obj, attr, opts, &block)
if block_given?
if block_given? && opts[:emit] != false
erb_form = buffer = offset = nil
block = proc do
wrapped_form = erb_form.instance_variable_get(:@form)
Expand Down
6 changes: 3 additions & 3 deletions lib/roda/plugins/forme_erubi_capture.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class Form < ::Forme::Template::Form
%w'inputs tag subform'.each do |meth|
class_eval(<<-END, __FILE__, __LINE__+1)
def #{meth}(*)
if block_given?
if block_given? && @form.opts[:emit] != false
@scope.capture_erb do
super
@scope.instance_variable_get(@scope.render_opts[:template_opts][:outvar])
Expand All @@ -33,8 +33,8 @@ def emit(tag)
end

module InstanceMethods
def form(*)
if block_given?
def form(obj=nil, attr={}, opts={}, &block)
if block && (obj.is_a?(Hash) ? attr : opts)[:emit] != false
capture_erb do
super
instance_variable_get(render_opts[:template_opts][:outvar])
Expand Down
12 changes: 12 additions & 0 deletions spec/erb_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@
END
end

r.get 'no_emit' do
erb <<END
<%= form([:foo, :bar], {:action=>'/baz'}, :emit=>false) do |f|
f.tag(:p, {}, 'FBB')
f.tag(:div) do
f.input(:first)
f.input(:last)
end
end %>
END
end

r.get 'nest' do
erb <<END
<% form([:foo, :bar], :action=>'/baz') do |f| %>
Expand Down
12 changes: 12 additions & 0 deletions spec/erubi_capture_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@
END
end

r.get 'no_emit' do
erb <<END
<%= form([:foo, :bar], {:action=>'/baz'}, :emit=>false) do |f|
f.tag(:p, {}, 'FBB')
f.tag(:div) do
f.input(:first)
f.input(:last)
end
end %>
END
end

r.get 'nest' do
erb <<END
<%|= form([:foo, :bar], :action=>'/baz') do |f| %>
Expand Down
17 changes: 17 additions & 0 deletions spec/roda_integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,23 @@ def _forme_set(meth, obj, orig_hash, *form_args, &block)
body.sub(%r{<input name="_csrf" type="hidden" value="([^"]+)"/>}, '<input name="_csrf" type="hidden" value="csrf"/>').must_equal '0 <form action="/baz" class="forme album" method="post"><input name="_csrf" type="hidden" value="csrf"/> 1 <input id="album_artist_attributes_id" name="album[artist_attributes][id]" type="hidden" value="2"/><table><caption>Foo</caption><thead><tr><th>Name</th></tr></thead><tbody><tr><td class="string"><input id="album_artist_attributes_name" maxlength="255" name="album[artist_attributes][name]" type="text" value="A"/></td></tr></tbody></table> 2 <input type="submit" value="Sub"/></form>3'
end

it "should have subform work correctly when using emit: false form option" do
@app.route do |r|
@album = Album.load(:name=>'N', :copies_sold=>2, :id=>1)
@album.associations[:artist] = Artist.load(:name=>'A', :id=>2)
erb <<END
0
<%= form(@album, {:action=>'/baz'}, :button=>'Sub', :emit=>false) do |f|
f.subform(:artist, :inputs=>[:name], :legend=>'Foo', :grid=>true, :labels=>%w'Name')
end %>
3
END
end

body = @app.call('REQUEST_METHOD'=>'GET')[2].join.gsub("\n", ' ').gsub(/ +/, ' ').chomp(' ')
body.sub(%r{<input name="_csrf" type="hidden" value="([^"]+)"/>}, '<input name="_csrf" type="hidden" value="csrf"/>').must_equal '0 <form action="/baz" class="forme album" method="post"><input name="_csrf" type="hidden" value="csrf"/><input id="album_artist_attributes_id" name="album[artist_attributes][id]" type="hidden" value="2"/><table><caption>Foo</caption><thead><tr><th>Name</th></tr></thead><tbody><tr><td class="string"><input id="album_artist_attributes_name" maxlength="255" name="album[artist_attributes][name]" type="text" value="A"/></td></tr></tbody></table><input type="submit" value="Sub"/></form> 3'
end

it "#forme_set should include HMAC values if form includes inputs for obj" do
h = forme_set(@ab, :name=>'Foo')
proc{forme_call(h)}.must_raise Roda::RodaPlugins::FormeSet::Error
Expand Down
4 changes: 4 additions & 0 deletions spec/shared_erb_specs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ def o.puts(*) end
sin_get('/inputs_block_wrapper').must_equal '<form action="/baz"><fieldset class="inputs"><legend>FBB</legend><ol> <input id="last" name="last" type="text" value="bar"/> </ol></fieldset></form>'
end

it "#form should handle emit: false option with self-contained blocks to be usable" do
sin_get('/no_emit').must_equal '<form action="/baz"><p>FBB</p><div><input id="first" name="first" type="text" value="foo"/><input id="last" name="last" type="text" value="bar"/></div></form>'
end

it "#form should add start and end tags and yield Forme::Form instance" do
sin_get('/nest').must_equal '<form action="/baz"> <p>FBB</p> <div> <input id="first" name="first" type="text" value="foo"/> <input id="last" name="last" type="text" value="bar"/> </div> </form>'
end
Expand Down

0 comments on commit b0453b1

Please sign in to comment.