diff --git a/aio/content/examples/animations/e2e/src/app.e2e-spec.ts b/aio/content/examples/animations/e2e/src/app.e2e-spec.ts index 09475031869..8a81fd13934 100644 --- a/aio/content/examples/animations/e2e/src/app.e2e-spec.ts +++ b/aio/content/examples/animations/e2e/src/app.e2e-spec.ts @@ -8,6 +8,7 @@ import * as auto from './auto.po'; import * as filterStagger from './filter-stagger.po'; import * as heroGroups from './hero-groups'; import { getLinkById, sleepFor } from './util'; +import { getComponentSection, getToggleButton } from './querying.po'; describe('Animation Tests', () => { const openCloseHref = getLinkById('open-close'); @@ -17,6 +18,7 @@ describe('Animation Tests', () => { const autoHref = getLinkById('auto'); const filterHref = getLinkById('heroes'); const heroGroupsHref = getLinkById('hero-groups'); + const queryingHref = getLinkById('querying'); beforeAll(() => browser.get('')); @@ -218,7 +220,7 @@ describe('Animation Tests', () => { describe('Hero Groups Component', () => { beforeAll(async () => { await heroGroupsHref.click(); - await sleepFor(300); + await sleepFor(400); }); it('should attach a flyInOut trigger to the list of items', async () => { @@ -242,4 +244,49 @@ describe('Animation Tests', () => { await browser.wait(async () => await heroesList.count() < total, 2000); }); }); + + describe('Querying Component', () => { + const queryingAnimationDuration = 2500; + + beforeAll(async () => { + await queryingHref.click(); + await sleepFor(queryingAnimationDuration); + }); + + it('should toggle the section', async () => { + const toggleButton = getToggleButton(); + const section = getComponentSection(); + + expect(await section.isPresent()).toBe(true); + + // toggling off + await toggleButton.click(); + await sleepFor(queryingAnimationDuration); + expect(await section.isPresent()).toBe(false); + + // toggling on + await toggleButton.click(); + await sleepFor(queryingAnimationDuration); + expect(await section.isPresent()).toBe(true); + await sleepFor(queryingAnimationDuration); + }); + + it(`should disable the button for the animation's duration`, async () => { + const toggleButton = getToggleButton(); + expect(await toggleButton.isEnabled()).toBe(true); + + // toggling off + await toggleButton.click(); + expect(await toggleButton.isEnabled()).toBe(false); + await sleepFor(queryingAnimationDuration); + expect(await toggleButton.isEnabled()).toBe(true); + + // toggling on + await toggleButton.click(); + expect(await toggleButton.isEnabled()).toBe(false); + await sleepFor(queryingAnimationDuration); + expect(await toggleButton.isEnabled()).toBe(true); + }); + + }); }); diff --git a/aio/content/examples/animations/e2e/src/querying.po.ts b/aio/content/examples/animations/e2e/src/querying.po.ts new file mode 100644 index 00000000000..aa7ad828ba3 --- /dev/null +++ b/aio/content/examples/animations/e2e/src/querying.po.ts @@ -0,0 +1,16 @@ +import { by } from 'protractor'; +import { locate } from './util'; + +export function getComponent() { + return by.css('app-querying'); +} + +export function getToggleButton() { + const toggleButton = () => by.className('toggle'); + return locate(getComponent(), toggleButton()); +} + +export function getComponentSection() { + const findSection = () => by.css('section'); + return locate(getComponent(), findSection()); +} diff --git a/aio/content/examples/animations/src/app/animations.ts b/aio/content/examples/animations/src/app/animations.ts index 87c355251e4..3a379e8d006 100644 --- a/aio/content/examples/animations/src/app/animations.ts +++ b/aio/content/examples/animations/src/app/animations.ts @@ -39,11 +39,11 @@ export const slideInAnimation = ]), query(':enter', [ animate('300ms ease-out', style({ left: '0%' })) - ]) + ]), + query('@*', animateChild()) ]), - query(':enter', animateChild()), ]), - transition('* <=> FilterPage', [ + transition('* <=> *', [ style({ position: 'relative' }), query(':enter, :leave', [ style({ @@ -59,13 +59,13 @@ export const slideInAnimation = query(':leave', animateChild()), group([ query(':leave', [ - animate('200ms ease-out', style({ left: '100%' })) + animate('200ms ease-out', style({ left: '100%', opacity: 0 })) ]), query(':enter', [ animate('300ms ease-out', style({ left: '0%' })) - ]) + ]), + query('@*', animateChild()) ]), - query(':enter', animateChild()), ]) // #enddocregion query ]); diff --git a/aio/content/examples/animations/src/app/app.component.css b/aio/content/examples/animations/src/app/app.component.css index ed21b8546c1..de51b7b5db4 100644 --- a/aio/content/examples/animations/src/app/app.component.css +++ b/aio/content/examples/animations/src/app/app.component.css @@ -1,3 +1,15 @@ nav a { padding: .7rem; } + +h1 { + margin-bottom: .3rem; +} + +form { + margin-bottom: 2rem; +} + +nav { + padding-bottom: 3rem; +} diff --git a/aio/content/examples/animations/src/app/app.component.html b/aio/content/examples/animations/src/app/app.component.html index 762020973b7..50195d3f563 100644 --- a/aio/content/examples/animations/src/app/app.component.html +++ b/aio/content/examples/animations/src/app/app.component.html @@ -17,7 +17,7 @@ Filter/Stagger Hero Groups Insert/Remove - + Querying diff --git a/aio/content/examples/animations/src/app/app.module.ts b/aio/content/examples/animations/src/app/app.module.ts index d48133f79f1..8be60aa8f9d 100644 --- a/aio/content/examples/animations/src/app/app.module.ts +++ b/aio/content/examples/animations/src/app/app.module.ts @@ -20,6 +20,7 @@ import { HeroListAutoComponent } from './hero-list-auto.component'; import { HomeComponent } from './home.component'; import { AboutComponent } from './about.component'; import { InsertRemoveComponent } from './insert-remove.component'; +import { QueryingComponent } from './querying.component'; @NgModule({ @@ -28,17 +29,61 @@ import { InsertRemoveComponent } from './insert-remove.component'; BrowserAnimationsModule, RouterModule.forRoot([ { path: '', pathMatch: 'full', redirectTo: '/enter-leave' }, - { path: 'open-close', component: OpenClosePageComponent }, - { path: 'status', component: StatusSliderPageComponent }, - { path: 'toggle', component: ToggleAnimationsPageComponent }, - { path: 'heroes', component: HeroListPageComponent, - data: { animation: 'FilterPage' } }, - { path: 'hero-groups', component: HeroListGroupPageComponent }, - { path: 'enter-leave', component: HeroListEnterLeavePageComponent }, - { path: 'auto', component: HeroListAutoCalcPageComponent }, - { path: 'insert-remove', component: InsertRemoveComponent}, - { path: 'home', component: HomeComponent, data: { animation: 'HomePage' } }, - { path: 'about', component: AboutComponent, data: { animation: 'AboutPage' } }, + { + path: 'open-close', + component: OpenClosePageComponent, + data: { animation: 'openClosePage' } + }, + { + path: 'status', + component: StatusSliderPageComponent, + data: { animation: 'statusPage' } + }, + { + path: 'toggle', + component: ToggleAnimationsPageComponent, + data: { animation: 'togglePage' } + }, + { + path: 'heroes', + component: HeroListPageComponent, + data: { animation: 'filterPage' } + }, + { + path: 'hero-groups', + component: HeroListGroupPageComponent, + data: { animation: 'heroGroupPage' } + }, + { + path: 'enter-leave', + component: HeroListEnterLeavePageComponent, + data: { animation: 'enterLeavePage' } + }, + { + path: 'auto', + component: HeroListAutoCalcPageComponent, + data: { animation: 'autoPage' } + }, + { + path: 'insert-remove', + component: InsertRemoveComponent, + data: { animation: 'insertRemovePage' } + }, + { + path: 'querying', + component: QueryingComponent, + data: { animation: 'queryingPage' } + }, + { + path: 'home', + component: HomeComponent, + data: { animation: 'HomePage' } + }, + { + path: 'about', + component: AboutComponent, + data: { animation: 'AboutPage' } + }, ]) ], // #enddocregion route-animation-data @@ -59,6 +104,7 @@ import { InsertRemoveComponent } from './insert-remove.component'; HeroListAutoComponent, HomeComponent, InsertRemoveComponent, + QueryingComponent, AboutComponent ], bootstrap: [AppComponent] diff --git a/aio/content/examples/animations/src/app/querying.component.css b/aio/content/examples/animations/src/app/querying.component.css new file mode 100644 index 00000000000..3ee3c939363 --- /dev/null +++ b/aio/content/examples/animations/src/app/querying.component.css @@ -0,0 +1,31 @@ +section { + border: 1px solid black; + overflow: hidden; +} + +section > * { + margin: 1rem; +} + +.hero { + display: flex; + align-items: center; + border-radius: 4px; + color: black; + background-color: #DDD; +} + +.hero .badge { + display: inline-block; + font-size: small; + color: white; + padding: 0.5rem; + background-color: #3d5157; + margin-right: .8em; + border-radius: 4px 0 0 4px; + align-self: stretch; +} + +.hero .name { + height: min-content; +} diff --git a/aio/content/examples/animations/src/app/querying.component.ts b/aio/content/examples/animations/src/app/querying.component.ts new file mode 100644 index 00000000000..c8a9dba78e1 --- /dev/null +++ b/aio/content/examples/animations/src/app/querying.component.ts @@ -0,0 +1,80 @@ +import { + Component, +} from '@angular/core'; +import { + trigger, + style, + animate, + transition, + group, + query, + animateChild, + keyframes +} from '@angular/animations'; + +import { HEROES } from './mock-heroes'; + +@Component({ + selector: 'app-querying', + template: ` + +
+

I am a simple child element

+

I am a child element that enters and leaves with its parent

+

I am a child element with an animation trigger

+
+ {{ hero.id }} + {{ hero.name }} (heroes are always animated!) +
+
+ `, + styleUrls: ['./querying.component.css'], + animations: [ + trigger('query', [ + transition(':enter', [ + style({ height: 0 }), + group([ + animate(500, style({ height: '*' })), + query(':enter', [ + style({ opacity: 0, transform: 'scale(0)'}), + animate(2000, style({ opacity: 1, transform: 'scale(1)' })) + ]), + query('.hero', [ + style({ transform: 'translateX(-100%)'}), + animate('.7s 500ms ease-in', style({ transform: 'translateX(0)' })) + ]), + ]), + query('@animateMe', animateChild()), + ]), + transition(':leave', [ + style({ height: '*' }), + query('@animateMe', animateChild()), + group([ + animate('500ms 500ms', style({ height: '0', padding: '0' })), + query(':leave', [ + style({ opacity: 1, transform: 'scale(1)'}), + animate('1s', style({ opacity: 0, transform: 'scale(0)' })) + ]), + query('.hero', [ + style({ transform: 'translateX(0)'}), + animate('.7s ease-out', style({ transform: 'translateX(-100%)' })) + ]), + ]), + ]), + ]), + trigger('animateMe', [ + transition('* <=> *', animate('500ms cubic-bezier(.68,-0.73,.26,1.65)', keyframes([ + style({ backgroundColor: "transparent", color: '*', offset: 0 }), + style({ backgroundColor: "blue", color: 'white', offset: 0.2 }), + style({ backgroundColor: "transparent", color: '*', offset: 1 }) + ]))) + ]), + ] +}) +export class QueryingComponent { + toggleDisabled = false; + show = true; + hero = HEROES[0]; +} diff --git a/aio/content/guide/complex-animation-sequences.md b/aio/content/guide/complex-animation-sequences.md index aeb9256eb69..eee10fad75b 100644 --- a/aio/content/guide/complex-animation-sequences.md +++ b/aio/content/guide/complex-animation-sequences.md @@ -20,11 +20,34 @@ The functions that control complex animation sequences are: {@a complex-sequence} +## The query() function + +Most complex animations rely on the `query()` function to find child elements and apply animations to them, basic examples of such are: + - `query()` followed by `animate()` + used to query simple HTML elements and directly apply animations to them. + - `query()` followed by `animateChild()` + used to query child elements which themselves have animations metadata applied to them and trigger such animation (which would be otherwise be blocked by the current/parent element's animation) + +The first argument of `query()` is a [css selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) string which can also contain the following angular-specific tokens: + - `:enter`/`:leave` for entering/leaving elements + - `:animating` for elements currently animating + - `@*`/`@triggerName` for elements with any or a specific trigger + - `:self` the animating element itself + +
+ +
Entering and Leaving Elements
+ + Not all child elements are actually considered as entering/leaving, this can at times be counterintuitive and confusing, please see the [query api docs](/api/animations/query#entering-and-leaving-elements) for more information. + + + You can also see an illustration of this in the animations live example (introduced in the animations [introduction section](/guide/animations#about-this-guide)) under the Querying tab. + +
+ ## Animate multiple elements using query() and stagger() functions -The `query()` function lets you find inner elements within the element that is being animated. This function targets specific HTML elements within a parent component and applies animations to each element individually. Angular intelligently handles setup, teardown, and cleanup as it coordinates the elements across the page. - -The `stagger()` function lets you define a timing gap between each queried item that is animated and thus animates elements with a delay between them. +After having queried child elements via `query()`, the `stagger()` function lets you define a timing gap between each queried item that is animated and thus animates elements with a delay between them. The following example demonstrates how to use the `query()` and `stagger()` functions to animate a list (of heroes) adding each in sequence, with a slight delay, from top to bottom. diff --git a/aio/content/guide/transition-and-triggers.md b/aio/content/guide/transition-and-triggers.md index 45869ee1a74..1b37927bb32 100644 --- a/aio/content/guide/transition-and-triggers.md +++ b/aio/content/guide/transition-and-triggers.md @@ -75,12 +75,6 @@ Combine wildcard and void states in a transition to trigger animations that ente This section shows how to animate elements entering or leaving a page. -
- -**Note:** For this example, an element entering or leaving a view is equivalent to being inserted or removed from the DOM. - -
- Add a new behavior: * When you add a hero to the list of heroes, it appears to fly onto the page from the left. @@ -109,6 +103,12 @@ So, use the aliases `:enter` and `:leave` to target HTML elements that are inser The `:enter` transition runs when any `*ngIf` or `*ngFor` views are placed on the page, and `:leave` runs when those views are removed from the page. +
+ + **Note:** Entering/leaving behaviors can sometime be confusing. As a rule of thumb consider that any element being added to the DOM by Angular passes via the `:enter` transition, but only elements being directly removed from the DOM by Angular pass via the `:leave` transition (e.g. an element's view is removed from the DOM because its parent is being removed from the DOM or the app's route has changed, then the element will not pass via the `:leave` transition). + +
+ This example has a special trigger for the enter and leave animation called `myInsertRemoveTrigger`. The HTML template contains the following code.