I have a React functional component that recursively renders nested lists. That part is working fine.
The part that I am struggling with is getting the divs that contain the nested lists to have an expanding animation when the nested lists (of varying sizes) appear and disappear.
Because the amount of content is not known, simply animating the max-height property does not work. For example, when one level of lists is rendered, the height of the frame might expand to 100px. However, if you animate the max-height to 100px, then the div can not expand later on to accommodate more and more nested lists that get expanded.
<div className="frame"> [nested ordered lists] </div>
Again, I previously tried using a CSS transition on the max-height property but that is much less than an ideal solution, because it does not work well on unpredictable heights. So, I don't want to do it that way, if at all possible.
I followed a tutorial and got it working with vanilla JavaScript, without React (parts of the code that are commented out), but when translating it to React, I am not sure why I can not get the code to work.
At the moment, the expanding animation is working, but it will stop working if the border of the is removed or changed to the color white. I don't know why. ??
** Also, the collapsing animation is not working at all. The 'transitioned' event does not always fire on the ref object, and sometimes fires after it is supposed to have been removed.
Is anyone able to help point me in the right direction? Thanks!
(I tried to transfer it to JS Fiddle, but it's not working, so pls don't down-vote for that).
(another attempt on codepen, for reference. The nested lists are appearing and disappearing, but not transitioning). https://codepen.io/maiya-public/pen/MWgGzBE
And here is the raw code: (I think it is easier to understand on the codepen, but pasting here for good practice).
index.html
<div id="root">
</div>
style.css
.frame {
overflow:hidden;
transition: all 0.5s ease-out;
height:auto;
// for some reason,without the border, not all of them will transition AND it can't be white !??
border: solid purple 1px;
}
button {
margin: 0.25rem;
}
Dummy data: (the nested lists will render based on this object)
let data = {
text: 'list',
children: [
{
text: "groceries",
children: [
{
text: "sandwich",
children: [
{
text: "peanut butter",
children: [{text: 'peanuts', children: [{text: 'nut family'},{text: 'plant', children: [{text: 'earth'}]}] }]
},
{
text: "jelly",
children: [
{ text: "strawberries", children: null },
{ text: "sugar", children: null }
]
}
]
}
]
},
{
text: "flowers",
children: [
{
text: "long stems",
children: [
{
text: "daisies",
children: null
},
{
text: "roses",
children: [
{ text: "pink", children: null },
{ text: "red", children: null }
]
}
]
}
]
}
] };
React code: index.js
// component recursively renders nested lists. Every list item is a list.
const ListItem = ({item, depth}) => {
// depth prop allows me to give a className depending on how deeply it is nested, and do CSS style based on that
let { text, children } = item
let [showChildren, setShowChildren] = React.useState(false)
let frame = React.useRef()
const expandFrame = (frame) => {
//let frameHeight = frame.style.height //was using this at one point, but not anymore b/c not working
// was supposed to have frame.style.height = frameHeight + 'px'
frame.style.height = 'auto'
frame.addEventListener('transitionend', () =>{
frame.removeEventListener('transitionend', arguments.callee)
frame.style.height = null
})
}
const collapseFrame = (frame) => {
let frameHeight = frame.scrollHeight;
// temporarily disable all css transitions
let frameTransition = frame.style.transition;
frame.style.transition = ''
requestAnimationFrame(function() {
frame.style.height = frameHeight + 'px';
frame.style.transition = frameTransition;
requestAnimationFrame(function() {
frame.style.height = 0 + 'px';
})
})
}
return(
<ol>
<button onClick={(e)=>{
if(!showChildren) {
// console.log('children not showing/ expand')
setShowChildren(true)
expandFrame(frame.current)
} else {
// console.log('children showing/ collapse')
setShowChildren(false)
collapseFrame(frame.current)
}
}}
>
{text}-{depth}
{ children && <i className="fas fa-sort-down"></i> || children && <i className="fas fa-sort-up"></i>}
</button>
{/*THIS IS THE ELEMENT BEING ANIMATED:*/}
<div className={`frame depth-${depth}`} ref={frame}>
{showChildren && children && children.map(item => {
return (
<li key={uuid()}>
<ListItem item={item} depth={depth + 1}/>
</li>)
})
}
</div>
</ol>
)
}
class App extends React.Component {
render() {
return (
<div>
<ListItem key={uuid()} item={data} depth={0} />
</div>
);
}
}
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
}
render();
I think if you use an animation library it is much easier. I refactored your code to use react-spring. The code is much cleaner this way.
ListItem component:
const ListItem = ({ item, depth }) => {
// depth prop allows me to give a className depending on how deeply it is nested, and do CSS style based on that
let { text, children } = item;
let [showChildren, setShowChildren] = React.useState(false);
const transition = useTransition(
showChildren ? children : [],
item => item.text,
{
from: { opacity: 0, transform: 'scaleY(0)', maxHeight: '0px' },
enter: { opacity: 1, transform: 'scaleY(1)', maxHeight: '1000px' },
leave: { opacity: 0, transform: 'scaleY(0)', maxHeight: '0px' }
}
);
return (
<ol>
<button
onClick={e => {
item.children && setShowChildren(!showChildren);
}}
>
{text}-{depth}
{(children && <i className="fas fa-sort-down" />) ||
(children && <i className="fas fa-sort-up" />)}
</button>
<div className={`frame depth-${depth}`}>
{transition.map(({ item, key, props }) => (
<animated.li key={key} style={props}>
<ListItem item={item} depth={depth + 1} />
</animated.li>
))}
</div>
</ol>
);
};
A little explanation: You can specify enter and leave styles. Enter called when new nodes added to a list. Leave called when it removed. And all styles are animated. One problem remained, if I set the maxHeight value too high the height animation is too quick. If I set it too low then in could constraint the appareance of the list element with too much children. The 1000px is a compromise and it works with this example. It could be solved either with animating other style property or calculating a maxHeight value with a recursive function for each node.
Working example: https://codesandbox.io/s/animated-hierarchical-list-5hjhz