Angular2 + Spin.js

I’m currently working on a project using Angular2 and as with many applications I needed a loading indicator. After doing a bit of searching I decided to use Spin.js, a really cool JavaScript library that can be used to render a ‘spinner’ without using external images/css. I then started thinking, how do I do this with Angular2… do I need a component that I put in every page, is there a way to share a single root level component with my app, etc. I then came across a really good blog by Tom Buyse explaining how to setup an observable subscription in order to control a single component using a service that you pass around your application (Thanks Tom!). I think this can be a really useful pattern of development for a number of purposes, but here’s a look at how I created an Angular2 spinner component with Spin.js.

The spinner component

First of all,I created the component that I’ll put an instance of in my root application view.

@Component({
   selector: 'ng2-spinner',
   templateUrl: './app/spinner/spinner.component.html'
})
export class SpinnerComponent implements OnInit, OnDestroy {

In the component I exposed the Spin.js options as Angular2 component inputs

@Input() lines: number = 12; // The number of lines to draw
@Input() length: number = 20; // The length of each line
@Input() width: number = 12; // The line thickness
@Input() radius: number = 50; // The radius of the inner circle
@Input() scale: number = 1.0; // Scales overall size of the spinner
...

Then on the component’s ngOnInit callback I initialize the component with a setup of inputs from the view specifying spin.js options. It’s important to do
so in the ngOnInit callback as in the constructor would be too early and the inputs are not passed from the parent view element.

ngOnInit() {
    this.initSpinner();
    this.createServiceSubscription();
}

private initSpinner() {
    let options = {
        lines: this.lines,
        length: this.length,
        width: this.width,
        radius: this.radius,
        scale: this.scale,
        corners: this.corners,
        color: this.color,
        opacity: this.opacity,
        rotate: this.rotate,
        direction: this.direction,
        speed: this.speed,
        trail: this.trail,
        fps: this.fps,
        zIndex: 2e9, // Artificially high z-index to keep on top
        className: this.className,
        top: this.top,
        left: this.left,
        shadow: this.shadow,
        hwaccel: this.hwaccel,
        position: this.position
    };
    console.log('Creating spinner with options:');
    console.log(JSON.stringify((options)));
    this.spinner = new Spinner(options);
}

Now we’ve got an instance of the spin.js object created than we can show and hide as needed. Next comes the magic, we create a service that holds a shared observable in order to control the spinner component from other components within the application. Note: it’s important to import the Rxjs ‘share’ operator in order to create the shared observable, I did so in a file called rxjs-operators.ts that I’ve imported into my main application component.

@Injectable()
export class SpinnerService {
    private spinnerObserver: Observer;
    public spinnerObservable: Observable;

    constructor() {
        this.spinnerObservable = new Observable(observer => {
                this.spinnerObserver = observer;
            }
        ).share();
    }
}

The spinner component can then subscribe to this observable and receive notifications from the service as whether it should show or hide the spinner. I’ve also called this subscription code from within the ngOnInit callback, though it’d be fine to do so within the component constructor as well.

private createServiceSubscription() {
        this.subscription = this.spinnerService.spinnerObservable.subscribe(show => {
            if (show) {
                this.startSpinner();
            } else {
                this.stopSpinner();
            }
        });
    }

The service can use the observable in order to trigger the show and hide states in the component.

show() {
    if (this.spinnerObserver) {
        this.spinnerObserver.next(true);
    }
}

hide() {
    if (this.spinnerObserver) {
        this.spinnerObserver.next(false);
    }
}

Now I can drop an instance of my spinner component in my main app component specifying it’s view options:

<ng2-spinner [radius]="25" [lines]="15" [width]="8" 
[length]="15" [opacity]="0.1" [shadow]="true"></ng2-spinner>

And pass around the spinner service to control showing/hiding it everywhere else in my application

constructor(private spinnerService: SpinnerService) {
}

public spin(event: MouseEvent): void {
    event.preventDefault();
    this.spinnerService.show();
    setTimeout(() => {
        this.spinnerService.hide();
    }, 1000);
}

I think this is a really useful pattern and can imagine it’ll come in handy on my current and future projects. So far I really like Angular2, I think it brings some sanity back to client side web application development. I can only imagine I’ll get more than 1 grumble about that comment, but so far I’ve found it to be a great development experience.

You can check out my demo project with the full code listings at:

https://github.com/seanlmcgill/ng2spin

References

http://tombuyse.com/creating-a-loading-indicator-in-angular-2-ionic-2/

http://spin.js.org/

Unit Testing Ionic Applications Part 2: Concepts and Project Structure

Jasmine

Jasmine is a BDD (behavior driven development) testing framework that can be used to write JavaScript unit tests and is currently the most popular framework choice for testing angular components. In a nutshell it helps us to create suites of tests that each state a behavior we expect to occur in our program’s logic for it to run correctly. You’ll often see Jasmine test descriptions that state thing’s like ‘it should’ do this, ‘it can’ do that, ‘it cant do that’, etc each describing what a small piece of logic in our program does.

Jasmine concepts and terminology

  • Suite: a group of specs, contained within a ‘describe block’
  • Spec: a function block that contains one or more test expectations, contained within an ‘it block’
  • Matchers: functions that will be used to validate test expectations, these can compare values or objects in order to assert an expected result
  • Spies: mock stand-in functions that can be used to validate logic, you can check a spy to see if it’s been called, not been called, called one or more times, etc.

Below is a small code snippet containing the obligatory ‘add 2 numbers’ test to illustrate a super simple example of a suite containing one spec that is uses a matcher to assert an expectation.

describe('Simple math tests', function() {
  it('Should add 2 numbers', function() {
   var calculator = new Calculator();
   expect(calculator.add(2, 2)).toBe(4);
  });   
 });

In more realistic coding scenarios we’ll also need to use spies which allow us to provide mock implementations that return results to setup particular test expectations and also let us know if a given function has been called or not.

There are several ways to utilize spies with Jasmine including:

SpyOn an Existing Object

To spy on an object, is to create a stand-in for particular function in order to assert whether or not it has been called and also creates opportunity to arrange for a particular results to be returned in order to facilitate a test sceanrio. It’s important as well that that this stand-in function is called instead of the real code so that our test is isolated from interacting with other systems through network requests, database requests, file io, etc.

In the example below I’m creating a stand-in function called ‘createAccount’ that will be called and then testing that our manager object has called that function when its ‘addUser’ function is called:

Note: if instead of just calling the stand-in function that does nothing you really need the underlying function to be called as well to complete your test scenario you can call spyOn(…).andCallThrough() which will execute both the stand-in and the real function.

describe('User account creation tests', function() {
    it('Should create a new user account', function() {
      var accountService = new AccountService();
      var accountManager = new AccountManager(accountService);
      spyOn(accountService, 'createAccount');
      accountManager.addUser('John', 'Doe');
      expect(accountService.createAccount).toHaveBeenCalled();
    });   
    .
    .
    .
  });

 

Create a Spy Object

Another facility Jasmine provides is the ability to create completely fake stub objects that can do absolutely nothing, or can provide a fake test stub implementation.

describe('User account creation tests', function() {
    it('Should create a new user account', function() {
      var accountService = jasmine.createSpyObj('AccountService', ['userExists', 'createAccount']);
      var accountManager = new AccountManager(accountService);
      accountService.userExists.and.callFake(function(){
        return true;
      });
      spyOn(accountService, 'createAccount')
      expect(accountService.createAccount).not.toHaveBeenCalled();
    });   
    .
    .
    .
  });

Use a Spy to Arrange an Expectation

Either by spying on or creating a spy object we can also return a specific result that is needed in order to test a specific scenario, in the example below I’m returning a result from a spy object stub function to say ‘the user name already exists, so the account should not have been created’

describe('User account creation tests', function() {
      it('Should not create a new user account if the user already exists', function() {
        var accountService = new AccountService();
        var accountManager = new AccountManager(accountService);
        spyOn(accountService, 'createAccount');
        spyOn(accountService, 'userExists').and.returnValue(true);
        accountManager.addUser('John', 'Doe');
        expect(accountService.createAccount).not.toHaveBeenCalled();
      });   
    });

Unit Test File Structure

File structure is a very important part of creating a well organized and maintainable software project. How unit tests are woven into the code is no exception and there are a couple of obvious ways to do so:

  • Add all your tests in a test directory
  • Add your tests along side the code you are testing

The best way I’ve found is a way I’d read about where you use the .spec.js naming convention, adding your tests right along side the components they are testing. This makes it really easy to find unit tests for a given component as well as to see what components have or have not had tests written for them.

Example Project Structure

- myApp
  karma.conf.js
 - www
  - app
   - dashboard
       dashboardController.js
       dashboardController.spec.js
       dashboardService.js
       dashboardService.spec.js
       dashboardModule.js
   - login
       loginController.js
       loginController.spec.js
   .
   .
   .

Test Cleanup Cordova Hook

One issue with creating your unit tests along side your production code in an Ionic project is that they will then be copied out to your application build folder along with the production code. Below is an example Cordova hook I’ve written however that will be used to remove .spec.js files from the build output, removing them from app.  Just add a hook file under your project’s hooks -> after_prepare directory and thefollowing hook code will take care of the clean-up:

note: it requires the node ‘del’ package, so be sure to npm install that as well

#!/usr/bin/env node
var del = require('del');
var specFiles = ['platforms/ios/www/app/**/*.spec.js', 
'platforms/android/assets/www/app/**/*.spec.js'];

// Output the names of the files we're about to delete on the console for diagnostic puposes
del(specFiles, {dryRun: true})
.then(function (paths) {
  console.log('Spec files to be removed:\n', paths.join('\n'));
});

// Delete the test .spec.js files
del(specFiles).then(function () {
  console.log("Tests have been removed from your build");
});

Unit Testing Ionic Applications Part 1: Tools and Setup

Unit testing is an important part of the software development process as it allow a developer to spell out their intent by creating tests containing assertions that prove it. In a dynamic language like JavaScript it’s even more so important to do so for the project’s stability, quality and longer term maintainability. JavaScript’s great flexibility and power to quickly create software also presents a lot of opportunity to create problems if checks and balances are not in place to maintain order. In the following blog series I’ll talk a bit about how to create unit tests for applications written with Ionic, a mobile hybrid framework based on Angular.js and Cordova which are JavaScript based technologies.

The Components

Ionic uses Gulp, a JavaScript based streaming build system, out of the box so we’ll also leverage Gulp to create a task for running unit tests and seeing their results. Gulp tasks are generally composed by using an existing Gulp module, as thousands exist to perform almost any task you can think of, and some custom code to configure those modules for your project. I’ll create a task using Karma, a JavaScript test runner, that will load our test files and execute them in the headless broswer called PhantomJS and then display the results on the command line. Finally, we’ll use the Jasmine test framework along with Angular mocks in order to create suites of tests containing assertions about our code that we expect in order for it to run properly.

In our ionic project directory the following npm and bower commands will install the required packages:

npm install karma --save-dev
npm install karma-jasmine --save-dev
npm install karma-phantomjs-launcher --saved-dev
npm install -g karma-cli
npm install karma-spec-reporter --save-dev
npm -g install phantomjs
bower install angular-mocks --save-dev

Initialize Karma

Karma uses a configuration file to tell it how to run tests, what source files it should include, how to display results and more. In order to create a configuration file we can use karma’s command line client:

karma init

Karma guides you through it’s initial setup on the command line with a series of questions, for this example I specified:

  • Testing framework: Jasmine
  • Require.js: no
  • Capture any browsers automatically: PhantomJS
  • Source location: www/js/**/*.spec.js
  •  (Default for the rest)

Detailed Test Output on the Console

By default Karma is configured to use the ‘progress’ reporter which will show an ascii progress bar in the console output with a summary of the test results. In order to see a bit more detail about what tests are passing/failing change the reporter from ‘progress’ to ‘spec’ in the generated Karma config file:

// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['spec'],

Gulp task to run our tests

In the gulpfile.js that is created with our Ionic project, I’ll add a task to kick off Karma and run our unit tests:

var Server = require('karma').Server;
gulp.task('test', function (done) {
    new Server({
        configFile: __dirname + '/karma.conf.js',
        singleRun: true
    }, done).start();
});

Run Tests and See Results

Now any tests that have been created in our project can be run and have the results displayed in detail on the command line by running ‘gulp test’ in our project directory

$ gulp test
    [22:32:38] Using gulpfile C:\Development\Blogging\ionicut\gulpfile.js
    [22:32:38] Starting 'test'...
    05 03 2016 22:32:38.745:INFO [karma]: Karma v0.13.21 server started at http://localhost:9876/
    05 03 2016 22:32:38.757:INFO [launcher]: Starting browser PhantomJS
    05 03 2016 22:32:40.488:INFO [PhantomJS 2.1.1 (Windows 8 0.0.0)]: Connected on socket /#5PSNRP-konOjgWF1AAAA with id 41621962
    
      Dashboard controller tests
        When the dashboard view is shown
          ✓ Should show a one time welcome message
          ✓ Should store that the message was shown in local storage
    
    PhantomJS 2.1.1 (Windows 8 0.0.0): Executed 2 of 2 SUCCESS (0.005 secs / 0.039 secs)
    TOTAL: 2 SUCCESS
    
    [22:32:40] Finished 'test' after 2.08 s

In the next part of this blog series I’ll talk a bit about Jasmine test framework concepts and how to structure tests within an Ionic project