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
屬性的用法,先指定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>
<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>
標籤。此例與上一例用法理由相同,但因為知道了這種用法,所以聯想到還可以像這樣使用在自定義的彈跳視窗元件中。
首先,我們要先知道按鈕一共有三個屬性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'
我們將裡面的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'
黑色框框為使用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;"
這樣一來,紅框框(抽象的框框)與黑框框的距離為12px,因此黑色框框裡頭的元素就不會黏合黑色框框了喔。
由於在網路上並沒有特別的文章介紹form
屬性的好處,因此今天特別立了今天這篇文章,將好用的屬性搭配實例介紹給讀者。