'animateChild() for nested Angular child animation not working

I would like to fire two animations at the same time on click. The animation triggers use the same state, one is placed on an outer parent div and the other is nested within this div. The style changes are made, but the transition is applied only on the parent component. I've used animateChild within my parent animation with no luck. How can I apply an animation both a parent and child element?

animations.ts

import {
  trigger,
  state,
  style,
  transition,
  animate,
  query,
  group,
  animateChild,
} from "@angular/animations";

export const Animations = {
  animations: [
    trigger("expansionTrigger", [
      state(
        "true",
        style({
          height: "*"
        })
      ),
      state(
        "false",
        style({
          height: "0",
          display: "none"
        })
      ),
      transition("false <=> true", [
        group([
          query("@colExpansion", [animateChild()]),
          animate("3s ease")
        ])
      ])
    ]),
    trigger("colExpansion", [
      state(
        "true",
        style({
          "-webkit-box-flex": "0",
          flex: "0 0 66.66667%",
          "max-width": "66.66667%"
        })
      ),
      state(
        "false",
        style({
          "flex-basis": "0",
          "-webkit-box-flex": "1",
          "flex-grow": "1",
          "max-width": "100%"
        })
      ),
      transition("false <=> true", animate(3))
    ])
  ]
};

body.component.ts

import { Component, Input } from '@angular/core';
import { Animations } from '../animations';

@Component({
  selector: 'app-body',
  templateUrl: './body.component.html',
  styleUrls: ['./body.component.css'],
  animations: Animations.animations
})
export class BodyComponent  {

  @Input() isExpanded: string;

}

body.component.html

<div [@expansionTrigger]="isExpanded === 'true' ? 'true' : 'false'">
    <div [@colExpansion]="isExpanded === 'true' ? 'true' : 'false'">
    </div>
</div>


Solution 1:[1]

I made a working demo. I did my own styles, as I don't understad the purpose of this component. You can easily adapt it to your styles...

animations.ts

import {
    trigger,
    state,
    style,
    transition,
    animate,
    query,
    group,
    animateChild,
    keyframes,
} from "@angular/animations";
//
// Global Animate Time
const animateTime0 = "1s ease"
const animateTime1 = "3s"
const animateTime2 = "200ms ease-in-out"
//
// Global Animations
const fade0 = animate(animateTime2, keyframes([
    style({ opacity: 0 }),
    style({ opacity: 1 })
]))
//
// Styles
// Expansion Trigger
const expansionTriggerOnStyle = {
    height: "*",
    display: "*"
}
const expansionTriggerOffStyle = {
    height: "0",
    display: "none"
}
// Col.
const colExpansionOnStyle = {
    width: "66.66667%"
}
const colExpansionOffStyle = {
    width: "100%"
}
//
// Animations
// Expansion Trigger
function expansionTriggerAni(start, end) {
    return group([
        animate(animateTime0, keyframes([
            style(start),
            style(end)
        ])),
        query("@colExpansion", group([
            animateChild(),
            fade0
        ]))
    ])
}
//
// *Exported* Triggers
export const expansionTriggerAnimation = trigger("expansionTrigger", [
    //
    // States
    state("on", style(expansionTriggerOnStyle)),
    state("off", style(expansionTriggerOffStyle)),
    //
    // Transitions
    transition(
        "on => off",
        expansionTriggerAni(expansionTriggerOnStyle, expansionTriggerOffStyle)
    ),
    transition(
        "off => on",
        expansionTriggerAni(expansionTriggerOffStyle, expansionTriggerOnStyle)
    )
])
export const colExpansionAnimation = trigger("colExpansion", [
    //
    // States
    state("on", style(colExpansionOnStyle)),
    state("off", style(colExpansionOffStyle)),
    //
    // Transitions
    transition("on <=> off", animate(animateTime1))
])

body.component.ts

import { Component, OnInit } from '@angular/core';
import { expansionTriggerAnimation, colExpansionAnimation } from './animations';

@Component({
  selector: 'body',
  templateUrl: './body.component.html',
  styleUrls: ['./body.component.scss'],
  animations: [
    expansionTriggerAnimation,
    colExpansionAnimation
  ]
})
export class BodyComponent implements OnInit {
  isExpanded: boolean
  status: string = 'off'

  constructor() { }

  ngOnInit() {
  }

  toggle() {
    this.isExpanded = !this.isExpanded
    this.status = !this.isExpanded ? 'off' : 'on'

    console.log("Status:", this.status)
  }

}

body.component.scss

.wrapper {
    width: 200px;
    height: 50px;

    background-color: gray;
    cursor: pointer;

    overflow: hidden;

    .col {
        width: 100%;
        background-color: beige;
    }
}

body.component.html

<button (click)="toggle()">Toggle expansion</button>

<div class="wrapper" [@expansionTrigger]="status">
    <div class="col" [@colExpansion]="status">
        I am col
    </div>
</div>

When expanded

Solution 2:[2]

I really struggled with this for a long time. I finally managed to make it work and I can share the syntax I used.

In your setup above I would suggest switching this block:

transition("false <=> true", [
  group([
    query("@colExpansion", [animateChild()]),
    animate("3s ease")
  ])
])

to this:

transition("false <=> true", [
  group([
    query("@colExpansion", [
      animateChild(),
      animate("3s ease")
    ]),
    animate("3s ease")
  ])
])

i.e. include the animate("3s ease") block into the array that defines how the child should be animated as well as defining it in the block for the parent.

You can see that there are now two specifications - one for the parent and one for the child (which happen to have the same timing).

I can't vouch that that code will work as I haven't run it but I can share mine. You'll see that I don't actually use the @ selector to target the animation, I just target a class called name and I supply the animation directly but I can confirm that the following syntax does work:

const animationMetadata = [
  group([
    animate(1000, style({ opacity: 0.5 })),
    query('.name', [
      animateChild(),
      animate(1000, style({ backgroundColor: 'teal', color: 'white' }))
    ])
  ])
]

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Peter Nixey