Export data to excel in Angular.

In this post we will try to learn how to export data (JavaScript array or data) to an excel file using the SheetJS / XLSX library.

Steps:
  • Create the angular app.
  • Add the dependencies, like, xlsx.
  • Create the table in UI.
  • Logic to export data in the table to excel file.
Create the angular app

We are going to use the angular guide to create a new angular project.

ng new ng-xlsx

Now we have our angular application ready, let’s try to add other dependencies to show table data on the app component and also add the xlsx library.

Add the dependencies
npm i bootstrap lodash xlsx --save

Here we are installing few packages, bootstrap (To display the data in a table), lodash (For performing data manipulation), xlsx (SheetJS library)

We also need @types for lodash, so let’s install that too.

npm i @types/lodash --save-dev
Create the data table

First, we need to import the bootstrap CSS framework into our Angular project so that we can see the styled components / table on the page. Open styles.scss file and add below code.

@import './node_modules/bootstrap/scss/bootstrap';

html,
body {
    font-size: 12px !important;
    margin: 0;
    overflow: hidden;
    width: 100vw !important;
    height: 100vh !important;
}

.page-container {
    padding: 20px;
}

Now open the app component template file (app.component.html) and paste below code, which displays our table.

<div class="page-container">
  <h3>
    {{ title }}
  </h3>
  <div class="row">
    <div class="col-md-12">
        <table class="table table-striped">
            <thead>
                <tr>
                    <th scope="col">#</th>
                    <th scope="col">First</th>
                    <th scope="col">Last</th>
                    <th scope="col">Handle</th>
                </tr>
            </thead>
            <tbody>
                <tr *ngFor="let item of users">
                    <th scope="row">{{ item.uid }}</th>
                    <td>{{ item.first }}</td>
                    <td>{{ item.last }}</td>
                    <td>{{ item.handle }}</td>
                </tr>
            </tbody>
        </table>
        <button type="button" class="btn btn-primary" (click)="exportRawDataToExcel(users, title)">Export to Excel</button>
    </div>
  </div>
</div>

And our data in the table is as below, we need to paste it inside the app component script file (app.component.ts).

// Add import statements
import * as XLSX from "xlsx";
import * as _ from "lodash";

// Add below code to the class
title = "ng-xlsx";

users = [
    {
      uid: "1",
      first: "Mark",
      last: "Otto",
      handle: "@mdo",
    },
    {
      uid: "2",
      first: "Jacob",
      last: "Thornton",
      handle: "@fat",
    },
    {
      uid: "3",
      first: "Larry the Bird",
      last: "Thornton",
      handle: "@twitter",
    },
  ];

Run the Angular application.

npm start

Open the app on the browser, http://localhost:4200/ and we should see our table as below.

Table
Export data to excel file

We have our data ready, now let’s write the logic to download this table data to the excel file.

exportRawDataToExcel(tableData: any, fileName: string) {
    if (!tableData || (tableData && tableData.length === 0)) {
      throw new Error("No data to export");
    }
    const wb = XLSX.utils.book_new();
    const data = [];
    const merges = [];
    const headers = [];
    const keys: string[] = _.keys(_.head(tableData));
    let mergeAcrossStartC = 0;
    for (let m = 0; m < keys.length; m++) {
      merges.push({
        s: { r: 0, c: mergeAcrossStartC },
        e: {
          r: 0,
          c: mergeAcrossStartC,
        },
      });
      mergeAcrossStartC = mergeAcrossStartC + 1;
      headers.push(keys[m]);
    }
    data.push(headers);
    for (let j = 0; j < tableData.length; j++) {
      const rowData = [];
      for (let k = 0; k < keys.length; k++) {
        rowData.push(tableData[j][keys[k]]);
      }
      data.push(rowData);
    }
    const ws = XLSX.utils.aoa_to_sheet(data);

    ws["!cols"] = [];
    _.forEach(headers, (val) => {
      ws["!cols"].push({ wpx: 120 });
    });
    ws["!merges"] = merges;
    /**
     * Add worksheet to workbook
     */
    XLSX.utils.book_append_sheet(wb, ws, "Sheet 1");
    XLSX.writeFile(wb, fileName + ".xlsx");
  }

Above code will be overwhelming, so I would suggest to investigate the SheetJS tool and learn few things. But to summarise…

  • The function takes two parameters, first the raw data (JSON / array) and second the file name. The raw data is exported to the excel file and we name the excel file with the given file name in the function parameter.
  • We parse the data to construct into a format that is needed for the xlsx library.
  • Using few functions in the library, like,
    • XLSX.utils.book_new() – Creates a new work book.
    • XLSX.utils.aoa_to_sheet() – Create a new work sheet with the given data (JS array)
    • XLSX.utils.book_append_sheet(wb, ws, “Sheet 1”) – Adds a work sheet to the work book.
    • XLSX.writeFile(wb, fileName + “.xlsx”) – Finally create the excel file.

I guess this is it, when we click the export button below the table we should see a new .xlsx file downloaded with the data as seen in the table!!.

Checkout complete code on Github.

Created my first react application.

I call myself a Web developer but in reality there are many more frameworks and libraries out there that I need to learn so to better myself. One such library is React.

React is very popular, you can tell by looking at the trend below in comparison with Angular.

React vs Angular
So, I started learning react…It’s been few days…and I decided to create my very first react application. It’s that simple.

There are many posts on the web talking about Why react is popular?, two posts I actually like are, this one and this one.

One thing I like about react is its…

Simplicity

Checkout my first react application on Github and let me know what you think!.

I have started another react application called billing to learn more or practise. But I suppose its going to take me some time to finish this application given that I only spend like few hours in a week on side projects on Github.

Export html content to PDF file in Angular using JSPDF.

We already have two posts on using JSPDF in Angular application and also how to export a table to PDF. Check these posts first.

Now to export html content.!

Let’s look at the docs first, and here we have a method called html which takes a HTMLElement as the first arg and few config options as the second arg. So, we can try using this method in our Angular application to export any html content, right?.

Say, for example we have our html content in the template file of our Angular application like below.

<div
    #rentReceiptsDiv
    class="rent-receipts"
    *ngIf="resultsCalculated && !errorModel.errorMsg"
>
    <div class="receipt" *ngFor="let item of receiptDateRanges">
        <div class="row">
            <h3>Rent receipt</h3>
        </div>
        <div class="row pt-2">
            <h6 class="fw-light">
                from {{ item.fromDate | date : 'dd/MM/yyyy' }} to
                {{ item.toDate | date : 'dd/MM/yyyy' }}
            </h6>
        </div>
        <div class="row pt-3">
            <div class="fs-6" [innerHTML]="getDescriptionForPreview(item.fromDate, item.toDate)"></div>
        </div>
        <div class="row pt-5">
            <div class="fs-6">Signature</div>
            <div class="fs-5">{{ _calculatorInputs?.ownerName }}</div>
            <div class="fs-6">{{ _calculatorInputs.ownerPANNumber }}</div>
        </div>
    </div>
</div>

We want to export the above Html content to the PDF file. But the html method in the JSPDF is expecting HTMLElement so to get this object instance of our html content we need to declare the above div element in our component by referencing the id rentReceiptsDiv, so let’s do that.

@ViewChild('rentReceiptsDiv') rentReceiptsDiv: ElementRef;

The ViewChild decorator queries our html template file for the provided id and maintains the reference to it. So, now we have the element reference of our div element that we want to export to PDF. Now, let’s try to export using the html method of the JSPDF library.

downloadReceipts() {
        const fileName = `${this.title}.pdf`;
        const doc = new jsPDF();
        doc.html(this.rentReceiptsDiv.nativeElement as HTMLElement, {
            callback: (d) => {
                d.save(fileName);
            },
            margin: 0,
            autoPaging: true,
            width: 200,
            windowWidth: this.rentReceiptsDiv.nativeElement.clientWidth,
            x: 5,
            y: 0,
        });
    }

As you can see I am making use of the callback function of the html method to export the PDF (save) once it’s ready. I am also passing the div Html element in to the function.

There are also few Html config options you need to be aware of.

Below is the html function definition from the docs.

// jsPDF plugin: html
html(src: string | HTMLElement, options?: HTMLOptions): HTMLWorker;

Also the HTMLOptions definition is as below…We are making use of few of these options, like callback, width, windowWidth. You may have to try few times with these options to get the html rendered correctly.

export interface HTMLOptions {
    callback?: (doc: jsPDF) => void;
    margin?: number | number[];
    autoPaging?: boolean | "slice" | "text";
    filename?: string;
    image?: HTMLOptionImage;
    html2canvas?: Html2CanvasOptions;
    jsPDF?: jsPDF;
    x?: number;
    y?: number;
    width?: number;
    windowWidth?: number;
    fontFaces?: HTMLFontFace[];
}

Checkout complete code base here.

Create Heat map using d3.js with Angular.

Checkout the previous post to get started with d3.js in Angular application.

In this post we will talk about creating a heat map chart using the capabilities of d3.js.

Just like any chart or graph, heat map is also a charting technique to visualize data in two dimensions w.r.t magnitude of an occurrence of an event represented with a color schema, typically the color schema varies from low to high depending on the magnitude.

Heat map

Create a new component.

import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import * as d3 from 'd3v6';
import * as _ from 'lodash';
import {
    HeatMap_ColorRange,
    HeatMap_DomainRange,
    HeatMap_Groups,
    HeatMap_Variables,
} from '../commom/constants';
import { HeatMapDataItemInterface } from '../commom/interfaces/heat-map-data-item.interface';
import { ChartsDataService } from '../commom/services/charts-data.service';

@Component({
    selector: 'app-heat-map',
    templateUrl: './heat-map.component.html',
    styleUrls: ['./heat-map.component.scss'],
})
export class HeatMapComponent implements OnInit {
    @ViewChild('chartContainer', { static: true }) chartContainer: ElementRef;
    @Input() chartId: string = 'heatmap';
    @Input() width: number = 600;
    @Input() height: number = 450;
    _chartData: HeatMapDataItemInterface[];

    private margin = { top: 40, right: 20, bottom: 20, left: 40 };
    private _chart = {
        svg: null,
        mainContainer: null,
        data: [],
        daysCount: 30,
        datesStrAsPerRange: '',
    };

    element!: HTMLElement;

    constructor(private service: ChartsDataService) {
        this._chart.data = service.getHeatMapData();
    }

    ngOnInit(): void {
        this.createHeatmap();
    }

    createHeatmap() {
        // set the dimensions and margins of the graph
        this.width =
            this.chartContainer.nativeElement.getBoundingClientRect().width;
        const widgetWidth =
            (this.chartContainer.nativeElement.getBoundingClientRect().width *
                60) /
            100;
        let heatMapHeight = Math.round(widgetWidth / 3.167);
        this.width = widgetWidth - this.margin.left - this.margin.right;
        this.height = heatMapHeight - this.margin.top - this.margin.bottom;
        const legendWidth = 80;

        /**
         * Remove all the elements in d3 charts if already exist
         */
        d3.select('#' + this.chartId)
            .selectAll('*')
            .remove();
        d3.select('#' + this.chartId + '_legend')
            .selectAll('*')
            .remove();

        /**
         * Mouse over function handler for the treemap nodes
         * Here we highlight the circle with the box shadow css
         */
        const mouseover = function () {
            d3.select(this).style('stroke', 'black');
        };

        /**
         * Mouse out handler
         */
        const mouseout = function () {
            d3.select(this).style('stroke', 'none');
        };

        // append the svg object to the body of the page
        const svg = d3
            .select('#' + this.chartId)
            .append('svg')
            .attr('width', this.width + this.margin.left + this.margin.right)
            .attr('height', this.height + this.margin.top + this.margin.bottom)
            .append('g')
            .attr(
                'transform',
                'translate(' + this.margin.left + ',' + this.margin.top + ')'
            );

        let groupNames = Object.values(HeatMap_Groups);
        let variableNames = Object.values(HeatMap_Variables);

        // Build X scales and axis:
        const x = d3
            .scaleBand()
            .range([0, this.width])
            .domain(groupNames)
            .padding(0.05);
        svg.append('g')
            .style('font-size', 12)
            .style('color', '#ADADAD')
            .attr('transform', 'translate(0, -5)')
            .call(d3.axisTop(x).tickSize(0))
            .select('.domain')
            .remove();

        // Build Y scales and axis:
        const y = d3
            .scaleBand()
            .range([this.height, 0])
            .domain(variableNames)
            .padding(0.05);
        svg.append('g')
            .style('font-size', 12)
            .style('color', '#ADADAD')
            .attr('transform', 'translate(-5, 0)')
            .call(d3.axisLeft(y).tickSize(0))
            .select('.domain')
            .remove();

        const myColor = d3
            .scaleLinear<string>()
            .range(HeatMap_ColorRange)
            .domain(HeatMap_DomainRange);

        // add the squares
        const cards = svg
            .selectAll()
            .data(this._chart.data, function (d) {
                return d.group + ':' + d.variable;
            })
            .enter()
            .append('rect')
            .attr('x', function (d) {
                return x(d.group);
            })
            .attr('y', function (d) {
                return y(d.variable);
            })
            .attr('rx', 4)
            .attr('ry', 4)
            .attr('value', function (d) {
                return d.value;
            })
            .attr('width', x.bandwidth())
            .attr('height', y.bandwidth())
            .attr('fill', 'white')
            .style('fill', function (d) {
                if (d.value === null || isNaN(d.value)) {
                    return '#C3C3C3';
                }
                return myColor(d.value);
            })
            .style('stroke-width', 2)
            .style('stroke', 'none')
            .style('opacity', 1)
            .on('mouseover', mouseover)
            .on('mouseout', mouseout)
            .on('mouseleave', mouseout);

        // append the legend svg object to the body of the page
        const legend = d3
            .select('#' + this.chartId + '_legend')
            .append('svg')
            .attr(
                'transform',
                'translate(' +
                    +(
                        this.width -
                        HeatMap_ColorRange.length * legendWidth +
                        this.margin.left
                    ) +
                    ',0)'
            )
            .attr('width', legendWidth * HeatMap_ColorRange.length)
            .attr('height', 35);

        const domainRange: string[] = HeatMap_DomainRange.map(
            (item, index, items) => {
                const domain = '' + item;
                if (index === 0) {
                    return '>' + domain;
                } else if (index === items.length - 1) {
                    return '<' + domain;
                }
                return '';
            }
        );
        legend
            .selectAll()
            .data(domainRange, function (d) {
                return d;
            })
            .enter()
            .append('g')
            .attr('class', 'g-legend')
            .append('rect')
            .attr('x', function (d, i) {
                return legendWidth * i;
            })
            .attr('y', 20)
            .attr('rx', 0)
            .attr('ry', 0)
            .attr('width', legendWidth)
            .style('width', '' + legendWidth + 'px')
            .attr('height', 10)
            .style('height', '10px')
            .style('fill', function (d, i) {
                return '' + HeatMap_ColorRange[i];
            });

        legend
            .selectAll('.g-legend')
            .append('text')
            .attr('class', 'g-legend-text')
            .text(function (d) {
                return '' + d;
            })
            .attr('x', function (d, i) {
                if (i === 0) {
                    return 0;
                }
                return legendWidth * i;
            })
            .attr('y', 10)
            .style('font-size', 11)
            .style('fill', '#ADADAD');

        legend.exit().remove();
    }
}

Code is pretty much self explanatory, there are two scales and axis (x, y) and the data is represented with groups, variables and domain range (color schema).

Check out the complete Angular project code on GitHub repo.

Hope you like the post!.

In the next post I will be trying transitions (animations), responsiveness and tooltips for the bar chart, so be sure to check it out.

Developing charts using d3.js in Angular.

d3.js is one of the popular library to draw charts as per your requirements or needs. The library gives the developer with many options or freedom to draw the charts. Their gallery has many incredible examples and the community around this library is also quite impressive.

Its super easy to get started. First off you need some basic knowledge about SVG and CSS. There are many posts or articles out there for you to explore. But in this post I will be talking about using d3.js to develop a simple bar chart in Angular application.

Dependencies
  1. A sample angular project to get started or use @angular/cli to create one, again many posts available online to do this.
  2. Install D3 packages.
    1. $npm i d3
    2. $npm i -D @types/d3
Bar chart

Create a new bar chart component $ng g c BarChart. A bar chart typically has two axis (X, Y) and the bars.

import {
    Component,
    OnInit,
    Input,
    ChangeDetectionStrategy,
    ChangeDetectorRef,
    ViewChild,
    ElementRef,
} from '@angular/core';
import * as d3 from 'd3v6';
import cloneDeep from 'lodash/cloneDeep';
import { DataItemInterface } from '../commom/interfaces/data-item.interface';
import { ChartsDataService } from '../commom/services/charts-data.service';

@Component({
    selector: 'app-bar-chart',
    templateUrl: './bar-chart.component.html',
    styleUrls: ['./bar-chart.component.scss'],
    changeDetection: ChangeDetectionStrategy.OnPush,
})
export class BarChartComponent implements OnInit {
    @ViewChild('chartContainer', { static: true }) chartContainer: ElementRef;
    @Input() chartId: string = 'bar';
    @Input() width: number = 600;
    @Input() height: number = 450;
    _chartData: DataItemInterface[];

    private margin = { top: 20, right: 20, bottom: 35, left: 40 };
    private _chart = {
        svg: null,
        xAxis: null,
        yAxis: null,
        xScale: null,
        yScale: null,
        x: null,
        y: null,
        mainContainer: null,
        bars: null,
    };

    get barWidth(): number {
        {
            return this.width - this.margin.left - this.margin.right;
        }
    }

    get barHeight(): number {
        {
            return this.height - this.margin.top - this.margin.bottom;
        }
    }

    constructor(
        private _cd: ChangeDetectorRef,
        private dataService: ChartsDataService
    ) {
        this._chartData = dataService.getData(22);
        this._cd.markForCheck();
    }

    ngOnInit(): void {
        this.initChart();
        this.draw();
        this._cd.markForCheck();
    }

    @Input()
    set chartData(items: DataItemInterface[]) {
        if (items) {
            this._chartData = cloneDeep(items);
            this.draw();
        }
    }

    get chartData(): DataItemInterface[] {
        return this._chartData;
    }

    private initChart() {
        this._chart.svg = d3.select('#bar').select('svg');
        this._chart.xScale = d3.scaleBand();
        this._chart.yScale = d3.scaleLinear();
        this.setSVGDimensions();
        this._chart.mainContainer = this._chart.svg
            .append('g')
            .attr(
                'transform',
                `translate(${this.margin.left}, ${this.margin.top})`
            );
        this._chart.y = this._chart.mainContainer
            .append('g')
            .attr('class', 'axis axis--y');
        this._chart.x = this._chart.mainContainer
            .append('g')
            .attr('class', 'axis axis--x');
    }

    private setSVGDimensions() {
        const rect = this.chartContainer.nativeElement.getBoundingClientRect();
        this.width = rect.width;
        this._chart.svg.style('width', this.width).style('height', this.height);
    }

    private draw() {
        this.setAxisScales();
        this.drawAxis();
        this.drawBars();
    }

    private setAxisScales() {
        this._chart.xScale = d3.scaleBand();
        this._chart.yScale = d3.scaleLinear();

        this._chart.xScale
            .rangeRound([0, this.barWidth])
            .padding(0.1)
            .domain(this.chartData.map((d) => d.name));
        this._chart.yScale
            .range([this.barHeight, 0])
            .domain([0, Math.max(...this.chartData.map((x) => x.value))]);
        this._chart.xAxis = d3.axisBottom(this._chart.xScale);
        this._chart.yAxis = d3.axisLeft(this._chart.yScale);
    }

    private drawAxis() {
        this._chart.y
            .attr('transform', `translate(0, 0)`)
            .call(this._chart.yAxis);
        this._chart.x
            .attr('transform', `translate(0, ${this._chart.yScale(0)})`)
            .call(this._chart.xAxis);
    }

    private drawBars() {
        const calcBarY = (yPos) => {
            return !yPos ? this._chart.yScale(0) - 1 : this._chart.yScale(yPos);
        };
        const calcBarHeight = (yPos) =>
            Math.max(0, this._chart.yScale(0) - calcBarY(yPos));

        this._chart.bars = this._chart.mainContainer
            .selectAll('.bar')
            .remove()
            .exit()
            .data(this.chartData)
            .enter()
            .append('rect')
            .attr('class', 'chart-bar');

        this._chart.bars
            .attr('x', (d) => this._chart.xScale(d.name))
            .attr('y', (d) => this._chart.yScale(d.value))
            .attr('width', this._chart.xScale.bandwidth())
            .attr('height', (d) => calcBarHeight(d.value));
    }
}

The above code is basically self-explanatory, try changing things and explore, and see below the chart we get.

Let’s look at our bar chart template code.

<div class="bar-chart" #chartContainer>
    <div class="bar-chart__main" id="bar">
        <svg></svg>
    </div>
</div>

And finally some styling…

.bar-chart {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;

    &__main {
        flex: 1;
        position: relative;

        ::ng-deep svg {
            $lineColor: #6f7583;
            outline: none;

            .axis {
                path,
                line {
                    fill: none;
                    stroke: $lineColor;
                    stroke-width: 0.5;
                    shape-rendering: crispEdges;
                }

                text {
                    fill: #c3c3c3;
                    stroke: #c3c3c3;
                    stroke-width: 0.5;
                }
            }

            .chart-bar {
                outline: none;
                stroke: transparent !important;
                fill-opacity: 1 !important;
            }
        }
    }
}

Check out the complete Angular project code on GitHub repo.

Conclusion

d3.js is a powerful library but there is also a steep learning curve and you could easily get lost when trying to debug or find some solution. Best recommend approach is to go over the docs first and understand how things work under the hood.

Hope you like the post!.

In the next post I will be trying transitions (animations), responsiveness and tooltips for the bar chart, so be sure to check it out.

Developed mobile application using Ionic and Angular.

Ionic framework lets you develop mobile applications with single code base for multiple platforms. It basically lets us build mobile experiences with open web.

This is something very cool.

So, I took some time out and developed one Android mobile application.

App – https://play.google.com/store/apps/details?id=com.ninetyninet.hogenakkal_falls

Check it out and please let me know.

Github code at – https://github.com/kumargandhi/hogenakkal-falls

Features
  • Browse through exotic collection of photos.
  • Google maps integration, showing location on map.
  • Social sharing.
  • AdMob integration.

Using forkJoin in Angular to perform multiple HTTP requests but wait until all done.

forkJoin is an operator in RxJS which takes one more observables as inputs and then wait until all observables to emit and complete.

One popular use case in Angular where we should use forkJoin is while performing multiple HTTP requests. There will be a use case where we would want to finish few HTTP requests in one go because the data fetched from one API call or HTTP request might be dependent on other API call data.

For example, say we are fetching all the users in an application, however we would also want to fetch all the user roles too so we can map the users data to the roles accordingly in the grid or make some decisions. So the solution for this is to use the forkJoin operator pass these two HTTP request as the inputs and wait for these two HTTP requests to complete, once done, we will have all the data we need in a single observable array so we can proceed to parse the data.

Lets, see some code example.

// Service class with two mock API functions to fetch the data.

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
import { IUser } from '../interfaces/user.interface';
import { MOCK_USER_ROLES, MOCK_USERS_DATA } from '../constants';

@Injectable({
    providedIn: 'root',
})
export class UserService {
    
    // Mock function to fetch the users data.
    getUsers(): Observable<IUser[]> {
        return of(MOCK_USERS_DATA).pipe(delay(2000));
    }

    // Mock function to fetch the user roles.
    getUserRoles(): Observable<string[]> {
        return of(MOCK_USER_ROLES).pipe(delay(2000));
    }
}
// Mock data for our service requests.

import { IUser } from './interfaces/user.interface';

// Mock users data
export const MOCK_USERS_DATA: IUser[] = [
    {
        id: '1',
        email: 'jackbing@gmail.com',
        roles: ['Student'],
        displayName: 'Jack Bing',
    },
    {
        id: '2',
        email: 'chandlerbing@gmail.com',
        roles: ['Super Admin'],
        displayName: 'Chandler Bing',
    },
    {
        id: '3',
        email: 'appAdmin@gmail.com',
        roles: ['Teacher'],
        displayName: 'Admin',
    },
];

// Mock user roles data
export const MOCK_USER_ROLES: string[] = ['Student', 'Super Admin', 'Teacher'];
// User interface, defines the type of our users data.

export interface IUser {
    id: string;
    email: string;
    password?: string;
    displayName: string;
    photoURL?: string;
    emailVerified?: boolean;
    roles: string[];
}
import { Component, OnInit } from '@angular/core';
import { UserService } from './services/user.service';
import { forkJoin } from 'rxjs';
import { IUser } from './interfaces/user.interface';

@Component({
    selector: 'app-root',
    templateUrl: './app.component.html',
    styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
    users: IUser[] | undefined;

    userRoles: string[] | undefined;

    constructor(private _userService: UserService) {}

    ngOnInit() {
        this.getUsersData();
    }

    // We are using the forkJoin operator for the two requests from the service class.
    getUsersData() {
        forkJoin({
            users: this._userService.getUsers(),
            userRoles: this._userService.getUserRoles(),
        }).subscribe(
            ({ users, userRoles }) => {
                this.users = users;
                this.userRoles = userRoles;
            },
            (error) => {
                console.log(error);
            }
        );
    }
}

Above code sample function getUsersData is where we are using the forkJoin to make multiple HTTP requests and then we process the response from all the requests once all the requests are completed!!.

How TypeScript changed my life.

Don’t get me wrong, JavaScript is a powerful language. However, it is also a dynamic language.

The problem with being dynamic language is you can do all kinds of crazy things, like, reference variables that don’t exist or work with objects of an unknown shape, since the code is executed by the browser if there are issues with your code you won’t be able to catch it until runtime when the browser throws an error and then your code / app is broken for good!!.

Runtime error and my app is broken!

Now, the whole point of TypeScript language is it prevents just that…prevents errors from ever happening by extending JavaScript with types. Hence the language is a super-set of JavaScript which means you can write plain JavaScript with all of its features completely optional.

IDE’s have become smarter now that they provide instant feedback when writing code because TypeScript behaves like a compiled language where as JavaScript is the compilation target.

Feedback by IDE

For example, IDE will provide feedback about using a variable that does not exist and we can fix this issue then and there instead of fixing this issue weeks or months later when a hardworking QA engineer finds it. And we could have saved lot of time and money, also most importantly not find yourself in an embarrassing situation.

So, TypeScript, the primary goal of this language is to enable static typing, one way to achieve that is by allowing you to annotate your code with types.

let userName: string = 'Kumar Gandhi';
// Also known as explicit type since we set the type. 

In above code sample you can see strongly typed variable using a colon followed by a type.

There are other types we can set, like, number, boolean …This is called explicit type. So if we try to assign a value of wrong type then we get an error. Alternatively, we can also set initial value and it will implicitly assign type, see below code.

let userName = 'Kumar Gandhi';
// Type is implicitly set to string since the value is of type string.

You can also opt-out of type when you annotate with the any type which allows you to loosely type or opt-out of type checking.

let userName: any;
// Opt-out of type check and type can change dynamically.

You can also define your own custom types and interfaces which is extremely powerful when working with objects.

// Custom type using interface
export interface User {
    id: string;
    email: string;
    displayName: string;
    photoURL?: string;
}

// My variable with custom type
let currentUser: User;

Above User interface defines various types of the properties on an object. We can then apply this interface to the plain JavaScript object, the currentUser object.

The beauty of having this strongly typed code is that we get auto-complete everywhere in our IDE and we don’t have to jump back and forth through documentation or traces to figure our why our code is not working!!.

Auto-complete by IDE
Working with TypeScript project
  • Project will will have a tsconfig.json file which provides ways to customise the behaviour of the compiler.
  • Run the TypeScript compiler using the tsc command it will take the .ts file and transpile it into vanilla JavaScript.
  • You may also choose which version of JavaScript you want to in the compiler settings if you are targeting some older browser.
  • This means you can start using the latest features of JavaScript without having to worry if they will be supported in an older environment.
Conclusion

Use DocumentReference to add a document to a collection in Angular + Firebase.

As per the docs A DocumentReference refers to a document location in a Firestore database [READ]. In simple terms, it creates a relation between two collections. Let’s look at Firestore Database with the actual reference.

question collection on Firebase Database

From the above screenshot, every document in question collection actually refers to a document in survey collection. That is the relation between those two collections. And once we have established such a reference there are lot more things we can do with it, like, apply a filter query on question collection!!.

In Angular application when you create the interface or class for above question collection you can set the type of the property surveyId as DocumentReference. See below code sample.

import { DocumentReference } from '@angular/fire/compat/firestore/interfaces';

export interface IQuestion {
    id?: string | number;
    type: string;
    question: string;
    options?: string | number | IOption | IOption[];
    surveyId?: DocumentReference;
    creationDate?: Date;
}

export interface IOption {
    selected: boolean;
    answer: string;
}

Okay, so, we have defined the type and collection, now, how do you add a document to the collection in Angular. See below code sample.

saveQuestion(question: IQuestion, surveyId: string) {
    const surveyDoc = this.firestore
        .collection(COLLECTION_SURVEY)
        .doc(surveyId);
    question.surveyId = surveyDoc.ref;
    return this.firestore.collection(COLLECTION_QUESTION).add(question);
}

Now to get the questions belonging to a survey you can write a simple query function. See below code sample.

getQuestionsForSurvey(surveyId: string) {
    const surveyDoc = this.firestore
        .collection(COLLECTION_SURVEY)
        .doc(surveyId);
    return this.firestore
        .collection(COLLECTION_QUESTION, (ref) =>
            ref.where('surveyId', '==', surveyDoc.ref)
        )
        .snapshotChanges();
}

Please leave a comment if there is something more you would want me to share.

Versions
  • @angular/cli@~12.2.6
  • @angular/fire@^7.1.1

**Soon I will share the complete code on Github.

Learning CRUD operations in Angular + Firebase

Let’s say we have a collection in our Firebase Database called survey and we want to perform the CRUD operations to manipulate this collection.

survey collection on Firebase Database

But, before we go there, first let’s look at the type of data the collection holds.

export interface ISurvey {
    id?: string;
    name: string;
    desc: string;
    isConfigured: boolean;
    passScore: number;
}

Above is the survey interface in Angular which we are going to use as the type or document of surveys for our collection.

Create
saveSurvey(survey: ISurvey) {
    return this.firestore.collection(COLLECTION_SURVEY).add(survey);
}

We are adding a new survey to the collection.

Read
getSurveys() {
    return this.firestore.collection(COLLECTION_SURVEY).snapshotChanges();
}

Get the documents in the collection. The function actually returns an Observable, so to get the actual payload we need to subscribe to the function and parse the data to the needed type, see below.

surveys: ISurvey[];

getSurveys().subscribe(
    (data) => {
        this.surveys = data.map((e) => {
            const s: ISurvey = e.payload.doc.data() as ISurvey;
            s.id = e.payload.doc.id;
            return s;
        });
        this.loading = false;
        this._cd.markForCheck();
    },
    (error) => {
        this.errorText = error;
        this.loading = false;
        this._cd.markForCheck();
    }
);
Update
updateSurvey(survey: ISurvey) {
    const id = survey.id;
    delete survey.id;
    return this.firestore.doc(`${COLLECTION_SURVEY}/` + id).update(survey);
 }
Delete
deleteSurvey(surveyId) {
    return this.firestore.doc(`${COLLECTION_SURVEY}/` + surveyId).delete();
}

All the above functions except getSurveys returns a promise, so you would use then() and catch() blocks to process the response. For example, say after deleting a survey we need to refresh the data. The we can do this on the then() block of the promise returned by the deleteSurvey method, see below for sample code.

this.deleteSurvey(survey.id)
    .then(() => {
      this.loading = false;
      this.getSurveys();
    })
    .catch((error) => {
      this.loading = false;
      this.errorText = error;
    }
);
Versions
  • @angular/cli@~12.2.6
  • @angular/fire@^7.1.1

**Soon I will share the complete code on Github.