Angular Material Basic Spreadsheet

Spreadsheets are the lifeblood of many organisations. For example, a fruit company might be keeping track of the number of orders for Apples and Oranges over time using a spreadsheet. To scale, the organisation might have chosen to create 2 teams, one that looks after orders for Apples and the other for Oranges. Management wants a wholistic view of orders, so it instructs you to gather sales of Apples and Oranges from the two teams and aggregate the sales in a single spreadsheet. A trail of emails starts with many attached spreadsheets to keep track of the sales. Arguments ensue on whether the spreadsheet was updated correctly, who updated it and when.

What if there is a simpler solution? You decide to try building an internal website that keeps track of the sales. As a starting point, you only want the website to display orders broken down by product (Apples and Oranges) and date. You decide to try to use work that other people have already done for you. In particular, you decide to use @angular/material. To focus your mind on what you want to achieve, you draw up a draft of what you would like the website to look like:

Draft of what the website should look like.

Order Model

The first step is to define a model for an order. A simple model would keep track of the product, the date of the order and the amount that was ordered.
order.model.ts

// order.model.ts
export class Order {
    date: string;
    product: string;
    value: number;
}

Displaying the Order

To get a feeling for the model, you decide to write a component that can display a single Order. For now, it will be fairly basic and only display the value of the order, formatted as a number.
order.component.ts
order.component.html

// order.component.ts
import { Component, OnInit, Input } from '@angular/core';

import { Order } from './order.model';

@Component({
  selector: 'app-order',
  templateUrl: './order.component.html',
  styleUrls: ['./order.component.css']
})
export class OrderComponent implements OnInit {
  @Input() order: Order;

  constructor() { }

  ngOnInit() {
  }

}

The component code is simple, it just accepts an Order as input.

<!-- order.component.html -->
<ng-container *ngIf="order">
  {{ order.value | number }}
</ng-container>

In the template, you decide to check whether the order is truthy (ie. it has actually been passed in) using the *ngIf statement on line 2 and only display the value in that case.

Order Table Model

You anticipate the need to display multiple orders in a table. This table needs to be displayed by the Angular Material table component. To keep the template as simple as possible, you decide to use the following table model.
order-table.model.ts

// order-table.model.ts
import { Order } from './order.model';

export class OrderTable {
    headers: string[];
    rowLabels: string[];
    orders: Order[];
    orderMap: {[header: string]: {[rowLabel: string]: Order}};
}

It is designed to keep track of the orders in the table using the orders member variable. The ordering of the headers and rows is defined by headers and rowLabels member variables, respectively. To link a header and row label to and Order, you use the orderMap member variable.

Order Service

As you still need to convince your management and the teams you work with that the website is a good idea, for demonstration purposes, you decide to create a service that can easily be extended in the future, but that returns a hardcoded table for now.
order.service.ts

// order.service.ts
import { Injectable } from '@angular/core';

import { Order } from './order.model';
import { OrderTable } from './order-table.model';

@Injectable({
  providedIn: 'root'
})
export class OrderService {

  constructor() { }

  getOrderTable(): OrderTable {
    const orders: Order[] = [
      {date: '2000-01-01', product: 'Apples', value: 1000},
      {date: '2000-02-01', product: 'Apples', value: 2000},
      {date: '2000-01-01', product: 'Oranges', value: 10000}
    ];
    return {
      headers: ['2000-01-01', '2000-02-01'],
      rowLabels: ['Apples', 'Oranges'],
      orders: orders,
      orderMap: {
        '2000-01-01': {
          'Apples': orders[0],
          'Oranges': orders[2]
        },
        '2000-02-01': {
          'Apples': orders[1]
        }
      }
    };
  }
}

For the moment it returns 3 static orders, 2 for Apples and 1 for Oranges.

Displaying the Order Table

To be able to use @angular/material, you need to install some packages as shown in the getting started guide. To be able to use the Angular Material table, you need to import the MatTableModule and put it into the imports array of the module which will display the table.

The component code needs to make use of the OrderService to get the order table. It also needs to define the heading for the product name. The component template needs to dynamically generate headings and rows, depending on the number of Orders that need to be displayed.
table.component.ts
table.component.html

// table.component.ts
import { Component, OnInit } from '@angular/core';

import { OrderTable } from './order-table.model';
import { OrderService } from './order.service';

@Component({
  selector: 'app-order-table',
  templateUrl: './order-table.component.html',
  styleUrls: ['./order-table.component.css']
})
export class OrderTableComponent implements OnInit {
  orderTable: OrderTable;
  productHeader = 'Product';
  headers: string[];

  constructor(private orderService: OrderService) { }

  ngOnInit() {
    this.orderTable = this.orderService.getOrderTable();
    this.headers = [this.productHeader].concat(this.orderTable.headers);
  }

}

Line 14 defines the header under which the product will be displayed. In the component initialiser on line 20, the OrderTable is loaded. On line 21 the headers of the table are concentrated with the header for the product.

<!-- order-table.component.html -->
<table mat-table [dataSource]="orderTable.rowLabels" class="mat-elevation-z8">
  <ng-container [matColumnDef]="header" *ngFor="let header of headers">
    <th mat-header-cell *matHeaderCellDef>
      <ng-container *ngIf="header === productHeader">{{ header }}</ng-container>
      <ng-container *ngIf="header !== productHeader">{{ header | date }}</ng-container>
    </th>
    <td mat-cell *matCellDef="let element">
      <ng-container *ngIf="header === productHeader">{{ element }}</ng-container>
      <ng-container *ngIf="header !== productHeader"><app-order [order]="orderTable.orderMap[header][element]"></app-order></ng-container>
    </td>
  </ng-container>

  <tr mat-header-row *matHeaderRowDef="headers"></tr>
  <tr mat-row *matRowDef="let row; columns: headers;"></tr>
</table>

The table template is the most complicated piece of the puzzle. Line 2 defines that the data source for the table are the row labels of the table. This allows you to dynamically generate a row for each row label in the table. Line 14 defines how the header columns are defined and line 15 defines how each row puts data into the columns. Line 3 is the generator expression for each column of the table.

Lines 4-7 define how the header row is generated. Line 5 checks whether the current header is the product header, and if so displays the product header. Line 6 checks the opposite and displays the header after passing it through the date pipe.

Lines 8-11 define how the data rows are generated. Similar to the header row, line 9 checks for the product header and then displays the product. Line 10 checks for the opposite, picks out the Order based on the header and row label using the orderMap and passes on the Order to be displayed by the OrderComponent.

Now you have a working website that you can take to your management and the teams you work with as a showcase. They will probably ask you for additional features, such as being able to edit orders and add new orders to the table. To be able to deliver those more complex features, you also start thinking about how you might write tests for what you have created so far.