"I Like Big Data for $400" - Implementing a Jeopardy! Question Explorer using DocumentDB, AzureSearch, Angular and Electron - Part 2


Building the Angular and Electron application

Now that we have the DocumentDB collection and API in place, let's build an application that consumes that data. We will be building an application using Angular 2 and the angular-cli to build our application. But, we are also going add a small twist, we are going to use the ElectronJS framework to create a deployable application that can run on Windows, Mac and Linux. ElectronJS allows you to build cross platform desktop apps using JavaScript, HTML and CSS. It is an open source library developed by GitHub that combines Chromium and Node.js into a single run-time environment that allows you to easily package apps for Mac, Windows and Linux. At the end of this post, we will have an application that leverages Angular and Electron to create a running application.

Setting up your angular-cli generated application to run with ElectronJS requires a couple of extra steps. I want to thank Bruno d'Auria for creating a walk through that helps you complete the angular and electron integration. You can view that blog post here: http://www.blog.bdauria.com/?p=806.

Create the Angular CLI application

Angular CLI allows you to rapidly generate, test and deploy Angular applications using the command line.

If you don't already have angular-cli installed on your computer, you can easily install it using NPM. If you don't have NPM installed on your computer, you can install it by installing the latest stable build of Node.js (https://nodejs.org/).

To install angular-cli, from a command prompt, run the following command:

npm install -g @angular/cli

After Angular CLI is installed, we need to install ElectronJS. To do so, run the following command:

npm install -g electron

Now that both angular-cli and Electron are installed, we can create our Angular application.

From a command prompt, navigate to an appropriate folder on your computer and run the command:

ng new NoSqlJeopardy-Client

Open that folder in your favorite code editor (I'm using Visual Studio Code)
Angular and Electron - VS Code

Configure Angular and Electron

To integrate Angular and Electron, we will need to create a new folder inside your src folder and title that folder "electron"

Add a new file "package.json" inside that folder and use the following code:

{
"name" : "noql-jeopardy",
"version" : "0.1.0",
"main" : "electron.js"
}

Create another file inside that folder called "electron.js" We will use a slightly modified version of the electron.js file available from the Electron quick start. (Your electron.js file specified how electron should startup, where it should get it's application runtime files from and generally how your application is configured, we will use most of the default settings as specified in the quick start. The one difference we have in this file is that we removed the openDevTools call.)

// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let win

function createWindow () {
// Create the browser window.
win = new BrowserWindow({width: 800, height: 600})

// and load the index.html of the app.
win.loadURL(`file://${__dirname}/index.html`)

// Emitted when the window is closed.
win.on('closed', () => {
// Dereference the window object, usually you would store windows
// in an array if your app supports multi windows, this is the time
// when you should delete the corresponding element.
win = null
})
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', createWindow)

// Quit when all windows are closed.
app.on('window-all-closed', () => {
// On macOS it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit()
}
})

app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (win === null) {
createWindow()
}
})

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

Next, we need to modify the angular code in order to have it run correctly. To do so, open src/index.html and make the following changes:

<base href="/">

to:

<base href="./">

* This change is necessary because we will be running our Angular application using the file:// protocol instead of http:// and we need to tell it how to configure the base URLs.

Next, we need to add some node build scripts to compile and run our application using electron.

To do so, open up your root package.json and add the following scripts to your "scripts" section:

"build-electron": "ng build --base-href . && copy src\\electron\\* dist",
"electron": "npm run build-electron && electron dist"

Now that that we have integrated angular and electron, we can run our application and ensure everything is setup properly. To do that, from your command prompt, make sure you are in your NoSqlJeopardy-Client folder and type

npm run electron

Once angular-cli compiles your application, you should see the "app works!" screen that is generated using angular and electron.

Angular and Electron - App Works

Create the base component and add Twitter Bootstrap

That's just the start. We need to actually create our application screens. Let's get started by adding bootstrap to our application.

From the command prompt, type:

npm install bootstrap --save
npm install ng2-bootstrap --save

To complete the bootstrap installation, we need to add the bootstrap css to our angular-cli build script. To do that, open angular-cli.json, locate the styles section and add the following stylesheet:

"../node_modules/bootstrap/dist/css/bootstrap.min.css",

Now that Bootstrap is installed, let's update our main src/app/app.component.html file with our basic layout:

<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false"
aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#"></a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li routerLink="/game" routerLinkActive="active"><a routerLink="/game">Game</a></li>
<li routerLink="/search" routerLinkActive="active"><a routerLink="/search">Search Questions</a></li>
</ul>
</div>
<!--/.nav-collapse -->
</div>
</nav>
<div class="container-fluid">
<router-outlet></router-outlet>
</div>

And add a basic blue background to our style.css file:

/* You can add global styles to this file, and also import other style files */
body {
padding-top: 55px;
background-color: #0100a0;
}

Next, we need to add the RouterModule to our app module so that we can make use of the functionality. To do that, open src/app/app.module.ts and add the following code at the top of your file:

import { RouterModule } from '@angular/router';

and then update your imports array to include the following code:

RouterModule.forRoot([
{
path: '',
redirectTo: '/game',
pathMatch: 'full'
},
{
path: 'game',
component: GameComponent
}
]),

Before this will work, we need to create a base GameComponent. To do that, from a command prompt, run the following command:

ng g component Game

When that command completes, our app.module.ts should automatically update to include the proper imports. Our app.module.ts shoudl look like this:

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

import { AppComponent } from './app.component';
import { GameComponent } from './game/game.component';

@NgModule({
declarations: [
AppComponent,
GameComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule,
RouterModule.forRoot([
{
path: '',
redirectTo: '/game',
pathMatch: 'full'
},
{
path: 'game',
component: GameComponent
}
])
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Before we go further, let's check to see how things are going. From a command prompt, run:

npm run electron

Angular and Electron - App Works 2

Great! we have a base framework for our application. Let's get it a bit further.

Create the GameService

To load data from our application, we will use an Angular service to wrap the HTTP call to our API.

To begin with, let's create a DTO that matches the JeopardyBoard ViewModel we created earlier.It will consist of three objects, a question, question category and Jeopardy board.

From a command prompt run the folllowing:

ng g class Question
ng g class QuestionCategory
ng g class JeopardyBoard

Back in our editor open question.ts and add the following code:

export class Question {
value: string;
question: string;
answer: string;
}

Set Question-Category.ts to:

import { Question } from './question';

export class QuestionCategory {
categoryName: string;
questions: Question[];
}

Set Jeopardy-Board.ts to:

import { QuestionCategory } from './question-category';

export class JeopardyBoard {
categories: QuestionCategory[];
}

Next, let's create the GameService to handle our calls to our GameAPI. From a command prompt run the command:

ng g service services/Game

Open up the file src/app/services/game.service.ts and add the following code:

import { Injectable } from '@angular/core';
import { Headers, Http } from '@angular/http';
import { JeopardyBoard } from '../jeopardy-board';
import 'rxjs/add/operator/toPromise';

@Injectable()
export class GameService {
private gamesUrl = 'http://localhost:2475/api/questions/'

constructor(private http: Http) { }

getGame(showNumber: number, round: string): Promise<JeopardyBoard> {
var url = this.gamesUrl + '?showNumber=' + String(showNumber) + '&round=' + encodeURI(round);
return this.http.get(url)
.toPromise()
.then(response => response.json() as JeopardyBoard)
.catch(this.handleError);

//return Promise.resolve(MOCK_BOARD);
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error); // for demo purposes only
return Promise.reject(error.message || error);
}
}

The getGame call will use Angular2's http and rxjs services to return JSON from our API service. we can then use reference this in our GameComponent to load the game and return it to the view.

Complete the game component

Now that we have a service to wrap our API call, let's complete our game component.

Open up src/app/game/game.component.ts and enter the following code:

import { Component, OnInit } from '@angular/core';
import { GameService } from '../services/game.service';
import { JeopardyBoard } from '../jeopardy-board';
import { Question } from '../question';

@Component({
selector: 'app-game',
templateUrl: './game.component.html',
styleUrls: ['./game.component.css']
})
export class GameComponent implements OnInit {
board: JeopardyBoard = new JeopardyBoard();
currentQuestion: Question;
showNumberField: number;
roundField: string;

constructor(private gameService: GameService) { }

ngOnInit() {
this.showNumberField = 4680;
this.roundField = 'Jeopardy!';
this.getBoard();
}

getBoard(): void {
this.gameService.getGame(this.showNumberField, this.roundField).then(boardV => {this.board = boardV; console.log(boardV)});
}

setCurrentQuestion(question: Question){
this.currentQuestion = question;
}

clearCurrentQuestion() {
this.currentQuestion = undefined;
}

updateShow() {
this.getBoard();
}
}

the key code here is in the getBoard() function. It uses an instance of our gameService (as created in the constructor) to make the getGame call and handle the results when they are returned.

Open src/app/game/game.component.html and set it to the following:

<div class="board" *ngIf="!currentQuestion">
<ul class="categories">
<li *ngFor="let category of board.categories"><span class="category-name"></span>
<ul class="category">
<li *ngFor="let question of category.questions" (click)="setCurrentQuestion(question)"><span class="question-value"></span></li>
</ul>
</li>
</ul>
</div>

<div class="question-display" *ngIf="currentQuestion">
<span class="question" [innerHTML]="currentQuestion.question"></span><br/>
<span class="answer"></span> <br/>
<span class="btn btn-large btn-info" (click)="clearCurrentQuestion()">Back &lt; </span>
</div>

<div class="search-footer">
<input type="number" required [(ngModel)]="showNumberField" />
<input type="text" required [(ngModel)]="roundField" />
<span class="btn btn-large btn-info" (click)="updateShow()">Update Show</span>
</div>

Next, let's add some CSS. Opensrc/app/game/game.component.css and set it to:

.board {
width: 100%;
margin: auto;
}

ul.categories {
list-style-type: none;
margin: 10px;
padding: 0;
}

ul.categories>li {
float: left;
text-align: center;
margin: 20px;
height: 130px;
}

ul.category {
list-style-type: none;
}

ul.category>li {
margin: 20px;
height: 80px;
text-align: center;
}

.category-name {
font-weight: 500;
font-size: 30px;
color: white;
max-width: 280px;
display: block;
height: 130px;
}

.question-value {
font-weight: 500;
font-size: 30px;
color: yellow;
text-align: center;
cursor: pointer;
}

.search-footer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 60px;
/* Height of the footer */
background: #6cf;
margin-top: 10px;
margin-left:auto;
}

.question-display {
text-align: center;
}

.question {
color: yellow;
font-size: 60px;
}

.answer {
color: white;
font-size: 20px;
}

Finally, we need to register our game service as a provider so Angular will know how to handle the constructor injection. Open src/app/app.module.ts and add the following code to the headeR:

import { GameService } from './services/game.service';

Then add the following code to the providers array:

GameService

Now, let's compile and run our angular and electron application. Before we run our application, make sure you are running the API we built in yesterday's post. If you aren't running, it the application will not work. Once again, from a command line run:

npm run electron

You'll need to maximize the application when it loads as we haven't implemented truly responsive UI just yet
Angular and Electron - App Works 3

Enter a different round (Double Jeopardy!) or show number to see the data return from our collection. And finally, click on a question to confirm we are able to see a question and answer:
Jeopardy Question and Answer

Conclusion

In this post, we've successfully used Angular and Electron to create an application that allows you to explore our DocumentDB collection using our .NET Core API. There are some definite improvements to the UI, but for now, we wanted to build a working application that performs this basic function. We will include this first series of blog posts tomorrow when we add full text search capabilities to our application using Azure Search.

If you have any questions regarding integrating angular and electron or DocumentDB and angular, please comment on this post, or reach out directly via Twitter (@SigaoStudios) or FaceBook as we are eager to hear back from you.

Check out part 3 of our series to add Azure Serach into the mix!

Until tomorrow!

"I like big data for $400" - Implementing a Jeopardy! Question Explorer using DocumentDB, Azure Search, AngularJS and Electron - Part 1 "I Like Big Data for $400" - Adding Azure Search to the DocumentDB stack - Part 3