Multistep Form (Wizard) — Resep Rel
Anatomi Bentuk Multistep
Bentuk multistep adalah varian dari wizard. Wizard adalah alat kegunaan. Ini merupakan tugas sebagai satu set langkah diatur dalam urutan yang ditentukan . Pengguna harus menyelesaikan setiap langkah dalam urutan yang diberikan untuk mencapai tujuannya.
Formulir bertingkat membagi formulir kompleks menjadi bagian-bagian yang lebih kecil, mengelompokkan bidangnya menurut kriteria yang berbeda . Ini menghasilkan pengalaman pengisian formulir yang tidak terlalu menakutkan bagi pengguna akhir. Saat digabungkan dengan instruksi, formulir multilangkah secara signifikan meningkatkan kemudahan penggunaan aplikasi kami .
Kelemahan utama mereka adalah efisiensi . Mengisi formulir multi-langkah biasanya membutuhkan waktu lebih lama daripada mengisi formulir tunggal (jika Anda sudah terbiasa dengan prosesnya). Ini karena navigasi antar langkah, yang tidak ada dalam bentuk standar.
Cara Anda menerapkan navigasi ini dapat memengaruhi pengalaman pengguna secara signifikan : apakah Anda akan mengizinkan pengguna menavigasi bolak-balik ke langkah mana pun dalam wisaya atau hanya ke langkah berikutnya dan sebelumnya? apakah informasi langkah sebelumnya terlihat di langkah saat ini? Pilihan desain yang buruk dapat menyebabkan pengalaman pengisian formulir yang lambat dan membuat frustrasi sehingga pengguna enggan mengisi formulir kami dan bahkan menggunakan sistem kami.
Resep ini akan mengajari Anda cara membuat formulir multilangkah yang dapat digunakan kembali dan disesuaikan di Rails.
Tantangan
Dalam aplikasi kami, kami mengelola Projects. Sebuah proyek didefinisikan oleh a namedan a description. A Projectdapat memiliki banyak Booksyang terkait dengannya. A Bookdidefinisikan oleh a titledan a description. Kami ingin menerapkan formulir multilangkah untuk membuat file Projects. Langkah pertama akan memerlukan informasi dasar proyek, dan yang kedua, opsional, informasi tentang yang Bookterkait dengan Project.
Resep
Prasyarat
- (Wajib) Rails
ViewComponentgem diinstal :ViewComponentmembungkus Railsviewsmenjadi objek. Hal ini memudahkan untuk menguji pandangan kami dan memberikan metode tambahan untuk pengalaman pengembang yang lebih baik saat bekerja dengannya. - Plugin (Opsional) diinstal : kerangka kerja ini menghilangkan kebutuhan untuk menulis dalam kode kami dengan menyediakan satu set kelas yang kuat.
TailwindCSSPostCSSCSSCSSCSS
Di Rails, kami menggunakan formulir untuk membuat dan memperbarui catatan. Operasi ini dilakukan melalui POST, PUT, dan PATCH HTTPpermintaan dikirimkan dari satu formulir. Permintaan diproses mengikuti proses yang sama:
- Lapisan Rails
Controllermem-parsing data permintaan formulir dan memintaModellapisan untuk melakukan operasi yang diminta dengan informasi yang diekstraksi. - Lapisan Rails
Modelmemproses data permintaan, memvalidasinya, dan melakukan operasi yang diminta jika valid. Hasil operasi dikirim kembali dariModelkeController. - Layer Rails
ControllermemintaViewlayer untuk memberikan respons berdasarkan hasil operasi. Operasi yang berhasil membuat tampilan hasil operasi. Operasi yang gagal merender formulir yang dikirimkan dengan data permintaan formulir dan pesan kesalahan.
Tugas pertama kita adalah mengimplementasikan logika untuk memvalidasi apakah semua langkah dalam bentuk multilangkah sudah selesai.
Rails ActiveRecord::Validations API , termasuk dalam ApplicationRecord Modelkelas, mendefinisikan valid?metode. Metode ini dijalankan pada setiap operasi pembuatan dan pembaruan . Itu mengevaluasi semua Modelkondisi validasi, kembali trueketika semua validasi lulus. Hanya valid?operasi yang dapat mempertahankan informasi dalam database.
Validasi untuk formulir multilangkah kami harus memeriksa indeks langkah saat ini dalam permintaan formulir sesuai dengan langkah terakhirnya . Kami memerlukan dua atribut untuk melacak variabel ini: current_stepuntuk menyimpan indeks langkah saat ini dan total_stepsuntuk menyimpan jumlah langkah dalam bentuk multilangkah. Karena validasi dan atributnya sama untuk semua model, kita dapat merangkumnya dalam a concernyang dapat digunakan kembali di Modelkelas kita:
# app/models/concerns/multistep_form_model.rb
module MultistepFormModel
ERROR_ATTRIBUTE = :current_step
ERROR_DETAIL = :incompleted_multistep_form
extend ActiveSupport::Concern
included do
# Defines two new attributes in including Model class
attr_accessor :current_step, :total_steps
# Adds new validation rule in including Model class
validate :all_multistep_form_steps_completed
end
private
def all_multistep_form_steps_completed? = current_step == total_steps
def all_multistep_form_steps_completed
errors.add(ERROR_ATTRIBUTE, ERROR_DETAIL) unless all_multistep_form_steps_completed?
end
end
# app/models/concerns/multistep_form_model.rb
module MultistepFormModel
...
included do
# Adds new validation rule in including Model class
validate :all_multistep_form_steps_completed,
if: -> { current_step.present? && total_steps.present? }
end
...
end
Terakhir, kita dapat memperbarui model kita dengan menyertakan formulir multilangkah kita concernsehingga kita dapat menggunakannya dengan formulir multilangkah kita:
# app/models/project.rb
class Project < ApplicationRecord
...
# Now our Project class has current_step and total_steps attributes, and the multistep validation rule
include MultistepFormModel
...
end
Controllersbertanggung jawab untuk mem-parsing data permintaan formulir dan meneruskannya ke kelas Model. Untuk bekerja dengan formulir multilangkah, kita perlu memperbarui parameter kuat pengontrol untuk memastikan parameter curret_stepdan total_stepsdiuraikan dan diteruskan ke Modelkelas.
# app/controllers/projects_controller.rb
class ProjectsController < ApplicationController
...
private
...
def project_params
params
.require(:project)
.permit(:name, :description, :total_steps, :current_step, books_attributes: [:title, :description])
end
...
end
Mari kita uraikan formulir multi-langkah dalam ViewComponents. Bentuk multistep adalah bentuk yang dibagi menjadi beberapa bagian (langkah). Dalam hal komponen, ini dapat dimodelkan sebagai MultistepFormComponentkonten yang terdiri dari beberapa StepComponents.
Setiap langkah berisi sekumpulan bidang formulir. Sebuah langkah selesai ketika semua bidang yang diperlukan langkah diisi dengan data yang valid. A StepComponentmerender bidang yang sesuai dengan langkah formulir multilangkah dan menyediakan antarmuka sederhana untuk mengetahui, mengingat data formulir, jika langkah yang diwakilinya selesai .
Kami sebelumnya berbicara tentang valid?metode dan bagaimana menentukan apakah data formulir itu valid. Selain perilaku ini, perbarui juga objek dengan kamus yang berisi kesalahan validasi . Itu dapat diakses melalui , yang mengembalikan objek. valid?Modelmodel.errorsActiveModel::Errors
Untuk menentukan apakah a StepComponentselesai, kita dapat menggunakan form.object.errorsobjek. Diberi langkah dengan satu set bidang masukan, kami dapat mengonfirmasi bahwa langkah tersebut selesai jika objek form.object.errorstidak menyertakan kesalahan apa pun yang terkait dengan bidang langkah.
# How to check whether a step is completed.
def completed?(form_object)
input_attributes.none? { |attr| form_object.errors.key?(attr) }
end
- Objek
ActionView::Helpers::FormBuilderuntuk merender bidang langkah dan mengambil data formulir melalui.objectmetode. - Daftar bidang
input_attributesyang dirender. - Tidak terungkap dari metodenya tetapi yang jelas, kita juga perlu mengetahui indeks langkah dalam bentuk multistep.
# app/components/common/multistep_form_component/step_component.rb
module Common
class MultistepFormComponent::StepComponent < ApplicationComponent
class << self
def title = raise "StepComponent #{name} does not define title method"
def input_attributes = raise "StepComponent #{name} does not define list of input attributes"
def completed?(form_object)
input_attributes.none? { |attr| form_object.errors.key?(attr) }
end
end
attr_reader :multistep_component, :index, :form
def initialize(multistep_component:, index:, html_attributes: {})
@index = index
@form = multistep_component.form
@multistep_component = multistep_component
super(html_attributes:)
end
def call
raise "
#{self.class.name} is a StepComponent. StepComponents are abstract by default.
Components inheriting from it must define its own template or call method.
"
end
def current_step? = index == multistep_component.current_step
def completed? = self.class.completed?(multistep_component.form.object)
end
end
Ini StepComponentadalah komponen abstrak . Itu tidak dirancang untuk dirender tetapi diperluas oleh StepComponentkelas lain. Untuk Projectlangkah formulir kami, kami akan menentukan dua komponen baru, masing-masing untuk setiap langkah yang ingin kami render:
# app/components/common/projects/form_component/project_details_component.rb
module Common
class Projects::FormComponent::ProjectDetailsComponent < Common::MultistepFormComponent::StepComponent
class << self
def title = "Project Details"
def input_attributes = %w[name]
end
...
end
end
# app/components/common/projects/form_component/project_deliverables_component.rb
module Common
class Projects::FormComponent::ProjectDeliverablesComponent < Common::MultistepFormComponent::StepComponent
class << self
def title = "Project Deliverables"
# Example of a step component with nested attributes / nested form fields
def input_attributes = %w[books.title]
end
...
end
end
- Di
URLmana formulir data akan diserahkan. - Dan bentuk data itu sendiri. Data ini mewakili catatan yang ingin kami buat atau perbarui. Dalam setiap iterasi dari siklus formulir multilangkah, data akan mewakili informasi yang diberikan oleh pengguna dalam pengiriman formulir sebelumnya.
# app/components/common/multistep_form_component.rb
module Common
class MultistepFormComponent < ApplicationComponent
class << self
def steps = raise "#{name} MultistepFormComponent does not define steps class model"
end
delegate :steps, to: :class
attr_reader :form_url, :back_url, :model, :form
def initialize(form_url:, back_url:, model:, html_attributes: {})
@form_url = form_url
@back_url = back_url
@model = model
super(html_attributes:)
end
def call
form_with(url:, model: model) do |form|
@form = form
concat(form.hidden_field(:total_steps, value: steps.count))
concat(form.hidden_field(:current_step, value: current_step_index))
steps.each_with_index do |step, index|
concat(render step.new(multistep_form: self, index:))
end
end
end
end
end
Kita sekarang dapat mendefinisikan proyek kita FormComponentdengan memperluas kelas dan menentukan langkah StepComponentkelas yang menentukan bentuknya:
# app/components/common/projects/form_component.rb
module Common
class Projects::FormComponent < Common::MultistepFormComponent
class << self
def steps = [ProjectDetailsComponent, ProjectDeliverablesComponent]
end
end
end
Ada beberapa cara untuk bernavigasi di antara langkah-langkah: beberapa hanya mengizinkan navigasi maju sampai semua langkah selesai; di tempat lain, dimungkinkan untuk menavigasi bolak-balik satu langkah dan satu waktu, dan yang lain bahkan memungkinkan menampilkan beberapa langkah sebelumnya sekaligus. Bentuk multilangkah kita harus cukup fleksibel untuk mengimplementasikan semua kemungkinan strategi navigasi . Namun, seharusnya tidak menerapkan strategi navigasi apa pun. Kami akan mendelegasikannya ke komponen yang memperluas MultistepFormComponentkelas.
Kami dapat menyembunyikan dan menampilkan langkah-langkah menggunakan input checkboxdan sederhana radio HTML. A checkboxadalah kotak sederhana yang dapat checked. A radioadalah grup checkboxesdi mana hanya satu yang bisa checked. CSS dapat menargetkan status yang diperiksa dari elemen ini melalui :checkedpemilih. Ini memungkinkan HTMLelemen gaya berdasarkan status a checkboxatau radio.
Kita dapat menetapkan a radiodan checkboxinput ke setiap langkah dalam formulir kita dan memberi gaya pada langkah tersebut sesuai dengan statusnya sehingga hanya terlihat saat input yang sesuai dicentang. Kami akan menentukan metode baru untuk membungkus setiap instance langkah dalam tag yang berisi radio dan kotak centang untuk mencapainya.
# app/components/common/multistep_form_component.rb
...
def call
...
steps.each_with_index do |step, index|
concat(step_wrapper(step, index) { render step.new(multistep_form: self, index:) })
end
...
end
def radio_id(step_index) = "#{object_id}_radio_#{step_index}"
def checkbox_id(step_index) = "#{object_id}_radio_#{step_index}"
def step_wrapper(step, index, html_attributes: {}, &content)
tag.div(**html_attributes) do
concat(
radio_button_tag(
radio_id(nil),
1,
current_step_index == index,
id: radio_id(index),
class: "hidden peer/radio"
)
)
concat(
check_box_tag(
checkbox_id(index),
1,
current_step_index == index,
id: checkbox_id(index),
class: "hidden peer/checkbox"
)
)
concat(capture(&content))
end
end
...
Komponen yang memperluas MultistepFormComponentdapat memperbarui metode ini mendefinisikan elemen umum yang ada di semua langkah dan menggunakan peersuntuk menampilkan dan menyembunyikan langkah berdasarkan checkedstatus.
# app/components/common/projects/form_component.rb
...
def step_wrapper(step, index, html_attributes: {}, &content)
selected = index == current_step_index
options = (selected ? { "aria-current": "step" } : {})
super(step, index, html_attributes: options) do
concat(
tag.label(
for: index <= current_step_index ? radio_id(index) : "",
class: "
w-full inline-flex items-center px-4 py-2 gap-4 border-2 text-zinc-500 border-zinc-500
peer-checked/radio:bg-zinc-500 peer-checked/radio:text-white
"
) do
concat(tag.p(index + 1, class: "h-6 w-6 text-center rounded-full text-white bg-zinc-500"))
concat(tag.p(step.title))
end
)
concat(
tag.div(
class: "
hidden px-4 py-6
peer-checked/radio:block
"
) do
concat(capture(&content))
concat(
tag.div(class: "mt-5 w-full inline-flex gap-2 items-center justify-end") do
concat(link_to("cancel", back_url, class: "px-4 py-2 bg-zinc-500 text-white")) if index == 0
concat(tag.label("previous", for: radio_id(index - 1), class: "px-4 py-2 bg-zinc-500 text-white")) if index > 0
concat(form.submit("next", class: "cursor-pointer px-4 py-2 bg-zinc-500 text-white")) if index < (steps.count - 1)
concat(form.submit("create project", class: "cursor-pointer px-4 py-2 bg-zinc-500 text-white")) if index == (steps.count - 1)
end
)
end
)
end
end
...
Pada baris 31 terdapat logika untuk menavigasi langkah mundur pada form. Kami menggunakan MultistepFormComponent radio_idmetode untuk memetakan label ke radioinput dari langkah sebelumnya. Saat kita mengklik label, radiotombol akan dicentang. Ini akan menyembunyikan langkah saat ini dan mengungkapkan yang sebelumnya.
Untuk menavigasi ke depan, kami selalu perlu mengirimkan formulir. Jika, setelah pengiriman formulir, semua langkah sebelumnya selesai, multistep akan merender langkah berikutnya. Jika tidak, formulir akan mundur ke langkah yang belum selesai.
# app/components/common/multistep_form_component.rb
...
def first_incompleted_step_index
steps.find_index { |s| !s.completed?(model) }
end
def current_step_index
previous_step = model.current_step&.to_i
@current_step_index =
if model.current_step.nil?
0
else
next_step = previous_step + (steps[previous_step].completed?(model) ? 1 : 0)
[next_step, first_incompleted_step_index].compact.min
end
end
...
# app/components/common/multistep_form_component.rb
...
def step_wrapper(step, index, html_attributes: {}, &content)
tag.div(**html_attributes) do
concat(
radio_button_tag(
...
# If true radio is checked
current_step_index == index,
...
)
)
concat(
check_box_tag(
...
# If true checkbox is checked
current_step_index == index,
...
)
)
concat(capture(&content))
end
end
...
Kami akan menambahkan metode baru yang bertanggung jawab untuk menghapus semua pesan kesalahan yang terkait dengan langkah-langkah yang tidak dikunjungi pengguna dan satu lagi untuk menghapus kesalahan current_step. Untuk mencapainya, kami akan menambahkan atribut ketiga yang bertanggung jawab untuk melacak langkah terbaru dalam formulir yang diisi pengguna sehingga kami dapat menentukan langkah mana yang belum dikunjungi.
# app/components/common/multistep_form_component.rb
...
def call
...
concat(form.hidden_field(:total_steps, value: steps.count))
concat(form.hidden_field(:latest_step, value: latest_step_index))
concat(form.hidden_field(:current_step, value: current_step_index))
...
end
end
...
def latest_step_index
@latest_step_index =
if model.latest_step.nil?
0
else
[current_step_index, model.latest_step].max
end
end
...
Ada dua elemen kunci untuk menguji formulir multilangkah kami: Modelperhatian kami dan MultistepFormComponent. Pengujian unit dan sistem tambahan untuk pengontrol, komponen proyek, dan seluruh aliran pengguna dalam pembuatan proyek juga penting. Namun, mereka tidak melibatkan konfigurasi khusus seperti yang lainnya.
Untuk menguji MultistepFormModelperhatian, kita mungkin tergoda untuk menguji current_stepvalidasi melalui Projectmodel. Ini bukan praktik yang baik karena dapat menyebabkan pengujian yang tidak pasti: kami tidak boleh menguji masalah melalui Modelshal yang mungkin tidak menyertakan atau memperluasnya di masa mendatang.
Sebagai gantinya, kita dapat mendefinisikan Modelkelas tambahan dengan satu-satunya tujuan untuk menguji masalah tersebut, dan kemudian menjalankan pengujian dengannya. Pendekatan ini memiliki dua manfaat: menguji kepedulian tanpa mengandalkan Modelkelas dari logika bisnis kami dan mendokumentasikan bagaimana kepedulian dapat digunakan di Modelkelas kami.
class MultistepFormModelTest < ActiveSupport::TestCase
class ValidMultistepFormMode
include ActiveModel::Model
include MultistepFormModel
end
def setup
@model = ValidMultistepFormMode.new
end
# Validations
# -----------
test "MultistepFormModel does not apply step validation if total and current attibutes are not specified" do
assert_nil @model.current_step
assert_nil @model.total_steps
assert @model.valid?
end
test "MultistepFormModel is valid only when total and current steps are the same" do
@model.current_step = 1
@model.total_steps = 2
refute @model.valid?
@model.current_step = 3
@model.total_steps = 3
assert @model.valid?
end
test "MultistepFormModel sets custom error attribute and detail when total and current step attributes are invalid" do
@model.current_step = 1
@model.total_steps = 2
refute @model.valid?
assert_equal @model.errors.first.attribute, MultistepFormModel::ERROR_ATTRIBUTE
assert_equal @model.errors.first.detail, MultistepFormModel::ERROR_DETAIL
end
end
class Common::MultistepFormComponentTest < ViewComponent::TestCase
class SampleModel
include ActiveModel::Model
include MultistepFormModel
attr_accessor :attribute_1, :attribute_2, :attribute_3
validates_presence_of :attribute_1
validates_presence_of :attribute_2
validates_presence_of :attribute_3
end
class TestMultistepFormComponent < Common::MultistepFormComponent
class << self
def steps = [FirstTestStepComponent, SecondTestStepComponent, ThirdTestStepComponent]
end
end
class FirstTestStepComponent < Common::MultistepFormComponent::StepComponent
class << self
def title = "First Step"
def input_attributes = %i[attribute_1]
end
def call
tag.div(**wrapper_attributes)
end
end
class SecondTestStepComponent < Common::MultistepFormComponent::StepComponent
class << self
def title = "Second Step"
def input_attributes = %i[attribute_2]
end
def call
tag.div(**wrapper_attributes)
end
end
class ThirdTestStepComponent < Common::MultistepFormComponent::StepComponent
class << self
def title = "Third Step"
def input_attributes = %i[attribute_3]
end
def call
tag.div(**wrapper_attributes)
end
end
def setup
@component = TestMultistepFormComponent
end
test "Renders multistep form" do
render_inline(@component.new(form_url: "test", back_url: "test", model: SampleModel.new))
assert_selector(".#{@component.component_class}")
end
test "Renders multistep form steps" do
render_inline(@component.new(form_url: "test", back_url: "test", model: SampleModel.new))
assert_selector(".#{FirstTestStepComponent.component_class}", visible: :all)
assert_selector(".#{SecondTestStepComponent.component_class}", visible: :all)
assert_selector(".#{ThirdTestStepComponent.component_class}", visible: :all)
end
test "Selects first step as current step if current_step value is nil" do
model = SampleModel.new
model.valid?
result = @component.new(form_url: "test", back_url: "test", model:).current_step_index
assert_equal 0, result
end
...
end
Multisteps di Rails digabungkan dengan erat ke controller dan layer model. Untuk menjaga logika bisnis kami sedapat mungkin terpisah dari komponen ini, kami mengandalkan concernsdan komponen abstrak. Alat-alat ini berfungsi sebagai logika plug & play yang membuatnya sangat mudah untuk menggunakan beberapa langkah di bagian mana pun dari aplikasi kita.
Sifat abstrak MultistepFormComponentmenyederhanakan tugas mengimplementasikan formulir dengan desain dan logika navigasi yang berbeda, dengan mendelegasikan implementasi logika ini ke komponen yang memperluasnya.
Terakhir, dengan menulis beberapa pengujian umum yang mengandalkan pengujian Modeldan Componentkelas agnostik, kami memastikan fungsi multilangkah yang benar di seluruh aplikasi kami dan memberikan contoh terperinci dan terdokumentasi dengan baik tentang bagaimana pengembang dapat menggunakan logika multilangkah di masa mendatang.
Kode di Github
- 140 formulir multilangkah oleh AlbertoHdezCerezo · Tarik Permintaan #150 · Albert-Markdown-Editor/Albert
- How-to: Buat formulir yang dapat diakses — Proyek A11Y
- Dalam formulir dengan beberapa langkah, identifikasi langkah saat ini menggunakan aria-current
Jika Anda menemukan diri Anda tersesat di titik mana pun dalam artikel atau mencoba meniru solusi saya tanpa hasil, Anda dapat menghubungi saya di bagian komentar. Saya akan dengan senang hati memberikan informasi tambahan dan mencoba mengklarifikasi pertanyaan terbuka. Segera saya akan menerapkan saluran tambahan untuk berbagi umpan balik dan pertanyaan.
Jika Anda telah mencapai titik ini, terima kasih atas waktu dan kesabaran Anda. Saya harap Anda menemukan tutorial saya bermanfaat!

![Apa itu Linked List? [Bagian 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































