Tinder mit Knockout und CSS Animationen

Deklarative Bindings und CSS basierte Animationen passten bislang nicht so toll zusammen. In einer JavaONE Session habe ich vor einigen Jahren gezeigt, wie trotzdem ein Schuh draus wird. Als Beispiel habe ich damals dafür einen einfachen Candy Crush Klon mit knockout und DukeScript dafür gebaut um zu zeigen dass sich so sogar interaktive Spiele entwickeln lassen.

Damals musste ich dafür noch custom bindings und timeouts verwenden um die "Candies" aus dem Datenmodell zu entfernen, nachdem sie per CSS Animation explodiert waren.

Im Moment entwickle ich einen Tinder Klon für die Javaland Konferenz und hier habe ich ein ähnliches Problem. Die "Tindercards" fliegen nach einem Klick auf den like oder nope Button animiert aus dem Bild:

Die zugehörigen Animationen sind ganz einfach in CSS definiert:

.swipeLeft {
    animation: swipe-left 0.8s forwards;
    animation-timing-function:ease-in;
    -webkit-animation-timing-function:ease-in;
    -webkit-animation: swipe-left 0.8s forwards;
}

.swipeRight {
    animation: swipe-right 0.8s forwards;
    animation-timing-function:ease-in;
    -webkit-animation-timing-function:ease-in;
    -webkit-animation: swipe-right 0.8s forwards;
}

.swipeUp {
    animation: swipe-up 0.8s forwards;
    animation-timing-function:ease-in;
    -webkit-animation-timing-function:ease-in;
    -webkit-animation: swipe-up 0.8s forwards;
}

@keyframes swipe-left {
    to {
        transform: rotate(-13deg) translate3d(-100%, 0, 0);
        opacity: 0;
    }
}

@-webkit-keyframes swipe-left {
    to {       
        transform:  rotate(-13deg) translate3d(-100%, 0, 0) ;
        opacity: 0;
    }
}

@keyframes swipe-right {
    to {
        transform: rotate(13deg) translate3d(100%, 0, 0);
        opacity: 0;
    }
}

@-webkit-keyframes swipe-right {
    to {
        transform: rotate(13deg) translate3d(100%, 0, 0);
        opacity: 0;
    }
}

@keyframes swipe-up {
    to {
        transform: translate3d(0, -100%, 0);
        opacity: 0;
    }
}

@-webkit-keyframes swipe-up {
    to {
        transform: translate3d(0, -100%,  0);
        opacity: 0;
    }
}

Durch Klick auf die like- nope und superlike-Buttons wird die css-Klasse auf dem Element deklarataiv gesetzt, und die Anmimation wird ausgeführt:

<div class="trainer-card-wrapper" 
       data-bind="css: {swipeLeft: status() == 'DISMISSED',
                        swipeRight: status() == 'LIKED',
                        swipeUp: status() == 'SUPERLIKED'}">

Das Problem ist, dass die Karten, nachdem sie verschwunden sind, aus dem Datenmodell genommen werden müssen. macht man es vorher sind sie ohne Animation ganz einfach weg. Während ich mir zuvor mit einem speziellen Custom Binding beholfen habe, also mit (verstecktem) JavaScript, gibt es inzwischen eine rein deklarative Lösung. Dafür können wir in Knockout inzwischen das "event" binding verwenden. Ich habe es auf dem Elternelement meiner Cards gesetzt:

<div id="activeProfile" class="trainer-card-outer" 
    data-bind="event: {animationEnd: $root.newTop}">

    <div class="trainer-card-wrapper" 
       data-bind="css: {swipeLeft: status() == 'DISMISSED',
                        swipeRight: status() == 'LIKED',
                        swipeUp: status() == 'SUPERLIKED'}">
    <!-- mehr html... -->
    </div>

</div>

In der newTop Methode des Datenmodells wird dann das Element aus dem Datenmodell entfernt und eine neue oberste Karte bestimmt, also eine rein deklarative Lösung ohne JavaScript.

Mehr zu deklarativen UIs und DukeScript:

DukeScript Workshop: Moderne GUIs mit Java und HTML