Компонент делает из плоского списка на входе, анимированную 3d гармошку. Декларативный JSX компонент, в котором также потребовалось динамически поддерживать обновление DOM, но делается это аккуратно.
Чтобы обойтись минимумом скриптов и использовать лишь CSS3 для расщёта самой анимации, каждая следующая секция списка рекурсивно вкладывается в предыдущую (блок с reduceRight). Таким образом то, что за “сгибом”, уже поворачивается всё вместе не распадаясь на части.
import { element } from 'deku'
import toArray from 'to-array'
import c from 'classnames'
let log = debug('app:Origami')
let data = {}
const TRANSITION_FRAME_DELAY_MS = 1
export default {
render({ children, props, path }) {
const { scheme, folded } = props
const origami = children.reduceRight( (acc, it, index) => {
return (
<div class={ c(['origami-fold', { 'odd' : ((index+1) % 2) != 0,
'even' : ((index+1) % 2) == 0,
'first' : index == 0 } ] ) } >
{ it }
{ acc }
</div>)
}, [])
return (
<div id={ path }
class={ c([ 'origami',
(props.type || ''),
(props.class || ''), {
'folded' : folded,
'unfolded' : !folded
}]) }>
{ origami }
</div>
)
},
При создании компонента, запускаем цикл обновления реальных границ элемента, с целью чтобы они постоянно соответствовали видимым. При удалении компонента - останавливаем этот цикл:
onCreate({ path }) {
data[path] = {}
requestAnimationFrame( () => {
const element = document.getElementById( path ),
stopFix = fixHeightCycle(element)
data[path].stopFix = stopFix
})
},
onUpdate({ props : { folded } , path }) {
requestAnimationFrame( () => {} )
},
onRemove({ path }) {
data[path] && data[path].stopFix()
}
}
Функция расчёта реальной высоты:
function foldsHeight(element) {
const folds = Array.from(element.querySelectorAll('.origami-fold')),
height = folds.reduce((
(acc, it) => {
const firstChild = it.children[0]
if(firstChild) {
return acc + firstChild.getBoundingClientRect().height
} else {
return acc
}
} ), 0)
return height
}
Цикл обновления реального размера.
requestAnimationFrame - способ делать это, дожидаясь отрисовки нового кадра.
function fixHeightCycle (element) {
let requestId = null, state = { play : true }
if(element) {
requestAnimationFrame( frameHandler )
}
function frameHandler() {
const firstFold = element.querySelector('.origami-fold'),
rect = firstFold.getBoundingClientRect()
element.style.maxHeight = foldsHeight(element) + 'px' //rect.height + 'px'
if( state.play ) {
setTimeout( () => {
requestAnimationFrame( frameHandler )
}, TRANSITION_FRAME_DELAY_MS)
} else { return }
}
return function stop() { state.play = false }
}
Благодаря вложенному списку исходных элементов, получился лаконичным:
.origami
position relative
perspective 1000px
-moz-perspective 1000px
perspective-origin 50% 0%
overflow hidden
max-height 2000px
.origami-fold
transform-style preserve-3d
transform-origin top
box-sizing border-box
transition-duration .25s
transition-delay 0s
transition-timing-function ease-in-out
&.folded
max-height 0px
.origami-fold
transition-delay 0s
&.first
transform rotateX(-90deg) !important
&.odd
transform rotateX(-180deg)
&.even
transform rotateX(180deg)
&.unfolded
.origami-fold
&.first
transform rotateX(0) !important
&.odd
transform rotateX(0)
&.even
transform rotateX(0)