0

I would like to append an SVG Path element dynamically via JavaScript and immediately after the element has been appended to the HTML DOM execute a transition via CSS.

My set-up (as shown in the below example): The SVG element to which the Path element is added sits inside a DIV element in the HTML.

The idea is that after appending the Path element to the SVG element, I add a class to the DIV to initialise the transition. To make sure that the order of appending the path element and adding the class is correct, I use a promise. Since I tried to make sure that the element is first appended to the DOM and class is only then added to the DIV element, I would expect the transition to work.

However, I observe that the transition does not work, not even when I add a delay to it (via CSS), and the element will immediately receive the styling from the end of the transition.

If I add a timeout after the path element has been appended and before the Div gets the class, it works. But this somehow seems like a hack to me. I also observed that if I append multiple Path elements to the SVG element, only the transition for the last child is broken.

My question therefore is: Why is the transition not working as expected? Is there a more elegant way to achieve what I want than adding the timeout?


This is a code example to illustrate my problem:

const animatePath = () => {
  return new Promise((resolve, reject) => {
    const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    pathElement.setAttribute('d', 'M0,0H500');
    pathElement.setAttribute('stroke', 'black');
    pathElement.setAttribute('stroke-width', 10);
    pathElement.setAttribute('fill', 'none');

    document.getElementById('svgElement').appendChild(pathElement);
    resolve();
  });
};

document.getElementById('append-toggle').addEventListener('click', (event) => {
  animatePath().then(() => {
    document.getElementById('wrapper').classList.add('show');
  }).catch((error) => {
    console.log(error);
    return Promise.reject(error);
  });
}, false);

let waitTimeout = false;
document.getElementById('append-wait-toggle').addEventListener('click', (event) => {
  animatePath().then(() => {
    clearTimeout(waitTimeout);
    waitTimeout = setTimeout(() => {
      document.getElementById('wrapper').classList.add('show');
    }, 250);
  }).catch((error) => {
    console.log(error);
    return Promise.reject(error);
  });
}, false);

document.getElementById('toggle').addEventListener('click', (event) => {
  document.getElementById('wrapper').classList.toggle('show');
});

document.getElementById('reset').addEventListener('click', (event) => {
  document.getElementById('wrapper').classList.remove('show');
  while(document.getElementById('svgElement').lastChild){
    document.getElementById('svgElement').lastChild.remove();
  }
});
#wrapper path {
  opacity: 0;
  transition: opacity 2s ease;
}

#wrapper.show path {
  opacity: 1;
}

#wrapper.show {
  outline: 1px solid red;
}
<div id="wrapper">
  <svg width="500px" height="20px" viewBox="0 0 500 20" id="svgElement"></svg>
</div>
<br />
<button id="append-toggle">Append path and then toggle class</button>
<br />
<button id="append-wait-toggle">Append path, wait and then toggle class</button>
<br />
<button id="toggle">Toggle class</button>
<br />
<button id="reset">Reset</button>

Note: I need to animate the Path element and not one of its parents, because what I want to animate in my project is not the opacity of the element but the stroke-dashoffset which I get into the CSS via style="--path-length: ${pathElement.getTotalLength()}". I first thought that this might be the cause of the problem, but as the above example shows, it is not.


Edit

Part of my question, namely as to why this happens, has been answered here. A solution is obviously to force the browser to reflow after the element has been appended to the DOM. However, the linked answers don't relate to SVG.

So, my question would still be: Is there an optimal way to force a reflow especially for SVG elements?

Jasper Habicht
  • 211
  • 3
  • 12
  • Might be easier to animate with (native SVG) SMIL instead of CSS – Danny '365CSI' Engelman Nov 22 '22 at 15:22
  • @Danny'365CSI'Engelman Yes, this would be a solution. But I would still be interested what causes the above behavior. I further observed that if I append more than paths to the SVG parent, only the transition for the last child is broken. Somehow, I think this has to do with the browser not yet being ready when I change the class for the parent. But how could I check for that? – Jasper Habicht Nov 22 '22 at 16:08
  • 1
    Force a reflow after you did append your element. I suppose the less invasive reflow would be to call `.getBBox()` on your `` element, smart-enough browsers should be able to not recalculate the whole page layout but only that one element. https://jsfiddle.net/rmcvy297/ – Kaiido Nov 23 '22 at 01:19
  • @Kaiido Thanks! This is a great idea! Indeed I already found the idea to force a reflow, but I did not know about `.getBBox()` until now. – Jasper Habicht Nov 23 '22 at 08:50
  • Regarding your edit, [many things will force a reflow in SVG](https://gist.github.com/paulirish/5d52fb081b3570c81e3a#svg) (that same link is also available in one of the linked answers in the dupe target), and you don't even *need* to use an "SVG specific" trigger. Things like `getComputedStyle(elem).width` also work on SVG. Honestly I don't see the point in reopening that question. It's all answered in the dupe target(s). – Kaiido Nov 23 '22 at 10:16
  • @Kaiido Okay, than leave it as it is. Thanks! – Jasper Habicht Nov 23 '22 at 10:25

1 Answers1

0

I found this answer which seems to be a feasable approach in this case even without the nested timeout.

The relevant MDN page states:

The window.requestAnimationFrame() method tells the browser that you wish to perform an animation and requests that the browser calls a specified function to update an animation before the next repaint.

The following seems to work:

const animatePath = () => {
  return new Promise((resolve, reject) => {
    const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
    pathElement.setAttribute('d', 'M0,0H500');
    pathElement.setAttribute('stroke', 'black');
    pathElement.setAttribute('stroke-width', 10);
    pathElement.setAttribute('fill', 'none');

    document.getElementById('svgElement').appendChild(pathElement);
    resolve();
  });
};

document.getElementById('append-toggle').addEventListener('click', (event) => {
  animatePath().then(() => {
    window.requestAnimationFrame(() => {
      document.getElementById('wrapper').classList.add('show');
    });
  }).catch((error) => {
    console.log(error);
    return Promise.reject(error);
  });
}, false);

document.getElementById('toggle').addEventListener('click', (event) => {
  document.getElementById('wrapper').classList.toggle('show');
});

document.getElementById('reset').addEventListener('click', (event) => {
  document.getElementById('wrapper').classList.remove('show');
  while(document.getElementById('svgElement').lastChild){
    document.getElementById('svgElement').lastChild.remove();
  }
});
#wrapper path {
  opacity: 0;
  transition: opacity 2s ease;
}

#wrapper.show path {
  opacity: 1;
}

#wrapper.show {
  outline: 1px solid red;
}
<div id="wrapper">
  <svg width="500px" height="20px" viewBox="0 0 500 20" id="svgElement"></svg>
</div>
<br />
<button id="append-toggle">Append path and then toggle class</button>
<br />
<button id="toggle">Toggle class</button>
<br />
<button id="reset">Reset</button>
Jasper Habicht
  • 211
  • 3
  • 12