Angular animation resets to original state


#1

I have an animation that occurs after a swipe on an item of an ngFor list of custom components. Each item starts with a state ‘novote’. After being swiped on, it should then be in a state either ‘upvote’ or ‘downvote’, depending on the direction swiped. The problem is that when you swipe on an object, the state changes and the animation occurs, but if you swipe on it for a second time, the state is again ‘novote,’ so the animation occurs again. If you swipe on it a third time, it is the correct state, so the animation does not occur.

My list html:

    <ion-list *ngFor="let song of songList | async" [class.selected]="song">
      <song-item (swipeleft)="vote(song, true)" (swiperight)="vote(song, false)" [song]="song" class="bottom-border"></song-item>
    </ion-list>

song-item.html:

<ion-item (swipeleft)="toggleVoteAnim(-1)" (swiperight)="toggleVoteAnim(1)" [@myvote]="voteState">
  <div>&nbsp;&nbsp;&nbsp;&nbsp; {{song.title}}<br/>
    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;- <small>{{song.artist}}</small></div>
  <div item-right>{{song.upVotes}}</div>
  <div item-right>UP</div>
  <div item-right>|</div>
  <div item-right>DOWN</div>
  <div item-right>{{song.downVotes}}</div>
</ion-item>

song-item.ts:

import { Component, Input, OnInit } from '@angular/core';
import { trigger, state, style, transition, animate, keyframes } from '@angular/animations';
import { Song } from "../../interfaces/song";

/**
 * Generated class for the SongItemComponent component.
 *
 * See https://angular.io/api/core/Component for more info on Angular
 * Components.
 */
@Component({
  selector: 'song-item',
  templateUrl: 'song-item.html',
  animations: [
    trigger('myvote', [
      state('novote', style({
        backgroundColor: '#191414'
      })),
      state('upvote', style({
        backgroundColor: '#191414'
      })),
      state('downvote', style({
        backgroundColor: '#191414'
      })),
      transition('* => upvote',
        animate('.25s', keyframes([
          style({backgroundColor: '#191414', offset: 0}),
          style({backgroundColor: '#1db954', offset: 0.25}),
          style({backgroundColor: '#191414', offset: 1})
        ]))
      ),
      transition('* => downvote',
        animate('.25s', keyframes([
          style({backgroundColor: '#191414', offset: 0}),
          style({backgroundColor: '#f53d3d', offset: 0.25}),
          style({backgroundColor: '#191414', offset: 1})
        ]))
      )
    ])
  ]
})


export class SongItemComponent implements OnInit {

  @Input() song: Song;

  voteState = 'novote';


  constructor() {}

  ngOnInit() {}

  /**
   * Toggles the vote animation
   * @param dir - direction of the swipe. -1 is left (upvote), 1 is right (downvote)
   */
  toggleVoteAnim(dir: number) {
    console.log('Current vote state: ' + this.voteState);
    if (dir === -1) {
      console.log('direction is: ' + dir);
      if (this.voteState === 'novote') {
        this.voteState = (this.voteState === 'novote') ? 'upvote' : 'novote';
        console.log(this.song.title + " was novote, now it is: " + this.voteState);
      }
      if (this.voteState === 'downvote') {
        this.voteState = (this.voteState === 'downvote') ? 'upvote' : 'downvote';
        console.log(this.song.title + " was downvote, now it is: " + this.voteState);
      }
    } else if (dir === 1) {
      console.log('direction is: ' + dir);
      if (this.voteState === 'novote') {
        this.voteState = (this.voteState === 'novote') ? 'downvote' : 'novote';
        console.log(this.song.title + " was novote, now it is: " + this.voteState);
      }
      if (this.voteState === 'upvote') {
        this.voteState = (this.voteState === 'upvote') ? 'downvote' : 'upvote';
        console.log(this.song.title + " was upvote, now it is: " + this.voteState);
      }
    }
    console.log(this.song.title + " final votestate: " + this.voteState);
  }
}

On the first left swipe, it prints:

Current vote state: novote
direction is: -1
song was novote, now it is: upvote
song final votestate: upvote

So far so good.

On the second left swipe:

Current vote state: novote
direction is: -1
song was novote, now it is: upvote
song final votestate: upvote

Not good. The voteState at the start should have been ‘upvote’ not ‘novote’, as per the final votestate from the previous swipe.

On the third left swipe:

Current vote state: upvote
direction is: -1
song final votestate: upvote

This is what should have happened with the second swipe. This is awfully confusing as it was working perfectly just a couple days ago, and I don’t think any changes were made to anything involved here…


#2

You are overloading your swipe actions. Only define them in the component, not in both. If the component hears swipe left, it emits “swipe left heard” and your page calls vote(song,true) when the page hears “swipe left heard.”


#3

Is it not possible to trigger an animation and call a function with one swipe? The same behavior occurs if I define both of them in the component

EDIT: I just tried only calling the vote function on the swipe, and then calling the animation through the vote function, as well as vice-versa, same thing.


#4

I didn’t understand your comment. The component listens for the swipe. When it hears the swipe left, it performs as many actions as it can inside the component, like the animation. Then it emits, “I’ve been swiped left” as an @Output to the page. When “I’ve been swiped left” hits the page, it does whatever it needs to do globally with that information.


#5

I’m confused. So how do I get it to both perform the animation properly and call the vote function?


#6

Well, I would put the vote function in the VoteManager provider, or similar, because the rule of thumb is for pages to display, and for providers to compute. But you don’t need to do that.

  1. user swipes left on song-item
  2. song-item notices this
    a. song-item performs animation
    b. song-item emits “leftSwipedEvent” with its unique ID
  3. page hears “leftSwipedEvent” with unique ID
    a. page votes that unique ID true/false depending what left swipe means
    b. any additional cleanup

#7

I would put the vote function in the VoteManager provider, or similar, because the rule of thumb is for pages to display, and for providers to compute.

The actual voting occurs with a provider, the vote function in the page just handles the logic of which function in the provider to call, as it’s different depending on whether the song has already been voted on or not.

So I tried to do this but it does not work:

In the page html:

    <ion-list *ngFor="let song of songList | async" [class.selected]="song">
      <song-item [song]="song" class="bottom-border" (change)="vote(song, $event)"></song-item>
    </ion-list>

In song-item.ts:

import { Component, Input, OnInit, Output, EventEmitter } from '@angular/core';
import { trigger, state, style, transition, animate, keyframes } from '@angular/animations';
import { Song } from "../../interfaces/song";

/**
 * Generated class for the SongItemComponent component.
 *
 * See https://angular.io/api/core/Component for more info on Angular
 * Components.
 */
@Component({
  selector: 'song-item',
  templateUrl: 'song-item.html',
  animations: [
    trigger('myvote', [
      state('novote', style({
        backgroundColor: '#191414'
      })),
      state('upvote', style({
        backgroundColor: '#191414'
      })),
      state('downvote', style({
        backgroundColor: '#191414'
      })),
      transition('* => upvote',
        animate('.25s', keyframes([
          style({backgroundColor: '#191414', offset: 0}),
          style({backgroundColor: '#1db954', offset: 0.25}),
          style({backgroundColor: '#191414', offset: 1})
        ]))
      ),
      transition('* => downvote',
        animate('.25s', keyframes([
          style({backgroundColor: '#191414', offset: 0}),
          style({backgroundColor: '#f53d3d', offset: 0.25}),
          style({backgroundColor: '#191414', offset: 1})
        ]))
      )
    ])
  ]
})


export class SongItemComponent implements OnInit {

  @Output() change: EventEmitter<Boolean> = new EventEmitter<Boolean>();
  @Input() song: Song;
  room: any;
  roomId: any;
  voteState = 'novote';
  songList: any;


  constructor() {
  }

  ngOnInit() {
  }

  /**
   * Toggles the voting animation.
   * @param {Boolean} isUpVote - true if an upvote, false if a downvote
   */
  toggleVoteAnim(isUpVote: Boolean) {
    console.log('Current vote state: ' + this.voteState);
    this.change.emit(isUpVote);
    if (isUpVote) {
      console.log('direction is: ' + isUpVote);
      if (this.voteState === 'novote') {
        this.voteState = (this.voteState === 'novote') ? 'upvote' : 'novote';
        console.log(this.song.title + " was novote, now it is: " + this.voteState);
      }
      if (this.voteState === 'downvote') {
        this.voteState = (this.voteState === 'downvote') ? 'upvote' : 'downvote';
        console.log(this.song.title + " was downvote, now it is: " + this.voteState);
      }
      if (this.voteState === 'upvote') {
        this.voteState = (this.voteState === 'upvote') ? 'upvote' : 'upvote';
        console.log(this.song.title + " was upvote, now it is: " + this.voteState);
      }
    } else if (!isUpVote) {
      console.log('direction is: ' + isUpVote);
      if (this.voteState === 'novote') {
        this.voteState = (this.voteState === 'novote') ? 'downvote' : 'novote';
        console.log(this.song.title + " was novote, now it is: " + this.voteState);
      }
      if (this.voteState === 'upvote') {
        this.voteState = (this.voteState === 'upvote') ? 'downvote' : 'upvote';
        console.log(this.song.title + " was upvote, now it is: " + this.voteState);
      }
      if (this.voteState === 'downvote') {
        this.voteState = (this.voteState === 'downvote') ? 'downvote' : 'downvote';
        console.log(this.song.title + " was downvote, now it is: " + this.voteState);
      }
    }
    console.log(this.song.title + " final votestate: " + this.voteState);
  }
}

Song-item.html is the same, though I switched the direction to the isUpVote boolean.

Voting works but voteState still goes back to ‘novote’ after the first swipe, and now it occurs twice I think? Either way the animation only happens on the second swipe.

First swipe:

Current vote state: novote
direction is: true
df was novote, now it is: upvote
df was upvote, now it is: upvote
df final votestate: upvote

Second swipe:

Current vote state: novote
direction is: true
df was novote, now it is: upvote
df was upvote, now it is: upvote
df final votestate: upvote

I tried to not call vote at all, and then the animation works perfectly.


#8

These might not be the same thing. Is voteState correct and the animation lags? That’s a change detection issue. Do voteState and animation always agree? That’s a programming logic issue.


#9

I’m not sure what you mean. voteState starts at ‘novote’. After one left swipe, a vote is counted but no animation occurs, but the console logs print that voteState changed from ‘novote’ to ‘upvote’. After a second left swipe, no vote is counted (as it should), but the animation occurs, with console logs showing that voteState was again ‘novote’ and changed to ‘upvote’. On the third left swipe, no vote is counted, and no animation occurs, and console logs show voteState as ‘upvote’.

Seems that whenever a vote is counted, the animation does not occur and the voteState stays ‘novote’ despite what the print outs say.


#10

Actually, the animation apparently does happen on the first swipe sometimes, but it does lag and only appears for a moment halfway through the animation.

EDIT: I believe I fixed it. Because every time a vote occurred, the list had to be updated, so ngFor would recreate the entire list, which I think caused the song-items to reinitialize, making the voteState ‘novote’ again. I added a trackBy function to the list, and now it appears to work.