Create reusable form partials in Rails

If you’ve got an admin part in your rails app and some settings to take care of, chances are that you have a lot of similar data. And what can be more wearying, than creating all those forms, index/edit/new views over and over again? Of course you could use the scaffold, but I personally strongly dislike it.

So I figured out I try to make my partials variable as possible. This post shows you how you can create highly reusable form partials.

My structure is as following:

There is an admin panel. Inside admin panel there are settings (which get repetitive) and inside settings you can manage models like “colors”, “patterns”, “fabric” (you can guess it – it’s a clothing online shop). All of them contain identical data, but have some differences as well.

I’ve got only 3 views – index, edit and new, because there is no sense for having a show view, as it should be the edit view right from the get go. This said, we need only two partials – one, for listing items inside our index view and one for creating and editing existing data.

Let’s start with routes.rb:

namespace :admin do
  	get '', to: 'dashboard#index', as: "/"

  	namespace :settings do
  		resources :borders
  		resources :colors
  		resources :patterns
  		resources :categories
      resources :fabrics
  	end
  end

Simple dashboard when you visit plain old “/admin” and then resources inside another namespace “settings”.

My index view – index.html.haml:

=render partial: 'admin/shared/list_items', locals: {name_ru: 'ткань', current_objects: @fabrics, name_en: "fabric"}

As you can see, I pass 3 local variable to my partial (this is the only thing you need to edit for each model. But hey, it’s only 3 words!) – the russian name, so I can have some customized buttons and links. The current collection and the name in english.

Listing items partial (a.k.a. index)

.panel-body.pn
    .table-responsive
      %table.table.admin-form.theme-warning.tc-checkbox-1.fs13
        %thead
          %tr.bg-light
            %th.text-center Select
            %th Наименование
            %th.text-right Статус
        %tbody
          -unless current_objects.nil?
            -current_objects.each do |object|
              %tr
                %td.text-center
                  %label.option.block.mn
                    %input{:name => "mobileos", :type => "checkbox", :value => "FR"}
                      %span.checkbox.mn
                %td
                  =link_to object.name, send("edit_admin_settings_#{single}_path", object)
                %td.text-right
                  .btn-group.text-right
                    %button.btn.btn-success.br2.btn-xs.fs12.dropdown-toggle{"aria-expanded" => "false", "data-toggle" => "dropdown", :type => "button"}
                      Действия
                      %span.caret.ml5
                    %ul.dropdown-menu{:role => "menu"}
                      %li
                        =link_to 'Редактировать', send("edit_admin_settings_#{single}_path", object)
                      %li.divider
                      %li
                        =link_to 'Удалить', send("admin_settings_#{single}_path", object.id), method: :delete, data: {confirm: 'Действительно удалить?'}
          %tr
            %td
            %td
            %td.text-right
              .btn-group.text-right
                =link_to "Новая/ый #{name}", send("new_admin_settings_#{single}_path"), class: 'btn btn-primary'

Well, apart from some html, the important things are:

  • to check if there is at least one item in the collection (remember, we pass the collection under the name “current_objects”)
  • for each object in current_objects we access it using the variable “object”
  • since our partial is variable we can’t use hardcoded paths, like “admin_settings_colors_path”. Meaning we must make use of the “send” method, which basically makes a method from a string. To pass parameters to a method created by “send” you simply put this parameter as a parameter to the send-method. send(“hello_there”, caramba) is equivalent to calling method hello_there(caramba)
  • Because “object” is actually an object (wow!) we can’t use it in the path generation. This is why I pass “single” as a local variable. “Single” is simply the singular form of the model (category, color, pattern etc.)

My edit/new view:

They are absolutely identical.

.tray.tray-center
  =render 'admin/navigation/flash'
  =render partial: 'admin/shared/form', locals: {current_object: @pattern}

I render some flash messages to tell users about successful or failed actions. Then I render my second partial using again a local variable to transmit the collection.

Form partial

The core 🙂

=form_for [:admin, :settings, current_objects] do |f|
  .panel.mb25.mt5
    .panel-heading
      %span.panel-title.hidden-xs Добавить новую сущность
      %ul.nav.panel-tabs-border.panel-tabs
        %li.active
          %a{"data-toggle" => "tab", :href => "#tab1_1"} Описание сущности
    .panel-body.p20.pb10
      .tab-content.pn.br-n.admin-form
        .section.row.mbn
          -if current_objects.respond_to?(:picture)

            .col-md-4
              .fileupload.fileupload-new.admin-form{"data-provides" => "fileupload"}
                .fileupload-preview.thumbnail.mb20
                  -unless current_objects.picture.nil?
                    =image_tag current_objects.picture
                  -else
                    %img{:alt => "holder", "data-src" => "holder.js/100%x140"}
                .row
                  .col-xs-7.pr5
                    %input#name2.text-center.event-name.gui-input.br-light.bg-light{:name => "name2", :placeholder => "Img Keywords", :type => "text"}
                      %label.field-icon{:for => "name2"}
                  .col-xs-5
                    %span.button.btn-system.btn-file.btn-block
                      %span.fileupload-new Select
                      %span.fileupload-exists Change
                      =f.file_field :picture, id: 'fileupload', type: :file
                      / %input{:type => "file", name: 'picture'}
          .col-md-8.pl15
            .section.mb10
              %label.field.prepend-icon{:for => "name2"}
                =f.text_field :name, id: "price", class: "event-name gui-input br-light light", placeholder: 'Название сущности'
                %label.field-icon{:for => "name2"}
                  %i.fa.fa-tag
            -if current_objects.respond_to?(:price)
              .section.mb10
                %label.field.prepend-icon
                  =f.number_field :price, id: "price", class: "event-name gui-input br-light light", placeholder: 'Стоимость'
                  %label.field-icon{:for => "comment"}
                    %i.fa.fa-comments
                  %span.input-footer.hidden
                    %strong> Hint:
                    Don't be negative or off topic! just be awesome...
            -if current_objects.respond_to?(:hex)
              .section.mb10
                %label.field.prepend-icon
                  =f.text_field :hex, id: "hex", class: "event-name gui-input br-light light", placeholder: 'Hex-значение цвета'
                  %label.field-icon{:for => "comment"}
                    %i.fa.fa-paint-brush

       
        / end section row section
        %hr.short.alt
          .section.row.mbn
            .col-sm-8
              %label.field.option.mt10
                %input{:checked => "", :name => "info", :type => "checkbox"}
                  %span.checkbox>
                  Save Customer
                  %em.small-text.text-muted - A Random Unique ID will be generated
            .col-sm-4
              %p.text-right
                =f.submit "Сохранить", class: "btn btn-primary"
          / end section
  -if current_objects.respond_to?(:image)
    .panel
      .panel-heading
        %span.panel-title Загруженные фотографии рецепта
        .widget-menu.pull-right
          %code.mr10.bg-light.dark.p3.ph5 фото можно удалить
      .panel-body
        =f.fields_for :pictures do |builder|
          =image_tag builder.object.image.mini_thumb.url, class: "img-responsive thumbnail mr25 uzhin_doma_mini_thumb"
          =builder.label :_destroy, 'Удалить фотографию?'
          =builder.check_box :_destroy

Well, again a lot of HTML, but I will summarize the core points for you:

  • First of all we build our form using triple nesting =form_for [:admin, :settings, current_objects] do |f|
  • Now comes the most interesting part – surely your models will differ a bit. Maybe on has images to upload, the other doesn’t. One has a price tag, other don’t. Etc. and so on. Thus, we need to create an all-purpose-form, disabling fields when not required. This is done by respond_to? method. You can see it in this line  -if current_objects.respond_to?(:picture) This basically means, that if there is no :picture method for current object, the block below won’t be executed. Nifty, eh?
  • In my example every model has a field, called “name”, this is why I don’t check for it. All other fields are encapsulated in a respond_to? check.

Resume

With just a few methods – send and respond_to? you can create highly reusable partials where your maintain effort goes towards zero. Only one file for each action (index and edit/new) – truly amazing!

Published by

Anton

Hello! My name is Anton. I am a passionate project manager who loves digging deep into code. You can check my Github and CodeEval. Hopefully my thoughts on management can lead you to one or another good idea.