railsで管理者画面のあれやこれ

メモと整理がてら記載します。

docker環境で実施していることを前提としています。

docker-compose exec web rails g controller admin/products index show new edit --no-helper
model

下記コマンドでモデルの作成を行う

docker-compose exec web rails g model Product name description:text price:integer

作成したファイルの中身はこんな感じ

  create_table "products", force: :cascade do |t|
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "name"
    t.text "description"
    t.integer "price"
  end

下記コマンドを実行して上記で確認したテーブルを作成する

rails db:migrate
routes

上記実施後config/routes.rbにて下記を確認します。
※今回は下記画面のみの作成になります。

  • 商品一覧
  • 商品作成
  • 商品編集
  • 商品削除(一覧画面から削除できるように)
Rails.application.routes.draw do
  namespace :admin do
    # []内のアクション以外を使うという意味を表す
    resources :products, except: [:show]
  end

namespace :admin do ~~ endまで囲まれている部分はadmin/が付与されます
下記のようGETした際はadmin/products#xxxxがルートして呼び出されるようになります。

docker-compose exec web rails routes | grep products

admin_products         GET         /admin/products(.:format)              admin/products#index
                                      POST      /admin/products(.:format)              admin/products#create
new_admin_product  GET         /admin/products/new(.:format)      admin/products#new
edit_admin_product  GET         /admin/products/:id/edit(.:format) admin/products#edit
admin_product           PATCH    /admin/products/:id(.:format)         admin/products#update
                                     PUT         /admin/products/:id(.:format)         admin/products#update
admin_product           DELETE  /admin/products/:id(.:format)         admin/products#destroy

なぜnewとcreateの2つも必要なのか?と思ったが、 理由はnewアクションでフォームを、そのフォームをcreateでデータの保存等を行うとのことらしい。
他にもeditも同様に、editでフォームを、updateでデータの更新等を行う。

controller

adimin/products_controller.rb

# frozen_string_literal: true

module Admin
  class ProductsController < ApplicationController
    before_action :basic_auth
    before_action :set_product, only: %i[update edit destroy]

    def index
      @products = Product.all
    end

    def new
      @product = Product.new
    end

    def update
      if @product.update(product_params)
        redirect_to ({ action: :index }), notice: 'Product was successfully updated.'
      else
        render :edit, status: :unprocessable_entity
      end
    end

    def create
      @product = Product.new(product_params)
      if @product.save
        redirect_to ({ action: :index }), notice: 'Product was successfully create.'
      else
        render(:new)
      end
    end

    def edit; end

    def destroy
      @product.destroy
      redirect_to ({ action: :index }), notice: 'Product was successfully destroyed.'
    end

    private

    def set_product
      @product = Product.find(params[:id])
    end

    def product_params
      params.require(:product).permit(:name, :description, :price, :image)
    end

    def basic_auth
      authenticate_or_request_with_http_basic do |username, password|
        username == 'admin' && password == 'pw'
      end
    end
  end
end

XXXX_pathは各画面に遷移することを示す routesファイルに記載のあるやつで、xxx_pathとつければその画面に飛ぶ

before_action :set_product, only: %i[update edit destroy]を使って何度もコードを記載するのを防いでます。(DRY) after_actionもあるので良ければ調べてみよう。

編集画面だけを紹介

<section class="py-5">
  <%= form_with(model: @product, url: admin_product_path(@product), local: true) do |f| %>
    <div class="form-floating">
      <%= f.text_area :name, class: 'form-control', id: 'floatingName', placeholder: 'Enter product name' %>
      <%= f.label :name, 'Name', for: 'floatingName' %>
    </div>
    <div class="form-floating">
      <%= f.text_area :description, class: 'form-control', id: 'floatingDescription', placeholder: 'Enter product name' %>
      <%= f.label :description, 'Description', for: 'floatingDescription' %>
    </div>
    <div class="form-floating">
      <%= f.number_field :price, class: 'form-control', id: 'floatingPrice', placeholder: 'Enter product price' %>
      <%= f.label :price, 'Price', for: 'floatingPrice' %>
    </div>
    <div class="form-floating">
      <%= f.file_field :image, class: 'form-control', id: 'floatingImage' %>
      <%= f.label :image, 'Image', for: 'floatingImage' %>
    </div>
    <%= f.submit "更新", class: 'btn btn-primary' %>
  <% end %>
</section>

<%= form_with(model: @product, url: admin_product_path(@product), local: true) do |f| %>
model:にはフォームで作成または編集される具体的なインスタンスを指定(コントローラのアクション内で設定)
編集画面では、通常updateアクションが呼び出される(更新ボタン押下時)ので、url:にはそのパス(admin_product_path(@product))を指定します。

流れ的にはこんな感じ
form_withで指定されたurl: のパス(このケースでは admin_product_path(@product))に対してHTTP POST(新規作成)またはPATCH(更新)リクエストを送信

Railsのルーティングシステムは、そのリクエストを適切なコントローラとアクション(この場合は ProductsControllerのupdateアクション)にルーティング

updateアクション内で @product.update(product_params) が呼び出される
これにより、フォームから送信されたデータを使用して @product オブジェクトが更新されます。

updateメソッドが成功すれば(つまり、バリデーションにパスすれば)、 redirect_to(admin_products_path(@product)) が呼び出され、ユーザは @product のビューにリダイレクトされます。
updateメソッドが失敗した場合(つまり、バリデーションに失敗した場合)、 render(:edit) が呼び出され、ユーザは同じ edit ビューに留まり、エラーメッセージが表示されます。

qiita.com

zenn.dev

以上、メモがてらのあれやこれでした。