-
Notifications
You must be signed in to change notification settings - Fork 0
/
form_helpers.html
1093 lines (977 loc) · 80.2 KB
/
form_helpers.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Action View 表單輔助方法 — Ruby on Rails 指南</title>
<meta name="description" content="Ruby on Rails 指南:系統學習 Rails(Rails 4.2 版本)" >
<meta name="keywords" content="Ruby on Rails Guides 指南 中文 學習 免費 網路 Web 開發" >
<meta name="author" content="http://git.io/G_R1sA">
<meta property="fb:admins" content="1340181291">
<meta property="og:title" content="Action View 表單輔助方法 — Ruby on Rails 指南" >
<meta property="og:site_name" content="Ruby on Rails 指南">
<meta property="og:image" content="http://rails.ruby.tw/images/rails_guides_cover.jpg">
<meta property="og:url" content="http://rails.ruby.tw/">
<meta property="og:type" content="article">
<meta property="og:description" content="Ruby on Rails 指南:系統學習 Rails(Rails 4.2 版本)">
<link rel="stylesheet" href="stylesheets/application.css">
<link href="http://fonts.googleapis.com/css?family=Noto+Sans:400,700|Noto+Serif:700|Source+Code+Pro" rel="stylesheet">
<link href="images/favicon.ico" rel="shortcut icon" type="image/x-icon">
</head>
<body class="guide">
<div id="fb-root"></div>
<script>(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/zh-TW/sdk.js#xfbml=1&appId=837401439623727&version=v2.0";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));</script>
<script type="text/javascript">
window.twttr=(function(d,s,id){var t,js,fjs=d.getElementsByTagName(s)[0];if(d.getElementById(id)){return}js=d.createElement(s);js.id=id;js.src="https://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);return window.twttr||(t={_e:[],ready:function(f){t._e.push(f)}})}(document,"script","twitter-wjs"));
</script>
<div id="topNav">
<div class="wrapper">
<strong class="more-info-label">更多內容 <a href="http://rubyonrails.org/">rubyonrails.org:</a></strong>
<span class="red-button more-info-button">
更多內容
</span>
<ul class="more-info-links s-hidden">
<li class="more-info"><a href="http://rubyonrails.org/">綜覽</a></li>
<li class="more-info"><a href="http://rubyonrails.org/download">下載</a></li>
<li class="more-info"><a href="http://rubyonrails.org/deploy">部署</a></li>
<li class="more-info"><a href="https://github.com/rails/rails">原始碼</a></li>
<li class="more-info"><a href="http://rubyonrails.org/screencasts">影片</a></li>
<li class="more-info"><a href="http://rubyonrails.org/documentation">文件</a></li>
<li class="more-info"><a href="http://rubyonrails.org/community">社群</a></li>
<li class="more-info"><a href="http://weblog.rubyonrails.org/">Blog</a></li>
</ul>
</div>
</div>
<div id="header">
<div class="wrapper clearfix">
<h1><a href="index.html" title="回首頁">Guides.rubyonrails.org</a></h1>
<ul class="nav">
<li><a class="nav-item" href="index.html">首頁</a></li>
<li class="guides-index guides-index-large">
<a href="index.html" id="guidesMenu" class="guides-index-item nav-item">指南目錄</a>
<div id="guides" class="clearfix" style="display: none;">
<hr>
<dl class="L">
<dt>起步走</dt>
<dd><a href="getting_started.html">Rails 起步走</a></dd>
<dt>Models</dt>
<dd><a href="active_record_basics.html">Active Record 基礎</a></dd>
<dd><a href="active_record_migrations.html">Active Record 遷移</a></dd>
<dd><a href="active_record_validations.html">Active Record 驗證</a></dd>
<dd><a href="active_record_callbacks.html">Active Record 回呼</a></dd>
<dd><a href="association_basics.html">Active Record 關聯</a></dd>
<dd><a href="active_record_querying.html">Active Record 查詢</a></dd>
<dt>Views</dt>
<dd><a href="layouts_and_rendering.html">Rails 算繪與版型</a></dd>
<dd><a href="form_helpers.html">Action View 表單輔助方法</a></dd>
<dt>Controllers</dt>
<dd><a href="action_controller_overview.html">Action Controller 綜覽</a></dd>
<dd><a href="routing.html">Rails 路由:深入淺出</a></dd>
</dl>
<dl class="R">
<dt>深入了解</dt>
<dd><a href="active_support_core_extensions.html">Active Support 核心擴展</a></dd>
<dd><a href="i18n.html">Rails 國際化 API</a></dd>
<dd><a href="action_mailer_basics.html">Action Mailer 基礎</a></dd>
<dd><a href="active_job_basics.html">Active Job 基礎</a></dd>
<dd><a href="security.html">Rails 安全指南</a></dd>
<dd><a href="debugging_rails_applications.html">除錯 Rails 應用程式</a></dd>
<dd><a href="configuring.html">Rails 應用程式設定</a></dd>
<dd><a href="command_line.html">Rake 任務與 Rails 命令列工具</a></dd>
<dd><a href="asset_pipeline.html">Asset Pipeline</a></dd>
<dd><a href="working_with_javascript_in_rails.html">在 Rails 使用 JavaScript</a></dd>
<dd><a href="constant_autoloading_and_reloading.html">Constant Autoloading and Reloading</a></dd>
<dt>擴充 Rails</dt>
<dd><a href="rails_on_rack.html">Rails on Rack</a></dd>
<dd><a href="generators.html">客製與新建 Rails 產生器</a></dd>
<dd><a href="rails_application_templates.html">Rails 應用程式模版</a></dd>
<dt>貢獻 Ruby on Rails</dt>
<dd><a href="contributing_to_ruby_on_rails.html">貢獻 Ruby on Rails</a></dd>
<dd><a href="api_documentation_guidelines.html">API 文件準則</a></dd>
<dd><a href="ruby_on_rails_guides_guidelines.html">Ruby on Rails 指南準則</a></dd>
<dt>維護方針</dt>
<dd><a href="maintenance_policy.html">維護方針</a></dd>
<dt>發佈記</dt>
<dd><a href="upgrading_ruby_on_rails.html">升級 Ruby on Rails</a></dd>
<dd><a href="4_2_release_notes.html">Ruby on Rails 4.2 發佈記</a></dd>
<dd><a href="4_1_release_notes.html">Ruby on Rails 4.1 發佈記</a></dd>
<dd><a href="4_0_release_notes.html">Ruby on Rails 4.0 發佈記</a></dd>
<dd><a href="3_2_release_notes.html">Ruby on Rails 3.2 發佈記</a></dd>
<dd><a href="3_1_release_notes.html">Ruby on Rails 3.1 發佈記</a></dd>
<dd><a href="3_0_release_notes.html">Ruby on Rails 3.0 發佈記</a></dd>
<dd><a href="2_3_release_notes.html">Ruby on Rails 2.3 發佈記</a></dd>
<dd><a href="2_2_release_notes.html">Ruby on Rails 2.2 發佈記</a></dd>
<dt>Rails 指南翻譯術語</dt>
<dd><a href="translation_terms.html">翻譯術語</a></dd>
</dl>
</div>
</li>
<li><a class="nav-item" href="//github.com/docrails-tw/guides">貢獻翻譯</a></li>
<li><a class="nav-item" href="contributing_to_ruby_on_rails.html">貢獻</a></li>
<li><a class="nav-item" href="credits.html">致謝</a></li>
<li class="guides-index guides-index-small">
<select class="guides-index-item nav-item">
<option value="index.html">指南目錄</option>
<optgroup label="起步走">
<option value="getting_started.html">Rails 起步走</option>
</optgroup>
<optgroup label="Models">
<option value="active_record_basics.html">Active Record 基礎</option>
<option value="active_record_migrations.html">Active Record 遷移</option>
<option value="active_record_validations.html">Active Record 驗證</option>
<option value="active_record_callbacks.html">Active Record 回呼</option>
<option value="association_basics.html">Active Record 關聯</option>
<option value="active_record_querying.html">Active Record 查詢</option>
</optgroup>
<optgroup label="Views">
<option value="layouts_and_rendering.html">Rails 算繪與版型</option>
<option value="form_helpers.html">Action View 表單輔助方法</option>
</optgroup>
<optgroup label="Controllers">
<option value="action_controller_overview.html">Action Controller 綜覽</option>
<option value="routing.html">Rails 路由:深入淺出</option>
</optgroup>
<optgroup label="深入了解">
<option value="active_support_core_extensions.html">Active Support 核心擴展</option>
<option value="i18n.html">Rails 國際化 API</option>
<option value="action_mailer_basics.html">Action Mailer 基礎</option>
<option value="active_job_basics.html">Active Job 基礎</option>
<option value="security.html">Rails 安全指南</option>
<option value="debugging_rails_applications.html">除錯 Rails 應用程式</option>
<option value="configuring.html">Rails 應用程式設定</option>
<option value="command_line.html">Rake 任務與 Rails 命令列工具</option>
<option value="asset_pipeline.html">Asset Pipeline</option>
<option value="working_with_javascript_in_rails.html">在 Rails 使用 JavaScript</option>
<option value="constant_autoloading_and_reloading.html">Constant Autoloading and Reloading</option>
</optgroup>
<optgroup label="擴充 Rails">
<option value="rails_on_rack.html">Rails on Rack</option>
<option value="generators.html">客製與新建 Rails 產生器</option>
<option value="rails_application_templates.html">Rails 應用程式模版</option>
</optgroup>
<optgroup label="貢獻 Ruby on Rails">
<option value="contributing_to_ruby_on_rails.html">貢獻 Ruby on Rails</option>
<option value="api_documentation_guidelines.html">API 文件準則</option>
<option value="ruby_on_rails_guides_guidelines.html">Ruby on Rails 指南準則</option>
</optgroup>
<optgroup label="維護方針">
<option value="maintenance_policy.html">維護方針</option>
</optgroup>
<optgroup label="發佈記">
<option value="upgrading_ruby_on_rails.html">升級 Ruby on Rails</option>
<option value="4_2_release_notes.html">Ruby on Rails 4.2 發佈記</option>
<option value="4_1_release_notes.html">Ruby on Rails 4.1 發佈記</option>
<option value="4_0_release_notes.html">Ruby on Rails 4.0 發佈記</option>
<option value="3_2_release_notes.html">Ruby on Rails 3.2 發佈記</option>
<option value="3_1_release_notes.html">Ruby on Rails 3.1 發佈記</option>
<option value="3_0_release_notes.html">Ruby on Rails 3.0 發佈記</option>
<option value="2_3_release_notes.html">Ruby on Rails 2.3 發佈記</option>
<option value="2_2_release_notes.html">Ruby on Rails 2.2 發佈記</option>
</optgroup>
<optgroup label="Rails 指南翻譯術語">
<option value="translation_terms.html">翻譯術語</option>
</optgroup>
</select>
</li>
</ul>
</div>
</div>
</div>
<hr class="hide">
<div id="feature">
<div class="wrapper">
<h2>Action View 表單輔助方法</h2><p>表單是 Web 應用程式裡,供使用者輸入的基本介面。然而表單的各種繁雜的名稱與屬性,使得撰寫表單很快便變得繁瑣與難以維護。Rails 透過 Action View 提供輔助方法,來簡化表單的撰寫。但各種輔助方法的應用場景不盡相同,開發者需要知道輔助方法之間的差異,才能完善的使用這些輔助方法。</p><p>讀完本篇,您將了解:</p>
<ul>
<li>如何建立搜索表單與其它常見的通用表單。</li>
<li>如何替 Model 打造出編輯與建立資料庫記錄的表單。</li>
<li>如何從多種類型的資料產生下拉選單。</li>
<li>Rails 提供的日期與時間輔助方法。</li>
<li>上傳檔案表單的特別之處。</li>
<li>打造供外部資源使用的表單。</li>
<li>如何打造複雜表單。</li>
</ul>
<div id="subCol">
<h3 class="chapter"><img src="images/chapters_icon.gif" alt="" />Chapters</h3>
<ol class="chapters">
<li>
<a href="#%E8%99%95%E7%90%86%E7%B0%A1%E5%96%AE%E7%9A%84%E8%A1%A8%E5%96%AE">處理簡單的表單</a>
<ul>
<li><a href="#%E9%80%9A%E7%94%A8%E6%90%9C%E7%B4%A2%E8%A1%A8%E5%96%AE">通用搜索表單</a></li>
<li><a href="#%E5%91%BC%E5%8F%AB%E8%A1%A8%E5%96%AE%E8%BC%94%E5%8A%A9%E6%96%B9%E6%B3%95%E5%90%8C%E6%99%82%E5%82%B3%E5%A4%9A%E5%80%8B-hash">呼叫表單輔助方法同時傳多個 Hash</a></li>
<li><a href="#%E7%94%A2%E7%94%9F%E8%A1%A8%E5%96%AE%E5%85%83%E7%B4%A0%E7%9A%84%E8%BC%94%E5%8A%A9%E6%96%B9%E6%B3%95">產生表單元素的輔助方法</a></li>
<li><a href="#%E5%85%B6%E5%AE%83%E7%9B%B8%E9%97%9C%E8%BC%94%E5%8A%A9%E6%96%B9%E6%B3%95">其它相關輔助方法</a></li>
</ul>
</li>
<li>
<a href="#%E8%99%95%E7%90%86-model-%E7%89%A9%E4%BB%B6">處理 Model 物件</a>
<ul>
<li><a href="#model-%E7%89%A9%E4%BB%B6%E8%BC%94%E5%8A%A9%E6%96%B9%E6%B3%95">Model 物件輔助方法</a></li>
<li><a href="#%E5%B0%87%E8%A1%A8%E5%96%AE%E7%B6%81%E5%AE%9A%E5%88%B0%E7%89%A9%E4%BB%B6">將表單綁定到物件</a></li>
<li><a href="#%E8%A8%98%E9%8C%84%E8%87%AA%E5%8B%95%E8%AD%98%E5%88%A5%E6%8A%80%E8%A1%93">記錄自動識別技術</a></li>
<li><a href="#patch%E3%80%81put%E3%80%81delete-%E8%A1%A8%E5%96%AE%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86">PATCH、PUT、DELETE 表單的工作原理</a></li>
</ul>
</li>
<li>
<a href="#%E8%BC%95%E9%AC%86%E8%A3%BD%E4%BD%9C%E4%B8%8B%E6%8B%89%E5%BC%8F%E9%81%B8%E5%96%AE">輕鬆製作下拉式選單</a>
<ul>
<li><a href="#select-%E8%88%87-option-%E6%A8%99%E7%B1%A4">Select 與 Option 標籤</a></li>
<li><a href="#%E8%99%95%E7%90%86-models-%E7%9A%84%E4%B8%8B%E6%8B%89%E9%81%B8%E5%96%AE">處理 Models 的下拉選單</a></li>
<li><a href="#%E5%BE%9E%E4%BB%BB%E4%BD%95%E7%89%A9%E4%BB%B6%E9%9B%86%E5%90%88%E7%94%A2%E7%94%9F%E9%81%B8%E9%A0%85">從任何物件集合產生選項</a></li>
<li><a href="#%E6%99%82%E5%8D%80%E8%88%87%E5%9C%8B%E5%AE%B6%E9%81%B8%E5%96%AE">時區與國家選單</a></li>
</ul>
</li>
<li>
<a href="#%E6%97%A5%E6%9C%9F%E8%88%87%E6%99%82%E9%96%93%E7%9A%84%E8%A1%A8%E5%96%AE%E8%BC%94%E5%8A%A9%E6%96%B9%E6%B3%95">日期與時間的表單輔助方法</a>
<ul>
<li><a href="#%E6%BA%96%E6%96%B9%E6%B3%95">準方法</a></li>
<li><a href="#%E7%B5%A6-model-%E7%89%A9%E4%BB%B6%E7%94%A8%E7%9A%84%E6%96%B9%E6%B3%95">給 Model 物件用的方法</a></li>
<li><a href="#%E9%80%9A%E7%94%A8%E9%81%B8%E9%A0%85">通用選項</a></li>
<li><a href="#%E5%96%AE%E4%B8%80%E6%99%82%E9%96%93%E5%96%AE%E4%BD%8D">單一時間單位</a></li>
</ul>
</li>
<li>
<a href="#%E6%AA%94%E6%A1%88%E4%B8%8A%E5%82%B3">檔案上傳</a>
<ul>
<li><a href="#%E7%A9%B6%E7%AB%9F%E4%B8%8A%E5%82%B3%E4%BA%86%E4%BB%80%E9%BA%BC">究竟上傳了什麼</a></li>
<li><a href="#%E8%99%95%E7%90%86-ajax">處理 Ajax</a></li>
</ul>
</li>
<li><a href="#%E5%AE%A2%E8%A3%BD%E5%8C%96%E8%A1%A8%E5%96%AE%E6%A7%8B%E9%80%A0%E5%99%A8">客製化表單構造器</a></li>
<li>
<a href="#%E7%90%86%E8%A7%A3%E5%8F%83%E6%95%B8%E5%91%BD%E5%90%8D%E6%85%A3%E4%BE%8B">理解參數命名慣例</a>
<ul>
<li><a href="#%E5%9F%BA%E6%9C%AC%E7%B5%90%E6%A7%8B">基本結構</a></li>
<li><a href="#%E7%B5%90%E5%90%88%E8%B5%B7%E4%BE%86">結合起來</a></li>
<li><a href="#%E4%BD%BF%E7%94%A8%E8%A1%A8%E5%96%AE%E8%BC%94%E5%8A%A9%E6%96%B9%E6%B3%95">使用表單輔助方法</a></li>
</ul>
</li>
<li><a href="#%E9%80%81%E5%87%BA%E8%87%B3%E5%A4%96%E9%83%A8%E8%B3%87%E6%BA%90%E7%9A%84%E8%A1%A8%E5%96%AE">送出至外部資源的表單</a></li>
<li>
<a href="#%E6%89%93%E9%80%A0%E8%A4%87%E9%9B%9C%E8%A1%A8%E5%96%AE">打造複雜表單</a>
<ul>
<li><a href="#model-%E9%83%A8%E5%88%86">Model 部分</a></li>
<li><a href="#%E5%B5%8C%E5%A5%97%E8%A1%A8%E5%96%AE">嵌套表單</a></li>
<li><a href="#controller-%E9%83%A8%E5%88%86">Controller 部分</a></li>
<li><a href="#%E7%A7%BB%E9%99%A4%E7%89%A9%E4%BB%B6">移除物件</a></li>
<li><a href="#%E9%81%BF%E5%85%8D%E7%A9%BA%E7%9A%84%E7%B4%80%E9%8C%84">避免空的紀錄</a></li>
<li><a href="#%E5%8B%95%E6%85%8B%E6%B7%BB%E5%8A%A0%E6%AC%84%E4%BD%8D">動態添加欄位</a></li>
</ul>
</li>
</ol>
</div>
</div>
</div>
<div id="container">
<div class="wrapper">
<div id="mainCol">
<div class="note"><p>本篇不是表單輔助方法完整的文件,完整文件請參考 <a href="http://api.rubyonrails.org/">Rails API 文件</a>。</p></div><h3 id="處理簡單的表單">1 處理簡單的表單</h3><p>最基本的表單輔助方法是 <code>form_tag</code>。</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= form_tag do %>
Form contents
<% end %>
</pre>
</div>
<p>像這樣不傳參數呼叫時,會建立出 <code><form></code> 標籤。按下送出時,會對目前的頁面做 POST。舉例來說,假設目前的頁面是 <code>/home/index</code>,上例產生的 HTML 會像是(加了某些斷行提高可讀性):</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<form accept-charset="UTF-8" action="/" method="post">
<input name="utf8" type="hidden" value="&#x2713;" />
<input name="authenticity_token" type="hidden" value="J7CBxfHalt49OSHp27hblqK20c9PgwJ108nDHX/8Cts=" />
Form contents
</form>
</pre>
</div>
<p>注意到 HTML 裡有個隱藏的 <code>input</code>,這個隱藏的 <code>input</code> 很重要,沒有這個 <code>input</code> 表單便無法順利送出。第一個 <code>name</code> 屬性為 <code>utf8</code> 的 <code>input</code>,強制瀏覽器正確採用表單指定的編碼,所有 HTTP 動詞為 GET 或 POST 表單,Rails 都會產生這個 input。第二個 <code>name</code> 屬性為 <code>authenticity_token</code> 的 <code>input</code>,是 Rails 內建用來防止 CSRF (cross-site request forgery protection) 攻擊的安全機制,任何非 GET 的表單,Rails 都會產生一個這樣的 <code>input</code>(安全機制有啟用的話)。詳情請閱讀<a href="security.html#cross-site-request-forgery-csrf">安全指南</a>。</p><h4 id="通用搜索表單">1.1 通用搜索表單</h4><p>Web 世界最基本的表單之一是「搜索表單」。通常由以下元素組成:</p>
<ul>
<li>一個有 GET 動詞的表單</li>
<li>供輸入的文字欄位</li>
<li>輸入有標籤</li>
<li>送出元素</li>
</ul>
<p>要建立這樣的搜索表單,可以使用 <code>form_tag</code>、<code>label_tag</code>、<code>text_field_tag</code> 以及 <code>submit_tag</code>:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= form_tag("/search", method: "get") do %>
<%= label_tag(:q, "Search for:") %>
<%= text_field_tag(:q) %>
<%= submit_tag("Search") %>
<% end %>
</pre>
</div>
<p>會產生出如下 HTML:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<form accept-charset="UTF-8" action="/search" method="get">
<input name="utf8" type="hidden" value="&#x2713;" />
<label for="q">Search for:</label>
<input id="q" name="q" type="text" />
<input name="commit" type="submit" value="Search" />
</form>
</pre>
</div>
<div class="info"><p>每個表單的輸入 <code>input</code>,都會根據 <code>name</code> 屬性來產生 ID 屬性(上例為 <code>q</code>)。有了 ID,CSS 要新增樣式、或 JavaScript 要操作表單都很方便。</p></div><p>除了 <code>text_field_tag</code> 與 <code>submit_tag</code> 之外,每個表單元素都有對應的輔助方法。</p><div class="warning"><p>搜索表單永遠使用 GET 動詞。這允許使用者可以把搜索結果加入書籤,之後便能透過書籤瀏覽。Rails 普遍鼓勵使用正確的 HTTP 動詞。</p></div><h4 id="呼叫表單輔助方法同時傳多個-hash">1.2 呼叫表單輔助方法同時傳多個 Hash</h4><p><code>form_tag</code> 輔助方法接受 2 個參數:表單送出的目標路徑和 Hash 選項。Hash 選項用來指定表單所使用的方法,以及其它 HTML 選項,如指定表單的 <code>class</code>。</p><p>和 <code>link_to</code> 輔助方法類似,路徑不需要是字串。可以是 Rails Router 看的懂的 URL Hash,Rails 的路由機制會把 Hash 轉換為有效的 URL。但由於傳給 <code>form_tag</code> 的兩個參數都是 Hash 時,同時指定會碰到下例所演示的問題:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
form_tag(controller: "people", action: "search", method: "get", class: "nifty_form")
# => '<form accept-charset="UTF-8" action="/people/search?class=nifty_form&amp;method=get" method="post">'
</pre>
</div>
<p>這裡 <code>method</code> 與 <code>class</code> 變成了 URL 的查詢字串,因為 Rails 將這四個參數認成了一個 Hash。需要把第一組 Hash 放在大括號裡(或明確使用大括號亦可),才會產生出正確的 HTML:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
form_tag({ controller: "people", action: "search" }, method: "get", class: "nifty_form")
# => '<form accept-charset="UTF-8" action="/people/search" class="nifty_form" method="get">'
</pre>
</div>
<h4 id="產生表單元素的輔助方法">1.3 產生表單元素的輔助方法</h4><p>Rails 提供一系列的輔助方法,用來產生表單元素,像是多選方框(checkboxes)、文字欄位(text fields)以及單選按鈕(radio button)。名字以 <code>_tag</code> 結尾的輔助方法(譬如 <code>text_field_tag</code> 與 <code>check_box_tag</code>)只會產生一個 <code><input></code> 元素。這些輔助方法的第一個參數都是 <code>input</code> 的名稱(name)。表單送出時,<code>name</code> 會與表單資料一起送出,使用者輸入的資料會存在 <code>params</code> Hash 裡,可在 Controller 取用。舉個例子,若表單的 <code>input</code> 是 <code><%= text_field_tag(:query) %></code>,則可在 Controller 用 <code>params[:query]</code> 來獲得使用者的輸入。</p><p>Rails 使用特定的慣例來命名 <code>input</code>,使得送出像是陣列與 Hash 的值,也可以在 <code>params</code> 裡取用。了解更多可閱讀本文第七章:<a href="#%E7%90%86%E8%A7%A3%E5%8F%83%E6%95%B8%E5%91%BD%E5%90%8D%E6%85%A3%E4%BE%8B">理解參數命名慣例</a>。這些輔助方法更精確的用途,請參考 <a href="http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html">API 文件</a>。</p><h5 id="多選方框">1.3.1 多選方框</h5><p>多選方框是一種表單控件,給使用者一組可啟用停用的選項:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= check_box_tag(:pet_dog) %>
<%= label_tag(:pet_dog, "I own a dog") %>
<%= check_box_tag(:pet_cat) %>
<%= label_tag(:pet_cat, "I own a cat") %>
</pre>
</div>
<p>會產生出如下 HTML:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<input id="pet_dog" name="pet_dog" type="checkbox" value="1" />
<label for="pet_dog">I own a dog</label>
<input id="pet_cat" name="pet_cat" type="checkbox" value="1" />
<label for="pet_cat">I own a cat</label>
</pre>
</div>
<p><code>checkbox_box_tag</code> 第一個參數是 <code>input</code> 的 <code>name</code>,第二個參數通常是 <code>input</code> 的 <code>value</code>,當該多選方框被選中時,<code>value</code> 會被包含在表單資料一併送出,便可在 <code>params</code> 取用。</p><h5 id="radio-buttons">1.3.2 Radio Buttons</h5><p>單選按鈕與多選方框類似,但每個選項是互斥的(也就是只能選一個):</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= radio_button_tag(:age, "child") %>
<%= label_tag(:age_child, "I am younger than 21") %>
<%= radio_button_tag(:age, "adult") %>
<%= label_tag(:age_adult, "I'm over 21") %>
</pre>
</div>
<p>會產生出如下 HTML:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<input id="age_child" name="age" type="radio" value="child" />
<label for="age_child">I am younger than 21</label>
<input id="age_adult" name="age" type="radio" value="adult" />
<label for="age_adult">I'm over 21</label>
</pre>
</div>
<p>和 <code>check_box_tag</code> 類似,<code>radio_button_tag</code> 的第二個參數同樣是 <code>input</code> 的 <code>value</code>。因為這兩個單選按鈕的 <code>name</code> 都是 <code>age</code>,使用者只能選一個, <code>params[:age]</code> 的值會是 <code>"child"</code> 或 <code>"adult"</code>。</p><div class="note"><p>永遠記得幫多選方框與單選按鈕加上 <code>label</code>。<code>label</code> 可以為特定的輸入新增說明文字,也會加大可按範圍,讓使用者更容易選中。</p></div><h4 id="其它相關輔助方法">1.4 其它相關輔助方法</h4><p>其它值得一提的表單控件有:textareas、password fields、hidden fields、search fields、telephone fields、date fields、time fields、color fields、datetime fields、datetime-local fields、month fields、week fields、url fields、email fields、number fields 以及 range fields:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= text_area_tag(:message, "Hi, nice site", size: "24x6") %>
<%= password_field_tag(:password) %>
<%= hidden_field_tag(:parent_id, "5") %>
<%= search_field(:user, :name) %>
<%= telephone_field(:user, :phone) %>
<%= date_field(:user, :born_on) %>
<%= datetime_field(:user, :meeting_time) %>
<%= datetime_local_field(:user, :graduation_day) %>
<%= month_field(:user, :birthday_month) %>
<%= week_field(:user, :birthday_week) %>
<%= url_field(:user, :homepage) %>
<%= email_field(:user, :address) %>
<%= color_field(:user, :favorite_color) %>
<%= time_field(:task, :started_at) %>
<%= number_field(:product, :price, in: 1.0..20.0, step: 0.5) %>
<%= range_field(:product, :discount, in: 1..100) %>
</pre>
</div>
<p>產生的 HTML:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<textarea id="message" name="message" cols="24" rows="6">Hi, nice site</textarea>
<input id="password" name="password" type="password" />
<input id="parent_id" name="parent_id" type="hidden" value="5" />
<input id="user_name" name="user[name]" type="search" />
<input id="user_phone" name="user[phone]" type="tel" />
<input id="user_born_on" name="user[born_on]" type="date" />
<input id="user_meeting_time" name="user[meeting_time]" type="datetime" />
<input id="user_graduation_day" name="user[graduation_day]" type="datetime-local" />
<input id="user_birthday_month" name="user[birthday_month]" type="month" />
<input id="user_birthday_week" name="user[birthday_week]" type="week" />
<input id="user_homepage" name="user[homepage]" type="url" />
<input id="user_address" name="user[address]" type="email" />
<input id="user_favorite_color" name="user[favorite_color]" type="color" value="#000000" />
<input id="task_started_at" name="task[started_at]" type="time" />
<input id="product_price" max="20.0" min="1.0" name="product[price]" step="0.5" type="number" />
<input id="product_discount" max="100" min="1" name="product[discount]" type="range" />
</pre>
</div>
<p>隱藏的 <code>input</code> 不會顯示給使用者,但和其它文字輸入一樣可以存放資料。隱藏的 <code>input</code> 的值可以使用 JavaScript 來修改。</p><div class="warning"><p>search、telephone、date、time、color、datetime、datetime-local、month、week、URL、email、number 以及 range inputs 是 HTML5 控件。若需要應用程式在舊版的瀏覽器也有一致的瀏覽體驗,需要使用 HTML5 polyfill(由 CSS 或 JavaScript 提供)。<a href="https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills">雖然 polyfill 很好</a>,但目前主流工具是 <a href="http://www.modernizr.com/">Modernizr</a> 以及 <a href="http://yepnopejs.com/">yepnope</a>,這兩個工具提供一種簡單的方式,用來新增 HTML5 的新功能。</p></div><div class="info"><p>若使用了 password input fields(不論用途),輸入的值可能不要記錄在 Log。詳細做法請參考安全指南:<a href="security.html#logging">logging 一節</a>。</p></div><h3 id="處理-model-物件">2 處理 Model 物件</h3><h4 id="model-物件輔助方法">2.1 Model 物件輔助方法</h4><p>表單通常拿來新建或編輯 Model 物件。可以使用 <code>*_tag</code> 這些輔助方法來處理,但太繁瑣了,參數名稱和預設值都得正確才行。Rails 提供更多方便的輔助方法(沒有 <code>_tag</code> 字尾),像是 <code>text_field</code>、<code>text_area</code> 等,專門用來處理 Model 物件。</p><p>這些輔助方法的第一個參數是實體變數的名字,第二個參數是要對實體變數呼叫的方法名稱(通常是屬性)。Rails 會將呼叫的結果存成 <code>input</code> 的 <code>value</code>,並幫你給 <code>input</code> 的 <code>name</code> 取個好名字。假設 Controller 已經定義了 <code>@person</code>,<code>@person.name</code> 是 <code>Henry</code>,則:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= text_field(:person, :name) %>
</pre>
</div>
<p>會產生</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<input id="person_name" name="person[name]" type="text" value="Henry"/>
</pre>
</div>
<p>送出表單時,使用者的輸入會存在 <code>params[:person][:name]</code>,<code>params[:person]</code> 可傳給 <code>Person.new</code>;若 <code>@person</code> 是 <code>Person</code> 的實體,則可傳給 <code>Person#update</code>。通常第二個參數是屬性名稱,實在是太常用了,通常可省略不寫,只要該物件有實作 <code>name</code> 與 <code>name=</code> 方法即可。</p><div class="warning"><p>第一個參數必須是實體變數的“名稱”,如:<code>:person</code> 或 <code>"person"</code>,而不是傳實際的實體物件進去。</p></div><p>Rails 還提供了用來顯示與 Model 物件驗證錯誤訊息的輔助方法。這些方法在 <a href="/active_record_validations.html#%E5%9C%A8-view-%E9%A1%AF%E7%A4%BA%E9%A9%97%E8%AD%89%E5%A4%B1%E6%95%97%E8%A8%8A%E6%81%AF">Active Record 驗證</a>一文裡詳細說明。</p><h4 id="將表單綁定到物件">2.2 將表單綁定到物件</h4><p>雖然這些去掉 <code>_tag</code> 的輔助方法很方便,但還不夠好。若 <code>Person</code> 有很多屬性時,得一直重複傳入要編輯的物件名稱,來生成對應的表單。Rails 提供了 <code>form_for</code>,用來將表單綁定至 Model 的物件。</p><p>假設有處理文章的 Controller <code>app/controllers/articles_controller.rb</code>:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
def new
@article = Article.new
end
</pre>
</div>
<p>對應的 View <code>app/views/articles/new.html.erb</code>,使用了 <code>form_for</code> 看起來會像是這樣:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= form_for @article, url: {action: "create"}, html: {class: "nifty_form"} do |f| %>
<%= f.text_field :title %>
<%= f.text_area :body, size: "60x12" %>
<%= f.submit "Create" %>
<% end %>
</pre>
</div>
<p>有幾件要說明的事情:</p>
<ul>
<li><p><code>@article</code> 是實際被編輯的物件。
& <code>form_for</code> 接受一個 Hash 選項。路由相關選項放在 <code>:url</code> 傳入,HTML 相關選項放在 <code>html:</code> 選項傳入。還可以提供 <code>:namespace</code> 選項,用來確保 ID 的唯一性。<code>namespace</code> 的值會自動成為 HTML ID 的前綴。</p></li>
<li><p><code>form_for</code> 方法會產生一個 <strong>表單構造器(Form Builder)</strong> 物件(<code>f</code> 變數)。</p></li>
<li><p>輔助方法皆在 <code>f</code>,表單構造器上呼叫。</p></li>
</ul>
<p>產生的 HTML 為:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<form accept-charset="UTF-8" action="/articles/create" method="post" class="nifty_form">
<input id="article_title" name="article[title]" type="text" />
<textarea id="article_body" name="article[body]" cols="60" rows="12"></textarea>
<input name="commit" type="submit" value="Create" />
</form>
</pre>
</div>
<p>傳給 <code>form_for</code> 的名稱會成為在 <code>params</code> 取用表單數值的鍵。上例名稱為 <code>article</code>,因此所有的 <code>name</code> 都是 <code>article[attribute_name]</code>。在 <code>create</code> 動作裡的 <code>params[:article]</code> 會是有著 <code>:title</code> 與 <code>:body</code> 鍵的 Hash。輸入名稱的重要性,可參閱<a href="#%E7%90%86%E8%A7%A3%E5%8F%83%E6%95%B8%E5%91%BD%E5%90%8D%E6%85%A3%E4%BE%8B">理解參數命名慣例</a>一節。</p><p>對表單構造器呼叫輔助方法,和對 Model 物件上呼叫的效果相同。但不需要指定編輯的物件,因為編輯的物件即表單構造器。</p><p>使用 <code>fields_for</code> 輔助方法也可以達到上面的效果,但不會產生出 <code><form></code> 標籤。同個表單用來編輯多個 Model 物件時很有用。譬如 <code>Person</code> Model 有個關聯的 <code>ContactDetail</code> Model,下面的表單可以同時建立初兩個 Model 的物件:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= form_for @person, url: {action: "create"} do |person_form| %>
<%= person_form.text_field :name %>
<%= fields_for @person.contact_detail do |contact_details_form| %>
<%= contact_details_form.text_field :phone_number %>
<% end %>
<% end %>
</pre>
</div>
<p>會產生出以下輸出:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<form accept-charset="UTF-8" action="/people/create" class="new_person" id="new_person" method="post">
<input id="person_name" name="person[name]" type="text" />
<input id="contact_detail_phone_number" name="contact_detail[phone_number]" type="text" />
</form>
</pre>
</div>
<p><code>fields_for</code> 給出的物件也是個表單構造器,和 <code>form_for</code> 一樣(實際上 <code>form_for</code> 內部呼叫的是 <code>fields_for</code>)。</p><h4 id="記錄自動識別技術">2.3 記錄自動識別技術</h4><p>如使用者可以直接操作 <code>Article</code> Model,則依據 Rails 開發的最佳實踐,應將 <code>Article</code> 視為<strong>一個資源</strong>。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
resources :articles
</pre>
</div>
<div class="info"><p>宣告成資源有許多副作用。見 <a href="routing.html#%E8%B3%87%E6%BA%90%E5%BC%8F%E8%B7%AF%E7%94%B1%EF%BC%9Arails-%E7%9A%84%E9%A0%90%E8%A8%AD%E8%B7%AF%E7%94%B1">Rails 路由:深入淺出〈資源式路由:Rails 的預設路由〉</a>來瞭解更多關於設定與使用資源的資訊。</p></div><p>處理 RESTful 資源時,若用了記錄自動識別技術,則呼叫 <code>form_for</code> 便很容易使用。簡單的說,可以只把 Model 實體傳進去,Rails 會自己處理好 Model 名稱與其它內容:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
## Creating a new article
# long-style:
form_for(@article, url: articles_path)
# same thing, short-style (record identification gets used):
form_for(@article)
## Editing an existing article
# long-style:
form_for(@article, url: article_path(@article), html: {method: "patch"})
# short-style:
form_for(@article)
</pre>
</div>
<p>無論記錄是否存在,使用簡短風格的 <code>form_for</code> 呼叫都長得一樣。記錄自動識別技術很聰明,會對紀錄呼叫 <code>record.new_record?</code> 來檢查是否是新紀錄。也能根據物件的類別,選出正確的送出路徑與名稱。</p><p>Rails 也會自動幫表單設定適當的 <code>class</code> 與 <code>id</code>。新增文章的表單 <code>id</code> 與 <code>class</code> 可能是 <code>new_article</code>。若編輯 ID 為 23 的文章,<code>class</code> 則會設為 <code>edit_article</code>、<code>id</code> 設為 <code>edit_article_23</code>。為求行文簡潔,這些屬性後文忽略不計。</p><div class="warning"><p>使用 STI(單表繼承)時,如父類宣告為資源,則子類便不能依賴記錄自動識別技術。必須要明確指定 Model 的名稱、<code>:url</code> 以及 <code>:method</code>。</p></div><h5 id="處理命名空間">2.3.1 處理命名空間</h5><p>若建立的路由有命名空間,<code>form_for</code> 也有對應的簡寫形式。假設應用程式有 <code>admin</code> 命名空間:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
form_for [:admin, @article]
</pre>
</div>
<p>會在 <code>admin</code> 命名空間裡,建立出對 <code>ArticlesController</code> 提交的表單,送出結果到 <code>admin_article_path(@article)</code>(假設是更新文章的情況)。若有多層命名空間,語法類推:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
form_for [:admin, :management, @article]
</pre>
</div>
<p>關於 Rails 路由系統的更多資訊以及有關的慣例,請參見:[Rails 路由:深入淺出]。</p><h4 id="patch、put、delete-表單的工作原理">2.4 PATCH、PUT、DELETE 表單的工作原理</h4><p>Rails 框架鼓勵用 RESTful 風格來設計應用程式,這表示會用到許多 “PATCH” 與 “DELETE” 請求(而不只是 GET 與 POST)。但多數瀏覽器 <strong>只支援</strong> 用 GET 或 POST 來送出表單。</p><p>Rails 透過使用 POST 請求模擬出其它 HTTP 方法來解決這個問題。在表單裡新增一個 <code>name</code> 為 <code>_method</code>、<code>value</code> 為真正希望使用的方法名稱的隱藏輸入:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
form_tag(search_path, method: "patch")
</pre>
</div>
<p>輸出:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<form accept-charset="UTF-8" action="/search" method="post">
<input name="_method" type="hidden" value="patch" />
<input name="utf8" type="hidden" value="&#x2713;" />
<input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
...
</form>
</pre>
</div>
<p>解析 POST 過來的資料時,Rails 會將特殊的 <code>_method</code> 參數考慮進去,以 <code>value</code> 的值作為 HTTP 方法(上例為 “PATCH”)。</p><h3 id="輕鬆製作下拉式選單">3 輕鬆製作下拉式選單</h3><p>HTML 的下拉選單需要大量的 Markup(一個選項就要一個 <code>OPTION</code> 元素),非常適合動態產生這些選項。</p><p>以下是可能的 Markup:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<select name="city_id" id="city_id">
<option value="1">Lisbon</option>
<option value="2">Madrid</option>
...
<option value="12">Berlin</option>
</select>
</pre>
</div>
<p>這裡有一組給使用者選擇的城市清單。應用程式內部只需要處理各選項的 ID,因此把 <code>option</code> 的 <code>value</code> 設為 ID。接著看 Rails 如何化繁為簡。</p><h4 id="select-與-option-標籤">3.1 Select 與 Option 標籤</h4><p>最通用的輔助方法是 <code>select_tag</code>,從名字就可以看出來,是用來產生封裝了選項字串的 <code>select</code> 標籤:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= select_tag(:city_id, '<option value="1">Lisbon</option>...') %>
</pre>
</div>
<p>這只是剛開始而已,上面把字串封裝在 <code>select_tag</code> 裡面,無法動態生成 <code>option</code> 標籤,於是有了 <code>options_for_select</code>:</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...]) %>
輸出:
<option value="1">Lisbon</option>
<option value="2">Madrid</option>
...
</pre>
</div>
<p><code>options_for_select</code> 的第一個參數是選項組成的嵌套陣列,每個選項有兩個元素,選項文字(城市名稱)與選項數值(城市 ID)。選項數值會送給 Controller 處理。通常會是資料庫對應物件的 ID,但也不強迫一定要用 ID。</p><p>瞭解之後,可以結合 <code>select_tag</code> 與 <code>options_for_select</code> 來實作完整的 Markup:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= select_tag(:city_id, options_for_select(...)) %>
</pre>
</div>
<p><code>options_for_select</code> 的第二個參數可以設定預設選項。</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
<%= options_for_select([['Lisbon', 1], ['Madrid', 2], ...], 2) %>
輸出:
<option value="1">Lisbon</option>
<option value="2" selected="selected">Madrid</option>
...
</pre>
</div>
<p>Rails 在發現屬性值與 <code>options_for_select</code> 第二個參數的值相同時,便會給該選項新增 <code>selected</code> 屬性。</p><div class="info"><p><code>options_for_select</code> 的第二個參數,必須與需要選中選項的值完全相等。特別注意若該選項的值是整數 <code>2</code>,<code>options_for_select</code> 第二個參數的值便不可以是 <code>"2"</code>,必須是 <code>2</code>。需要注意的是從 <code>params</code> 取出的數值都是字串。</p></div><p>可以用 Hash 給每個選項加上任意的屬性:</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
<%= options_for_select(
[
['Lisbon', 1, { 'data-size' => '2.8 million' }],
['Madrid', 2, { 'data-size' => '3.2 million' }]
], 2
) %>
輸出:
<option value="1" data-size="2.8 million">Lisbon</option>
<option value="2" selected="selected" data-size="3.2 million">Madrid</option>
...
</pre>
</div>
<h4 id="處理-models-的下拉選單">3.2 處理 Models 的下拉選單</h4><p>多數情況下表單控件與特定的資料庫模型綁在一起,可能會好奇 Rails 有沒有針對 Model 提供 的輔助方法可用呢?答案是有。針對 Model 的輔助方法和其它的表單輔助方法相同,名稱去掉 <code>select_tag</code> 的 <code>_tag</code> 即可:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
# controller:
@person = Person.new(city_id: 2)
</pre>
</div>
<div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
# view:
<%= select(:person, :city_id, [['Lisbon', 1], ['Madrid', 2], ...]) %>
</pre>
</div>
<p>注意 <code>select</code> 的第三個參數,由選項組成的陣列,跟傳給 <code>options_for_select</code> 的參數一樣。好處是無需煩惱預選的城市是那個,Rails 會自己去讀取 <code>@person.city_id</code> 來決定預選城市是那個。</p><p>和其它輔助方法一樣,對表單構造器也可以使用,語法是:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
# select on a form builder
<%= f.select(:city_id, ...) %>
</pre>
</div>
<p><code>select</code> 也接受區塊:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= f.select(:city_id) do %>
<% [['Lisbon', 1], ['Madrid', 2]].each do |c| -%>
<%= content_tag(:option, c.first, value: c.last) %>
<% end %>
<% end %>
</pre>
</div>
<p>上例 Person 與 City Model 存在 <code>belongs_to</code> 關係,在使用 <code>select</code> 時必須傳入 foreign key,否則會報這個錯誤:<code>ActiveRecord::AssociationTypeMismatch</code>。</p><p>若使用 <code>select</code> (或其它類似的輔助方法,像是 <code>collection_select</code>、<code>select_tag</code>)來設定 <code>belongs_to</code> 關聯,則必須傳入外鍵的名稱(上例須傳入 <code>city_id</code>),而不是關聯名稱。若指定的是 <code>city</code> 而不是 <code>city_id</code>,把 <code>params</code> 傳給 <code>Person.new</code> 或 <code>Person.update</code> 時,Active Record 會拋出錯誤: <code>ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750)</code>。換句話說也就是表單輔助方法只能編輯屬性。應該要注意讓使用者直接編輯外鍵,所存在的安全性風險。</p><h4 id="從任何物件集合產生選項">3.3 從任何物件集合產生選項</h4><p>用 <code>options_for_select</code> 來產生選項,需要先建立陣列,陣列裡有選項文字與數值。但要是已經有了 City Model(假設是個繼承自 Active Record 的 Model),想要直接從 Model 的實體產生出這些選項該怎麼做?解法之一是迭代這些物件,產生出嵌套的陣列:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<% cities_array = City.all.map { |city| [city.name, city.id] } %>
<%= options_for_select(cities_array) %>
</pre>
</div>
<p>這個方法完美可行,但 Rails 提供更簡潔的解法:<code>options_from_collection_for_select</code>。這個輔助方法接受一組任意物件的集合和兩個額外的參數:用來讀取選項 <strong>數值</strong> 與 <strong>文字</strong> 的方法名稱。</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= options_from_collection_for_select(City.all, :id, :name) %>
</pre>
</div>
<p>從名字可以看出來,<code>options_from_collection_for_select</code> 只會產生出 <code>option</code> 標籤。要產生出會動的 <code>select</code>,需要與 <code>select_tag</code> 一起使用。就跟 <code>options_for_select</code> 需要與 <code>select_tag</code> 同時使用的情況相同。在處理 Model 物件時,<code>select</code> 結合了 <code>select_tag</code> 與 <code>options_for_select</code>;<code>collection_select</code> 則結合了 <code>select_tag</code> 與 <code>options_from_collection_for_select</code>。</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= collection_select(:person, :city_id, City.all, :id, :name) %>
</pre>
</div>
<p>和其他輔助方法一樣,若想在 form builder 的作用在 <code>@person</code> 物件裡使用 <code>collection_select</code>,應當這麼寫:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= f.collection_select(:city_id, City.all, :id, :name) %>
</pre>
</div>
<p>複習一下,<code>options_from_collection_for_select</code> 與 <code>collection_select</code> 的關係,和 <code>options_for_select</code> 與 <code>select</code> 之間的關係一樣。</p><div class="note"><p>傳給 <code>options_for_select</code> 的陣列需要先傳 <code>name</code>,再傳 <code>id</code>;而 <code>options_from_collection_for_select</code> 則是先傳 <code>id</code>,再傳 <code>name</code>。</p></div><h4 id="時區與國家選單">3.4 時區與國家選單</h4><p>要完善利用 Rails 支援的時區功能,首先要詢問使用者所在的時區為何。要詢問時區得先產生所有的時區選項,再傳給 <code>collection_select</code> 來產生選單,但可以直接使用 <code>time_zone_select</code> 輔助方法,已經包裝好了:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= time_zone_select(:person, :time_zone) %>
</pre>
</div>
<p>還有一個 <code>time_zone_options_for_select</code> 輔助方法,這個的客製性更高。關於這個方法的使用方法,請查閱 API 文件,來了解 <code>time_zone_select</code> 與 <code>time_zone_options_for_select</code> 可用的參數有那些。</p><p>Rails 曾有過 <code>country_select</code> 輔助方法,用來選擇國家。但已經抽出來變成 <a href="https://github.com/stefanpenner/country_select">country_select</a> 套件。使用這個套件時,請注意清單裡的國家名稱,有些國家有列在清單裡、有些沒有、有些有爭議。這也是為什麼 Rails 不內建這個功能的原因。</p><h3 id="日期與時間的表單輔助方法">4 日期與時間的表單輔助方法</h3><p>可選擇不用會產生出 HTML5 日期與時間輸入欄位的輔助方法,而使用替代的日期與時間輔助方法。這些日期與時間方法和其它的表單輔助方法主要有以下兩點不同:</p>
<ul>
<li>日期與時間不代表單一的 <code>input</code> 元素,而是多個 <code>input</code>,每個有每個的用途(年份、月份、日等)。所以 <code>params</code> 裡的日期與時間不會是個單獨的數值。</li>
<li>其它的表單輔助方法用 <code>_tag</code> 來區分,這個方法是個準方法,或是針對 Model 物件的輔助方法。而日期與時間的輔助方法有:<code>select_date</code>、<code>select_time</code> 以及 <code>select_datetime</code> 是準方法;而 <code>date_select</code>、<code>time_select</code> 以及 <code>datetime_select</code> 則是針對 Model 物件的輔助方法。</li>
</ul>
<p>準方法和針對 Model 物件的方法,都會針對不同的時間單位(年、月、日等)來建出選單。</p><h4 id="準方法">4.1 準方法</h4><p><code>select_*</code> 家族的輔助方法,第一個參數接受的是日期的實體,<code>Date</code>、<code>Time</code> 或 <code>DateTime</code>,用來作為目前選中的日期。第一個參數可以忽略,預設會選擇當下日期。舉個例子:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= select_date Date.today, prefix: :start_date %>
</pre>
</div>
<p>輸出(省略選項數值,保持簡單):</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<select id="start_date_year" name="start_date[year]"> ... </select>
<select id="start_date_month" name="start_date[month]"> ... </select>
<select id="start_date_day" name="start_date[day]"> ... </select>
</pre>
</div>
<p>以上的輸入送出時會存在 <code>params[:start_date]</code>,以散列表的形式儲存,鍵有 <code>:year</code>、<code>:month</code> 以及 <code>day</code>。要獲得實際的 <code>Time</code> 或 <code>Date</code> 物件,可以將時間各個單位取出來,傳給適當的建構子,參考下例:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, params[:start_date][:day].to_i)
</pre>
</div>
<p>上例的 <code>:prefix</code> 選項為 <code>:start_date</code>,是時間單位存在 <code>params</code> 的鍵名。沒給的話預設值是 <code>date</code>。</p><h4 id="給-model-物件用的方法">4.2 給 Model 物件用的方法</h4><p><code>select_date</code> 與 Active Record 配合的不好,因為 Active Record 期望每個 <code>params</code> 的元素,都對應到一個屬性。而 Model 物件的日期與時間輔助方法,會採用特殊的名稱來送出參數。Active Record 看到這些特殊名稱的參數時,便知道要將這些參數結合起來,傳給欄位類型的建構子。譬如:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= date_select :person, :birth_date %>
</pre>
</div>
<p>輸出(省略選項數值,保持簡單):</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<select id="person_birth_date_1i" name="person[birth_date(1i)]"> ... </select>
<select id="person_birth_date_2i" name="person[birth_date(2i)]"> ... </select>
<select id="person_birth_date_3i" name="person[birth_date(3i)]"> ... </select>
</pre>
</div>
<p>產生出來的 <code>params</code> :</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
{'person' => {'birth_date(1i)' => '2008', 'birth_date(2i)' => '11', 'birth_date(3i)' => '22'}}
</pre>
</div>
<p><code>params</code> 傳給 <code>Person.new</code> 或 <code>Person.update</code> 時,Active Record 會注意到這些參數名稱,要一起傳進來,來產生 <code>birth_date</code> 屬性,並根據字尾的資訊(<code>ni</code>),來決定傳給 <code>Date.civil</code> 的順序。</p><h4 id="通用選項">4.3 通用選項</h4><p>這兩個家族的輔助方法,內部使用同一組核心功能,來產生 <code>select</code> 標籤,因此接受的選項大致相同。特別要提 Rails 預設會產生前後五年的年份。若這個範圍不夠用,<code>:start_year</code> 以及 <code>:end_year</code> 選項可以修改。可用選項更詳細的清單,請參考 <a href="http://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html">API 文件</a>。</p><p>經驗法則表示,處理 Model 物件使用 <code>date_select</code>、其它情況用 <code>select_date</code>,像是用來過濾日期的搜尋表單。</p><div class="note"><p>內建的日期選單不太好用,無法幫助使用者處理日期與星期幾這個問題。</p></div><h4 id="單一時間單位">4.4 單一時間單位</h4><p>有時只需顯示日期的某個部分,像年或月。Rails 提供一系列的輔助方法:<code>select_year</code>、<code>select_month</code>、<code>select_day</code>、<code>select_hour</code>、<code>select_minute</code> 以及 <code>select_second</code>。這些輔助方法的使用方式非常直觀,產生出來的 <code>input</code>,<code>name</code> 屬性預設會產生以時間單位命名的(譬如 <code>select_year</code> 產生出來的 <code>select</code>,<code>name</code> 為 <code>year</code>,以此類推)。這可以透過 <code>:field_name</code> 選項修改。<code>:prefix</code> 選項和 <code>select_date</code> 與 <code>select_time</code> 裡的用途相同,預設值也相同。</p><p>這些輔助方法的第一個參數指定要選中的數值,可以是 <code>Date</code>、<code>Time</code> 或 <code>DateTime</code> 的實體,或是數值也可以,對應的時間單位會被選中,譬如:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= select_year(2009) %>
<%= select_year(Time.now) %>
</pre>
</div>
<p>若今年是 2009 年,上面兩種用法的輸出相同,使用者選的數值可以在 <code>params[:date][:year]</code> 取出。</p><h3 id="檔案上傳">5 檔案上傳</h3><p>常見的任務是上傳檔案,舉凡使用者的圖片或需要處理的 CSV。檔案上傳最重要要記住的一點是,表單的編碼必須是 <code>"multipart/form-data"</code>。若使用 <code>form_for</code>,已經自動設定好了。若使用 <code>form_tag</code>,則必須自己設定,以下是表單上傳檔案的兩個例子:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= form_tag({action: :upload}, multipart: true) do %>
<%= file_field_tag 'picture' %>
<% end %>
<%= form_for @person do |f| %>
<%= f.file_field :picture %>
<% end %>
</pre>
</div>
<p>Rails 提供成對的輔助方法:準方法 <code>file_field_tag</code> 以及供 Model 物件使用的 <code>file_field</code>。這兩個輔助方法與其它表單輔助方法的差別在於無法設定預設值,因為預設值在這沒有意義。第一個例子,使用 <code>file_field_tag</code> 上傳的檔案會存在 <code>params[:picture]</code>,而 <code>file_field</code> 上傳的檔案則放在 <code>params[:person][:picture]</code>。</p><h4 id="究竟上傳了什麼">5.1 究竟上傳了什麼</h4><p><code>params</code> Hash 裡的物件,是 <code>IO</code> 子類別的實體。取決於上傳的檔案大小,會是 <code>StringIO</code> 或存在臨時檔案的 <code>File</code> 實體。兩種都會有 <code>original_filename</code> 屬性,記錄使用者電腦裡的檔案名稱;以及 <code>content_type</code> 屬性,記錄了上傳檔案的 <code>MIME</code> 類型。以下程式碼片段將上傳的內容存在 <code>#{Rails.root}/public/uploads</code>,使用原始上傳的檔名存放(假設使用前例 <code>form_for</code> 的表單來上傳)。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
def upload
uploaded_io = params[:person][:picture]
File.open(Rails.root.join('public', 'uploads', uploaded_io.original_filename), 'wb') do |file|
file.write(uploaded_io.read)
end
end
</pre>
</div>
<p>一旦檔案上傳成功,有許多事情可以做。譬如把檔案存到別的地方(硬碟、Amazon S3 等);或把檔案與 Model 關聯起來;縮放圖片檔案、產生縮圖等。這些事情超出了本文的範疇,但有許多專門設計的函式庫來協助完成這些任務。其中兩個不錯也比較多人知道的是 <a href="https://github.com/jnicklas/carrierwave">CarrierWave</a> 以及 <a href="https://github.com/thoughtbot/paperclip">Paperclip</a>。</p><div class="note"><p>若使用者沒有選擇檔案,對應的參數會是空字串。</p></div><h4 id="處理-ajax">5.2 處理 Ajax</h4><p>要非同步的上傳檔案,不像其它的方法那麼簡單,像 <code>form_for</code> 只要加個 <code>remote: true</code> 即可。Ajax 表單的序列化由跑在瀏覽器的 JavaScript 處理,由於 JavaScript 無法從硬碟讀取檔案,檔案則無法上傳。最常見的解法是使用隱藏的 iframe,作為表單送出的目的地。</p><h3 id="客製化表單構造器">6 客製化表單構造器</h3><p>如前所述,由 <code>form_for</code> 與 <code>fields_for</code> 給出的物件,是 <code>FormBuilder</code> (或子類)的實體。表單構造器封裝了單一物件的顯示。當然可以如往常一樣使用輔助方法,也可以繼承 <code>FormBuilder</code>,再往裡面新增輔助方法。譬如:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= form_for @person do |f| %>
<%= text_field_with_label f, :first_name %>
<% end %>
</pre>
</div>
<p>可以替換成</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= form_for @person, builder: LabellingFormBuilder do |f| %>
<%= f.text_field :first_name %>
<% end %>
</pre>
</div>
<p>藉由定義 <code>LabellingFormBuilder</code> 類別:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class LabellingFormBuilder < ActionView::Helpers::FormBuilder
def text_field(attribute, options={})
label(attribute) + super
end
end
</pre>
</div>
<p>若很常需要使用這個功能,可以定義一個 <code>labeled_form_for</code> 輔助方法,來自動代入 <code>builder: LabellingFormBuilder</code> 選項:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
def labeled_form_for(record, options = {}, &block)
options.merge! builder: LabellingFormBuilder
form_for record, options, &block
end
</pre>
</div>
<p>表單構造器也決定了下面這行程式碼的行為:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= render partial: f %>
</pre>
</div>
<p>若 <code>f</code> 是 <code>FormBuilder</code> 的實體,則會算繪(render)<code>form</code> 這個部分頁面(partial),並把傳入的 <code>f</code> 設定成表單構造器。若表單構造器是 <code>LabellingFormBuilder</code> 的實體,則會算繪 <code>labelling_form</code> 這個部分頁面。</p><h3 id="理解參數命名慣例">7 理解參數命名慣例</h3><p>如前一節所見,表單的數值可以在 <code>params</code> 的第一層,或是嵌套在 Hash 裡。舉例來說,<code>Person</code> Model 對應的 Controller <code>create</code> 動作裡,<code>params[:person]</code> 這個 Hash,會存放建立 <code>person</code> 所需的屬性。<code>params</code> Hash 也可以包含陣列、陣列裡有 Hash 等都可以。</p><p>HTML 表單基本上不知道資料的結構,只是產生出純字串組成的 name-value 對。應用程式裡的陣列與 Hash,是透過 Rails 參數的命名慣例所產生。</p><div class="info"><p>可能會發現在 Console 裡試試這些例子,可以瞭解得比較快。直接像下例這樣呼叫 Rack 的參數即可:</p></div><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
Rack::Utils.parse_query "name=fred&phone=0123456789"
# => {"name"=>"fred", "phone"=>"0123456789"}
</pre>
</div>
<h4 id="基本結構">7.1 基本結構</h4><p>兩個基本結構是陣列與 Hash。Hash 取值的方法和 <code>params</code> 相同。假設表單的內容為:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<input id="person_name" name="person[name]" type="text" value="Henry"/>
</pre>
</div>
<p>則 <code>params</code> 的內容為:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
{'person' => {'name' => 'Henry'}}
</pre>
</div>
<p>在 Controller 可以用 <code>params[:person][:name]</code> 來取出表單送出的數值。</p><p>Hash 可以多層嵌套,如:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<input id="person_address_city" name="person[address][city]" type="text" value="New York"/>
</pre>
</div>
<p>產生的 <code>params</code> Hash:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
{'person' => {'address' => {'city' => 'New York'}}}
</pre>
</div>
<p>通常 Rails 會忽略重複的參數。若參數名稱有中括號,則會被放在陣列裡。若想使用者能夠輸入多組電話號碼,可以使用下面這個表單:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<input name="person[phone_number][]" type="text"/>
<input name="person[phone_number][]" type="text"/>
<input name="person[phone_number][]" type="text"/>
</pre>
</div>
<p>則 <code>params[:person][:phone_number]</code> 會是個陣列。</p><h4 id="結合起來">7.2 結合起來</h4><p>陣列與 Hash 可以混合使用。舉個例子,Hash 的一個元素可能像前面的例子一樣,是個陣列;或是可以有一個陣列,裡面存 Hash。下例是用來新建多筆地址的表單:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<input name="addresses[][line1]" type="text"/>
<input name="addresses[][line2]" type="text"/>
<input name="addresses[][city]" type="text"/>
</pre>
</div>
<p>則 <code>params[:addresses]</code> 會是裡面有 Hash 的陣列,每個 Hash 的鍵有 <code>line1</code>, <code>line2</code> 以及 <code>city</code>。Rails 在目前的 Hash 發現有同樣的輸入時,會新建 Hash 來存放。</p><p>但有個限制,Hash 可以隨意嵌套,但陣列只能嵌套一次。陣列通常可以用 Hash 取代,譬如可以用 Hash 組成的 Model 物件來取代陣列組成的 Model 物件,Hash 的鍵是 <code>id</code>、陣列的索引、以及其它的參數。</p><div class="warning"><p>陣列參數與 <code>check_box</code> 輔助方法配合的不好。根據 HTML 規範,沒選中的多選方框不會送出值。但多選方框總是送出值會比較方便。<code>check_box</code> 透過建立一個同名的隱藏輸入來處理。若多選方框沒有被勾選,則只會送出隱藏輸入;若勾選了多選方框,則會將隱藏輸入與勾選的值一起送出,但勾選的值優先權比較高。處理陣列參數時,重複的送出會使 Rails 困惑,因為 Rails 見到重複的輸入,就會建立一個新的陣列。使用 <code>check_box_tag</code> 或用 Hash 取代陣列是推薦的做法。</p></div><h4 id="使用表單輔助方法">7.3 使用表單輔助方法</h4><p>前一節完全沒用到 Rails 的表單輔助方法。自己手寫 <code>input</code> 再直接傳給 <code>text_field_tag</code> 沒有問題。但 Rails 提供了更抽象的方法。這裡介紹 <code>form_for</code> 與 <code>fields_for</code>,以及 <code>:index</code> 選項。</p><p>可能會想要有地址表單,裡面有一組可編輯的欄位,分別編輯地址的各個部分。</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= form_for @person do |person_form| %>
<%= person_form.text_field :name %>
<% @person.addresses.each do |address| %>
<%= person_form.fields_for address, index: address.id do |address_form|%>
<%= address_form.text_field :city %>
<% end %>
<% end %>
<% end %>
</pre>
</div>
<p>假設 <code>person</code> 有兩個地址(<code>id</code> 分別是 23 與 <code>45</code>),輸出會像是:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<form accept-charset="UTF-8" action="/people/1" class="edit_person" id="edit_person_1" method="post">
<input id="person_name" name="person[name]" type="text" />
<input id="person_address_23_city" name="person[address][23][city]" type="text" />
<input id="person_address_45_city" name="person[address][45][city]" type="text" />
</form>
</pre>
</div>
<p>產生出的 <code>params</code>:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
{'person' => {'name' => 'Bob', 'address' => {'23' => {'city' => 'Paris'}, '45' => {'city' => 'London'}}}}
</pre>
</div>
<p>Rails 知道所有的 <code>input</code> 皆屬於 <code>person</code> Hash,因為對 <code>person_form</code> 呼叫了 <code>fields_for</code>。透過指定 <code>:index</code> 選項 <code>index: address.id</code>,可以告訴 Rails,<code>input</code> 的 <code>name</code> 不要命名為 <code>person[address][city]</code>,而是在 <code>address</code> 與 <code>city</code> 之間插入索引值(放在中括號內)。通常這很有用,因為可以簡單的找出要修改的地址記錄是那個。<code>:index</code> 的值可以是其它有意義的屬性,字串,甚至是 <code>nil</code> 也可以(<code>nil</code> 會建立一個陣列參數出來)。</p><p>要產生更複雜的嵌套,可以明確指定 <code>input</code> <code>name</code> 的第一個部分(<code>person[address]</code>):</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= fields_for 'person[address][primary]', address, index: address do |address_form| %>
<%= address_form.text_field :city %>
<% end %>
</pre>
</div>
<p>建立出來的輸入:</p><div class="code_container">
<pre class="brush: xml; gutter: false; toolbar: false">
<input id="person_address_primary_1_city" name="person[address][primary][1][city]" type="text" value="bologna" />
</pre>
</div>
<p>一個通用的規則是,最後的 <code>input</code> <code>name</code> 是傳給 <code>fields_for</code> 或 <code>form_for</code> 的名字,加上索引值,再加上屬性名稱。也可以直接將 <code>:index</code> 選項傳給像是 <code>text_field</code> 的輔助方法,但這樣比較繁瑣,在表單構造器一起指定來減少重複。</p><p>忽略 <code>:index</code> 選項的簡寫是,在傳給 <code>form_for</code> 或 <code>fields_for</code> 的名稱後面加上一個中括號。這與指定 <code>index: address</code> 的效果相同:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= fields_for 'person[address][primary][]', address do |address_form| %>
<%= address_form.text_field :city %>
<% end %>
</pre>
</div>
<p>會產生與前例相同的輸出。</p><h3 id="送出至外部資源的表單">8 送出至外部資源的表單</h3><p>Rails 的表單輔助方法,也可以用來打造送出資料到外部資源的表單。但需要給資源指定一個 <code>authenticity_token</code>,可以使用 <code>:authenticity_token</code> 選項來指定:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token' do %>
Form contents
<% end %>
</pre>
</div>
<p>某些時候在送出資料到外部資源時,像是付款閘到。可以使用的欄位受外部 API 限制,還有可能不需要 <code>authenticity_token</code>,此時將 <code>:authenticity_token</code> 設為 <code>false</code> 即可:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= form_tag 'http://farfar.away/form', authenticity_token: false do %>
Form contents
<% end %>
</pre>
</div>
<p>同樣的技術 <code>form_for</code> 也適用:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %>
Form contents
<% end %>
</pre>
</div>
<p>不需要 <code>authenticity_token</code> 的情況:</p><div class="code_container">
<pre class="brush: ruby; html-script: true; gutter: false; toolbar: false">
<%= form_for @invoice, url: external_url, authenticity_token: false do |f| %>
Form contents
<% end %>
</pre>
</div>
<h3 id="打造複雜表單">9 打造複雜表單</h3><p>許多應用程式表單不僅是編輯單一物件這麼簡單。例如建立 <code>person</code> 時,可能想讓使用者(在同一個表單)建立出多筆地址記錄(住家地址、工作地址等)。之後在編輯 <code>person</code> 時,使用者應該要能夠新增、刪除或修改地址。</p><h4 id="model-部分">9.1 Model 部分</h4><p>Active Record 在 Model 層級提供這樣的支援,請用 <code>accepts_nested_attributes_for</code> 方法:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
has_many :addresses
accepts_nested_attributes_for :addresses
end
class Address < ActiveRecord::Base
belongs_to :person
end
</pre>
</div>
<p>會建出一個 <code>Person#addresses_attributes=</code> 方法,用來新建、更新與刪除地址。</p><h4 id="嵌套表單">9.2 嵌套表單</h4><p>下面的表單允許使用者用 <code>Person</code> 的實體來建立地址。</p><div class="code_container">
<pre class="brush: plain; gutter: false; toolbar: false">
<%= form_for @person do |f| %>
Addresses:
<ul>
<%= f.fields_for :addresses do |addresses_form| %>
<li>
<%= addresses_form.label :kind %>
<%= addresses_form.text_field :kind %>
<%= addresses_form.label :street %>
<%= addresses_form.text_field :street %>
...
</li>
<% end %>
</ul>
<% end %>
</pre>
</div>
<p>當關聯接受嵌套屬性時,<code>fields_for</code> 會對關聯的每個元素,執行 <code>fields_for</code> 的區塊。若 <code>person</code> 沒有地址,便不執行 <code>fields_for</code> 區塊。常見的做法是在 Controller 建一個或多個空的子元素,這樣只少有一組欄位會顯示給使用者。下例會在新建 <code>person</code> 的表單產生兩組地址欄位,</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
def new
@person = Person.new
2.times { @person.addresses.build}
end
</pre>
</div>
<p><code>fields_for</code> 給出一個表單構造器。參數的名稱要與 <code>accepts_nested_attributes_for</code> 指定的相同。舉個例子,建立有兩組地址的使用者,送出的參數看起來會像是:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
{
'person' => {
'name' => 'John Doe',
'addresses_attributes' => {
'0' => {
'kind' => 'Home',
'street' => '221b Baker Street'
},
'1' => {
'kind' => 'Office',
'street' => '31 Spooner Street'
}
}
}
}
</pre>
</div>
<p><code>:addresses_attributes</code> Hash 的鍵不重要,每個地址的鍵不要重複就好。</p><p>若關聯物件已經儲存了,<code>fields_for</code> 會自動產生一個隱藏輸入,<code>id</code> 是該記錄的 <code>id</code>。可以傳入 <code>include_id: false</code> 給 <code>fields_for</code> 來禁用這個行為。可能會想要禁止產生隱藏輸入,因為自動產生的輸入擺放的位置不對,導致 HTML 不合法;或者是使用的 ORM,子物件沒有 <code>id</code>。</p><h4 id="controller-部分">9.3 Controller 部分</h4><p>通常需要在傳給 Model 之前,先在 Controller <a href="action_controller_overview.html#strong-parameters">過濾參數</a>:</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
def create
@person = Person.new(person_params)
# ...
end
private
def person_params
params.require(:person).permit(:name, addresses_attributes: [:id, :kind, :street])
end
</pre>
</div>
<h4 id="移除物件">9.4 移除物件</h4><p>可以透過傳入 <code>allow_destroy: true</code> 給 <code>accepts_nested_attributes_for</code>,來允許使用者刪除關聯物件。</p><div class="code_container">
<pre class="brush: ruby; gutter: false; toolbar: false">
class Person < ActiveRecord::Base
has_many :addresses