[RUBY] Create a parent-child relationship form with form_object (I also wrote a test)

background

There are tables with various child elements such as @ comment and @ employee associated with @ shop. At the time of initial information registration, all child elements such as @ shop, @ comment, and @employee could be saved together with the parent element.

Although these were achieved using ʻaccepts_nested_attributes_for` in the form of the article below,

▼ It was realized like this Create child table data at once with fields_for (I also write a test) [Rails] [Rspec]

Of these, editing forms only for @ shop and posting / editing forms such as @ comment and @employee became necessary, so the model gradually became bloated with various descriptions.

What is form_object?

↑ In the above situation, form_object can simplify the description of the model by collecting validation and default value settings related to a specific form in one place. Personally, I've stumbled upon the introduction, so I'll write an article and keep a record.

The execution environment is as follows.

Introduction method

The basic way to write form_object, controller, and view is as follows. This time, I would like to take an example of a form that allows you to register one @ comment when you first register @ shop.

This article was the most helpful for implementation.

Save multiple child records without accepts_nested_attributes_for

DB structure

shops
name string
category integer

↑ category is the shop type. ʻEnum` column.

comments
content text
shop_id integer

Created file

form_object

/forms/shop_entry_form.rb


class ShopEntryForm
  include ActiveModel::Model

  # @Description about shop-----------------------------
  concerning :ShopBuilder do
    def initialize(params = {})
      super(params)
      @category = params[:category]
    end

    def facility
      @shop ||= Shop.new
    end
  end

  attr_accessor :name, :category
  validates :name, presence: true
  validates :category, presence: true

  # @Description about comment-----------------------------
  concerning :CommentBuilder do
    attr_reader :comments_attributes

    def comments
      @comments_attributes ||= Comment.new
    end

    def comments_attributes=(attributes)
      @comments_attributes = Comment.new(attributes)
    end
  end

  attr_accessor :content
 
  #Implementation logic------------------------------------
  def save
    #If a validation error occurs, false is returned and the following processing is not performed.
    return false if invalid?

    shop.assign_attributes(shop_params)
    build_asscociation

    shop.save ? true : false
  end
  
  private

  def shop_params
    {
      name: name,
      category: @category,
    }
  end

  def build_asscociations
    #Add comment to the child element of shop. However, if the contents are empty, it will not be added.
    shop.comments << comments if comments[:content].present?
  end

  
end

There was a lot of stumbling block with this alone. .. .. .. First, the concerning: ShopBuilder do ... end part has the following meanings.

#This description is...
concern :ShopBuilder do
  ...
end

#Same as below
module ShopBuilder
  extend ActiveSupport::Concern
  ...
end

For details, see This article that I referred to when implementing.

Next, the part of ʻinitialize (params = {}) ... end` has the following meaning.

def initialize(params = {})
  # @Make shop params accessible
  super(params)
  
  #Description for columns for which default values are set in DB
  @category = params[:category]
end

First of all, regarding super (params), according to the following article, which was also a very helpful article for implementation.

Use form class

A description that stores parameters with super (params), and has the same meaning as the following description.

@attributes = self.class._default_attributes.deep_dup
assign_attributes(params)

In addition, columns for which default values are set on the db side cannot access params unless you explicitly write to access params as shown below, and even if you enter a ** value, it will be the default value of DB. It has become ... ** **

@category = params[:category]

This mystery cannot be solved. I would like to make it a future issue. .. .. The reason why the default value is required on the db side for columns using enum is [this article](https://framgia.com/journal/rails%E3%81%AEenum-%E8%AB%B8%E5% Please see 88% 83% E3% 81% AE% E5% 89% A3% EF% BC% 88% EF% BC% 91% EF% BC% 89-% E3% 80% 80from-viblo /).

And the part of def comments_attributes = (attributes) ... end,

def comments_attributes=(attributes)
  @comments_attributes = Comment.new(attributes)
end

This is a way of writing setter method that you rarely see if you are doing only Rails, and you can change the element with @ by ** argument in the form of method (argument)ending with= I will. Personally, I think it's close to the image of doing something like this.

def comments_attributes=(attributes) # ...The following is omitted

#Such an image
comments_attributes = attributes

#So you can call it like this
self.comments_attributes
# =>Contents of attributes

It's because I didn't understand Ruby getters and setters correctly. .. .. .. Tohoho. .. .. I will do my best. .. .. For the method ending with =, see ["Introduction to Ruby for professionals, from language specifications to test-driven development / debugging techniques "](https://www.amazon.co.jp/dp/B077Q8BXHC). I reread p215` of /) about 15 times.

controller

The following is a description of the controller. The controller looks like this.

app/controllers/shops_controller.rb


class ShopsController < ApplicationController
  
  def new
    @shop = ShopEntryForm.new
  end

  def create
    @shop = ShopEntryForm.new(shop_entry_params)
    if @shop.save
      #What to do when it succeeds
    else
      #What to do if it fails
    end
  end

  private

  def shop_entry_params
    params.require(:shop_entry_form).permit(:caregory,
                                            :name,
                                            comments_attributes: [:content])
  end
end

I have the impression that the description did not decrease unexpectedly. At first, I expected that shop_entry_params would be reduced from the controller, but I couldn't delete it from the controller after all. Only the method that creates the association could be removed from the controller.

As for Model, ** validation and default value setting methods, associations, etc. were all deleted! ** No additional description! !! After all, form_object is a convenient way to write a slim model! !!

View Finally, the View looks like this.

haml:app/views/shops/new.html.haml


= form_with model: @shop, url: shops_path, local: true do |f|
    = f.text_field :name

    = f.fields_for :shop_comments, local: true do |comment_form|
      = comment_form.text_field

    = f.submit "Send"

Using fields_for is no different from the implementation using ʻaccept_nested_attributes_for` ^^

test

The test was also very simple!

spec/forms/shop_entry_form_spec.rb


require 'rails_helper'

RSpec.describe ShopEntryForm, type: :model do
  before do
    @shop_form = ShopEntryForm.new(category: "category1", name: "Test shop")
  end

  describe "Validation test" do
    it "Pass validation if you have a name and category" do
      @shop_form.valid?
      expect(@shop_form).to be_valid
    end
    
    #The following is omitted
  end
end

All I had to do was pay attention to the location of the file, the RSpec.describe ShopEntryForm ... part, and the description when creating the instance for testing ^^

This is a little old, but I created it with reference to this article.

Write a form object test in RSpec

Impressions, reference materials, etc.

Well, it took a really long time to implement. The actual form had three types of nested child elements, and the shape was quite complicated, but most of all, I think it was because I wasn't used to writing plain Ruby. .. .. Once calmed down, I would like to review Ruby again.

This is a summary of the articles and materials that I referred to this time.

▼ Overall writing Save multiple child records without accepts_nested_attributes_for

▼ How to access params Use form class "Introduction to Ruby for professionals, from language specifications to test-driven development and debugging techniques" (p.215)

▼ About Concerning Bite-sized separation of concerns

▼ How to write a test Write a form object test in RSpec

After this, the edit and update forms also remain, so I would like to work on that next ^ ^

Recommended Posts

Create a parent-child relationship form with form_object (I also wrote a test)
I wrote a test with Spring Boot + JUnit 5 now
I wrote a CRUD test with SpringBoot + MyBatis + DBUnit (Part 1)
I wrote a primality test program in Java
[RSpec] I wrote a test for uploading a profile image.
I tried to create a java8 development environment with Chocolatey
[Rails] I tried to create a mini app with FullCalendar
I want to create a form to select the [Rails] category
I tried to create a padrino development environment with Docker
Create a playground with Xcode 12
I want a quick test S3. Start minio with docker-compose and create a state with an initial bucket.
I tried printing a form with Spring MVC and JasperReports 1/3 (JasperReports settings)
Do I need a test if I do DDD in a language with types?
I tried printing a form with Spring MVC and JasperReports 3/3 (Spring MVC control)
I can't create a Java class with a specific name in IntelliJ
I made a simple search form with Spring Boot + GitHub Search API.
Create a Vue3 environment with Docker!
I made a GUI with Swing
Create exceptions with a fluid interface
Create a Maven project with a command
I tried printing a form with Spring MVC and JasperReports 2/3 (form template creation)
I want to create a dark web SNS with Jakarta EE 8 with Java 11
I wrote a Lambda function in Java and deployed it with SAM