Build an Animated Angular v2+ Dashboard

Tendai Mutunhire
👁️ 8,348 views
💬 comments

Angular v2x puts in your hands the capability to develop powerful user interfaces, including animations. In this post, I will show you how you can use this exciting new framework to make cool animations.

We will build a simple stock market dashboard app that showcases different Angular animations. The app will allow a user to get real-time updates on their positions, and we will animate these updates in the user interface to highlight information to the user.

You will get to see how Angular animations can bring your user interface to life.

Setting up Angular 2 and Creating the Project

The code for the app we are making is available on the Github repository.

You can see the final stock dashboard on Plunkr.

To set up development on your local environment, you will want to have the Angular CLI installed, as well as Node.js and npm. On my environment, I'm running Node v6.9.2 as well as Angular 2.3.1.Make sure you use these versions of the tools. Otherwise you will run into problems.

Having installed the Angular CLI, create a new Angular app in a directory of your choice. Execute the command ng new stockdashboard to create the basic app. Angular will take a while as it sets up its dependencies and creates the project.

Run cd stockdashboard to change directory into the project directory.

Run ng serve to start the development server, then open up the development URL in your browser, typically http://localhost:4200. If you get errors, you may be missing npm dependencies, in which case you need to run npm install to install them.

Here is what you should see in your browser.

Hands-On Introduction to Angular v2 Animations

Once you're able to run the project and the main component appears in your browser, you are ready to jump into animating things with Angular.

Angular animations work around triggers and states. A trigger is simply a way of defining a handle that gives rise to differences in the user interface, based on the state, typically some data that we set in the application.

When the state changes from one value to another, we can define an animation to take place as the view changes to reflect the change of state.

Let's see what this looks like in practice. First make sure that your main app module, located at src/app/app.module.ts loads your main component. Here's what the main module should look like.

// src/app.module.ts

// import Angular modules
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

// a local module, our main component
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

In your AppComponent, we define the template and style files. We also import the usual Component functionality from Angular, as well as some new functions that add animation capabilities. These are animate, trigger, state, style and transition. Update your main component to look like the following.

// src/app/app.component.ts

// import animation-related functions
import { Component, trigger, state, style, animate, transition } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  // animations metadata, we define a trigger with two possible states
  animations: [
    trigger('movable', [
      state('fixed', style({

      })),
      state('roaming', style({
        'background-color': 'green',
        'left': '90%'
      })),
    // define a transition from one state to another, and an associated animation  
      transition('* => *', animate('5s 0s ease-in'))
    ])
  ]
})
export class AppComponent {
  title = 'Stock Dashboard';
  moving: string;
  moveIt() {
    console.log('on the move');
    // change the state of moving to roaming, activating the trigger and animation    
    this.moving = 'roaming';
  }
}

The code above shows how we define the trigger, its state, and a transition animation. These may seem like a lot of concepts to grasp, but they are fairly straightforward when you think about how we accomplish CSS animations. The idea is to define a beginning state, an end state, and a duration during which our UI is transformed from old state to new state.

As you can see above, for our trigger, named movable, we are defining two states. The first state, fixed, has no unique style, so it's going to reflect the original styles in our HTML. The second state, named roaming, defines a unique style, changing from whatever background color previously existed to green. Also, the new state changes the position of the elements to which it is applied, shifting them 90% from the left edge.

In our component's HTML, let's define a div and apply this trigger to it so that we can see the animation in action. Update your component's template to look as follows.

<!-- src/app/app.component.html -->
<h1>
  {{title}}
</h1>

<!-- bind to the movable trigger, setting this div's values to the state of the trigger -->
<div class="movable" [@movable]="moving">
  I like to move it, move it.
</div>
<button type="button" class="btn btn-success" (click)="moveIt()">Move It!</button>

Let's examine this file a bit. You'll see that we have included an unusual binding that looks like [@movable]="moving". This is the trigger. The @movable part brings in the trigger we defined in the Component, and by setting it equal to the variable moving, we are setting up Angular to change the state of the trigger based on whatever the value on the right evaluates to. So this means that if moving is set to a string like fixed, then the @movable trigger will trigger the state that corresponds to the value of fixed. Whenever this moving variable changes, it fires off the trigger to update to the corresponding state. What happens if we set it to a state that does not exist? In that case, because the new state to transition to is undefined, no change will take place in the user interface.

Now let's define the initial style for the .movable div. This is just a class name we have defined for CSS so that we can style the div. If you look back at our component definition in app.component.ts, you'll notice that one of the states changes this style.

Here is the code you should add to app.component.css to set the initial style.

/*   src/app.component.css   */
.movable {
  background: coral;
  height: 200px;
  width: 200px;
  text-align: center;
  font-size: 20px;
  color: white;
  font-weight: bold;
  margin-bottom: 10px;
  position: relative;
}

Finally, we should update our index.html file to import Twitter Bootstrap so that our app styles will reflect. Update index.html to look as follows.

<!-- src/index.html -->
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>Stockportfolio</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

<!-- Optional theme -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">

</head>
<body>
  <div class="container">
    <div class="row">
      <app-root>Loading...</app-root>
    </div>
  </div>

</body>
</html>

With the code ready, fire up the server, if you haven't already, with ng serve and let's look at the animation in action.

This is what it looks like before.

Here is a https://embed.plnkr.co/NOYpMexOicpMICkiIzTf/ showing the animation. When you click on the "Move it" button, the orange box moves across the page and gradually changes color to green.

At the end of the animation, this is what we get.

The Stock Market Service

Now that you're familiar with basic animations, we'll quickly walk through applying these concepts in a larger application.

First, let's develop a stock service which provides updated stock prices for our application. Angular encourages the use of separate services to handle dedicated functionality like this. The result is decoupled, easier-to-debug, and overall less buggy code.

Angular's generators help us generate things like services and components with ease. To generate our service, make sure you're in the root directory of the project, then run ng generate service stock.

You should see an update that the generator created files, a service file, and a test file. Here's what your service file will look like.

// src/app/stock.service.ts
import { Injectable } from '@angular/core';

// make our stock service injectable
@Injectable()
export class StockService {

  constructor() { }

}

We'll want to update the constructor of the service to list some stocks right off the bat, as follows.

// src/app/stock.service.ts
...
constructor() {
    // define the stocks within the service, each has a symbol, price, and other characteristics
    StockService.stocks = [
                    { symbol: "GOOG", price: 120, compute: StockService.uptrend, bought_price: 50, quantity: 0, total_value: 21000, trend: 'UP' },
                    { symbol: "YHOO", price: 100, compute: StockService.uptrend, bought_price: 100, quantity: 0, total_value: 21000, trend: 'UP' },
                    { symbol: "MSFT", price: 20, compute: StockService.uptrend, bought_price: 120, quantity: 0, total_value: 21000, trend: 'UP' },
                    { symbol: "AAPL", price: 200, compute: StockService.uptrend, bought_price: 85, quantity: 20, total_value: 21000, trend: 'UP' },
                  ];

  }
  ...

Note that we will need to define the stocks property near the top of the StockService class. We will also need functions to increase and decrease stock prices, as well as the public interface to the class, presented by the function getStockPrices. To the outside world, all the functionality of the class will be boiled down to that function. But where does the service get stock prices? Typically you would call out to an HTTP REST API, or access fresh stock data from a stock-broker's server via web sockets. Here we will have a function, named computePrices, which simulates the stock market. To call out to an actual stock market service, you can proxy this service and modify it to delegate to your service of choice.

With all the above functions implemented, your StockService should look as follows.

// src/app/stock.service.ts

import { Injectable } from '@angular/core';

@Injectable()
export class StockService {
  static stocks: any;

  // return the prices of all stocks, the main interface to our service    
  getStockPrices = () => {
    StockService.computePrices(StockService.stocks);
    return Promise.resolve(StockService.stocks);
  }

  // simulate a stock market price increase, increasing price and setting an up trend    
  static incrementPrice = (stock, index) => {
    var price = stock.price;
    price += 5;
    if (price > 120) {
      price = 120;
    }
   // when a stock is in a price increase pattern, 
   // its compute property is a function that raises price over time    
    StockService.stocks[index].compute = StockService.uptrend;
    StockService.stocks[index].trend = 'UP';
    StockService.stocks[index].price = price;
  }

 // simulate a stock market price decrease
  static decreasePrice = (stock, index) => {
    var price = stock.price;
    price -= 5;
    if (price < 0) {
      price = 0;
    }
   // in this case, once a stock price is falling, the stock enters a downtrend, decreasing price over time
    StockService.stocks[index].compute = StockService.downtrend;
    StockService.stocks[index].trend = 'DOWN';
    StockService.stocks[index].price = price;
  }

  static computePrices = (input) => {
    console.log('computing new prices');
    input.forEach(function(stock, index) {
     // stocks are range-bound, once they hit a max price, they begin to fall    
      if (stock.price >= 120) {
        StockService.decreasePrice(stock, index);
      }
      // likewise, a stock can't fall below 0, at that extremity its price begins to rise      
      if (stock.price <= 0) {
        StockService.incrementPrice(stock, index)
      }

      input[index].price = stock.compute.call(null, stock);
    });
    return input;
  }

 // an uptrend is defined by a pattern of gentle increases in price by $5
  static uptrend = (stock) => {
    return stock.price + 5;
  }

 // a downtrend shows price decreasing by 5 with successive ticks
  static downtrend = function(stock) {
    return stock.price - 5;
  }

  constructor() {
   // initially, stocks are trending up, but they start with different prices
    StockService.stocks = [
                    { symbol: "GOOG", price: 120, compute: StockService.uptrend, bought_price: 50, quantity: 0, total_value: 21000, trend: 'UP' },
                    { symbol: "YHOO", price: 100, compute: StockService.uptrend, bought_price: 100, quantity: 0, total_value: 21000, trend: 'UP' },
                    { symbol: "MSFT", price: 20, compute: StockService.uptrend, bought_price: 120, quantity: 0, total_value: 21000, trend: 'UP' },
                    { symbol: "AAPL", price: 200, compute: StockService.uptrend, bought_price: 85, quantity: 20, total_value: 21000, trend: 'UP' },
                  ];
    console.log("Stocks from the stock service ", StockService.stocks.toString());

  }

}

In the stock service code above, you will see that we are annotating the class with @Injectable(). Make sure to input this annotation correctly with the brackets at the end. Otherwise you'll run into errors. If you have used Angular1 or have used Java or C#, you will likely be familiar with the dependency injection pattern. If not, fear not, dependency injection is a way of providing a class with a piece of isolated functionality that is implemented inside another class.

A good way to think of this is by way of an illustration. Let's use James Bond as an example. Have you ever noticed that James Bond is not the guy who makes all the cool technology that he ends up using on any mission? Before he gets that technology, he is a regular, relatively harmless guy.

But before each mission, he goes to see the tech guy, Q, who whips up some cool gizmo and then hands that over to Bond. With the tech in hand, Bond is now ready to go; he is equipped. This is the essence of dependency injection. Bond doesn't know how Q makes the gizmos, or where he gets them, all he knows is Q will provide the tech when he needs it.

This is what injecting accomplishes. The annotation makes our StockService an injectable class. Think of the service as Q, and consumers of the service as a James Bond that needs the isolated functionality provided by the service.

So in our case, we will be injecting the StockService into our main component, AppComponent. The AppComponent will then be able to call the getStockPrices method of the service to retrieve fresh stock prices.

Updating Our Component With Stock Data

Now that we have stock data available from our service let's make it available to our app component.

We will update our AppComponent's constructor to accept the injectable StockService. The constructor will look like this.

// src/app/app.component.ts
...
constructor(stockService: StockService) {
   // set the stockService received in the dependency injection
    this.stockService = stockService;
  }
  ...

You will see errors at this point since we haven't declared the StockService as a member of this component class. We will fix that shortly. However, first, I would like you to examine the method theAppComponent will use to access the data from the StockService.

// src/app/app.component.ts
...
getStockPrices() {
    // call out asynchronously to the stock service for prices, then update prices once data is received back 
    this.stockService.getStockPrices().then(prices => {
      console.log('just got prices: ', prices);
      this.stockPrices = prices;
    });
  }
  ...

This getStockPrices inside the component makes use of the injected service. Notice that it uses the asynchronous Promise notation .then. There is a difference between calling a synchronous method, which would be called as:

let prices = this.stockService.getStockPrices(); //synchronous

Versus the async method above. This is because in our service method definition, getStockService returns a Promise. Using Promises is a good approach when designing methods that need to do things like fetch data via http, or other asynchronous tasks. When the data is available, we then resolve the Promise and update the caller with the received data.

Now we can fill in the rest of the AppComponent class. Add imports and missing methods to make your AppComponent look as follows.

// src/app/app.component.ts

// import the animation functions alongside other Angular imports
import { Component, trigger, state, style, animate, transition } from '@angular/core';

import { StockService } from './stock.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [StockService]
})
export class AppComponent {
  title = 'Stock Dashboard';
  stockPrices = [];
  stockService: StockService;
  constructor(stockService: StockService) {
    this.stockService = stockService;
  }

  getStockPrices() {
    this.stockService.getStockPrices().then(prices => {
      console.log('just got prices: ', prices);
      this.stockPrices = prices;
    });
  }

 // once the component is created, poll the stock service regularly for fresh prices
  ngOnInit() {
    // check for updated prices
    setInterval(() => {this.getStockPrices(); }, 1000);
  }

}

We will be listing stock data in a table. Let's update the styles for our AppComponent to target table cells. Your AppComponent's style file should look as follows.

/*   src/app.component.css   */
.movable {
  background: coral;
  height: 200px;
  width: 200px;
  text-align: center;
  font-size: 20px;
  color: white;
  font-weight: bold;
  margin-bottom: 10px;
  position: relative;
}

tr {
  color: white;
  background: fuchsia;
}

th {
  color: white;
}

Price Updates and Stock Trends

Now we are ready to list stock data in our component's HTML. Update app.component.html to look as follows.

<!-- src/app/app.component.html -->
<h1>
  {{title}}
</h1>

<!--top-Header-menu-->
<ul class="nav nav-pills">
  <li role="presentation" class="active"><a href="#">Stock Summary</a></li>
  <li role="presentation"><a href="#">Add New</a></li>
  <li role="presentation"><a href="https://github.com/tranc99/stockportfolio" target="_blank">About StockDashboard</a></li>
</ul>

<hr>
<h2>Welcome, here are the latest updates on your stock picks.</h2>
<br>

<div class="row-fluid">
  <div class="widget-box">
          <div class="widget-content nopadding">
            <table class="table table-bordered data-table">
              <thead>
                <tr>
                  <th>Stock Symbol</th>
                  <th>Price</th>
                  <th>Bought At</th>
                  <th>Quantity</th>
                  <th>Total Value</th>
                  <th>Current Trend</th>
                </tr>
              </thead>
              <tbody>
          <!-- loop through all stock price data, and list them in a row, binding the trending trigger to the value of the stock's trend property -->      
                <tr class="gradeX" *ngFor="let stock of stockPrices" [@trending]="stock.trend">
                  <td>{{stock.symbol}}</td>
                  <td>USD {{stock.price}}</td>
                  <td>{{stock.bought_price}}</td>
                  <td class="center">{{stock.quantity}}</td>
                  <td>{{stock.price * stock.quantity}}</td>
                  <td>{{stock.trend}}</td>
                </tr>
              </tbody>
            </table>
          </div>
        </div>
</div>

The most important part for you to focus on is the *ngFor inside our tr definition. We are using this directive to loop through all stockPrices in our component, which it received from the StockService, and listing each one here in a row of the table.

Notice also we have declared a trigger [@trending], and we are updating its state to reflect the value of our stock's stock.trend attribute.

Stocks in our case can have an UP trend or a DOWN trend, based on the current behavior of the stock price. In the real market, stock analysts typically assign these trends based on sentiment analysis and other esoteric techniques. Here, our StockService just flags a downtrend or uptrend based on data that it controls.

This [@trending] trigger will need to be defined in our component file to work and start using the animations. We do this next.

Animating Changes In Trend And Price

Remember how we defined the triggers, states, and animations in the introduction to Angular2 animations? If so, then you will be somewhat familiar now with the exact steps we need to follow.

We want to define two states, an UP state and a DOWN state.

The 'UP' state activates when a stock is trending upwards. The 'DOWN' state activates when the stock is headed down.

For the 'UP' state, we will define a style where the rows of the stock table are green. We will make it easy for the user to distinguish this data from that for a stock whose trend is 'DOWN', which we indicate with a row style of color red.

To define these states and the trigger, add the following lines to our AppComponent's metadata, the part of the component prefaced with the @Component decorator.

// src/app/app.component.ts

...
animations: [
    trigger('trending', [
      // define a down state, with a background style of red to distinguish it from an up state    
      state('DOWN', style({
        'background-color': 'red'
      })),
      // define an up state, with a less jarring green color       
      state('UP', style({
        'background-color': 'green'
      })),
      // when a stock changes from up to down, animate the change over 3 seconds
      transition('UP => DOWN', [
        style({transform: 'translateX(-100%)'}),
        animate(3000)
      ]),
      // when a stock changes from down to up, animate vertically, over 200ms      
      transition('DOWN => UP', [
        style({transform: 'translateY(-100%)'}),
        animate(200)
      ])
    ])
  ],
...

You will notice that we are defining a transition from 'UP' to 'DOWN', and animating the change of style over 3000 milliseconds. We translate the row -100% along the X-axis, which in CSS means 'move this 100% to the left'. Mozilla's reference gives you a bit more information on transforms.

This animation style is different from that for the transition from 'DOWN' to 'UP'. For this second transition, we animate along the Y-Axis instead, making it easy to distinguish what has just happened to the stock price based merely on a visual cue of the animation. Once the user sees the two animations, he no longer has to look at the actual price to be able to tell that a stock is going up, or down. Each animation is associated with only one of the two states. For your apps, you will need to think about which sorts of animations make sense.

Your entire AppComponent will look like so.

// src/app/app.component.ts

import { Component, trigger, state, style, animate, transition } from '@angular/core';

import { StockService } from './stock.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  animations: [
    trigger('trending', [
      state('DOWN', style({
        'background-color': 'red'
      })),
      state('UP', style({
        'background-color': 'green'
      })),
      transition('UP => DOWN', [
        style({transform: 'translateX(-100%)'}),
        animate(3000)
      ]),
      transition('DOWN => UP', [
        style({transform: 'translateY(-100%)'}),
        animate(200)
      ])
    ])
  ],
  providers: [StockService]
})
export class AppComponent {
  title = 'Stock Dashboard';
  stockPrices = [];
  stockService: StockService;
  constructor(stockService: StockService) {
    this.stockService = stockService;
  }

  getStockPrices() {
    this.stockService.getStockPrices().then(prices => {
      console.log('just got prices: ', prices);
      this.stockPrices = prices;
    });
  }

  ngOnInit() {
    // check for updated prices
    setInterval(() => {this.getStockPrices(); }, 1000);
  }

}

Back in your browser, you should be able to see these animations in action as your stock prices automatically update.

Here is a picture of the animation in progress for a transition from 'UP' to 'DOWN'.

And here is an image of stocks all in the 'UP' state.

Here is a demo of the final dashboard, available on Plunkr. As new stocks are received from the StockService, the price updates continuously. We animate each state change to show when the trend has changed. This makes our dashboard active and continuously up-to-date. You could apply the same concept and animations in a weather app, a chat application or other apps that work with real-time data.

Further Adventures With Angular v2+ Animations

Pat yourself on the back. You've come pretty far in your journey.

We did an initial hands-on exercise to get you acquainted with Angular 2's animation support. We then moved into a more ambitious app that uses animations to show updates to a stock market dashboard.

In the course of both exercises, you learned how to define triggers in your components. You learned how to set the states for your triggers based on user interactions with the app. Also, you learned how to define styles that were tied to each possible state of your triggers. You then learned how to define animations for transitioning from different states. Now you should be capable of jumping in and animating your projects with Angular 2+.

For further adventures, you should check out things like keyframes, the void state, and other related concepts. Let us know in the comments how you like Angular 2 animations.

Tendai Mutunhire

1 post

I'm a JavaScript and Elixir web developer. In my spare time I like to read thrillers and noir fiction. Ken Bruen is my favorite fiction writer.