Temel Seviye Svelte
Svelte’in geliştiriciler tarafından bu kadar sevilmesini sağlayan ve onu diğer önyüz çerçevelerinden farklı kılan şeyin ne olduğunu inceliyoruz.
Svelte Rich Harris tarafından veri görselleştirme amacıyla başlatılan, topluluk tarafından çok sevilen, derlendiği için daha hızlı çalışan, Typescript destekleyen, HTML benzeri bir sözdizime sahip bir açık kaynaklı önyüz çerçevesidir: [https://github.com/sveltejs/svelte]
Svelte’in eğitimini uygulamak, oyun alanında deneme yapmak ve dokümantasyonunu okumak için bu adrese bakabilirsiniz: [https://svelte.dev/tutorial]
Svelte (sıvelt), İngilizce bir kelimedir ve anlamı ince yapılı, fidan gibi zariftir. Ben de o yüzden satırları ince ve zarif tutacağım.
1. İlk Bileşen
App.svelte:
<script lang="ts">
let name = 'Svelte';
</script>
<h1>Merhaba {name.toUpperCase()}!</h1>
2. Dinamik Nitelikler (Dynamic Attributes)
<script>
let src = '/tutorial/kandirdim.gif';
let name = "Kenan Doğulu"
</script>
<img {src} alt="{name} kandırıyor."/>
3. Stilleme (Styling)
<p>Bu bir paragraftır.</p>
<style>
p {
color: goldenrod;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
4. İç İçe Geçmiş Bileşenler (Nested Components)
Diğer dosyalardan bileşenleri içe aktarabilir ve bunları işaretlememize dahil edebiliriz. Bileşen adları HTML öğelerinden ayırt edilebilmesi için büyük harfle yazılmaktadır.
App.svelte:
<script lang="ts">
import Nested from './Nested.svelte';
</script>
<p>Bu bir paragraftır.</p>
<Nested/>
<style>
p {
color: goldenrod;
font-family: 'Comic Sans MS', cursive;
font-size: 2em;
}
</style>
Nested.svelte:
<p>Bu da başka bir paragraftır.</p>
Nested.svelte’nin <p> öğesi olmasına rağmen, App.svelte’den gelen stiller in içeri sızmadığına dikkat edin.
5. HTML Etiketleri (Tags)
Svelte’de {@html …} etiketi, bir bileşenin içinde ham HTML görüntüleştirmesini (render) sağlar. Ancak, Svelte bu içeriği temizlemez; dolayısıyla, HTML güvenilmeyen bir kaynaktan geliyorsa güvenlik riski oluşturur. Bu durum, kötü amaçlı komut dosyalarının kullanıcının tarayıcısında çalışarak hassas verileri çalabileceği veya web sayfası içeriğini değiştirebileceği Siteler Arası Komut Dosyası Çalıştırma (XSS) saldırılarına yol açabilir. Bu tür saldırıları önlemek için, güvenilmeyen girdileri HTML olarak görüntüleştirmeden önce her zaman temizleyin.
<script>
let string = `Bu dizge'nin içerisinde <strong>HTML</strong> var!!!`;
</script>
<p>{@html string}</p>
6. Durum (State)
Svelte’in merkezinde, DOM’u uygulama durumunuzla senkronize tutmak için, örneğin, bir olaya yanıt olarak, güçlü bir reaktivite sistemi bulunur. $state
bir rün olarak adlandırılır ve Svelte'e count
değişkeninin sıradan bir değişken olmadığını bildirmenin yoludur. Rünler fonksiyon gibi görünür, ancak aslında öyle değildirler, dilin kendisinin bir parçasıdırlar.
<script>
let count = $state(0);
function increment() {
count += 1;
}
</script>
<button onclick={increment}>
{count} kez tıklandı
</button>
7. Derin Durum (Deep State)
Önceki egzersizde gördüğümüz gibi, state yeniden atamalara tepki verir. Ancak mutasyonlara da tepki verir, buna derin reaktivite (deep reactivity) denir. Derin reaktivite, proxy’ler kullanılarak uygulanır ve proxy üzerindeki değişiklikler, orijinal nesneyi etkilemez.
<script>
let numbers = $state([1, 2, 3, 4]);
function addNumber() {
numbers.push(numbers.length + 1);
}
</script>
<p>{numbers.join(' + ')} = ...</p>
<button onclick={addNumber}>
Bir sayı ekle
</button>
8. Türev Durum (Derived State)
Sıklıkla, durumu başka bir durumdan türetmeniz gerekecektir. $derived bildiriminin içindeki ifade, bağımlılıkları (bu durumda, sadece sayılar) güncellendiğinde yeniden değerlendirilecektir. Normal durumdan farklı olarak, türetilmiş durum salt okunurdur.
<script>
let numbers = $state([1, 2, 3, 4]);
let total = $derived(numbers.reduce((t, n) => t + n, 0));
function addNumber() {
numbers.push(numbers.length + 1);
}
</script>
<p>{numbers.join(' + ')} = {total}</p>
<button onclick={addNumber}>
Bir sayı ekle
</button>
9. Durumu İnceleme (Inspecting State)
Bir durumun (state) zaman içindeki değişimini takip edebilmek genellikle faydalıdır. Ancak, numbers
gibi reaktif bir proxy'yi doğrudan console.log
ile yazdırmak hata verir, çünkü bu tür veriler klonlanamaz. Bunu aşmak için iki yol vardır:
- Durumun tepkisiz (non-reactive) bir anlık görüntüsünü
$state.snapshot(...)
ile alabilirsiniz. - Durum her değiştiğinde otomatik olarak kaydeden
$inspect
rune’unu kullanabilirsiniz. Bu kodlar üretim (production) sürümünde otomatik olarak kaldırılır. Ayrıca$inspect(...).with(fn)
ile görüntüyü özelleştirebilir, örneğinconsole.trace
ile değişikliğin kaynağını görebilirsiniz.
<script>
let numbers = $state([1, 2, 3, 4]);
let total = $derived(numbers.reduce((t, n) => t + n, 0));
function addNumber() {
numbers.push(numbers.length + 1);
console.log($state.snapshot(numbers));
}
$inspect(numbers).with(console.trace);
</script>
<p>{numbers.join(' + ')} = {total}</p>
<button onclick={addNumber}>
Add a number
</button>
10. Etkiler (Effects)
Şimdiye kadar reaktiviteden bahsederken durumu (state) temel almıştık. Ancak bu sadece işin yarısıdır bir şey tepki vermediği sürece, durum sadece havalı bir değişkendir.
Duruma tepki veren şeye effect (etki) denir. Svelte’in, duruma göre DOM’u güncellemek için sizin yerinize oluşturduğu etkileri zaten görmüşsünüzdür. Ancak kendi etkilerinizi de $effect
anahtarıyla oluşturabilirsiniz.
Çoğu zaman bunu yapmanız gerekmez. $effect
, sık kullanılacak bir araçtan ziyade, mecbur kalındığında başvurulan bir “kaçış yolu” olarak düşünülmelidir. Örneğin, yan etkilerinizi bir olay (event) işleyicisine koyabiliyorsanız, bu neredeyse her zaman daha iyidir.
Etkiler, sunucu tarafı görüntüleştirme işlemi sırasında çalıştırılmaz.
Aşağıda setInterval kullanarak bileşenin ne kadar süredir takılı (mounted) olduğunu ölçmek istiyoruz.
<script>
let elapsed = $state(0);
let interval = $state(1000);
$effect(() => {
setInterval(() => {
elapsed += 1;
}, interval);
});
</script>
<button onclick={() => interval /= 2}>Hızlandır</button>
<button onclick={() => interval *= 2}>Yavaşlat</button>
<p>Geçen: {elapsed}</p>
‘Hızlandır’ düğmesine birkaç kez tıklarsak geçen zamanın daha hızlı arttığını fark edebiliriz, çünkü her aralık küçüldüğünde setInterval’ı çağırıyoruz. Daha sonra ‘yavaşlat’ düğmesine tıklarsak işe yaramadığını görürüz. Bunun nedeni, efekt güncellendiğinde eski aralıkları temizlemememizdir. Bunu bir temizleme işlevi döndürerek düzeltebiliriz:
<script>
let elapsed = $state(0);
let interval = $state(1000);
$effect(() => {
const id = setInterval(() => {
elapsed += 1;
}, interval);
return () => {
clearInterval(id);
};
});
</script>
<button onclick={() => interval /= 2}>Hızlandır</button>
<button onclick={() => interval *= 2}>Yavaşlat</button>
<p>Geçen: {elapsed}</p>
Temizleme fonksiyonu, aralık değiştiğinde ve ayrıca bileşen yok edildiğinde efekt fonksiyonu yeniden çalıştırılmadan hemen önce çağrılır. Eğer efekt fonksiyonu çalışırken herhangi bir durum okumazsa, sadece bir kez, bileşen bağlandığında çalışacaktır.
11. Evrensel Tepkisellik (Universal Reactivity)
Rünleri bileşenlere tepkisellik (reactivity) eklemek için kullandık ancak onları küresel bir durumu paylaşmak için gibi sebeplerle bileşenlerin dışında da kullanabiliriz.
Bu alıştırmadaki <Counter>
bileşenleri, shared.js
dosyasından counter
nesnesini içe aktarıyor. Ancak bu, normal bir nesne olduğundan, butonlara tıklanınca hiçbir şey olmuyor. Nesneyi $state(...)
ile sarmalamak gerekiyor. Fakat böyle bir kullanım hataya sebep olur, çünkü rünler sadece .svelte.js
dosyalarında kullanılabilir, normal .js
dosyalarında kullanılamaz. Bunu düzeltmek için dosya adını shared.svelte.js
olarak değiştiriyoruz. Bu değişiklikten sonra herhangi bir butona tıklandığında, tüm sayaçlar (counter bileşenleri) aynı anda güncellenir hale geliyor.
App.svelte:
<script>
import Counter from './Counter.svelte';
</script>
<Counter />
<Counter />
<Counter />
Counter.svelte:
<script>
import { counter } from './shared.svelte.js';
</script>
<button onclick={() => counter.count += 1}>
tıklama: {counter.count}
</button>
Shared.svelte.js:
export const counter = $state({
count: 0
});
Dikkat: $state
bildirimi bir modülden dışa aktarılırken, eğer bu bildirim yeniden atanırsa (sadece değişikliğe uğratılmazsa), bunu içe aktaranlar bu değişikliği fark edemez. Bu yüzden mutasyon yapılmalıdır, yeniden atama değil.
12. Özellikleri Tanımlama (Declaring Props)
Şu ana kadar yalnızca dahili durumla ilgilendik yani, değerler yalnızca belirli bir bileşen içinde erişilebilirdi. Gerçek bir uygulamada ise, verileri bir bileşenden alt bileşenlerine aktarmanız gerekir. Bunu yapmak için, “özellikler” anlamına gelen ve genellikle “props” olarak kısaltılan yapıları tanımlarız. Svelte’te bunu $props
rünü ile yaparız.
App.svelte:
<script>
import Nested from './Nested.svelte';
</script>
<Nested answer={42} />
Nested.svelte:
<script>
let { answer } = $props();
</script>
<p>Cevap {answer}</p>
13. Varsayılan Değerler (Default Values)
Nested.svelte’de özellikleri (props) için varsayılan değerleri kolayca belirleyebiliriz. Şimdi cevap özelliği (prop’u) olmayan ikinci bir bileşen eklersek, varsayılana değeri alacaktır:
App.svelte:
<script>
import Nested from './Nested.svelte';
</script>
<Nested answer={42} />
<Nested />
Nested.svelte:
<script lang="ts">
let { answer = 'bir gizem'} = $props();
</script>
<p>Cevap {answer}</p>
14. Yayılmış Özellik (Spread Props)
Bu alıştırmada, App.svelte dosyasında PackageInfo.svelte bileşenine beklenen name
prop'unu iletmeyi unuttuğumuz için <code>
etiketi boş kalıyor ve npm bağlantısı çalışmıyor.
App.svelte:
<script>
import PackageInfo from './PackageInfo.svelte';
const pkg = {
name: 'svelte',
version: 5,
description: 'fişek gibi hızlı',
website: 'https://svelte.dev'
};
</script>
<PackageInfo
name={pkg.name}
version={pkg.version}
description={pkg.description}
website={pkg.website}
/>
PackageInfo.svelte:
<script>
let { name, version, description, website } = $props();
</script>
<p>
<code>{name}</code> paketi {description}. Versiyon {version}'i
<a href="https://www.npmjs.com/package/{name}">npm</a> adresinden indir ve <a href={website}>daha fazla bilgi için buraya bak.</a>
</p>
15. Eğer Bloğu (If Block)
HTML’de, koşullar ve döngüler gibi mantığı ifade etmenin bir yolu yoktur. Svelte’de ise vardır. Bazı işaretlemeleri koşullu olarak işlemek için, bunları bir if bloğuna sararız. Örnek olarak sayı 10'dan büyük olduğunda görünen bir metin ekleyelim.
App.svelte:
<script>
let count = $state(0);
function increment() {
count += 1;
}
</script>
<button onclick={increment}>
{count}
{count === 1 ? 'kez' : 'kereler'}
tıklandı
</button>
{#if count > 10}
<p> {count} sayısı 10'dan büyük</p>
{/if}
16. Aksi Halde Bloğu (Else Block)
Tıpkı JavaScript’te olduğu gibi, bir if bloğunun bir else bloğu olabilir: {#…} bir bloğu açar. {/…} bir bloğu kapatır. {:…} bir bloğu devam ettirir.
<script>
let count = $state(0);
function increment() {
count += 1;
}
</script>
<button onclick={increment}>
Clicked {count}
{count === 1 ? 'kez' : 'kereler'}
</button>
{#if count > 10}
<p>{count}, 10'dan büyüktür </p>
{:else}
<p>{count}, 0 ile 10 arasında</p>
{/if}
17. Aksi Halde Eğer Bloğu (Else-if Block)
Birden fazla koşul, else if ile birlikte ‘zincirlenebilir’.
<script>
let count = $state(0);
function increment() {
count += 1;
}
</script>
<button onclick={increment}>
{count}
{count === 1 ? 'kez' : 'kereler'}
tıklandı
</button>
{#if count > 10}
<p>{count} sayısı 10'dan büyüktür</p>
{:else if count < 5}
<p>{count} 5'den azdır</p>
{:else}
<p>{count} sayısı 0 ile 10 arasındadır</p>
{/if}
18. Her Biri Blokları (Each Blocks)
Kullanıcı arayüzleri oluştururken, kendinizi genellikle veri listeleriyle çalışırken bulursunuz. Bu alıştırmada, <button>
etiketini, her seferinde rengini değiştirerek, birçok kez tekrarladık ancak hala eklenmesi gerekenler var. Tek tek kopyalayıp yapıştırmak ve düzenlemek yerine, ilk düğme dışındaki tüm düğmeleri kaldırıp bir each
bloğu kullanmalıyız. Bu ifade (bu durumda colors
) herhangi bir yineleyici (iterable) ya da dizi benzeri (array-like) nesne olabilir başka bir deyişle, Array.from
ile çalışan her şey. Bu değişikliği yaptığımızda aşağıda olması gereken kodu görebilirsiniz.
App.svelte:
<script>
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet'];
let selected = $state(colors[0]);
</script>
<h1 style="color: {selected}">Bir renk seç:</h1>
<div>
{#each colors as color, i}
<button
style="background: {color}"
aria-label={color}
aria-current={selected === color}
onclick={() => selected = color}
>{i +1}</button>
{/each}
</div>
<style>
h1 {
font-size: 2rem;
font-weight: 700;
transition: color 0.2s;
}
div {
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-gap: 5px;
max-width: 400px;
}
button {
aspect-ratio: 1;
border-radius: 50%;
background: var(--color, #fff);
transform: translate(-2px,-2px);
filter: drop-shadow(2px 2px 3px rgba(0,0,0,0.2));
transition: all 0.1s;
color: black;
font-weight: 700;
font-size: 2rem;
}
button[aria-current="true"] {
transform: none;
filter: none;
box-shadow: inset 3px 3px 4px rgba(0,0,0,0.2);
}
</style>
20. Anahtarlı Her Biri Blokları (Keyed Each Blocks)
Varsayılan olarak, bir each
bloğunun değerini güncellemek, boyut değişirse bloğun sonuna DOM düğümleri ekler veya kaldırır ve kalan DOM’u günceller. Ancak bu, istediğiniz davranış olmayabilir. Bunu açıklamaktan ziyade göstermek daha kolaydır. Thing.svelte
dosyasının içinde, name
dinamik bir prop’tur ama emoji
sabittir.
‘İlk öğeyi kaldır’ düğmesine birkaç kez tıklayın ve ne olduğuna dikkat edin:
- Son bileşeni kaldırır.
- Ardından kalan DOM düğümlerindeki
name
değerini günceller (örneğin, ‘doughnut’ içeren metin düğümü artık ‘egg’ içerir) amaemoji
’yi güncellemez.
Not: React’ten geliyorsanız bu size garip gelebilir, çünkü state değiştiğinde tüm bileşenin yeniden render edilmesine alışkınsınızdır. Svelte farklı çalışır: Bileşen bir kez “çalıştırılır” ve sonraki güncellemeler “ince ayarlıdır”. Bu da işlemleri hızlandırır ve size daha fazla kontrol sağlar.
Bunu düzeltmenin bir yolu, emoji
’yi $derived
bir değer yapmak olabilir. Ama tüm Thing
bileşeninin ilkini kaldırmak, sonuncuyu kaldırıp diğerlerini güncellemekten daha mantıklıdır. Bunu yapmak için, each
bloğundaki her yineleme için benzersiz bir anahtar belirtiriz: Svelte dahili olarak bir Map
kullandığından, anahtar olarak herhangi bir nesne kullanabilirsiniz, yani (thing.id)
yerine (thing)
de yazabilirsiniz. Ancak genellikle bir dizge (string) ya da sayı kullanmak daha güvenlidir, çünkü bu, bir API sunucusundan gelen taze verilerle güncellerken gibi senaryolarda kimliğin referans eşitliği olmadan da korunmasını sağlar.
App.svelte:
<script>
import Thing from './Thing.svelte';
let things = $state([
{ id: 1, name: 'elma' },
{ id: 2, name: 'muz' },
{ id: 3, name: 'havuç' },
{ id: 4, name: 'donut' },
{ id: 5, name: 'yumurta' }
]);
</script>
<button onclick={() => things.shift()}>
İlk şeyi kaldır
</button>
{#each things as thing (thing.id)}
<Thing name={thing.name} />
{/each}
Thing.svelte:
<script>
const emojis = {
elma: '🍎',
muz: '🍌',
havuç: '🥕',
donut: '🍩',
yumurta: '🥚'
};
// prop değeri değişince `name` değeri de değişir
let { name } = $props();
// `emoji` değeri başlangıçtan beri sabit kalır
const emoji = emojis[name];
</script>
<p>{emoji} = {name}</p>
21. Bekleme Blokları (Await Blocks)
Çoğu web uygulaması, bir noktada asenkron verilerle uğraşmak zorundadır. Svelte, vaatlerin (promises) değerini doğrudan işaretleme (markup) içinde beklemeyi kolaylaştırır. Yalnızca en son verilen söz (promise) dikkate alınır, bu da yarış durumu (race condition) konusunda endişelenmenize gerek olmadığı anlamına gelir. Eğer sözünüzün (promise) reddedilmeyeceğinden eminseniz, catch bloğunu atlayabilirsiniz. Söz çözülene kadar hiçbir şey göstermek istemiyorsanız, ilk bloğu da atlayabilirsiniz.
utils.js:
export async function roll() {
// 1 ile 6 arasında rastgele bir sayı getir (gecikmeyle getir böylece görebilelim gelişini)
return new Promise((fulfil, reject) => {
setTimeout(() => {
// şansa bağlı olarak bazen başarısız ol böylece kötü bir internet bağlantısını simüle etmiş oluruz
if (Math.random() < 0.3) {
reject(new Error('Request failed'));
return;
}
fulfil(Math.ceil(Math.random() * 6));
}, 1000);
});
}
App.svelte:
<script>
import { roll } from './utils.js';
let promise = $state(roll());
</script>
<button onclick={() => promise = roll()}>
zar at
</button>
{#await promise}
<p>Atılıyor...</p>
{:then number}
<p>{number} attınız!</p>
{:catch error}
<p style="color: red">{error.message}></p>
{/await}
22. DOM Olayları (DOM Events)
Zaten kısaca gördüğümüz gibi, bir element üzerindeki herhangi bir DOM olayını (örneğin click ya da pointermove gibi) on<isim> fonksiyonu ile dinleyebilirsiniz. İsmin değeriyle eşleştiği diğer tüm özelliklerde olduğu gibi, burada da kısa formu kullanabiliriz.
App.svelte:
<script>
let m = $state({ x: 0, y: 0 });
function onpointermove(event) {
m.x = event.clientX;
m.y = event.clientY;
}
</script>
<div {onpointermove}>
İşaretçiniz {Math.round(m.x)} x {Math.round(m.y)} konumunda.
</div>
<style>
div {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
padding: 1rem;
}
</style>
23. Satır İçi Ele Alıcılar (Inline Handlers)
Ayrıca olay işleyicilerini satır içi olarak da bildirebilirsiniz:
App.svelte:
<script>
let m = $state({ x: 0, y: 0 });
</script>
<div onpointermove= {(event) => {
m.x = event.clientX;
m.y = event.clientY;
}}>
İşaretçiniz {Math.round(m.x)} x {Math.round(m.y)} konumunda
</div>
<style>
div {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
padding: 1rem;
}
</style>
24. Yakalama (Capturing)
Normalde, olay yakalayıcıları (event handler) kabarcıklanma (bubbling) aşamasında çalışır. Bu örnekteki <input>
alanına bir şey yazarsanız ne olduğuna dikkat edin. Olay, hedef öğeden belgeye (document) doğru kabarcıklanırken, önce içteki yakalayıcı çalışır, ardından dıştaki.
Bazı durumlarda, olay yakalayıcılarının yakalama (capture) aşamasında çalışmasını isteyebilirsiniz. Bunun için, olay adının sonuna capture
eklemelisiniz.
Artık, çalıştırılma sırası tersine döner. Eğer aynı olay için hem yakalama hem de normal (kabarcıklanma aşamasında çalışan) yakalayıcılar varsa, yakalama aşamasındaki yakalayıcılar önce çalışır.
App.svelte:
<div onkeydowncapture={(e) => alert(`<div> ${e.key}`)} role="presentation">
<input onkeydowncapture={(e) => alert(`<input> ${e.key}`)} />
</div>
25. Bileşen Olayları (Component Events)
Bileşenlere olay yakalayıcılarını (event handler) diğer prop’lar gibi geçirebilirsiniz. Stepper.svelte dosyasında increment
ve decrement
prop'larını ekleyip bunları bağlayalım. App.svelte dosyasında olay yakalayıcıları tanımlayalım.
App.svelte:
<script>
import Stepper from './Stepper.svelte';
let value = $state(0);
</script>
<p>Şu anki değer {value}</p>
<Stepper
increment={() => value += 1}
decrement={() => value -= 1}
/>
Stepper.svelte:
<script lang="ts">
let {increment,decrement} = $props();
</script>
<button onclick={decrement}>-1</button>
<button onclick={increment}>+1</button>
26. Yayılan Olaylar (Spreading Events)
Etkinlik işleyicilerini doğrudan öğelere de yayabiliriz. Burada, App.svelte
içinde bir onclick
işleyicisi tanımladık, tek yapmamız gereken, özellikleri (props
) BigRedButton.svelte
içindeki <button>
öğesine şu şekilde aktarmak: <button {…props}>
.
App.svelte:
<script>
import BigRedButton from './BigRedButton.svelte';
import horn from './horn.mp3';
const audio = new Audio();
audio.src = horn;
function honk() {
audio.load();
audio.play();
}
</script>
<BigRedButton onclick={honk} />
BigRedButton.svelte:
<script>
let props = $props();
</script>
<button {...props}>
BAS
</button>
<style>
button {
font-size: 1.4em;
width: 6em;
height: 6em;
border-radius: 50%;
background: radial-gradient(circle at 25% 25%, hsl(0, 100%, 50%) 0, hsl(0, 100%, 40%) 100%);
box-shadow: 0 8px 0 hsl(0, 100%, 30%), 2px 12px 10px rgba(0,0,0,.35);
color: hsl(0, 100%, 30%);
text-shadow: -1px -1px 2px rgba(0,0,0,0.3), 1px 1px 2px rgba(255,255,255,0.4);
text-transform: uppercase;
letter-spacing: 0.05em;
transform: translate(0, -8px);
transition: all 0.2s;
}
button:active {
transform: translate(0, -2px);
box-shadow: 0 2px 0 hsl(0, 100%, 30%), 2px 6px 10px rgba(0,0,0,.35);
}
</style>
horn.mp3:
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BNwzNcvNxHOU/NctNG0N22NOQMKMMyWGgU8DAM4YdEaEoZtMa1cbd0bdkbd8b1wb+UapWbBubBKZ5GZ4mYgOYgKpFmSCUwAszCE0ik1Ss1TM30U3TE2UM2LpMQU1FMy45N1VVf/75MTwA/q5oKxN81TAAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjk3VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVf/75MTwA8AAAaQAAAAgAAA0gAAABFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjk3VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVRBRzE4MjkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABB/78AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACU
27. Metin Girdileri (Text Inputs)
Genel bir kural olarak, Svelte’de veri akışı yukarıdan aşağıya doğrudur, bir üst bileşen, alt bir bileşene prop atayabilir ve bir bileşen bir element’e nitelik (attribute) atayabilir, ancak bunun tersi mümkün değildir. Bazen bu kuralı bozmak faydalı olabilir. Bu bileşendeki <input>
öğesini ele alalım, bir oninput
olay dinleyicisi ekleyip name
değerini event.target.value
ile güncelleyebiliriz, ancak bu biraz kalıplaşmış olur. Diğer form öğelerinde bu durum daha da kötüleşir, göreceğiz. Bunun yerine bind:value
yönergesini kullanabiliriz. Bu, sadece name
değerindeki değişikliklerin girdi değerini güncellemesini değil, girdi değerindeki değişikliklerin de name
'i güncellemesini sağlar.
App.svelte:
<script>
let name = $state('Dünya');
</script>
<input bind:value={name} />
<h1>Merhaba {name}!</h1>
28. Sayısal Girdiler (Numeric Inputs)
DOM’da her girdi (input) değeri bir dizge olarak tutulur. Bu, sayısal girdilerle, type=”number” ve type=”range”, çalışırken pek yardımcı olmaz; çünkü input.value değerini kullanmadan önce onu cebretmeyi/dönüştürmeyi (coerce) unutmamanız gerekir. Ancak bind:value kullandığınızda, Svelte bunu sizin yerinize halleder.
App.svelte:
<script>
let a = $state(1);
let b = $state(2);
</script>
<label>
<input type="number" bind:value={a} min="0" max="10" />
<input type="range" bind:value={a} min="0" max="10" />
</label>
<label>
<input type="number" bind:value={b} min="0" max="10" />
<input type="range" bind:value={b} min="0" max="10" />
</label>
<p>{a} + {b} = {a + b}</p>
29. Onay Kutusu Girdileri (Checkbox Inputs)
Checkbox’lar durumlar arasında geçiş yapmak için kullanılır. input.value yerine input.checked’e bağlanılır.
App.svelte:
<script>
let yes = $state(false);
</script>
<label>
<input type="checkbox" bind:checked={yes} />
Evet! Bana düzenli e-posta yağdır.
</label>
{#if yes}
<p>
Teşekkür ederiz. Gelen kutunuzu bombardımana tutacağız ve kişisel bilgilerinizi satacağız.
</p>
{:else}
<p>
Devam etmek için katılmanız gerekir. Ödeme yapmıyorsanız, ürün sizsiniz.
</p>
{/if}
<button disabled={!yes}>Abone ol!</button>
30. Seçim Bağlamaları (Select Bindings)
<select>
öğeleriyle birlikte bind:value
kullanabiliriz. Dikkat edin, <option>
değerleri string değil, nesnedir. Svelte için bu sorun değildir. Başlangıçta selected
için bir değer atamadığımızdan, bağlama (binding) bunu varsayılan değere (listedeki ilk öğe) otomatik olarak ayarlayacaktır. Ancak dikkatli olun, bağlama başlatılana kadar selected
tanımsız (undefined) kalır, bu yüzden şablonda örneğin selected.id
gibi ifadelere körü körüne başvuramayız.
App.svelte:
<script>
let questions = $state([
{
id: 1,
text: `Okula nerede gittiniz?`
},
{
id: 2,
text: `Annenizin ismi nedir?`
},
{
id: 3,
text: `Bir saldırganın Google ile kolayca bulabileceği başka bir kişisel bilgi nedir?`
}
]);
let selected = $state();
let answer = $state('');
function handleSubmit(e) {
e.preventDefault();
alert(
`answered question ${selected.id} (${selected.text}) with "${answer}"`
);
}
</script>
<h2>Insecurity questions</h2>
<form onsubmit={handleSubmit}>
<select
bind:value={selected}
onchange={() => (answer = '')}
>
{#each questions as question}
<option value={question}>
{question.text}
</option>
{/each}
</select>
<input bind:value={answer} />
<button disabled={!answer} type="submit">
Yolla
</button>
</form>
<p>
Seçilen soru {selected
? selected.id
: '[bekleniyor...]'}
</p>
31. Grup Girdileri (Group Inputs)
Eğer aynı değere ait birden fazla type=”radio” veya type=”checkbox” girdi varsa, bunlarla birlikte value
özelliğini kullanarak bind:group
özelliğinden faydalanabilirsiniz. Aynı gruba ait radyo girdileri birbirini dışlar; aynı gruba ait onay kutusu girdileri ise seçilen değerlerden oluşan bir dizi oluşturur. Radyo girdilerine bind:group={scoops}
, onay kutusu girdilerine ise bind:group={flavours}
ekleyin.
App.svelte:
<script>
let scoops = $state(1);
let flavours = $state([]);
const formatter = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' });
</script>
<h2>Boyut</h2>
{#each [1, 2, 3] as number}
<label>
<input
type="radio"
name="scoops"
value={number}
bind:group={scoops}
/>
{number} top
</label>
{/each}
<h2>Aromalar</h2>
{#each ['kurabiyeli krema', 'naneli çikolata parçacıklı', 'ahududu'] as flavour}
<label>
<input
type="checkbox"
name="flavours"
value={flavour}
bind:group={flavours}
/>
{flavour}
</label>
{/each}
{#if flavours.length === 0}
<p>Lütfen en az bir aroma seçin.</p>
{:else if flavours.length > scoops}
<p>Seçtiğiniz top sayısından fazla aroma seçemezsiniz!</p>
{:else}
<p>
{scoops} top {formatter.format(flavours)} aromalı sipariş verdiniz.
</p>
{/if}
32. Çoklu Seçim (Select Multiple)
Bir <select>
elementi, çoklu seçim yapılabilmesini sağlayan multiple
niteliğine sahip olabilir. Bu durumda, tek bir değer yerine bir dizi (array) oluşturur. Onay kutularını (checkbox) bir <select multiple>
elementi ile değiştirin. <option>
etiketi üzerinde value
niteliğini atlayabildiğimize dikkat edin, çünkü değer, elementin içeriğiyle aynıdır. Birden fazla seçenek seçmek için kontrol tuşuna (MacOS'ta komut tuşuna) basılı tutun.
App.svelte:
<script>
let scoops = $state(1);
let flavours = $state([]);
const formatter = new Intl.ListFormat('tr', { style: 'long', type: 'conjunction' });
</script>
<h2>Boyut</h2>
{#each [1, 2, 3] as number}
<label>
<input
type="radio"
name="scoops"
value={number}
bind:group={scoops}
/>
{number} {number === 1 ? 'top' : 'top'}
</label>
{/each}
<h2>Aromalar</h2>
<select multiple bind:value={flavours}>
{#each ['kurabiyeli kremalı', 'naneli çikolatalı', 'frambuazlı'] as flavour}
<option>{flavour}</option>
{/each}
</select>
{#if flavours.length === 0}
<p>Lütfen en az bir aroma seçin</p>
{:else if flavours.length > scoops}
<p>Top sayısından fazla aroma seçemezsiniz!</p>
{:else}
<p>
{scoops} {scoops === 1 ? 'top' : 'top'}
{formatter.format(flavours)} aromalı dondurma sipariş ettiniz
</p>
{/if}
33. Metin Alanı Girdileri (TextArea Inputs)
<textarea>
elementi, Svelte'te bir metin girdisi (text input) gibi davranır,bind:value
kullanılır. Bu gibi durumlarda, adlar eşleştiğinde, kısaltma biçimini de kullanabiliriz. Bu tüm bağlamalar (binding) için geçerlidir, sadece <textarea>
bağlamaları için değil.
App.svelte:
<script>
import { marked } from 'marked';
let value = $state(`Bazı kelimeler *eğik*, bazıları **kalın**\n\n- listeler\n- de\n- havalıdır`);
</script>
<div class="grid">
Girdi
<textarea bind:value></textarea>
Çıktı
<div>{@html marked(value)}</div>
</div>
<style>
.grid {
display: grid;
grid-template-columns: 5em 1fr;
grid-template-rows: 1fr 1fr;
grid-gap: 1em;
height: 100%;
}
textarea {
flex: 1;
resize: none;
}
</style>
34. Sınıf Niteliği (The Class Attribute)
Herhangi bir başka özellik gibi, JavaScript özelliğiyle sınıflar da belirtebilirsiniz. Burada, karta flipped
(çevrilmiş) sınıfını ekleyebiliriz: Bu beklediğimiz gibi çalışır, artık karta tıkladığınızda dönecektir. Ancak bunu daha güzel hâle getirebiliriz. Bir koşula bağlı olarak bir sınıf eklemek veya kaldırmak, arayüz (UI) geliştirmede o kadar yaygın bir kalıptır ki, Svelte size clsx
tarafından string'e dönüştürülen bir nesne veya dizi geçirmenize izin verir. Bu şu anlama gelir: "Her zaman card
sınıfını ekle, ve flipped
doğruyken flipped
sınıfını da ekle." Koşullu sınıfların nasıl birleştirileceğine dair daha fazla örnek için sınıf dokümantasyonuna bakabilirsiniz: [https://svelte.dev/docs/svelte/class]
App.svelte:
<script>
let flipped = $state(false);
</script>
<div class="container">
Kartı ters çevir
<button
class={["card", {flipped}]}
onclick={() => flipped = !flipped}
>
<div class="front">
<span class="symbol">♠</span>
</div>
<div class="back">
<div class="pattern"></div>
</div>
</button>
</div>
<style>
.container {
display: flex;
flex-direction: column;
gap: 1em;
height: 100%;
align-items: center;
justify-content: center;
perspective: 100vh;
}
.card {
position: relative;
aspect-ratio: 2.5 / 3.5;
font-size: min(1vh, 0.25rem);
height: 80em;
background: var(--bg-1);
border-radius: 2em;
transform: rotateY(180deg);
transition: transform 0.4s;
transform-style: preserve-3d;
padding: 0;
user-select: none;
cursor: pointer;
}
.card.flipped {
transform: rotateY(0);
}
.front, .back {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
backface-visibility: hidden;
border-radius: 2em;
border: 1px solid var(--fg-2);
box-sizing: border-box;
padding: 2em;
}
.front {
background: url(./svelte-logo.svg) no-repeat 5em 5em, url(./svelte-logo.svg) no-repeat calc(100% - 5em) calc(100% - 5em);
background-size: 8em 8em, 8em 8em;
}
.back {
transform: rotateY(180deg);
}
.symbol {
font-size: 30em;
color: var(--fg-1);
}
.pattern {
width: 100%;
height: 100%;
background-color: var(--bg-2);
/* pattern from https://projects.verou.me/css3patterns/#marrakesh */
background-image:
radial-gradient(var(--bg-3) 0.9em, transparent 1em),
repeating-radial-gradient(var(--bg-3) 0, var(--bg-3) 0.4em, transparent 0.5em, transparent 2em, var(--bg-3) 2.1em, var(--bg-3) 2.5em, transparent 2.6em, transparent 5em);
background-size: 3em 3em, 9em 9em;
background-position: 0 0;
border-radius: 1em;
}
</style>
svelte-logo.svg:
<svg viewBox='0 0 98.2 118' xmlns='http://www.w3.org/2000/svg'><path fill='#ff3e00' d='m91.9 15.6c-10.9-15.7-32.6-20.3-48.2-10.3l-27.5 17.5c-7.5 4.7-12.7 12.4-14.2 21.1-1.3 7.3-.2 14.8 3.3 21.3-2.4 3.6-4 7.6-4.7 11.8-1.6 8.9.5 18.1 5.7 25.4 11 15.7 32.6 20.3 48.2 10.4l27.5-17.5c7.5-4.7 12.7-12.4 14.2-21.1 1.3-7.3.2-14.8-3.3-21.3 2.4-3.6 4-7.6 4.7-11.8 1.6-9-.4-18.2-5.7-25.5m-50.9 88.3c-8.9 2.3-18.2-1.2-23.4-8.7-3.2-4.4-4.4-9.9-3.5-15.3.2-.9.4-1.7.6-2.6l.5-1.6 1.4 1c3.3 2.4 6.9 4.2 10.8 5.4l1 .3-.1 1c-.1 1.4.3 2.9 1.1 4.1 1.6 2.3 4.4 3.4 7.1 2.7.6-.2 1.2-.4 1.7-.7l27.4-17.5c1.4-.9 2.3-2.2 2.6-3.8s-.1-3.3-1-4.6c-1.6-2.3-4.4-3.3-7.1-2.6-.6.2-1.2.4-1.7.7l-10.4 6.6c-1.7 1.1-3.6 1.9-5.6 2.4-8.9 2.3-18.2-1.2-23.4-8.7-3.1-4.4-4.4-9.9-3.4-15.3.9-5.2 4.1-9.9 8.6-12.7l27.4-17.5c1.7-1.1 3.6-1.9 5.6-2.5 8.9-2.3 18.2 1.2 23.4 8.7 3.2 4.4 4.4 9.9 3.5 15.3-.2.9-.4 1.7-.7 2.6l-.5 1.6-1.4-1c-3.3-2.4-6.9-4.2-10.8-5.4l-1-.3.1-1c.1-1.4-.3-2.9-1.1-4.1-1.6-2.3-4.4-3.3-7.1-2.6-.6.2-1.2.4-1.7.7l-27.4 17.6c-1.4.9-2.3 2.2-2.6 3.8s.1 3.3 1 4.6c1.6 2.3 4.4 3.3 7.1 2.6.6-.2 1.2-.4 1.7-.7l10.5-6.7c1.7-1.1 3.6-1.9 5.6-2.5 8.9-2.3 18.2 1.2 23.4 8.7 3.2 4.4 4.4 9.9 3.5 15.3-.9 5.2-4.1 9.9-8.6 12.7l-27.4 17.5c-1.8 1.1-3.7 1.9-5.7 2.5'/></svg>
35. Stil Yönergesi (The Style Directive)
Sınıfta olduğu gibi, satır içi stil özelliklerinizi doğrudan yazabilirsiniz, çünkü Svelte aslında süslü kısımlarla birlikte sadece HTML’dir. Çok fazla stiliniz olduğunda, işler biraz garip görünmeye başlayabilir. Bunu düzene sokmak için style:
yönergesini kullanabiliriz.
svelte-logo.svg:
<svg viewBox='0 0 98.2 118' xmlns='http://www.w3.org/2000/svg'><path fill='#ff3e00' d='m91.9 15.6c-10.9-15.7-32.6-20.3-48.2-10.3l-27.5 17.5c-7.5 4.7-12.7 12.4-14.2 21.1-1.3 7.3-.2 14.8 3.3 21.3-2.4 3.6-4 7.6-4.7 11.8-1.6 8.9.5 18.1 5.7 25.4 11 15.7 32.6 20.3 48.2 10.4l27.5-17.5c7.5-4.7 12.7-12.4 14.2-21.1 1.3-7.3.2-14.8-3.3-21.3 2.4-3.6 4-7.6 4.7-11.8 1.6-9-.4-18.2-5.7-25.5m-50.9 88.3c-8.9 2.3-18.2-1.2-23.4-8.7-3.2-4.4-4.4-9.9-3.5-15.3.2-.9.4-1.7.6-2.6l.5-1.6 1.4 1c3.3 2.4 6.9 4.2 10.8 5.4l1 .3-.1 1c-.1 1.4.3 2.9 1.1 4.1 1.6 2.3 4.4 3.4 7.1 2.7.6-.2 1.2-.4 1.7-.7l27.4-17.5c1.4-.9 2.3-2.2 2.6-3.8s-.1-3.3-1-4.6c-1.6-2.3-4.4-3.3-7.1-2.6-.6.2-1.2.4-1.7.7l-10.4 6.6c-1.7 1.1-3.6 1.9-5.6 2.4-8.9 2.3-18.2-1.2-23.4-8.7-3.1-4.4-4.4-9.9-3.4-15.3.9-5.2 4.1-9.9 8.6-12.7l27.4-17.5c1.7-1.1 3.6-1.9 5.6-2.5 8.9-2.3 18.2 1.2 23.4 8.7 3.2 4.4 4.4 9.9 3.5 15.3-.2.9-.4 1.7-.7 2.6l-.5 1.6-1.4-1c-3.3-2.4-6.9-4.2-10.8-5.4l-1-.3.1-1c.1-1.4-.3-2.9-1.1-4.1-1.6-2.3-4.4-3.3-7.1-2.6-.6.2-1.2.4-1.7.7l-27.4 17.6c-1.4.9-2.3 2.2-2.6 3.8s.1 3.3 1 4.6c1.6 2.3 4.4 3.3 7.1 2.6.6-.2 1.2-.4 1.7-.7l10.5-6.7c1.7-1.1 3.6-1.9 5.6-2.5 8.9-2.3 18.2 1.2 23.4 8.7 3.2 4.4 4.4 9.9 3.5 15.3-.9 5.2-4.1 9.9-8.6 12.7l-27.4 17.5c-1.8 1.1-3.7 1.9-5.7 2.5'/></svg>
App.svelte:
<script>
let flipped = $state(false);
</script>
<div class="container">
Kartı çevir
<button
class="card"
style:transform={flipped ? 'rotateY(0)' : ''}
style:--bg-1="palegoldenrod"
style:--bg-2="black"
style:--bg-3="goldenrod"
onclick={() => flipped = !flipped}
>
<div class="front">
<span class="symbol">♠</span>
</div>
<div class="back">
<div class="pattern"></div>
</div>
</button>
</div>
<style>
.container {
display: flex;
flex-direction: column;
gap: 1em;
height: 100%;
align-items: center;
justify-content: center;
perspective: 100vh;
}
.card {
position: relative;
aspect-ratio: 2.5 / 3.5;
font-size: min(1vh, 0.25rem);
height: 80em;
background: var(--bg-1);
border-radius: 2em;
transform: rotateY(180deg);
transition: transform 0.4s;
transform-style: preserve-3d;
padding: 0;
user-select: none;
cursor: pointer;
}
.card.flipped {
transform: rotateY(0);
}
.front, .back {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
backface-visibility: hidden;
border-radius: 2em;
border: 1px solid var(--fg-2);
box-sizing: border-box;
padding: 2em;
}
.front {
background: url(./svelte-logo.svg) no-repeat 5em 5em, url(./svelte-logo.svg) no-repeat calc(100% - 5em) calc(100% - 5em);
background-size: 8em 8em, 8em 8em;
}
.back {
transform: rotateY(180deg);
}
.symbol {
font-size: 30em;
color: var(--fg-1);
}
.pattern {
width: 100%;
height: 100%;
background-color: var(--bg-2);
/* pattern from https://projects.verou.me/css3patterns/#marrakesh */
background-image:
radial-gradient(var(--bg-3) 0.9em, transparent 1em),
repeating-radial-gradient(var(--bg-3) 0, var(--bg-3) 0.4em, transparent 0.5em, transparent 2em, var(--bg-3) 2.1em, var(--bg-3) 2.5em, transparent 2.6em, transparent 5em);
background-size: 3em 3em, 9em 9em;
background-position: 0 0;
border-radius: 1em;
}
</style>
36. Bileşen Stilleri (Component Styles)
Çoğu zaman, bir alt bileşenin içindeki stilleri etkilemeniz gerekir. Belki bu kutuları kırmızı, yeşil ve mavi yapmak istiyoruz. Bunu yapmanın bir yolu, diğer bileşenlerin içindeki öğeleri ayırt etmeden hedeflemenizi sağlayan :global
CSS değiştiricisini kullanmaktır. Ancak bunu yapmamak için birçok neden var. Birincisi, son derece uzun ve karmaşıktır. İkincisi, kırılgandır. Box.svelte dosyasındaki uygulama ayrıntılarında yapılacak herhangi bir değişiklik seçiciyi bozabilir. Ama en önemlisi, bu kabalıktır. Bileşenler, hangi stil özelliklerinin "dışarıdan" kontrol edilebileceğine kendileri karar verebilmelidir; tıpkı hangi değişkenlerin prop olarak dışa açılacağına karar verdikleri gibi. :global
son çare olarak bir kaçış kapısı olarak kullanılmalıdır.
Box.svelte içinde background-color
değerini bir CSS özel özelliği (custom property) tarafından belirlenecek şekilde değiştirin. Herhangi bir üst öğe (örneğin <div class="boxes">
) --color
değerini ayarlayabilir, ama aynı zamanda bunu tekil bileşenler üzerinde de belirleyebiliriz: Değerler, diğer tüm özellikler gibi dinamik olabilir. Bu özellik, gerektiğinde her bileşeni display: contents
özelliğine sahip bir öğe içinde sarmalayarak ve özel özellikleri bu öğeye uygulayarak çalışır. Öğeleri inceleyecek olursanız, bu şekilde bir işaretleme (markup) görürsünüz. display: contents
özelliği sayesinde bu, düzeninizi (layout) etkilemez, ancak eklenen öğe, .parent > .child
gibi seçicileri etkileyebilir.
App.svelte:
<script>
import Box from './Box.svelte';
</script>
<div class="boxes">
<Box --color="red" />
<Box --color="green" />
<Box --color="blue" />
</div>
Box.svelte:
<div class="box"></div>
<style>
.box {
width: 5em;
height: 5em;
border-radius: 0.5em;
margin: 0 0 1em 0;
background-color: var(--color, #ddd);
}
</style>
37. Kullanım Yönergesi (The Use Directive)
Eylemler, esasen öğe düzeyinde yaşam döngüsü (lifecycle) fonksiyonlarıdır. Şu gibi durumlarda faydalıdırlar:
- Üçüncü parti kütüphanelerle arayüz oluşturmak.
- Tembel yüklenen (lazy-loaded) görseller.
- Araç ipuçları (tooltips).
- Özel olay dinleyicileri (event handler) eklemek.
Bu uygulamada, <canvas>
üzerinde karalayabilir, menü aracılığıyla renkleri ve fırça boyutunu değiştirebilirsiniz. Ancak menüyü açıp Sekme (Tab) tuşuyla seçenekler arasında geçiş yaparsanız, odaklamanın modal (açılır pencere) içinde kilitlenmediğini fark edeceksiniz. Bunu bir eylem (action) ile düzeltebiliriz. actions.svelte.js
dosyasından trapFocus
fonksiyonunu içe aktaralım ve ardından use:
yönergesi ile bu eylemi menüye ekleyelim. Şimdi actions.svelte.js
içindeki trapFocus
fonksiyonuna bir göz atalım. Bir eylem fonksiyonu, bir düğüm (bizim durumumuzda <div class="menu">
) DOM'a eklendiğinde çağrılır.
Eylemin içinde bir etki (effect) oluştururuz. Öncelikle, Tab tuşuna basıldığında bunu yakalayacak bir olay dinleyicisi eklememiz gerekir. İkinci olarak, node DOM’dan kaldırıldığında bazı temizlik işlemleri yapmamız gerekir. Buna örnek olay dinleyicisini kaldırmak ve odaklamayı, öğe eklenmeden önceki yerine geri döndürmektir. Artık menüyü açtığınızda, Tab tuşuyla seçenekler arasında geçiş yapabilirsiniz.
App.svelte:
<script>
import Canvas from './Canvas.svelte';
import { trapFocus } from './actions.svelte.js';
const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'white', 'black'];
let selected = $state(colors[0]);
let size = $state(10);
let showMenu = $state(true);
</script>
<div class="container">
<Canvas color={selected} size={size} />
{#if showMenu}
<div
role="presentation"
class="modal-background"
onclick={(event) => {
if (event.target === event.currentTarget) {
showMenu = false;
}
}}
onkeydown={(e) => {
if (e.key === 'Escape') {
showMenu = false;
}
}}
>
<div class="menu" use:trapFocus>
<div class="colors">
{#each colors as color}
<button
class="color"
aria-label={color}
aria-current={selected === color}
style="--color: {color}"
onclick={() => {
selected = color;
}}
></button>
{/each}
</div>
<label>
küçük
<input type="range" bind:value={size} min="1" max="50" />
büyük
</label>
</div>
</div>
{/if}
<div class="controls">
<button class="show-menu" onclick={() => showMenu = !showMenu}>
{showMenu ? 'kapat' : 'menü'}
</button>
</div>
</div>
<style>
.container {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.controls {
position: absolute;
left: 0;
top: 0;
padding: 1em;
}
.show-menu {
width: 5em;
}
.modal-background {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
left: 0;
top: 0;
width: 100%;
height: 100%;
backdrop-filter: blur(20px);
}
.menu {
position: relative;
background: var(--bg-2);
width: calc(100% - 2em);
max-width: 28em;
padding: 1em 1em 0.5em 1em;
border-radius: 1em;
box-sizing: border-box;
user-select: none;
}
.colors {
display: grid;
align-items: center;
grid-template-columns: repeat(9, 1fr);
grid-gap: 0.5em;
}
.color {
aspect-ratio: 1;
border-radius: 50%;
background: var(--color, #fff);
transform: none;
filter: drop-shadow(2px 2px 3px rgba(0,0,0,0.2));
transition: all 0.1s;
}
.color[aria-current="true"] {
transform: translate(1px, 1px);
filter: none;
box-shadow: inset 3px 3px 4px rgba(0,0,0,0.2);
}
.menu label {
display: flex;
width: 100%;
margin: 1em 0 0 0;
}
.menu input {
flex: 1;
}
</style>
Canvas.svelte:
<script>
let { color, size } = $props();
let canvas = $state();
let context = $state();
let coords = $state();
$effect(() => {
context = canvas.getContext('2d');
resize();
});
function resize() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
</script>
<svelte:window onresize={resize} />
<canvas
bind:this={canvas}
onpointerdown={(e) => {
coords = { x: e.offsetX, y: e.offsetY };
context.fillStyle = color;
context.beginPath();
context.arc(coords.x, coords.y, size / 2, 0, 2 * Math.PI);
context.fill();
}}
onpointerleave={() => {
coords = null;
}}
onpointermove={(e) => {
const previous = coords;
coords = { x: e.offsetX, y: e.offsetY };
if (e.buttons === 1) {
e.preventDefault();
context.strokeStyle = color;
context.lineWidth = size;
context.lineCap = 'round';
context.beginPath();
context.moveTo(previous.x, previous.y);
context.lineTo(coords.x, coords.y);
context.stroke();
}
}}
></canvas>
{#if coords}
<div
class="preview"
style="--color: {color}; --size: {size}px; --x: {coords.x}px; --y: {coords.y}px"
></div>
{/if}
<style>
canvas {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.preview {
position: absolute;
left: var(--x);
top: var(--y);
width: var(--size);
height: var(--size);
transform: translate(-50%, -50%);
background: var(--color);
border-radius: 50%;
opacity: 0.5;
pointer-events: none;
}
</style>
actions.svelte.js:
export function trapFocus(node) {
const previous = document.activeElement;
function focusable() {
return Array.from(node.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'));
}
function handleKeydown(event) {
if (event.key !== 'Tab') return;
const current = document.activeElement;
const elements = focusable();
const first = elements.at(0);
const last = elements.at(-1)
if (event.shiftKey && current === first) {
last.focus();
event.preventDefault();
}
if (!event.shiftKey && current === last) {
first.focus();
event.preventDefault();
}
}
$effect(() => {
focusable()[0]?.focus();
node.addEvenLisener('keydown', handleKeydown);
return () => {
node.removeEventListener('keydown', handleKeydown);
previous?.focus();
};
});
}
38. Parametre Ekleme (Adding Parameters)
Geçişler ve animasyonlar gibi, bir eylem de bir argüman alabilir; bu argüman, eylem fonksiyonu çağrılırken ait olduğu öğeyle birlikte verilir. Bu alıştırmada, Tippy.js kütüphanesini kullanarak <button>
öğesine bir bilgi balonu (tooltip) eklemek istiyoruz. Eylem zaten use:tooltip
ile bağlanmış durumda, ancak butonun üzerine geldiğinizde (veya klavye ile odaklandığınızda) bilgi balonu herhangi bir içerik göstermiyor.
İlk olarak, eylemin Tippy’ye iletilecek bazı seçenekleri (options) döndüren bir fonksiyonu kabul etmesi gerekiyor. Burada doğrudan seçenekleri değil, bir fonksiyonu geçiriyoruz çünkü seçenekler değiştiğinde tooltip
fonksiyonu yeniden çalıştırılmıyor. Sonrasında, bu seçenekleri eyleme iletmemiz gerekiyor: Svelte 4’te eylemler update
ve destroy
metotlarını içeren bir nesne döndürüyordu. Bu hâlâ çalışıyor, ancak daha fazla esneklik ve detay kontrolü sunduğu için $effect
kullanmanızı öneriyoruz.
App.svelte:
<script>
import tippy from 'tippy.js';
let content = $state('Hello!');
function tooltip(node, fn) {
$effect(() => {
const tooltip = tippy(node, fn());
return tooltip.destroy;
});
}
</script>
<input bind:value={content} />
<button use:tooltip={() => ({content})}>
Üzerime gel
</button>
<style>
:global {
[data-tippy-root] {
--bg: #666;
background-color: var(--bg);
color: white;
border-radius: 0.2rem;
padding: 0.2rem 0.6rem;
filter: drop-shadow(1px 1px 3px rgb(0 0 0 / 0.1));
* {
transition: none;
}
}
[data-tippy-root]::before {
--size: 0.4rem;
content: '';
position: absolute;
left: calc(50% - var(--size));
top: calc(-2 * var(--size) + 1px);
border: var(--size) solid transparent;
border-bottom-color: var(--bg);
}
}
</style>
39. Geçiş Yönergesi (The Transition Directive)
DOM’a girip çıkan öğeleri zarif geçişlerle yöneterek daha çekici kullanıcı arayüzleri oluşturabiliriz. Svelte, bu işlemi transition
yönergesiyle çok kolaylaştırır. Öncelikle, svelte/transition
modülünden fade
fonksiyonunu içe aktarın sonra bunu <p>
öğesine ekleyin:
App.svelte:
<script lang="ts">
import { fade } from 'svelte/transition';
let visible = $state(true);
</script>
<label>
<input type="checkbox" bind:checked={visible} />
Görünür
</label>
{#if visible}
<p transition:fade>
Geçişimle (fade/hafif geçiş) görünüp kayboluyor.
</p>
{/if}
40. Parametre Ekleme (Adding Parameters)
Geçiş (transition) fonksiyonları parametre alabilir. Fade geçişini fly (uçma) ile değiştirin ve bunu bazı seçeneklerle birlikte <p>
etiketine uygulayın: Geçişin tersine çevrilebilir olduğunu unutmayın, geçiş devam ederken onay kutusunu (checkbox) değiştirirseniz, geçiş başlangıçtan veya sondan değil, mevcut noktadan itibaren gerçekleşir.
App.svelte:
<script lang="ts">
import { fly } from 'svelte/transition';
let visible = $state(true);
</script>
<label>
<input type="checkbox" bind:checked={visible} />
Görünür
</label>
{#if visible}
<p transition:fly = {{y:200,duration:2000}}>
Uçarak belirip kaybolur
</p>
{/if}
41. Giriş ve çıkış (In and Out)
Geçiş yönergesi yerine, bir öğe in (giriş) veya out (çıkış) yönergesine ya da her ikisine birden sahip olabilir. fade
efektini fly
ile birlikte içe aktarın ardından geçiş yönergesini, ayrı in ve out yönergeleriyle değiştirin. Bu durumda, geçişler tersine çevrilmez.
App.svelte:
<script>
import { fade, fly } from 'svelte/transition';
let visible = $state(true);
</script>
<label>
<input type="checkbox" bind:checked={visible} />
Görünür
</label>
{#if visible}
<p in:fly={{ y: 200, duration: 2000 }} out:fade>
Uçarak gelir, geçişimle gider
</p>
{/if}
42. Özel CSS Geçişleri (Custom CSS Transitions)
Svelte/transition modülü birkaç yerleşik geçiş efekti içerir, ancak kendi geçişlerinizi oluşturmak oldukça kolaydır. Örneğin, işte fade (geçişim) geçişinin kaynak kodu:
function fade(node, { delay = 0, duration = 400 }) {
const o = +getComputedStyle(node).opacity;
return {
delay,
duration,
css: (t) => `opacity: ${t * o}`
};
}
Fonksiyon iki argüman alır: Geçişin uygulanacağı düğüm (node) ve geçirilen parametreler. Bu fonksiyon, aşağıdaki özelliklere sahip olabilen bir geçiş (transition) nesnesi döndürür:
- delay: Geçişin başlamasından önceki milisaniye cinsinden gecikme süresi.
- duration: Geçişin milisaniye cinsinden süresi.
- easing: Bir
p => t
easing (yumuşatma) fonksiyonu (tweening bölümüne bakın). - css:
(t, u) => css
biçiminde bir fonksiyon, buradau === 1 - t.
- tick: Düğüm üzerinde bir etki oluşturan
(t, u) => {...}
biçiminde bir fonksiyon.
t
değeri, bir giriş (intro) animasyonunun başında veya bir çıkış (outro) animasyonunun sonunda 0’dır; bir giriş animasyonunun sonunda veya çıkış animasyonunun başında ise 1 olur.
Çoğu zaman tick
özelliğini değil, css
özelliğini döndürmelisiniz; çünkü CSS animasyonları, mümkün olduğunca takılmaları (jank) önlemek için ana iş parçacığının dışında çalışır. Svelte, geçişi "simüle eder" ve bir CSS animasyonu oluşturur, ardından bu animasyonun kendi kendine çalışmasına izin verir.
Örneğin, fade geçişi aşağı yukarı şu şekilde bir CSS animasyonu üretir:
0% { opacity: 0 }
10% { opacity: 0.1 }
20% { opacity: 0.2 }
/* ... */
100% { opacity: 1 }
Ama çok daha yaratıcı olabiliriz. Hadi gerçekten gereksiz bir şey yapalım :).
App.svelte:
<script lang="ts">
import { fade } from 'svelte/transition';
import { elasticOut } from 'svelte/easing';
let visible = $state(true);
function spin(node, { duration }) {
return {
duration,
css: (t, u) => {
const eased = elasticOut(t);
return `
transform: scale(${eased}) rotate(${eased * 1080}deg);
color: hsl(
${Math.trunc(t * 360)},
${Math.min(100, 1000 * u)}%,
${Math.min(50, 500 * u)}%
);`
}
};
}
</script>
<label>
<input type="checkbox" bind:checked={visible} />
Görünür
</label>
{#if visible}
<div
class="centered"
in:spin={{ duration: 8000 }}
out:fade
>
<span>Geçişler!</span>
</div>
{/if}
<style>
.centered {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
span {
position: absolute;
transform: translate(-50%, -50%);
font-size: 4em;
}
</style>
43. Özel JS Geçişleri (Custom JS Transitions)
Genel olarak geçişler için mümkün olduğunca CSS kullanmanız gerekse de, daktilo efekti gibi bazı efektler JavaScript olmadan gerçekleştirilemez.
App.svelte:
<script>
let visible = $state(false);
function typewriter(node, { speed = 1 }) {
const valid = node.childNodes.length === 1 && node.childNodes[0].nodeType === Node.TEXT_NODE;
if (!valid) {
throw new Error(`This transition only works on elements with a single text node child`);
}
const text = node.textContent;
const duration = text.length / (speed * 0.01);
return {
duration,
tick: (t) => {
const i = Math.trunc(text.length * t);
node.textContent = text.slice(0, i);
}
};
}
</script>
<label>
<input type="checkbox" bind:checked={visible} />
Görünür
</label>
{#if visible}
<p transition:typewriter>
Pijamalı hasta yağız şoföre çabucak güvendi.
</p>
{/if}
44. Geçiş Olayları (Transition Events)
Geçişlerin ne zaman başlayıp ne zaman bittiğini bilmek faydalı olabilir. Svelte, diğer DOM olayları gibi dinleyebileceğiniz olaylar (event) gönderir (dispatch):
App.svelte:
<script>
import { fly } from 'svelte/transition';
let visible = $state(true);
let status = $state('bekleniyor...');
</script>
<p>Durum: {status}</p>
<label>
<input type="checkbox" bind:checked={visible} />
Görünür
</label>
{#if visible}
<p
transition:fly={{ y: 200, duration: 2000 }}
onintrostart={()=> status = 'Giriş başladı'}
onoutrostart={()=> status = 'Çıkış başladı'}
onintroend={()=> status = 'Giriş bitti'}
onoutroend={()=>status='Çıkış bitti'}
>
Uçarak girer ve çıkar
</p>
{/if}
45. Küresel Geçişler (Global Transitions)
Normalde geçişler, yalnızca doğrudan kapsayıcı blokları eklendiğinde veya kaldırıldığında öğelerde çalışır. Buradaki örnekte, listenin tamamının görünürlüğünü değiştirmek, bireysel liste öğelerine geçiş efektlerini uygulamaz.
Bunun yerine, yalnızca kaydırıcıyla bireysel öğeler eklenip kaldırıldığında değil, aynı zamanda onay kutusunu değiştirdiğimizde de geçişlerin çalışmasını istiyoruz. Bunu, geçişleri içeren herhangi bir blok eklendiğinde veya kaldırıldığında devreye giren global bir geçişle sağlayabiliriz.
Svelte 3'te geçişler varsayılan olarak küreseldi ve bunları yerel yapmak için |local
belirtecini kullanmanız gerekiyordu.
App.svelte:
<script>
import { slide } from 'svelte/transition';
let items = ['bir', 'iki', 'üç', 'dört', 'beş', 'altı', 'yedi', 'sekiz', 'dokuz', 'on'];
let showItems = $state(true);
let i = $state(5);
</script>
<label>
<input type="checkbox" bind:checked={showItems} />
listeyi göster
</label>
<label>
<input type="range" bind:value={i} max="10" />
</label>
{#if showItems}
{#each items.slice(0, i) as item}
<div transition:slide|global>
{item}
</div>
{/each}
{/if}
<style>
div {
padding: 0.5em 0;
border-top: 1px solid #eee;
}
</style>
46. Anahtar Bloklar (Key Blocks)
İfade değerinin değişmesi durumunda, key block’lar içeriklerini yok edip yeniden oluştururlar. Bu, sadece öğe DOM’a girip çıktığında değil bir değerin değiştiği her seferde bir öğenin geçiş animasyonunu (transition) oynatmasını istiyorsanız kullanışlıdır.
Örneğin aşağıda, i
değiştiğinde yükleme mesajının her seferinde transition.js
'ten typewriter geçişini oynatmasını istiyoruz. Bunu için<p>
öğesini bir key block içine saracağız:
App.svelte:
<script>
import { typewriter } from './transition.js';
import { messages } from './loading-messages.js';
let i = $state(-1);
$effect(() => {
const interval = setInterval(() => {
i += 1;
i %= messages.length;
}, 2500);
return () => {
clearInterval(interval);
};
});
</script>
<h1>Yükleniyor...</h1>
{#key i}
<p in:typewriter={{ speed: 10 }}>
{messages[i] || ''}
</p>
{/key}
loading-messages.js:
// Teşekkürler: https://gist.github.com/meain/6440b706a97d2dd71574769517e7ed32
export const messages = [
"retiküle spline'lar...",
"esprili diyaloglar üretiliyor...",
"zaman ve mekân yer değiştiriliyor...",
"640K herkes için yeterli olmalı",
"bölgenizdeki kütleçekim sabiti kontrol ediliyor...",
"sakin ol ve npm install yap",
"Sonsuzluktan geriye sayılıyor",
"Üzgünüm Dave, bunu yapamam.",
"akı kapasitörü ayarlanıyor...",
"ek direkler inşa ediliyor...",
"rm -rf /",
];
transition.js:
export function typewriter(node, { speed = 1 }) {
const valid = node.childNodes.length === 1 && node.childNodes[0].nodeType === Node.TEXT_NODE;
if (!valid) {
throw new Error(`Bu geçiş yalnızca tek bir metin düğümü çocuğu olan öğelerde çalışır.`);
}
const text = node.textContent;
const duration = text.length / (speed * 0.01);
return {
duration,
tick: (t) => {
const i = Math.trunc(text.length * t);
node.textContent = text.slice(0, i);
}
};
}
Bu yazı çok eğlenceliydi, çok havalı şeyler gördüm. Bu eğitimin gelişmiş kısmı da var devamında. Ardından SvelteKit’in temel ve gelişmiş kısımları var. Onları da inceleyebilirim belki. İlgilendiğiniz için teşekkürler.