Previous page: Integrate GraphQL Subscriptions into Angular components to build a list of messages
Angular Animations
Animate new incoming messages in an Angular component
Messages are appearing within our app in real-time from our subscription. Lets make the user interface more interesting with animations.
There are lots of methods to approaching integration of animations for web documents. We could use SVG animations or add a different set of elements and CSS properties to achieve the same result. This tutorial describes just one method that will work well for our application. It features a responsive web design animation that can adapt to any resolution or display.
Our approach
Our intention is to create a cloned message or spacer
element that will act
as a placeholder to create the space we need to animate a new incoming
message in real time. Once the animation has completed, the spacer will be
deleted.
Before we can start using animations, we need to first import the
BrowserAnimationsModule
module into our new feature module:
From your project folder, open the file
web-app/src/app/messages/messages.module.ts
and add the following lines
defined in comments:

// Existing imports...
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; // <-- Add new import
@NgModule({
// Existing code...
imports: [
CommonModule,
FormsModule,
BrowserAnimationsModule, // <-- Add new module import
MessagesRoutingModule
]
})
export class MessagesModule {}
We're going to add the logic in our component to hook up the animation for
new messages before we describe the animation behaviour. Open the file
web-app/src/app/messages/view-conversation/view-conversation.component.ts
in
your IDE and add the new imports:

// Existing code...
import { trigger, transition, useAnimation } from '@angular/animations'; // <-- Add new import
interface MessageModel {
// Existing code...
}
// Existing code...
To attach an animation to an HTML element, we first need to describe a trigger
to make the association between our template and component animation. In the
component metadata add the animations
property and new trigger
called
newMessage
:

// Existing code...
@Component({
// Existing code...
animations: [ // Add new animation property and trigger
trigger('newMessage', [
])
]
})
export class ViewConversationComponent implements OnInit {
// Existing code...
}
The trigger name will be used in the HTML template to bind to events of the animation. We will later describe the animation inside the trigger.
View-model message properties
We will need to add two optional properties to our existing MessageViewModel
interface called spacer
and isNew
to hold Boolean values.

// Exiting code...
interface MessageViewModel {
// Exiting code...
fromYou: boolean;
spacer?: Boolean; // <-- Add new property
isNew?: Boolean; // <-- Add new property
}
// Exiting code...
These will help the template render the animation by defining the state of a new message and also the difference between new and existing messages.
We will use these properties to create two new (almost identical)
MessageViewModel
instances. One will be a spacer
, the other will be the
readable message that uses with the isNew
property set to true for the
duration of the animation. We need to change the addNewMessage
method we
previously defined to create two instances on our messages
array:

export class ViewConversationComponent implements OnInit {
// Existing code...
updateMessages(result: ApolloQueryResult<{messages: Array<MessageModel>}>) {
// Existing code...
}
/**
* Change method to code below
*/
addNewMessage(messageData: any) {
this.messages.push(
ViewConversationComponent.createMessageInstance({
...messageData,
isNew: true // <-- New property within mixin
})
);
// New spacer instance
this.messages.push(
ViewConversationComponent.createMessageInstance({
...messageData,
spacer: true
})
);
}
// Existing code...
}
If you look at our application now (you can open the browser dev tools to explore how the HTML elements interact), you will see two identical messages get created when sending a single message.
We need to turn the additional message into a spacer by attaching existing
styles we previously included with a conditional class name to a new element
in the template. And adding conditional inline-style rules to the containing
view-conversation__item
.
Open the HTML layout file view-conversation.component.html
within the same
folder and add (or replace with) the following code:

<div class="view-conversation">
<div class="view-conversation__list">
<!-- 1. Add the [ngStyle]="..." directive below -->
<div *ngFor="let message of messages"
class="view-conversation__item"
[ngClass]="{'view-conversation__item--you': message.fromYou}"
[ngStyle]="{
'opacity': message.spacer ? 0 : 1,
'z-index': message.spacer ? 0 : 1,
'position': message.isNew ? 'absolute' : 'static'
}">
<span class="view-conversation__time">{{message.sent | date: 'shortTime'}}</span>
<!-- 2. Add the [@.disabled] and [@newMessage] directives below -->
<div class="view-conversation__message-animation"
[ngStyle]="{'border-color': '#' + message.user.colour}"
[@.disabled]="!message.isNew"
[@newMessage]="!message.spacer || ''">
<!-- 3. Add new cover element -->
<div class="view-conversation__message-cover cover-anim"
[ngStyle]="{
'display': message.isNew ? 'block' : 'none',
'background-color': '#' + (message.user.colour || 'dca629')
}"></div>
</div>
</div>
</div>
Part 1. Add the ngStyle directive
The first part (under comment number 1.
above) conditionally adds styles
based on whether the message is a spacer or not. The spacer will take up normal
document flow
to push out the right amount of space for our message so its position
is
static
. This is the reason the spacer also includes the same text as the
message so that the size of the space can change with the text, whatever the
resolution of the
viewport.
The spacer is also transparent and stacked underneath the readable message
with a z-index
and opacity
of 0
.
Part 2. Add the @.disabled and @newMessage directives
The second part uses an
animation event binding
@.disabled
to conditionally disable the animation only if the message is a
spacer
. We want the spacer to appear immediately.
This part also uses another animation event binding to our custom trigger
@newMessage
that we defined in our component metadata. This syntax will
attach our animation to this HTML element only if it's not a spacer
. The
@
syntax in Angular HTML templates is reserved for describing animation
semantics.
Part 3. Add new cover element
In the third part, we add a new element that will display only when the
message isNew
and not a spacer
. This will cover the entire message with a
block of colour (the colour defined by Slack data) as it animates and eventually
fades to reveal the message underneath. At the moment, the cover has no size
to it, which we will adjust later with animations.
When the animation is done
To achieve our animation we create a lot of artefacts that we need to clean up once our animation is complete.
Open the file view-conversation.component.ts
and add the following method
after the addNewMessage
method.

// Existing code...
export class ViewConversationComponent implements OnInit {
// Existing code...
addNewMessage(messageData: any) {
// Existing code...
}
/**
* Add the new method below
*/
onNewMessageAnimated(event, newMessageInstance) {
const spacerInstanceIndex = this.messages.findIndex(message => message.spacer && message.id === newMessageInstance.id);
if (spacerInstanceIndex > -1) {
this.messages.splice(spacerInstanceIndex, 1);
newMessageInstance.isNew = false;
}
}
// Existing code...
}
We only want the additional spacer message to appear for the duration of our
animation and remove it once the animation is complete. To remove this we call
the
splice
method on the messages
array. We also remove the cover element that's no
longer needed once the message has faded into view. We take this out of the
document flow by setting the isNew
property of the recently animated
message to false
.
We need to hook up our new onNewMessageAnimated
method to be called
upon when the animation becomes complete (or done
). Back in our HTML
template
view-conversation.component.html
add the following line beginning with
(@newMessage.done)="
:
Be sure to move the existing angle bracket >
to this next line in order to
close the element tag with the new attribute.

<div class="view-conversation">
<div class="view-conversation__list">
<div *ngFor="let message of messages"
class="view-conversation__item"
[ngClass]="{'view-conversation__item--you': message.fromYou}"
[ngStyle]="{
'opacity': message.spacer ? 0 : 1,
'z-index': message.spacer ? 0 : 1,
'position': message.isNew ? 'absolute' : 'static'
}">
<span class="view-conversation__time">{{message.sent | date: 'shortTime'}}</span>
<!-- Add the (@newMessage.done) event binding below -->
<div class="view-conversation__message-animation"
[ngStyle]="{'border-color': '#' + message.user.colour}"
[@.disabled]="!message.isNew"
[@newMessage]="!message.spacer || ''"
(@newMessage.done)="message.isNew && onNewMessageAnimated($event, message);"><!-- <-- New event binding -->
<!-- Existing code... -->
</div>
</div>
</div>
The above line references the event @newMessage.done
, which as you may have
guessed calls the function specifed as its value when the newMessage
animation
completes. The call to onNewMessageAnimated($event, message);
from this
event is guarded by the isNew
property check to make sure this will only
fire once for a new message and not for a spacer.