Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add extra form fields outside the form declaration block

The main goal: Allow plugins, gems to add additional form fields to predefined forms.

For example, application has a devise login form:

<%= form_for(resource, :as => resource_name, ...) do |f| %>
  <%= devise_error_messages! %>
  ...
<% end %>

Marketing department wants to start a campaign for the next 2 days (ex: register with a promo code and get X bonus points). So we need to add an additional promo code field to ALL our registration forms.

Is there a way to add an extra field to the form from my rails-plugin/railtie, and define a on_submit callback method (to take action on my additional field data)?

Benefits:

  • it allows removing the functionality in 2 days or a week simply by removing it from gem file
  • guarantee that core site's functionality is not broken, it simply falls back to the original functionality
  • guarantee that a developer has not left any code somewhere in the main app
  • plugin/railtie takes care of the saving/updating data that belongs to it

Looked at ActionView code, and it seems there is no built in way of doing it. What are your thoughts?

NOTE: Drupal's form_alter hooks are a great example.

like image 559
Uzbekjon Avatar asked Feb 08 '12 16:02

Uzbekjon


2 Answers

First of all, your idea to isolate this code in a gem/railtie/engine is excellent. I think your best bet may be to monkey patch the form_for method and stick in the extra field. Regarding the on-submit trigger, if you're on Rails 3.1 and are using the asset pipeline, you can have the gem serve javascript as well, although that would require a small change in your application.js to require the gem's js file, e.g. require 'promo/application.js, if the gem were called "promo".

Take a look at the docs for Customized Form Builder

Here's some rough idea how this could work, although I haven't tried this code. I'd put this in the promo.rb file that sub-classes Railtie or Engine.

ActiveSupport.on_load(:action_view) do

  module ActionView
    module Helpers
      module FormHelper
        extend ActiveSupport::Concern

        included do
          alias_method_chain :form_for, :promo_code
        end

        module InstanceMethods

          def form_for_with_promo_code(record, options = {}, &proc)
            output = form_for_without_promo_code(record, options.merge(builder: FormBuilderWithPromoCode), proc)
            # See file: actionpack-3.1.3/lib/action_view/helpers/form_helper.rb for details
            # the output will have "</form>" as the last thing, strip that off here and inject your input field
            promo_field = content_tag :input, name: 'promo_code'  # you can also run this through the proc if you want access to the object
            output.sub(%r{</form>$},promo_field+'</form>')
          end
        end
      end
    end
  end
end

Down the road, esp. if your marketing department may run more campaigns, you may even want to change the app's forms, to point to a specific builder that you can override from a gem without the monkey patching here.

like image 178
Wolfram Arnold Avatar answered Oct 18 '22 20:10

Wolfram Arnold


Idea in steps:

1) Define a model such as AdditionalField (id, field_name, field_type, default_value, is_required)

2) then create a function like:

def self.for_form(my_form_name = nil)
 if my_form_name.nil?
  self.all
else
  self.find(:all, :contitions => {:form_type => my_form_name.type} # or whatever selection criteria
end

3) then you can iterate over the found AdditionalFields and build the correct field types as needed.

I used this solution for a comparison website where they needed to configure the questionnaires for each different comparison type.

Here's the render code I used, you'll need to amend it to suit your situation. relationships are:

convention -< booking >- user
convention -< convention_question
booking -< guests
guest -< guest_answers

QuestionsHelper

def render_guest_questions(guest, convention_question)      

    fields_for "booking[guest_answer_attributes][]", convention_question do |m|
      case convention_question.display_type
      when "Text"
        '<td>' + text_field_tag("booking[guest_answer_attributes][convention_question_#{guest.id}_#{convention_question.id}]") + '</td>'
      when "Boolean"
        '<td>' + hidden_field_tag("booking[guest_answer_attributes][convention_question_#{guest.id}_#{convention_question.id}]", "No") + check_box_tag("booking[guest_answer_attributes][convention_question_#{guest.id}_#{convention_question.id}]", "Yes") + '</td>'
      end
    end

end

Controller

# TURN GUEST/QUESTIONS INTO guest answers
if params[:booking] && !params[:booking].blank? && !params[:booking][:guest_answer_attributes].blank?
    params[:booking][:guest_answer_attributes].each do |k,v|
      handle_answers(k, v)
    end
end

def handle_answers(k, v)
  x = k.mb_chars.split(/_/)
  g_id = x[2]
  q_id = x[3]
  item = GuestAnswer.find_or_create_by_guest_id_and_convention_question_id(
                  {:guest_id => g_id,
                  :convention_question_id => q_id, 
                  :answer => v})
 end
like image 30
TomDunning Avatar answered Oct 18 '22 22:10

TomDunning