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!