1

I have an ul list where every list-item (li) updates dynamically. On every new data for a specific li I want to apply a css effect via jQuery.

Current HTML is:

<ul>
 <li class="list-item">data0</li>
 <li class="list-item">data1</li>
</ul>

When <li>data0</li> is changed, I want to apply a css effect which changes that li's background color from white to green and remains green. Until that same li is updated again when I would like to change the background back to white then to green again.

My current jQuery logic is this, but doesn't seem to work:

if (!$('.list-item').hasClass('effect')){
     $('.list-item').addClass('effect');
} else {
     $('.list-item').removeClass('effect');
     $('.list-item').addClass('effect');
}

effect css:

.effect { background-color: rgba(45, 171, 64, 0.1); transition: 0.2s; } 

list-item css class doesn't set any background settings.

Here's a jsfiddle for current code: http://jsfiddle.net/dwmahagq/

tosi
  • 611
  • 9
  • 24
  • try this http://api.jquery.com/toggleclass/ toggleClass – Álvaro Touzón Nov 02 '17 at 12:05
  • I hope it just because you haven't included it in the code example, but its `.effect {}` not `effect {}` – Carsten Løvbo Andersen Nov 02 '17 at 12:08
  • The transition must be set on the .list-item class, not the .effect class so that all li elements have it by default. – Scott Marcus Nov 02 '17 at 12:08
  • Both the logic and the syntax are wrong. For example, you need a dot in `.effect` to indicate it's a class – Christian Vincenzo Traina Nov 02 '17 at 12:08
  • To put what @ScottMarcus said another way: the transition is on `.effect` so if you remove that class, it won't have a transition applied. – freedomn-m Nov 02 '17 at 12:09
  • How/when are you calling your js code? Had a question earlier that was using `load` event but using `.append`. – freedomn-m Nov 02 '17 at 12:10
  • @freedomn-m js code is executed once `updateListItem()` event fires, which is triggered by another section of the site. I've tried adding a new class `transition` as a default to every `li` and remove/add `effect` accordingly via the same jQuery code, it still doesn't work. – tosi Nov 02 '17 at 12:15
  • @freedomm-m Not really. The transition (the way the OP has it) is on any CSS property change. The element must be told this before the change happens, not at the moment of the change. – Scott Marcus Nov 02 '17 at 12:19
  • @gitterio Take a look form my solution. No code overhead, just one simple command line and you have what you want. –  Nov 02 '17 at 12:54

4 Answers4

4

OK, starting with the CSS: to do a transition, you need to specify what you're transitioning from as well as what you're transitioning to, and the transition time should be on the "before" state (you have it on the "after" state).

So your CSS could look like this:

li {background-color: #FFF; transition: all 0.2s}
.effect { background-color: rgba(45, 171, 64, 0.1) } 

(the 'no effect' li has a background color and the transition time; the 'with effect' has a different background color.)

Now on to the javascript:

if (!$('.list-item').hasClass('effect')){
     $('.list-item').addClass('effect');

That "if" is harmless but unnecessary here. You can just call addClass to add a class; if the element already has the class nothing will happen.

} else {
     $('.list-item').removeClass('effect');
     $('.list-item').addClass('effect');

I assume you're trying to trigger the transition again even on elements that already have the effect --- this won't work the way you have it, because the javascript will batch those DOM changes together, so as far as the DOM is concerned nothing changed at all.

Instead you need to remove the effect and then do a setTimeout to bring it back.

Putting all that together results in:

$('.test').on("click",function() {
  $('.list-item').removeClass('effect');
  window.setTimeout(function() {
      $('.list-item').addClass('effect');
  }, 200); // <-- same duration as the transition
});
li {transition: all 0.2s; background-color:#FFF}

.effect { background-color: rgba(45, 171, 64, 0.1); } 
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul>
 <li class="list-item">data0</li>
 <li class="list-item">data1</li>
</ul>

<div class="test">Click</div>

(Naturally in real life you'd probably want to apply this to a single list item instead of all of them as shown here; sounds from the comments like you have that part under control already).

Another method

A commenter points out that instead of setTimeout you can trigger a DOM reflow by reading a property from the DOM between removing and re-adding the class, e.g.

$('.test').on("click", function() {
  $('.list-item').removeClass('effect');
  void this.clientWidth; // read any dom property, doesn't matter which
  $('.list-item').addClass('effect');
});

This works well, but with one limitation: there can't be any transition duration set on the effect's exit. (In this case, if there's a transition duration set on li the animation will not restart; if the duration is set only on li.effect then it will work.) To demonstrate:

$('button').click(function() {
  var li = $(this).prev('li');

  li.removeClass('effect');
  void this.clientWidth;
  li.addClass('effect');
});
li {
  background-color: #FFF; padding: 0.5em
}

li.effect {
  background-color: #FFC;
  transition: all 1s;
}

.b {
  transition: all 1s;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p>Here you can repeat the animation:</p>
<li>Item</li>
<button>This will work</button>

<p>...but here you can't (because the transition time on the 'li' prevents the effect from being visible):
<li class="b">Item</li>
<button>This will not work</button>
Daniel Beck
  • 20,653
  • 5
  • 38
  • 53
  • I don't understand what you're saying, @WaldemarIce? – Daniel Beck Nov 02 '17 at 12:29
  • setTimeout() approach is not ideal. –  Nov 02 '17 at 12:30
  • In what way is it not ideal? Do you know of another way to remove a class, wait for a specified duration, and add it again? (Or another way to restart that transition?) – Daniel Beck Nov 02 '17 at 12:31
  • @WaldemarIce I've found another way - to clone and re-insert the `li`, but that would be even less ideal in my case, I think, since `li`s have a lot of data. – tosi Nov 02 '17 at 12:43
  • You are good googler :) But cloning is not ideal also. Look for my solution. Whole problem is in browsers lazy approach to redraw. You must just trigger the reflow event in browser. And its simple, between two "write to dom" functions insert "reading dom" one. –  Nov 02 '17 at 12:51
  • That trick to trigger the doc reflow is a good one, and new to me. In this case I think the timeout is preferable, because it allows the exit transition to run as well as the enter transition, but that's a good tool to know about, thanks! – Daniel Beck Nov 02 '17 at 12:58
  • @DanielBeck One thing Daniel... What do you mean with this "This works well, but with one limitation: there can't be any transition..."? I dont understand. Try few quick clicks in your snippet and in my snippet. I think you get more consistent UX with my reflow solution, as with setTimeout - I see no setTimeout benefits. What I am missing? –  Nov 02 '17 at 13:58
  • @WaldemarIce do you have a link to the copy? – freedomn-m Nov 02 '17 at 14:06
  • @freedomn-m Copy what? –  Nov 02 '17 at 14:14
  • @freedomn-m give me link to your question if you need help. –  Nov 02 '17 at 14:17
  • @WaldemarIce I've updated my answer with an example demonstrating the limitation. Took me a while at first to realize that the reason your javascript worked in your example but not in mine was because of a minor difference in the CSS; forcing a doc reflow only works for immediate, instantaneous changes, nothing with a transition time. – Daniel Beck Nov 02 '17 at 14:32
  • @DanielBeck Ok, thanx. –  Nov 02 '17 at 14:33
  • (Secondarily, the benefit of the setTimeout I had in mind was that it fades out as well as in, instead of jumping directly to the 'off' state before fading back on. A small detail, to be sure.) – Daniel Beck Nov 02 '17 at 14:38
  • @WaldemarIce I don't have a question, you stated *"Copied solution from another stackoverflow answer."*, so I was asking for a link to that other answer. – freedomn-m Nov 02 '17 at 15:48
  • 1
    @freedomn-m Hm... I don't remember. Google for "jquery add remove class to restart transition". I think you will find many links recommending setTimeout. Including stackoverflow answers. –  Nov 02 '17 at 15:58
  • @freedomn-m Here, for example: https://stackoverflow.com/questions/16050914/css-animation-doesnt-restart-when-resetting-class –  Nov 02 '17 at 16:00
  • 1
    Yeah, the not-so-vague insinuation that I couldn't possibly have remembered the simple setTimeout technique without "copying another answer", or that glitterio couldn't possibly have thought of cloning and replacing without google, was kind of weird and unnecessary. – Daniel Beck Nov 02 '17 at 16:00
  • 1
    Exactly, there's a huge difference between copying another answer *verbatim* (as implied) and providing a new answer based on vaguely similar principles. – freedomn-m Nov 02 '17 at 17:09
1

Ideal solution, no unnecessary setTimeouts, element cloning, etc. One must just force the reflow process in browser, because browsers are batching - the have "lazy approach" - to dom writes.

$('button').on('click', function () {
  $('.list-item').each(function () {
    $(this).toggleClass('effect')
    if (!$(this).hasClass('effect')) {

      // Between two consequent DOM writes,
      // two .toggleClass() method calls,
      // insert command which reads DOM.
      // In other words, give browser no
      // chance to optimize (batch)
      // consequent DOM writes.
      // This creates something called
      // "layout trashing", which itself
      // is undesirable effect in most
      // cases. But as you can see,
      // sometimes is "the evil"
      // the simpliest solution :)
      void this.clientWidth

      $(this).toggleClass('effect')
    }
  })
})
.effect {
  transition: all 1s;
  background-color: yellow;
}
<ul>
  <li class="list-item">ITEM 1</li>
  <li class="list-item">ITEM 2</li>
</ul>

<button>run/restart effect</button>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
  • Yes except every list-item shouldn't be updated, which is already handled. The second part of my jQuery code doesn't work. `} else { $('.list-item').removeClass('effect'); $('.list-item').addClass('effect'); }` – tosi Nov 02 '17 at 12:19
  • @gitterio In other words, you wants to restart the transition? –  Nov 02 '17 at 12:21
0

Here is an example of switching the list item classes back and forth using a button click event. See the snippet below to see how you can remove or add the green background class:

$(function(){
  $("#switchBtn").click(function(){
    if (!$('.list-item').hasClass('effect')){
        $('.list-item').addClass('effect');
    } else {
         $('.list-item').removeClass('effect');
         //$('.list-item').addClass('effect');
    }
  });
});
.effect { 
  background-color: rgba(45, 171, 64, 0.1); transition: 0.2s; 
} 
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<ul>
 <li class="list-item">data0</li>
 <li class="list-item">data1</li>
</ul>
<br /> 
<button id="switchBtn">Switch</button>
MUlferts
  • 1,310
  • 2
  • 16
  • 30
0

Please try this, by just click on li and it will show you effect.

    <!DOCTYPE html>
    <html>
    <head>
    <style>
    .effect { background-color: rgba(45, 171, 64, 0.1); transition: 0.2s; }
    </style>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <script>
    $(document).ready(function(){
        $("li").click(function(){
       $(this).toggleClass("effect");
        });
    });
    </script>
    </head>
    <body>
    <ul>
     <li class="list-item">data0</li>
     <li class="list-item">data1</li>
    </ul>
    </body>
    </html>
Scott Marcus
  • 64,069
  • 6
  • 49
  • 71
Bhanu Pratap
  • 1,635
  • 17
  • 17