Günümüzde birçok modern web uygulaması, bileşenler(components) kullanılarak oluşturulmuştur. Bir uygulama eklemek için React gibi çerçeveler mevcut olsa da, web bileşenleri bu uygulamaları standart hale getirmeye ve tarayıcınızın bir parçası haline getirmeye çalışır.
Bu yazıda, Web Component’lerin ne olduğuna, bunları bir çerçeve olmadan nasıl oluşturabileceğimize ve geliştirme sırasında akılda tutulması gereken bazı sınırlamalara değineceğiz. Daha sonra, bir sonraki makalede, hafif bir çerçevenin (Lit gibi) daha büyük ölçekli uygulamalar oluşturmak isteyenler için nasıl yaşam kalitesi iyileştirmeleri sağlayabileceğini göstereceğiz.

Web Component(Web Bileşeni) Nedir?
Web bileşenlerinin ne olduğu hakkında bile birçok yanlış anlama var. Bazıları bunun yalnızca özel kullanıcı arabirimi, stil ve mantıkla özel öğeleri tek bir konsolide yerde oluşturma yeteneği olduğunu varsaysa da (daha fazlası için), kesinlikle daha fazlası var
Web bileşenleri, birlikte kullanıldıklarında, benzer işlevsellik sunan React gibi bir çerçeve kullanmaya uygun bir alternatif sunabilen 3 farklı web standardının bir karışımıdır. Bu web standartları şunlardan oluşur:
- Custom elements – ilgili HTML etiketi eklendiğinde benzersiz kullanıcı arayüzü ve uygulama mantığı sağlayacak yeni öğeler oluşturma yeteneği sağlar.
- Shadow DOM – Belirli öğeleri ana belge DOM’unuzdan bölümlere ayırarak belge çakışma sorunlarından kaçınmanıza olanak tanıma yeteneği sağlar.
- HTML templates – Sayfaya çizilmemiş HTML yazmanıza izin veren, ancak başka bir yerde yeniden kullanmak üzere biçimlendirme için bir şablon olarak kullanılabilen öğeler sağlar.
Shadow DOM ve HTML templates uygulamalarda kuşkusuz yararlı olsa da, web bileşenlerini bir bütün olarak tanıtmaya başlamak için en kolay yer olduklarını düşündüğümüzden, bugün özel öğelere odaklanacağız.
Bunlar Web Bileşenlerinin tek resmi belirtimleri olsa da, uyumlu bir geliştirme deneyimi oluşturmak için genellikle diğer JavaScript ve tarayıcı özellikleriyle birlikte kullanılırlar.
Sık kullanılan bu özelliklerden biri JavaScript Modülleridir. Uygulamanızı birden çok dosyaya bölme kavramı, Webpack gibi paketleyicilerde bir süredir olağan olmakla birlikte, tarayıcıya entegre olmak oyunun kurallarını değiştiriyor.
Custom Element(Özel Öğe) nedir?
Özünde, özel öğeler esasen yeni HTML etiketleri oluşturmanıza olanak tanır. Bu etiketler daha sonra uygulamanız boyunca kullanılabilecek özel kullanıcı arabirimi ve mantığı uygulamak için kullanılır.
1 2 3 4 5 6 7 8 | <!-- sayfa.html --> <!-- Bunlar özel öğe combinasyonundan bir kaç tane örnek --> <sayfa-header></sayfa-header> <sayfa-content></sayfa-content> <sayfa-footer></sayfa-footer> |
Bu bileşenler, stilize edilmiş bir düğme kadar basit veya iş mantığınızla birlikte uygulamanızın tam bir sayfası kadar karmaşık olabilir.
HTML etiketlerini doğrudan tek bir DOM öğesine eşleme olarak düşünme eğiliminde olsak da, özel öğelerde durum her zaman böyle değildir. Örneğin, yukarıdaki örnekteki “sayfa-header” etiketi, alt öğelerinin listesi olarak “nav” ve “a” öğelerini içerebilir.

Bu nedenle, daha iyi bir akışla okumak için tek bir dosyada görünen etiket miktarını azaltarak bir uygulamanın organizasyonunu iyileştirebiliriz.
Ancak özel öğeler yalnızca HTML’den oluşmaz – JavaScript mantığını bu etiketlerle de ilişkilendirebilirsiniz! Bu, mantığınızı ilişkili kullanıcı arayüzünün yanında tutmanıza olanak tanır. Başlığınızın JavaScript tarafından desteklenen bir açılır menü olduğunu söyleyin. Artık bu JavaScript’i “page-header” bileşeninizin içinde tutabilir ve mantığınızı bir arada tutabilirsiniz.
Son olarak, bileşenlerin sağladığı önemli bir gelişme, birleştirilebilirliktir. Bu bileşenleri farklı sayfalarda kullanabilir ve başlık kodunuzu sayfalar arasında senkronize halde tutabilirsiniz. Bu, standart bileşenlerde (bir sayfada birden çok farklı boyutta düğme olması gibi) kullanıcılarınızın kafasını karıştırabilecek varyasyonlara sahip olma potansiyelini azaltır. Mevcut bileşenlerinizi kullanma konusunda dikkatli olduğunuz sürece, uygulamanızı bu şekilde daha tutarlı hale getirebilirsiniz.
Yaşam Döngüsü Metotları
Bileşenlerin birçok uygulamasında farklılıklar olsa da, oldukça evrensel olan bir kavram “yaşam döngüsü metotları”dır. Özünde, yaşam döngüsü yöntemleri, bir öğede olaylar meydana geldiğinde kod çalıştırmanıza olanak tanır. Sınıflardan uzaklaşan React gibi çerçeveler bile, bir bileşen bir şekilde değiştirildiğinde hala benzer eylemler gerçekleştirme kavramlarına sahiptir.
Tarayıcının uygulanmasına dahil edilen bazı yaşam döngüsü yöntemlerine bir göz atalım.
Özel öğeler, bir bileşene eklenebilecek 4 yaşam döngüsü yöntemine sahiptir.
| connectedCallback | DOM’a eklendiğinde çalıştırılır |
| disconnectedCallback | DOM’a bağlantısı kaldırıldığında çalıştırılır |
| attributeChangedCallback | Web bileşeninin özniteliklerinden biri değiştirildiğinde çalıştırılır. |
| adoptedCallback | Bir HTML belgesinden diğerine taşındığında çalıştırılır. |
Her birinin kendi kullanımları olsa da, öncelikle ilk 3’e odaklanacağız. attributeChangedCallback, öncelikle niş durumlarda kullanışlıdır ve bu nedenle basit bir demo yapmak zordur.
Artık yaşam döngüsü yöntemlerinin ne olduğunu bildiğimize göre, eylem halindeki bir örneğini görelim.
Web Component Örnek
Bahsedeceğimiz ilk iki yaşam döngüsü yöntemi tipik olarak bir çift olarak birlikte kullanılır: ve connectedCallbackdisconnectedCallback
DOM’a bir bileşen eklendiğinde connectCallback çalıştırılır. Bu, öğenin gösterilmesini istediğinizde innerHTML’nizi değiştirebileceğiniz, öğelere olay dinleyicileri ekleyebileceğiniz veya bileşeninizi kurmak için başka herhangi bir tür kod mantığı yapabileceğiniz anlamına gelir.
Bu arada, öğe DOM’dan kaldırılırken çalıştırılır. Bu genellikle, disconnectedCallbackconnectCallback sırasında eklenen olay dinleyicilerini kaldırmak veya öğe için gereken diğer temizleme biçimlerini yapmak için kullanılır.
İşte “Merhaba dünya” metniyle bir başlık oluşturan basit bir web bileşeni örneği.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <script type="module"> class MerhabaComponent extends HTMLElement { connectedCallback() { console.log("Merhaba bağlandım"); this.innerHTML = `<h1>Merhaba Dünya</h1>`; } disconnectedCallback() { console.log("Bağlantı Kesildi"); } } window.customElements.define('merhaba-component', MerhabaComponent); </script> |
Kodları çalışan bir örnek üzerinde görelim.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | <!DOCTYPE html> <html lang="tr"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Tasarım Kodlama</title> <!--component--> <script type="module"> class MerhabaComponent extends HTMLElement { connectedCallback() { console.log("Merhaba bağlandım"); this.innerHTML = `<h1>Merhaba Dünya</h1>`; } disconnectedCallback() { console.log("Bağlantı Kesildi"); } } window.customElements.define('merhaba-component', MerhabaComponent); </script> <!--sürücü kodlar--> <script> //gizle göster var gorunurmu = false; window.onload = (event) => { elementToggle(); }; function elementToggle() { gorunurmu = !gorunurmu; const containEl = document.querySelector('#container'); if (gorunurmu) { // Append const newComp = document.createElement('merhaba-component'); containEl.append(newComp); } else { // Remove all children for (const child of containEl.children) { child.remove(); } } } </script> </head> <body> <button onclick="elementToggle()">Toggle</button> <div id="container"></div> </body> </html> |
Özellik Değiştirildi (attributeChanged)
Bir öğeye veri aktarmanın başka yöntemleri olsa da (ki buna birazdan değineceğiz), niteliklerin yadsınamaz basitliğini inkar etmek zordur. HTML spesifik etiketlerinde yaygın olarak kullanılırlar ve çoğu görüntü özel öğesi, bir üst öğeden önemsiz bir şekilde veri iletmek için öznitelikleri kullanabilmelidir.
AttributeChangedCallback, bir özniteliğin değerinin ne zaman değiştirildiğini algılamak için kullanılan yaşam döngüsü yöntemi olsa da, bileşene hangi özniteliklerin izleneceğini söylemelisiniz.
Örneğin, bu örnekte mesaj özniteliğini izliyoruz. Mesaj özniteliği değeri değişirse, this.render()‘ı çalıştırır. Ancak, başka hiçbir özniteliğin değer değişikliği, AttributeChangedCallback tetiklemeyecektir çünkü başka hiçbir şey izlenecek şekilde işaretlenmemiştir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | <!DOCTYPE html> <html lang="tr"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Tasarım Kodlama</title> <!--component--> <script type="module"> class MerhabaComponent extends HTMLElement { connectedCallback() { this.render(); } static get observedAttributes() { return ["isim",'mesaj']; } attributeChangedCallback(attr, eski, yeni) { this.render() } disconnectedCallback() { console.log("Bağlantı Kesildi"); } render() { //attributeChangedCallback property const mesaj = this.attributes.mesaj.value || ''; const isim = this.attributes.isim.value.toUpperCase() || ''; this.innerHTML = `<h1>${mesaj} ${isim}</h1>`; } } window.customElements.define('merhaba-component', MerhabaComponent); </script> </head> <body> <merhaba-component mesaj="Selam" isim='hayri'></merhaba-component> </body> </html> |
“attributeChangedCallback“in, değiştirilen özniteliğin adını, önceki değerini ve mevcut değerini aldığını fark edeceksiniz. Bu, ayrıntılı manuel değişiklik algılama optimizasyonları için kullanışlıdır.
Ancak, değerleri bir bileşene iletmek için öznitelikleri kullanmanın sınırlamaları vardır. Bu sınırlamaları açıklamak için önce serileştirilebilirlik hakkında konuşarak başlamalıyız.
Serileştirme
Serileştirme, bir veri yapısını veya nesnesini daha sonra saklanabilecek ve yeniden oluşturulabilecek bir biçime dönüştürme işlemidir. Basit bir serileştirme örneği, verileri kodlamak için JSON kullanmaktır.
1 2 3 4 5 6 7 8 9 | JSON.stringify([ {isim: 'hayri'}, {soyisim: 'koç'}, {yas:39} ]) //[{"isim":"hayri"},{"soyisim":"koç"},{"yas":39}] |
Bu JavaScript nesnesi basit olduğundan ve yalnızca ilkel veri türlerini kullandığından, bir dizeye dönüştürmek nispeten önemsizdir. Bu dize daha sonra bir dosyaya kaydedilebilir, HTTP üzerinden bir sunucuya (ve geri) gönderilebilir ve verilere yeniden ihtiyaç duyulduğunda yeniden oluşturulabilir.
Serileştirme Sınırlamaları
Basit nesneler ve diziler nispeten önemsiz bir şekilde serileştirilebilirken, sınırlamalar vardır. Örneğin, aşağıdaki kodu alın:
1 2 3 4 5 6 7 | const obj = { method() { console.log(window); } } |
Bu kodun davranışı, geliştiriciler olarak okumamız için basit görünse de, bunu bir makinenin bakış açısından düşünün.
Bu nesneyi bir istemciden bir sunucuya, yöntem bozulmadan uzaktan göndermek istiyorsak, bunu nasıl yapmalıyız?
window, tarayıcıda mevcutken, sunucunun muhtemelen yazıldığı NodeJS’de mevcut değildir. Window nesnesini seri hale getirmeye ve yöntemle birlikte iletmeye çalışmalı mıyız? Windownesnesindeki yöntemlerden ne haber? Bu yöntemlerle aynı şeyi yapmalı mıyız?
Ölçeğin diğer ucunda, console.log hem NodeJS’de hem de tarayıcılarda uygulanırken, her iki çalışma zamanında da yerel kod kullanılarak uygulanır. İstesek bile yerel yöntemleri seri hale getirmeye nasıl başlayabiliriz? Belki makine kodunu geçebiliriz? Güvenlik endişelerini görmezden gelsek bile, bir kullanıcının ARM cihazı ile bir sunucunun x86_64 mimarisi arasındaki makine kodundaki farklılıkları nasıl ele alabiliriz?
Tüm bunlar, sunucunuzun NodeJS çalıştırmadığını düşünmeden önce bir sorun haline gelir. Bunun kavramını Java gibi bir dilde temsil etmeye nasıl başlarsınız? JavaScript ve C++ gibi dinamik olarak yazılmış bir dil arasındaki farkları nasıl ele alırsınız?
Fonksiyonları Dizgeye Dönüştürelim
Artık serileştirme işlevleriyle ilgili sorunları bildiğinize göre, JSON.stringify() öğesini obj üzerinde çalıştırırsanız ne olacağını merak edebilirsiniz.
1 2 3 4 5 6 7 8 9 | const obj = { method() { console.log(this, window); } } JSON.stringify(obj); // "{}" |
Anahtarı JSON dizesinden çıkarır. Bu, ilerlerken akılda tutulması önemlidir.
HTML Özellik Dizeleri
Bu yazıda neden serileştirmeden bahsediyoruz? Bunu yanıtlamak için HTML öğeleriyle ilgili iki gerçeği belirtmek istiyorum.
- HTML özellikleri büyük/küçük harfe duyarsızdır
- HTML öznitelikleri dize olmalıdır
Bu gerçeklerden ilki, herhangi bir öznitelik için anahtar kasasını değiştirebilirsiniz ve o da aynı şekilde yanıt verecektir. HTML spesifikasyonuna göre, aşağıdakiler arasında fark yoktur:
1 2 3 | <input type="checkbox"/> |
ve
1 2 3 | <input tYpE="checkbox"/> |
İkinci gerçek, bu tartışmada bizim için çok daha alakalı. Bir özniteliğe dize olmayan değerler atayabilirsiniz gibi görünse de, bunlar her zaman kaputun altındaki dizeler olarak ayrıştırılır.
Bir özniteliğe dize olmayan değerler atamak için yanıltıcı olmayı ve JavaScript kullanmayı düşünebilirsiniz:
1 2 3 4 | const el = document.querySelector('input'); el.setAttribute('data-arr', [1, 2, 3, 4]); |
Ancak, özniteliğin atanan değeri beklentilerinize uymayabilir:
1 2 3 | <input type="checkbox" data-arr="1,2,3,4"> |
Öznitelikte parantez bulunmadığını fark edeceksiniz. Bunun nedeni, JavaScript’in dizinizde örtük olarak toString çalıştırıyor olması ve bu da onu özniteliğe atamadan önce onu bir dizgeye dönüştürmesidir.
Nasıl döndürdüğünüz önemli değil – niteliğiniz bir dize olacaktır.
Bu nedenle, dize olmayan değerler için öznitelikleri kullanmaya çalışırken, aksi takdirde beklenmeyen davranışlarla karşılaşabilirsiniz. Bu, girdi gibi yerleşik öğeler için bile geçerlidir.
1 2 3 | <input type="checkbox" checked="false"/> |
Bu HTML öznitelik sınırlamasının farkında olmadan, onay kutusunun işaretlenmemiş olmasını bekleyebilirsiniz. Ancak, işlendiğinde, işaretli görünüyor.
Bunun nedeni, yanlış Boolean değerini geçmemeniz, (kafa karıştırıcı bir şekilde) doğru olan “False” dizesini geçmenizdir.
1 2 3 | console.log(Boolean("false")); // true |
Bazı nitelikler, bir nitelik aracılığıyla bir öğeye bir sayı veya başka bir ilkel değer atamayı düşündüğünüzde bunu bilecek kadar akıllıdır, ancak uygulama dahili olarak şöyle görünebilir:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class NumValidator extends HTMLElement { connectedCallback() { this.render(); } static get observedAttributes() { return ['max']; } attributeChangedCallback(name, oldValue, newValue) { this.render(); } render() { // Coerce "attribute.value" to a number. Again, attributes // can only be passed as a string const max = Number(this.attributes.max.value || Infinity); // ... } } |
Bu, HTML öğesinin özniteliklerin seri durumdan çıkarılmasının kapsamı olma eğiliminde olsa da, bu işlevi daha da genişletebiliriz.
String Dizisi Geçme
Birazdan değindiğimiz gibi, JavaScript’in setAttribute özelliğini kullanarak bir özniteliğe bir dizi iletmeyi denersek, bu parantezleri içermeyecektir. Bunun nedeni Array.toString()‘in çıktısıdır.
JS’den bir özniteliğe ["deneme", "hayri", "merhaba"] dizisini geçirmeye çalışırsak, çıktı şöyle görünür:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | <html> <head> <!-- Render --> <script> class MerhabaComponent extends HTMLElement { connectedCallback() { this.render(); } static get observedAttributes() { return ['todos']; } attributeChangedCallback(name, oldValue, newValue) { this.render(); } render() { const todos = this.attributes.todos.value || ''; this.innerHTML = `<h1>${todos}</h1>`; } } customElements.define('merhaba-component', MerhabaComponent); </script> <!-- Form event --> <script> var todoList = []; window.onload = function() { const formEl = document.querySelector('#todoForm'); const inputEl = document.querySelector('#todoName'); formEl.addEventListener('submit', e => { e.preventDefault(); todoList.push(inputEl.value); inputEl.value = ''; changeElement(); }) } </script> <!-- Update --> <script> function changeElement() { const compEl = document.querySelector('#mycomp'); compEl.attributes.todos.value = todoList; } </script> </head> <body> <form id="todoForm"> <input id="todoName" type="text"/> <button>Toggle</button> </form> <merhaba-component id="mycomp" todos="[]"></merhaba-component> </body> </html> |
Bu, HTML öğesinin özniteliklerin seri durumdan çıkarılmasının kapsamı olma eğiliminde olsa da, bunu toString’in çıktısı nedeniyle genişletebiliriz, öznitelik değerini tekrar bir dizgeye dönüştürmek zordur. Bu nedenle, yalnızca bir
etiketinin içindeki verileri görüntüleriz. Ancak listeler tek bir paragraf etiketine ait değildir! Listedeki her bir öğe için ayrı liste içeren bir ul’ye aittirler. Sonuçta, semantik HTML, erişilebilir bir web sitesi için ayrılmazdır!
Bunun yerine, bu verileri seri hale getirmek için JSON.stringify’ı kullanalım, bu dizeyi öznitelik değerine iletelim, ardından JSON.parse.unctionality kullanarak öğede bunu seri durumdan çıkaralım.
Dize Dizisini Geç
Birazdan değindiğimiz gibi, JavaScript’in setAttribute özelliğini kullanarak bir özniteliğe bir dizi iletmeyi denersek, bu parantezleri içermeyecektir. Bunun nedeni Array.toString()’in çıktısıdır.
JS’den bir özniteliğe ["deneme", "hayri", "merhaba"] dizisini geçirmeye çalışırsak, çıktı şöyle görünür:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | <html> <head> <!-- Render string array as "ul" --> <script> class MyComponent extends HTMLElement { connectedCallback() { this.render(); } static get observedAttributes() { return ['todos']; } attributeChangedCallback(name, oldValue, newValue) { this.render(); } render() { const todosArr = JSON.parse(this.attributes.todos.value || '[]'); console.log(todosArr); const todoEls = todosArr.map(todo => `<li>${todo}</li>`).join('\n'); this.innerHTML = `<ul>${todoEls}</ul>`; } } customElements.define('my-component', MyComponent); </script> <!-- Form event handler --> <script> var todoList = []; window.onload = function() { const formEl = document.querySelector('#todoForm'); const inputEl = document.querySelector('#todoName'); formEl.addEventListener('submit', e => { e.preventDefault(); todoList.push(inputEl.value); inputEl.value = ''; changeElement(); }) } </script> <!-- Update custom element attributes --> <script> function changeElement() { const compEl = document.querySelector('#mycomp'); compEl.attributes.todos.value = JSON.stringify(todoList); } </script> </head> <body> <form id="todoForm"> <input id="todoName" type="text"/> <button>Toggle</button> </form> <my-component id="mycomp" todos="[]"></my-component> </body> </html> |
Bu metodu kullanarak render metodumuzda bir dizi elde edebiliyoruz. Oradan, li öğeleri oluşturmak için bu dizinin haritasını çıkarırız, ardından bunu innerHTML’mize iletiriz.
Nesne Geçme
Bir dizi dizi, serileştirme özniteliklerinin basit bir gösterimi olsa da, gerçek dünyadaki veri yapılarını pek temsil etmez.
Verilerimizi daha gerçekçi hale getirmek için çalışmaya başlayalım. Dize dizimizi bir dizi nesneye dönüştürmek iyi bir başlangıç olabilir. Sonuçta, bir yapılacaklar uygulamasında öğeleri “tamamlandı” olarak işaretleyebilmek istiyoruz.
Şimdilik küçük tutacağız ve daha sonra büyüteceğiz. Yapılacaklar öğesinin “adını” ve tamamlanıp tamamlanmadığını takip edelim:
1 2 3 | const data = [{name: "hello", completed: false}]; |
Özel öğemizi kullanarak bunu nasıl makul bir şekilde gösterebileceğimize bir göz atalım:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | <html> <head> <!-- Render object array as "ul" --> <script> class MyComponent extends HTMLElement { connectedCallback() { this.render(); } static get observedAttributes() { return ['todos']; } attributeChangedCallback(name, oldValue, newValue) { this.render(); } render() { const todosArr = JSON.parse(this.attributes.todos.value || '[]'); console.log(todosArr); const todoEls = todosArr .map(todo => ` <li> <input type="checkbox" ${todo.completed ? 'checked' : ''}/> ${todo.name} </li> `) .join('\n'); this.innerHTML = `<ul>${todoEls}</ul>`; } } customElements.define('my-component', MyComponent); </script> <!-- Form event handler --> <script> var todoList = []; window.onload = function() { const formEl = document.querySelector('#todoForm'); const inputEl = document.querySelector('#todoName'); formEl.addEventListener('submit', e => { e.preventDefault(); todoList.push({name: inputEl.value, completed: false}); inputEl.value = ''; changeElement(); }) } function toggleAll() { todoList = todoList.map(todo => ({...todo, completed: !todo.completed})); changeElement(); } </script> <!-- Update custom element attributes --> <script> function changeElement() { const compEl = document.querySelector('#mycomp'); compEl.attributes.todos.value = JSON.stringify(todoList); } </script> </head> <body> <button onclick="toggleAll()">Toggle all</button> <form id="todoForm"> <input id="todoName" type="text"/> <button>Add</button> </form> <my-component id="mycomp" todos="[]"></my-component> </body> </html> |
Fonksiyonla Nesneleri Geçirme
Bir özel öğedeki kullanıcı girdisinin bir ebeveynin veri kümesiyle etkileşime girmesini sağlamanın birçok yolu olsa da, her bir yapılacaklar nesnesinde bir yöntem depolayalım ve onu özel öğeye aktaralım.
Bu model, verilerin tek yönlü geçmesini sağlayarak bileşenler için en iyi uygulamaları takip eder. Geçmişte, hem React hem de Web Bileşenleri için bileşenlerinizi nasıl tek yönlü tutacağınıza değinmiştik.
Benzer bir şeyi yansıtmak için yapılacaklar nesnesini değiştirelim:
1 2 3 4 5 6 7 8 9 10 | todoList.push({ name: inputEl.value, completed: false, id: todoId, onChange: () => { toggleTodoItem(todoId) } }); |
Ardından, ilgili yapılacaklar nesnesini değiştirmek için kimliği kullanarak toggleTodoItem yöntemimizi uygulayacağız:
1 2 3 4 5 6 7 8 9 10 11 12 | function toggleTodoItem(todoId) { thisTodo = todoList.find(todo => todo.id == todoId); thisTodo.completed = !thisTodo.completed; changeElement(); } function changeElement() { const compEl = document.querySelector('#mycomp'); compEl.attributes.todos.value = JSON.stringify(todoList); } |
Bu değişikliklerle, onay kutusu mantığını işlemek için ebeveynimizden ihtiyacımız olan tüm mantığa sahibiz. Şimdi onay kutusu işaretlendiğinde onChange yöntemini tetiklemek için özel öğemizi güncellememiz gerekiyor. Bir olay dinleyicisini “input” öğesini bağlamak için, alttaki HTMLElement referansına erişmemiz gerekir. Bunu yapmak için, daha önce kullandığımız innerHTML mantığından document.createElement lehine geçiş yapmamız gerekecek.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | render() { this.clear(); // Create list element const todosArr = JSON.parse(this.attributes.todos.value || '[]'); const todoEls = todosArr .map(todo => { // Use `createElement` to get access to the element. We can then add event listeners const checkboxEl = document.createElement('input'); checkboxEl.type = "checkbox"; // This doesn't work, we'll explain why shortly checkboxEl.addEventListener('change', todo.onChange); checkboxEl.checked = todo.completed; const liEl = document.createElement('li'); liEl.append(checkboxEl); liEl.append(todo.name); return liEl; }); const ulEl = document.createElement('ul'); for (const liEl of todoEls) { ulEl.append(liEl); } // Add header. This should update to tell us how many items are completed const header = document.createElement('h1'); header.innerText = todosArr.filter(todo => todo.completed).length; // Reconstruct logic this.append(header); this.append(ulEl); } |
Mükemmel! Şimdi gerekli tüm değişiklikleri yaptık, hep birlikte çalışıp çalışmadığını görelim!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | <html> <head> <!-- Render object array as "ul", passing fn to checkbox change event --> <script> class MyComponent extends HTMLElement { connectedCallback() { this.render(); } static get observedAttributes() { return ['todos']; } attributeChangedCallback(name, oldValue, newValue) { this.clear(); this.render(); } clear() { for (const child of this.children) { child.remove(); } } render() { this.clear(); // Create list element const todosArr = JSON.parse(this.attributes.todos.value || '[]'); const todoEls = todosArr .map(todo => { const checkboxEl = document.createElement('input'); checkboxEl.type = "checkbox"; console.log(todo); // This doesn't work checkboxEl.addEventListener('change', todo.onChange); // This does work // checkboxEl.addEventListener('change', () => toggleTodoItem(todo.id)) checkboxEl.checked = todo.completed; const liEl = document.createElement('li'); liEl.append(checkboxEl); liEl.append(todo.name); return liEl; }); const ulEl = document.createElement('ul'); for (const liEl of todoEls) { ulEl.append(liEl); } // Add header const header = document.createElement('h1'); header.innerText = todosArr.filter(todo => todo.completed).length; // Reconstruct logic this.append(header); this.append(ulEl); } } customElements.define('my-component', MyComponent); </script> <!-- Todo list construction & helper function declaration --> <script> var currTodoId = 0; var todoList = []; function toggleTodoItem(todoId) { // This is mutable thisTodo = todoList.find(todo => todo.id == todoId); thisTodo.completed = !thisTodo.completed; changeElement(); } window.onload = function() { const formEl = document.querySelector('#todoForm'); const inputEl = document.querySelector('#todoName'); formEl.addEventListener('submit', e => { e.preventDefault(); const todoId = currTodoId++; todoList.push({ name: inputEl.value, completed: false, id: todoId, // This fn will disappear during `JSON.stringify` // as it cannot be serialized onChange: () => { toggleTodoItem(todoId) } }); inputEl.value = ''; changeElement(); }) } function toggleAll() { todoList = todoList.map(todo => ({...todo, completed: !todo.completed})); changeElement(); } </script> <!-- Update custom element attributes --> <script> function changeElement() { console.log(todoList); const compEl = document.querySelector('#mycomp'); compEl.attributes.todos.value = JSON.stringify(todoList); } </script> </head> <body> <button onclick="toggleAll()">Toggle all</button> <form id="todoForm"> <input id="todoName" type="text"/> <button>Add</button> </form> <my-component id="mycomp" todos="[]"></my-component> </body> </html> |
Ah… Garip… Onay kutularımız güncelleniyor gibi görünse de, h1’imiz değil. Dahası, geliştirici konsolumuza bakarsak, yeniden oluşturma sırasında görmeyi beklediğimiz console.log’ları görmüyoruz.
Nedenmiş?
Peki, serileştirme sınırlamaları ile ilgili bölümümüzde bahsettiğimiz gibi, fonksiyonlar seri hale getirilemez. Bu nedenle, yöntemleri olan bir nesne JSON.parse’a geçirildiğinde, bu anahtarlar kaldırılır. Olay dinleyicimizi eklerken işlev tanımsızdır ve bu nedenle hiçbir şey yapmaz.
1 2 3 | checkboxEl.addEventListener('change', todo.onChange); // onChange is undefined |
Onay kutusunun verilerimize yansımadan görsel olarak güncellenmesi durumu, DOM ile DOM’u oluşturmak için kullandığımız veriler arasındaki yanlış hizalamaya bir örnektir.
Ancak, serileştirme sorunları dışında kodumuzun doğru olduğunu doğrulayabiliriz. Bu kod satırını doğrudan toggleTodoItem global işlevini kullanacak şekilde değiştirirsek, beklendiği gibi çalışır:
1 2 3 | checkboxEl.addEventListener('change', () => toggleTodoItem(todo.id)) |
Bu, mevcut kurulumumuz için geçerli olsa da, özel öğeler oluşturmanın avantajlarından biri, uygulamanızın kod tabanını düzenli tutmak için uygulamanızı birden çok dosyaya bölme yeteneğidir. toggleTodoItem artık özel öğeyle aynı kapsamda olmadığında bu kod bozulur.
Bu uzun vadeli iyi bir çözüm değilse, serileştirmeyle ilgili sorunumuzu çözmek için ne yapabiliriz?
Nitelik ve Özelliklerle Parametre Geçme
Nitelikler, ilkel verileri özel öğelerinize iletmek için basit bir yöntem sağlar. Ancak, gösterdiğimiz gibi, verilerinizi seri hale getirme gereksinimi nedeniyle daha karmaşık kullanımda düz kalır.
Nitelikleri kullanarak bu sınırlamayı atlayamayacağımızı bildiğimizden, verileri daha doğrudan iletmek için JavaScript sınıflarından yararlanalım.
Bileşenlerimiz HTMLElement öğesini genişleten sınıflar olduğundan, özel öğemizin üst öğesinden özelliklerimize ve yöntemlerimize erişebiliriz. Diyelim ki, özellik değiştirildiğinde yapılacakları güncellemek ve işlemek istiyoruz.
Bunu yapmak için, bileşenimizin sınıfına “setTodos” adlı bir yöntem ekleyeceğiz. Bu yöntem, daha sonra Document.querySelector kullanarak öğemizi sorguladığımızda erişilebilir olacaktır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | class MyComponent extends HTMLElement { todos = []; connectedCallback() { this.render(); } setTodos(todos) { this.todos = todos; this.clear(); this.render(); } render() { // ... } } // ... function changeElement() { const compEl = document.querySelector('#mycomp'); compEl.setTodos(todoList); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | <html> <head> <!-- Render object array, properly setting fn from prop instead of attribute --> <script> class MyComponent extends HTMLElement { todos = []; connectedCallback() { this.render(); } // This function can be accessed in element query to set internal data externally setTodos(todos) { this.todos = todos; this.clear(); this.render(); } clear() { for (const child of this.children) { child.remove(); } } render() { this.clear(); // Create list element const todosArr = this.todos; const todoEls = todosArr .map(todo => { const checkboxEl = document.createElement('input'); checkboxEl.type = "checkbox"; // onChange should now show as it's not being serialized anymore checkboxEl.addEventListener('change', todo.onChange); checkboxEl.checked = todo.completed; const liEl = document.createElement('li'); liEl.append(checkboxEl); liEl.append(todo.name); return liEl; }); const ulEl = document.createElement('ul'); for (const liEl of todoEls) { ulEl.append(liEl); } // Add header const header = document.createElement('h1'); header.innerText = todosArr.filter(todo => todo.completed).length; // Reconstruct logic this.append(header); this.append(ulEl); } } customElements.define('my-component', MyComponent); </script> <!-- Todo list construction & helper function declaration --> <script> var currTodoId = 0; var todoList = []; function toggleTodoItem(todoId) { // This is mutable thisTodo = todoList.find(todo => todo.id == todoId); thisTodo.completed = !thisTodo.completed; changeElement(); } window.onload = function() { const formEl = document.querySelector('#todoForm'); const inputEl = document.querySelector('#todoName'); formEl.addEventListener('submit', e => { e.preventDefault(); const todoId = currTodoId++; todoList.push({ name: inputEl.value, completed: false, id: todoId, onChange: () => { toggleTodoItem(todoId) } }); inputEl.value = ''; changeElement(); }) } function toggleAll() { todoList = todoList.map(todo => ({...todo, completed: !todo.completed})); changeElement(); } </script> <!-- Update custom element properties via setter function --> <script> function changeElement() { const compEl = document.querySelector('#mycomp'); compEl.setTodos(todoList); } </script> </head> <body> <button onclick="toggleAll()">Toggle all</button> <form id="todoForm"> <input id="todoName" type="text"/> <button>Add</button> </form> <my-component id="mycomp"></my-component> </body> </html> |
Şimdi, yapılacaklar listemizdeki öğeleri değiştirirsek, h1 etiketimiz beklediğimiz gibi güncellenir: DOM’umuz ve veri katmanımız arasındaki uyuşmazlığı çözdük!
Özel öğelerimizin özelliklerini güncellediğimiz için, buna “özniteliklerden geçme” serileştirme sorunlarını çözen “özellikler üzerinden geçme” adını veriyoruz.
Ama hepsi bu değil! Özelliklerin, veri geçişi için de özniteliklere göre gizli bir avantajı vardır: bellek boyutu.
Yapılacak işlerimizi niteliklere serileştirirken verilerimizi çoğaltıyorduk. Yalnızca yapılacaklar listesini JavaScript’imizde bellekte tutmakla kalmıyor, aynı zamanda tarayıcı yüklü DOM öğelerini de bellekte tutuyor. Bu, eklediğimiz her yapılacak iş için yalnızca JavaScript’te değil, DOM’de de (öznitelik dizesi aracılığıyla) bir kopyasını tuttuğumuz anlamına gelir.
Ama kesinlikle, mülklere geçiş yaparken belleği geliştirmenin tek yolu bu, değil mi? Yanlış!
Unutmayın, ana komut dosyası etiketimizde JS’de ve DOM aracılığıyla tarayıcıda bellek içi yüklenmenin yanı sıra, onu özel öğemizde de seri durumdan çıkarıyorduk! Bu, aynı anda bellekte başlatılan verilerimizin üçüncü bir kopyasını tuttuğumuz anlamına geliyordu!
Bu performans değerlendirmeleri bir demo uygulamasında önemli olmasa da, üretim ölçeğindeki uygulamalarda önemli komplikasyonlar ekler.











1 Yorum