Skip to content

Instantly share code, notes, and snippets.

@jcreamer898
Last active December 17, 2015 22:59
Show Gist options
  • Save jcreamer898/5686004 to your computer and use it in GitHub Desktop.
Save jcreamer898/5686004 to your computer and use it in GitHub Desktop.
A base ko viewModel

How to use this thing...

var MyNewViewModel = ViewModel.extend({
    subscriptions: {},
    publications: {},
    defaults: {
        // ko.observable
        foo: 1,
        // ko.ovservableArrray
        bar: [],
        // ko.computed
        bam: function() {}
    },
    initialize: function() {
        //This is always ran
    }
});

var instance =  new MyViewModel({
    foo: 10
});
define(['underscore', 'postal', 'machina'], function (_, postal, machina) {
/**
* An fsm for loading resources
* @class app.fsm.AsyncLoader
* @extends machina.Fsm
*/
var AsyncLoader = machina.Fsm.extend({
namespace: 'data',
initialState: 'notCached',
cache: {},
states: {
/**
* State is not cached before a template is retrieved.
* @event notCached
*/
'notCached': {
/**
* Fetch a template, either retrieve from transition to cached and handle fetch or pull with `$.ajax`.
* @param {Object} data Incoming data for handler
* @method notCached.fetch
*/
'fetch': function (data) {
if (this.cache[data.name]) {
this.transition('cached');
this.handle('fetch', _.extend({}, data, {
content: this.cache[data.name]
}));
return;
}
$.ajax({
url: (window.BASEPATH || '') + (data.url || data.name)
}).done(function(result) {
this.cache[data.name] = result;
this.done({
name: data.name,
content: result,
callback: data.callback
});
}.bind(this));
}
},
/**
* State when a template has been retrieved via cache or via `$.ajax`
* @event cached
*/
'cached': {
/**
* Call the `done` helper method
* @param {Object} data Incoming template data
* @method cached.fetch
*/
fetch: function (data) {
this.done(data);
}
}
},
/**
* The fsm has processed and cached a template.
* @param data The template data
* @method done
*/
done: function(data) {
this.transition('notCached');
this.emit('done', {
name: data.name,
content: data.content
});
_.isFunction(data.callback) && data.callback.call(this, data.content);
},
/**
* Helper method to handle fetching a template
* @param data The incoming template data to fetch
* @method fetch
*/
fetch: function (data) {
this.handle('fetch', data);
}
});
return AsyncLoader;
});
/*global define */
define([
'knockout',
'knockout-amd',
'postal',
'underscore',
'app/shared/publishing',
'app/shared/events',
'app/shared/messagingMixin',
'app/shared/extend',
'app/fsm/asyncLoader'
], function (ko, koAmd, postal, _, PublishingMixin, Events, MessagingMixin, extend, AsyncLoader) {
'use strict';
/**
* Allow other ViewModels to extend a baseViewModel.
* Parameters set in the `default` option get set up as observables.
* To use a ViewModel...
var MyViewModel = ViewModel.extend({
defaults: {
foo: '',
bar: [],
bam: function() {}
},
someFunc: function() {},
initialize: function() {
this.bind(this, $('#someElement')[0]);
}
});
var instOfMyViewModel = new MyViewModal({
foo: 'bar'
});
* Any option passed in that has been set as an observable gets set, or uses the default value.
* @class app.ViewModel
* @constructor
* @param {object} options The options passed in on object instantiation.
* @property {object} editor The app main editor
* @uses app.messagingMixin
* @uses app.publishingMixing
* @uses app.events
**/
var ViewModel = function (options) {
options = options || {};
this._fsm = new AsyncLoader();
this._setup(options);
this._templates();
/**
* The app main editor
* @property editor
* @type {CKEDITOR.editor}
*/
this.editor = options.editor;
this.initialize.call(this, options);
};
_.extend(ViewModel.prototype, PublishingMixin, Events, MessagingMixin);
_.extend(ViewModel.prototype, {
/**
* Path for templates
* @type {String}
* @property templatePath
*/
templatePath: 'js/app/views',
/**
* Templates for this viewModel.
* @type {Object}
* @property Templates
*/
templates: {},
/**
* The values defined here will be set up as `ko.observable`, `ko.observableArray` or `ko.computed`.
*
{
foo: '',
bar: [],
bam: function() {}
}
* @type {Object}
* @property
*/
defaults: {},
/**
* Meant to be overridden by inheriting modules
* @method initialize
*/
initialize: function () {},
/**
* Sets up the default values and configures messaging
* @method _setup
* @private
*/
_setup: function (options) {
var self = this;
_.each(this.defaults, function (value, prop) {
if (_.isFunction(value)) {
self[ prop ] = ko.computed(self.defaults[prop], self);
}
else {
self[ prop ] = _.isArray(value) ?
// Use an existing observable, or copy the options or default array
(ko.isObservable(options[prop]) ?
options[prop] :
ko.observableArray((options[prop] || value).slice(0))) :
// Use an existing observable, or use the options value or default value
(ko.isObservable(options[prop]) ?
options[prop] :
ko.observable(options[prop] || value));
}
});
this.configureMessaging();
},
/**
* Set up a listener for template loading
* @method _templates
* @private
*/
_templates: function () {
this._fsm.on('done', function(data) {
var callback = this[this.templates[data.name]],
fn = _.isFunction(callback) && callback.call(this, data.content);
return fn;
}.bind(this));
},
/**
* Loads a template from viewModel.templatePath
* @method template
* @param name The name of the template to load without the path and extension. IE. channel/email ===
* templatePath/chanel/email.html
* @param callback That executes when the template is loaded.
*/
template: function(name, callback) {
this._fsm.fetch({
name: name,
url: [this.templatePath, name].join('/') + (~name.indexOf('.html') ? '' :'.html'),
callback: callback
});
},
/**
* Set up ko.applyBindings
* @param {object} bindings The bindings to apply
* @param {object} el The element to apply the bindings to
* @method bind
*/
bind: function(bindings, el) {
ko.applyBindings(bindings, el);
},
_resetValidation: function() {
_.each(this._koSubscriptions, function(sub) {
sub.dispose();
});
this._koSubscriptions = [];
},
_setupValidation: function() {
var self = this;
this._resetValidation();
this.validationMessages = {};
this.errors = ko.computed({
read: function() {
var errors = _.reduce(this.validation, function(memo, value, key) {
if(this[key].errors().length) {
memo = memo.concat(this[key].errors());
}
return memo;
}.bind(this), []);
return errors;
},
deferEvaluation: true
}, this);
_.each(this.validation, function(rules, key) {
if (this[key] && ko.isObservable(this[key])) {
self._koSubscriptions.push(this[key].subscribe(function(value) {
var errorMessages = [],
msgGroup;
errorMessages = this._validateObservable(value, rules);
if(errorMessages.length) {
_.each(errorMessages, function(error) {
this.validationMessages[key] = {};
this.validationMessages[key][error.ruleName] = error.message;
}, this);
self.trigger('error', {
errors: errorMessages,
value: value
});
}
else {
this.validationMessages[key] = {};
}
this[key].errors(_.map(this.validationMessages[key], function(validationMessages) {
return validationMessages;
}));
}, this));
}
this[key].errors = ko.observableArray().withFormatting();
}, this);
},
_validateObservable: function(value, rules) {
value = !isNaN(parseInt(value, 10)) ? +value : value;
return _.reduce(rules, function(memo, ruleValue, ruleName) {
var message;
// TODO: Check context here.
if (message = typeof ruleValue === 'function' ? ruleValue(value) :
validator[ruleName](value, typeof ruleValue === 'undefined' ? {} : ruleValue)) {
memo.push({
message: message,
ruleName: ruleName
});
}
return memo;
}, []);
},
isValid: function() {
var valid = _.every(this.validation, function(validation, key) {
return !this._validateObservable(ko.utils.unwrapObservable(this[key]), validation).length;
}, this);
return valid;
},
});
/**
* Method to create other ViewModel constructors. Extends the ViewModel.
* @method extend
* @returns A new constructor.
* @static
*/
ViewModel.extend = extend;
return ViewModel;
});
define([
'postal',
'app/shared/baseViewModel',
'jquery',
'app/customBindings/logger'],
function (postal, ViewModel, $, logger) {
logger();
module('Base view model', {
setup: function () {
}
});
test('should have an extend method', function () {
var TestViewModel = ViewModel.extend({
name: 'TestViewModel',
foo: 'bar',
init: function () {}
});
var inst = new TestViewModel();
ok(inst.foo, 'Should have set the foo property');
equal(typeof inst.init, 'function', 'Should have set the foo property');
equal(inst.toString(), 'TestViewModel', 'Should return name of object');
});
test('the view model should have default options', function () {
var DefaultViewModel = ViewModel.extend({
defaults: {
firstName: 'Tyson',
latName: 'Cadenhead',
interests: ['javascript', 'jquery', 'urmom']
}
});
var inst = new DefaultViewModel();
equal(inst.firstName(), 'Tyson', 'Should have set tyson as the default first name');
equal(inst.interests()[0], 'javascript', 'Should have set an array');
});
test('should fire an initialize method', function () {
var initCalled = false,
TestViewModel = ViewModel.extend({
foo: 'bar',
initialize: function () {
initCalled = true;
}
});
new TestViewModel();
ok(initCalled, 'The initialize method should be called');
});
test('should set up inheritance', function () {
var initCalled;
var TestViewModel = ViewModel.extend({
foo: 'bar',
initialize: function () {
initCalled = true;
}
});
var InheritedViewModel = TestViewModel.extend({});
ok(new InheritedViewModel().foo, "foo should be inherited from the base model");
});
test('should set up templates', function () {
var dfd = new $.Deferred();
expect(1);
sinon.stub(jQuery, 'ajax').returns(dfd.promise());
var TestViewModel = ViewModel.extend({
templates: {
'channel/foo': 'fooLoaded'
},
fooLoaded: function (html) {
}
});
var model = new TestViewModel();
model.template('channel/foo');
ok(jQuery.ajax.calledOnce, 'should call jQuery ajax');
jQuery.ajax.restore();
});
test('should cache templates', function () {
var dfd = new $.Deferred();
sinon.stub(jQuery, 'ajax').returns(dfd.promise());
var TestViewModel = ViewModel.extend({
templates: {
'channel/foo': 'fooLoaded'
},
fooLoaded: function (html) {
equal(html, 'something', 'should get cached result');
}
});
var model = new TestViewModel();
var callback = sinon.spy();
model.template('channel/foo', callback);
dfd.resolve('something');
model.template('channel/foo');
ok(callback.called, 'should have called the callback');
ok(jQuery.ajax.calledOnce, 'should call jQuery ajax');
jQuery.ajax.restore();
});
test('should create ko.computed if a function is set', function () {
var TestViewModel = ViewModel.extend({
defaults: {
bar: 'bam',
foo: function () {
return this.bar();
}
}
});
var viewModel = new TestViewModel();
equal(typeof viewModel.foo, 'function', 'should create a function');
equal(viewModel.foo(), 'bam', 'should set up correct context');
});
test('should use options passed in creation to set observables', function () {
var TestViewModel = ViewModel.extend({
defaults: {
bar: 'bam'
}
});
var viewModel = new TestViewModel({
bar: 'boom'
});
equal(viewModel.bar(), 'boom', 'should set the value on the observable');
});
module('viewModel validation');
test('when a viewModel has validation rules', function() {
var TestViewModel = ViewModel.extend({
defaults: {
bar: 'bam'
},
validation: {
bar: {
required: true
}
}
});
var viewModel = new TestViewModel();
viewModel.on('error', function(prop) {
ok(prop, 'it should trigger an error');
});
viewModel.bar('');
ok(!viewModel.isValid(), 'should have an isValid method');
});
test('when a viewModel has many validation rules', function() {
var TestViewModel = ViewModel.extend({
defaults: {
bar: 0
},
validation: {
bar: {
required: true,
type: 'number'
}
}
});
var viewModel = new TestViewModel();
viewModel.on('error', function(prop) {
ok(prop, 'it should trigger an error');
});
viewModel.bar(42);
ok(viewModel.isValid(), 'should be valid');
});
test('when a viewModel has type boolean', function() {
var TestViewModel = ViewModel.extend({
defaults: {
bar: true
},
validation: {
bar: {
type: 'boolean'
}
}
});
var viewModel = new TestViewModel();
ok(viewModel.isValid(), 'should be valid');
});
test('when a viewModel has a greater than validation', function() {
var TestViewModel = ViewModel.extend({
defaults: {
bar: 60
},
validation: {
bar: {
greaterThan: 50
}
}
});
var viewModel = new TestViewModel();
ok(viewModel.isValid(), 'should be valid');
viewModel.bar(40);
ok(!viewModel.isValid(), 'should not be valid');
});
test('when a viewModel has a greater than equal validation', function() {
var TestViewModel = ViewModel.extend({
defaults: {
bar: 60
},
validation: {
bar: {
greaterThanEqual: 60
}
}
});
var viewModel = new TestViewModel();
ok(viewModel.isValid(), 'should be valid');
viewModel.bar(40);
ok(!viewModel.isValid(), 'should not be valid');
});
test('when a viewModel has a less than validation', function() {
var TestViewModel = ViewModel.extend({
defaults: {
bar: 20
},
validation: {
bar: {
lessThan: 50
}
}
});
var viewModel = new TestViewModel();
ok(viewModel.isValid(), 'should be valid');
viewModel.bar(60);
ok(!viewModel.isValid(), 'should not be valid');
});
test('when a viewModel has a less equal to than validation', function() {
var TestViewModel = ViewModel.extend({
defaults: {
bar: 50
},
validation: {
bar: {
lessThanEqual: 50
}
}
});
var viewModel = new TestViewModel();
ok(viewModel.isValid(), 'should be valid');
viewModel.bar(60);
ok(!viewModel.isValid(), 'should not be valid');
});
test('when a viewModel has custom validation rules', function() {
var TestViewModel = ViewModel.extend({
defaults: {
bar: 'bam'
},
validation: {
bar: {
custom: function(value) {
equal(value, 'boom', 'should pass value to the custom validator');
return value === 'foo' ? null : "An error";
}
}
}
});
var viewModel = new TestViewModel();
viewModel.on('error', function(prop) {
ok(prop, 'should trigger an error');
});
viewModel.bar('boom');
});
test('when a viewModel disposes its validation', function() {
var TestViewModel = ViewModel.extend({
defaults: {
bar: 'bam'
},
validation: {
bar: {
custom: function(value) {
equal(value, 'boom', 'should pass value to the custom validator');
return value === 'foo' ? null : "An error";
}
}
}
});
var viewModel = new TestViewModel();
ok(viewModel._koSubscriptions, 'it should have a koSubscriptions member');
equal(viewModel._koSubscriptions.length, 1, 'it should have added a new subscription');
viewModel._resetValidation();
equal(viewModel._koSubscriptions.length, 0, 'it should have removed subscriptions');
});
test('when there are errors in a viewModel', function() {
var TestViewModel = ViewModel.extend({
defaults: {
bar: 'bam'
},
validation: {
bar: {
required: true
}
}
});
var viewModel = new TestViewModel();
viewModel.on('error', function(prop) {
equal(prop.errors[0].message, 'This value is required');
ok(prop, 'it should trigger an error');
});
viewModel.bar('');
ok(!viewModel.isValid(), 'should have an isValid method');
equal(viewModel.bar.errors()[0], 'This value is required');
equal(viewModel.errors()[0], 'This value is required');
});
});
define([
'underscore'
], function (_) {
/**
* The events manager
*
* @class app.events
*/
var Events = {
/**
* Bind an event to a `callback` function. Passing `"all"` will bind
* the callback to all events fired.
* @method on
* @param {String} name Name of the event to subscribe to
* @param {Function} callback Callback to fire when the event fires
* @param {[type]} context Sets the context of the callback
* @return Returns `this`
*/
on: function(name, callback, context) {
if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
this._events || (this._events = {});
var events = this._events[name] || (this._events[name] = []);
events.push({callback: callback, context: context, ctx: context || this});
return this;
},
/**
* Bind an event to only be triggered a single time. After the first time
* the callback is invoked, it will be removed.
* @method once
* @param {String} name Name of the event to subscribe to
* @param {Function} callback Callback to fire when the event fires
* @param {[type]} context Sets the context of the callback
* @return Returns `this`
*/
once: function(name, callback, context) {
if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
var self = this;
var once = _.once(function() {
self.off(name, once);
callback.apply(this, arguments);
});
once._callback = callback;
return this.on(name, once, context);
},
/**
* Remove one or many callbacks. If `context` is null, removes all
* callbacks with that function. If `callback` is null, removes all
* callbacks for the event. If `name` is null, removes all bound
* callbacks for all events.
* @method off
* @param {String} name Name of the event to turn off
* @param {Function} callback Callback to turn off
* @param {[type]} context Sets the context of the callback
* @return Returns `this`
*/
off: function(name, callback, context) {
var retain, ev, events, names, i, l, j, k;
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
if (!name && !callback && !context) {
this._events = {};
return this;
}
names = name ? [name] : _.keys(this._events);
for (i = 0, l = names.length; i < l; i++) {
name = names[i];
if (events = this._events[name]) {
this._events[name] = retain = [];
if (callback || context) {
for (j = 0, k = events.length; j < k; j++) {
ev = events[j];
if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
(context && context !== ev.context)) {
retain.push(ev);
}
}
}
if (!retain.length) delete this._events[name];
}
}
return this;
},
/**
* Trigger one or many events, firing all bound callbacks. Callbacks are
* passed the same arguments as `trigger` is, apart from the event name
* (unless you're listening on `"all"`, which will cause your callback to
* receive the true name of the event as the first argument).
* @method trigger
* @param {String} name The name of the event to trigger
* @return Returns `this`
*/
trigger: function(name) {
if (!this._events) return this;
var args = Array.prototype.slice.call(arguments, 1);
if (!eventsApi(this, 'trigger', name, args)) return this;
var events = this._events[name];
var allEvents = this._events.all;
if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, arguments);
return this;
},
/**
* Tell this object to stop listening to either specific events ... or
* to every object it's currently listening to.
* @method stopListening
* @param {Object} obj Object to stop listening to events on
* @param {String} name Name of the event to stop listening for
* @param {Function} callback
* @return Returns `this`
*/
stopListening: function(obj, name, callback) {
var listeners = this._listeners;
if (!listeners) return this;
var deleteListener = !name && !callback;
if (typeof name === 'object') callback = this;
if (obj) (listeners = {})[obj._listenerId] = obj;
for (var id in listeners) {
listeners[id].off(name, callback, this);
if (deleteListener) delete this._listeners[id];
}
return this;
}
};
// Regular expression used to split event strings.
var eventSplitter = /\s+/;
// Implement fancy features of the Events API such as multiple event
// names `"change blur"` and jQuery-style event maps `{change: action}`
// in terms of the existing API.
var eventsApi = function(obj, action, name, rest) {
if (!name) return true;
// Handle event maps.
if (typeof name === 'object') {
for (var key in name) {
obj[action].apply(obj, [key, name[key]].concat(rest));
}
return false;
}
// Handle space separated event names.
if (eventSplitter.test(name)) {
var names = name.split(eventSplitter);
for (var i = 0, l = names.length; i < l; i++) {
obj[action].apply(obj, [names[i]].concat(rest));
}
return false;
}
return true;
};
// A difficult-to-believe, but optimized internal dispatch function for
// triggering events. Tries to keep the usual cases speedy (most internal
// Backbone events have 3 arguments).
var triggerEvents = function(events, args) {
var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
switch (args.length) {
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);
}
};
var listenMethods = {listenTo: 'on', listenToOnce: 'once'};
// Inversion-of-control versions of `on` and `once`. Tell *this* object to
// listen to an event in another object ... keeping track of what it's
// listening to.
_.each(listenMethods, function(implementation, method) {
Events[method] = function(obj, name, callback) {
var listeners = this._listeners || (this._listeners = {});
var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));
listeners[id] = obj;
if (typeof name === 'object') callback = this;
obj[implementation](name, callback, this);
return this;
};
});
return Events;
});
/*global define */
define([
'underscore'
], function (_) {
'use strict';
/**
* Allows us to extend classes
*
* @class app.extend
* @param {Object} protoProps These properties are added to the prototype
* @param {Object} saticProps
*/
return function(protoProps, staticProps) {
var parent = this,
Surrogate,
child;
// The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent's constructor.
if (protoProps && _.has(protoProps, 'constructor')) {
child = protoProps.constructor;
} else {
child = function(){ return parent.apply(this, arguments); };
}
// Add static properties to the constructor function, if supplied.
_.extend(child, parent, staticProps);
// Set the prototype chain to inherit from `parent`, without calling
// `parent`'s constructor function.
Surrogate = function(){ this.constructor = child; };
Surrogate.prototype = parent.prototype;
child.prototype = new Surrogate();
// Add prototype properties (instance properties) to the subclass,
// if supplied.
if (protoProps) {
_.extend(child.prototype, protoProps);
}
// Set a convenience property in case the parent's prototype is needed
// later.
child.__super__ = parent.prototype;
if (protoProps.name) {
child.prototype.toString = function() {
return protoProps.name;
};
}
return child;
};
});
define([
'knockout',
'underscore',
'postal'
], function(ko, _, postal) {
function logger() {
ko.bindingHandlers.logger = {
update: function(element, valueAccessor, allBindings) {
console.log("hit", ko.toJSON(allBindings()));
}
};
ko.bindingHandlers.htmlValue = {
init: function(element, valueAccessor, allBindingsAccessor) {
var updateValue = _.debounce(function() {
var modelValue = valueAccessor();
var elementValue = element.innerHTML;
if (ko.isWriteableObservable(modelValue)) {
modelValue(elementValue);
}
}, 500);
postal.channel('editor').subscribe('datafield.insert', updateValue);
postal.channel('editor').subscribe('content.update', updateValue);
postal.channel('editor').subscribe('conditionality.added', updateValue);
return { 'controlsDescendantBindings': true };
},
update: function(element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor()) || "";
if (element.innerHTML !== value) {
element.innerHTML = value;
}
}
};
}
ko.observableArray.fn.withFormatting = function(format) {
format = format || ',';
this.formatted = ko.computed(function() {
return this().join(format);
}, this);
return this;
};
return logger;
});
define(['postal'], function (postal) {
/**
* The messaging mixin
*
* @class app.messagingMixin
*/
return {
/**
*
* Set up publications with the pattern...
publications: {
'triggerPublish': 'channel some.topic'
}
* To publish the message, call trigger...
this.trigger('triggerPublish', {
foo: 'bar'
});
* @property publications
* @type {Object}
*/
publications: {},
/**
* Set up subscribers with the pattern...
subscriptions: {
'callbackMethod': 'channel some.topic'
}
* @property subscriptions
* @type {Object}
*/
subscriptions: {},
/**
* Set up messaging for this object.
* @method configureMessaging
*/
configureMessaging: function () {
this.messaging = this.messaging || {};
this.setupSubscriptions();
this.bridgeEvents();
},
/**
* Sets up publishers by looking at `publications` on `this`
* @method bridgeEvents
*/
bridgeEvents: function () {
this.unbridgeEvents();
if (!_.isEmpty(this.publications)) {
_.each(this.publications, function (publication, evnt) {
var _publication = publication;
if (!this.messaging.publications[evnt]) {
this.messaging.publications[evnt] = {};
}
if (!_.isObject(publication)) {
_publication = {};
_publication[publication] = _.identity;
}
_.each(_publication, function (accessor, pub) {
var meta = pub.split(' ');
var channel = meta[0];
var topic = meta[1];
var listener = function () {
var args = Array.prototype.slice.call(arguments, 0);
var data = accessor.apply(this, args);
postal.publish({
channel: channel,
topic: topic,
data: data || {}
});
};
this.on(evnt, listener, this);
this.messaging.publications[evnt][pub] = _.bind(function () {
this.off(evnt, listener);
}, this);
}, this);
}, this);
}
},
/**
* Remove all publications
* @method bridgeEvents
*/
unbridgeEvents: function () {
if (this.messaging.publications) {
_.each(this.messaging.publications, function (publication) {
_.each(publication, function (pub) {
while (pub.length) {
pub.pop()();
}
});
});
}
this.messaging.publications = {};
},
/**
* Sets up subscriptions by looking at the `subscriptions` property
* @method setupSubscriptions
*/
setupSubscriptions: function () {
this.unwindSubscriptions();
if (!_.isEmpty(this.subscriptions)) {
_.each(this.subscriptions, function (sub, handler) {
sub = _.isArray(sub) ? sub : [sub];
_.each(sub, function (subscription) {
var meta = subscription.split(' ');
var channel = meta[0];
var topic = meta[1];
// TODO: After adding app.warn, perhaps consider warning if handler/channel are not present...
if (this[handler]) {
this.messaging.subscriptions[subscription] = postal.subscribe({
channel: channel,
topic: topic,
callback: this[handler]
}).withContext(this);
}
}, this);
}, this);
}
},
/**
* Remove all subscriptions
* @method unwindSubscriptions
*/
unwindSubscriptions: function () {
if (this.messaging.subscriptions) {
_.each(this.messaging.subscriptions, function (sub) {
sub.unsubscribe();
});
}
this.messaging.subscriptions = {};
}
};
});
define( [
'postal'
], function ( postal ) {
function setupChannel ( obj ) {
obj.channel = postal.channel( obj.namespace || obj.channelName );
}
/**
* The publishing mixin
*
* @class app.publishingMixing
*/
var PublishingMixin = {
/**
* The postal channel setup by _ensureChannel()
* @type {ChannelDefinition}
* @property channel
*/
channel: null,
/**
* Sets up a channel
* @method _ensureChannel
* @private
*/
_ensureChannel: function () {
if ( !this.channel ) {
setupChannel( this );
}
},
/**
* Publish a message on the current channel
* @method publish
* @param {string} topic The topic for the message
* @param {object} data The data to send in the message
*/
publish: function ( topic, data ) {
this._ensureChannel();
return this.channel.publish.call( this.channel, { topic: topic, data: data || {} } );
},
/**
* Subsribe to a message on the current channel
* @method subscribe
* @param {string} topic The topic to subscribe to
* @param {function} callback A callback to fire when the subscription is fired
*/
subscribe: function () {
this._ensureChannel();
var sub = this.channel.subscribe.apply( this.channel, arguments );
if ( !this.subscriptions ) {
this.subscriptions = [];
}
if (this.subscriptions.push) {
this.subscriptions.push( sub );
}
return sub;
}
// /**
// * Removes all subscriptions
// * @return {[type]}
// */
// removeAllSubscriptions: function () {
// if ( !this.subscriptions || !this.subscriptions.length ) {
// return;
// }
// var sub;
// while( sub = this.subscriptions.pop() ) {
// sub.unsubscribe();
// }
// },
};
return PublishingMixin;
});
define([
'underscore'
], function(_) {
return {
required: function(value, options) {
value = value.toString();
return $.trim(value).length ? null : options.message || "This value is required";
},
type: function(value, options) {
var type = (options.type || options);
if (!value.length) return;
return typeof value === type ? null : options.message || value + " is not a " + type;
},
greaterThan: function(value, options) {
var gtVal = options.value || options;
if (typeof value !== 'number') return;
return value > gtVal ? null : options.message || value + " is not greater than " + gtVal;
},
lessThan: function(value, options) {
var gtVal = options.value || options;
if (typeof value !== 'number') return;
return value < gtVal ? null : options.message || value + " is not less than " + gtVal;
},
greaterThanEqual: function(value, options) {
var gtVal = options.value || options;
if (typeof value !== 'number') return;
return value >= gtVal ? null : options.message || value + " is not greater than " + gtVal;
},
lessThanEqual: function(value, options) {
var gtVal = options.value || options;
if (typeof value !== 'number') return;
return value <= gtVal ? null : options.message || value + " is not less than " + gtVal;
}
};
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment