Rails: Two buttons on a form
Recently I had a create form where one should be able to chose an associated model from a large set of possible values. It wouldn’t be too pleasant to have a drop down listing all these options. My idea was to render paginated radio buttons and a search field to limit the items available.
Submitting a query shouldn’t wipe out your previous inputs. So, both, create and search button, submit to the same action which then must decide by the button pressed what to do. As searching is a common concern I put this into a – surprise – Concern.
Form
Imagine a politics discussion board. You think it would be a nice feature if users could award prizes to politicians to honor their efforts. Maybe someone wants to award a politician for his achievements as a servant of a neoliberal lobby, e.g. the financial sector. So a user creates the „Disguised private investor bailout magician“ prize and needs to select the awardee. There are a lot of candidates, the German Bundestag alone currently already has 631 members. So here is how the form could look like:
The labels and inputs for tile and description are regular form elements. Here is the code for the search field and the select options, pagination is done by kaminari:
- <%= render layout: 'shared/paginated', locals: {collection: @politicians, entry_name: 'Politicians', params: {action: :new}} do %>
- <div class="input-group">
- <%= text_field_tag :query, params[:query], class: 'form-control', placeholder: 'Search politician...' %>
- <div class="input-group-btn">
- <%= button_tag type: 'submit', class: 'btn btn-default', name: :commit, value: :search_politicians do %>
- Search
- <% end %>
- </div>
- </div>
- <div class="btn-group-vertical form-control-static" role="group" data-toggle="buttons">
- <% @politicians.each do |politician| %>
- <% active = politician.id.to_s == @award.awardee_id %>
- <% classes = 'btn btn-default text-left-important' + (active ? ' active' : '') %>
- <%= label_tag '', class: classes do %>
- <%= f.radio_button 'awardee', politician.id %><%= "#{politician.last_name}, #{politician.first_name}" %>
- <% end %>
- <% end %>
- </div>
- <% end %>
The important parts are the text input named query and the button with value search_politicians to submit the query.
Controller
Here is what the controller actions for new and create look like – the magic happens in line 4 where the SearchAndRedirect concern is called:
- class AwardsController < ApplicationController
- include SearchAndRedirect
- search_and_redirect commit: :search_politicians, redirects: {create: :new}, forwarded_params: [:award, :query]
- def new
- if params[:award]
- @award = Award.new(params.require(:award).permit(:title, :description, :awardee_id))
- else
- @award = Award.new
- end
- @politicians = Politician.all.page(params[:page])
- search = "%#{params[:query]}%"
- @politicians = @politicians.where("first_name LIKE ? OR last_name LIKE ?", search, search) if params[:query].present?
- end
- def create
- @award = Award.new(params.require(:award).permit(:title, :description, :awardee_id))
- if @award.save
- redirect_to :award, notice: 'Award was created.'
- else
- @politicians = Politician.all.page(params[:page])
- render 'new'
- end
- end
- end
search_and_redirect is called with three parameters: :commit, :redirects and :forwarded_params.
The :commit parameter tells the concern for which submit actions it should get active. So if a form is submitted with ’search_politicians‘ as commit value it will forward the specified parameters :award (to maintain previous inputs) and :query to the requested action.
In this case this rule applies if a form was submitted to the create action and will be redirected to new. As :redirects takes a hash, you can specify multiple redirects for the same forwarding rule.
Concern
Finally, here is the concern’s code:
- module SearchAndRedirect
- extend ActiveSupport::Concern
- module ClassMethods
- def search_and_redirect(options)
- options.deep_stringify_keys!
- commits = (options['commits'] || [options['commit']]).map(&:to_s)
- before_filter only: options['redirects'].keys do
- action = params[:action]
- if commits.include?(params[:commit]) && options['redirects'].keys.include?(action)
- forwarded_params = options['forwarded_params'].reduce({}) { |memo, param| memo.tap { |m| m[param] = params[param] } }
- redirect_to({action: options['redirects'][action]}.merge(forwarded_params))
- end
- end
- end
- end
- end
Now, we can search for Wolfram without having to rewrite the whole description again: