JavaScript İle DOM Değişikliklerinin İzlenmesi
CSS Animations ve MutationObserver Kullanımı
Google Analytics İle Görsel Tıklamalarının Log'lanması başlıklı yazıda yer alan örnekte WordPress NextGEN Gallery eklentisi ile oluşturulan bir galeri içerisinde yer alan görsel tıklamalarının nasıl izlenebileceğinden bahsetmiştim.
Not olarak eklemekte fayda var. NextGEN Gallery eklentisi galeri içerisindeki sayfalamayı iki şekilde ele alır; GET ve AJAX1. İlgili örnek içerisinde URL değişikliği temelinde ilerlemiştim. AJAX ile sayfalama değişiğin JS ile bir önceki sayfaya ait görsel adresleri değer olarak kalmaya devam etmekte. Bu durum da, görüntüde değişiklik olsa da tıklamalar neticesinde iletilen bilgiler aynı kalmaktaydı. Bu yazıda ilgili durum ile ilişkili olarak DOM değişikliklerinin nasıl takip edilebileceğinden bahsetmeye çalışacağım.
DOM Değişikliklerinin İzlenmesi
Bir önceki yazıda JS İle Temel DOM İşlemleri konusu ile DOM işlemlerine dair temel bir başlangıç yapmaya çalışmıştım. Yazıda da bahsi geçtiği üzere, JS ile element'leri seçip listelediğimizde gelen değerler statiktir. Dolayısıyla, anlık değişiklikler listeye yansımayacaktır. Ancak, JS ile DOM değişikliklerini kısmen veya tamamını izleyerek değişimlere bağlı olarak liste içeriklerini güncelleyebilmekteyiz.
Öncelikle, örnek işlemler doğrultusunda kullanacağımız basit bir HAML sayfası oluşturalım.
!!!5
%html
%head
%meta{:content => "text/html; charset=UTF-8", "http-equiv" => "Content-Type"}
%meta{:charset => "utf-8"}
%meta{:content => "width=device-width, initial-scale=1", :name => "viewport"}
%title Hello Bulma!
%link{:href => "https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.min.css", :rel => "stylesheet"}
%body
%section.section
.container.is-max-desktop
.columns.is-vcentered.is-centered.is-variable.is-6
.column.is-8-desktop.is-8-tablet.is-12-mobile
.box
%p#changeme.content
The Caterpillar and Alice looked at each other for some time in silence: at last the Caterpillar took the hookah out of its mouth, and addressed her in a languid, sleepy voice.
.box
.field.has-addons
.control.is-expanded
%input.input{:placeholder => "Text input", :type => "text"}
.control
%button#addItem.button.is-link Add to list
%ul#myList
%li.notification.is-link.is-light No item!
%button#disconnect.button.is-warning Disconnect
:javascript
-#...
Elimizde input ve liste alanından oluşan bir to-do list HTML arayüzü var. Amacımız text alanı üzerinden yazılan metni ilgili listeye eklemek. Bu aşamada 2 olay gerçekleşecek; var olan No item! ibaresi silinecek ve text alanı içerisine yazılan değer listeye eklenecek. Normalde bu etkinlikleri izlemek için etkinliklerin kendi tanımları ile ilişkilenebiliriz.
document
.querySelector('#addItem')
.addEventListener('click', e => {
//...
});
Yukarıdaki kod parçacığı ile ilgili eleman için gerçekleşecek tıklama etkinlikleri dinlenmekte ve tıklama olduğunda belirtilen eylemler işleme alınmaktadır. Ancak, bu tür bir kod içerisinde değişiklik yapmadan DOM değişikliklerini gözlemlemek durumunda kalabiliriz. Bu amaçla kullanabileceğimiz birkaç yol var.
DOM Level 3 - UI Events bu seçeneklerden biri idi. Ancak, güncel durumda uygun bulunmadığı (deprecated) için geliştirilmesi durduruldu2 3. Yazının geri kalanında haberdar olduğum 2 yönemden bahsedeceğim; animationstart
ve MutationObserver
.
CSS Animations
MutationObserver
kullanımı ile ilgili okuma yaparken denk geldiğim bu çözüm de oldukça ilginç / faydalı göründüğü için yazıya dahil etmek istedim. Çözümleme David Walsh'e ait. Diğer yandan, ilgili yazıya da ilham veren Daniel Buchner'in SelectorListener örneğinin de oldukça zihin açıcı olduğunu söyleyebilirim.
Bu çözümünün temel aldığı prensip CSS animasyonları. Bildiğiniz üzere; CSS animasyonları, bir CSS stili yapılandırmasından diğerine geçişleri (transitions) mümkün kılar. Bu işlem iki bileşen sayesinde gerçekleşir: CSS animasyonunu açıklayan bir stil tanımı ve animasyon stilinin başlangıç ve bitiş durumlarının yanı sıra olası ara geçiş noktalarını gösteren bir dizi anahtar kare (keyframe)4 5.
DOM etkinliği ile ilişkilendirmeden önce CSS animasyonları için basit bir örnek oluşturalım.
p#changeme {
animation-duration: 3s;
animation-name: colorize;
animation-iteration-count: 3;
}
@keyframes colorize {
from {
color: blue;
}
to {
color: red;
}
}
İlgili animatik geçiş @keyframes
ile belirtilen yeteneklere sahip anahtar kare tanımları sayesinde gerçekleşmekte. JavaScript ile bu anahtar karelerin başlangıç, animasyon akışı ve bitişleri sırası ile animationstart
, animationiteration
ve animationend
etkinlikleri aracılığı ile takip edilebilmekte. Bu şekilde, animasyon ile ilişkili farkı işlemlerin de gerçekleştirilebilmesini sağlamak mümkün hale gelmekte. DOM değişikliklerinin yakalanması da bunlardan biri5.
Yukarıdaki örnek p
etiketini temel almaktaydı. O halde, aynı örnek üzerinden ilerleyelim ve animasyon akışını izleyelim.
const element = document.getElementById("changeme");
element.addEventListener("animationstart", e => console.log(e), false);
element.addEventListener("animationend", e => console.log(e), false);
element.addEventListener("animationiteration", e => console.log(e), false);
Yukarıdaki kod parçacığı @keyframes
duurmlarını console üzerinden bize iletecektir.
Görüldüğü üzere animation-iteration-count: infinite
tanımı sebebiyle animationstart
sonrasında gelen animationiteration
etkinliği sürekli yinelenmekte ve harici bir müdahale olana kadar animationend
etkinliğine ulaşılamamakta. Şimdi animation-iteration-count: 3
ile bir tekrar sınırı koyalım.
Görüldüğü üzere animationstart
sonrasında animationiteration
n-1 kez yineleniyor ve sonrasında animationend
etkinliği ile animasyon sonlandırılıyor.
Şimdi bu mantığı DOM değişimlerinin takibinde nasıl kullanabileceğimize bakalım.
David'in ilgili yazıda bahsettiği hack6 esasında animasyon kareleri arasındaki geçişi çok yakın tutup sadece animayon başlangıcını izlemek.
O halde bu mantığı yukarıdaki listeye uygulayalım ve yeni eklenen item'leri izleyelim.
const node = document.createElement('li');
const textMode = document.createTextNode('A new item!');
node.appendChild(textMode);
node.setAttribute('class', 'notification is-link')
list.appendChild(node);
list.addEventListener("animationstart", e => {
if (e.animationName == "nodeInserted") console.warn("Another node has been inserted! ", e, e.target);
}, false);
Evet, CSS animasyon ile ilişkili DOM değişimlerini bu şekilde takip edebilmek mümkün. Ancak, bu işlem sadece bir node ile ilişkili olacaktır, bir attribute değişikliğinde ilgili etkinlik değişimi tetiklenemeyebilir. Bu tür durumlar için MutationObserver
seçeneği değerlendirilebilir.
MutationObserver API
MutationObserver
, bir DOM öğesini gözlemleyen ve değişiklik durumunda bir callback tetikleyen ve modern internet tarayıcıları tarafından desteklenen Mutation events yerine gelişirilen yerleşik (built-in) bir nesnedir7.
const observer = new MutationObserver(callback);
MutationObserver
sayesinde, bir node değişimi yaşandığında (ekleme, silme) ve/veya node özelliği veya içeriğine müdahale edildiğinde haberdar olabilmekteyiz8 9. DOM değişimlerini izleyebilmek amacıyla, öncelikle bir MutationObserver
instance (örneklem) oluşturmamız gerekir. Bu işlem eğer belirli bir süre için geçerliyse işlem sonunda instance durdurulabilir. Instance ile ilişkili oluşturduğumuz fonksiyonun ilk argümanı tek bir grupta meydana gelen mutation'a ait bir koleksiyondur. Her mutation meydana gelen değişiklikler hakkında bilgi döner10.
const obsInstance = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log(mutation);
});
});
Instance oluşturulmasıyla birlikte artık ilgili nesneye observe
, disconnect
ve takeRecords
metotlarını kullanarak erişebiliriz. observe
hangi node ile ilgili ne tür değişimlerin izleneceğini (observe) belirtir; mutationObserver.observe(target, options)
.
obsInstance.observe(document.documentElement, {
attributes: true,
characterData: true,
childList: true,
subtree: true,
attributeOldValue: true,
characterDataOldValue: true
});
Genelde target
için doğrudan ilgili node'un tanımlanması, eğer bir querySelection*
kullanılacaksa, işlem öncesinde sorgunun null
dönmediğinden emin olunması önerilmektedir. Yukarıdaki opsiyon (option) tanımları şu karşılıklara sahip3:
- childList
- Belirtilen node'a ait child node modifikasyonlarını izler.
- subtree
- Belirtilen node içerisindeki tüm torunları (descendants) izler.
- attributes
- Belirtilen node özniteliklerini (attributes) izler.
- attributeFilter
- İzlenecek öznitelikleri (attributes) beliren bir dizi tanımıdır.
- characterData
- node.data'nın izlenip izlenmeyeceğini belirtir.
Şimdi, bu bilgileri içerecek şekilde örneğimizi düzenleyebiliriz.
const item = {
add: (item, val, clss = 'is-link') => {
if(val){
const node = document.createElement('li'),
textMode = document.createTextNode(val);
node.appendChild(textMode);
node.classList.add('notification', clss);
list.appendChild(node);
}
},
remove: (clss) => {
const found = [...list.children].find(e => e.classList.contains(clss));
if(found) found.remove(this);
}
}
const list = document.getElementById('myList'),
addItemButton = document.querySelector('#addItem'),
disconnectButton = document.querySelector('#disconnect'),
txtInput = document.querySelector('.input');
addItemButton.addEventListener('click', () => {
if (txtInput.value) {
item.remove('is-light');
item.add(list, txtInput.value);
txtInput.value = '';
txtInput.focus();
}
});
if(list){
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
const observer = new MutationObserver((mutations, observer) => {
mutations.forEach(e => {
console.log(e);
// console.log(e.type);
// console.log(e.target);
// console.log(e.addedNodes)
});
});
observer.observe(list, {
childList: true,
subtree: true,
});
disconnectButton.addEventListener('click', () => {
observer.disconnect();
item.add(list, 'Disconnected');
});
}
Console alanını izlediğinizde, listeye her yeni kayıt eklemesinde MutationRecord
nesnesinin eklenen, kaldırılan node içerikleri, bundan etkilenen node ile ilişki, diğer ilişkili node'lar gibi pek çok bilgiyi bize sunduğunu görebilirsiniz3 11 8. Edindiğiniz bu bilgilere göre artık işlemlerinizi gerçekleşirebilirsiniz. İşlemlerinizin tamamlanmasının ardından Disconnect butonunu tıklarsanız, sonraki lise değişimlerinin artık izlenmediğini görebilirsiniz.
- NextGEN Basic Thumbnail Gallery. Imagely ↩
- UI Events. W3C Working Draft, 04 August 2016 ↩
- Mutation observer. javascript.info ↩ ↩ ↩
- CSS Animations. MDN web docs ↩
- Using CSS animations. MDN web docs ↩ ↩
- David Walsh. (2012). Detect DOM Node Insertions with JavaScript and CSS Animations ↩
- MutationObserver. MDN web docs ↩
- Mutation observers. DOM Living Standard — Last Updated 24 December 2020 ↩ ↩
- Is there a JavaScript / jQuery DOM change listener? StackOverflow ↩
- Alexander Zlatkov. (2018). How JavaScript works: tracking changes in the DOM using MutationObserver ↩
- Emre Erkoca. (2020). MutationObserver and Event Usage ↩