En pratik Wizard Pattern nasıl kodlanır, VueJS’te çoklu adımlı form yönetimi
Çok sık olmasa da birden çok adımdan oluşan formlar geliştirmemiz gerekebiliyor. Ne zaman bu sorunu çözen bir kod okusam hep development sürecinde kafaların karıştığına tanık oluyorum. Peki bu konunun en pratik çözümü nedir, gelin birlikte inceleyelim.
Bu makale ile ortaya koyacağım yönteme açıkçası pek çok rastlamıyorum. Yani bu genel geçer kabul görmüş bir yöntem değil. Ama kesinlikle sorunsuz, stabil ve bakımı kolay bir yöntem, neden yaygınlaşmıyor bilemiyorum.
Neden VueJS
ReactJS ile form yönetimi hakkında çok sayıda kaynak var. VueJS için böyle bir boşluk olduğunu fark ettiğim için yazma gereği duydum, dolayısıyla kod parçacıkları VueJS 3 Composition API standartlarında olacak. Ancak okuduğunuzda fark edeceksiniz ki, bunu herhangi frontend framework’ün de clone’lamak oldukça kolay.
Hadi başlayalım
Öncelikle çoklu adımlı olsun ya da olmasın formun elemanları bu elamanların nitelikleri (attributes) ve hatta gereklilikleri (validations, conditions) backendden gelmeli.
Bakın backend developer yazmalı demiyorum, backendden gelmeli. Frontend developer’ın fazla düşünmeyeni makbuldür. Frontend düşünmeye başlarsa işler karışabilir. Hangi input’u formun neresinde, nasıl görmek istiyorsanız bunu bir JSON ile sabitleyin, mutabık olun. Beklediğiniz UI/UX çıktısını da figma, sketch gibi bir uygulamayla mockup olarak frontend developer’a iletin.
Kodlamaya geçelim
steps.js
adında bir dosya oluşturalım ve içerisinde formun adımlarını ve bu adımlardaki elemanları açıkça belirtelim. (Bu demonun daha fazla karmaşıklaşmaması için input’ların gereklilikleri (validations) konusa değinmedim, o başka bir yazının konusu olsun)
// steps.js
import { reactive } from "vue"
const steps = reactive([
{
"id": "step1",
"number": "1",
"title": "Personal Information",
"buttonLabel": "Next",
"fields": [
{
"label": "First Name",
"type": "text",
"placeholder": "Enter your first name",
"key": "firstName",
"value":""
},
{
"label": "Last Name",
"type": "text",
"placeholder": "Enter your last name",
"key": "lastName",
"value": ""
},
{
"label": "Date of Birth",
"type": "date",
"key": "dateOfBirth",
"value": ""
},
{
"label": "Email",
"type": "email",
"placeholder": "Enter your email address",
"key": "email",
"value": ""
}
]
},
{
"id": "step2",
"number": "2",
"title": "Education Details",
"buttonLabel": "Next",
"fields": [
{
"label": "Graduated School",
"type": "text",
"placeholder": "Enter the name of the school you graduated from",
"key": "graduatedSchool",
"value": ""
},
{
"label": "Major",
"type": "text",
"placeholder": "Enter your major",
"key": "major",
"value": ""
},
{
"label": "Graduation Date",
"type": "date",
"key": "graduationDate",
"value": ""
},
{
"label": "Degree",
"type": "text",
"placeholder": "Enter your degree (e.g. Bachelor's, Master's)",
"key": "Degree",
"value": ""
}
]
},
{
"id": "step3",
"number": "3",
"title": "Work Experience",
"buttonLabel": "Complete",
"fields": [
{
"label": "Last Company",
"type": "text",
"placeholder": "Enter the name of your last company",
"key": "lastCompany",
"value": ""
},
{
"label": "Position",
"type": "text",
"placeholder": "Enter your position at the company",
"key": "position",
"value": ""
},
{
"label": "Duration of Employment",
"type": "text",
"placeholder": "How long did you work there?",
"key": "durationOfEmployment",
"value": ""
},
{
"label": "Responsibilities",
"type": "textarea",
"placeholder": "Describe your responsibilities and duties in this role",
"key": "responsibilities",
"value": ""
}
]
}
])
export default steps
Gördüğünüz gibi burada her şey çok açık. Yeni mezun bir yazılımcı bile bir bakışta nasıl bir form üretileceğini anlayabilir.
Sorumlulukları doğru dağıtalım
Üreteceğimiz bu çoklu adımlı formu generic bir component olarak inşa edelim, böylece projenin başka yerlerinde farklı input’larla aynı akışı üretmemiz gerektiğinde bu generic component’i kullanabilelim.
Bunun için sorumlulukları doğru dağıtmamız lazım.
Projenin içerisinde <MultipleStepsForm />
şeklinde componenti kullandığımızda two way binding ile, bu component’i import ettiğimiz seviyede, formun sonucunu alabilmeliyiz.
Yani <MultipleStepsForm v-model='formData' />
gibi kullanmalıyız. Buradaki formData, MultipleStepsForm adlı component tarafından doldurulacak.
// App.vue
<script setup>
import { ref } from 'vue';
import MultipleStepsForm from './components/MultipleStepsForm/MultipleStepsForm.vue'
const formData = ref([])
</script>
<template>
<MultipleStepsForm v-model="formData"/>
<section class="result-as-json">
<strong>Result as JSON</strong>
<pre>{{ formData }}</pre>
</section>
</template>
Süper, artık MultipleStepsForm
component’ini yazmaya başlayabiliriz.
// MultipleStepsForm.vue
<script setup>
// Depends
import steps from '../../data/steps'
import { ref } from 'vue'
const modelValue = defineModel({type: Object})
// Components
import Step1 from './steps/Step1.vue'
import Step2 from './steps/Step2.vue'
import Step3 from './steps/Step3.vue'
import Completed from './steps/Completed.vue'
const activeStepId = ref('step1') // 'step1', 'step2', 'step3', 'completed'
const setActiveStepId = id => activeStepId.value = id
const onCompletedForm = () => {
const payload = steps.map((step) => {
return {
id: step.id,
fields: step.fields.map((field) => {
return {
key: field.key,
value: field.value
}
})
}
})
modelValue.value = payload // Parent component'e form verisi gönder
setActiveStepId('completed') // Tebrikler sayfasını aç
}
</script>
<template>
<section class="multiple-steps-form">
<header v-if="activeStepId !== 'completed'">
<h1>Please fill out this form completely to apply for a job</h1>
<p>We wish you good luck.</p>
</header>
<section v-if="activeStepId !== 'completed'" class="steps-number">
<ul>
<li v-for="step in steps" :key="step.id" :class="{active: step.id === activeStepId}"
@click="activeStepId = step.id">
{{ step.number }}
<small>{{ step.title }}</small>
</li>
</ul>
</section>
<section class="active-step">
<Step1 v-if="activeStepId === 'step1'" @step-is-completed="setActiveStepId('step2')" />
<Step2 v-if="activeStepId === 'step2'" @step-is-completed="setActiveStepId('step3')" @go-previous-step="setActiveStepId('step1')" />
<Step3 v-if="activeStepId === 'step3'" @step-is-completed="onCompletedForm" @go-previous-step="setActiveStepId('step2')" />
<Completed v-if="activeStepId === 'completed'" />
</section>
</section>
</template>
<style scoped>
.multiple-steps-form {
max-width: 1000px;
margin: auto;
border: 1px solid lightgray;
padding: 1rem;
border-radius: .5rem;
& .active-step {
padding-inline: 6rem;
}
& .steps-number ul {
all: unset;
list-style: none;
display: flex;
justify-content: space-around;
align-items: center;
background-color: #eaeaea;
padding: .3rem;
border-radius: 2rem;
& li {
display: flex;
flex-direction: column;
text-align: center;
&.active {
background-color: #f3f3f3;
padding: 1rem;
border-radius: 2rem;
}
& small {
font-size: .5rem;
}
}
}
}
</style>
// Step1.vue
<script setup>
import steps from '../../../data/steps'
const step = steps.find((step) => step.id === 'step1')
const emits = defineEmits(['step-is-completed'])
const onNext = () => {
// if u need validation, add here
emits('step-is-completed')
}
</script>
<template>
<header><h2>{{ step.title }}</h2></header>
<main>
<section class="field" v-for="field in step.fields" :key="field.label">
<label :for="field.label">{{ field.label }}</label>
<input :id="field.label" :type="field.type" v-model="field.value" :placeholder="field.placeholder">
</section>
</main>
<footer class="actions">
<button @click="onNext">{{ step.buttonLabel }}</button>
</footer>
</template>
<style scoped>
main {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 1rem;
}
.field {
border: 1px solid rgb(239, 239, 239);
padding: 1rem;
display: flex;
flex-direction: column;
}
.actions {
padding-block: 1rem;
text-align: right;
}
</style>
Diğer Step component’lerinin de içeriği aynı. Burada da generic bir yapıya gidilebilir ama ben böyle kalmasını tercih ettim.
Bitti
Evet, hepsi bu kadar, tüm projemizde gönül rahatlığıyla kullanabileceğimiz tertemiz bir formumuz oldu.
Live Demo: https://multiple-steps-form-wizard.netlify.app/
GitHub: https://github.com/gayret/multiple-steps-form-wizard-pattern