Mutation Observer

Maybe a daft experiment.

There was a question on here this morning which promptly disappeared.

The OP had a form with an ordered list of form groups, each with a delete button. The class names in the form were in sequential order.

e.g.

<ol type='a'>
  <li><input type='text' class='something1'><button>delete</button></li>
  <li><input type='text' class='something2'><button>delete</button></li>
  <li><input type='text' class='something3'><button>delete</button></li>
</ol>

The OP wanted to be able to delete an item, keeping the class names in sequential order i.e. no gaps in the numbers

e.g.

<ol type='a'>
  <li><input type='text' class='something1'><button>delete</button></li>
  <li><input type='text' class='something2'><button>delete</button></li> <!-- delete me -->
  <li><input type='text' class='something3'><button>delete</button></li>
</ol>

should not result in this

<ol type='a'>
  <li><input type='text' class='something1'><button>delete</button></li>
  <li><input type='text' class='something3'><button>delete</button></li>
</ol>

but this instead

<ol type='a'>
  <li><input type='text' class='something1'><button>delete</button></li>
  <li><input type='text' class='something2'><button>delete</button></li>
</ol>

I have been looking into observables (is that a pun?), and this was an excuse for me to familiarise myself with Mutation Observers

codepen here

html

<form>
    <fieldset id='set1'>
      <legend>MutationObserver Experiment</legend>
      <ol type='A'>
        <li>
          <input type='text' class='color1' value='item1'>
          <button class='delete'>Delete</button>
        </li>
        <li>
          <input type='text' class='color2' value='item2'>
          <button class='delete'>Delete</button>
        </li>
        <li>
          <input type='text' class='color3' value='item3'>
          <button class='delete'>Delete</button>
        </li>
        <li>
          <input type='text' class='color4' value='item4'>
          <button class='delete'>Delete</button>
        </li>
        <li>
          <input type='text' class='color5' value='item5'>
          <button class='delete'>Delete</button>
        </li>
        <li>
          <input type='text' class='color6' value='item6'>
          <button class='delete'>Delete</button>
        </li>
      </ol>
    </fieldset>
  </form>

javascript

window.addEventListener('DOMContentLoaded', () => {

  const mutationHandler = ([ record ]) => {
    // assign previous element to deleted element as start point
    let element = record.previousSibling

    // Loop through the following elements/items
    while(element = element.nextElementSibling) {
      const input = element.querySelector('input[type="text"]')

      input.className = input.className
        // decrement color number
        .replace(/color(\d+)/, (_, num) => `color${parseInt(num, 10) - 1}`)
    }
  }

  const observer = new MutationObserver(mutationHandler)

  observer.observe(
    document.querySelector('#set1 ol'),
    { childList: true }
  )

  const deleteButtons = document.querySelectorAll('.delete')
  const removeItem = ({ target }) => { target.parentNode.remove() }

  deleteButtons.forEach(elem => elem.addEventListener('click', removeItem))
})

I don’t tend to do well with these posts, but any feedback or information is welcome.

As an aside, I don’t know if this is of interest to others, but the bright individual in this series of videos looks into hand rolling a mini rxjs framework as a study into observables and reactive programming. Thought it was quite interesting. :slight_smile:

1 Like

Careful. If the chosen one was the first element in the list, this will be null.

EDIT: Nope, now i’m confused. Either the element exists or it doesnt at the moment of execution. If it does, why go back at all, just use the being-deleted item as the start point and walk from there, or if it doesnt, you won’t be able to reference it’s siblings…

Thanks for the feedback m_hutley

You can access that deleted item from within the mutation observer callback. On the record it is in the removeNodes list (forgive me, I don’t have the facility of checking right now, going by memory)

Unfortunately as it is removed both next and previous sibling are null

Good point, I think in my test it didn’t run into an issue, because the previousSibling was an empty text node. That’s not reliable though.

I appreciate this could all be done in a more conventional way, but it was certainly worth investigating :slight_smile:

won’t that mean this line will fail? All of this code block is in the same execution space. You could probably avoid the null possibility and an unnecessary loop iteration with just let element = record instead?

1 Like

I’m currently at work, will have to get back to you on this Thanks again :slight_smile:

2 Likes

@m_hutley

Well as a quick test, a small amend to the html and removing the whitespace before the first list item confirms your point

<ol type='A'><li>
  <input type='text' class='color1' value='item1'>
  <button class='delete'>Delete</button>
</li>

Type error thrown
index.html:124 Uncaught TypeError: Cannot read property 'nextElementSibling' of null at MutationObserver.mutationHandler (index.html:124)

And the logged record

MutationRecord
  > addedNodes: NodeList []
    attributeName: null
    attributeNamespace: null
  > nextSibling: text
  > oldValue: null
  > previousSibling: null <---
  > removedNodes: NodeList [li]
  > target: ol
    type: "childList"
  > __proto__: MutationRecord

Assigning to record instead, makes good sense, however the record does not have a nextElementSibling property to get the ball rolling with the while loop

Brains are feeling a bit like pea soup at the moment, but one method is to just run with the old skool nextSibling approach and checking on the nodeType

const mutationHandler = ([ record ]) => {

    let element = record

    // record has a nextSibiling propery, but not a nextElementSibling
    while(element = element.nextSibling) {
      if (element.nodeType === 1) {
        const input = element.querySelector('input[type="text"]')

        input.className = input.className
          // decrement color number
          .replace(/color(\d+)/, (_, num) => `color${parseInt(num, 10) - 1}`)
      }
    }
  }

I sense I am missing the blindingly obvious though

1 Like

Kind of the downside of reacting after the change, rather than before it. (This is why onclicks exist!)

Presumably, record still has it’s className? Pseudocode:

set rep = record.className;
while(target = document.querySelector(rep.replace(+1))) {
  set next = target.className;
  target.className = rep;
  set rep = next;
}
1 Like

Wait… no the record is the ol… does record.removedNodes[0] have a nextSibling?

1 Like

Yes, but previous and next siblings are null.

Let me get my brain wrapped around that one : )

In the meantime a slight variation

const mutationHandler = ([ record ]) => {

    let element = record.nextSibling

    // if we have deleted the last child element return
    if (element === null) return

    // if element is not an element (text-node), get the next element
    if (element.nodeType !== 1) element = element.nextElementSibling

    while (element) {
      const input = element.querySelector('input[type="text"]')

        input.className = input.className
          // decrement color number
          .replace(/color(\d+)/, (_, num) => `color${parseInt(num, 10) - 1}`)

      element = element.nextElementSibling
    }
  }

(This is why onclicks exist!)

I think you are right, but it is good to examine it.

I am sure there are scenarios where this is a more applicable approach.

1 Like

I don’t understand how your original code was working though…
let element = record.previousSibling
if record is pointing at the ol, you’d be looking at the LIST’s previous sibling. Which would be the <legend> tag.
It then walks forward to nextElementSibling, so back to the ol we started at… and then… goes diving into the list and finds the first input… changes that… and then it should stop (because there are no siblings after the </ol>).

Based on your original HTML, you should end up with a list that removed a LI, and then set color1 to color0? Or have i gotten something completely arse-backwards here…

I’m having my doubts now. lol

I’m ready to crash @m_hutley, early morning and frazzled.

I need to have another read, but I think the key thing is childList. That is what we are observing

observer.observe(
  document.querySelector('#set1 ol'),
  { childList: true }
)

Cheers again for the feedback :slight_smile:

Odd, when i look at your codepen, and break inside the mutator callback, record.nextSibling is defined as a text node, and record.nextSibling would have a nextElementSibling that is the next li in the list… so… your codepen is telling me that let element = record.nextSibling should work… might want to doublecheck the result you got in post #6

Alternatively, queryselectorall, walk the entire list and just replace all the classNames with a brand new numbering and call it good. :laughing:

2 Likes

Is this a style of programming in JS:

One of those techniques is a destructuring technique called parameter context matching, and the other is an arrow-function.

They’re both a part of ES6, otherwise known of as ECMAScript 2015, and has been around for a while now.

There are many other handy ES6 features. I recommend that you investigate them further, now that Internet Explorer and its lack of support isn’t around to discourage their use.

2 Likes

Thank you so much @Paul_Wilkins
If we break the logic of the end result is as follows:

Premise: Irrespective of which element is deleted the color class should be in increasing order such as:’

If #3 is deleted still the end result will be:

1,2,3,4,5 in terms of color classes in that order.

I will try to do it with whatever JS I know.

1 Like

The flow chart that I can visualize for this is very simple. Of whatever li element and whatever button is clicked the last of the parent ol's li should be deleted.

How can we do this:

  1. First on every click on delete go to the parent( ol ), and then
  2. Delete the last child of the parent.

Are there any function in JS that can help us achieve this easily.

I tried something:

const parent = document.getElementById('parent');
parent.addEventListener('click', e => {
  var select = document.getElementById('parent');
  select.removeChild(select.lastChild);  
})

but that doesn’t deliver results.

I think I am close, but eventually it doesnt work:

const trackParent = document.getElementById('parent');
var lastChild = trackParent.lastChild;
trackParent.addEventListener('click', e => {
  console.log("click is succesful");
  var select = document.getElementById('parent');
  // select.removeChild(select.lastChild);  
  select.removeChild(select.childNodes[3]);  
})

Spot on!

The last of the list items should be deleted?

It could be me being dense, but not so sure on that one!

1 Like

Do you have a codepen?

a direct reaction version of the original problem is something like:

//Attach to all the buttons...
document.querySelectorAll('ol[type="a"] > li > button').forEach((button) => {
  //A click listener..
  button.addEventListener("click", (e) => {
   //I got tired of typing this.
    let my = e.target;
    //Pointer variable for moving through the list of LI's.
    let pointer = my.parentElement;
    //It's easier to do this now than in-situ. Calculate the number of the element we start at.
    let walker = parseInt(my.previousElementSibling.classList[0].replace("something", ""));
    //For all subsequent siblings:
    while (pointer = pointer.nextElementSibling) {
      //Find the text field.
      let input = pointer.querySelector("input");
      //Remove its current class.
      input.classList.remove(input.classList[0]);
      //Add the new one.
      input.classList.add("something" + walker);
      //Advance the walker.
      walker++;
    }
    //Finally, actually remove the LI we clicked the button on.
    my.parentElement.remove();
  });
});

(There are several assumptions made in this code, it is not a ‘generic’ code, it is specific to the layout in post #1. It would be far simpler to have the classes in the LI elements, but c’est la vie.)

2 Likes