Skip to content

Latest commit

 

History

History
363 lines (273 loc) · 10 KB

README.md

File metadata and controls

363 lines (273 loc) · 10 KB

angular-pouchdb

Build Status Coverage Status

AngularJS v1.x wrapper for PouchDB

A lightweight AngularJS (v1.x) service for PouchDB that:

  • Wraps Pouch's methods with $q
  • Makes Angular aware of asynchronous updates

Disclaimer: angular-pouchdb works by monkey patching PouchDB's public API. Your milage may vary.

Why?

Since PouchDB is asynchronous, you will often need to call $scope.$apply() before changes are reflected on the UI. For example:

angular.controller('MyCtrl', function($scope, $window) {
  var db = $window.PouchDB('db');
  db.get('id')
    .then(function(res) {
      // Binding may/may not be updated depending on whether a digest cycle has
      // been triggered elsewhere as Angular is unaware that `get` has resolved.
      $scope.one = res;
    });

  var db2 = $window.PouchDB('db2');
  db.get('id')
    .then(function(res) {
      $scope.$apply(function() {
        // Value has been bound within Angular's context, so a digest will be
        // triggered and the DOM updated
        $scope.two = res;
      });
    });
});

Writing $scope.$apply each time is laborious and we haven't even mentioned exception handling or $digest already in progress errors.

angular-pouchdb handles $scope.$apply for you by wrapping PouchDB's promises with $q. You can then use its promises as you would with any Angular promise, including the .finally method (not in the Promises A+ spec).

angular.controller('MyCtrl', function($scope, pouchDB) {
  var db = pouchDB('db');
  db.get('id')
    .then(function(res) {
      // Update UI (almost) instantly
      $scope.one = res;
    })
    .catch(function(err) {
      $scope.err = err;
    })
    .finally(function() {
      $scope.got = true;
    });
});

Put another way, angular-pouchdb is not required to integrate PouchDB and AngularJS; they can and do happily work together without it. However, angular-pouchdb makes it more convenient to do so.

Usage

  1. Install angular-pouchdb via Bower:

    bower install --save angular-pouchdb
  2. Add pouchdb as a module dependency:

    angular.module('app', ['pouchdb']);
  3. Inject the pouchDB service in your app:

    angular.service('service', function(pouchDB) {
      var db = pouchDB('name');
    });

From then on, PouchDB's standard promises API applies. For example:

angular.controller('MainCtrl', function($log, $scope, pouchDB) {
  var db = pouchDB('dbname');
  var doc = { name: 'David' };

  function error(err) {
    $log.error(err);
  }

  function get(res) {
    if (!res.ok) {
      return error(res);
    }
    return db.get(res.id);
  }

  function bind(res) {
    $scope.doc = res;
  }

  db.post(doc)
    .then(get)
    .then(bind)
    .catch(error);
});

See examples for further usage examples.

Event emitters

angular-pouchdb decorates PouchDB event emitters (such as those used by replicate.{to,from}) with a .$promise property to make them more useful within Angular apps, per the following mapping:

Event Deferred method
change .notify
paused .notify
complete .resolve
reject .reject

For example:

var db = pouchDB('test');
db.replicate.to('https://couch.example.com/remote').$promise
  .then(null, null, function(progress) {
    console.log('replication status', progress);
  })
  .then(function(result) {
    console.log('replication resolved with', result);
  })
  .catch(function(reason) {
    console.error('replication failed with', reason);
  })
  .finally(function() {
    console.log('done');
  });

Options

pouchDBProvider.methods

A hash of pouchDBMethod: decorator pairs, with arbitrary nesting. Defaults to POUCHDB_METHODS (a constant mapping PouchDB's core API).

Example:

pouchDBProvider.methods = {
  get: 'qify',
  replicate: 'replicate'
};

pouchDBDecorators

A service containing decorator functions used to wrap PouchDB's. By default, this includes qify, eventEmitter and replicate.

Since they're contained in a service, they can be substituted per standard dependency injection semantics, or reused outside of angular-pouchdb.

FAQ

Does this work with PouchDB plugins?

angular-pouchdb only wraps PouchDB's core API by default. If you need to wrap other methods (for example, one exposed by a PouchDB plugin), there are (at least) two strategies:

If the method exists synchronously, add the method name to pouchDBProvider.methods in an angular.config block, for example:

.config(function(pouchDBProvider, POUCHDB_METHODS) {
  // Example for nolanlawson/pouchdb-authentication
  var authMethods = {
    login: 'qify',
    logout: 'qify',
    getUser: 'qify'
  };
  pouchDBProvider.methods = angular.extend({}, POUCHDB_METHODS, authMethods);
})

If the method is added after instantiation asynchronously (perhaps via a promise), manually apply a decorator to the instance, for example:

.controller('myCtrl', function(pouchDB, pouchDBDecorators) {
  // Example for nolanlawson/pouchdb-find
  var db = pouchDB('db');
  db.find = pouchDBDecorators.qify(db.find);
});

How can I debug this?

Debugging angular-pouchdb in a console can be done by first retrieving the injector and calling the pouchDB service as normal, e.g.:

var pouchDB = angular.element(document.body).injector().get('pouchDB');
var db = pouchDB('mydb');
db.get('id').then();

For further tips and tricks, see CouchDB Best Practices.

Can this be used with Browserify?

Yes! For example:

require('angular').module('app', [
  require('angular-pouchdb')
]);

Can this be used with webpack?

Yes, though you need to use expose-loader to ensure PouchDB is available as a global, for example:

require('expose?PouchDB!pouchdb');

Why do promises timeout in my test suite?

Note: some (or all) parts of this section may be incorrect or misleading. Your input is welcome.

In short, AngularJS uses a different task scheduler than native promises.

Promises can be implemented differently. PouchDB uses native (A+-compliant) promises (or lie in environments without native support). Native promises are scheduled using "the microtask queue". AngularJS uses its own promise implementation; $q, which are scheduled via $evalAsync.

During normal use, PouchDB's (wrapped) promise is resolved correctly. However during testing, suites that use ngMock (angular-mocks) often unexpectedly timeout.

Typically, $rootScope.$apply() is used to propagate promise resolution in asynchronous tests. This triggers a digest cycle, which in turn flushes Angular's asyncQueue. Whilst this resolves $q promises, it does not resolve PouchDB's native promises, hence causing the test runner (e.g. Karma) to timeout.

Until Angular's promise implementation is decoupled from its digest cycle and/or Angular-specific implementations can be swapped out with their native equivalents, there are a few known workarounds:

Do not use ngMock

ngMock modifies Angular's deferred implementation in order to support writing tests in a synchronous manner. Arguably, this simplifies control flow, but comes at the cost of making $q-wrapped promises difficult to test.

One workaround (and the one that angular-pouchdb currently uses) is to not use ngMock and manually handle $injector, for example:

describe('Working $q.when tests', function() {
  var pouchdb;
  beforeEach(function() {
    // Note, `ngMock` would usually inject `ng` for us
    var $injector = angular.injector(['ng', 'test']);
    var pouchDB = $injector.get('pouchdb');
    pouchdb = pouchDB('db');
  });

  it('should resolve a promise', function(done) {
    pouchdb.info()
      .then(function(info) {
        expect(info).toBeDefined();
      })
      .finally(done);
  });
});

This preserves "normal" promise resolution behaviour, but will not suit all scenarios, such as when you need the additional introspection/async control features ngMock provides e.g. $httpBackend.flush.

Spam $rootScope.$apply

Calling $rootScope.$apply in quick succession to cause a near-continuous digest cycle forces promise resolution. This appears to be due to tight coupling between Angular's promises and its digest cycle.

it('should wrap destroy', function(done) {
  // Note, you might want to experiement with a interval timeout here
  var interval = $window.setInterval($rootScope.$apply.bind());
  db.destroy()
    .then(shouldBeOK)
    .then($window.clearInterval.bind(null, interval))
    .catch(shouldNotBeCalled)
    .finally(done);
})

Note, this is likely to significantly decrease your test's performance.

Does this work with Angular v2?

No and it doesn't need to! Angular v2's concept of change detection completely differs to Angular v1's; the digest cycle, $scope.$apply and friends are no more. Just use PouchDB directly.

Authors

License

Released under the MIT License.