[RUBY] Form object ~ An epoch-making guy who interacts with multiple models in one form ~

[1. Create the skeleton of the app](# 1-Create the skeleton of the app) [2. Start form object](# 2-Start form object)

It was difficult to understand, but as the title says, form objects are used to save and update information in multiple models.

First is an image diagram image.png

Use the form object to implement the format that needs to go through multiple models in this way.

Now let's think about what kind of description is required in the form object.

-Migration of validation set in the model that uses form object -Description that enables the form object to be used in the controller ・ Description through the model

Roughly, you'll need these three! Now, let's create a format to register user information and address information at the same time.

1-Create the skeleton of the app

1. Make the skeleton of the app Execute the following command
% rails _6.0.0_ new format_app -d mysql
% cd format_app
% rails db:create

After that, let's connect to the local host and check if you can connect to the rails initial screen

Setting / creating MVC etc.

Controller creation and routing settings Creating a controller
 % rails g controller formats index

Routing settings

Rails.application.routes.draw do
  root 'formats#index'
  resources :formats, only: [:index, :new, :create]
end

Controller action settings First of all, only user registration and browsing will be possible

class FormatsController < ApplicationController
  def index
    @formats = User.all
  end

  def new
    @format = User.new
  end

  def create
    @format = User.create(format_params)
    redirect_to root_path
  end

  private
  def format_params
    params.require(:user).permit(:name, :name_kana, :nickname)
  end
end
Creating a model migration
% rails g model user
% rails g model address

Migration file settings

XXXXXXXXXX_create_user.rb

class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      t.string :name,           null: false
      t.string :name_kana,   null: false
      t.string :nickname,       null: false
      t.timestamps
    end
  end
end

XXXXXXXXXX_create_address.rb

class CreateAddresses < ActiveRecord::Migration[6.0]
  def change
    create_table :addresses do |t|
      t.string :postal_code,    default: "",  null: false
      t.integer :prefecture_id,                  null: false
      t.string :city,           default: ""
      t.string :house_number,   default: ""
      t.string :building_name,  default: ""
      t.references :user,                     null: false,  foreign_key: true
      t.timestamps
    end
  end
end

Model settings

user.rb ・ Association settings -> user model parent and address model child

・ Validation settings -> Prevent the value from becoming empty -> Names are only kanji, hiragana, and katakana -> Katakana is the only frigana -> Nickname is only single-byte alphanumeric characters

class User < ApplicationRecord
  has_one :address
  with_options presence: true do
    validates :name,      format: {with:/\A[Ah-Hmm-One-龥]/,  message: "is invalid. Please input full-width characters."}
    validates :name_kana, format: {with:/\A[A-Car-]+\z/,    message: "is invalid. Please input full-width katakana characters."}
    validates :nickname,  format: {with:/\A[a-z0-9]+\z/i,     message: "is invalid. Please input half-width characters."}
  end
end

address.rb ・ Association settings -> user model parent and address model child

class Address < ApplicationRecord
  belongs_to :user
end
Create view file First, make it possible to register and view only user information.

``formats/index.html.erb`


<div class="wrapper">
  <div class="btn">
    <%= link_to 'to register', new_format_path %>
  </div>
  <div class="formats">
    <% @formats.each do |format| %>
      <div class="format">
        <div class="format-name"><%= format.name %>/<%= format.name_kana %>/<%= format.nickname %></div>
      </div>
    <% end %>
  </div>
</div>

Create a format that allows you to register only user information

``formats/new.html.erb`


<%= form_with(model: @format, url: formats_path, local: true) do |form| %>
  <h1>Enter user name</h1>
  <div class="field">
    <%= form.label :name, "Name (full-width)" %>
    <%= form.text_field :name %>
  </div>
  <div class="field">
    <%= form.label :name_kana, "Frikana (double-byte katakana)" %>
    <%= form.text_field :name_kana %>
  </div>
  <div class="field">
    <%= form.label :nickname, "Nickname (half-width alphanumeric characters)" %>
    <%= form.text_field :nickname %>
  </div>
  <div class="actions">
    <%= form.submit "to register" %>
  </div>
<% end %>

2. Start form object # 2-form object started Well, the main subject is from here. First, let's create the necessary files. Create a forms directory in your app and generate user_address.rb in it
% mkdir app/forms
% touch app/forms/user_address.rb

Then define the same class name as the file name inside

``user_address.rb`


class UserAddress
end

When using the form object, embed the description ActiveModel :: Model in the file generated above. This makes user_address.rb act like a model. See below.

``user_address.rb`


class UserAddress
  include ActiveModel::Model
end

This file is now a pseudo model.

Next, set the values that can be referenced / updated in this file. The value required at the time of user registration earlier

It was: name,: name_kana,: nickname.

And I want to register the address further, so add the following value. Zip code (: postal_code), prefecture (: prefecture_id), city (: city), address (: house_number), building name (: building_name)

So, use attr_accessor to describe so that you can refer to and change the contents of the instance variable.

``user_address.rb`


class UserAddress
  include ActiveModel::Model
  attr_accessor :name, :name_kana, :nickname, :postal_code, :prefecture_id, :city, :house_number, :building_name
end

Next, we will put together the validation of the user model and address model in the form object (user_address.rb). The reason for doing this is to improve manageability and readability.

``models/user.rb`



class User < ApplicationRecord
  has_one :address
# ----------from here-------------
  with_options presence: true do
    validates :name,      format: {with:/\A[Ah-Hmm-One-龥]/,  message: "is invalid. Please input full-width characters."}
    validates :name_kana, format: {with:/\A[A-Car-]+\z/,    message: "is invalid. Please input full-width katakana characters."}
    validates :nickname,  format: {with:/\A[a-z0-9]+\z/i,     message: "is invalid. Input half-width characters."}
  end
# -------Cut up to here----------
end

``forms/user_address.rb`



class UserAddress
  include ActiveModel::Model
  attr_accessor :name, :name_kana, :nickname, :postal_code, :prefecture_id, :city, :house_number, :building_name
  
 # -----Paste the cut part from here-----
  with_options presence: true do
    validates :name,      format: {with:/\A[Ah-Hmm-One-龥]/,  message: "is invalid. Please input full-width characters."}
    validates :name_kana, format: {with:/\A[A-Car-]+\z/,    message: "is invalid. Please input full-width katakana characters."}
    validates :nickname,  format: {with:/\A[a-z0-9]+\z/i,     message: "is invalid. Input half-width characters."}
  end
# -----Paste the cut part up to here-----  
end

And I will add the validation of the address model. This time's notes ・ The postal code should be a specification that allows you to register only numbers, 3 digits, hyphens, and 4 digits in that order. -Prefectures use active_hash so that they cannot be registered unless the prefecture is selected.

I will not touch on active_hash so much, so please refer to other articles.

``forms/user_address.rb`


class UserAddress
  include ActiveModel::Model
  attr_accessor :name, :name_kana, :nickname, :postal_code, :prefecture_id, :city, :house_number, :building_name
  
  with_options presence: true do
    validates :name,      format:   {with:/\A[Ah-Hmm-One-龥]/,  message: "is invalid. Please input full-width characters."}
    validates :name_kana, format:   {with:/\A[A-Car-]+\z/,    message: "is invalid. Please input full-width katakana characters."}
    validates :nickname,  format:   {with:/\A[a-z0-9]+\z/i,     message: "is invalid. Please input half-width characters."}
    # -----From here, validation of the address model-----
    validates :postal_code, format: {with: /\A[0-9]+\z/, message: "is invalid. Please input half-width characters."}
    validates :prefecture_id, numericality: { other_than: 0, message: "can't be blank" }
    # -----Up to this point, validation of the address model-----
  end
end

Now that the integration of user model and address model validation is complete, it's time to edit how the controller works.

The first thing you want to do with the controller is to register the value. Therefore, we need to create a method that allows values to be registered in the form object via two models. Let's save this time in an easy-to-understand manner.

`` forms/user_address.rb`



class UserAddress
  include ActiveModel::Model
  attr_accessor :name, :name_kana, :nickname, :postal_code, :prefecture_id, :city, :house_number, :building_name

   # -----Validation description omitted-----

  def save
    #Here is a description that saves the value via two models
  end
end

The models I want to go through this time are User.rb and Address.rb, so they are as follows.

``forms/user_address.rb`


 
class UserAddress.rb
  include ActiveModel::Model
  attr_accessor :name, :name_kana, :nickname, :postal_code, :prefecture_id, :city, :house_number, :building_name

   # -----Validation description omitted-----

  def save
     user = User.create(name: name, name_kana: name_kana, nickname: nickname)
     Address.create(postal_code: postal_code, prefecture_id: prefecture_id, city: city, house_number: house_number, building_name: building_name, user_id: user.id)
  end
end

As a result, the description of the controller is also changed to the description via the form object.

``formats_controller.rb`


class FormatsController < ApplicationController
  def index
    @formats = User.includes(:address) #Change here
  end

  def new
    # @format = User.new Delete here
    @format = UserAddress.new    #Add here
  end

  def create
    # @format = User.create(format_params)Delete here
    @format = UserAddress.new(format_params) #Add here
    @format.save  # user_addres.Use the method defined in rb here
    redirect_to root_path
  end

  private
  def format_params 
    #Add the value required for address registration in permit
    params.require(:user_address).permit(:name, :name_kana, :nickname, :postal_code, :prefecture_id, :city, :house_number, :building_name)
  end
end

Edit the view file to create an address item registration field.

Added active_hash to Gemfile

gem 'active_hash'

Create the following file and describe it

``models/prefecture.rb`


class Prefecture < ActiveHash::Base

  self.data = [
               {id: 0, name: '--'}, {id: 1, name: 'Hokkaido'}, {id: 2, name: 'Aomori Prefecture'}, 
               {id: 3, name: 'Iwate Prefecture'}, {id: 4, name: 'Miyagi Prefecture'}, {id: 5, name: 'Akita'}, 
               {id: 6, name: 'Yamagata Prefecture'}, {id: 7, name: 'Fukushima Prefecture'}, {id: 8, name: 'Ibaraki Prefecture'}, 
               {id: 9, name: 'Tochigi Prefecture'}, {id: 10, name: 'Gunma Prefecture'}, {id: 11, name: 'Saitama'}, 
               {id: 12, name: 'Chiba'}, {id: 13, name: 'Tokyo'}, {id: 14, name: 'Kanagawa Prefecture'}, 
               {id: 15, name: 'Niigata Prefecture'}, {id: 16, name: 'Toyama Prefecture'}, {id: 17, name: 'Ishikawa Prefecture'}, 
               {id: 18, name: 'Fukui prefecture'}, {id: 19, name: 'Yamanashi Prefecture'}, {id: 20, name: 'Nagano Prefecture'}, 
               {id: 21, name: 'Gifu Prefecture'}, {id: 22, name: 'Shizuoka Prefecture'}, {id: 23, name: 'Aichi prefecture'}, 
               {id: 24, name: 'Mie Prefecture'}, {id: 25, name: 'Shiga Prefecture'}, {id: 26, name: 'Kyoto'}, 
               {id: 27, name: 'Osaka'}, {id: 28, name: 'Hyogo prefecture'}, {id: 29, name: 'Nara Prefecture'}, 
               {id: 30, name: 'Wakayama Prefecture'}, {id: 31, name: 'Tottori prefecture'}, {id: 32, name: 'Shimane Prefecture'}, 
               {id: 33, name: 'Okayama Prefecture'}, {id: 34, name: 'Hiroshima Prefecture'}, {id: 35, name: 'Yamaguchi Prefecture'}, 
               {id: 36, name: 'Tokushima Prefecture'}, {id: 37, name: 'Kagawa Prefecture'}, {id: 38, name: 'Ehime Prefecture'}, 
               {id: 39, name: 'Kochi Prefecture'}, {id: 40, name: 'Fukuoka Prefecture'}, {id: 41, name: 'Saga Prefecture'}, 
               {id: 42, name: 'Nagasaki Prefecture'}, {id: 43, name: 'Kumamoto Prefecture'}, {id: 44, name: 'Oita Prefecture'}, 
               {id: 45, name: 'Miyazaki prefecture'}, {id: 46, name: 'Kagoshima prefecture'}, {id: 47, name: 'Okinawa Prefecture'}
              ]
end

Add association to model

``models/address.rb`


class Address < ApplicationRecord
  belongs_to :user
  extend ActiveHash::Associations::ActiveRecordExtensions
    belongs_to_active_hash :prefecture
end

Edit the view as below

`` views/formats/new.html.erb`


<%= form_with(model: @format, url: formats_path, local: true) do |form| %>
  <h1>Enter user name</h1>
  <div class="field">
    <%= form.label :name, "Name (full-width)" %>
    <%= form.text_field :name %>
  </div>
  <div class="field">
    <%= form.label :name_kana, "Frikana (double-byte katakana)" %>
    <%= form.text_field :name_kana %>
  </div>
  <div class="field">
    <%= form.label :nickname, "Nickname (half-width alphanumeric characters)" %>
    <%= form.text_field :nickname %>
  </div>
  <%#Add from here%>
  <h1>Enter your address</h1>
  <div class="field">
    <%= form.label :postal_code, "Zip code (including hyphens)" %>
    <%= form.text_field :postal_code %>
  </div>
  <div class="field">
     <%= form.label :prefecture_id, "Prefectures" %>
     <%= form.collection_select :prefecture_id, Prefecture.all, :id, :name, {} %>
  </div>
  <div class="field">
    <%= form.label :city, "Municipalities (optional)" %>
    <%= form.text_field :city %>
  </div>
  <div class="field">
    <%= form.label :house_number, "Address (optional)" %>
    <%= form.text_field :house_number %>
  </div>
  <div class="field">
    <%= form.label :building_name, "Building name (optional)" %>
    <%= form.text_field :building_name %>
  </div>
  <%#Added up to here%>
  <div class="actions">
    <%= form.submit "to register" %>
  </div>
<% end %>

Let's display the registered value.

``views/formats/index.html.erb`


<div class="wrapper">
  <div class="btn">
    <%= link_to 'to register', new_format_path %>
  </div>
  <div class="formats">
    <% @formats.each do |format| %>
      <div class="format">
        <div class="format-name">User information:<%= format.name %>/<%= format.name_kana %>/<%= format.nickname %></div>
        <div class="format-name">Address information:<%= format.address.postal_code%>/<%= format.address.prefecture.name%>/<%= format.address.city%>/<%= format.address.house_number%>/<%= format.address.building_name%></div>
      </div>
    <% end %>
  </div>
</div>

This is completed!

Recommended Posts

Form object ~ An epoch-making guy who interacts with multiple models in one form ~
A story about saving an image with carrierwave in a nested form using a form object.