Day23. 在讲表单之前,先来谈谈routes和mvc - 表单 part1

在讲解基本的表单架构以前,我们先将基本的CRUD建立起来。

以下的前情提要会提到有关mvc & routes的建设,还不熟悉 Rails 框架的读者们,可以趁者以下举的例子熟悉基本的架构

https://ithelp.ithome.com.tw/upload/images/20210926/20115854rm4WbyLVPx.png

model

首先我们先建立部落格的model。部落格的栏位 ➡️ 标题、内文、种类、创建者

rails g model Blog title content user:belongs_to genre:integer

确认无误migration以後,我们进行资料库migrate

class CreateBlogs < ActiveRecord::Migration[6.0]
  def change
    create_table :blogs do |t|
      t.string :title
      t.string :content
      t.references :user, null: false
      t.integer :genre
      t.timestamps
    end
  end
end
rails db:migrate

资料迁徙完後,开始做以下设定

  • 针对userforeign key做设定。
    • User has_many blogs
    • Each blog belongs_to user
  • 种类 genre 型别为integer,并使用Rails内建的enum
  • 部落格Blog的3个栏位:title, :content :genre 不能为空值
# 备注: 由於这边不会带到会员,所以假设会员都已经设定完成的前提下进行串接
class User < ApplicationRecord
  has_many :blogs
end

class Blog < ApplicationRecord
  belongs_to :user

  validates :title, :content, :genre, presence: true 
  
  enum genre: {
    life: 0,
    casual: 1,
    technology: 2,
  }
end

建立完资料库以後,就可以实作资料的新增、删除、改动的动作。因此创建完以後我会先在Rails Console 动手实作一遍

rails console

#===== 确定存在的栏位为空值的时候不能存取
User.first.blogs.create!
# ActiveRecord::RecordInvalid (校验失败: Title 不能为空白, Content 不能为空白, Genre 不能为空白)

顺道一提,如果没有加惊叹号(意思为 bang!, 便不会引发错误,但这边我们可以看到id: nil,代表实际上这笔资料没有被存进去。

User.first.blogs.create
#=> #<Blog id: nil, title: nil, content: nil, user_id: 1, genre: nil, created_at: nil, updated_at: nil>

我们也可以使用model的方法判断这笔资料在哪一个生命周期。这里我们要使用 new_record?, persisted? 判断这一笔是否已经被存进资料库。

User.first.blogs.create.new_record?   #=> true
User.first.blogs.create.persisted?    #=> false

以下为 ActiveRecord 的生命周期

new record ➡️ 尚未写入阶段会判断为true

blog = Item.new
blog.new_record? #=> true

persisted ➡️ 已写入阶段会判断为true

blog.save
blog.persisted? #=> true

changed ➡️ 资料被改写但尚未存进资料库会被判断true

blog.name = "other"
blog.changed? #=> true

destroyed ➡️ 资料被删除但该笔纪录,但还没重整。因此该笔资料暂时存在在model。遇到这种情况则会判断为true

blog.destroy
blog.destroyed? #=> true

接着我们试着存入一笔

User.first.blogs.create!(title: 'Title 1', content: 'Content 1', genre: :life)
#=> #<Blog id: 1, title: "Title 1", content: "Content 1", user_id: 1, genre: "life", created_at: "2021-09-18 12:11:56", updated_at: "2021-09-18 12:11:56">
  User Load (1.0ms)  SELECT `users`.* FROM `users` WHERE `users`.`banned` = FALSE ORDER BY `users`.`id` ASC LIMIT 1

  Blog Create (15.3ms)  INSERT INTO `blogs` (`title`, `content`, `user_id`, `genre`, `created_at`, `updated_at`) VALUES ('Title 1', 'Content 1', 1, 0, '2021-09-18 12:11:56.986857', '2021-09-18 12:11:56.986857')

确认 model 运作可以的话,我们便可以继续下一步的操作。我们会在Day29-30讲更多关於ModelModel的部分就先打住

routes

接着,我们先制造可以读取到的路径

Rails.application.routes.draw do  
  namespace :admin do
    # 基本路径
    resources :blogs do
      post :spectial_update, on: :collection
    end
  end
end

使用了resources,Rails会创造8种路径。

$ rails routes -g blog

         Prefix Verb   URI Pattern                     Controller#Action
spectial_update_admin_blogs POST   /admin/blogs/spectial_update(.:format) admin/blogs#spectial_update
                admin_blogs GET    /admin/blogs(.:format)                 admin/blogs#index
                            POST   /admin/blogs(.:format)                 admin/blogs#create
             new_admin_blog GET    /admin/blogs/new(.:format)             admin/blogs#new
            edit_admin_blog GET    /admin/blogs/:id/edit(.:format)        admin/blogs#edit
                 admin_blog GET    /admin/blogs/:id(.:format)             admin/blogs#show
                            PATCH  /admin/blogs/:id(.:format)             admin/blogs#update
                            PUT    /admin/blogs/:id(.:format)             admin/blogs#update
                            DELETE /admin/blogs/:id(.:format)             admin/blogs#destroy

controller

接着我们将controller所有的动作内容定义出来(以下内容会慢慢带到)

# in app/controllers/admin/blogs_controller.rb
#
module Admin
  class BlogsController < ApplicationController
    before_action :set_blog, only: %i(show edit update)
    SPECIAL_ARTICLE = 1..3
    
    def index
      # 单笔资料新增一笔待储存的纪录
      @blog = current_user.blogs.new
    end

    # 创建一笔新的纪录
    def create
    end

    # 创建或修改前10笔
    def special_update
    end

    def show; end
  end
end

view

我们先建立部落格相关的列表页 index

module BlogHelper
  def blog_genre
    [
      { id: 'index', wording: '列表页' },
      { id: 'special', wording: '特殊新增' },
    ]
  end
end

我们使用 Day22 介绍的自定义 helper 组成我们要的画面

// in app/views/admin/blogs/index.html.slim

= title '部落格列表'

/ 卡片内容
= card do
  = card_header(title: '部落格列表') do
    = link_to '新增', '#', class: 'ml-auto btn btn-primary',
              data: { toggle: 'modal', target: '#new-blog-modal' }
  = card_body do
    = tab_list(blog_genre)
    = tab_contents do
      / 当前显示内容
      = tab_active_content(blog_genre.first[:id]) do
        / 页签1内容
        = tag.span "列表页"
      / 隐藏内容
      = tab_content(blog_genre.second[:id]) do
        / 页签2内容
        = tag.span "特殊新增"

/ 弹跳视窗        
= modal(id: 'new-blog-modal', confirm_wording: '送出文章',
        confirm_form: nil, title: '新增文章') do
  = tag.span "我是弹跳视窗内容"

画面上有几个看点

  • 新增按钮使用的是ml-auto,若没有margin-left: auto则会是以下的样子。在 Day19, Day20 提过margin相关技巧。

    https://ithelp.ithome.com.tw/upload/images/20210918/20115854vFgG3jYc5i.png

  • 页签的切换与弹跳视窗的自定义helper,在 Day22 介绍过。

将基本的环境设置完後,我们便开始使用simple_form写新增弹跳视窗的表单

/ 弹跳视窗
= modal(id: 'new-blog-modal', confirm_wording: '送出文章',
        confirm_form: 'new_modal', title: '新增文章') do
  = simple_form_for [:admin, @blog], html: { method: :post, id: :new_modal } do |f|
    = f.input :title, label: tag.strong('标题')
    = f.input :content, label: tag.strong('内文')
    = f.input :genre, as: :select, label: tag.strong('分类')

而产生的画面如下 ⬇️

我们可以看到,当我们将标题、内文、分类设为必填之後,simple_form 帮忙新增了米字号*

https://ithelp.ithome.com.tw/upload/images/20210918/20115854GI2Q4zz0XW.png

而不幸的是,simple_form没有办法帮我们判断是否有使用enum

https://ithelp.ithome.com.tw/upload/images/20210918/20115854sv8yic8bAd.png

因此我们要将选项填上去,并且使用:selected预设选择休闲

/ 弹跳视窗
= modal(id: 'new-blog-modal', confirm_wording: '送出文章',
        confirm_form: 'new_modal', title: '新增文章') do
  = simple_form_for [:admin, @blog], html: { method: :post, id: :new_modal } do |f|
    = f.input :title, label: tag.strong('标题')
    = f.input :content, label: tag.strong('内文')
    = f.input :genre, as: :select, label: tag.strong('分类'), 
              collection: [["生活", :life], ["休闲", :casual], ["科技", :technology]], 
              selected: :casual

https://ithelp.ithome.com.tw/upload/images/20210918/20115854QvF7SFvyjt.png

接着我们看看简单的使用simple_form查看对应的html 画面为何

<form
  class="simple_form new_blog"
  id="new_modal"
  novalidate="novalidate"
  action="/admin/blogs"
  accept-charset="UTF-8"
  method="post"
>
  <input
    type="hidden"
    name="authenticity_token"
    value="jJ2jdgkIRXRXE8Ioz7i3kEe3Se/tVTXMJd0qAsB+Sw2DJvCz2Y2i9P2H50w2lfPS6uDY9F5x235kXIxUqHdkZA=="
  />
  <div class="form-group string required blog_title">
    <label class="string required" for="blog_title"
      ><strong>标题</strong> <abbr title="required">*</abbr></label
    ><input
      class="form-control string required"
      type="text"
      name="blog[title]"
      id="blog_title"
    />
  </div>
  <div class="form-group string required blog_content">
    <label class="string required" for="blog_content"
      ><strong>内文</strong> <abbr title="required">*</abbr></label
    ><input
      class="form-control string required"
      type="text"
      name="blog[content]"
      id="blog_content"
    />
  </div>
  <div class="form-group select required blog_genre">
    <label class="select required" for="blog_genre"
      ><strong>分类</strong> <abbr title="required">*</abbr></label
    ><select
      class="form-control select required"
      name="blog[genre]"
      id="blog_genre"
    >
      <option value="life">生活</option>
      <option selected="selected" value="casual">休闲</option>
      <option value="technology">科技</option>
    </select>
  </div>
</form>

除了以下这段和CSRF token有关,其他都为与我们有关的内容。

  <input
    type="hidden"
    name="authenticity_token"
    value="..."
  />

可以看到Rails帮忙处理很多表单的问题,让我们不必自己重刻html

若使用:radio_buttons

= modal(id: 'new-blog-modal', confirm_wording: '送出文章',
        confirm_form: 'new_modal', title: '新增文章') do
  = simple_form_for [:admin, @blog], html: { method: :post, id: :new_modal } do |f|
    = f.input :title, label: tag.strong('标题')
    = f.input :content, label: tag.strong('内文')
    = f.input :genre, as: :radio_buttons, label: tag.strong('分类'),
              collection: [["生活", :life], ["休闲", :casual], ["科技", :technology]],
              selected: :casual

https://ithelp.ithome.com.tw/upload/images/20210918/20115854M2ewDX6pTT.png

当我们加入了item_wrapper_class: 'form-check form-check-inline'排列成inline,会是我们预期的样子。

= simple_form_for [:admin, @blog], html: { method: :post, id: :new_modal } do |f|
  = f.input :title, label: tag.strong('标题')
  = f.input :content, label: tag.strong('内文')
  = f.input :genre, as: :radio_buttons, label: tag.strong('分类'),
            collection: [["生活", :life], ["休闲", :casual], ["科技", :technology]],
            selected: :casual, item_wrapper_class: 'form-check form-check-inline'

https://ithelp.ithome.com.tw/upload/images/20210918/20115854OFo3uw4XNO.png

即便方便,在simple_form上面会遇到一些困难

  • 网路上相关资讯不多,若经验不足的读者也许可以在自己专案内找到其他人的用法。不过我同事基本上不会用simple_form,所以我只能靠自己经验摸索

  • simple_form_for 若要加上html属性,key 要为:html

    里面的f.input若要加上html属性,key 要为:input_html

  • 多留意radio_button, checkbox的用法

结语

今天介绍了基本的CRUD,以及基本的simple_form技巧。明天会开始介绍表单的用法及玩法!

参考资料


<<:  DAY 08 Nesting

>>:  爬虫怎麽爬 从零开始的爬虫自学 DAY9 python字串怎麽用

自动化 End-End 测试 Nightwatch.js 与 BrowserStack

前文介绍了 BrowserStack 本篇写一些在撰写测项的写法与一些要注意的小地方 首先 Brow...

不只懂 Vue 语法:Vue 3 如何使用 Proxy 实现响应式(Reactivity)?

问题回答 Vue 3 会为 data 建立一个 Proxy 物件,并在里面建立 getter 和 s...

[Day3] 安全签章 - XOR加密(HashID)

API流程 I have A Nonce, I have A key, Uh It's time t...

Day10 Android - Toast快显元件

今天讲的内容属於简单的元件使用,而我在前面几天已经先有拿来用几次来观察结果,但我一直没有好好提到,今...

Day 8-单元测试完善 HelloBank、基础总结与核心技术概述 (基础-7)

单元测试基础的示范专案 HelloBank 收尾与现阶段总结 我们在Day 4-Visual Stu...