Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork932
Description
Mithril version: 2.0.4
Browser and OS: Chrome 83.0.4103.116 on Windows 64-bit, Safari 13.1 on iOS, Firefox 78.0.2 on Windows 64-bit
Project:
Code
<buttonid="toggle">Toggle</button><divstyle="display:flex"><ulid="prev"><li>0</li><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li></ul><ulid="animate"></ul><ulid="next"><li>0</li><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li></ul></div>
constanimateRoot=document.getElementById("animate")letvalues=Array.from({length:8},(_,i)=>i)functionrender(){m.render(animateRoot,values.map(i=>m("li",{key:i},i)))}letinterval=setTimeout(updateBody,500)functionupdateBody(){interval=setTimeout(updateBody,500)constprev=values// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffleconstnext=values=prev.slice()for(leti=next.length-1;i>0;i--){constj=Math.floor(Math.random()*(i+1))consttemp=next[j]next[j]=next[i]next[i]=temp}requestAnimationFrame(()=>{constprevList=document.getElementById("prev").childNodesconstnextList=document.getElementById("next").childNodes// Reorder elements in DOMfor(leti=0;i<prev.length;i++){prevList[i].textContent=prev[i]}for(leti=0;i<next.length;i++){nextList[i].textContent=next[i]}constprevElems=Array.from(animateRoot.childNodes)// FirstconstfirstParent=animateRoot.getBoundingClientRect()constfirst=prevElems.map(e=>{constchild=e.getBoundingClientRect()return{top:child.top-firstParent.top,left:child.left-firstParent.left,}})render()// Last + InvertconstlastParent=animateRoot.getBoundingClientRect()constmovedElems=prevElems.filter((e,i)=>{constlast=e.getBoundingClientRect()constdx=first[i].left-(last.left-lastParent.left)constdy=first[i].top-(last.top-lastParent.top)if(dx===0&&dy===0)returnfalsee.style.transform=`translate(${dx}px,${dy}px)`e.style.transitionDuration="0s"returntrue})// Force re-layoutdocument.body.offsetHeight// PlaymovedElems.forEach((e,i)=>{constcallback=()=>{e.removeEventListener("transitionend",callback,false)e.classList.remove("transition")}e.classList.add("transition")e.addEventListener("transitionend",callback,false)e.style.transform=e.style.transitionDuration=''})})}document.getElementById('toggle').onclick=()=>{if(interval){clearTimeout(interval)interval=null}else{updateBody()}p('toggle',interval!=null)}render()
Steps to Reproduce
- Open web page
You can enable and disable the animations at will using the "Toggle" button, but that's not part of the repro.
Expected Behavior
The animation to be smooth.
Current Behavior
The animation appears choppy whenever the subtree updates, and list items often unexpectedly jump.
Context
The following vanilla code works correctly. It's a near-exact clone of the above, but it simply usesappendChild instead of a full keyed diff.
<buttonid="toggle">Toggle</button><divstyle="display:flex"><ulid="prev"><li>0</li><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li></ul><ulid="animate"><li>0</li><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li></ul><ulid="next"><li>0</li><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li></ul></div>
constanimateRoot=document.getElementById("animate")letcachedElems=Array.from(animateRoot.childNodes)letinterval=setTimeout(updateBody,500)functionrender(){for(constelemofcachedElems)animateRoot.appendChild(elem)}functionupdateBody(){interval=setTimeout(updateBody,500)constprevElems=cachedElemsconstnextElems=cachedElems=prevElems.slice()// https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shufflefor(leti=nextElems.length-1;i>0;i--){constj=Math.floor(Math.random()*(i+1))consttemp=nextElems[j]nextElems[j]=nextElems[i]nextElems[i]=temp}requestAnimationFrame(()=>{constprevList=document.getElementById("prev").childNodesconstnextList=document.getElementById("next").childNodes// Reorder elements in DOMfor(leti=0;i<prevElems.length;i++){prevList[i].textContent=prevElems[i].textContent}for(leti=0;i<nextElems.length;i++){nextList[i].textContent=nextElems[i].textContent}// FirstconstfirstParent=animateRoot.getBoundingClientRect()constfirst=prevElems.map(e=>{constchild=e.getBoundingClientRect()return{top:child.top-firstParent.top,left:child.left-firstParent.left,}})render()// Last + InvertconstlastParent=animateRoot.getBoundingClientRect()constmovedElems=prevElems.filter((e,i)=>{constlast=e.getBoundingClientRect()constdx=first[i].left-(last.left-lastParent.left)constdy=first[i].top-(last.top-lastParent.top)if(dx===0&&dy===0)returnfalsee.style.transform=`translate(${dx}px,${dy}px)`e.style.transitionDuration="0s"returntrue})// Force re-layoutdocument.body.offsetHeight// PlaymovedElems.forEach((e,i)=>{constcallback=()=>{e.removeEventListener("transitionend",callback,false)e.classList.remove("transition")}e.classList.add("transition")e.addEventListener("transitionend",callback,false)e.style.transform=e.style.transitionDuration=''})})}// Utility bitsdocument.getElementById('toggle').onclick=()=>{if(interval){clearTimeout(interval)interval=null}else{updateBody()}p('toggle',interval!=null)}render()
Edit:
Mithril's far from the only one with this issue:https://twitter.com/isiahmeadows1/status/1284726730574315522
- React:Bug: Updates to keyed lists break FLIP animations when they occur mid-animation facebook/react#19406
- Preact:Updates to keyed lists break FLIP animations when they occur mid-animation preactjs/preact#2637
- Inferno:Updates to keyed lists break FLIP animations when they occur mid-animation infernojs/inferno#1519
- And likely others
Note: Vue isnot affected by this bug and carries the same behavior as the vanilla version - gohere and spam the "Shuffle" button to see.
Edit 2: Also relevant:https://github.com/whatwg/html/issues/5742
Metadata
Metadata
Assignees
Labels
Type
Projects
Status