iT邦幫忙

2021 iThome 鐵人賽

DAY 25
0
Modern Web

初階 Rails 工程師的養成系列 第 25

Day25. Form 裡面還有 Form 怎麼辦?- 表單 part3

Day23 的彈跳視窗元件,送出表單按鈕在form標籤的外面,理當來說form 外面的送出表單按鈕和裡面無關,今天我們會講如何處理這種狀況。

<div class="modal-content">
  <div class="modal-header">
    <strong>新增文章</strong
    ><button
      name="button"
      type="button"
      class="close"
      data-dismiss="modal"
      aria-label="Close"
    >
      <span aria-hidden="true">×</span>
    </button>
  </div>
  <div class="modal-body">
    <form
      class="simple_form new_blog"
      id="new_modal"
      novalidate="novalidate"
      action="/admin/blogs"
      accept-charset="UTF-8"
      method="post"
    >
      <!-- 不重要內容省略 -->
    </form>
  </div>
  <div class="modal-footer">
     <button
      name="button"
      type="submit"
      class="btn btn-primary"
      form="new_modal"
      data-confirm="是否確定要送出編輯?
            請注意!送出後無法復原!"
      data-disable-with="載入中...">
      送出文章
    </button>
  </div>
</div>

w3c 提到可以用以下的用法,將按鈕移到DOM外面執行,只要外面指定的form 的值等於表單id的值。

<form action="/action_page.php" method="get" id="form1">
  <label for="fname">First name:</label>
  <input type="text" id="fname" name="fname" /><br /><br />
  <label for="lname">Last name:</label>
  <input type="text" id="lname" name="lname" />
</form>

<button type="submit" form="form1" value="Submit">Submit</button>

w3c輕描淡寫提到的這種解決方式,卻解決相當多的問題

  • Form 裡面還有 Form
  • Modal 送出按鈕在<form> 外面
  • 匯出表單 & Datatable 共用輸入框

匯出表單 & Datatable 共用輸入

https://ithelp.ithome.com.tw/upload/images/20210921/20115854dS4o4E91vE.png

上述的樣式,利用表單form屬性的用法,先指定form特定的id="export-orders",接著將上述的搜尋框、清除重填按鈕、下拉式選單、時間選單利用form="export-orders" 的行為指向id="export-orders"的表單。

<form
  id="export-orders"
  action="/admin/orders/download_orders"
  accept-charset="UTF-8"
  method="post">
  <input
    type="hidden"
    name="authenticity_token"
    value="..."
  />
</form>

<div
  class="card shadow mb-4"
  data-controller="orders"
  data-orders-current-url-value="/admin/orders"
>
  <div class="card-header py-3">
    <h6 class="h6 m-0 font-weight-bold text-primary d-flex align-items-center">
      <span>漢漢老師的訂單列表</span
      ><input
        type="submit"
        name="commit"
        value="訂單匯出"
        class="btn btn-sm btn-primary float-right ml-auto"
        id="export-orders"
        data-confirm="確定要匯出嗎?"
        form="export-orders"
      />
    </h6>
  </div>
  <div class="card-body">
    <div class="d-flex ml-1 mb-1 flex-nowrap">
      <div class="input-group mb-2" style="max-width: 350px">
        <input
          type="search"
          name="太長省略..."
          id="太長省略..."
          class="form-control"
          data-orders-target="keyword"
          form="export-orders"
          placeholder="搜尋訂單編號/購買⼈姓名或⼿機"
        />
        <div class="input-group-append">
          <i
            class="fas fa-search input-group-text"
            style="padding-top: 10px"
          ></i>
        </div>
      </div>
      <button
        name="button"
        type="reset"
        form="export-orders"
        class="btn btn-secondary btn-sm ml-2 mb-2"
        data-action="click->orders#reset"
        data-orders-target="resetBtn"
      >
        清除重填
      </button>
    </div>
    <div class="d-flex flex-nowrap">
      <select
        name="ransack_search[status_eq]"
        id="status-select"
        class="form-control input-sm mx-1"
        style="max-width: 250px"
        form="export-orders"
        data-orders-target="status"
        data-action="orders#"
      >
        <option value="">所有訂單狀態</option>
        <option value="unpaid">未付款</option>
        <option value="processing">處理中</option>
        <option value="waiting">已出貨</option>
        <option value="done">已完成</option>
        <option value="canceled">已取消</option>
        <option value="returned">退貨/退款</option>
      </select>

      <!-- 省略... -->

      <div class="input-group mb-3">
        <input
          type="date"
          name="ransack_search[created_at_gteq]"
          id="ransack_search_created_at_gteq"
          value="2020-09-21"
          class="form-control"
          style="height: 100%; max-width: 220px"
          data-orders-target="startedAt"
          form="export-orders"
        />
        <div class="input-group-append" style="height: 100%">
          <label class="input-group-text">至</label>
        </div>
        <input
          type="date"
          name="ransack_search[created_at_lt]"
          id="ransack_search_created_at_lt"
          value="2021-09-22"
          class="form-control"
          style="height: 100%; max-width: 220px"
          data-orders-target="endedAt"
          form="export-orders"
        />
      </div>
    </div>
    
    <!-- 省略... -->
    
  </div>
</div>

使用w3c 提到的用法,我們可以將<form>拉出外面後,因此我們不用煩惱多了<form>以後的樣式問題,寫法可以更自由

<form
  id="export-orders"
  action="/admin/orders/download_orders"
  accept-charset="UTF-8"
  method="post">
  <input
    type="hidden"
    name="authenticity_token"
    value="..." />
</form>

Modal 送出按鈕在<form> 外面

Day22 提到的彈跳視窗按鈕在form標籤外面的狀況要如何處理?首先以下為自定義的彈跳視窗元件

module ApplicationHelper
    def modal(id: nil, confirm_wording: '確認', confirm_form:, 
              confirm_target: nil, title: nil, controller: nil, 
              close_btn: '取消')
    content_tag :div, id: id, class: 'modal fade',
                tabindex: -1, role: 'dialog', aria: { hidden: true },
                data: { "#{controller}-target": 'modal' } do
      content_tag :div, role: 'document', class: 'modal-dialog' do
        content_tag :div, class: 'modal-content' do
          # Header
          content_tag(:div, class: 'modal-header') do
            content_tag(:strong, title) +
            button_tag(type: 'button', class: 'close', 
                       data: { dismiss: 'modal' }, aria: { label: 'Close' }) do
              content_tag :span, '×', aria: { hidden: true }
            end
          end +
            # Body: 自定義內容交給 yield
            content_tag(:div, class: 'modal-body') { yield if block_given? } +
            # Footer
            content_tag(:div, class: 'modal-footer') do
              button_tag(close_btn, type: 'button', class: 'btn btn-warning',
                               data: { dismiss: 'modal' }, aria: { label: 'Close' }) +
              (confirm_wording && button_tag(confirm_wording, 
                type: 'submit', class: 'btn btn-primary',
                "data-#{controller}-target": confirm_target, 
                 form: confirm_form, 
                 data: { confirm: "是否確定要送出編輯?\n請注意!送出後無法復原!", 
                         disable_with: "載入中..." }))
            end
        end
      end
    end
  end
end

彈跳視窗元件搭配自定義內容的結構

/ 彈跳視窗
= 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, item_wrapper_class: 'form-check form-check-inline'

上述例子的結構特徵為送出表單的按鈕在<form> 標籤之外,因此也可以使用w3c所提到的用法,將外面的送出表單按鈕指定到裡面的<form>標籤。此例與上一例用法理由相同,但因為知道了這種用法,所以聯想到還可以像這樣使用在自定義的彈跳視窗元件中。

Form 裡面還有 Form

首先,我們要先知道按鈕一共有三個屬性reset, button, submit,而表單送出為type="submit"

在某個情境是,<simple_form_for>(form1) 標籤裡面再寫一個<simple_form_for>(form2)<simple_form_for>(form2) 裡面和外面分別有送出按鈕,則這兩個送出按鈕送出的都會是<simple_form_for>(form1) 的表單,而不會是<simple_form_for>(form2) 的。

<form id="form1">
  <!-- form2 -->
  <input />
  
  <form id="form2">
    <!-- form2 -->
    <input />
    <button type="submit">
  </form2>
    
  <button type="submit">
</form1>

舉一個實際例子,首先我們先把ajax routes寫出來

resources :blogs do
  #====== ajax
  post :search, on: :collection
end

並且將 controller , view 寫出來

module Admin
  class BlogsController < ApplicationController
    def search
      blog = Blog.find_by_id params[:id]
      
      render partial: 'searched_blog', locals: { blog: blog }
    end
  end
end

app/views/admin/blogs/_searched_blog

br
= log_template do
  = log_item title: 'id' do
    = blog&.id || ''
  = log_item title: '標題' do
    = blog&.title || '找不到標題'
  = log_item title: '內文' do
    = blog&.content || '找不到內文'

接著我們將Form 包著 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('內文')
    = tag.div style: "border: 1px solid black"
      = simple_form_for(@blog, url: search_admin_blogs_path, method: :post,
                        html: { data: { remote: true } } do |g|
        = g.input :id, label: tag.strong('搜尋id')
        = g.submit '搜尋', class: 'btn btn-primary btn-sm'
    = 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/20210921/20115854Ll7wndXp3r.png

我們將裡面的form (<simple_form_for>(form2)) 用黑色框包覆住。右鍵檢查查看黑色框的原始碼,可以看到<simple_form_for>(form2)<form> 標籤已經在被渲染階段被消除,因此造成兩個搜尋, 送出文章 按下去後,送出的表單都是<simple_form_for>(form1)表單。

<div style="border: 1px solid black">
  <input
    type="hidden"
    name="authenticity_token"
    value="eIBlHsbmL/tmFSMMYCtF3u1VSwo9XZDCCR4Vg2lW0WF3OzbbFmPIe8yBBmiZBgGcQALaEY55fnBIn7PVAV/+CA=="
  />
  <div class="form-group string required admin_blogs_search_id">
    <label class="string required" for="_admin_blogs_search_id"
      ><strong>搜尋id</strong> <abbr title="required">*</abbr></label
    ><input
      class="form-control string required"
      type="text"
      name="/admin/blogs/search[id]"
      id="_admin_blogs_search_id"
    />
  </div>
  <input
    type="submit"
    name="commit"
    value="搜尋"
    class="btn btn-primary btn-sm"
    data-disable-with="搜尋"
  />
</div>

接著我們再加入<form_tag>(form3),並且使用Ajax

/ 彈跳視窗
= 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|
    = tag.div '<simple_form_for>(form1)'
    = f.input :title, label: tag.strong('標題')
    = f.input :content, label: tag.strong('內文')
    = tag.div style: "border: 1px solid black"
      = simple_form_for(@blog, url: search_admin_blogs_path, method: :post,
                        html: { data: { remote: true } } do |g|
        = tag.div '<simple_form_for>(form2)'
        = g.input :id, label: tag.strong('搜尋id')
        = g.submit '搜尋', class: 'btn btn-primary btn-sm'
    = tag.div style: "border: 1px solid blue; padding: 6px;" do
      - form_tag({ controller: "admin/blogs", action: "search" }, method: "post",
                data: { remote: true,
                        action: "ajax:success->admin--blogs#onBlogSuccess 
                                 ajax:error->admin--blogs#onBlogError" })
        = tag.div '<form_tag>(form3)'
        = tag.div(class: 'form-group string required admin_blogs_search_id"')
          = tag.label tag.strong('搜尋id')
          = tag.input name: 'id', class: "form-control string required"
        input(type="submit" name="commit" value="搜尋" 
              class="btn btn-primary btn-sm" data-disable-with="搜尋")
        = tag.div data: { 'admin--blogs-target': 'searchedContent' }
    = 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/20210921/20115854641eu20E9h.png

黑色框框為使用simple_form_for實作,而下面的表單為form_tag 實作。

這裡的ajax 是使用stimulus實現,我們會在明天講到stimulus

import { Controller } from 'stimulus';

export default class extends Controller {
  static targets = ["searchedContent"]

  onBlogSuccess(event) {
    let [data, status, xhr] = event.detail;
    this.searchedContentTarget.innerHTML = xhr.response;
  }

  onBlogError(event) {
    let [data, status, xhr] = event.detail;
    console.log(xhr.response);
  }
}

目前用黑色框包覆住form (<simple_form_for>(form2)) 還是壞的,因此我們將其往上搬,並且為了不失焦,先把<form_tag>(form3)刪除。

我們將simple_form_for(@blog, url: search_admin_blogs_path, method: :post拉至上方,並且賦予id=form2,並將用黑色框包覆的輸入框&送出按鈕給予form=form2,即可以打Ajax

  = modal(id: 'new-blog-modal', confirm_wording: '送出文章',
          confirm_form: 'new_modal', title: '新增文章') do
    = simple_form_for(@blog, url: search_admin_blogs_path, method: :post,
            html: { data: { remote: true,
                    action: "ajax:success->admin--blogs#onBlogSuccess 
                             ajax:error->admin--blogs#onBlogError" },
                    id: 'form2' }) do
    = simple_form_for [:admin, @blog], html: { method: :post, id: :new_modal } do |f|
      = tag.div '<simple_form_for>(form1)'
      = f.input :title, label: tag.strong('標題')
      = f.input :content, label: tag.strong('內文')
      = tag.div style: "border: 1px solid black"
        = tag.div '<simple_form_for>(form2)'
        = tag.div(class: 'form-group string required admin_blogs_search_id"')
          = tag.label tag.strong('搜尋id')
          = tag.input name: 'id', class: "form-control string required", form: 'form2'
        = submit_tag "搜尋", class: "btn btn-primary btn-sm", form: 'form2',
                            data: { disable_with: '載入中...' }
        = tag.div data: { 'admin--blogs-target': 'searchedContent' }
      = f.input :genre, as: :radio_buttons, label: tag.strong('分類'),
                collection: [["生活", :life], ["休閒", :casual], ["科技", :technology]],
                selected: :casual, item_wrapper_class: 'form-check form-check-inline'

w3c 所介紹的屬性,可以讓我們解決simple_form_for/form_for包覆simple_form_for/form_for的問題!

最後我們黑色框框裡頭的元素都黏合黑色框框,而我們如何優化以上樣式?只要加入內距即可。

我們對黑色框框的樣式進行改寫

= tag.div style: "border: 1px solid black; padding: 12px;"

https://ithelp.ithome.com.tw/upload/images/20210921/20115854KmNJVgAiGg.png

這樣一來,紅框框(抽象的框框)與黑框框的距離為12px,因此黑色框框裡頭的元素就不會黏合黑色框框了喔。

結論

由於在網路上並沒有特別的文章介紹form 屬性的好處,因此今天特別立了今天這篇文章,將好用的屬性搭配實例介紹給讀者。

參考資料


上一篇
Day24. form_tag 與 simple_form_for 的用法 - 表單 part2
下一篇
Day26. 認識 Stimulus,與Javascript成為好朋友
系列文
初階 Rails 工程師的養成34

尚未有邦友留言

立即登入留言