Before showing you my solution it is recommended to understand how it is going to work, and what are the basic principals. I used a gem which is a part of a bigger project, a new architecture for Rails apllications, called Trailblazer. As it stays on the github page:
"Trailblazer is a thin layer on top of Rails. It gently enforces encapsulation, an intuitive code structure and gives you an object-oriented architecture."
The creators of this gem are trying to change the architecture of rails applications. As a result your models will not contain any logic inside, no callbacks, nested attributes or even validations. Controllers will be less extensive and helpers will be replaced by an additional abstract layer. It is a new structure of your application, with the code organized in smaller components created to handle different concerns. Framework is concept-driven, which means that all files connected with one concept (models, views, assets etc.) are stored in one place. It adds new layers to MVC structure of rails application. It separates codes into: controller, operation and cells. Operation contains model, form object and representer object. Cell object is rendering a view. On their Github page you can find a link to a sample application, which will help you its structure. I think this is really worth checking, even if you are not going to use it even once in your life. You will see that there is also a second path, a different way of thinking, with its pros and cons and sometimes it may prove to be a better solution for your particular app requirements.
It would be really difficult to implement this kind of solution, especially if development has already started. Fortunately it can be implemented partially in the typical, conventional rails application. And this is the approach I have used in my situation. Instead of changing the entire architecture of the app I added a reform gem which allows me to create a form model. It can handle validations and wrap selected models in one form, so exactly what I needed.
The part of my application’s structure defining ‘product’ looks like this:
├── models │ ├── product │ │ ├── description.rb │ │ ├── image.rb │ │ ├── product.rb
I have three models inside the product module. Both
image.rb contain fields defining their respective models, and both are related to
product.rb. To generate a form that will create those three objects at once I will use reform gem.
Inside the product module I added a new class – ProductForm.rb which will define form fields using
::property, and all validations.
odule Product class ProductForm < Reform::Form property :name validates :name, presence: true property :description do property :content property :short_content validates :content, :short_content, presence: true end collection :images do property :image_path property :image_title validates :image_title, :image_path, presence: true end end end
ProductForm is a class that inherits from Reform gem. A
property defines field of the product object to be created, additionally I added validations. Product is in a 1-1 relation with the description model (
has_one), so it can be treated as another
::property, but as a block. Inside are fields of this particular model, with validations. Product is also in relation with image model (
has_many), so it is added in the same way as description, but instead of property it is a collection. It works like a nested
property that iterates over objects collection.
class ProductsController < ApplicationController before_action :new_form_setup, only: [:new, :create] def new ; end def create if @form.validate(params[:product_product]) @form.save redirect_to admin_dashboard_path, notice: t('admin.product.create_success') else render 'new', alert: @form.errors.full_messages.join(', ') end end private def product_params params.require(:product).permit(:id, :name) end def new_form_setup @product = Product::Product.new @product.description = Product::Description.new @product.images.build @form = Product::ProductForm.new(@product) end end
To render a form all objects have to be initialized on both new and create method as shown in the
new_form_setup. It is also important to understand how create action works. Form object is saved instead of product, and it depends on params validations. Instead of this detail both actions look pretty similar to typical ones.
= form_for @form, url: products_path, method: :post do |f| = t('admin.dashboard.product.name') = f.text_field :name = f.fields_for :description do |d| .form-group = t('admin.dashboard.product.content') = d.text_field :content .form-group = t('admin.dashboard.product.short_content') = d.text_field :short_content = f.fields_for :images do |i| .form-group = t('admin.dashboard.product.image_title') = i.text_field :image_title .form-group = t('admin.dashboard.product.image_path') = i.file_field :image_path .form-group = f.submit t('admin.dashboard.product.submit'), name: nil, class: 'btn btn-primary'
Form created in haml with translation looks more or less like this. Again it is not working on the
@product but on the
@form. Thats the only difference between conventional forms.
As you see the gem gives a new functionality without any difficult changes in the code (but with a change in structure and meaning). This is just simple usage, the gem is far more powerful and gives oportunities to use it in many cases. It is really worth to check out. This solution can be even used as a first step to trying Trailblazer framework itself. I think it is sometimes very good to try a different way of building applications.