Skip to content

Commit

Permalink
Add filters to App base class
Browse files Browse the repository at this point in the history
Closes TryGhost#6

- Change extend helper to wrap life cycle events
- Add filter registration to App base class
  • Loading branch information
jgable committed Dec 7, 2013
1 parent 1bb1bf4 commit 7a86949
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 6 deletions.
72 changes: 69 additions & 3 deletions lib/App.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

var helpers = require('./helpers'),
var _ = require('underscore'),
helpers = require('./helpers'),
App;

/**
Expand All @@ -14,6 +15,11 @@ App = function (ghost) {
this.initialize();
};

/**
* A mapping of filter names to method names
*/
App.prototype.filters = {};

/**
* A method that is run after the constructor and allows for special logic
*/
Expand Down Expand Up @@ -45,7 +51,7 @@ App.prototype.uninstall = function () {
* @parameter {Ghost} The current Ghost app instance
*/
App.prototype.activate = function () {
return;
this.registerFilters();
};

/**
Expand All @@ -54,7 +60,67 @@ App.prototype.activate = function () {
* @parameter {Ghost} The current Ghost app instance
*/
App.prototype.deactivate = function () {
return;
this.unregisterFilters();
};

/**
* Register Ghost filters based on a passed in mapping object
* @parameter {Object} A mapping of filter names to methods names on the app instance.
*/
App.prototype.registerFilters = function (filters) {
var self = this;
this._eachFilter(filters, function (filterName, filterHandlerArgs) {
var parms = [filterName].concat(filterHandlerArgs);

self.app.registerFilter.apply(self.app, parms);

This comment has been minimized.

Copy link
@ErisDS

ErisDS Feb 9, 2014

The api for this has changed to self.app.filters.register

});
};

/**
* Unregister Ghost filters based on a passed in mapping object
* @parameter {Object} A mapping of filter names to methods names on the app instance.
*/
App.prototype.unregisterFilters = function (filters) {
var self = this;

this._eachFilter(filters, function (filterName, filterHandlerArgs) {
var parms = [filterName].concat(filterHandlerArgs);

self.app.unregisterFilter.apply(self.app, parms);

This comment has been minimized.

Copy link
@ErisDS

ErisDS Feb 9, 2014

The api for this has changed to self.app.filters.unregister

});
};

/**
* Iterate through each passed in filter (or this.filters if nothing passed)
* and normalize the arguments that should be passed to *registerFilter methods
* @parameter {Object} optional mapping of filter names to methods names on the app instance.
* @parameter {Function} the callback to run for each filter key value mapping.
*/
App.prototype._eachFilter = function (filters, filterDataHandler) {
filters = filters || this.filters;

// Allow passing a function as the filters
if (_.isFunction(filters)) {
filters = filters();
}

var self = this;

_.each(filters, function (filterHandlerArgs, filterName) {
// Iterate through and determine if there is a priority or not
if (_.isArray(filterHandlerArgs)) {
// Account for some idiot only passing one value in the array.
if (filterHandlerArgs.length === 1) {
filterHandlerArgs.splice(0, 0, null);
}

filterHandlerArgs[1] = self[filterHandlerArgs[1]];
} else {
filterHandlerArgs = [null, self[filterHandlerArgs]];
}

filterDataHandler(filterName, filterHandlerArgs);
});
};

// Offer an easy to use extend method.
Expand Down
30 changes: 29 additions & 1 deletion lib/helpers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use strict';

var _ = require('underscore');
var _ = require('underscore'),
when = require('when'),
lifeCycleMethods = ['install', 'uninstall', 'activate', 'deactivate'];

// Taken from Backbone 1.0.0 (http://backbonejs.org)
// Helper function to correctly set up the prototype chain, for subclasses.
Expand Down Expand Up @@ -32,6 +34,32 @@ var extend = function (protoProps, staticProps) {
// Add prototype properties (instance properties) to the subclass,
// if supplied.
if (protoProps) {

// Iterate through our life cycle methods and wrap them so they
// call the parent class first
_.each(lifeCycleMethods, function (methodName) {
// Check that the method is defined in both the parent and
// new class
if (!(_.has(protoProps, methodName) &&
_.has(parent.prototype, methodName) &&
_.isFunction(protoProps[methodName]))) {
return;
}

protoProps[methodName] = _.wrap(protoProps[methodName], function (newMethod) {
// Grab the arguments passed to the function; passed
// as arguments[1..]
var self = this,
args = _.toArray(arguments).slice(1);

// Call the parent class method first
return when(parent.prototype[methodName].apply(self, args)).then(function () {
// Call the new method
return newMethod.apply(self, args);
});
});
});

_.extend(child.prototype, protoProps);
}

Expand Down
53 changes: 51 additions & 2 deletions test/App_spec.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
/*global describe, it*/
/*global describe, beforeEach, afterEach, it*/
'use strict';

var _ = require('underscore'),
sinon = require('sinon'),
App = require('../lib/App');

describe('App', function () {
var sandbox,
fakeGhost;

beforeEach(function () {
sandbox = sinon.sandbox.create();

fakeGhost = {
registerFilter: sandbox.stub(),
unregisterFilter: sandbox.stub()
};
});

afterEach(function () {
sandbox.restore();
});

it('has correct methods', function () {
var app = new App({}),
var app = new App(fakeGhost),
methods = [
'initialize',
'install',
Expand All @@ -19,4 +36,36 @@ describe('App', function () {
_.isFunction(app[method]).should.equal(true);
});
});

it('registers filters on activate', function (done) {
var FilterApp = App.extend({
filters: {
ghost_head: 'handleGhostHead',
ghost_foot: [9, 'handleGhostFoot'],
prePostRender: ['handlePrePostRender']
},

activate: function () {
// For testing this actually was run
this.otherThing = true;
},

handleGhostHead: sandbox.stub(),

handleGhostFoot: sandbox.stub(),

handlePrePostRender: sandbox.stub()
}),
app = new FilterApp(fakeGhost);

app.activate().then(function () {
app.otherThing.should.equal(true);

fakeGhost.registerFilter.calledWithExactly('ghost_head', null, app.handleGhostHead).should.equal(true);
fakeGhost.registerFilter.calledWithExactly('ghost_foot', 9, app.handleGhostFoot).should.equal(true);
fakeGhost.registerFilter.calledWithExactly('prePostRender', null, app.handlePrePostRender).should.equal(true);

done();
}, done);
});
});
59 changes: 59 additions & 0 deletions test/helpers_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*global describe, beforeEach, afterEach, it*/
'use strict';

var _ = require('underscore'),
should = require('should'),
sinon = require('sinon'),
App = require('../lib/App'),
helpers = require('../lib/helpers.js');

describe('Helpers', function () {
var sandbox,
fakeGhost;

beforeEach(function () {
sandbox = sinon.sandbox.create();

fakeGhost = {
registerFilter: sandbox.stub(),
unregisterFilter: sandbox.stub()
};
});

afterEach(function () {
sandbox.restore();
});

it('can extend a class', function () {
function Class1() { return; }

Class1.prototype.method1 = function () { return; };

Class1.extend = helpers.extend;

var Class2 = Class1.extend({
method2: function () { return; }
});

should.exist(Class2.prototype.method1);
should.exist(Class2.prototype.method2);
});

it('wraps App life cycle events', function (done) {
var newInstallStub = sandbox.stub(),
NewApp = App.extend({
install: newInstallStub
}),
baseInstallSpy = sandbox.spy(App.prototype, 'install'),
newApp = new NewApp(fakeGhost);

newApp.install(fakeGhost).then(function () {
baseInstallSpy.calledWith(fakeGhost).should.equal(true);
baseInstallSpy.calledBefore(newInstallStub).should.equal(true);

newInstallStub.calledWith(fakeGhost).should.equal(true);

done();
}, done);
});
});

0 comments on commit 7a86949

Please sign in to comment.