Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

The Modern Application Stack – Part 4b: Building a Client UI Using Angular 2 (formerly AngularJS) and TypeScript

DZone's Guide to

The Modern Application Stack – Part 4b: Building a Client UI Using Angular 2 (formerly AngularJS) and TypeScript

Welcome back! Today, we'll finish going over how to build a complete, working, MEAN stack application using Angular 2 and Typescript.

· Web Dev Zone
Free Resource

Should you build your own web experimentation solution? Download this whitepaper by Optimizely to find out.

A Simple Component That Accepts Data From Its Parent

Recall that the application consists of five components: the top-level application which contains each of the add, count, update, and sample components.

When building a new application, you would typically start by designing the top-level container and then work downwards. As the top-level container is the most complex one to understand, we'll start at the bottom and then work up.

A simple sub-component to start with is the count component:

Mongopop Angular2 component

public/app/count.component.html

defines the elements that define what's rendered for this component:

<h2>Count Documents</h2>
<p>
    Collection name:
    <input #CountCollName id="count-collection-name" type="text" value="{{MongoDBCollectionName}}">
</p>
<p>
    <button (click)="countDocs(CountCollName.value)">Count Docs</button>
</p>
<p>
    <span class="successMessage">{{DocumentCount}}</span>
    <span class="errorMessage">{{CountDocError}}</span>
</p>


You'll recognize most of this as standard HTML code.

The first Angular extension is for the single input element, where the initial value (what's displayed in the input box) is set to {{MongoDBCollectionName}}. Any name contained within a double pair of braces refers to a data member of the component's class (public/app/count.component.ts).

When the button is clicked, countDocs (a method of the component's class) is invoked with CountCollName.value (the current contents of the input field) passed as a parameter.

Below the button, the class data members of DocumentCount and CountDocError are displayed – nothing is actually rendered unless one of these has been given a non-empty value. Note that these are placed below the button in the code, but they would still display the resulting values if they were moved higher up – position within the HTML file doesn't impact logic flow. Each of those messages is given a class so that they can be styled differently within the component's CSS file:

.errorMessage {
    font-weight: bold;
    color: red;
}
 .warningMessage {
    color: brown;
}
.successMessage {
    color: green;
}


Angular 2 success message

Angular 2 error message

The data and processing behind the component is defined in public/app/count.component.ts:

import {
    Component,
    OnInit,
    Injectable,
    EventEmitter,
    Input,
    Output
} from '@angular/core';
import {
    Response
} from '@angular/http';
import {
    Observable,
    Subscription
} from 'rxjs/Rx';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import {
    DataService
} from './data.service';
// This component will be loaded into the <my-count> element of `app/app.component.html`

@Component({
    selector: 'my-count',
    templateUrl: 'app/count.component.html',
    styleUrls: ['stylesheets/style.css']
})
@Injectable()
export class CountComponent implements OnInit {
    CountDocError: string = "";
    DocumentCount: string = "";
    // Parameters sent down from the parent component (AppComponent)
    @Input() dataService: DataService;
    @Input() MongoDBCollectionName: string;
    // Event emitters to pass changes back up to the parent component
    @Output() onCollection = new EventEmitter < string > ();
    ngOnInit() {}
    // Invoked from the component's html code
    countDocs(CollName: string) {
        this.DocumentCount = "";
        this.CountDocError = "";
        this.dataService.sendCountDocs(CollName)
            .subscribe(
                results => {
                    // Invoked if/when the observable is succesfully resolved
                    if (results.success) {
                        this.DocumentCount = "Collection '" + CollName +
                            "' contains " + results.count.toLocaleString() + " documents";
                        this.MongoDBCollectionName = CollName;
                        this.onCollection.emit(this.MongoDBCollectionName);
                    } else {
                        // Invoked if/when the back-end sucessfully sends a response
                        // but that response indicates an application-level error
                        this.CountDocError = "Application Error: " + results.error;
                    }
                },
                error => {
                    // Invoked if/when the observable throws an error
                    this.CountDocError = "Network Error: " + error;
                })
    }
}


Starting with the @component decoration for the class:

@Component({
    selector: 'my-count',
    templateUrl: 'app/count.component.html',
    styleUrls: ['stylesheets/style.css']
})


This provides metadata for the component:

  • selector: The position of the component within the parent's HTML should be defined by a <my-count></my-count> element.
  • templateUrl: The HMTL source file for the template (public/app/count.component.ts in this case – public is dropped as the path is relative).
  • styleUrls: The CSS file for this component – all components in this application reference the same file: public/stylesheets/style.css

The class definition declares that it implements the OnInit interface; this means that itsngOnInit() method will be called after the browser has loaded the component; it's a good place to perform any initialization steps. In this component, it's empty and could be removed.

The two data members used for displaying success/failure messages are initialized to empty strings:

this.DocumentCount = "";
this.CountDocError = "";


Recall that data is passed back and forth between the count component and its parent:

Flow of Data Between Angular Components
Child component Data passed down Data changes pased back up
CountComponent
Data service Collection name
Collection name

To that end, two class members are inherited from the parent component – indicated by the @Input() decoration:

// Parameters sent down from the parent component (AppComponent)
@Input() dataService: DataService;
@Input() MongoDBCollectionName: string;


The first is an instance of the data service (which will be used to request the document count); the second is the collection name that we used in the component's HTML code. Note that if either of these is changed in the parent component then the instance within this component will automatically be updated.

When the name of the collection is changed within this component, the change needs to be pushed back up to the parent component. This is achieved by declaring an event emitter (onCollection):


@Output() onCollection = new EventEmitter<string>();
...
this.onCollection.emit(this.MongoDBCollectionName);


Recall that the HTML for this component invokes a member function: countDocs(CountCollName.value) when the button is clicked; that function is implemented in the component class:

countDocs(CollName: string) {
    this.DocumentCount = "";
    this.CountDocError = "";
    this.dataService.sendCountDocs(CollName)
        .subscribe(
            results => {
                // Invoked if/when the observable is succesfully resolved
                if (results.success) {
                    this.DocumentCount = "Collection '" + CollName +
                        "' contains " + results.count.toLocaleString() + " documents";
                    this.MongoDBCollectionName = CollName;
                    this.onCollection.emit(this.MongoDBCollectionName);
                } else {
                    // Invoked if/when the back-end sucessfully sends a response
                    // but that response indicates an application-level error
                    this.CountDocError = "Application Error: " + results.error;
                }
            },
            error => {
                // Invoked if/when the observable throws an error
                this.CountDocError = "Network Error: " + error;
            })
}


After using the data service to request the document count, either the success or error messages are sent – depending on the success/failure of the requested operation. Note that there are two layers to the error checking:

  1. Was the network request successful? Errors such as a bad URL, out of service back-end, or loss of a network connection would cause this check to fail.
  2. Was the back-end application able to execute the request successfully? Errors such as a non-existent collection would cause this check to fail.

Note that when this.CountDocError or this.DocumentCount are written, Angular will automatically render the new values in the browser.

Passing Data Down to a Sub-Component (and Receiving Changes Back)

We've seen how CountComponent can accept data from its parent and so the next step is to look at that parent – AppComponent.

The HTML template app.component.html includes some of its own content, such as collecting database connection information, but most of it is a delegated to other components. For example, this is the section that adds in CountComponent:

<div>
   <my-count
   [dataService]="dataService"
   [MongoDBCollectionName]="MongoDBCollectionName"
   (onCollection)="onCollection($event)">
   </my-count>
</div>


Angular will replace the <my-count></my-count> element with CountComponent; the extra code within that element passes data down to that sub-component. For passing data members down, the syntax is:

[name-of-data-member-in-child-component]="name-of-data-member-in-this-component"


As well as the two data members, a reference to the onCollection event handler is passed down (to allow CountComponent to propagate changes to the collection name back up to this component). The syntax for this is:

(name-of-event-emitter-in-child-component)="name-of-event-handler-in-this-component($event)"


As with the count component, the main app component has a Typescript class – defined in app.component.ts – in addition to the HTML file. The two items that must be passed down are the data service (so that the count component can make requests of the back-end) and the collection name – these are both members of the AppComponent class.

The dataService object is implicitly created and initialized because it is a parameter of the class's constructor, and because the class is decorated with @Injectable:

@Injectable()
export class AppComponent implements OnInit {
    ...
    constructor(private dataService: DataService) {}
    ''
    '
}


MongoDBCollectionName is set during component initialization within the ngOnInit() method by using the data service to fetch the default client configuration information from the back-end:

export class AppComponent implements OnInit {
    ...
    MongoDBCollectionName: string;
    ...
    ngOnInit() {
        // Fetch the default client config from the back-end
        this.dataService.fetchClientConfig().subscribe(
            results => {
                // This code is invoked if/when the observable is resolved sucessfully
                ...
                this.MongoDBCollectionName = results.mongodb.defaultCollection;
                ...
            },
            error => {
                // This code is executed if/when the observable throws an error.
                console.log("Failed to fetch client content data. Reason: " + error.toString);
            });
    }
    ...
}



Finally, when the collection name is changed in the count component, the event that it emits gets handled by the event handler called, onCollection, which uses the new value to update its own data member:

// This is invoked when a sub-component emits an onCollection event to indicate
// that the user has changes the collection within its form. The binding is
// created in app.component.html
onCollection(CollName: string) {
    this.MongoDBCollectionName = CollName;
}


Conditionally Including a Component

It's common that a certain component should only be included if a particular condition is met. Mongopop includes a feature to allow the user to apply a bulk change to a set of documents - selected using a pattern specified by the user. If they don't know the typical document structure for the collection then it's unlikely that they'll make a sensible change. Mongopop forces them to first retrieve a sample of the documents before they're given the option to make any changes.

The ngIf directive can be placed within the opening part of an element (in this case a <div>) to make that element conditional. This approach is used within app.component.htmlto only include the update component if the DataToPlayWith data member is TRUE:

<div *ngIf="DataToPlayWith">
    <my-update [dataService]="dataService" [MongoDBCollectionName]="MongoDBCollectionName" (onCollection)="onCollection($event)">
    </my-update>
</div>



Note that, as with the count component, if the update component is included then it's passed the data service and collection name and that it also passes back changes to the collection name.

Angular includes other directives that can be used to control content; ngFor being a common one as it allows you to iterate through items such as arrays:

<ul>
    <li *ngFor="let item of items; let i = index">
        {{i}} {{item}}
    </li>
</ul>


Returning to app.component.html, an extra handler (onSample) is passed down to the sample component:

<div>
    <my-sample [dataService]="dataService" [MongoDBCollectionName]="MongoDBCollectionName" (onSample)="onSample($event)" (onCollection)="onCollection($event)">
    </my-sample>
</div>


sample.component.html is similar to the HTML code for the count component but there is an extra input for how many documents should be sampled from the collection:

<h2>Sample Data</h2>
<p>
    Collection name:
    <input #SampleCollName id="sample-collection-name" type="text" value="{{MongoDBCollectionName}}">
</p>
<p>
    Sample size:
    <input #SampleSize id="sample-size" type="number" min="1" max="10" value="1" />
</p>
<p>
    <button (click)="sampleDocs(SampleCollName.value, SampleSize.value)">Sample Docs</button>
</p>
<p>
    <span class="errorMessage"> {{SampleDocError}}</span>
</p>
<div class="json">
    <pre>
{{SampleDocResult}}
</pre>
</div>


On clicking the button, the collection name and sample size are passed to the sampleDocsmethod in sample.component.ts which (among other things) emits an event back to the AppComponent's event handler using the onSample event emitter:

@Injectable()
export class SampleComponent implements OnInit {
    ...
    // Parameters sent down from the parent component (AppComponent)
    @Input() dataService: DataService;
    @Input() MongoDBCollectionName: string;
    // Event emitters to pass changes back up to the parent component
    @Output() onSample = new EventEmitter < boolean > ();
    @Output() onCollection = new EventEmitter < string > ();
    ...
    sampleDocs(CollName: string, NumberDocs: number) {
        this.SampleDocResult = "";
        this.SampleDocError = "";
        this.onSample.emit(false);
        this.dataService.sendSampleDoc(CollName, NumberDocs)
            .subscribe(
                results => {
                    // Invoked if/when the observable is succesfully resolved
                    if (results.success) {
                        this.SampleDocResult = this.syntaxHighlight(results.documents);
                        this.MongoDBCollectionName = CollName;
                        this.onSample.emit(true);
                        this.onCollection.emit(this.MongoDBCollectionName);
                    } else {
                        this.SampleDocError = "Application Error: " + results.error;
                    }
                },
                error => {
                    // Invoked if/when the observable throws an error
                    this.SampleDocError = "Network Error: " + error.toString;
                }
            );
    }
}



Other Code Highlights

Returning to app.component.html; there is some content there in addition to the sub-components:

<h1>Welcome to MongoPop</h1>
<div>
    <p>
        The IP address of the server running MongoPop is {{serverIP}}, if using <a href="https://cloud.mongodb.com" name="MongoDB Atlas" target="_blank">MongoDB Atlas</a>, please make sure you've added this to your IP Whitelist unless you have VPC peering configured.
    </p>
</div>

<div>
    <p>
        Connect String provided by MongoDB Atlas:
        <input #MongoDBBaseString id="MongoDB-base-string" value="{{dBInputs.MongoDBBaseURI}}" (keyup)="setBaseURI(MongoDBBaseString.value)" (change)="setBaseURI(MongoDBBaseString.value)" />
    </p>

    <!-- Only ask for the password if the MongoDB URI has been changed from localhost -->
    <div *ngIf="dBInputs.MongoDBUser">
        <p>
            Password for user {{dBInputs.MongoDBUser}}:
            <input #MongoDBPassword id="MongoDB-password" value="{{dBInputs.MongoDBUserPassword}}" type="password" (keyup)="setPassword(MongoDBPassword.value)" (change)="setPassword(MongoDBPassword.value)" />
        </p>
    </div>
    <p>
        Preferred database name:
        <input #MongoDBDBName id="MongoDB-db-name" value="{{dBInputs.MongoDBDatabaseName}}" (keyup)="setDBName(MongoDBDBName.value)" (change)="setDBName(MongoDBDBName.value)" />
    </p>
    <p>
        Socket (operation) timeout in seconds:
        <input #SocketTimeout id="socket-timeout" value="{{dBInputs.MongoDBSocketTimeout}}" type="number" min="1" max="1000" (change)="setMongoDBSocketTimeout(SocketTimeout.value)" />
    </p>
    <p>
        Connection Pool size:
        <input #ConnectionPoolSize id="connection-pool-size" value="{{dBInputs.MongoDBConnectionPoolSize}}" type="number" min="1" max="1000" (change)="setMongoDBConnectionPoolSize(ConnectionPoolSize.value)" />
    </p>
    <p>
        MongoDB URI: {{dBURI.MongoDBURIRedacted}}
        <button (click)="showPassword(true)">Show Password</button>
    </p>
    ...
</div>


Most of this code is there to allow a full MongoDB URI/connection string to be built based on some user-provided attributes. Within the input elements, two event types (keyup & change) make immediate changes to other values (without the need for a page refresh or pressing a button):

Reactive Angular 2 Component

The actions attached to each of these events call methods from the AppComponent class to set the data members – for example the setDBName method (from app.component.ts):

setDBName(dbName: string) {
    this.dBInputs.MongoDBDatabaseName = dbName;
    this.dBURI = this.dataService.calculateMongoDBURI(this.dBInputs);
}


In addition to setting the dBInputs.MongoDBDatabaseName value, it also invokes the data service method calculateMongoDBURI (taken from data.service.ts ):

calculateMongoDBURI(dbInputs: any): {
    "MongoDBURI": string,
    "MongoDBURIRedacted": string
} {
    /*
    Returns the URI for accessing the database; if it's for MongoDB Atlas then include the password and
    use the chosen database name rather than 'admin'. Also returns the redacted URI (with the password
    masked).
    */
    let MongoDBURI: string;
    let MongoDBURIRedacted: string;
    if (dbInputs.MongoDBBaseURI == "mongodb://localhost:27017") {
        MongoDBURI = dbInputs.MongoDBBaseURI +
            "/" + dbInputs.MongoDBDatabaseName +
            "?authSource=admin&socketTimeoutMS=" +
            dbInputs.MongoDBSocketTimeout * 1000 +
            "&maxPoolSize=" +
            dbInputs.MongoDBConnectionPoolSize;
        MongoDBURIRedacted = dbInputs.MongoDBBaseURI;
    } else {
        // Can now assume that the URI is in the format provided by MongoDB Atlas
        dbInputs.MongoDBUser = dbInputs.MongoDBBaseURI.split('mongodb://')[1].split(':')[0];
        MongoDBURI = dbInputs.MongoDBBaseURI
            .replace('<DATABASE>', dbInputs.MongoDBDatabaseName)
            .replace('<PASSWORD>', dbInputs.MongoDBUserPassword) +
            "&socketTimeoutMS=" +
            dbInputs.MongoDBSocketTimeout * 1000 +
            "&maxPoolSize=" +
            dbInputs.MongoDBConnectionPoolSize;
        MongoDBURIRedacted = dbInputs.MongoDBBaseURI
            .replace('<DATABASE>', dbInputs.MongoDBDatabaseName)
            .replace('<PASSWORD>', "**********") +
            "&socketTimeoutMS=" +
            dbInputs.MongoDBSocketTimeout * 1000 +
            "&maxPoolSize=" +
            dbInputs.MongoDBConnectionPoolSize;
    }
    this.setMongoDBURI(MongoDBURI);
    return ({
        "MongoDBURI": MongoDBURI,
        "MongoDBURIRedacted": MongoDBURIRedacted
    });
}



This method is run by the handler associated with any data member that affects the MongoDB URI (base URI, database name, socket timeout, connection pool size, or password). Its purpose is to build a full URI which will then be used for accessing MongoDB; if the URI contains a password then a second form of the URI, MongoDBURIRedacted has the password replaced with **********.

It starts with a test as to whether the URI has been left to the default localhost:27017 – in which case it's assumed that there's no need for a username or password (obviously, this shouldn't be used in production). If not, it assumes that the URI has been provided by the MongoDB Atlas GUI and applies these changes:

  • Change the database name from <DATATBASE> to the one chosen by the user.
  • Replace <PASSWORD> with the real password (and with ********** for the redacted URI).
  • Add the socket timeout parameter.
  • Add the connection pool size parameter.

Testing & Debugging the Angular Application

Now that the full MEAN stack application has been implemented, you can test it from within your browser:

Debugging the Angular 2 client is straightforward using the Google Chrome Developer Tools which are built into the Chrome browser. Despite the browser executing the transpiled JavaScript, the Dev Tools allows you to browse and set breakpoints in your Typescript code:

Summary and What's Next in the Series

Previous posts stepped through building the Mongopop application back-end. This post describes how to build a front-end client using Angular 2. At this point, we have a complete, working, MEAN stack application.

The coupling between the front and back-end is loose; the client simply makes remote, HTTP requests to the back-end service – using the interface created in Part 3: Building a REST API with Express.js.

This series will finish out by demonstrating alternate methods to implement front-ends; using ReactJS for another browser-based UI (completing the MERN stack) and then more alternative methods.

Continue following this blog series to step through building the remaining stages of the Mongopop application:

Implementing an Experimentation Solution: Choosing whether to build or buy?

Topics:
http ,web dev ,angular ,typescript ,mongo db

Published at DZone with permission of Andrew Morgan, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}