Ember Animated - animated-if

Hello All,

I’m using the Ember Animated add-on and I’m having some issues getting animated-if to do what I want (ref: https://ember-animation.github.io/ember-animated/docs/api/components/animated-if).

First of all I’ll explain what I’m trying to do.

I am updating the location of a div element based on a piece of event logic (but not a simple single user initiated UI fired interaction like a button press). Without the animation the element instantly appears in the destination location so the logic works fine. I am attempting to use animated-if to perform a smooth animation from the original location to the destination location when the location of the div changes.

I wrote a simple example where a div is randomly moved around the screen based on an attached timer. I have used animated-if and associated a custom “transition”, but from my console logging I can see that the transition never runs. I also can’t seem to retrigger the animated-if when the predicate changes.

Here is the code…

template.hbs

<div class="w-screen h-screen">
    {{#animated-if this.onAnimateMove use=this.transition}}
        <div
            class="bg-red-600 text-white text-xl font-bold w-32 h-32 mx-2 my-2 shadow rounded flex items-center justify-center"
            {{did-insert this.attachRandomMoveBehaviour}}
        >
            1
        </div>
    {{/animated-if}}
</div>

controller.js

import Controller from '@ember/controller';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { fadeIn, fadeOut } from 'ember-animated/motions/opacity';
import { move } from 'ember-animated/motions/move';
import { easeOut, easeIn } from 'ember-animated/easings/cosine';

export default class AnimationSmoothmoveController extends Controller {
    @tracked onAnimateMove = true;

    @action
    attachRandomMoveBehaviour(element) {
        const id = setInterval(() => {
            const top = Math.round(Math.random() * 500);
            const left = Math.round(Math.random() * 500);
        
            element.style.transform = `translate(${left}px, ${top}px)`;

            this.onAnimateMove = true;
        }, 2000);
    
        return () => clearInterval(id);
    };

    // custom transition
    *transition({ insertedSprites, keptSprites, removedSprites }) {
        console.log("transition start");       

        insertedSprites.forEach(sprite => {
            fadeIn(sprite);
        });

        removedSprites.forEach(sprite => {
            fadeOut(sprite);
        });

        keptSprites.forEach(sprite => {
            move(sprite, { easing: easeOut });
        });

        console.log("transition end");

        this.onAnimateMove = false;
    };   
}

I’ve tried a number of different variations but I haven’t been able to get this working. Appreciate any assistance.

Just spitballing a few things here but first of all have you tried wrapping the animated-if in an animated container? That’s often necessary for making sure the animations work properly. For example the simple movement examples in the docs wrap the animated-if in an animated-container.

I’d think that this example is the closest to your use case? In that case both origin and target locations have animated containers and animated if. You may also want to check out the beacon example, though I’m not sure if that’s relevant or not (i’m not clear on what your original location and target location are per se).

Does it work if you remove this line?

Thank you for your suggestions Dan. I had previously tried using AnimatedContainer but my understanding is that you use AnimatedContainer to effectively resize the contents “around” the container in the event that the transition changes the shape of the container (I hope that makes sense). I also studied the use of animated-if by @ijlee2 in one of his published projects where he doesn’t use AnimatedContainer.

I did try just now wrapping my Div in AnimatedContainer but it didn’t make any difference.

I also had a look at the examples you referenced. The first one is very similar to what I’m trying to do, but all of the working examples I’ve seen trigger an animation by a UI event / action (such as a button press) - including that one!

The questions that I still have related to the use of animated-if:

  1. On initial render the animated-if does not run the transition. I don’t see any console output from my log messages, and I have setup a similar example with animated-each where the transition code is run, but animated-each is for operating on a collection of elements.

  2. All of the animated-if examples use a UI event to trigger an action which fires the the transition. My understanding is that a tracked attribute would fire an “action” when the value changes but maybe I’m wrong.

Hi Murko,

I tried your suggestion, but it didn’t make any difference. I feel that the way I’m toggling that field isn’t necessarily right, but the issue I have here is that the transition function is not being run at all, which means that line is never run anyway!

Thanks for your suggestion though.

Regards,

Dave

I think the main problem, at least in the example code that you posted above, is that you aren’t changing this.onAnimateMove. The animated-if is only going to invoke the transition if that value changes. It’s also problematic to try to mutate state within a transition as IIRC the transition generator methods don’t get bound to the component context (and for good reason, you wouldn’t want the transition function, which runs a lot to be mutating your component state). The same applies to animated-each, it responds to changes in the observed property. You can think of them exactly like regular {{#if}} or {{#each}}.

If you just want to animate something moving around the screen based on translation changes you don’t need ember-animated at all, you can just use CSS transitions. You need ember-animated when you want your ember app state to be driving animations (e.g. show/hide or things being added to/removed from a list, etc). So when you say “updating the location” if it’s like a show/hide binary thing an animated-if makes sense, if it’s something rendered and you’re literally just translating it i think a CSS transition makes the most sense, if it’s moving from one section of the page to another i’d think you’d want two animated-if or animated-each blocks and move the object/component/sprite from one to the other.

Hi, @djodonnell. Thanks for trying out the Ember Animated tutorial; I’m glad to see that it may have inspired you to do more afterwards.

I haven’t worked with ember-animated recently and will need to brush up on its API and package updates. I’m currently on vacation; if I have time next week to try your code and find what was happening, I’ll let you know.

Best,

Think about what state you are actually trying to animate. It’s not a boolean that goes between true and false (onAnimateMove). It’s the coordinates of the div (const top and const left).

Here’s one way to model that:

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import move from 'ember-animated/motions/move';

export default class extends Component {
  @tracked
  boxes = [{ x: 200, y: 200 }];

  @action
  move() {
    this.boxes = [
      {
        x: Math.floor(Math.random() * 400),
        y: Math.floor(Math.random() * 400),
      },
    ];
  }

  *transition({ keptSprites }) {
    keptSprites.forEach(move);
  }
}

<style>
  .demobox {
    width: 10px;
    height: 10px;
    background-color: red;
    position: absolute;
  }
</style>

<button {{on "click" this.move}}>Move</button>

{{#animated-each this.boxes key="@index" watch="x,y" use=this.transition as |box|}}
  <div class="demobox" style="left: {{box.x}}px; top: {{box.y}}px">
  </div>
{{/animated-each}}

Explanation:

  • we’re using key="@index" here so that each box’s position in the this.boxes array determines its identity. For example, the old first box in the list will animate to the new first box in the list, etc. This isn’t strictly an ember-animated feature, it’s a feature of ember’s built-in #each as well, but ember-animated respects it to maintain identity across animations too.
  • we need watch="x,y" because by default the animated-each would only animate if the set of boxes themselves change, whereas we also want to animate if some deeper properties (the x and y on each box) change.

(If you always only have a single box, you can use animated-value instead of animated-each, and it will work pretty much the same. But even in that case, you need to think about object identity in the same way, probably using key and watch. I tend to reach for animated-each when teaching because it’s the fundamental building block. The others, including animated-if, are just shorthand around animated-each.)

Thank you Edward for your comprehensive explanation. It took some time, and some extermination for this to really sink in and I didn’t want to post a reply until I’d figured it out.

Of the solution variants proposed I ended up settling on animated-each, even though in this case I only my true use case only had a single box to transition. I did manage to achieve the same result with animated-value, but the problem I was having with animated-value was that although visually it was doing the right thing, in the process is was creating a new instance of the object which was not ideal and caused a whole bunch of side-effects. Interestingly using “key” for animated-value caused the page to become non-responsive (literally having to close the browser) although I’m not sure why it doesn’t like this since it works fine with animated-each.

Animated-each works perfectly for this task as long as I have the “slightly hacky” list (array) with a single element.