angular: Angular 2 RC 4 New Animations not supported in Directives

Hi,

It seems that in current release we can add animations to components only and cannot define them as directives. I mean:

the below works

@Component({
    selector: "animate-demo",
    animations: [
        trigger('openClose', [
            state('closed, void',
                style({ height: "0px" })),
            state('open',
                style({ height: "*" })),
            transition("* => *", animate(200))
        ])
    ]
})

Whereas the below doesn’t work:

@Directive({
    selector: "animate-demo",
    animations: [
        trigger('openClose', [
            state('closed, void',
                style({ height: "0px" })),
            state('open',
                style({ height: "*" })),
            transition("* => *", animate(200))
        ])
    ]
})

And gives compilation error in visual studio:

"Argument of type ‘{ selector: string; animations: AnimationEntryMetadata[]; template: string; }’ is not assignable to parameter of type ‘{ selector?: string; inputs?: string[]; outputs?: string[]; properties?: string[]; events?: strin…’.
"

Usually we might want to have the commonly used animations such as slideDown, sliddeUp, fadeIn, fadeOut defined at one place as a directive and then in components where we want to use them we would just want to import the directive and use the animation instead of redefining animation in each component.

About this issue

  • Original URL
  • State: open
  • Created 8 years ago
  • Reactions: 71
  • Comments: 33 (12 by maintainers)

Most upvoted comments

Even though directives are not attached to a view, they should still be able to place animations on the host element that they are attached to.

Say for example you wanted to create a directive to manipulate animate.css animations:

@Directive({
   styleUrls: ['animate.css'],
   selector: '[animateCss]',
   host: {
      '[@currentAnimation]': 'animationState'
   },
   animations: [
      trigger('currentAnimation', [
         transition('* => rotate3d', animate(1000, keyframes('rotate3d'))),
         transition('* => hinge', animate(1000, keyframes('hinge'))),
         transition('* => flipX', animate(1000, keyframes('flipX')))
      ])
   ]
})
class AnimatorMan {
   @Input("animateCss")
   public animationState;
}

(The use of using CSS classes/keyframes within animate will be possible once the CSS parser is integrated. The example above is just to show how useful it can be to make a directive that manages animations.)

As a workaround you can create animations.ts file like:

export class Animations: [
    public static heroState = trigger('heroState', [
      state('inactive', style({
        backgroundColor: '#eee',
        transform: 'scale(1)'
      })),
      state('active',   style({
        backgroundColor: '#cfd8dc',
        transform: 'scale(1.1)'
      })),
      transition('inactive => active', animate('100ms ease-in')),
      transition('active => inactive', animate('100ms ease-out'))
    ])
  ]
}

And then use it in components like:

import { Animations }                          from './animations';

@Component({
  selector: 'myapp-hero',
  styleUrls: ['./hero.component.css'],
  templateUrl: './heor.component.html',
  animations: [Animations.heroState]
})

@matsko Been a few months since your last update. Any news?

@matsko can you please share if there is any progress on this?

Thank you so much Eric fro your reply. Then whats the best way if we want to make reusable animations? Apparently it seems that if n of my components requires slide up/down animation, I will have to write the same animation n times i.e. in each component, correct?

I believe the ability to define animations as structural directive would embrace re-usability of animations, what do you think?

Yes there is a plan in the works. I will update this thread once I get a go-ahead on this.

@matsko Hasn’t been an update in awhile. Was wondering if this was still in the works?

Ok, I have managed to do animation using a directive. Here is an example:


import { Directive, Input, Renderer2 } from '@angular/core';
import { trigger, state, style, transition, animate } from '@angular/animations';
import { ɵAnimationEngine as AnimationEngine } from '@angular/animations/browser';

@Directive({ 
    selector: '[collapse]',
    host: {
      '[@animation]': 'collapse'
    }
})
export class Collapse {
    @Input() collapse: boolean;

    constructor( private animation: AnimationEngine,  private renderer: Renderer2) {
        
        const namespaceId = (<any>renderer).delegate._namespaceId; /* this is a hack and 
        the property '_namespaceId' is only available on AnimationRenderer and 
        not DefaultDomRenderer2 */

        const name = 'animation';
        
        this.animation.registerTrigger(trigger(`${namespaceId}#${name}`, [
            state('0', style({ height: '0', opacity: '0' })),
            state('1', style({ height: '*', opacity: '1' })),
            transition('0 => 1', animate('200ms ease-in')),
            transition('1 => 0', animate('200ms ease-out'))
        ]));
    }
}

and then use it like this :

import { Component } from '@angular/core';

@Component({
    selector: 'my-component',
    template: '<div [collapse]="isCollabsed"> Some content </div>',

    animations: [trigger('dummy', [])]  /* This is needed to force angular 
    to pass AnimationRenderer to our Collapse directive constructor 
    instead of passing the default Angular DefaultDomRenderer2 */

})
export class MyComponent {
    public isCollabsed: boolean = false; // set to true to trigger animation 
    constructor() {}
}

I have a suggestion for the angular team regarding animation. I wonder if it is possible to introduce a new way to manage animation similar to the way angular is currently managing Components and Directives; I mean through dependency injection.

Something like:

import { Animation, OnAnimate, AnimationMetadata } from '@angular/animation';

@Animation({
    selector: 'collapse'
})
export class CollapseAnimation implements OnAnimate {
    constructor() {}
    onAnimate() : AnimationMetadata {
        return [
            state('0', style({
                height: '0',
                opacity: '0'
            })),
            state('1', style({
                height: '*',
                opacity: '1'
            })),
            transition('0 => 1', animate('200ms ease-in')),
            transition('1 => 0', animate('200ms ease-out'))
        ];
    }
}

then used in templates like:

import { Component } from '@angular/core';

@Component({
    selector: 'my-component',
    template: '<div [@collapse]="isCollabsed"> Some content </div>',
    animations: [CollapseAnimation]
})
export class MyComponent {
    public isCollabsed: boolean = false; // set to true to trigger animation 
    constructor() {}
}

I did something around that @molcik mentioned above.

Gist here: https://gist.github.com/McQuinTrix/3bd041a4ffefc53633d38d927c782db7

Edit: This is just basic implementation, people can figure out how to expand on this

Thank you so much @matsko for your reply, his looks really exciting. Can you please also create a Plunker for this?

hi is there any news on this feature? it is planned for a future release?

@fayezmm I had to eventually ditch the directive approach, it cannot bind to the animation on enableProdMode() like what it does in the angular-cli.

However, animating with a reusable component is not difficult to put together, even if it is illogical from a syntax, structure, and philosophy point of view. Here’s what I did to get my reusable collapsing working, even in prod mode!

import { Component, Input, HostBinding } from '@angular/core'
import { trigger, state, style, transition, animate } from '@angular/animations'

@Component({
  selector: '[collapse]',
  template: '<ng-content></ng-content>',
  animations: [trigger('collapseAni', [
    state('0', style({ height: '0', opacity: '0' })),
    state('1', style({ height: '*', opacity: '1' })),
    transition('0 => 1', animate('400ms ease-in')),
    transition('1 => 0', animate('400ms ease-out'))
  ])],
})
export class CollapseComponent {
  @HostBinding('@collapseAni')
  @Input()
  collapse: boolean

  constructor() { }
}

@matsko - Trying this on 2.0.1 and getting an error.

import {
  Component,
  Directive, Input, Output, EventEmitter,
  HostListener, HostBinding, trigger,
  transition, animate, style, state
} from '@angular/core';

@Directive({
  selector: 'swui-overlay',
  host: {
    class: 'swui-overlay'
  },
  styles: [`
    .swui-overlay {
      position: fixed;
      top: 0;
      left: 0;
      bottom: 0;
      right: 0;
      width: 100%;
      overflow: hidden;
      height: 100%;
      background-color: #000;
      opacity: 0;
      visibility: hidden;
    }
  `],
  animations: [
    trigger('overlayTransition', [
      state('active', style({
        opacity: 0.8,
        visibility: 'visible'
      })),
      state('inactive', style({
        visibility: 'hidden',
        opacity: 0
      })),
      transition('* => active', [
        animate('100ms ease-in')
      ]),
      transition('* => inactive', [
        animate('100ms ease-out')
      ])
    ])
  ]
})
export class OverlayDirective {

  @HostBinding('@overlayTransition')
  get animationState() {
    return this.visible ? 'active' : 'inactive';
  }

  @Input() visible: boolean = false;

  @HostBinding('style.zIndex')
  @Input() zIndex: number = 990;

  @Output() onClick = new EventEmitter();

  @HostListener('click')
  backdropClick() {
    this.onClick.emit(true);
  }

}

which gives me:

bootstrap.ts?b333:9 TypeError: Cannot read property 'prop' of undefined
    at eval (eval at ./node_modules/@angular/compiler/src/view_compiler/property_binder.js (http://localhost:9999/vendor.js:967:1), <anonymous>:127:63)
    at Array.forEach (native)
    at bindAndWriteToRenderer (eval at ./node_modules/@angular/compiler/src/view_compiler/property_binder.js (http://localhost:9999/vendor.js:967:1), <anonymous>:81:16)
    at bindDirectiveHostProps (eval at ./node_modules/@angular/compiler/src/view_compiler/property_binder.js (http://localhost:9999/vendor.js:967:1), <anonymous>:181:5)
    at eval (eval at ./node_modules/@angular/compiler/src/view_compiler/view_binder.js (http://localhost:9999/vendor.js:983:1), <anonymous>:61:116)
    at Array.forEach (native)
    at ViewBinderVisitor.visitElement (eval at ./node_modules/@angular/compiler/src/view_compiler/view_binder.js (http://localhost:9999/vendor.js:983:1), <anonymous>:57:24)
    at ElementAst.visit (eval at ./node_modules/@angular/compiler/src/template_parser/template_ast.js (http://localhost:9999/vendor.js:847:1), <anonymous>:154:24)
    at eval (eval at ./node_modules/@angular/compiler/src/template_parser/template_ast.js (http://localhost:9999/vendor.js:847:1), <anonymous>:284:29)
    at Array.forEach (native)

of coursing switching this to a Component and it works fine.

Would this happen soon? I have been writing lots of components with similar animation applied. Directive is so desirable to make the code dry.