Angular2 + Electron + Yeoman + SASS + Bootstrap: A complete guide to setting up a new desktop SPA

As a single developer, creating and developing a big application such as Slidebean on your own is no easy task. In comes Angular to save the day. As the main framework for Slidebean, Angular — not without its kinks here and there — has exceptionally performed and exceeded my expectations as an application framework.

Link to original article: here!

 

That being said, Angular 2 is coming. And as much as Angular 1 was a groundbreaking framework, ng2 — as the cool kids call it — comes with big improvements (and big changes!) across the board. Alas, it’s time to move on.

The following is a guide I created after spending a lot of time finding out how to properly set up a project aimed for production which included:

 

Note: I’ll use ES6 js instead of TypeScript in this guide. Adding TypeScript should be easy enough though! If you’re into TypeScript, ES6 should feel very similar too.

Prerequisites

1) npm

Before you start, make sure you have installed node and npm on your system: https://docs.npmjs.com/getting-started/installing-node

 

2) Yeoman & Bower

Now that you have npm, install yeoman and bower using your command line or Terminal like so:

npm install -g yo
npm install -g bower

We’ll need a Yeoman Angular2 generator. There are several Angular2 generators out there, though personally I prefer to use the official generator. To install it, simply enter:

npm install -g generator-angular2

3) SASS

Additionally, if you’d like to use SASS (highly recommended), install it following these instructions: http://sass-lang.com/install

 

Initializing the Angular2 project with Yeoman

First off, we’ll use Yeoman to create the basic file structure for us. Create a folder where you want to store the project, move into that folder and then enter:

yo

You’ll see a menu with several options:

Go ahead and choose Run a generator > Angular2.

Yeoman will begin doing its thing, setting up the files and directories needed to run your project. After it’s done, you should have a sample but fully functional app with the following file structure:

  • src/  →  where all our project source files are located
  • build/  →  where final, built files will be placed
  • node_modules/  →  stores packages installed via npm
  • package.json  →  our main npm script
  • gulpfile.js→  our Gulp script

Wanna give it a spin? Simply enter:

npm start

You should see your app come to live in your default browser via the localhost:8000 address.

“Yes! Our work here is done.” Uh, no, not quite.

A brief explanation on npm scripts and gulp tasks

You may skip this section if you’re already familiar with npm and gulp.

To understand what occurs when you enter npm start, open up the package.json file in your favorite editor, and check out the following entry:

"scripts": {
  "build": "gulp",
  "start": "gulp dev"
},

Basically, npm is mapping the npm start command with the start entry in this setting, which in turn is calling gulp dev underneath. What gulp dev is doing is running the dev task described inside the gulpfile.js file. Open up gulpfile.js in your editor and look for the following entry:

// run development task
gulp.task('dev', ['watch', 'serve']);

So, this means the dev task runs both the watch and serve tasks simultaneously. Feel free to check out what these individual tasks do in detail but, in short: serve builds and loads the app in a web browser (serving it using a library called gulp-webserver in the default address localhost:8000) and watch detects any changes you make to your source files and refreshes the app (very handy during development).

We’ll need to understand how gulp works, so we can modify and create new tasks to fit our needs. With that out of the way, let’s make some changes.

Restructuring the src/ directory

Yeoman created a src/ directory with some sample files to get us started. That’s great and all but, as our application grows, having everything in one single directory is a big no no. We’ll now go ahead and create a well organized file structure.

Let’s start by creating the following directories inside src/:

  • app/  →  where our Angular2 files will be placed
  • styles/  →  contains our css/SASS files
  • fonts/  →  contains our font files (if any)
  • images/  →  contains our images
  • electron/  →  contains our electron stuff

We’ll focus on the app/ directory first. Delete all the sample files Yeoman created except index.html, and then create two files under app/ named main.js and app.js:

  • app/main.js will take care of bootstraping our application
  • app/app.js will be our first Angular2 component, which will be loaded inside main.js

Set the contents of app.js to:

import {Component} from 'angular2/core';
 
@Component({
  selector: 'sb-app',
  template: '<h1>Our app running properly now :)</h1>'
})
 
export class Application {
}

As you can see, Application is just an empty Angular2 component which displays a simple template. We use the selector "sb-app", though feel free to use anything you’d like; just make sure you use this selector in your src/index.html file where you wish to load the app.

Now set the contents of main.js to:

import {bootstrap} from 'angular2/platform/browser';
import {Application} from 'app/app';
 
bootstrap(Application);

This simple script kickstarts our Angular2 app by loading and bootstraping our Application component. Nothing too fancy.

Why two separate files just to do this?

Good question. Right now, it may seem like overkill to have two files just to bootstrap our app. However, as your application grows, your bootstrapping code will surely grow. By keeping main.js and app.js separated from each other, we separate concerns: main.js will strictly handle bootstrapping code and global dependency injection; app.js will handle business logic initialization stuff, such as routes and generally any other “non-plumbing” logic. For the most part, main.js will largely remain intact.

Now, let’s adjust src/index.html to load our new app correctly:

<!doctype html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>castleblack</title>
  </head>
  <body>
 
    <sb-app></sb-app>
 
    <script src="lib/es6-shim.min.js"></script>
    <script src="lib/angular2-polyfills.js"></script>
    <script src="lib/traceur-runtime.js"></script>
    <script src="lib/system-csp-production.src.js"></script>
    <script src="lib/Reflect.js"></script>
 
    <script>
      System.config({defaultJSExtensions: true});
    </script>
 
    <script src="lib/angular2.js"></script>
    <script src="lib/Rx.js"></script>
 
    <script>
      System.import('app/main').catch(console.log.bind(console));
    </script>
 
  </body>
</html>

All right! Now let’s test everything. Run these commands:

npm run build
npm start

If you followed all the steps, you should see a new tab in your browser with something like this:

Customizing the Gulp script

Yeoman created our gulpfile.js script and added several tasks to be able to run the sample project. However, there are a few things missing from the script, such as cleaning up the build directory. Plus, it’s a good idea to group similar tasks by prefixing their name, keeping things tidy. We’ll be working on the gulpfile.js script in this section.

config vars

Not a fan of having hardcoded strings all over the place, so let’s add a variable called config, which will store the main paths we’ll use throughout our gulpfile.js script:

gulpfile.js
var gulp = require('gulp'),
    rename = require('gulp-rename'),
    traceur = require('gulp-traceur'),
    webserver = require('gulp-webserver');
 
var config = {
  sourceDir: 'src',
  buildDir: 'build',
  npmDir: 'node_modules'
};

clean task

We’ll add a task to delete everything on the build/ folder before our project is built. This way we avoid issues where the build/ folder has outdated or extra files which are no longer needed. We’ll need to install a new npm package called del:

npm install --save-dev del

Let’s include del and a new task in gulpfile.js to empty the folder:

gulpfile.js
var gulp = require('gulp'),
    del = require('del'),
    rename = require('gulp-rename'),
    traceur = require('gulp-traceur'),
    webserver = require('gulp-webserver');
 
var config = {
  sourceDir:   'src',
  buildDir:    'build',
  npmDir:      'node_modules'
};
 
gulp.task('clean', function() {
  return del(config.buildDir + '/**/*', { force: true });
});

We’ll use this new clean task in the next section.

frontend tasks

Yeoman generated a few tasks for us, such as "dependencies", "js", "html", "css". These are all tasks related to building our Angular2 app. At some point, we’ll also have tasks related solely to building our Electron app; before we do this, let’s group all the Angular2 tasks with the prefix "frontend". Plus, we’ll go ahead an use the config var we created earlier. Finally, we’ll group the development tasks as well.

var gulp = require('gulp'),
    del = require('del'),
    rename = require('gulp-rename'),
    traceur = require('gulp-traceur'),
    webserver = require('gulp-webserver');
 
var config = {
  sourceDir: 'src',
  buildDir:  'build',
  npmDir:    'node_modules'
};
 
gulp.task('clean', function() {
  return del(config.buildDir + '/**/*', { force: true });
});
 
gulp.task('dev', [
  'dev:watch',
  'dev:serve'
]);
 
gulp.task('dev:serve', function () {
  gulp.src(config.buildDir)
    .pipe(webserver({
      open: true
    }));
});
 
gulp.task('dev:watch', function() {
  gulp.watch(config.sourceDir + '/**/*.js',   ['frontend:js']);
  gulp.watch(config.sourceDir + '/**/*.html', ['frontend:html']);
  gulp.watch(config.sourceDir + '/**/*.css',  ['frontend:css']);
});
 
gulp.task('frontend', [
  'frontend:dependencies',
  'frontend:js',
  'frontend:html',
  'frontend:css'
]);
 
gulp.task('frontend:dependencies', function() {
  return gulp.src([
    config.npmDir + '/traceur/bin/traceur-runtime.js',
    config.npmDir + '/systemjs/dist/system-csp-production.src.js',
    config.npmDir + '/systemjs/dist/system.js',
    config.npmDir + '/reflect-metadata/Reflect.js',
    config.npmDir + '/angular2/bundles/angular2.js',
    config.npmDir + '/angular2/bundles/angular2-polyfills.js',
    config.npmDir + '/rxjs/bundles/Rx.js',
    config.npmDir + '/es6-shim/es6-shim.min.js',
    config.npmDir + '/es6-shim/es6-shim.map'
  ])
    .pipe(gulp.dest(config.buildDir + '/lib'));
});
 
gulp.task('frontend:js', function() {
  return gulp.src(config.sourceDir + '/**/*.js')
    .pipe(rename({
      extname: ''
    }))
    .pipe(traceur({
      modules: 'instantiate',
      moduleName: true,
      annotations: true,
      types: true,
      memberVariables: true
    }))
    .pipe(rename({
      extname: '.js'
    }))
    .pipe(gulp.dest(config.buildDir));
});
 
gulp.task('frontend:html', function () {
  return gulp.src(config.sourceDir + '/**/*.html')
    .pipe(gulp.dest(config.buildDir))
});
 
gulp.task('frontend:css', function () {
  return gulp.src(config.sourceDir + '/**/*.css')
    .pipe(gulp.dest(config.buildDir))
});

A lot of changes here. Do notice:

  • We replaced all the "src", "build" and "node_modules" hardcoded strings with the config variable properties.
  • We renamed the dependencies, js, html and css tasks, prefixing thcode with "frontend:".
  • We added a new task simply called "frontend", which calls all the other tasks.

Ok so now let’s adjust the scripts section of package.json to use our new gulp tasks:

{
  "name": "sampleapp",
  "version": "0.0.0",
  "scripts": {
    "start": "gulp clean && gulp frontend && gulp dev",
    "build": "gulp clean && gulp frontend"
  },
  "dependencies": {

Now we have two npm commands we can use:

  • npm start cleans the build folder, builds the Angular app, and serves it.
  • npm run build cleans the build folder and builds the Angular app.

Preparing the Electron app

We’ll use Electron to package our Angular2 app as a native desktop application. Remember the src/electron directory we created earlier? We’ll create a file in there called main.js, which will be the main file electron will use to start the native app:

'use strict';
const electron = require('electron');
 
// 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.
var mainWindow;
 
electron.app.on('window-all-closed', function() {
  if (process.platform != 'darwin') {
    app.quit();
  }
});
 
electron.app.on('ready', function() {
 
  mainWindow = new electron.BrowserWindow({ width: 1200, height: 750 });
  mainWindow.loadURL('file://' + __dirname + '/index.html');
 
  mainWindow.webContents.openDevTools();
 
  mainWindow.on('closed', function() {
    mainWindow = null;
  });
});

If you’re not familiar with electron, this is the base script for running the native app. It simply:

  1. creates a new window
  2. loads index.html (from our Angular app, more on that below)
  3. opens the dev tools (make sure you disable this at some point)
  4. quits the application when needed

We’ll also need a basic package.json  file specifically for our electron app, so let’s create one in the same directory. Most importantly, it must specify which is the main electron script (the one we just created):

{
  "main": "main.js",
  "version": "0.0.0",
  "name": "sampleapp"
}

All right, now with both of these files under src/electron, we’ll adjust gulpfile.js to add a task that copies the electron files to the build folder:

gulp.task('electron', function() {
  return gulp.src([
    config.sourceDir + '/electron/main.js',
    config.sourceDir + '/electron/package.json'
  ])
    .pipe(gulp.dest(config.buildDir));
});

So far we just have the new files and a gulp task to copy them to the build folder. Nothing fancy yet.

To actually build and run the electron app, we’ll need a few npm packages. Go ahead and install these:

npm install --save-dev gulp-atom-electron
npm install --save-dev gulp-symdest

After that’s done, we’ll modify gulpfile.js to:

  • include the new npm packages
  • specify a a packages directory where the electron packages will be placed
  • add tasks to build the electron packages
var gulp = require('gulp'),
    del = require('del'),
    rename = require('gulp-rename'),
    traceur = require('gulp-traceur'),
    webserver = require('gulp-webserver'),
    electron = require('gulp-atom-electron'),
    symdest = require('gulp-symdest');
 
var config = {
  sourceDir:   'src',
  buildDir:    'build',
  packagesDir: 'packages',
  npmDir:      'node_modules'
};
 
gulp.task('clean', [
  'clean:build',
  'clean:package'
]);
 
gulp.task('clean:build', function() {
  return del(config.buildDir + '/**/*', { force: true });
});
 
gulp.task('clean:package', function() {
  return del(config.packagesDir + '/**/*', { force: true });
});
gulp.task('package', [
  'package:osx',
  'package:linux',
  'package:windows'
]);
 
gulp.task('package:osx', function() {
  return gulp.src(config.buildDir + '/**/*')
    .pipe(electron({
      version: '0.36.7',
      platform: 'darwin'
    }))
    .pipe(symdest(config.packagesDir + '/osx'));
});
 
gulp.task('package:linux', function() {
  return gulp.src(config.buildDir + '/**/*')
    .pipe(electron({
      version: '0.36.7',
      platform: 'linux'
    }))
    .pipe(symdest(config.packagesDir + '/linux'));
});
 
gulp.task('package:windows', function() {
  return gulp.src(config.buildDir + '/**/*')
    .pipe(electron({
      version: '0.36.7',
      platform: 'win32'
    }))
    .pipe(symdest(config.packagesDir + '/windows'));
});

 

Finally, we’ll add new scripts in the main package.json file to make use of these new gulp tasks:

{
  "name": "castleblack",
  "version": "0.0.0",
  "scripts": {
    "start": "gulp clean:build && gulp frontend && gulp dev",
    "build": "gulp clean:build && gulp frontend && gulp electron",
    "package": "gulp clean && gulp frontend && gulp electron && gulp package"
  },

I know I know, that was a lot to digest. But now, simply run npm run package, and look for your built electron app inside the packages directory!

  1. The frontend gulp task leaves all the Angular2 files in the build/ directory.
  2. The electron task copies src/electron/main.js and src/electron/package.json to the root path of build/. This is why in main.js the index.html page is loaded from the root directory as well.
  3. The package tasks grab everything that is in the build/ folder, and packages it.

Angular2 + Electron: Quirks and Pitfalls

So our app is not quite ready to work as an electron native app. As soon as you start importing files and using routes, you’ll run into a few issues.

<base href="/">

In order to make Angular2’s pushState routing work, we must add a <base href="/"> tag in the <head> section of our index.html. However, this breaks includes in Electron, and you’ll start seeing issues where files are trying to be loaded via file:// from the wrong directory. To address this, remove the <base href="/"> tag and add this setting as part of your Angular2 app’s bootstrap call.

We’ll add this to our src/app/main.js file:

import {bootstrap} from 'angular2/platform/browser';
import {provide} from 'angular2/core'
import {APP_BASE_HREF} from 'angular2/router';
import {Application} from 'app/app';
 
bootstrap(Application, [
  provide(APP_BASE_HREF, { useValue: '/' })
]);

This effectively sets the base href setting for the Angular app, but does not interfere with Electron’s file loading paths.

HashLocationStragegy

If you’re using Angular2’s router providers, you’ll run into an issue where routes cannot be loaded in HTML5 mode.

Say you have a main route called /dashboard. This works flawlessly when you’re serving your Angular2 app in the web, but since Electron serves files from the file system, it looks for /dashboard as an actual directory and fails.

To fix this, simply change your Angular2 app’s location strategy to use HashLocationStrategy. In the previous example, your /dashboard route would become /#/dashboard instead. But since you’re serving your app as a native desktop app and urls are not visible, this is probably not a big deal.

Again, we modify src/app/main.js to use HashLocationStrategy:

import {bootstrap} from 'angular2/platform/browser';
import {provide} from 'angular2/core'
import {ROUTER_PROVIDERS, APP_BASE_HREF, LocationStrategy, HashLocationStrategy} from 'angular2/router';
import {Application} from 'app/app';
 
bootstrap(Application, [
  ROUTER_PROVIDERS,
  provide(APP_BASE_HREF, { useValue: '/' }),
  provide(LocationStrategy, { useClass: HashLocationStrategy })
]);

 

Sass, Bower and Bootstrap (Optional)

Bower is not entirely necessary; you can do away with just npm. However, if you wish to use it, run this command and follow the instructions:

bower init

After that’s done, you’ll have a new file in your project called bower.json. To install dependencies, use the bower install --save <package name> command.

We’ll go ahead and install the official SASS version of Bootstrap:

bower install --save bootstrap-sass-official

Now we have all the Bootstrap files we need under the bower_components/ folder. Just as node_modules/ contains all packages installed via npm, bower_components/ contains everything we install via bower.

Let’s modify gulpfile.js to copy the Bootstrap js scripts to our build folder as well.

var config = {
  sourceDir:   'src',
  buildDir:    'build',
  packagesDir: 'packages',
  npmDir:      'node_modules',
  bowerDir:    'bower_components'
};
gulp.task('frontend:dependencies', function() {
  return gulp.src([
    config.npmDir + '/traceur/bin/traceur-runtime.js',
    config.npmDir + '/systemjs/dist/system-csp-production.src.js',
    config.npmDir + '/systemjs/dist/system.js',
    config.npmDir + '/reflect-metadata/Reflect.js',
    config.npmDir + '/angular2/bundles/angular2.js',
    config.npmDir + '/angular2/bundles/angular2-polyfills.js',
    config.npmDir + '/rxjs/bundles/Rx.js',
    config.npmDir + '/es6-shim/es6-shim.min.js',
    config.npmDir + '/es6-shim/es6-shim.map',
    config.bowerDir + '/jquery/dist/jquery.min.js',
    config.bowerDir + '/bootstrap-sass/assets/javascripts/bootstrap.min.js'
  ])
    .pipe(gulp.dest(config.buildDir + '/lib'));
});

And now we modify our index.html page to include the new scripts:

<script>
  System.config({defaultJSExtensions: true});
</script>
 
<script src="lib/jquery.min.js"></script>
<script src="lib/bootstrap.min.js"></script>
<script src="lib/angular2.js"></script>
<script src="lib/Rx.js"></script>

Besides the scripts, we also need to include Bootstrap’s css files. If we were not using SASS, it would be a matter of copying the appropriate css files to the build folder, and including them in index.html. But since we’ll be using SASS, let’s do so properly!

Let’s create our first stylesheet. Name it global.scss and place it in the src/styles directory:

@import "_variables";
@import "bootstrap";

What? Just two lines? Well, for now, yes. We’re first importing a file named _variables.scss, which will contain our global SASS variables and overrides Bootstrap’s default values; and then we’re importing Bootstrap itself. Let’s go ahead and create the file _variables.scss and place it in the same directory:

$body-bg: #CCC;
$text-color: #333;
$brand-primary: #9b59b6;

It’s important to keep our global variables in a separate file, since we’ll most probably be needing them in other places as our app grows.

Finally, let’s load global.scss in index.html. One assumption we can make is that global.scss will become global.css when built, so, in general, we can refer to all .scss files as .css.

<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>sampleapp</title>
  <link rel="stylesheet" href="styles/global.css">
</head>

Aren’t we forgetting something? We haven’t set up SASS. We’ll need an npm package named gulp-sass, so let’s install it:

npm install --save-dev gulp-sass

We’ll replace the frontend:css task with one called frontend:sass, which will compile our .scss files into .css:

var gulp = require('gulp'),
    del = require('del'),
    rename = require('gulp-rename'),
    traceur = require('gulp-traceur'),
    sass = require('gulp-sass'),
    webserver = require('gulp-webserver'),
    electron = require('gulp-atom-electron'),
    symdest = require('gulp-symdest');
gulp.task('frontend', [
  'frontend:dependencies',
  'frontend:js',
  'frontend:html',
  'frontend:sass'
]);
gulp.task('frontend:sass', function () {
  return gulp.src(config.sourceDir + '/**/*.scss')
    .pipe(sass({
      style: 'compressed',
      includePaths: [
        config.sourceDir + '/styles',
        config.bowerDir + '/bootstrap-sass/assets/stylesheets'
      ]
    }))
    .pipe(gulp.dest(config.buildDir));
});

Notice we also need to pass a few directories as the includePaths parameter for SASS. This is what makes the @import "bootstrap" directive in global.scss work! We also pass the src/styles directory itself to allow imports from the same directory.

Summary

Did you follow the whole guide? Well, kudos to you, this was a lot of code! I hope by now you:

  • are familiar with Yeoman, npm, bower, and gulp;
  • have a basic understanding of how gulp scripts work;
  • know how to set up a new Angular2 project using the official Yeoman generator;
  • are able to use SASS and Bootstrap in an Angular2 project;
  • are able to deploy a simple Angular2 native app with Electron!

Let me know in the comments if you have any questions or comments, or if you spotted or ran into any issues.

 

 

Happy coding ^____^

Posted by