Form Builder nasıl yapılır (VueJS ile)
VueJS ile (Jotform benzeri) form builder yapmanız gerektiğinde maalesef internette pek kaynak bulamayacaksınız, madem kaynak yok o halde beraber oluşturalım.
Bir form builder dört temel component’ten bir araya gelir Toolbox, Designer, Preview, Properties. Elbette ihtiyaca göre anlamlı en küçük yapıları component’leştirebiliriz. Ben bu complex yapıyı en kolay anlatabileceğim şekilde kodlamayı seçtim.
"dependencies": {
"remixicon": "^2.5.0",
"vue": "^3.2.45"
},
Bağımlılık olarak yalnızca icon paketi olan remixicon kullandım. Global state management’e dahi başvurmadım.
(bkz: Vuex olmasa da olur mu?)
Hadi başlayalım
Az sonra oluşturacağımız temel component’leri, App.vue içerisinde kullanacağız.
<div class="form-builder">
<div class="form-builder-header">
<h1>Form Builder</h1>
<div class="btn-group">
<button @click="toggleMode">{{ mode === 'designer' ? 'Preview' : 'Designer' }}</button>
</div>
</div>
<div class="form-builder-wrapper">
<Toolbox />
<div class="main">
<Designer v-if="mode === 'designer'" v-model="formBuilderJSON" @on-select-field="changeSelectedField" />
<Preview v-else v-model="formBuilderJSON" />
</div>
<Properties v-model="selectedField" />
</div>
</div>
Toolbox.vue component’i Görsel 1'de solda görünen araç kutusundan ibaret, bu kutunun kabiliyetleri şunlar olmalı. Form oluştururken kullanabileceğimiz tüm field türlerini listelemeli. Bu fieldlar kullanıcı tarafından sürükleme yoluyla alınmak istendiğinde event.dataTransfer’e set etmeli.
Tamam, bu durumda içerisinde onDrag fonksiyonu olan bir component işimizi görecektir.
<script setup>
import tools from '../composables/tools'
import Icon from './Icon.vue'
const onDrag = (e, tool) => {
e.dataTransfer.setData('text/plain', JSON.stringify(tool))
}
</script>
<template>
<div class="card toolbox">
<h4>Toolbox</h4>
<ul>
<li v-for="tool in tools" :key="tool.title" draggable="true" @dragstart="onDrag($event, tool)">
<span class="icon">
<Icon :name="tool.icon" />
</span>
<span class="title">
{{ tool.title }}
</span>
</li>
</ul>
</div>
</template>
İyi başladık. Toolbox’ımız hazır bile.
Şimdi biraz karmaşık olan Designer.vue component’imizi kodlayalım.
Designer, Preview ve Properties component’leri two way binding’e ihtiyaç duyuyor. Kullanıcının yapacağı her hareketten ilgili component’lerin haberdar olması gerekiyor. Bunu yapabilmeniz için parent component ile child component arasında two way binding ilişkisi kurmayı biliyor olmamız lazım. Endişelenmeyin onu da anlatmıştım zamanında
(bkz: Parent Component ile Child Component arasında v-model ilişkisi kurmak)
Designer component’imizden beklentilerimiz;
- Form ekranını satırlara ve sütunlara bölebilmeli
- Gerektiğinde satır ve sütunların title ve description bilgilerini güncelleyebilmeli
- Toolbox’tan sürüklenme yoluyla alınan field’lar sütunlara bırakıldığında field’ı temsil eden bir card oluşturabilmeli
- Gerektiğinde oluşturulan field’ları silebilmeli
- Satır ve sütunların yerlerini değiştirebilmeli
- Odaklanılan field’ın özellikleri değiştirilebilsin diye, designler üzerindeki field’lara tıklandığında (hiyerarşik olarak) bir üstteki component’i bilgilendirmeli (Bu kısım karmaşık görünebilir, Properties.vue component’ini yazarken daha anlaşılır hale gelecek)
Toolbox’taki onDrag fonksiyonuna benzer bir onDrop fonksiyonu yazalım.
const onDrop = (e, col) => {
const field = JSON.parse(e.dataTransfer.getData('text/plain'))
col.fields.push(field)
}
Bu fonksiyon herhangi bir sütunun üzerine bir field bırakıldığında çalışacak. Diğer kabiliyetleri nasıl kazandırdığımı component’i inceleyerek öğrebilirsiniz.
Designer component’imiz de hazır.
Sırada Preview.vue var
Bu component’ten beklentilerimiz şunlar;
- Designer component’inde oluşturulan satır ve sütunları göstermeli
- Bu sütunların içerisindeki field’ları render etmeli.
Preview componenti’nde VueJS yazarken pek başvurulmayan bir yönteme başvurmamız gerekiyor. HyperScript ile HTML element oluşturmak ve ekranda göstermek.
Yapması tarif etmekten daha kolay. VueJS içerisinde HyperScript gömülü geliyor. Nasıl kullanılacağı da dökümanda mevcut. Yine de kısaca Türkçeleştireyim.
h('div')
h fonksiyonuna parametre olarak bir HTML element’in ihtiyaç duyacağı prop’ları geçiyoruz. Aşağıdaki gibi.
h('div', { class: 'bar', innerHTML: 'hello' })
Form builder içerisindeki Preview.vue component’i ‘formBuilderJSON’ adlı bir JSON bekliyor. Bunu App.vue içerisinde reactive bir şekilde oluşturup göndermemiz gerekiyor. Kullanıcının oluşturacağı / oluşturduğu form ile ilgili tüm detaylar bu dinamik JSON içerisinde.
Preview component’i, kendisine verilen JSON’daki fieldları ‘renderer’ fonksiyonu ile ayıklayıp anlamlı çıktılar döndürüyor.
const renderer = (payload) => {
const { type, options, value, placeholder } = payload.field
switch (type) {
case 'text':
return h('input', { type, value, placeholder })
case 'checkbox':
return h('input', { type, value, placeholder })
case 'radio':
return h('input', { type, value, placeholder })
case 'datetime-local':
return h('input', { type, value, placeholder })
case 'email':
return h('input', { type, value, placeholder })
case 'number':
return h('input', { type, value, placeholder })
case 'password':
return h('input', { type, value, placeholder })
case 'textarea':
return h('textarea', { value, placeholder })
case 'select':
return h('select', options.map((option) => h('option', option.label)))
default:
return h('div', 'Unknown field type')
}
}
Bu fonksiyona form builder’ın motoru diyebiliriz. Tüm fieldları özellikleriyle beraber oluşturan ekrana basan fonksiyon bu. İçerisindeki case sayısını artırarak dilediğiniz kadar yaratıcı form elementleri oluşturabilirsiniz. Ben daha az karmaşık olması nedeniyle sınırlı sayıda element örneği verdim.
Renderer’e kardeş bir fonksiyon daha oluşturup bununla Condition Management yapılabilir. Yani A input’u doluysa B inputu görünür yap gibi kabiliyetler kazandıracaksak buralarda yapmalıyız. Anlatıyı karmaşıklaştırmamak için bu kabiliyeti eklemedim.
Vuhuu en karmaşık component’i yazdık bitirdik. Bizden korkulur :)
Sırada Properties.vue var
Bu component’ten beklentilerimiz;
- Designer içerisinde üzerine tıklanmış (seçilmiş) field’la ilgili bilgileri gösterecek
- Ve bu bilgileri değiştirmemize olanak verecek
Preview.vue’yu yazmış biri için bu çocuk oyuncağı. App.vue’ya bakarak kendinize yine two way binding yapmamız gerektiğini hatırlatabilirsiniz. selectedField adlı object’i App.vue ile Properties.vue arasında getirip götürmemiz lazım. Böylece kullanıcı field’la ilgili yaptığı değişikliği anlık olarak Preview modunda görüntüleyebilecek.
Properties.vue içerisinde elimizdeki field’ın ayarlarını yapabileceği elementleri oluşturalım.
<div class="settings">
<h4>Field properties</h4>
<div class="propertie">
<div class="propertie-header">
<div class="icon-and-title">
<Icon size="30px" :name="field.icon" :key="field.icon" />
<input type="text" v-model="field.title">
</div>
<div class="id">{{ field.id }}</div>
</div>
<div class="propertie-content">
<div class="propertie-row">
<div class="type" v-if="isInput(field.type)">
<label for="type">Type</label>
<select name="type" id="type" v-model="field.type">
<option value="text">Text</option>
<option value="number">Number</option>
<option value="email">Email</option>
<option value="password">Password</option>
<option value="datetime-local">Datetime</option>
</select>
</div>
<div class="value" v-if="isInput(field.type) || field.type === 'textarea'">
<label for="value">Value</label>
<input type="text" v-model="field.value">
</div>
<div class="placeholder" v-if="isInput(field.type) || field.type === 'textarea'">
<label for=" placeholder">Placeholder</label>
<input type="text" v-model="field.placeholder">
</div>
<div class="options" v-if="field.type === 'select'">
<label for="options">Options</label>
<div class="option" v-for="(option, optionIndex) in field.options" :key="option.id">
<input type="text" v-model="option.label">
<button class="btn" @click="onRemoveOption(field.options, optionIndex)">
<Icon name="close" />
</button>
</div>
<button class="btn" @click="onAddOption(field.options)">
<Icon name="add" />
Add Option
</button>
</div>
</div>
</div>
</div>
</div>
Kullanıcının seçtiği elementin arka plan rengini ve/veya yazı rengini değiştirebilmesini istiyorsak “color” type’lı inputlar da ekleyelim.
<div class="design">
<h4>Design properties</h4>
<div class="propertie">
<div class="background-color">
<label for="background-color">Background color</label>
<input type="color" v-model="field.style.backgroundColor">
</div>
</div>
<div class="propertie">
<div class="color">
<label for="color">Font color</label>
<input type="color" v-model="field.style.color">
</div>
</div>
</div>
Hepsi bu kadar. Artık VueJS ile yazılmış bir form builder’ımız var.
GitHub Repo: https://github.com/gayret/form-builder-vuejs