Skip to content

Commit f34d375

Browse files
authored
feat(all): add support for configuring animations on a per-page basis (#21433)
1 parent c8db0d5 commit f34d375

File tree

34 files changed

+334
-142
lines changed

34 files changed

+334
-142
lines changed

angular/src/directives/navigation/ion-back-button.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Directive, HostListener, Optional } from '@angular/core';
2+
import { AnimationBuilder } from '@ionic/core';
23

34
import { Config } from '../../providers/config';
45
import { NavController } from '../../providers/nav-controller';
@@ -7,11 +8,12 @@ import { IonRouterOutlet } from './ion-router-outlet';
78

89
@Directive({
910
selector: 'ion-back-button',
10-
inputs: ['defaultHref'],
11+
inputs: ['defaultHref', 'routerAnimation'],
1112
})
1213
export class IonBackButtonDelegate {
1314

1415
defaultHref: string | undefined | null;
16+
routerAnimation?: AnimationBuilder;
1517

1618
constructor(
1719
@Optional() private routerOutlet: IonRouterOutlet,
@@ -27,10 +29,11 @@ export class IonBackButtonDelegate {
2729
const defaultHref = this.defaultHref || this.config.get('backButtonDefaultHref');
2830

2931
if (this.routerOutlet && this.routerOutlet.canGoBack()) {
32+
this.navCtrl.setDirection('back', undefined, undefined, this.routerAnimation);
3033
this.routerOutlet.pop();
3134
ev.preventDefault();
3235
} else if (defaultHref != null) {
33-
this.navCtrl.navigateBack(defaultHref);
36+
this.navCtrl.navigateBack(defaultHref, { animation: this.routerAnimation });
3437
ev.preventDefault();
3538
}
3639
}

angular/src/directives/navigation/router-link-delegate.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
import { LocationStrategy } from '@angular/common';
22
import { Directive, ElementRef, HostListener, Optional } from '@angular/core';
33
import { Router, RouterLink } from '@angular/router';
4-
import { RouterDirection } from '@ionic/core';
4+
import { AnimationBuilder, RouterDirection } from '@ionic/core';
55
import { Subscription } from 'rxjs';
66

77
import { NavController } from '../../providers/nav-controller';
88

99
@Directive({
1010
selector: '[routerLink]',
11-
inputs: ['routerDirection']
11+
inputs: ['routerDirection', 'routerAnimation']
1212
})
1313
export class RouterLinkDelegate {
1414

1515
private subscription?: Subscription;
1616

1717
routerDirection: RouterDirection = 'forward';
18+
routerAnimation?: AnimationBuilder;
1819

1920
constructor(
2021
private locationStrategy: LocationStrategy,
@@ -50,7 +51,7 @@ export class RouterLinkDelegate {
5051
*/
5152
@HostListener('click', ['$event'])
5253
onClick(ev: UIEvent) {
53-
this.navCtrl.setDirection(this.routerDirection);
54+
this.navCtrl.setDirection(this.routerDirection, undefined, undefined, this.routerAnimation);
5455
ev.preventDefault();
5556
}
5657
}

angular/src/directives/navigation/stack-controller.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Location } from '@angular/common';
22
import { ComponentRef, NgZone } from '@angular/core';
33
import { ActivatedRoute, Router } from '@angular/router';
4-
import { RouterDirection } from '@ionic/core';
4+
import { AnimationBuilder, RouterDirection } from '@ionic/core';
55

66
import { bindLifecycleEvents } from '../../providers/angular-delegate';
77
import { NavController } from '../../providers/nav-controller';
@@ -52,7 +52,8 @@ export class StackController {
5252
}
5353

5454
setActive(enteringView: RouteView): Promise<StackEvent> {
55-
let { direction, animation } = this.navCtrl.consumeTransition();
55+
const consumeResult = this.navCtrl.consumeTransition();
56+
let { direction, animation, animationBuilder } = consumeResult;
5657
const leavingView = this.activeView;
5758
const tabSwitch = isTabSwitch(enteringView, leavingView);
5859
if (tabSwitch) {
@@ -105,6 +106,31 @@ export class StackController {
105106
enteringView.ref.changeDetectorRef.detectChanges();
106107
}
107108

109+
/**
110+
* If we are going back from a page that
111+
* was presented using a custom animation
112+
* we should default to using that
113+
* unless the developer explicitly
114+
* provided another animation.
115+
*/
116+
const customAnimation = enteringView.animationBuilder;
117+
if (
118+
animationBuilder === undefined &&
119+
direction === 'back' &&
120+
!tabSwitch &&
121+
customAnimation !== undefined
122+
) {
123+
animationBuilder = customAnimation;
124+
}
125+
126+
/**
127+
* Save any custom animation so that navigating
128+
* back will use this custom animation by default.
129+
*/
130+
if (animationBuilder !== undefined && leavingView) {
131+
leavingView.animationBuilder = animationBuilder;
132+
}
133+
108134
// Wait until previous transitions finish
109135
return this.zone.runOutsideAngular(() => {
110136
return this.wait(() => {
@@ -116,7 +142,7 @@ export class StackController {
116142
// In case the enteringView is the same as the leavingPage we need to reattach()
117143
enteringView.ref.changeDetectorRef.reattach();
118144

119-
return this.transition(enteringView, leavingView, animation, this.canGoBack(1), false)
145+
return this.transition(enteringView, leavingView, animation, this.canGoBack(1), false, animationBuilder)
120146
.then(() => cleanupAsync(enteringView, views, viewsSnapshot, this.location))
121147
.then(() => ({
122148
enteringView,
@@ -154,8 +180,8 @@ export class StackController {
154180
url = primaryOutlet.route._routerState.snapshot.url;
155181
}
156182
}
157-
158-
return this.navCtrl.navigateBack(url, view.savedExtras).then(() => true);
183+
const { animationBuilder } = this.navCtrl.consumeTransition();
184+
return this.navCtrl.navigateBack(url, { ...view.savedExtras, animation: animationBuilder }).then(() => true);
159185
});
160186
}
161187

@@ -225,7 +251,8 @@ export class StackController {
225251
leavingView: RouteView | undefined,
226252
direction: 'forward' | 'back' | undefined,
227253
showGoBack: boolean,
228-
progressAnimation: boolean
254+
progressAnimation: boolean,
255+
animationBuilder?: AnimationBuilder
229256
) {
230257
if (this.skipTransition) {
231258
this.skipTransition = false;
@@ -250,7 +277,8 @@ export class StackController {
250277
duration: direction === undefined ? 0 : undefined,
251278
direction,
252279
showGoBack,
253-
progressAnimation
280+
progressAnimation,
281+
animationBuilder
254282
});
255283
}
256284
}

angular/src/directives/navigation/stack-utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ComponentRef } from '@angular/core';
22
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
3-
import { NavDirection, RouterDirection } from '@ionic/core';
3+
import { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core';
44

55
export const insertView = (views: RouteView[], view: RouteView, direction: RouterDirection) => {
66
if (direction === 'root') {
@@ -96,4 +96,5 @@ export interface RouteView {
9696
savedData?: any;
9797
savedExtras?: NavigationExtras;
9898
unlistenEvents: () => void;
99+
animationBuilder?: AnimationBuilder;
99100
}

angular/src/directives/proxies.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ export class IonAvatar {
2626
}
2727
export declare interface IonBackButton extends Components.IonBackButton {
2828
}
29-
@ProxyCmp({ inputs: ["color", "defaultHref", "disabled", "icon", "mode", "text", "type"] })
30-
@Component({ selector: "ion-back-button", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["color", "defaultHref", "disabled", "icon", "mode", "text", "type"] })
29+
@ProxyCmp({ inputs: ["color", "defaultHref", "disabled", "icon", "mode", "routerAnimation", "text", "type"] })
30+
@Component({ selector: "ion-back-button", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["color", "defaultHref", "disabled", "icon", "mode", "routerAnimation", "text", "type"] })
3131
export class IonBackButton {
3232
protected el: HTMLElement;
3333
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
@@ -61,8 +61,8 @@ export class IonBadge {
6161
}
6262
export declare interface IonButton extends Components.IonButton {
6363
}
64-
@ProxyCmp({ inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "href", "mode", "rel", "routerDirection", "shape", "size", "strong", "target", "type"] })
65-
@Component({ selector: "ion-button", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "href", "mode", "rel", "routerDirection", "shape", "size", "strong", "target", "type"] })
64+
@ProxyCmp({ inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] })
65+
@Component({ selector: "ion-button", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] })
6666
export class IonButton {
6767
ionFocus!: EventEmitter<CustomEvent>;
6868
ionBlur!: EventEmitter<CustomEvent>;
@@ -86,8 +86,8 @@ export class IonButtons {
8686
}
8787
export declare interface IonCard extends Components.IonCard {
8888
}
89-
@ProxyCmp({ inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerDirection", "target", "type"] })
90-
@Component({ selector: "ion-card", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerDirection", "target", "type"] })
89+
@ProxyCmp({ inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] })
90+
@Component({ selector: "ion-card", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] })
9191
export class IonCard {
9292
protected el: HTMLElement;
9393
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
@@ -220,8 +220,8 @@ export class IonFab {
220220
}
221221
export declare interface IonFabButton extends Components.IonFabButton {
222222
}
223-
@ProxyCmp({ inputs: ["activated", "color", "disabled", "download", "href", "mode", "rel", "routerDirection", "show", "size", "target", "translucent", "type"] })
224-
@Component({ selector: "ion-fab-button", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["activated", "color", "disabled", "download", "href", "mode", "rel", "routerDirection", "show", "size", "target", "translucent", "type"] })
223+
@ProxyCmp({ inputs: ["activated", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "show", "size", "target", "translucent", "type"] })
224+
@Component({ selector: "ion-fab-button", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["activated", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "show", "size", "target", "translucent", "type"] })
225225
export class IonFabButton {
226226
ionFocus!: EventEmitter<CustomEvent>;
227227
ionBlur!: EventEmitter<CustomEvent>;
@@ -344,8 +344,8 @@ export class IonInput {
344344
}
345345
export declare interface IonItem extends Components.IonItem {
346346
}
347-
@ProxyCmp({ inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerDirection", "target", "type"] })
348-
@Component({ selector: "ion-item", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerDirection", "target", "type"] })
347+
@ProxyCmp({ inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] })
348+
@Component({ selector: "ion-item", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] })
349349
export class IonItem {
350350
protected el: HTMLElement;
351351
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
@@ -498,8 +498,8 @@ export class IonNav {
498498
}
499499
export declare interface IonNavLink extends Components.IonNavLink {
500500
}
501-
@ProxyCmp({ inputs: ["component", "componentProps", "routerDirection"] })
502-
@Component({ selector: "ion-nav-link", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["component", "componentProps", "routerDirection"] })
501+
@ProxyCmp({ inputs: ["component", "componentProps", "routerAnimation", "routerDirection"] })
502+
@Component({ selector: "ion-nav-link", changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-content></ng-content>", inputs: ["component", "componentProps", "routerAnimation", "routerDirection"] })
503503
export class IonNavLink {
504504
protected el: HTMLElement;
505505
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {

angular/src/providers/nav-controller.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { Location } from '@angular/common';
22
import { Injectable, Optional } from '@angular/core';
33
import { NavigationExtras, NavigationStart, Router, UrlSerializer, UrlTree } from '@angular/router';
4-
import { NavDirection, RouterDirection } from '@ionic/core';
4+
import { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core';
55

66
import { IonRouterOutlet } from '../directives/navigation/ion-router-outlet';
77

88
import { Platform } from './platform';
99

1010
export interface AnimationOptions {
1111
animated?: boolean;
12+
animation?: AnimationBuilder;
1213
animationDirection?: 'forward' | 'back';
1314
}
1415

@@ -22,6 +23,7 @@ export class NavController {
2223
private topOutlet?: IonRouterOutlet;
2324
private direction: 'forward' | 'back' | 'root' | 'auto' = DEFAULT_DIRECTION;
2425
private animated?: NavDirection = DEFAULT_ANIMATED;
26+
private animationBuilder?: AnimationBuilder;
2527
private guessDirection: RouterDirection = 'forward';
2628
private guessAnimation?: NavDirection;
2729
private lastNavId = -1;
@@ -65,7 +67,7 @@ export class NavController {
6567
* ```
6668
*/
6769
navigateForward(url: string | UrlTree | any[], options: NavigationOptions = {}): Promise<boolean> {
68-
this.setDirection('forward', options.animated, options.animationDirection);
70+
this.setDirection('forward', options.animated, options.animationDirection, options.animation);
6971
return this.navigate(url, options);
7072
}
7173

@@ -88,7 +90,7 @@ export class NavController {
8890
* ```
8991
*/
9092
navigateBack(url: string | UrlTree | any[], options: NavigationOptions = {}): Promise<boolean> {
91-
this.setDirection('back', options.animated, options.animationDirection);
93+
this.setDirection('back', options.animated, options.animationDirection, options.animation);
9294
return this.navigate(url, options);
9395
}
9496

@@ -111,7 +113,7 @@ export class NavController {
111113
* ```
112114
*/
113115
navigateRoot(url: string | UrlTree | any[], options: NavigationOptions = {}): Promise<boolean> {
114-
this.setDirection('root', options.animated, options.animationDirection);
116+
this.setDirection('root', options.animated, options.animationDirection, options.animation);
115117
return this.navigate(url, options);
116118
}
117119

@@ -121,7 +123,7 @@ export class NavController {
121123
* by default.
122124
*/
123125
back(options: AnimationOptions = { animated: true, animationDirection: 'back' }) {
124-
this.setDirection('back', options.animated, options.animationDirection);
126+
this.setDirection('back', options.animated, options.animationDirection, options.animation);
125127
return this.location.back();
126128
}
127129

@@ -150,9 +152,10 @@ export class NavController {
150152
*
151153
* It's recommended to use `navigateForward()`, `navigateBack()` and `navigateRoot()` instead of `setDirection()`.
152154
*/
153-
setDirection(direction: RouterDirection, animated?: boolean, animationDirection?: 'forward' | 'back') {
155+
setDirection(direction: RouterDirection, animated?: boolean, animationDirection?: 'forward' | 'back', animationBuilder?: AnimationBuilder) {
154156
this.direction = direction;
155157
this.animated = getAnimation(direction, animated, animationDirection);
158+
this.animationBuilder = animationBuilder;
156159
}
157160

158161
/**
@@ -168,6 +171,7 @@ export class NavController {
168171
consumeTransition() {
169172
let direction: RouterDirection = 'root';
170173
let animation: NavDirection | undefined;
174+
const animationBuilder = this.animationBuilder;
171175

172176
if (this.direction === 'auto') {
173177
direction = this.guessDirection;
@@ -178,10 +182,12 @@ export class NavController {
178182
}
179183
this.direction = DEFAULT_DIRECTION;
180184
this.animated = DEFAULT_ANIMATED;
185+
this.animationBuilder = undefined;
181186

182187
return {
183188
direction,
184-
animation
189+
animation,
190+
animationBuilder
185191
};
186192
}
187193

angular/test/test-app/src/app/alert/alert.component.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
<ion-header>
22
<ion-toolbar>
3+
<ion-buttons>
4+
<ion-back-button></ion-back-button>
5+
</ion-buttons>
36
<ion-title>
47
Alert test
58
</ion-title>

angular/test/test-app/src/app/home-page/home-page.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
</ion-header>
88
<ion-content>
99
<ion-list>
10-
<ion-item routerLink="/alerts">
10+
<ion-item routerLink="/alerts" [routerAnimation]="routerAnimation">
1111
<ion-label>
1212
Alerts test
1313
</ion-label>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
import { Component } from '@angular/core';
2+
import { AnimationBuilder, AnimationController } from '@ionic/angular';
23

34
@Component({
45
selector: 'app-home-page',
56
templateUrl: './home-page.component.html',
67
})
78
export class HomePageComponent {
9+
routerAnimation: AnimationBuilder = (_, opts) => {
10+
const { direction, enteringEl, leavingEl } = opts;
11+
const animation = this.animationCtrl.create().duration(500).easing('ease-out');
12+
const enteringAnimation = this.animationCtrl.create().addElement(enteringEl).beforeRemoveClass(['ion-page-invisible']);
13+
const leavingAnimation = this.animationCtrl.create().addElement(leavingEl).beforeRemoveClass(['ion-page-invisible']);
14+
if (direction === 'back') {
15+
enteringAnimation.fromTo('transform', 'translateX(-100%)', 'translateX(0%)');
16+
leavingAnimation.fromTo('transform', 'translateX(0%)', 'translateX(100%)');
17+
} else {
18+
enteringAnimation.fromTo('transform', 'translateX(100%)', 'translateX(0%)');
19+
leavingAnimation.fromTo('transform', 'translateX(0%)', 'translateX(-100%)');
20+
}
21+
return animation.addAnimation([enteringAnimation, leavingAnimation]);
22+
};
823

24+
constructor(private animationCtrl: AnimationController) {}
925
}

0 commit comments

Comments
 (0)