Bir açılır menü için kaç satır JavaScript yazdığınızı hatırlıyor musunuz? Dışarı tıklayınca kapanması, Escape tuşuyla kapanması, klavye ile gezinmesi, ekran okuyucuya açık ya da kapalı olduğunu bildirmesi. Her biri küçük bir iş gibi görünür, ama toplamda bir bileşen kütüphanesi kadar kod çıkar. Alpine.js'in çekiciliği bu kodun büyük kısmını HTML öznitelikleri içine taşımasında.
Alpine'in boyutu küçük, sözdizimi tanıdık ve kurulum neredeyse yoktur. Ama asıl değeri, bir bileşenin davranışının o bileşenin HTML'ine bakılarak okunabilmesidir. Bir öğenin durumu, o öğenin yanında yazılıysa bakım maliyeti düşer. Bu metinde Alpine ile sıfır bağımlılıklı, ama yine de erişilebilir ve sağlam etkileşimlerin nasıl kurulduğunu anlatacağım.
Yerel durum, yerel çözüm
Alpine'in temel birimi x-data özniteliğidir. Bu öznitelik, bir öğe ağacının durumunu tanımlar. Durum, o öğenin içinde yaşar ve dışarıdan yalnızca gerekirse erişilir. Bir açılır menü yalnızca kendi açık ya da kapalı durumunu biliyorsa, bu bilgiyi global bir depoya taşımanın bir anlamı yoktur.
<div x-data="{ open: false }">
<button type="button" @click="open = !open" :aria-expanded="open">
Menü
</button>
<div x-show="open" x-transition @click.outside="open = false">
<a href="/yazilar">Yazılar</a>
<a href="/hakkinda">Hakkında</a>
</div>
</div>Bu kodda üç Alpine kavramı bir arada: x-data durumu tanımlar, @click olayı dinler, x-show görünürlüğü yönetir. :aria-expanded ise durumu erişilebilirlik özniteliğine bağlar. Menünün açık olup olmadığını ekran okuyucu da bilir.
Az kod, çok niyet: iyi bir arayüz katmanı kendini gizler.
Dört temel direktif
Alpine'in çoğu etkileşimi dört temel direktifle kurulabilir. Hepsini ezberlemek gerekmiyor; hangisinin ne iş yaptığını bilmek yeterli.
| Direktif | Görevi | Örnek |
|---|---|---|
x-data | Bileşenin durumunu tanımlar | x-data="{ sekme: 'bir' }" |
x-show | Görünürlüğü CSS ile açıp kapatır | x-show="open" |
x-if | Öğeyi DOM'dan tamamen ekler ya da çıkarır | template x-if="hata" |
x-model | Girdiyi durumla çift yönlü bağlar | x-model="arama" |
x-show ile x-if arasındaki fark, sık karıştırılan bir ayrımdır. x-show öğeyi sayfada tutar ve yalnızca gizler; bu yüzden odak ve ekran okuyucu ilişkileri korunur, ama gizli öğenin içeriği DOM'da kalır. x-if öğeyi gerçekten kaldırır, ağır içerikler için daha uygundur ama her açılışta yeniden oluşturulur.
Sekmeler: klavyeyi unutmadan
Sekmeler, Alpine ile en çok yazılan bileşenlerdendir ve en çok yanlış yazılanlardan da. Görsel olarak çalışan bir sekme, klavye ile gezilemiyorsa erişilebilir değildir. Ok tuşlarıyla geçiş, doğru role değerleri ve aria-selected bu yüzden birlikte düşünülmelidir.
<div x-data="{ sekme: 'ozet' }">
<div role="tablist" aria-label="Yazı bilgileri">
<button role="tab" id="sekme-ozet"
:aria-selected="sekme === 'ozet'"
:tabindex="sekme === 'ozet' ? 0 : -1"
@click="sekme = 'ozet'"
@keydown.right.prevent="sekme = 'yorumlar'"
>Özet</button>
<button role="tab" id="sekme-yorumlar"
:aria-selected="sekme === 'yorumlar'"
:tabindex="sekme === 'yorumlar' ? 0 : -1"
@click="sekme = 'yorumlar'"
@keydown.left.prevent="sekme = 'ozet'"
>Yorumlar</button>
</div>
<div role="tabpanel" x-show="sekme === 'ozet'">…</div>
<div role="tabpanel" x-show="sekme === 'yorumlar'" x-cloak>…</div>
</div>tabindex ayrıntısı önemli. Yalnızca seçili sekme Tab sırasına girer; diğerleri ok tuşlarıyla erişilir. Bu, WAI-ARIA sekme desenine uygun bir davranıştır ve klavye kullanıcıları için sekmeler arasında gezinmeyi kısa tutar.
x-cloakile Alpine yüklenmeden önce görünen içerik gizlenir; bunun için ilgili CSS kuralını da sayfaya eklemeyi unutmayın.- Sekme panelleri
aria-labelledbyile sekmelere bağlanmalıdır; bu bağlantı ekran okuyucu için öğeyi anlamlı kılar.
Modal: odak yönetimi
Modal, Alpine ile en kolay kurulan ama en çok hata yapılan bileşendir. Açıldığında odak içeri alınmalı, Escape ile kapanmalı ve kapandığında odak onu açan düğmeye dönmelidir. Odak tuzağı için Alpine'in resmi x-trap eklentisi kullanılabilir.
<div x-data="{ acik: false }">
<button type="button" x-ref="acici" @click="acik = true">Paylaş</button>
<div x-show="acik" x-trap.noscroll="acik" @keydown.escape.window="acik = false"
role="dialog" aria-modal="true" aria-labelledby="paylas-baslik">
<h2 id="paylas-baslik">Yazıyı paylaş</h2>
<button type="button" @click="acik = false; $refs.acici.focus()">Kapat</button>
</div>
</div>Bu örnekte $refs, kapanınca odağı açan düğmeye geri götürür. Bu küçük adımı atlamak, klavye kullanıcılarını sayfanın başına döndürür ve nerede olduklarını kaybettirir.
Modal içinde arka plan kaydırmasını da engellemek gerekir; x-trap.noscroll bunu yapar. Ama bu davranışın kapanma anında geri alındığını kontrol edin, yoksa sayfa kilitli kalabilir.
Olaylar ve bileşenler arası iletişim
Bir bileşenin başka bir bileşeni tetiklemesi gerektiğinde iki yol var: doğrudan erişim ya da olay. Doğrudan erişim, iki bileşen aynı ağaçta olduğunda işe yarar. Olay ise daha gevşek bir bağ kurar. $dispatch ile bir olay yayınlarsınız, başka bir öğe .window değiştiricisiyle bu olayı dinler.
<button type="button" @click="$dispatch('open-modal', 'paylas')">Paylaş</button>
<div x-data="{ acik: false }"
@open-modal.window="if ($event.detail === 'paylas') acik = true">
…
</div>Bu desen, Pines UI gibi Blade ve Alpine tabanlı bileşen kütüphanelerinde de yaygındır. Modal, açılır menü ya da bildirim katmanı, tetikleyici ile içerik arasında bir olayla konuşur ve birbirlerinin iç durumunu bilmek zorunda kalmaz.
Global bir durum gerekirse Alpine.store kullanılabilir. Ama her şeyi global yapmak, yerel durumun getirdiği sadeliği kaybettirir. Kural basit: durum yalnızca bir yerde kullanılıyorsa yerel kalmalı.
Alpine ile HTMX birlikte
Sunucudan gelen HTML parçaları sayfaya eklendiğinde, içindeki Alpine öznitelikleri de çalışmalıdır. Alpine bu iş için bir gözlemci kullanır ve yeni eklenen öğeleri otomatik olarak başlatır. Bu yüzden HTMX ile yerleştirilen bir parçada x-data ayrıca bir yere kaydedilmek zorunda değildir.
Ancak dikkat edilmesi gereken bir nokta var: parça değiştiğinde eski durum yok olur. Bir sekmenin seçili olduğu, bir menünün açık kaldığı durum, yeni parçayla birlikte sıfırlanır. Bu istenmiyorsa durumu parçanın dışındaki bir kapsayıcıya taşımak ya da sunucudan gelen parçanın kendisine varsayılan değeri yazmak gerekir.
- Parça içinde Alpine öznitelikleri olabilir, ama durum yalnızca parçaya ait olmalıdır.
- Kalıcı tercihler (örneğin koyu tema) için
localStoragekullanın ve okumayı birtrybloğuna alın. - HTMX olaylarını (
htmx:afterSwap) dinlemek gerekirse Alpine'inx-initöznitelik içinde kalın.
Erişilebilirlik kontrol listesi
Alpine ile yazılan her etkileşim için birkaç temel soruyu sormak yeterlidir. Bu sorular bir kontrol listesi gibi, her bileşende tekrar edilebilir.
| Soru | Kontrol |
|---|---|
| Klavye ile açılabiliyor mu? | Enter ve Space ile tetiklenen düğmeler kullanın, div değil |
| Durum ekran okuyucuya bildiriliyor mu? | aria-expanded, aria-selected gibi öznitelikleri bağlayın |
| Escape ile kapanıyor mu? | @keydown.escape ekleyin |
| Odak kapanınca nereye dönüyor? | Tetikleyici öğeye geri odaklanın |
| Dışarı tıklamak kapatıyor mu? | @click.outside ekleyin |
Bu listedeki her madde basit görünür, ama birlikte uygulandığında bileşen gerçekten herkes için çalışır. Ekran okuyucuyla bir kez test etmek, listenin hangi maddelerinin eksik olduğunu hızla gösterir.
Sık yapılan hatalar
Alpine'de en yaygın hata, durumu gereğinden fazla büyütmektir. Her şeyi bir x-data içine koymak bir süre sonra okunmaz hâle gelir. Bir bileşen on alandan fazla durum taşıyorsa, onu daha küçük parçalara ayırmanın zamanı gelmiş olabilir.
İkinci hata, ifadelerin içine mantık yazmaktır. @click="sepet.push(urun); toplam = sepet.reduce((a, b) => a + b.fiyat, 0); bildirimGoster = true" gibi bir satır, okunması ve test edilmesi zor bir yapı kurar. Bu tür bir mantık bir fonksiyona taşınmalı ve x-data içinde tanımlanmalıdır.
Üçüncü hata, erişilebilirliği sona bırakmaktır. Görsel olarak tamamlanmış bir bileşene sonradan aria öznitelikleri eklemek, genellikle yapının yeniden düşünülmesini gerektirir. Rol ve durum öznitelikleri, bileşen ilk yazılırken birlikte düşünülmelidir.
Liste ve filtre: x-for ile çalışmak
Alpine ile bir liste üretmek ve filtrelemek, sunucuya gitmeden de mümkündür. x-for direktifi bir şablonu bir dizinin her elemanı için çoğaltır. Filtrelenmiş bir liste için, filtre metni ve sonuç bir arada tutulur; görünüm yalnızca bu sonucu okur.
<div x-data="{
arama: '',
etiketler: ['htmx', 'alpine-js', 'laravel', 'blade', 'tailwind-css'],
get sonuc() {
const aranan = this.arama.trim().toLowerCase();
return aranan === ''
? this.etiketler
: this.etiketler.filter((etiket) => etiket.includes(aranan));
},
}">
<label for="etiket-ara">Etiket ara</label>
<input id="etiket-ara" type="search" x-model="arama">
<ul>
<template x-for="etiket in sonuc" :key="etiket">
<li x-text="etiket"></li>
</template>
</ul>
<p role="status" aria-live="polite">
<span x-text="sonuc.length"></span> etiket bulundu
</p>
</div>Bu örnekte üç ayrıntı önemli. Birincisi, get sonuc() bir hesaplanmış özelliktir ve yalnızca okunduğunda çalışır. İkincisi, :key her elemanı benzersiz tutar; bu olmadan Alpine yeniden çizimde öğeleri yanlış eşleştirebilir. Üçüncüsü, sonuç sayısı canlı bir bölgede duyurulur. Kullanıcı yazarken ekran okuyucu her değişikliği değil, sonuç sayısını duyar.
Büyük listelerde bu yaklaşım bir sınıra gelir. Yüzlerce elemanı her tuş vuruşunda yeniden filtrelemek, tarayıcıda gözle görülür bir gecikme yaratabilir. O noktada sunucu tarafında filtreleme ya da bir HTMX çağrısı daha uygun olur.
Form alanları ve yerel doğrulama
Alpine, bir formun alanlarını anlık olarak izlemek ve kullanıcıya hızlı geri bildirim vermek için de kullanılabilir. Bir e-posta alanının biçimi, kullanıcı yazarken kontrol edilebilir ve hata metni hemen gösterilebilir. Bu, sunucuya gitmeden önce kullanıcıyı yönlendirir.
<div x-data="{ eposta: '', hataliMi() { return this.eposta !== '' && !this.eposta.includes('@'); } }">
<label for="eposta">E-posta</label>
<input id="eposta" name="eposta" type="email" x-model="eposta"
:aria-invalid="hataliMi()" aria-describedby="eposta-hata">
<p id="eposta-hata" x-show="hataliMi()" x-cloak>Geçerli bir adres yazın.</p>
</div>Bu kontrol yalnızca bir kolaylıktır. Asıl doğrulama her zaman sunucuda yapılmalıdır; tarayıcıdaki kontrol atlanabilir ve yalnızca kullanıcıya zaman kazandırır. Bu nedenle istemci tarafı doğrulama, sunucu kuralının bir yansıması olarak düşünülmeli, ona alternatif olarak değil.
Geçişler ve hareket tercihi
x-transition ile gelen varsayılan geçişler çoğu durumda yeterlidir. Ama sürelerini ve yumuşatmayı ayarlamak, arayüzün karakterini değiştirir. Bunun için her aşamayı ayrı ayrı tanımlayabilirsiniz:
<div x-show="acik"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 -translate-y-1"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0">
…
</div>Hareketin bazı kullanıcılar için rahatsız edici olabileceğini unutmayın. İşletim sisteminde hareket azaltma tercihi açıksa, geçiş süresini sıfıra indirmek ya da geçişi tamamen kaldırmak gerekir. Bunu tek bir yerden yönetmek için tercihi bir kez okuyup bir değişkende tutmak yeterlidir:
<body x-data="{ hareketAzalt: window.matchMedia('(prefers-reduced-motion: reduce)').matches }">Bu değer daha sonra panellerin süresine bağlanabilir. Böylece her bileşen aynı kuralı uygular ve bir bileşeni kopyalayan kişi kuralı kendiliğinden miras alır.
Sayfa yüklenirken görünmeyen içerik
Alpine yüklenmeden önce, x-show ile gizlenmiş öğeler kısa süreliğine görünür olabilir. Bu, sayfanın ilk saniyesinde kafa karıştırıcı bir görüntü yaratır. x-cloak özniteliği bu sorunu çözer; ama CSS'te karşılık gelen kuralın da tanımlı olması gerekir:
[x-cloak] {
display: none !important;
}Bu kural yüklenmeden önce öğeleri gizler, Alpine başladığında öznitelik kaldırılır ve öğe normal davranışına döner. Kuralı unutmak, x-cloak özniteliğini tamamen etkisiz bırakır; bu yüzden bu kuralı ana CSS dosyasının başına koymak en güvenli yoldur.
Bileşenleri test etmek
Alpine bileşenlerini test etmenin en gerçekçi yolu, tarayıcı otomasyonu kullanmaktır. Bir modalın açılıp kapandığını, Escape ile kapandığını ve odağın geri döndüğünü bir senaryo olarak yazmak, manuel denetimin yerini tutar. Bu testler yavaş olabilir, bu yüzden yalnızca kritik etkileşimler için kullanılmalıdır.
- Açılış ve kapanış davranışını test edin.
- Klavye ile tüm öğelerin erişilebilir olduğunu doğrulayın.
- Odağın tetikleyiciye döndüğünü kontrol edin.
- Hareket azaltma tercihi açıkken geçişin uygulanmadığını görün.
Sonuç
Alpine.js, bir arayüzün sunucuya gitmesi gerekmeyen kısmını sade ve okunabilir biçimde yönetmek için iyi bir araç. Doğru kullanıldığında, JavaScript dosyalarını küçük tutar, davranışı işaretlendiği yere yakın tutar ve erişilebilirliği bileşen tasarımının bir parçası hâline getirir.
Yeni bir bileşen yazarken şu sırayı izleyin:
- Durumun yalnızca bu bileşene ait olup olmadığına bakın.
- Rol ve durum öznitelikleri olmadan bileşeni yazmaya başlamayın.
- Klavye ile iki kez baştan sona gezin.
- Ekran okuyucuyla, bileşenin ne olduğunu ve ne durumda olduğunu duyup duymadığınızı kontrol edin.
Alpine'in en iyi özelliği, yanlış yapmanın bile görünür olmasıdır: yanlış bir öznitelik, HTML'de hemen göze çarpar.