Angular registering animation triggers
Tags: angular
Published: 2022-12-27 11:11:48

This post keeps digging into **AnimationRendererFactory** creation routine, with a  focus on triggers registering logic.

----

## AnimationRenderer, the real one
Inside first article of this series we examined main concepts about Angular's **AnimationRendererFactory** and how it creates a "dumb" **BaseAnimationRenderer** to allow components not declaring any [**animation trigger**](https://angular.io/api/animations/trigger) to be known by the animation engine.

In this post we're going to analyze the creation process of a more interesting child class of the former renderer: **AnimationRenderer**.
An instance of this type is the one generated for components declaring one or more triggers inside their `@Component` decorator metadata `animations` property, which are needed to be registered with the engine before injecting the latter inside renderer constructor.

##RendererType2: component's rendering metadata
We already seen factory's [**createRenderer**](https://github.com/angular/angular/blob/main/packages/platform-browser/animations/src/animation_renderer.ts#L44) expecting a _type_ argument, declared as [**RendererType2**](https://angular.io/api/core/RendererType2).
This interface is defined to store some rendering information generated at component creation.
```ts
interface RendererType2 {
  id: string;
  encapsulation: ViewEncapsulation;
  styles: (string | any[])[];
  data: {[kind: string]: any};
}
```
If second and third ones remind you of some `@Component` decorator's metadata, that's because the framework takes their values exactly from there when creating a component, being it routed or a child one.
Let's examine them one by one:

- **id**: this is an identifier generated by compiler for the specific Component **class**.
Don't be fooled by misleading official doc definition:
 `A unique identifying string for the new renderer`.
The assertion could let the reader think everytime he creates a new instance of the component, this will get a new **id**, but that's not true.
The property has to be considered as a _static class field_: **every instance of the same @Component class will get the same id**.
Official definition is not wrong per-se, but take in account only _DomRendererFactory2_, that for every instance of the same component will return the same cached renderer (at least for _ViewEncapsulation.Emulated_), making the id effectively unique for all _EmulatedEncapsulationDomRenderer2_ generated for that class, and obviously for the single instance of _DefaultDomRenderer2_ returned in some conditions.
This definition loses its accountability when talking about _AnimationRenderer_, where two different instances of the same component will get assigned two different instances of the renderer, due to the introduction of _namespace_ concept. (More about this later)
- **encapsulation**: with this value _DomRendererFactory2_ choose which type of encapsulation-specific renderer to create, through a simple switch statement:
```ts
switch (type.encapsulation) {
  case ViewEncapsulation.Emulated: {
    ...
    renderer = new EmulatedEncapsulationDomRenderer2(
      this.eventManager, this.sharedStylesHost, type, this.appId);
    ...
    return renderer;
  }
  case ViewEncapsulation.ShadowDom:
    return new ShadowDomRenderer(this.eventManager, this.sharedStylesHost, element, type);
  default: {
    ...
    return this.defaultRenderer;
  }
```
- **styles**: CSS style or list of styles to be assigned to host element of rendered component
- **data**: this property shaped as an open dictionary could sound "alien", but it's actually the one used to define a whole set of optional characteristics of the component that's gonna be created.
It's crucial for our case study because that's where _AnimationRenderer_ will look for declared [**AnimationTriggerMetadata**](https://angular.io/api/animations/AnimationTriggerMetadata) to be registered.

##Namespace independently tracking state of component instances triggers
To keep track of individual component instances' triggers state, Angular animation package introduces the concept of [**AnimationTransitionNamespace**](https://github.com/angular/angular/blob/f58ad86e51817f83ff18db790a347528262b850b/packages/animations/browser/src/render/transition_animation_engine.ts#L112).
You can think of it as a wrapper grouping all elements of a single animated component instance.
As already explained, _id_ property from _Renderer2_ interface is unique for component's **class**, thus is not enough to distinguish among different instances of same component.
That's why the factory declares a new property, incremented on every _AnimationRenderer_ creation, and concatenated with _Renderer2_'s _id_ to compose an identifier unique for _namespace_, used to register the latter with the engine:
```ts
const componentId = type.id;
const namespaceId = type.id + '-' + this._currentId;
this._currentId++;

this.engine.register(namespaceId, hostElement);
```
You can notice these identifiers added as a class to every DOM element of the host component: they are all prepended by `ng-tns-` string:


![ns_classes_added](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ec95pc1rbabda51273dk.png)
Look at the `ng-tns-c19-8` class assigned to `<app-home>` element: this means that `HomeComponent` got `c19` as its `RendererType2.id`, and that this specific instance has been bound to the **ninth** (__currentId_ is initialized as *0*) _AnimationRenderer_ created by the factory, which being injected as singleton make that number the creation order along the whole app.
(AFAIK, these identifiers have just a naming purpose, so their order should be of no interest.)

You can see that the same class has been added to everyone of its children, both `<div>` and `<app-child>` nodes.
But in the second a difference catches the eye:
another _namespace class_ is assigned beside the parent's one, `ng-tns-c18-9`.
It's clear that this namespace refers to a different component class than the parent one (`c-18` vs `c-19`). 
That's expected, being `<app-child>` the host element of a different component. 
Since `ChildComponent`'s declares some triggers inside  its decorator `animations` property, a new _AnimationRenderer_ has been created for it, thus a new namespace.
So, being this component instance both declaring some animations, both included in the template of an animated parent component, it's part of two different namespaces.

Looking at third childnode, another `<app-child>` element, we notice that parent namespace class is still there, but its own namespace class is different than the one of its sibling: `ng-tns-c18-10`, same component type id but counter incremented by one.
This concurs with source code we analyzed earlier: even being instances of the same component class, they got separate renderers and are part of distinct namespaces.

Last thing worth noting is the inner elements of child host elements, in our example a `<div>` inside the expanded last `<app-child>`: it gets the namespace of component it belongs to, `ng-tns-c18-10`, but not the one of its grandparent, like its host element does.
This sounds quite logical, considering that `<app-child>` is included into `HomeComponent`'s template, while child nodes inside its own template are not.

##Actual registration of triggers
On top of the source file declaring this factory, we got a couple of type declarations (a type and an interface, to be precise)
```ts
// Define a recursive type to allow for nested arrays of `AnimationTriggerMetadata`. Note that an
// interface declaration is used as TypeScript prior to 3.7 does not support recursive type
// references, see https://github.com/microsoft/TypeScript/pull/33050 for details.
type NestedAnimationTriggerMetadata = AnimationTriggerMetadata|RecursiveAnimationTriggerMetadata;
interface RecursiveAnimationTriggerMetadata extends Array<NestedAnimationTriggerMetadata> {}
```
They look quite tricky at first sight, but reading accompanying comment and analyzing their definitions, it turns out they just form a recursive data structure.
Their use, as we're gonna see, addresses a problem arisen switching from _ViewEngine_ to _Ivy_, with the new one losing the capabilities of flattening metadata arrays, resulting in wrong registering of triggers passed to `@Component` as nested arrays, leading to runtime errors.

Compiler parsed our `@Component` decorators metadata and stored their `animations` field content inside `animation` property of aforementioned `data` open-dictionary field of related _RendererType2_ object. 
Renderer creation routine casts this value to an array of the recursive data structure we already seen has been defined on purpose.
Then it iterates over that, issuing a registration function for every trigger it finds.
```ts
const animationTriggers = type.data['animation'] as NestedAnimationTriggerMetadata[];
animationTriggers.forEach(registerTrigger);
```

This registering function has to take in account the recursive nature of this data structure: the object it gets passed as argument could be an actual [**trigger**](https://angular.io/api/animations/AnimationTriggerMetadata) to be registered, or an array itself containing triggers definitions, and even an array containing other arrays. This nesting has an unpredictable depth, so our function has to be implemented with a recursive logic.
```ts
const registerTrigger = (trigger: NestedAnimationTriggerMetadata) => {
  if (Array.isArray(trigger)) {
    trigger.forEach(registerTrigger);
  } else {
    this.engine.registerTrigger(componentId, namespaceId, hostElement, trigger.name, trigger);
  }
};
```
We can see it's a common flattening algorithm often used with recursively nested arrays: a function recalling itself until its argument is proved to be an array, when this check fails we are in presence of a trigger definition to be registered with engine (and in turn with specific namespace).

When all defined triggers have been registered, a new **AnimationRenderer** can be built and returned, injected with the `namespaceId` composed in previous phase.

----
Hope this post has been an interesting reading, and since the topic has still many "obscure" details for me, I encourage anyone with a constructive suggestion to get in touch in comments.
Next time we'll take a look at _AnimationRenderer_ actual work.
Cheers.