Ionic Routing And Router Animations In Angular

Angular and its router are powerful tools to create single-page applications (SPAs). However, there are some critical differences in the context of an Ionic Framework application. Ionic Framework (short Ionic) comes with many new components and features, such as its router and animation system. Ionic's router enables us to write routing animations and page transitions in a simple manner. It even applies them to lazy loaded modules and pages – especially the last part is a more complex task in a standard Angular application.

In this article:

MS-rund
Max Marschall is engineer consultant at Thinktecture with focus on Angular and 2D/3D visualization.

The included Ionic animations system is framework-agnostic, and it leverages the Web Animations API to create performant and resource-saving animations. It is available with the Ionic Framework version 5.0 and upward.

Version Information:

  • Angular: 9+
  • Ionic Framework 5.0

Let’s start by taking a look at the Ionic router and explaining its introduced feature set. Afterward, we add navigation transitions using the Ionic animation system.

Differences Between Angular's and Ionic's Router

The Ionic Framework router extends the Angular router. You can use them next to each other, for example, in nested routes. However, some features are only available to the Ionic router, such as its page transition system and an internal history stack. The most significant difference between these two routers is that Ionic’s router does not remove loaded “pages” from the DOM, but it tracks them in an internal state and hidden in the DOM. That enables us to reuse and animate once-loaded pages because components will not be reinitialized; instead, they become just visible again. It can also speed up the load time and responsiveness on specific mobile scenarios.

How does the Ionic router keep track of opened and closed pages?

Ionic’s Navigation State

Every time a page is visited, it is also added to the stack and later hidden in the DOM; Angular, in contrast, removes it completely. But it keeps them available and hidden until they are popped off their internal state. The stack and hidden DOM elements are critical for Ionics page transitions. It enables Ionic to pre-load pages offscreen and then start the desired animation. The pre-loading also makes it easy to manage the animation because all elements are available and freely manipulated.

You can observe the behavior by opening the dev tools and inspecting the DOM nodes containing the router. You should see something similar to this:

An element is only removed from the stack if it is actively cleared or popped; this can be done either by navigating back using methods provided by Ionic’s framework or navigating back with the browser. However, navigating “forward” with the browser does not add the page back to the stack or plays its transition.

One method provided by Ionic is one of its UI elements, the back-button component or the routerDirection directive to remove the last element from the stack.

				
					<ion-back-button defaultHref="/"></ion-back-button>
<a routerLink="/path/to/page" routerDirection="back">Back to…</a>
				
			

Alternatively, you can invoke the pop or navigateBack method on the NavController class.

				
					import { NavController, Platform } from '@ionic/angular';

private navController: NavController,

this.navController.pop();
this.navController.navigateBack('path/to/page');
				
			

The pop is the recommended way to go back one step in history – in terms of the Ionic navigation stack and using the NavControler. The navigateBack method will go back in history and pop every state until the given path is found. Popping or going back in history will also display the back page transition to the target page and remove the page from the stack and the DOM.

Since pages are not directly added or removed from the DOM anymore and the OnInit and OnDestroy are not called as expected, we need a new way to interact with their lifecycle.

New Lifecycle Callbacks

Ionic Framework also introduces new component lifecycle steps, and this is necessary due to the loading/offloading behavior of the router.

Removing a page from the view but not from the DOM renders it mandatory to provide interfaces for this new behavior. Angular does not call the OnInit or OnDestroy and other lifecycle methods as long as the component is not removed from or added to the DOM. This chart shows the new lifecycle methods by Ionic and on which events and in which order they are called:

Adding Navigation Animations with Ionic

Let’s define and handle the page transition animation on a global scope using Ionic’s animation system. We plan to create a page transition like shown below:

Let’s start with the function definition: We are receiving two inputs. The first one is the affected router outlet and can be ignored for this use case. The second one contains all the required information.

				
					export const customAnimation = (_: HTMLElement, opts: any): Animation {
  ...
}
				
			

Ionic does not export the type definition for the second import, but we can grab it from the sources (Source 1Source 2Source 3) our self. For the animation, we are interested in the entering and leaving DOM element and the animation configuration direction and duration.

This is how a typical options object looks:

				
					export interface AnimationOptions {
  mode: 'md' | 'ios';
  animated: boolean;
  direction: 'root' | 'forward' | 'back';
  enteringEl: HTMLElement;
  leavingEl: HTMLElement;
  baseEl: HTMLElement;
  progressAnimation: boolean;
  showGoBack: boolean;
  animationBuilder: (_, opts) => {}
  progressionCallback?: () => {};
  duration?: number;
}
				
			

Let’s use Ionic’s animation system to create the desired page transition effect. First, we create an animation controller with some default configurations for all transitions.

				
					export const customAnimation = (_: HTMLElement, opts: any): Animation {
  // create root transition
  const rootTransition = animationCtrl
    .create()
    .duration(opts.duration || 333)
    .easing('cubic-bezier(0.7,0,0.3,1)');
}
				
			

Afterward, we start defining the transitions for the elements we want to animate.

				
					const enterTransition = animationCtrl.create().addElement(opts.enteringEl);
const exitTransition = animationCtrl.create().addElement(opts.leavingEl);
}
				
			

We can define different animations, depending on the navigation direction. In this case, going forward shifts a page in from the left, and going back reverses the animation. We are doing so by defining key points for a property – where to start the animation and with what value it should end. The framework then tweens between these two points using the Web Animations API wherever possible.

				
					enterTransition.fromTo('opacity', '0', '1');
exitTransition.fromTo('opacity', '1', '0');

// handle animation depending on direction
if (opts.direction === 'forward') {
  enterTransition.fromTo('transform', 'translateX(-1.5%)', 'translateX(0%)');
  exitTransition.fromTo('transform', 'translateX(0%)', 'translateX(1.5%)');
} else {
  enterTransition.fromTo('transform', 'translateX(1.5%)', 'translateX(0%)');
  exitTransition.fromTo('transform', 'translateX(0%)', 'translateX(-1.5%)');
}
				
			

Last but not least, we add our transitions to the preconfigured root transition and return it.

				
					// add transition to root and return root
...
rootTransition.addAnimation([enterTransition, exitTransition]);
return rootTransition;
				
			

The complete function should look like the following code:

				
					export const customAnimation = (_: HTMLElement, opts: any): Animation {
  // create root transition
  const rootTransition = animationCtrl
    .create()
    .duration(opts.duration || 333)
    .easing('cubic-bezier(0.7,0,0.3,1)');

  const enterTransition = animationCtrl.create().addElement(opts.enteringEl);
  const exitTransition = animationCtrl.create().addElement(opts.leavingEl);
    
  enterTransition.fromTo('opacity', '0', '1');
  exitTransition.fromTo('opacity', '1', '0');
    
  if (opts.direction === 'forward') {
    enterTransition.fromTo('transform', 'translateX(-1.5%)', 'translateX(0%)');
    exitTransition.fromTo('transform', 'translateX(0%)', 'translateX(1.5%)');
  } else {
    enterTransition.fromTo('transform', 'translateX(1.5%)', 'translateX(0%)');
    exitTransition.fromTo('transform', 'translateX(0%)', 'translateX(-1.5%)');
  }
  
  rootTransition.addAnimation([enterTransition, exitTransition]);
  return rootTransition;
}
				
			

Finally, we can tell Ionic to use our custom animation for navigation. We are doing so by providing the method to the IonicModule configuration in our app module.

				
					@NgModule({
  ...
  imports: [
    ...    
    IonicModule.forRoot({navAnimation: customAnimation})
  ]
})

				
			

Let’s take a look at the animation just created. It is the same as displayed above.

The leaving page fades away, and the incoming page fades in. We also see the movement matching the navigation direction, creating a smooth user experience. That way, the user can quickly grasp what happens and won’t lose focus on his task.

Source Code and Live Demo:

Stackblitz: https://stackblitz.com/edit/github-sxhycm-52soeu?

Conclusion

Using animations helps the user to understand changes. It eases cognitive load and can make complex changes comprehendible. Page changes can be hard to process, multiple parts can change, or an entirely new context is created. Ionic’s animation system and router make it easy to animate those changes and guide the user through the process without losing its focus. We can even write custom animations and pinpoint the user experience. Those animations are applied to lazy-loaded modules and pages, too. The produced custom animation code is easy to read and extendable, thanks to the provided animation API.

Ionic’s Router and animations API must not be feared. They provide a straightforward API and integrate well with Angular. Some challenging tasks even become easier using Ionic’s approach.

Free
Newsletter

Current articles, screencasts and interviews by our experts

Don’t miss any content on Angular, .NET Core, Blazor, Azure, and Kubernetes and sign up for our free monthly dev newsletter.

EN Newsletter Anmeldung (#7)
Related Articles
AI
sg
One of the more pragmatic ways to get going on the current AI hype, and to get some value out of it, is by leveraging semantic search. This is, in itself, a relatively simple concept: You have a bunch of documents and want to find the correct one based on a given query. The semantic part now allows you to find the correct document based on the meaning of its contents, in contrast to simply finding words or parts of words in it like we usually do with lexical search. In our last projects, we gathered some experience with search bots, and with this article, I'd love to share our insights with you.
17.05.2024
Angular
SL-rund
If you previously wanted to integrate view transitions into your Angular application, this was only possible in a very cumbersome way that needed a lot of detailed knowledge about Angular internals. Now, Angular 17 introduced a feature to integrate the View Transition API with the router. In this two-part series, we will look at how to leverage the feature for route transitions and how we could use it for single-page animations.
15.04.2024
.NET
KP-round
.NET 8 brings Native AOT to ASP.NET Core, but many frameworks and libraries rely on unbound reflection internally and thus cannot support this scenario yet. This is true for ORMs, too: EF Core and Dapper will only bring full support for Native AOT in later releases. In this post, we will implement a database access layer with Sessions using the Humble Object pattern to get a similar developer experience. We will use Npgsql as a plain ADO.NET provider targeting PostgreSQL.
15.11.2023