Learning jQuery 2019 (Part 3)

I ended my last post having realized I have never done any sort of fancy visual manipulation (like animations) in a Vue app. I have limited myself to Bootstrap, and to the directives built into Vue for showing and hiding things, etc.

In one particular Vue app I wrote, I was already using Bootstrap, which currently requires jQuery as a dependency, so I started thinking about adding some fancy flourish for its own sake and for practice.

Now, in my first attempt I am probably trying to rebuild something that is already built somewhere, but as always the goal is to learn. I wanted to learn how to make an element briefly highlight itself when it is first inserted into the page. To do this, I defined a custom directive like this.

Vue.directive('highlight', {
    inserted : function (el, binding) {
        el.style.backgroundColor = "yellow";
        setTimeout(()=> {
            el.style.backgroundColor = "";
        }, 2000);
    },
});

That is a first crude attempt. It doesn’t transition anything. It just makes the item yellow when it first appears. Then after 2 seconds it sharply changes to whatever it was before. It’s also worth nothing that I’m applying this to an element that gets cloned into a list using v-for and it’s possible to add and remove items from that list. Thus: when new items are added, I want each one to be highlighted at first and then fade to normal.

Unfortunately, whenever I tried to add transitions to this thing I started getting unexpected behavior. I changed the code above slightly as follows.

el.style.backgroundColor = "yellow";
setTimeout(()=> {
    el.style.transition = "background-color 2s";
    el.style.backgroundColor = "";
 }, 10);

This appears to work at first, but when I played with it, adding new list elements quickly, several in a row, etc., I found that sometimes they didn’t get this highlighting. Just did not. As a programmer, when I see an intermittent error, I usually suspect a race condition. I think under the hood Vue’s inserted hook might be causing the race condition. It does not seem to guarantee that things are properly mounted in time for me to perform style changes on the element.

As I write this, I just performed my experiment again. Out of many instances of adding a new element to my list, on two occasions the new element appeared but it wasn’t highlighted.

My own code is dead-simple, so I think the problem must be in the Vue engine itself. But I don’t plan to dive into the source and fix it myself. I think I need a more robust way to grab these elements and perform style changes. Maybe I can reproduce this error again and log the contents of el to the console so I can check if it did point to a (wrapped) dom element.

Another possible cause of this error occurred to me. Could it be a race condition between my style updates and bootstrap’s style updates? Perhaps somehow bootstrap styles are being applied programmatically, and they are racing with my style updates. I really have no idea what crazy crap bootstrap might be doing behind the scenes. To check that, I removed the Bootstrap.js file. The problem remains.

I think I’m unlikely to find the actual cause of this error. There are just too many moving parts. I’m not going to spend a months studying Vue’s source code right now. In this situation, what I want is some kind of monkey patch. What that means is this: Let’s make it work correctly, but the code might be a little hacky. Is the problem that we are trying to mutate styles on nodes while Vue is still doing some kind of mutation on it itself? If so, will Vue.nextTick help?

I tried using nextTick. This produced bizarre and totally unpredictable behavior.

I have a different idea: instead of using the el element that gets passed into the inserted function, I could grab the element using a more robust method. I could give the element a unique identifier when its created and I could use jQuery to grab that element. Here’s something that actually seems to work even though it’s ugly and hacky. If you have to write things that are ugly and hacky, a directive is supposed to be the place to put that logic. You can see here I’m referencing the element using a unique selector that gets put on the nodes programmatically.

    inserted : function (el, binding) {
        const selector = `[data-item=${binding.value}]`;
        setTimeout(()=>{
            $(selector).css("background-color","yellow");
        },10);
        setTimeout(() => {
            $(selector).css({"transition" : "background-color 1s", "backgroundColor" : ""});
        }, 1000);
        setTimeout(()=> {
            $(selector).css("transition","");
        }, 2000);
    },

Ugly, right? But it’s the most robust thing I’ve managed so far. It actually works. It produces the desired behavior and it doesn’t require any extra jQuery plugin on top of jQuery. I queued the first setting (bg color to yellow) to happen at 10 milliseconds just so it was queued up to happen after any any other processing it might be racing against but to the user it still feels instantaneous. That’s definitely a hack, but in the JavaScript world it’s a time-honored, traditional hack. And so far I haven’t seen it produce the same error as my previous attempts. Maybe under the hood jQuery is doing some kind of fancy asynchronous thing to make applying the styles work. Maybe some day I’ll read about that.

Update: I think I found a more jQuery-idiomatic way to write the robust code above, as follows.

$(selector).queue(function() {
    $(this).css("background-color","yellow").dequeue();
})
    .delay(20)
    .queue(function() {
        $(this).css({"transition" : "background-color 2s", "backgroundColor" : ""}).dequeue();
    })
    .delay(2200)
    .queue(function() {
        $(this).css("transition","").dequeue();
    });

This seems to work just as well and it’s more jQuery-ish.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s