FormBuilder

SkForm, FB ile üretilen fluent konfigürasyona göre dinamik formlar oluşturur. PrimeVue bağlantısını, definition yüklemeyi, bağımlı select'leri, dosya yüklemeyi ve yetki tabanlı salt-okunur modu kendi yönetir — sayfa katmanı ince bir render katmanı olarak kalır.

İmportlar

import { FB } from '@lvntr/components/FormBuilder/core';
import SkForm from '@lvntr/components/FormBuilder/SkForm.vue';

Temel Kullanım

<script setup lang="ts">
    import { FB } from '@lvntr/components/FormBuilder/core';
    import SkForm from '@lvntr/components/FormBuilder/SkForm.vue';
    import users from '@/routes/users';

    const formConfig = FB.form()
        .cols(2)
        .cardTitle('sk-user.create')
        .submit({
            url: users.store.url(),
            method: 'post',
        })
        .addFields(
            FB.inputText().key('first_name'),
            FB.inputText().key('last_name'),
            FB.inputText().key('email').inputType('email'),
            FB.inputMask().key('phone').mask('(999) 999-9999').unmask(),
            FB.select().key('status').definitionOptions('userStatus').default('active'),
            FB.select().key('gender').definitionOptions('gender'),
            FB.password().key('password').toggleMask(),
        )
        .build();
</script>

<template>
    <SkForm :config="formConfig" />
</template>

İki Çalışma Modu

Dahili submit modu

submit(...) tanımlanırsa SkForm, kendi içinde Inertia useForm() yönetir.

Harici model modu

submit(...) yoksa v-model ile form verisini kendin yönetebilirsin.

<SkForm v-model="formData" :config="formConfig" :errors="errors" />

Bu modda SkForm yalnızca bağladığın nesneyi okur ve yazar. initialData(), bir alanın .default() değeri ve dataUrl'den yüklenen veri yalnızca dahili Inertia formunu besler — v-model'i asla doldurmaz — ve reset() no-op'tur; bu yüzden formData'yı mount'tan önce kendin doldur (reactive({ status: 'active', ...record })).

Form Builder API

  • layout('vertical' | 'horizontal')
  • cols(number) — form grid sütun sayısı (1–12 aralığının tamamı desteklenir; daha önce 6'nın üzerindeki değerler varsayılan 2 sütunlu düzene düşüyordu)
  • dividers(boolean) — horizontal layout'ta alan satırları arasına ince ayraç çizgileri çeker ve label'ları sola yaslar (ayarlar-tarzı üst üste satırlar). Vertical layout'ta etkisi yoktur. Varsayılan false.
  • class(string)
  • dataUrl(url)
  • reloadOnDataUrlChange(boolean) — mount sonrası dataUrl değiştiğinde otomatik yeniden çekmeye opt-in olur (örn. farklı bir kayıt için tekrar kullanılan bir dialog). Varsayılan false: config'i her parent render'da yeniden kurulan bir form aksi halde her rebuild'de yeniden veri çekerdi, aynı URL ile rebuild ise devam eden düzenlemeleri silmemeli.
  • dataKey(key)
  • initialData(record)
  • actionsPosition('top' | 'bottom' | 'both')
  • submit({ url, method, preserveScroll? })
  • resource({ store, update, data, key, id? }) — submit, dataUrl ve dataKey'i tek bir config'ten türeten kısayol: id dolu ise edit modu (PUT ile update, veri data'dan yüklenir); id boş ise create modu (POST ile store)
  • actionLabels(...)
  • hideCancel(boolean)
  • hideSubmit(boolean)
  • hideActions(boolean) — tüm action bar'ı (üst ve alt) gizler; submit işlemini host, formun expose ettiği submit() metodu üzerinden yönetir
  • onCancel('back' | 'emit')
  • inDialog(boolean)
  • showBack(boolean)
  • cardTitle(string)
  • cardSubtitle(string)
  • isCard(boolean)
  • permission(key) — formu salt-okunur moda alan yetki anahtarı (yetki yoksa tüm alanlar disabled + submit gizli)
  • confirmLeave(boolean) — form dirty iken sayfadan çıkışta uyarır (yalnızca internal submit modunda). Varsayılan true; opt-out için false verin.
  • addFields(...fields)

Ortak Field Metotları

Çoğu alan şu metotları destekler:

  • key
  • label
  • trans(boolean) — label'ın $t() ile çözülen bir çeviri anahtarı mı (varsayılan true) yoksa hazır çözülmüş ham bir string mi olduğunu belirtir. Önceden çevrilmiş bir label verirken (örn. .label($t('admin.example')).trans(false)) false verin ki template üzerinde $t() tekrar çalışmasın.
  • required
  • labelPlacement('top' | 'inline') — vertical layout'ta label konumu. Varsayılan 'top' (kontrolün üstünde, alt alta); 'inline' ise label'ı kontrolün yanına yerleştirir (checkbox/toggle-button/toggle-switch tiplerinin zaten varsayılan olarak kullandığı görünüm).
  • controlPosition('left' | 'right') — kontrolün label'ına göre konumu. Varsayılan 'left'.
  • optional
  • class
  • hint
  • visible(fn)
  • disabled(fn)
  • hidden(boolean)
  • default(value)
  • props({...})
  • colSpan(number) — bu alanın form grid'inde kaç sütun kaplayacağı (1..cols). Belirtilmezse 1 hücre. Aktif cols değerini aşan değerler otomatik clamp edilir; section içindeyse clamp section'ın kendi cols değerini kullanır.

hidden(true), alanı gönderilen payload içinde tutarken görünür bir kontrol yerine gizli input olarak render eder.

FB.inputText().key('user_id').default(currentUserId).hidden();

Label for / control id kuralı

Çoğu field tipinde render edilen <label for>, field'ın kendi key'ini hedefler. Altı field tipi PrimeVue kontrolünü odaklanamayan bir wrapper elemanı içinde render eder (input-number, date-picker, select, multiselect, toggle-switch ve .feedback() açıkken password) — bunlarda iç odaklanabilir kontrol, PrimeVue'nun inputId prop'u üzerinden ${key}__control id'sini alır ve label'ın for'u wrapper yerine bu id'yi hedefler. Bu iç kablolamadır (core/ids.ts'in controlId() fonksiyonu); yalnızca render edilen markup'ı okurken veya label/id ile sorgu yapan bir test yazarken önemlidir.

Kullanılabilir Field Builder'lar

  • FB.inputText()
  • FB.inputNumber()
  • FB.inputOtp()
  • FB.inputMask()
  • FB.datePicker()
  • FB.select()
  • FB.multiselect()
  • FB.radio()
  • FB.selectButton()
  • FB.checkbox()
  • FB.checkboxGroup() — select/multiselect/radio/selectButton ile aynı şekilde optionsUrl destekler (bkz. API'den Dinamik Seçenekler)
  • FB.password()
  • FB.textarea()
  • FB.editor()
  • FB.translatableText()
  • FB.translatableTextarea()
  • FB.translatableEditor()
  • FB.toggleButton()
  • FB.toggleSwitch()
  • FB.fileUpload()
  • FB.colorSelector()
  • FB.title()
  • FB.section()
  • FB.slot()

İkonlar (Paket-Bağımsız)

Kit, paket-spesifik bir ikon kütüphanesine bağlı değildir. Tüm ikon API'leri (icon, labelIcon, iconPosition, labelIconPosition, title/section icon) bir string "ikon descriptor'ı" alır.

Descriptor üç formattan birini otomatik algılar:

Desen Anlam Örnek
<svg… ile başlar Ham SVG markup — v-html ile render '<svg viewBox="0 0 24 24">…</svg>'
https?: veya data: ile başlar URL veya data URI — <img src> ile render 'https://cdn.example.com/icon.svg', 'data:image/svg+xml;base64,…'
Diğer CSS class — <i :class> ile render 'pi pi-search', 'fa fa-user', 'mdi mdi-account'

Bu yaklaşım PrimeIcons, FontAwesome, Material Design Icons, Lucide, Iconify ve diğer class-tabanlı ikon setlerini aynı API üzerinden destekler.

Güvenlik notu: İkon descriptor'ları developer-controlled builder config'ten gelmelidir — kullanıcı girdisinden değil. <svg… yolu v-html ile render eder; bu bir XSS vektörüdür. API'den gelen bir string'i sanitize etmeden doğrudan field ikon config'ine sokmayın.

Label İkonları

.labelIcon(descriptor), bir field'ın label'ı yanına ikon ekler. Tüm field tiplerinde desteklenir.

  • .labelIconPosition('left' | 'right') — label metnine göre konum (varsayılan 'left').
FB.inputText()
    .key('email')
    .label('E-posta')
    .labelIcon('pi pi-envelope')
    .labelIconPosition('left')

Tüm layout modlarında çalışır: vertical (üst label), vertical (inline label) ve horizontal.

Input İkonları

.icon(descriptor), PrimeVue IconField + InputIcon kullanarak input elemanının içine ikon yerleştirir.

  • .iconPosition('left' | 'right') — konum (varsayılan 'left').

Desteklenen field tipleri: input-text, input-number, input-mask, password (yalnızca custom path — aşağıdaki nota bakın).

FB.inputText().key('search').label('Ara').icon('pi pi-search').iconPosition('left')
FB.inputNumber().key('price').label('Fiyat').icon('fa fa-dollar').iconPosition('right')
FB.inputMask().key('phone').label('Telefon').mask('(999) 999-9999').icon('mdi mdi-phone')

Uyarılar:

  • .groupPrefix() / .groupSuffix() önceliklidir — InputGroup wrapper varsa input ikonu devre dışı kalır.
  • FB.password().feedback(), PrimeVue <Password> üzerinden render eder (güç göstergesi yolu). Bu yolda .icon() etkisizdir. feedback kapalıyken (varsayılan custom path) .icon() normal çalışır.
  • .icon(), select, multiselect, textarea, editor, file-upload, color-selector ve date-picker (kendi showIcon mekanizması vardır) tiplerinde desteklenmez. Bu tipler için .labelIcon() kullanın ya da componentProps ile özelleştirin.

InputMask Alan API'si

FB.inputMask(), telefon, kimlik numarası ve formatlı tarih gibi alanlarda kullanışlıdır.

  • mask(string)
  • placeholder(string | boolean)
  • slotChar(string)
  • autoClear(boolean)
  • unmask(boolean)
FB.inputMask().key('phone').mask('(999) 999-9999').placeholder('sk-common.placeholder.phone').slotChar('_').unmask();

unmask(true) aktif olduğunda modelde tutulan değer, maske karakterleri olmadan döner.

DatePicker Alan API'si

FB.datePicker(), tarih, tarih-saat, aralık, çoklu tarih, ay ve yıl girişleri için PrimeVue DatePicker render eder.

  • placeholder(string | boolean)
  • dateFormat(string) — PrimeVue tarih formatı, varsayılan 'dd/mm/yy'.
  • selectionMode('single' | 'range' | 'multiple')
  • showTime(boolean)
  • hourFormat('12' | '24')
  • showIcon(boolean)
  • iconDisplay('input' | 'button')
  • minDate(Date)
  • maxDate(Date)
  • showButtonBar(boolean)
  • numberOfMonths(number)
  • view('date' | 'month' | 'year')
  • inline(boolean)
FB.datePicker()
    .key('published_at')
    .label('Yayın tarihi')
    .showIcon()
    .showTime()
    .hourFormat('24')
    .dateFormat('dd/mm/yy');

Password Alan API'si

FB.password(), opsiyonel güç göstergesi, crypto-safe üretici ve tutarlı göz toggle'ı ile gelen bir parola input'u üretir.

  • toggleMask(boolean) — göster/gizle göz toggle'ı (varsayılan true).

  • feedback(boolean) — PrimeVue <Password> güç göstergesine opt-in. Çağrılmadığında alan, daha hafif <InputText> + custom göz toggle yoluna düşer; böylece InputGroup içinde birebir aynı görünür. Varsayılan false.

  • generator(options?) — input'un yanına crypto-safe generate butonu ekleyen opt-in metodu. Tüm seçenekler opsiyonel:

    FB.password().key('password').generator();
    // → 16 karakter, mixed case + harf + rakam + sembol
    
    FB.password().key('password').generator({
        length: 20,
        mixedCase: true,
        letters: true,
        numbers: true,
        symbols: true,
    });
    

    Varsayılanlar bilinçli olarak proje-wide Password::defaults() kuralından daha sıkı — üretilen her değer ilk submit'te backend validation'ı geçer. Üretilen parola doğrudan input'a yazılır, toast üzerinden bir kez gösterilir (password_generated / password_generated_detail) ve alandan kopyalanabilir.

// Basit göz toggle'lı parola alanı
FB.password().key('password');

// Üretici butonlu parola alanı
FB.password().key('password').generator();

// Güç göstergeli varyant (PrimeVue <Password>'a düşer)
FB.password().key('password').feedback();

// Özel uzunluk ve sembol seti ile üretici
FB.password().key('password').generator({ length: 24 });

Editor Alan API'si

FB.editor(), Tiptap v3 tabanlı bir WYSIWYG editör'ü FormBuilder alanı olarak render eder. İçerik sanitize edilmiş HTML olarak saklanır — App\Support\HtmlSanitizer hem yazma hem okuma yolunda allowlist dışındaki tag, attribute ve URL scheme'lerini süzer.

  • toolbar('minimal' | 'standard' | 'full') — toolbar düzeni. minimal bold / italic / underline, yazı rengi ve vurgu rengi; standard üstü çizili, madde / numaralı / görev listesi, alıntı, hizalama (iki yana yaslama dahil), biçimi temizle ve tablo ekler; full H2–H4, satır içi kod, alt/üst simge, yazı tipi / boyut / satır aralığı, kod bloğu, ayraç, YouTube gömme ve geri al / yinele ekler. Her preset'te HTML kaynağı düğmesi (işaretlemeyi doğrudan düzenleme — her tuş form değerine anında yansır, kaynak açıkken gönderilse de düzenleme kaybolmaz; editörün saklayamadığı etiketler yazarken metin kutusunun altında listelenir) ve tam ekran düğmesi (Esc ile çıkılır) bulunur. Link ve görsel düğmeleri links() / imageUpload() ayarına bağlıdır. Varsayılan 'standard'.
  • placeholder(string) — editor boşken gösterilen çeviri anahtarı.
  • minHeight(string) — editor gövdesi için CSS min-height (varsayılan '10rem').
  • imageUpload({ context, contextId?, folderId?, folderName?, acceptedMimes? }) — File Manager üzerinden inline görsel upload'ını konfigüre eder. context zorunludur ve File Manager context registry içinde kayıtlı olmalıdır. folderName, bu editör üzerinden yüklenen her görseli ilgili context'te tek bir root-level klasör altında gruplar (örn. her welcome-message görseli "Welcome Message" altına gider). Server-side folder_name validator'ı ile aynı regex: yalnızca harf, rakam, boşluk, tire, altçizgi.
  • links(boolean) — link toolbar butonunu ve paste auto-linking davranışını açar. Varsayılan false. Aynı zamanda buton aracını açar: metni seçin (ya da buton metnini yazın), adres ve stil seçin — primary, secondary veya outline — primary / outline için editör paletinden isteğe bağlı renk de seçilebilir (seçilmezse tema ana rengi). Sonuç stili taşıyan normal bir linktir: <a href="…" data-sk-button="primary">…</a>. sk-prose bunu buton olarak çizer; aynı HTML'i gösteren mobil uygulama a[data-sk-button] etiketini kendi native butonuna eşlemelidir (aşağıya bakın), attribute'u tanımayan her renderer ise çalışan bir link gösterir.
  • treatEmptyAsBlank(boolean) — editör boşken <p></p> yerine boş string üretir. Varsayılan true.
FB.editor()
    .key('welcome_message')
    .toolbar('standard')
    .placeholder('sk-setting.general.welcome_message_placeholder')
    .imageUpload({ context: 'global', folderName: 'Welcome Message' });

Mobilde buton linkleri

Editör içeriği düz HTML'dir; native uygulama butonları <a> etiketi olarak görür. Sözleşme tek attribute: data-sk-button değeri primary, secondary veya outline (HtmlSanitizer başka değeri siler), isteğe bağlı data-sk-color #rrggbb biçiminde dolgu (primary) ya da kenarlık + yazı rengidir (outline) — dolgu üstündeki yazıyı parlaklığa göre beyaz ya da koyu seçin, href http(s), mailto veya tel, link metni buton etiketi. CSS'e güvenmek yerine HTML renderer'ınızda eşleyin — örn. react-native-render-html'de özel a renderer'ı, flutter_html'de a için bir TagExtension — attribute yoksa varsayılan link çizimine düşün. Buton uygulama içinde bir ekran açacaksa adres olarak https universal / app link kullanın.

Sanitize edilmiş içeriği render etme

Editor çıktısını admin UI'ın başka bir yerinde render ederken sk-prose container'ına sarın; böylece typography extension'ları tutarlı çözülür:

<div class="sk-prose" v-html="welcomeMessage" />

Server tarafında, frontend'e paylaşmadan önce her okumayı HtmlSanitizer::clean() üzerinden geçirin (defense-in-depth — yazma yolu da sanitize'liyor ama drift etmiş bir DB satırı veya sanitize öncesi eski bir kayıt tarayıcıya asla ulaşmamalı).

URL scheme allowlist'i

HtmlSanitizer relative URL'lerle birlikte http://, https://, mailto:, tel: scheme'lerine izin verir. Diğer her şey (blob:, data:, file:, ftp:, javascript:, vbscript:) reddedilir. Editor içeriğini programatik doldururken bunu hatırlayın — kayıt öncesinde kaçak scheme temizlenir.

Dosya Yükleme Alanı API'si

FB.fileUpload(), bir seçici butonu ile birlikte sürükle-bırak drop zone render eder. Hem tekli hem çoklu dosya modunda çalışır ve aynı değer içinde zaten bağlı medya ile yeni seçilen dosyaları karıştırabilir.

  • multiple(boolean) — birden fazla dosyaya izin verir. Varsayılan false (tekli dosya, değer düz bir File | null).
  • accept(string) — virgülle ayrılmış desen listesi (MIME tipi, image/* gibi wildcard veya .pdf gibi uzantı); bir dosya eklenmeden önce client-side eşleştirilir. Eşleşmeyen bırakılan/seçilen bir dosya sessizce atlanır — native dosya diyaloğunun accept özniteliğiyle aynı davranış.
  • maxFileSize(bytes) — dosya başına boyut sınırı. Yalnızca ayarlandığında uygulanır — client-side boyut kontrolü istemiyorsanız atlayın. Sınırı aşan bir dosya reddedilir ve reddedilen dosya adlarını listeleyen tek bir toast ile bildirilir; sınırın altındaki dosyalar yine de eklenir.
  • fileLimit(number) — multiple modunda, mevcut (korunan) + yeni seçilen dosyaların toplam sayısını sınırlar. Yalnızca ayarlandığında uygulanır. Bir bırakma/seçim bu sınırı aşarsa yalnızca sığan dosyalar eklenir, geri kalanı maxFileSize reddiyle aynı toast'ta bildirilir.
  • existingMedia(items) — edit modunda gösterilecek mevcut medya ({ id, name, url, size, mime_type }[]).
  • existingMediaKey(key) — initialData/remoteData içinde existingMedia'yı otomatik dolduran anahtar (örn. 'identity_document_media'), böylece dataUrl/resource kullanırken elle bağlamanıza gerek kalmaz.
  • deferExistingRemoval(boolean) — aşağıya bakın. Varsayılan false.
FB.fileUpload()
    .key('attachments')
    .multiple()
    .accept('image/*,.pdf')
    .maxFileSize(5 * 1024 * 1024)
    .fileLimit(10)
    .existingMediaKey('attachments_media');

Silme semantiği: anında vs. ertelenmiş

Zaten kaydedilmiş bir dosyayı silmenin iki modu vardır:

  • Varsayılan (deferExistingRemoval set edilmemiş/false) — silmeye tıklamak (onay diyaloğundan sonra) hemen DELETE /media/{id} gönderir, yani form hiç submit edilmese bile dosya silinmiş olur. Başarısız bir silme, dosyayı listede bırakır ve onu UI'dan sessizce düşürmek yerine bir hata toast'ı gösterir.
  • deferExistingRemoval(true) — tıklamada hiçbir şey silinmez; öğe yalnızca render edilen listeden ve field'ın keep-list'inden çıkar. Silme, field'ın kendi keep-list sözleşmesi üzerinden save isteğine ertelenir (aşağıya bakın).

Yeni seçilen (henüz yüklenmemiş) bir dosya her zaman bekleyen seçimden anında kaldırılır — sunucuda ertelenecek bir şey yoktur.

Save-side keep-list sözleşmesi (deferExistingRemoval)

deferExistingRemoval(true) set edildiğinde field, korunan mevcut medya id'lerini ve yeni UploadedFile'ları karıştıran bir dizi submit eder. Modelde Lvntr\StarterKit\Traits\HasMediaCollections::syncMediaCollection() ile eşleştirin; bu metot tam olarak bu şekli kabul eder:

// $request->validated('attachments') id'lerin (korunan) ve UploadedFile örneklerinin (yeni) karışık bir dizisidir
$user->syncMediaCollection('attachments', $request->validated('attachments'));

syncMediaCollection(), koleksiyondaki id'si submit edilen keep-list'te olmayan her medyayı siler ve dizideki her UploadedFile'ı ekler — yani frontend'in gönderdiği dizi bir diff değil, koleksiyonun olması gereken son hâlidir.

Sürükle-bırak

Drop zone, dosya seçici butonuna ek olarak üzerine sürüklenen dosyaları kabul eder; her iki yol da aynı addFiles() doğrulamasından geçer (accept, maxFileSize, fileLimit), yani sürükle-bırak seçicinin uyguladığı bir sınırı atlayamaz. accept ile eşleşmeyen bir dosya, seçicideki reddedişle aynı şekilde sessizce düşer.

Çevrilebilir Alan API'si

Bir metin alanının aktif her dil için ayrı değer saklaması gerekiyorsa translatable builder'ları kullanın:

  • FB.translatableText() — her locale için bir InputText.
  • FB.translatableTextarea() — her locale için bir Textarea.
  • FB.translatableEditor() — her locale için bir zengin editör.

Ortak metotlar:

  • onlyLocales(['tr', 'en']) — yalnız bu locale kodlarını render eder.
  • exceptLocales(['en']) — bu locale kodlarını gizler.
  • Çok dilli alanlar her zaman tab'lı locale panelleri olarak render edilir (kit'in tek çok dilli giriş tasarımı).
  • localeLabelStyle('badge' | 'name' | 'flag') — locale label görünümü.
FB.form().addFields(
    FB.translatableText().key('title').label('Title').required(),
    FB.translatableTextarea().key('description').label('Description').rows(4),
    FB.translatableEditor().key('content').label('Content').minHeight('220px'),
);

Backend eşleşmesi:

  • Her attribute'u JSON kolonda saklayın.
  • Modele Spatie HasTranslations ekleyin ve attribute'ları $translatable içine yazın.
  • FormRequest'lerde Lvntr\StarterKit\Support\HasTranslatableRules kullanın (bir trait — bu trait'in App\Support geriye-dönük alias'ı yoktur, vendor namespace'inden import edin).
  • Datatable arama/sıralama ve resource çıktısı için Lvntr\StarterKit\Support\TranslatableQueryHelpers kullanın.

Tam backend ve frontend rehberi için Çevrilebilir Alanlar dokümanına bakın.

ColorSelector Alan API'si

FB.colorSelector(), Tailwind renk paletinden seçim yapılan ve isteğe bağlı tone seçici içeren bir alan üretir.

  • colors(string[]) — kullanılabilir renk adları. Varsayılan: 22 Tailwind palet ailesinin tamamı — 17 kromatik aile (red'den rose'a) ve 5 nötr aile (slate, gray, zinc, neutral, stone).
  • tones(number[]) — gösterilecek tone basamakları. Varsayılan: [50, 100, …, 950].
  • format('hex' | 'name' | 'name-tone') — çıktı formatı. Varsayılan: 'name'.
  • defaultTone(number) — tone gerektiren formatlarda ilk seçim. Varsayılan: 500.

Çıktı formatı modele kaydedilecek değeri belirler:

format Kaydedilen değer
'name' "blue"
'name-tone' "blue-500"
'hex' "#3b82f6"

Tone seçici, 'name-tone' ve 'hex' formatlarında dropdown'un altında görünür. 'name' modunda tone dikkate alınmaz ve seçici gizlenir.

// Varsayılan — renk adını kaydeder
FB.colorSelector().key('brand_color');

// Renk adı + tone — "blue-500" kaydeder
FB.colorSelector().key('brand_color').format('name-tone').defaultTone(500);

// Hex değer — "#2563eb" kaydeder
FB.colorSelector().key('brand_color').format('hex').defaultTone(600);

// Paleti daralt
FB.colorSelector().key('accent').colors(['red', 'blue', 'green']).tones([400, 500, 600]);

Modele başlangıçta bir hex string geldiğinde, component ters arama yaparak eşleşen renk + tone seçimini geri yükler.

Title İkonları

FB.title(), başlığın yanına ikon render etmek için .icon() ve .iconPosition() metotlarını destekler.

  • .iconPosition('left' | 'right') — konum (varsayılan 'left').
FB.title('Genel Bilgiler').icon('pi pi-info-circle').iconPosition('left')

Section / Card Gruplama

FB.section(), ilgili alanları görsel olarak belirgin bir kart bloğunda gruplar. Section'lar FB.form().addFields(...) içinde üst seviye bir field tipi olarak render edilir.

import { FB } from '@lvntr/components/FormBuilder/core';

const config = FB.form()
    .layout('vertical')
    .cols(2)
    .isCard(false)  // form-level card kapatılır; section'lar kendi card'larında render edilir
    .addFields(
        FB.section('Kişisel Bilgiler')
            .icon('pi pi-user')
            .cols(2)
            .addFields(
                FB.inputText().key('first_name').label('Ad'),
                FB.inputText().key('last_name').label('Soyad'),
                FB.inputText().key('email').label('E-posta').icon('pi pi-envelope'),
                FB.password().key('password').label('Parola').icon('pi pi-lock'),
            ),
        FB.section('Adres')
            .icon('pi pi-map-marker')
            .subtitle('İletişim adresi bilgileri')
            .cols(2)
            .addFields(
                FB.inputText().key('city').label('Şehir'),
                FB.inputText().key('postal_code').label('Posta Kodu'),
                FB.textarea().key('address').label('Açık Adres').colSpan(2), // tam satır kapla — .class('col-span-2') yerine tercih edilir
            ),
        FB.section('Tercihler')
            .icon('pi pi-cog')
            .isCard(false)  // transparent section — card arka plan, kenarlık ve gölge yok
            .cols(1)
            .addFields(
                FB.toggleSwitch().key('newsletter').label('Bülten aboneliği'),
                FB.toggleSwitch().key('notifications').label('Bildirimler'),
            ),
    )
    .build();

Alan bazlı .colSpan() örneği — 12 sütunlu formda tam genişlik ve yan yana alanların karıştırılması:

FB.form()
    .cols(12)
    .addFields(
        FB.inputText().key('title').label('Başlık').colSpan(12),  // tam satır
        FB.inputText().key('first_name').label('Ad').colSpan(6),
        FB.inputText().key('last_name').label('Soyad').colSpan(6),
        FB.textarea().key('notes').label('Notlar').colSpan(12),
    )
    .build();

Section Builder API:

  • FB.section(title?) — factory metodu. title bir çeviri anahtarıdır (opsiyonel).
  • .title(key) — section başlığı olarak kullanılan çeviri anahtarını set eder veya override eder.
  • .subtitle(key) — başlığın altında gösterilen ikincil metin.
  • .icon(descriptor) — başlığın yanında gösterilen ikon.
  • .iconPosition('left' | 'right') — ikon konumu (varsayılan 'left').
  • .cols(number) — section içindeki alanlar için grid sütun sayısı (1–12). Belirtilmezse parent form'un cols değerini devralır.
  • .isCard(boolean) — false olduğunda section card kabuğu olmadan render edilir (arka plan, gölge veya kenarlık yok). Varsayılan true (card görünür).
  • .addFields(...fields) — iç içe field tanımları. Yalnızca tek seviye iç içe geçme desteklenir.

Notlar:

  • Section key değerleri gönderilen payload'da yer tutmaz — form verisi flat'tir. Yukarıdaki örnek şu veriyi üretir: { first_name, last_name, email, password, city, postal_code, address, newsletter, notifications }.
  • İç içe section (section içinde section) desteklenmez — tek seviye.
  • FB.title() ve FB.section() birlikte kullanılabilir: üst seviye başlıklar için section dışında title field kullanın, içerikler section'lar altında gruplandırılsın.

Card Başlık Sağ Slot

Hem form-level card (cardTitle ayarlandığında) hem her FB.section() card'ı, başlığın sağına action button, badge veya durum göstergesi yerleştirmek için bir slot açar.

  • Form card — slot adı: title-end.
  • Section card — slot adı: section-${key}-title-end. Section'da .key('your-key') çağırın ki slot adı kararlı olsun; aksi halde otomatik üretilen __section_N key'i kullanılır.
<SkForm :config="formConfig">
  <template #title-end>
    <Button icon="pi pi-refresh" text rounded @click="refresh" />
  </template>

  <template #section-address-title-end="{ values }">
    <Tag v-if="values.is_primary" severity="success" :value="$t('forms.primary')" />
  </template>
</SkForm>
FB.section('Adres').key('address').addFields(/* ... */)

Section slot scope'una { values } geçilir — mevcut form değerlerinin reaktif snapshot'ı, koşullu render için kullanışlıdır.

Görsel olarak caption bloğu (title + subtitle) form içeriğinden alt çizgi ile ayrılır (--p-surface-200 light / --p-surface-700 dark tema değişkenleri) — böylece başlık, slot içeriği ve alt başlık form alanlarının üzerinde bütünleşik tek bir başlık bloğu olarak okunur.

Select Benzeri Alanlarda Veri Kaynakları

Select alanları seçenekleri şu kaynaklardan alabilir:

  • options([...]) ile statik dizi
  • definitionOptions('userStatus') ile giriş gerektiren /definitions kayıtları
  • optionsUrl(...) ile uzaktan dinamik veri

enumOptions(...) geriye dönük uyumluluk için hâlâ duran, ancak yeni kodda tercih edilmemesi gereken deprecated bir alias'tır.

Reaktif Alan Bağımlılıkları

visible(fn) ve disabled(fn) metotları tüm güncel form değerlerini parametre olarak alır. SkForm her değişiklikte bunları yeniden değerlendirir, böylece alanlar birbirine bağımlı olabilir.

Başka bir alanın değerine göre disable etme

FB.form().addFields(
    FB.select()
        .key('notification_channel')
        .options([
            { label: 'Email', value: 'email' },
            { label: 'SMS', value: 'sms' },
            { label: 'Yok', value: 'none' },
        ]),
    FB.inputText()
        .key('notification_address')
        .disabled((values) => values.notification_channel === 'none'),
);

notification_channel olarak none seçildiğinde notification_address alanı disable olur.

Başka bir alanın değerine göre gösterme/gizleme

FB.toggleSwitch().key('use_custom_domain'),
FB.inputText()
    .key('custom_domain')
    .visible((values) => values.use_custom_domain === true),

custom_domain alanı yalnızca toggle açıkken görünür.

API'den Dinamik Seçenekler (Bağımlı Select'ler)

optionsUrl sabit bir string ya da güncel form değerlerini alıp URL (veya null) dönen bir fonksiyon kabul eder. SkForm dönen URL'yi izler — değiştiğinde otomatik olarak yeni seçenekleri çeker.

Sabit URL'den seçenekleri yükleme

FB.select().key('role').optionsUrl('/api/roles/options');

Bağımlı select — başka bir alana göre API'den veri çekme

FB.form().addFields(
    FB.select()
        .key('country')
        .options([
            { label: 'Türkiye', value: 'TR' },
            { label: 'Almanya', value: 'DE' },
        ]),
    FB.select()
        .key('city')
        .optionsUrl((values) => (values.country ? `/api/cities?country=${values.country}` : null)),
);

Nasıl çalışır:

  1. Kullanıcı bir country seçer
  2. optionsUrl fonksiyonu yeni değerlerle çalışır, /api/cities?country=TR döner
  3. SkForm URL'nin değiştiğini algılar, otomatik olarak yeni seçenekleri çeker
  4. city dropdown'ı gelen verilerle doldurulur
  5. null döndürmek "çekme" anlamına gelir — ülke seçilene kadar select boş kalır

disabled + bağımlı optionsUrl birlikte kullanma

FB.select()
    .key('department')
    .optionsUrl('/api/departments/options'),
FB.select()
    .key('team')
    .disabled((values) => !values.department)
    .optionsUrl((values) =>
        values.department
            ? `/api/teams/options?department=${values.department}`
            : null
    ),

team select'i departman seçilene kadar disable kalır. Seçildikten sonra API'den departmana göre filtrelenmiş takımlar çekilir.

Yetki Kontrolü (Form-Level)

Bir formu yalnızca belirli bir yetkisi olan kullanıcıların düzenlemesine izin vermek için .permission() metodu kullanılır:

FB.form()
    .resource({ store: ..., update: ..., data: ..., key: 'user', id: userId })
    .permission('users.update')
    .addFields(/* ... */)
    .build();

Yetki auth.permissions Inertia shared prop'undan useCan() composable'ı ile çözülür. Kullanıcıda yetki yoksa:

  • Tüm alanlar otomatik olarak disabled hale gelir (mevcut field.disabled(values => ...) callback'leri ile birlikte)
  • Submit butonu hem üst hem de alt action alanlarında gizlenir
  • handleSubmit ek bir güvenlik katmanı olarak herhangi bir submit'i iptal eder
  • Cancel/back butonu ve özel slot action'lar görünmeye devam eder

İyi Pratik

Alan tanımlarını, formun ait olduğu sayfa veya sekmeye yakın tutun. Backend tarafında Domain Action ve Form Request kullanın ki form katmanı iş mantığına dönüşmesin.

En İyi Kullanım Alanları

  • ayar sekmeleri
  • create ve edit resource formları
  • profil formları
  • tekrar eden alan desenlerine sahip admin araçları

Dahili Davranışlar

SkForm şunları hazır olarak yönetir:

  • dataUrl ile ilk veriyi çekme
  • definition verilerini önce yükleme
  • bağımlı alan değişince dinamik select seçeneklerini yenileme
  • gizli alanları doğal <input type="hidden"> elemanları olarak render etme
  • file upload alanları varsa forceFormData ile gönderme
  • dialog için uygun cancel davranışı
  • dahili veya harici modda birleşik hata gösterimi
  • permission set edilirse formu salt-okunur moda alma

Expose Edilen Component API'si

SkForm, üzerinde template ref tutan host'lar için defineExpose ile küçük bir imperative yüzey açar:

  • reset() — internal Inertia formunu resetler ve hataları temizler (yalnızca internal submit modunda).
  • submit() — action bar'ın submit butonunun kullandığı aynı submit yolunu programatik olarak tetikler. hideActions(true) submit kontrolünü host-render edilen bir footer'a taşıdığında kullanışlıdır.
  • reload() — mount-time yükleme ile aynı loading-state ve hata-toast semantiğiyle dataUrl'i istek üzerine yeniden çeker. Formda dataUrl yoksa no-op'tur. URL prop'u her değiştiğinde otomatik yeniden çekim yerine, açık bir yenileme tetikleyicisi ("Yenile" butonu, bir kardeş kaydetme olayı) istediğinizde .reloadOnDataUrlChange() yerine bunu kullanın.
  • processing, isDirty, dataLoading, remoteData, currentValues — host-render edilen bir action bar veya durum göstergesi için reaktif state ayna değerleri.
  • setValue(key, value) — tek bir field'ın değerini programatik olarak set eder.