1

I'm trying to make a fade in effect for new elements created with innerHTML. This is part of a FullStack course, which only allows basic JS and CSS (no jQuery or other libraries). The particular exercise has you add notes to a parent element, but rather than displaying immediately, the notes are supposed to fade in (space is made for the note immediately, but it should be blank, showing just the background, and the note content should then fade in).

To implement this, I'm using CSS transitions. I create the new note element by appending to the parent element's innerHTML property. Notes initially have an opacity of 0, which should create a hidden note. The fade-in is achieved with two pieces:

  • a transition on the opacity property of notes, and
  • a 'fadeIn' class (added to new notes after creating them) that has an opacity of 1

After creating a new note, the 'fadeIn' class is added to the note, which should change the opacity from 0 to 1 and trigger the transition, causing a gradual fade-in. Instead, new notes show up immediately.

The following snippet shows how notes are created, how the 'fadeIn' class is added, and the CSS that should create a fade-in.

var tasksArray = [
    {date: '2017-01-28', text: 'Work on FullStack course: CSS transitions exercise.', complete:true},
    {date: '2017-01-29', text: 'Debug solution; figure out why new elements display immediately, rather than fading in.', complete:true},
    {date: '2017-01-30', text: 'Ask for help on SO.', complete:true},
    {date: '2017-01-31', text: 'Accept solution & upvote.', complete:false},
];
var notesArea = document.getElementById('Notes');

// code to focus on
function PrintNote(taskIndex) {
    // Print a note for the given task.
    notesArea.innerHTML += 
       `<div id="taskN${taskIndex}">
            <span class="noteTitle">Task #${taskIndex + 1}</span>
            <p class="noteTXT">${tasksArray[taskIndex].text}</p>
            <span class="noteDate">${tasksArray[taskIndex].date}</span>
        </div>`;
    // Get the new note.
    noteDivsElement = document.querySelector("#taskN" + taskIndex);
    // Change the new note's opacity by adding a class.
    noteDivsElement.className = 'fadeIn';
}

// example only
function PrintNotes(i) {
    PrintNote(i++);
    if (i < tasksArray.length) {
        setTimeout(() => PrintNotes(i), 1500);
    }
}
PrintNotes(0);
    div[id^="taskN"] {
        /* styling to focus on */
        opacity: 0;
        transition: opacity 1s ease-in;

        /* example only */
        box-sizing: border-box;
        margin: 4%;
        border: 1px solid black;
        padding: 0.5em;
        background-color: #CCC;
        width: 40%;
        position: relative;
        display: inline-block;
    }

    div[id^="taskN"].fadeIn{
        /* styling to focus on */
        opacity: 1;
    }
<div id="Notes">
</div>

Why don't new notes fade-in, and how can I correct?

outis
  • 75,655
  • 22
  • 151
  • 221

1 Answers1

1

The browser is probably batching all DOM instructions in 1 paint. Therefore all divs immediately get 'fadeIn' applied.

To force 'fadeIn' to only be applied on the next paint-cycle, use reqeuestAnimationFrame.

This will result in the browser seeing a before-state (opacity:0) and after-state (opacity:1) and as a result the transition will kick in.

Something like:

function PrintNote(taskIndex) {
  //print a note, index is given to let the function know which task to print from the array
  notesArea.innerHTML += "<div id='taskN" + taskIndex + "'><span trash='" + taskIndex + "' class='glyphicon glyphicon-remove-circle deleteBtn'></span><strong><span class='noteTitle'>Task #" + (taskIndex + 1) + "</span><p class='noteTXT'>" + tasksArray[taskIndex].text + "</p><span class='noteDate'>" + tasksArray[taskIndex].date + "</span></strong></div>"

  //when done printing, get all the delete buttons into the deletesArray, give each the remove task function
  var noteDivsElement = document.querySelector("#taskN" + taskIndex);

  window.requestAnimationFrame(function(){
    noteDivsElement.className = 'fadeIn';
  });

  var deletesArray = document.getElementsByClassName("glyphicon glyphicon-remove-circle deleteBtn");
  for (var i = 0; i < deletesArray.length; i++) {
    deletesArray[i].onclick = RemoveTask;
  }
}

EDIT

As per comment, above should be called in a loop. Need to therefore make sure to have a proper closure on noteDivsElement (1)

To explain in a bit more detail: if you were to do a console.log(noteDivsElement) after the function body the variable noteDivsElement would still be set. This is probably counter-intuitive, but it's just how vars work in javascript. I.e.: it leaks to the global scope. On each iteration this same variable is overwritten. Since setting fadeIn is delayed, all assignments of fadeIn happen after the loop and thus all happen against the latest assignment of noteDivsElement.

This is a problem that happens a lot in javascript. Often when a loop and async operation is combined.

The default way to counter is, is to create a closure which binds the variable to a function argument, making it available as part of the context even after the loop finished. It's hard to explain so please read up on the link provided at the bottom.

function PrintNote(taskIndex) {
  //print a note, index is given to let the function know which task to print from the array
  notesArea.innerHTML += "<div id='taskN" + taskIndex + "'><span trash='" + taskIndex + "' class='glyphicon glyphicon-remove-circle deleteBtn'></span><strong><span class='noteTitle'>Task #" + (taskIndex + 1) + "</span><p class='noteTXT'>" + tasksArray[taskIndex].text + "</p><span class='noteDate'>" + tasksArray[taskIndex].date + "</span></strong></div>"

  //when done printing, get all the delete buttons into the deletesArray, give each the remove task function
  var noteDivsElement = document.querySelector("#taskN" + taskIndex);

  (function(el){ 
    //'el' is part of closure and is a local variable only to this iteration
    window.requestAnimationFrame(function(){
      el.className = 'fadeIn';
    });
  }(noteDivsElement))


  var deletesArray = document.getElementsByClassName("glyphicon glyphicon-remove-circle deleteBtn");
  for (var i = 0; i < deletesArray.length; i++) {
    deletesArray[i].onclick = RemoveTask;
  }
}

Another ES6-way to accomplish the same thing is using a proper local variable by using let instead of var which circumvents all the problems talked about. This isn't supported in all browsers yet though:

function PrintNote(taskIndex) {
  //print a note, index is given to let the function know which task to print from the array
  notesArea.innerHTML += "<div id='taskN" + taskIndex + "'><span trash='" + taskIndex + "' class='glyphicon glyphicon-remove-circle deleteBtn'></span><strong><span class='noteTitle'>Task #" + (taskIndex + 1) + "</span><p class='noteTXT'>" + tasksArray[taskIndex].text + "</p><span class='noteDate'>" + tasksArray[taskIndex].date + "</span></strong></div>"

  //when done printing, get all the delete buttons into the deletesArray, give each the remove task function
  //NOTE THE 'let' here
  let noteDivsElement = document.querySelector("#taskN" + taskIndex);

  window.requestAnimationFrame(function(){
    noteDivsElement.className = 'fadeIn';
  });

  var deletesArray = document.getElementsByClassName("glyphicon glyphicon-remove-circle deleteBtn");
  for (var i = 0; i < deletesArray.length; i++) {
    deletesArray[i].onclick = RemoveTask;
  }
}

BTW: having to use closures and window.requestAnimationFrame doesn't strike me as part of a JS beginners course so I might have pushed you in the wrong direction. Regardless knowing closures is a really important part of knowing javascript so I hope it still helps. Good luck!

1) how do closures work

Community
  • 1
  • 1
Geert-Jan
  • 18,623
  • 16
  • 75
  • 137
  • Hey, thanks for answering :) I'll give this a try, though we haven't learned window.requestAnimationFrame in class. will tell you how it worked out. – Eran Ballili Jan 31 '17 at 04:26
  • so, it worked ! the animation works. But now, when i print all the other notes on the array in one loop, only the last one shows. Any ideas ? `function PrintAllNotes() { //print all the tasks in the array for (var i = 0; i < tasksArray.length; i++) { PrintNote(i); } }` Thanks again ! – Eran Ballili Jan 31 '17 at 04:45
  • Hey @Geert-Jan Thanks again for helpin. I tried the new method, got the same results as before. It only fades in the last one in the array. If you add another note, it fades in beautifully. But when I refresh the page, its supposed to get everything saved in the localStorage into the array, and print all of it. Unfortunately, it shows only the last one. Edit: also, by the way, noteDivsElement is a global var. But it also doesn't work when its a local var for the function – Eran Ballili Feb 01 '17 at 06:42
  • Well `noteDivsElement` should definitely *not* be a global var. I've described that clearly. Not sure why the 2 options given above aren't working for you though – Geert-Jan Feb 01 '17 at 11:26