Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get the new attribute value for the current MutationRecord when using MutationObserver?

I have some code like the following,

const observer = new MutationObserver(records => {
    for (const record of records) {
        if (record.type !== 'attributes') continue

        handleAttributeChange(
            record.attributeName!,
            record.oldValue,
            record.target.attributes.getNamedItem(record.attributeName).value,
        )
    }
})

where handleAttributeChange parameters are the attribute name, the old value, and the new value.

However, as you can tell, the third argument passed into handleAttributeChange,

record.target.attributes.getNamedItem(record.attributeName!)!.value

is always going to be the very latest attribute value, not the "new value" for the particular mutation record that we are iterating on (i.e. not the "new value" that was observed at the time that the mutation happened).

How do we get the "new value" for each mutation as would've been observed at the time each mutation happened?

like image 677
trusktr Avatar asked Mar 03 '23 17:03

trusktr


2 Answers

We can emulate that like this:

const observer = new MutationObserver(records => {
    let lastAttributeValues = {}
    let name = ''

    // This loop goes through all the records, and for each mutation,
    // it calls handleAttributeChange with old and new attribute values
    // as would have been observed had we been able to react to each 
    // mutation synchronously.
    for (const record of records) {
        if (record.type !== 'attributes') continue

        name = record.attributeName

        if (lastAttributeValues[name] === undefined) {
            lastAttributeValues[name] = record.oldValue
            continue
        }

        handleAttributeChange(name, lastAttributeValues[name], record.oldValue)

        lastAttributeValues[name] = record.oldValue
    }

    let attr

    // This loop calls handleAttributeChange for each attribute that changed
    // with the last oldValue and the current value that's actually in
    // the DOM (the previous loop handled only differences between each
    // mutation of each mutated attribute, but not the difference between
    // the last mutation of a each attribute and the attributes' current
    // values in the DOM).
    for (const name in lastAttributeValues) {
        attr = el.attributes.getNamedItem(name)
        handleAttributeChange(name, lastAttributeValues[name], attr === null ? null : attr.value)
    }
})

What it does is, because we know we get mutations in order, and we have all the attribute values for a given attribute along the way while iterating, it just keeps track of previous and current attribute values (per attribute name) and fires handleAttributeChange with each previous and current value.

like image 50
trusktr Avatar answered Mar 05 '23 07:03

trusktr


I don't think it's possible with MutationObserver. Nothing in a MutationRecord exposes the new value, and since a MuationObserver runs in a microtask, and not completely synchronously after a change, the value that can be retrieved from the element inside the observer callback may have been changed after the change that triggered the observer, as you're seeing.

const observer = new MutationObserver(records => {
  for (const record of records) {
    if (record.type !== 'attributes') continue
    console.log(div.dataset.foo);
    console.log(record);
  }
});
observer.observe(div, { attributes: true });

div.setAttribute('data-foo', 'foo');
div.setAttribute('data-foo', 'bar');
<div id="div"></div>

While it might have be possible using a synchronous observer (listen for the DOMAttrModified event on the element), they're deprecated and slow.

If you know how the attribute is going to be changed, you might be able to patch that method so that it goes through your own logic first. For example, with setAttribute:

div.setAttribute = (...args) => {
  console.log(`Setting ${args[0]} from ${div.getAttribute(args[0])} to ${args[1]}`);
  return HTMLElement.prototype.setAttribute.apply(div, args);
};

div.setAttribute('data-foo', 'foo');
div.setAttribute('data-foo', 'bar');
<div id="div"></div>
like image 45
CertainPerformance Avatar answered Mar 05 '23 07:03

CertainPerformance