diff --git a/README.md b/README.md index ebc9cc5..75e0aee 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ ![](https://raw.github.com/jcreamer898/ko.ninja/master/ko-ninja.gif) -#ko.ninja +# ko.ninja ======== A framework for building awesome knockout.js apps. -The idea behind **ko.ninja** is that knockout has amazing two way binding functionallity, but not a lot of conventions for how to write ViewModels and such. +The idea behind **ko.ninja** is that knockout has amazing two way binding functionallity, but not a lot of conventions for how to write ViewModels, Models, Collections and such. -ko.ninja provides methods to create view models in a clean and reusable fashion that provide some built in helpers. +ko.ninja provides methods to create view models, models and collections in a clean and reusable fashion that provide some built in helpers. # Installation ko.ninja can be used by downloading the `dist/ko.ninja.min.js` file or using bower: @@ -37,457 +37,15 @@ If your project isn't using AMD, ko.ninja will also work as a non-AMD script lik ``` -### ko.ViewModel -The `ko.ViewModel` is a constructor to define ViewModels. +# Using ko.Ninja -```js -var Person = ko.ViewModel.extend({ - observables: { - firstName: "", - lastName: "", - fullName: function() { - return this.firstName() + " " + this.lastName()); - }, - friends: [] - } -}); - -var me = new Person({ - firstName: "Jonathan", - lastName: "Creamer" -}); - -me.firstName(); // "Jonathan" -me.fullName(); // "Jonathan Creamer" - -me.friends.push(new Person({ - firstName: "Tyson", - lastName: "Cadenhead" -})); -``` - -Some of the advantages are, you don't have to type `ko.observable()` all the time, and if you define a function in your `observables` property, it will create a `ko.computed` that is automatically scoped correctly so `this` points to the right place. - -### Validation - -The Ninja ViewModel can handle all of your client-side validations with the `validation` object. Adding a validation object to your ViewModel will look something like this: - -```js -var Person = ko.ViewModel.extend({ - - observables: { - firstName: '', - lastName: '', - email: '', - phone: '', - answer: '' - }, - - validation: { - firstName: { - required: 'Your first name is required', - minLength: { - message: 'Please make sure your name is at least 3 characters long.', - value: 3 - } - }, - email: { - required: 'Your email address is required', - email: 'Please enter a valid email address' - }, - phone: { - number: 'Please enter a valid number', - length: { - message: 'Please make sure your phone number has 9 digits', - value: 9 - } - }, - answer: { - maxLength: { - message: 'You have entered more than 2 characters... there is no way you are typing "44"!', - value: 2 - }, - custom: { - message: 'Please enter "44"', - validator: function (value) { - return value !== '44'; - } - } - } - }, - - submitPerson: function () { - var errors = this.validate(); - if (!errors) { - alert('Your form has been submitted. Just kidding!') - } - } - -}); -``` - -[Here is a JSFiddle](http://jsfiddle.net/tysoncadenhead/QUPg8/) showing the code above in action. - -As your observables are updated, there will also be an observable called [observableName].error that will be populated with errors on the observable. - -For example, if you want to watch for errors on the first name, your template would look like this: - -```html -

- - -

-
-``` - -You never need to change the error because ko.ninja will keep track of updates for you and populate the error observable if needed. - -Each viewModel also has an `errors` observable array with all of the errors in the viewModel. To show a list of all of the errors in the form, you could do something like this: - -```html -
-

Here are the fields that have errors:

- -
-``` - -Out of the box, ko.ninja comes with a few validators. You can also use the `custom` validator to create your own validation. - -##### required - -Checks to see if there is a value for the observable. - -```js -{ - required: 'This is required' -}, - -// Or -{ - required: { - message: 'This is required' - } -} -``` - -##### email - -Checks to see if the value is a valid email address. - -```js -{ - email: 'This is not an email address' -}, - -// Or -{ - email: { - message: 'This is not an email address' - } -} -``` - -##### number - -Checks to make sure that all of the characters in the observable are numbers. - -```js -{ - number: 'This is not a number' -}, - -// Or -{ - number: { - message: 'This is not a number' - } -} -``` - -##### maxLength - -Checks to make sure that the length is not more than the value passed in. - -```js -{ - length: { - message: 'This is more than 5 characters long', - value: 5 - } -} -``` - -##### minLength - -Checks to make sure that the length is not less than the value passed in. - -```js -{ - length: { - message: 'This is less than 5 characters long', - value: 5 - } -} -``` - -##### length - -Checks to make sure that the length is the same as the passed in value - -```js -{ - length: { - message: 'This is not 5 characters long', - value: 5 - } -} -``` - -##### custom - -If you want to create your own validation, use the custom validator. If the method returns a truthy value, the ViewModel will assume that there is an error. - -##### maxLength - -Checks to make sure that the length is not more than the value passed in. - -```js -{ - custom: { - message: 'Your name is not Tyson', - validator: function (value) { - return value !== 'Tyson'; - } - } -} -``` - -### ko.Model - -The ko.Model syncs data from the ko.ViewModel with a back-end service or with localStorage. It can be added to the ko.ViewModel like this: - -```js -ko.ViewModel.extend({ - observables: { - ... - }, - model: { - name: 'myModelName', - storage: 'localStorage' - } -}); -``` - -With that in place, anytime any of your observables change, the observables will be sent to your database and saved. Each time you initialize your viewModel, if there is an id attribute defined on the viewModel, it will be fetched from the data source. - -By default, if you include a model on your viewModel, it will automatically fetch and save data. If you want to turn off the automatic syncing, you can do so by specifying `autoSync: false` on your viewModel like this: - -```js -ko.ViewModel.extend({ - autoSync: false, - ... -}); -``` - -You can also access the model anywhere in your viewModel using `this.model`. Now, let's take a look at the ko.Model api. It is designed to use the same basic API regardless of your storage type. - -If you would like to see the localStorage in action, [check out this JSFiddle](http://jsfiddle.net/tysoncadenhead/8jdmJ/1/). The same example is also available in the `/examples` folder on this project. - -#### storage {String} -The type of storage to use on the viewModel. Currently, `localStorage` and `http` are both supported. - -#### http - -HTTP expects a REST API and can be defined like this: - -```js -ko.ViewModel.extend({ - model: { - - // The root that all ajax calls are made relative to - urlRoot: function () { - return '/list/' - }, - - // If you have a suffix appended to each URL, this can - // be used. It defaults to an empty string. - suffix: '.json', - - // For HTTP, this should always be http - storage: 'http', - - // The name of your model. If the urlRoot is not specified, - // this will be used to build the urlRoot as well. - name: 'list' - - }, - ... -}); -``` - -#### socket.io - -The socket.io storage expects a few different parameters. A socket.io setup might look something like this: - -```js -ko.ViewModel.extend({ - model: { - - // For socket.io, the storage should always be set to socket.io - storage: 'socket.io', - - // The name of the model. If message names are not specified, this will be used to generate the message names. This is required. - name: 'list', - - // The http protocol to use for socket.io messages. This is set to "http" by default - protocol: 'http', - - // The domain name to use for socket.io messages. This is set to "localhost" by default - domainName: 'localhost', - - // The port number to use for socket.io messages. This is set to 8080 by default - port: 3000, - - // The message names can be updated to be anything you want. These are all defaulted and not required. - messageNames: { - 'update': 'update-myList', // Defaults to {{name}}-update - 'insert': 'insert-intoMyList', // Defaults to {{name}}-insert - 'find': 'find-stuffInMyList', // Defaults to {{name}}-find - 'findOne': 'find-aThing', // Defaults to {{name}}-findOne - 'remove': 'remove-aThing' // Defaults to {{name}}-remove - } - - }, - .... -}); -``` - -It also important to note that ko.ninja does not require the socket.io JavaScript file. You will need to add it to your document like this: - -```html - -``` - -For more information on Socket.io and getting it set up, checkout out the [Socket.io documentation](http://socket.io/#how-to-use). - -#### idAttribute {Property} -The id attribute on the viewModel. This is used for syncing with databases. Defaults to `id`. - -```js -ko.ViewModel.extend({ - observables: { - _id: '123', - firstName: 'Heather', - lastName: 'Cadenhead' - }, - model: { - name: 'friends', - storage: 'localStorage', - idAttribute: '_id' - } -}); -``` - -#### insert (data) -Inserts new data into the database. - -```js -var model = new ko.Model({ name: 'friends' }); - -model.insert({ firstName: 'Milo', lastName: 'Cadenhead' }, function (data) { - console.log(data); - // { firstName: 'Milo', lastName: 'Cadenhead', id: 1382539084406 } -}); -``` - -#### update (id, data) -Updates the data in the database. - -```js -var model = new ko.Model({ name: 'friends' }); - -model.update(1382539084406, { firstName: 'Linus' }, function () { - model.findOne(1382539084406, function (data) { - console.log(data); - // { firstName: 'Linus', lastName: 'Cadenhead', id: 1382539084406 } - }); -}); -``` - -#### save (data) -Instead of using update and insert, you can use save. If there is an id attribute, save will do an update, otherwise, it will do an insert. - -```js -var model = new ko.Model({ name: 'friends' }); - -model.save({ firstName: 'Jonathan', lastName: 'Creamer', id: 1 }, function () { - console.log('Saved him!'); -}); -``` - -#### remove (id) -Removes a record from the database. - -```js -var model = new ko.Model({ name: 'friends' }); - -model.remove(1382539084406, function () { - console.log('1382539084406 was removed'); -}); -``` - -#### findOne (id) -Returns a single viewModel with the matching id. - -```js -var model = new ko.Model({ name: 'friends' }); - -model.save({ firstName: 'Jonathan', lastName: 'Creamer', id: 1 }, function () { - models.findOne(1, function (data) { - console.log(data); - // { firstName: 'Jonathan', lastName: 'Creamer', id: 1 } - }); -}); -``` - -#### find (query) -Search the data for any matches. All matches are returned in the promise. - -```js -var model = new ko.Model({ name: 'friends' }); - -model.save({ firstName: 'Jonathan', lastName: 'Creamer', id: 1 }); -model.save({ firstName: 'Tyson', lastName: 'Cadenhead', id: 2 }); -model.save({ firstName: 'Linus', lastName: 'Cadenhead', id: 3 }); - -model.find({ - lastName: 'Cadenhead' -}, function (data) { - console.log(data); - // [{ firstName: 'Tyson', lastName: 'Cadenhead', id: 2 }, { firstName: 'Linus', lastName: 'Cadenhead', id: 3 }] -}); -``` - -### Events -Changing properties also trigger's events on the ViewModels. - -```js -var me = new Person({ - firstName: "Jonathan", - lastName: "Creamer" -}); - -me.on("change:firstName", function(value) { - // value === "foo" -}); - -me.firstName("foo"); -``` +- [ViewModels](/docs/viewModels.md) +- [Collections](/docs/collections.md) +- [Models](/docs/models.md) + - [Validations](/docs/validations.md) + - [HTTP Storage](/docs/httpStorage.md) + - [Local Storage](/docs/localStorage.md) + - [Socket IO Storage](/docs/socketIoStorage.md) # Development ko.ninja is built using grunt and bower. To run the build... diff --git a/dist/ko.ninja.js b/dist/ko.ninja.js index 51a761e..b7d1b0e 100644 --- a/dist/ko.ninja.js +++ b/dist/ko.ninja.js @@ -292,7 +292,248 @@ // AMD if (typeof define === 'function' && define.amd) { - define('ko.ninja.baseModel',[ + define('ko.ninja.viewModel',[ + 'knockout', + 'underscore', + 'ko.ninja.events', + 'ko.ninja.extend' + ], factory); + + // Non-AMD + } else { + factory(root.ko, root._, root.ko.ninjaEvents, root.ko.ninjaExtend, root.ko.ninjaModel); + } + +} (this, function (ko, _, Events, extend) { + + + + var setupObservables = function(options) { + var computedObservables = _.functions(this.observables); + + computedObservables = _.reduce(this.observables, function(memo, value, prop) { + if (_.isObject(value) && !_.isArray(value) && (value.read || value.write)) { + memo.push(prop); + } + return memo; + }, computedObservables); + + // Process the observables first + _.each(_.omit(this.observables, computedObservables), function (value, prop) { + if (_.isArray(value)) { + if (ko.isObservable(options[prop])) { + this[prop] = options[prop]; + } + else { + this[prop] = ko.observableArray((options[prop] || value).slice(0)); + } + } + else { + if (ko.isObservable(options[prop])) { + this[prop] = options[prop]; + } + else { + this[prop] = ko.observable(options[prop] || value); + } + } + + this[prop].subscribe(function(value) { + this.trigger('change:' + prop, value); + }, this); + }, this); + + // Now process the computedObservables + _.each(_.pick(this.observables, computedObservables), function(value, prop) { + this[prop] = ko.computed({ + read: this.observables[prop], + write: function () { + // Keeps it from breaking. + // Perhaps we need a way to allow writing to computed observables, though + }, + owner: this + }, this); + }, this); + }; + + var setupModel = function () { + var model = this.model; + + if (this.options && this.options.autoSync) { + model.autoSync(this.options.autoSync, true); + } + + _.each(model.observables, function (observable, name) { + this[name] = model[name]; + }, this); + + }; + + var setupCollections = function () { + var collections = this.collections; + for (var collection in collections) { + if (collections.hasOwnProperty(collection)) { + if (this.options && this.options.autoSync) { + collections[collection].autoSync(this.options.autoSync); + } + this[collection] = collections[collection]._models; + } + } + }; + + //### ko.ViewModel + var ViewModel = function ViewModel(options) { + + options = options || {}; + + if (this.model) { + setupModel.call(this, options); + } + + if (this.collections) { + setupCollections.call(this, options); + } + + setupObservables.call(this, options); + + this.initialize.apply(this, arguments); + + options.el = options.el || this.el; + + if (options.el) { + ko.applyBindings(this, (typeof options.el === 'object') ? options.el : document.querySelector(options.el)[0]); + } + + }; + + _.extend(ViewModel.prototype, Events, { + initialize: function() {} + }); + + ViewModel.extend = extend; + + if (typeof define === 'function' && define.amd) { + return ViewModel; + } else { + ko.ninjaViewModel = ViewModel; + } + +})); +/*global define */ + +(function (root, factory) { + + + // AMD + if (typeof define === 'function' && define.amd) { + define('ko.ninja.class',[ + 'knockout', + 'underscore', + 'ko.ninja.events', + 'ko.ninja.extend' + ], factory); + + // Non-AMD + } else { + factory(root.ko, root._, root.ko.ninjaEvents, root.ko.ninjaExtend); + } + +} (this, function (ko, _, Events, extend) { + + + + //### ko.Class + var Class = function Class (options) { + options = options || {}; + this._setupObservables.call(this, options); + if (this._setup) { + this._setup(options); + } + this.initialize(options); + }; + + _.extend(Class.prototype, Events, { + + _setupObservables: function(options) { + + if (!this.observables || !options) { + return; + } + + var computedObservables; + + _.each(options, function (value, name) { + if (!_.isUndefined(this.observables[name])) { + this.observables[name] = value; + delete options[name]; + } + }, this); + + computedObservables = _.functions(this.observables); + + computedObservables = _.reduce(this.observables, function(memo, value, prop) { + if (_.isObject(value) && !_.isArray(value) && (value.read || value.write)) { + memo.push(prop); + } + return memo; + }, computedObservables); + + // Process the observables first + _.each(_.omit(this.observables, computedObservables), function (value, prop) { + if (_.isArray(value)) { + if (ko.isObservable(options[prop])) { + this[prop] = options[prop]; + } + else { + this[prop] = ko.observableArray((options[prop] || value).slice(0)); + } + } + else { + if (ko.isObservable(options[prop])) { + this[prop] = options[prop]; + } + else { + this[prop] = ko.observable(options[prop] || value); + } + } + + this[prop].subscribe(function(value) { + this.trigger('change:' + prop, value); + }, this); + }, this); + + // Now process the computedObservables + _.each(_.pick(this.observables, computedObservables), function(value, prop) { + this[prop] = ko.computed({ + read: this.observables[prop], + write: function () { + // Keeps it from breaking. + // Perhaps we need a way to allow writing to computed observables, though + }, + owner: this + }, this); + }, this); + }, + + initialize: function() {} + }); + + Class.extend = extend; + + if (typeof define === 'function' && define.amd) { + return Class; + } else { + ko.ninjaClass = Class; + } + +})); +/*global define */ + +(function (root, factory) { + + + // AMD + if (typeof define === 'function' && define.amd) { + define('ko.ninja.baseStorage',[ 'underscore', 'ko.ninja.events', 'ko.ninja.extend' @@ -307,8 +548,8 @@ - //### ko.BaseModel - var BaseModel = function (options) { + //### ko.BaseStorage + var BaseStorage = function (options) { options = options || {}; @@ -318,6 +559,8 @@ _.extend(this, Events, { + options: {}, + idAttribute: 'id', invalid: function () { @@ -328,20 +571,32 @@ }; } - if (!this.name) { + if (!this.options.name) { return { error: true, - message: 'This model has no name. Every model needs a name' + message: 'This storage has no name. Every storage needs a name' }; } }, save: function (data, done) { + + // If no data is passed in, send all of the observables off to be saved + if (!data) { + data = {}; + _.each(this.observables, function (value, name) { + data[name] = this[name](); + }, this); + } + if (data[this.idAttribute]) { return this.update(data[this.idAttribute], data, done); } else { return this.insert(data, done); } + }, + + fetch: function () { } }, options); @@ -349,12 +604,12 @@ }; - BaseModel.extend = extend; + BaseStorage.extend = extend; if (typeof define === 'function' && define.amd) { - return BaseModel; + return BaseStorage; } else { - ko.ninjaBaseModel = BaseModel; + ko.ninjaBaseStorage = BaseStorage; } })); @@ -365,20 +620,20 @@ // AMD if (typeof define === 'function' && define.amd) { - define('ko.ninja.localStorageModel',[ - 'ko.ninja.baseModel' + define('ko.ninja.localStorage',[ + 'ko.ninja.baseStorage' ], factory); // Non-AMD } else { - factory(root.ko.ninjaBaseModel, root.ko); + factory(root.ko.ninjaBaseStorage, root.ko); } -} (this, function (BaseModel, ko) { +} (this, function (BaseStorage, ko) { - var LocalStorageModel = BaseModel.extend({ + var LocalStorageModel = BaseStorage.extend({ find: function (data, done) { var response = [], match; @@ -387,7 +642,7 @@ if (!this.invalid()) { for(var key in localStorage) { - if (~key.indexOf(this.name + '-')) { + if (~key.indexOf(this.options.name + '-')) { match = true; for (var value in data) { if (data[value] !== JSON.parse(localStorage[key])[value]) { @@ -408,7 +663,7 @@ findOne: function (id, done) { if (!this.invalid()) { - done(JSON.parse(localStorage[this.name + '-' + id] || '{}')); + done(JSON.parse(localStorage[this.options.name + '-' + id] || '{}')); } else { done(this.invalid()); @@ -419,7 +674,7 @@ done = done || function () {}; data[this.idAttribute] = new Date().getTime(); if (!this.invalid(data)) { - localStorage[this.name + '-' + data.id] = JSON.stringify(data); + localStorage[this.options.name + '-' + data.id] = JSON.stringify(data); done(data); } else { done(this.invalid(data)); @@ -428,8 +683,9 @@ remove: function (id, done) { done = done || function () {}; + id = id || this.getId(); if (!this.invalid()) { - delete localStorage[this.name + '-' + id]; + delete localStorage[this.options.name + '-' + id]; done(null); } else { done(this.invalid()); @@ -440,11 +696,17 @@ done = done || function () {}; if (!this.invalid(data)) { data[this.idAttribute] = id; - localStorage[this.name + '-' + id] = JSON.stringify(data); + localStorage[this.options.name + '-' + id] = JSON.stringify(data); done(data); } else { done(this.invalid(data)); } + }, + + initialize: function (options) { + if (options && options.options) { + this.options = options.options; + } } }); @@ -527,6 +789,12 @@ self.AJAX.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); self.AJAX.send(passData); + } else if (/delete/i.test(postMethod)) { + uri = urlCall+'?'+self.updating.getTime(); + self.AJAX.open('DELETE', uri, true); + self.AJAX.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + self.AJAX.send(passData); + } else { uri = urlCall + '?' + passData + '×tamp=' + (self.updating.getTime()); self.AJAX.open('GET', uri, true); @@ -556,27 +824,27 @@ // AMD if (typeof define === 'function' && define.amd) { - define('ko.ninja.httpModel',[ - 'ko.ninja.baseModel', + define('ko.ninja.httpStorage',[ + 'ko.ninja.baseStorage', 'ko.ninja.ajax', 'underscore' ], factory); // Non-AMD } else { - factory(root.ko.ninjaBaseModel, root.ko.ninjaAjax, root._, root.ko); + factory(root.ko.ninjaBaseStorage, root.ko.ninjaAjax, root._, root.ko); } -} (this, function (BaseModel, Ajax, _, ko) { +} (this, function (BaseStorage, Ajax, _, ko) { - var HttpModel = BaseModel.extend({ + var HttpStorage = BaseStorage.extend({ suffix: '', urlRoot: function () { - return ((this.name) ? '/' + this.name : '') + '/'; + return ((this.options.name) ? '/' + this.options.name : '') + '/'; }, ajax: function (params) { @@ -648,6 +916,9 @@ remove: function (id, done) { done = done || function () {}; + if (!id) { + id = this.getId(); + } this.ajax({ url: this.urlRoot() + id + this.suffix, method: 'DELETE', @@ -657,6 +928,9 @@ update: function (id, data, done) { done = done || function () {}; + if (!id) { + id = this.getId(); + } this.ajax({ url: this.urlRoot() + id + this.suffix, method: 'PUT', @@ -668,9 +942,9 @@ }); if (typeof define === 'function' && define.amd) { - return HttpModel; + return HttpStorage; } else { - ko.ninjaHttpModel = HttpModel; + ko.ninjaHttpStorage = HttpStorage; } })); @@ -681,21 +955,21 @@ // AMD if (typeof define === 'function' && define.amd) { - define('ko.ninja.socketModel',[ - 'ko.ninja.baseModel', + define('ko.ninja.socketStorage',[ + 'ko.ninja.baseStorage', 'underscore' ], factory); // Non-AMD } else { - factory(root.ko.ninjaBaseModel, root._, root.ko); + factory(root.ko.ninjaBaseStorage, root._, root.ko); } -} (this, function (BaseModel, _, ko) { +} (this, function (BaseStorage, _, ko) { - var SocketModel = BaseModel.extend({ + var SocketStorage = BaseStorage.extend({ find: function (query, done) { if (!done) { @@ -707,6 +981,9 @@ }, findOne: function (id, done) { + if (!id) { + id = this.getId(); + } this.socket.emit(this.messageNames.findOne, { id: id }, done); @@ -719,12 +996,18 @@ }, remove: function (id, done) { + if (!id) { + id = this.getId(); + } this.socket.emit(this.messageNames.remove, { id: id }, done); }, update: function (id, data, done) { + if (!id) { + id = this.getId(); + } this.socket.emit(this.messageNames.update, { id: id, data: data @@ -733,25 +1016,34 @@ initialize: function (options) { - this.socket = io.connect((options.protocol || 'http')+ '://' + (options.hostName || 'localhost') + ':' + (options.port || '8080')); + options = options || {}; + + this.options = _.extend({ + protocol: 'http', + hostName: 'localhost', + port: 8080, + name: 'list' + }, options.options || {}); + + this.socket = io.connect(this.options.protocol + '://' + this.options.hostName + ':' + this.options.port); // This lets us override the message names if we want to this.messageNames = _.extend({ - 'update': options.name + '-update', - 'insert': options.name + '-insert', - 'find': options.name + '-find', - 'findOne': options.name + '-findOne', - 'remove': options.name + '-remove' - }, options.messageNames || {}); + 'update': this.options.name + '-update', + 'insert': this.options.name + '-insert', + 'find': this.options.name + '-find', + 'findOne': this.options.name + '-findOne', + 'remove': this.options.name + '-remove' + }, this.options.messageNames || {}); } }); if (typeof define === 'function' && define.amd) { - return SocketModel; + return SocketStorage; } else { - ko.ninjaSocketModel = SocketModel; + ko.ninjaSocketStorage = SocketStorage; } })); @@ -763,44 +1055,47 @@ // AMD if (typeof define === 'function' && define.amd) { - define('ko.ninja.model',[ + define('ko.ninja.storage',[ 'ko.ninja.extend', - 'ko.ninja.localStorageModel', - 'ko.ninja.httpModel', - 'ko.ninja.socketModel' + 'ko.ninja.localStorage', + 'ko.ninja.httpStorage', + 'ko.ninja.socketStorage' ], factory); // Non-AMD } else { - factory(root.ko.ninjaExtend, root.ko.ninjaLocalStorageModel, root.ko.ninjaSocketModel); + factory(root.ko.ninjaExtend, root.ko.ninjaLocalStorageStorage, root.ko.ninjaSocketStorage); } -} (this, function (extend, LocalStorageModel, HttpModel, SocketModel) { +} (this, function (extend, LocalStorageStorage, HttpStorage, SocketStorage) { - var Model = function (options) { - var model; - switch (options.storage) { + var Storage = function (options) { + var storage; + if (!options || !options.options || !options.options.storage) { + return {}; + } + switch (options.options.storage) { case 'http': - model = new HttpModel(options); + storage = new HttpStorage(options); break; case 'socket.io': - model = new SocketModel(options); + storage = new SocketStorage(options); break; - default: - model = new LocalStorageModel(options); + case 'localStorage': + storage = new LocalStorageStorage(options); break; } - return model; + return storage; }; - Model.extend = extend; + Storage.extend = extend; if (typeof define === 'function' && define.amd) { - return Model; + return Storage; } else { - ko.ninjaModel = Model; + ko.ninjaStorage = Storage; } })); @@ -937,165 +1232,394 @@ /*global define */ (function (root, factory) { + // AMD if (typeof define === 'function' && define.amd) { - define('ko.ninja.viewModel',[ - 'knockout', + define('ko.ninja.model',[ 'underscore', - 'ko.ninja.events', + 'ko.ninja.class', 'ko.ninja.extend', - 'ko.ninja.model', + 'ko.ninja.storage', + 'knockout', 'ko.ninja.validation' ], factory); // Non-AMD } else { - factory(root.ko, root._, root.ko.ninjaEvents, root.ko.ninjaExtend, root.ko.ninjaModel, root.ko.ninjaValidation); + factory(root._, root.ko.ninjaClass, root.ko.ninjaExtend, root.ko.ninjaStorage, root.ko, root.ko.ninjaValidation); } -} (this, function (ko, _, Events, extend, Model, Validation) { +} (this, function (_, Class, extend, Storage, ko, Validation) { - var setupObservables = function(options) { - var computedObservables = _.functions(this.observables); + var Model = Class.extend(_.extend(Validation, { - computedObservables = _.reduce(this.observables, function(memo, value, prop) { - if (_.isObject(value) && !_.isArray(value) && (value.read || value.write)) { - memo.push(prop); + idAttribute: 'id', + + /** + * Returns the Id + * @method getId + */ + getId: function () { + return this[this.idAttribute](); + }, + + /** + * @method _setup + * @param {Object} options + */ + _setup: function (options) { + + _.extend(this, new Storage(_.extend({}, this, options))); + + if (this.validation) { + this.watchValidations(); } - return memo; - }, computedObservables); - // Process the observables first - _.each(_.omit(this.observables, computedObservables), function (value, prop) { - if (_.isArray(value)) { - if (ko.isObservable(options[prop])) { - this[prop] = options[prop]; - } - else { - this[prop] = ko.observableArray((options[prop] || value).slice(0)); - } + }, + + /** + * @method get + * @param {String} name + */ + get: function (name) { + if (!this[name]) { + throw 'get observable not found'; } - else { - if (ko.isObservable(options[prop])) { - this[prop] = options[prop]; - } - else { - this[prop] = ko.observable(options[prop] || value); + return this[name](); + }, + + /** + * @method set + * @param {String} name + * @param {Object} value + */ + set: function (name, value) { + + // Take an entire object and set all of the observables with it + if (_.isObject(name)) { + for (var item in name) { + if (name.hasOwnProperty(item)) { + this.set(item, name[item]); + } } + + // Set a single observable + } else if (_.isFunction(this[name]) && this[name]() !== value) { + this[name](value); } + }, - this[prop].subscribe(function(value) { - this.trigger('change:' + prop, value); + /** + * @method has + * @param {String} name + */ + has: function (name) { + if (!this[name]) { + return; + } + return !!this[name](); + }, + + /** + * @method clear + */ + clear: function () { + _.each(this.observables, function (observable, name) { + this[name](null); }, this); - }, this); + }, - // Now process the computedObservables - _.each(_.pick(this.observables, computedObservables), function(value, prop) { - this[prop] = ko.computed({ - read: this.observables[prop], - write: function () { - // Keeps it from breaking. - // Perhaps we need a way to allow writing to computed observables, though - }, - owner: this + /** + * @method toJSON + */ + toJSON: function () { + var json = {}; + _.each(this.observables, function (observable, name) { + json[name] = this[name](); }, this); - }, this); - }; + return json; + }, - var setupValidation = function() { + /** + * @method autoSync + * @param {Boolean} autoSync + * @param {Boolean} fetch Fetch the data right away + */ + autoSync: function (autoSync, fetch) { - }; + var self = this; - var setupModel = function () { - var self = this, - sync = function () { - var data = {}; - _.each(self.observables, function (val, name) { - data[name] = self[name](); - }); - self.model.save(data, function () {}); - }, debounceSync = _.debounce(sync, 1); + if (autoSync) { - if (!this.model.status) { - this.model = new Model(this.model); - } + // Grab the data right away + if (fetch) { + this.findOne(this.getId(), function (data) { + self.set(data); + }); + } - // This keeps the model from autoSyncing if the viewModel has autoSync: false - // defined on it. - if (this.autoSync !== false) { - this.autoSync = true; + // Automatically save the data when a change occurs + _.each(this.observables, function (value, name) { + this[name].subscribe(function () { + self.save(); + }); + }, this); + } } - this.fetch = function (done) { - var self = this, - autoSync = this.autoSync; + })); + + Model.extend = extend; + + if (typeof define === 'function' && define.amd) { + return Model; + } else { + ko.ninjaModel = Model; + } + +})); +/*global define */ + +(function (root, factory) { + + + + // AMD + if (typeof define === 'function' && define.amd) { + define('ko.ninja.collection',[ + 'ko.ninja.extend', + 'knockout', + 'underscore', + 'ko.ninja.storage' + ], factory); + + // Non-AMD + } else { + factory(root.ko.ninjaExtend, root.ko, root._, root.ko.ninjaStorage); + } + +} (this, function (extend, ko, _, Storage) { + + + + var Collection = function (models) { + + this.options = this.options || {}; - this.autoSync = false; - this.model.findOne(this[this.idAttribute || 'id'](), function (data) { - _.each(data, function (value, name) { - if (typeof self[name] === 'function') { - self[name](value); + var _conditions = {}, + _autoSync = false, + Store = Storage.extend({}), + _storage = new Store({ + options: this.options + }); + + _.extend(this, { + + _models: ko.observableArray([]), + + /** + * Converts the collection to JSON + * @method toJSON + * @returns {Array} + */ + toJSON: function () { + var models = this._models(), + json = []; + for (var i = 0; i < models.length; i++) { + json.push(models[i].toJSON()); + } + return json; + }, + + /** + * Adds a model to the end of the collection + * @method push + * @param {Object} data the model data + * @param {Object} options + * @param {Number} options.position The position to insert the model at + */ + insert: function (data, options) { + + var model; + + // Make sure there is no id getting inserted + if (!data[this.model.prototype.idAttribute]) { + data[this.model.prototype.idAttribute] = null; + } + data.options = _.extend(this.options, this.model.prototype.options || {}); + + model = new this.model(data); + + if (options && _.isNumber(options.position)) { + this._models.splice(options.position, 0, model); + } else { + this._models.push(model); + } + if (_autoSync && model.save) { + model.save(null, function (data) { + model.set(data); + }); + } + }, + + /** + * Removes a model from the collection + * @method remove + * @param {String} id + */ + remove: function (id) { + + var i; + if (!id) { + return; + } + + if (_.isArray(id)) { + for (i = 0; i < id.length; i++) { + this.remove(id[i]); + } + } else { + for (i = 0; i < this._models().length; i++) { + if (this._models()[i].getId() === id) { + if (_autoSync && this._models()[i].remove) { + this._models()[i].remove(); + } + this._models.splice(i, 1); + } } - }); - self.autoSync = autoSync; - if (_.isFunction(done)) { - done(); } - }); - }; + }, + + /** + * Returns all of the models + * @method find + * @returns {Array} models + */ + find: function (where) { - _.each(this.observables, function (val, name) { - self[name].subscribe(function () { - if (self.autoSync && !self.validateAll().length) { - debounceSync(); + var models = [], + match = true; + + _conditions = where || {}; + + if (!_conditions) { + return this._models(); + } else { + for (var i = 0; i < this._models().length; i++) { + match = true; + for (var condition in _conditions) { + if (_conditions.hasOwnProperty(condition)) { + if (_conditions[condition] !== this._models()[i][condition]()) { + match = false; + } + } + } + if (match) { + models.push(this._models()[i]); + } + } + return models; } - }); - }); - }; + }, - //### ko.ViewModel - var ViewModel = function ViewModel(options) { + /** + * Gets the data from the backend services + * @method fetch + * @param {Object} where Conditions to send to the server + */ + fetch: function (where) { + var self = this, models = []; + where = where || _conditions || {}; + if (_storage.find) { + _storage.find(where, function (data) { + for (var i = 0; i < data.length; i++) { + models.push(new self.model(_.extend(data[i], { + options: _.extend(self.options, self.model.prototype.options || {}) + }))); + } + self._models(models); + }); + } + }, - options = options || {}; + /** + * Returns the model that matches the ID that is passed in + * @method get + * @param {String} id + * @returns {Object} model + */ + get: function (id) { + var returns; + for (var i = 0; i < this._models().length; i++) { + if (this._models()[i].getId() === id) { + returns = this._models()[i]; + } + } + return returns; + }, - setupObservables.call(this, options); + /** + * Returns the number of models in the collection + * @method count + * @returns {Number} + */ + count: function () { + return this._models().length; + }, - this.watchValidations(); + /** + * Returns the sorted version of the array base on the sorting function that is passed in + * @method sort + * @param {String} name The observable to sort by + * @param {Boolean} desc Sort in descending order + */ + sort: function (name, desc) { + this._models.sort(function (a, b) { + return (desc) ? a[name]() < b[name]() : a[name]() > b[name](); + }); + }, - if (this.validation) { - setupValidation.call(this); - } + /** + * Makes the collection automatically sync with backend services any time there is a change + * @method autoSync + * @param {Boolean} autoSync When this is set to true, we automatically sync the collection, if it is set to false, we don't + */ + autoSync: function (autoSync) { + var models = this._models(); + + if (autoSync) { + this.fetch(); + _autoSync = true; + } else if (_autoSync) { + _autoSync = false; + } - this.initialize.apply(this, arguments); + // Update the models to autosync with the collection + for (var i = 0; i < models.length; i++) { + models[i].autoSync(autoSync); + } - if (this.model) { - setupModel.call(this, options); - } + } - if (this.autoSync) { - this.fetch(); - } + }); - if (this.el) { - ko.applyBindings(this, (typeof this.el === 'object') ? this.el : document.querySelector(this.el)[0]); + if (this.model) { + this.model.prototype.storage = this.storage; + _.each(models || [{}], this.insert, this); } }; - _.extend(ViewModel.prototype, Events, Validation, { - initialize: function() {} - }); - - ViewModel.extend = extend; + Collection.extend = extend; if (typeof define === 'function' && define.amd) { - return ViewModel; + return Collection; } else { - ko.ninjaViewModel = ViewModel; + ko.ninjaCollection = Collection; } })); @@ -1110,29 +1634,26 @@ 'underscore', 'knockout', 'ko.ninja.viewModel', - 'ko.ninja.model' + 'ko.ninja.model', + 'ko.ninja.collection' ], factory); // Non-AMD } else { - factory(root._, root.ko, root.ko.ninjaViewModel, root.ko.ninjaModel); + factory(root._, root.ko, root.ko.ninjaViewModel, root.ko.ninjaModel, root.ko.ninjaCollection); } -} (this, function (_, ko, ViewModel, Model) { +} (this, function (_, ko, ViewModel, Model, Collection) { ko.ViewModel = ViewModel; ko.Model = Model; + ko.Collection = Collection; // AMD if (typeof define === 'function' && define.amd) { return ko; - - // Non-AMD - } else { - ko.ViewModel = ViewModel; - ko.Model = Model; } })); \ No newline at end of file diff --git a/dist/ko.ninja.min.js b/dist/ko.ninja.min.js index fa08e26..35ccc07 100644 --- a/dist/ko.ninja.min.js +++ b/dist/ko.ninja.min.js @@ -1 +1 @@ -!function(e,t){"function"==typeof define&&define.amd?define("ko.ninja.events",["underscore"],t):t(e._,e.ko)}(this,function(e,t){var n,i,o,a,s=/\s+/;return i=function(e,t,n,i){if(!n)return!0;if("object"==typeof n){for(var o in n)e[t].apply(e,[o,n[o]].concat(i));return!1}if(s.test(n)){for(var a=n.split(s),r=0,c=a.length;c>r;r++)e[t].apply(e,[a[r]].concat(i));return!1}return!0},a=function(e,t){var n,i=-1,o=e.length,a=t[0],s=t[1],r=t[2];switch(t.length){case 0:for(;++iu;u++)if(t=c[u],r===this._events[t]){if(this._events[t]=a=[],n||o)for(d=0,l=r.length;l>d;d++)s=r[d],(n&&n!==s.callback&&n!==s.callback._callback||o&&o!==s.context)&&a.push(s);a.length||delete this._events[t]}return this},trigger:function(e){if(!this._events)return this;var t=Array.prototype.slice.call(arguments,1);if(!i(this,"trigger",e,t))return this;var n=this._events[e],o=this._events.all;return n&&a(n,t),o&&a(o,arguments),this},stopListening:function(e,t,n){var i=this._listeners;if(!i)return this;var o=!t&&!n;"object"==typeof t&&(n=this),e&&((i={})[e._listenerId]=e);for(var a in i)i[a].off(t,n,this),o&&delete this._listeners[a];return this}},o={listenTo:"on",listenToOnce:"once"},e.each(o,function(t,i){n[i]=function(n,i,o){var a=this._listeners||(this._listeners={}),s=n._listenerId||(n._listenerId=e.uniqueId("l"));return a[s]=n,"object"==typeof i&&(o=this),n[t](i,o,this),this}}),"function"==typeof define&&define.amd?n:(t.ninjaEvents=n,void 0)}),function(e,t){"function"==typeof define&&define.amd?define("ko.ninja.extend",["underscore"],t):t(e._,e.ko)}(this,function(e,t){var n=function(t,n){var i,o,a=this;return o=t&&e.has(t,"constructor")?t.constructor:function(){return a.apply(this,arguments)},e.extend(o,a,n),i=function(){this.constructor=o},i.prototype=a.prototype,o.prototype=new i,t&&e.extend(o.prototype,t),o.__super__=a.prototype,t.name&&(o.prototype.toString=function(){return t.name}),o};return"function"==typeof define&&define.amd?n:(t.ninjaExtend=n,void 0)}),function(e,t){"function"==typeof define&&define.amd?define("ko.ninja.baseModel",["underscore","ko.ninja.events","ko.ninja.extend"],t):t(e._,e.ko.ninjaEvents,e.ko.ninjaExtend,e.ko)}(this,function(e,t,n,i){var o=function(n){n=n||{},e.isFunction(this.initialize)&&this.initialize(n),e.extend(this,t,{idAttribute:"id",invalid:function(){return localStorage?this.name?void 0:{error:!0,message:"This model has no name. Every model needs a name"}:{error:!0,message:"There is no localStorage available in this context"}},save:function(e,t){return e[this.idAttribute]?this.update(e[this.idAttribute],e,t):this.insert(e,t)}},n)};return o.extend=n,"function"==typeof define&&define.amd?o:(i.ninjaBaseModel=o,void 0)}),function(e,t){"function"==typeof define&&define.amd?define("ko.ninja.localStorageModel",["ko.ninja.baseModel"],t):t(e.ko.ninjaBaseModel,e.ko)}(this,function(e,t){var n=e.extend({find:function(e,t){var n,i=[];if(t=t||e,this.invalid())t(this.invalid(e));else{for(var o in localStorage)if(~o.indexOf(this.name+"-")){n=!0;for(var a in e)e[a]!==JSON.parse(localStorage[o])[a]&&(n=!1);n&&i.push(JSON.parse(localStorage.getItem(o)))}t(i)}},findOne:function(e,t){this.invalid()?t(this.invalid()):t(JSON.parse(localStorage[this.name+"-"+e]||"{}"))},insert:function(e,t){t=t||function(){},e[this.idAttribute]=(new Date).getTime(),this.invalid(e)?t(this.invalid(e)):(localStorage[this.name+"-"+e.id]=JSON.stringify(e),t(e))},remove:function(e,t){t=t||function(){},this.invalid()?t(this.invalid()):(delete localStorage[this.name+"-"+e],t(null))},update:function(e,t,n){n=n||function(){},this.invalid(t)?n(this.invalid(t)):(t[this.idAttribute]=e,localStorage[this.name+"-"+e]=JSON.stringify(t),n(t))}});return"function"==typeof define&&define.amd?n:(t.ninjaLocalStorageModel=n,void 0)}),function(e,t){"function"==typeof define&&define.amd?define("ko.ninja.ajax",[],t):t(e.ko)}(this,function(e){var t=function(e,t){var n,i=this;this.updating=!1,this.abort=function(){i.updating&&(i.updating=!1,i.AJAX.abort(),i.AJAX=null)},this.update=function(e,t){return i.updating?!1:(i.AJAX=null,i.AJAX=window.XMLHttpRequest?new XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP"),null===i.AJAX?!1:(i.AJAX.onreadystatechange=function(){4===i.AJAX.readyState&&(i.updating=!1,i.callback(i.AJAX.responseText,i.AJAX.status,i.AJAX.responseXML),i.AJAX=null)},i.updating=new Date,/post/i.test(t)?(n=o+"?"+i.updating.getTime(),i.AJAX.open("POST",n,!0),i.AJAX.setRequestHeader("Content-type","application/x-www-form-urlencoded"),i.AJAX.send(e)):/put/i.test(t)?(n=o+"?"+i.updating.getTime(),i.AJAX.open("PUT",n,!0),i.AJAX.setRequestHeader("Content-type","application/x-www-form-urlencoded"),i.AJAX.send(e)):(n=o+"?"+e+"×tamp="+i.updating.getTime(),i.AJAX.open("GET",n,!0),i.AJAX.send(null)),!0))};var o=e;this.callback=t||function(){}};return"function"==typeof define&&define.amd?t:(e.ninjaAjax=t,void 0)}),function(e,t){"function"==typeof define&&define.amd?define("ko.ninja.httpModel",["ko.ninja.baseModel","ko.ninja.ajax","underscore"],t):t(e.ko.ninjaBaseModel,e.ko.ninjaAjax,e._,e.ko)}(this,function(e,t,n,i){var o=e.extend({suffix:"",urlRoot:function(){return(this.name?"/"+this.name:"")+"/"},ajax:function(e){var i;"function"==typeof $&&$.ajax?$.ajax(n.extend({success:function(t){e.complete(t)}.bind(this),error:function(t){e.complete({error:!0,message:t})}.bind(this)},e)):(i=new t(e.url,function(t){n.isFunction(e.complete)&&e.complete(JSON.parse(t))}),i.update((JSON.stringify(e.data)||"").replace(/:/g,"=").replace(/"/g,"").replace(/,/g,"&").replace(/{/g,"").replace(/}/g,""),e.method))},find:function(e,t){this.ajax({url:this.urlRoot()+this.suffix,method:"GET",data:e,complete:t||e})},findOne:function(e,t){this.ajax({url:this.urlRoot()+e+this.suffix,method:"GET",complete:t})},insert:function(e,t){t=t||function(){},this.ajax({url:this.urlRoot()+this.suffix,method:"POST",data:e,complete:t})},remove:function(e,t){t=t||function(){},this.ajax({url:this.urlRoot()+e+this.suffix,method:"DELETE",complete:t})},update:function(e,t,n){n=n||function(){},this.ajax({url:this.urlRoot()+e+this.suffix,method:"PUT",data:t,complete:n})}});return"function"==typeof define&&define.amd?o:(i.ninjaHttpModel=o,void 0)}),function(e,t){"function"==typeof define&&define.amd?define("ko.ninja.socketModel",["ko.ninja.baseModel","underscore"],t):t(e.ko.ninjaBaseModel,e._,e.ko)}(this,function(e,t,n){var i=e.extend({find:function(e,t){t||(t=e),this.socket.emit(this.messageNames.find,{data:e},t)},findOne:function(e,t){this.socket.emit(this.messageNames.findOne,{id:e},t)},insert:function(e,t){this.socket.emit(this.messageNames.insert,{data:e},t)},remove:function(e,t){this.socket.emit(this.messageNames.remove,{id:e},t)},update:function(e,t,n){this.socket.emit(this.messageNames.update,{id:e,data:t},n)},initialize:function(e){this.socket=io.connect((e.protocol||"http")+"://"+(e.hostName||"localhost")+":"+(e.port||"8080")),this.messageNames=t.extend({update:e.name+"-update",insert:e.name+"-insert",find:e.name+"-find",findOne:e.name+"-findOne",remove:e.name+"-remove"},e.messageNames||{})}});return"function"==typeof define&&define.amd?i:(n.ninjaSocketModel=i,void 0)}),function(e,t){"function"==typeof define&&define.amd?define("ko.ninja.model",["ko.ninja.extend","ko.ninja.localStorageModel","ko.ninja.httpModel","ko.ninja.socketModel"],t):t(e.ko.ninjaExtend,e.ko.ninjaLocalStorageModel,e.ko.ninjaSocketModel)}(this,function(e,t,n,i){var o=function(e){var o;switch(e.storage){case"http":o=new n(e);break;case"socket.io":o=new i(e);break;default:o=new t(e)}return o};return o.extend=e,"function"==typeof define&&define.amd?o:(ko.ninjaModel=o,void 0)}),function(e,t){"function"==typeof define&&define.amd?define("ko.ninja.validation",["knockout","underscore"],t):t(e.ko,e._)}(this,function(e,t){var n={validationTypes:{_custom:function(e,t){return t.validator.call(this,e,t)},_maxLength:function(e,t){return e.length>t.value},_minLength:function(e,t){return e.lengthr;r++)t[e].apply(t,[s[r]].concat(i));return!1}return!0},s=function(t,e){var n,i=-1,o=t.length,s=e[0],a=e[1],r=e[2];switch(e.length){case 0:for(;++iu;u++)if(e=c[u],r===this._events[e]){if(this._events[e]=s=[],n||o)for(h=0,d=r.length;d>h;h++)a=r[h],(n&&n!==a.callback&&n!==a.callback._callback||o&&o!==a.context)&&s.push(a);s.length||delete this._events[e]}return this},trigger:function(t){if(!this._events)return this;var e=Array.prototype.slice.call(arguments,1);if(!i(this,"trigger",t,e))return this;var n=this._events[t],o=this._events.all;return n&&s(n,e),o&&s(o,arguments),this},stopListening:function(t,e,n){var i=this._listeners;if(!i)return this;var o=!e&&!n;"object"==typeof e&&(n=this),t&&((i={})[t._listenerId]=t);for(var s in i)i[s].off(e,n,this),o&&delete this._listeners[s];return this}},o={listenTo:"on",listenToOnce:"once"},t.each(o,function(e,i){n[i]=function(n,i,o){var s=this._listeners||(this._listeners={}),a=n._listenerId||(n._listenerId=t.uniqueId("l"));return s[a]=n,"object"==typeof i&&(o=this),n[e](i,o,this),this}}),"function"==typeof define&&define.amd?n:(e.ninjaEvents=n,void 0)}),function(t,e){"function"==typeof define&&define.amd?define("ko.ninja.extend",["underscore"],e):e(t._,t.ko)}(this,function(t,e){var n=function(e,n){var i,o,s=this;return o=e&&t.has(e,"constructor")?e.constructor:function(){return s.apply(this,arguments)},t.extend(o,s,n),i=function(){this.constructor=o},i.prototype=s.prototype,o.prototype=new i,e&&t.extend(o.prototype,e),o.__super__=s.prototype,e.name&&(o.prototype.toString=function(){return e.name}),o};return"function"==typeof define&&define.amd?n:(e.ninjaExtend=n,void 0)}),function(t,e){"function"==typeof define&&define.amd?define("ko.ninja.viewModel",["knockout","underscore","ko.ninja.events","ko.ninja.extend"],e):e(t.ko,t._,t.ko.ninjaEvents,t.ko.ninjaExtend,t.ko.ninjaModel)}(this,function(t,e,n,i){var o=function(n){var i=e.functions(this.observables);i=e.reduce(this.observables,function(t,n,i){return e.isObject(n)&&!e.isArray(n)&&(n.read||n.write)&&t.push(i),t},i),e.each(e.omit(this.observables,i),function(i,o){this[o]=e.isArray(i)?t.isObservable(n[o])?n[o]:t.observableArray((n[o]||i).slice(0)):t.isObservable(n[o])?n[o]:t.observable(n[o]||i),this[o].subscribe(function(t){this.trigger("change:"+o,t)},this)},this),e.each(e.pick(this.observables,i),function(e,n){this[n]=t.computed({read:this.observables[n],write:function(){},owner:this},this)},this)},s=function(){var t=this.model;this.options&&this.options.autoSync&&t.autoSync(this.options.autoSync,!0),e.each(t.observables,function(e,n){this[n]=t[n]},this)},a=function(){var t=this.collections;for(var e in t)t.hasOwnProperty(e)&&(this.options&&this.options.autoSync&&t[e].autoSync(this.options.autoSync),this[e]=t[e]._models)},r=function(e){e=e||{},this.model&&s.call(this,e),this.collections&&a.call(this,e),o.call(this,e),this.initialize.apply(this,arguments),e.el=e.el||this.el,e.el&&t.applyBindings(this,"object"==typeof e.el?e.el:document.querySelector(e.el)[0])};return e.extend(r.prototype,n,{initialize:function(){}}),r.extend=i,"function"==typeof define&&define.amd?r:(t.ninjaViewModel=r,void 0)}),function(t,e){"function"==typeof define&&define.amd?define("ko.ninja.class",["knockout","underscore","ko.ninja.events","ko.ninja.extend"],e):e(t.ko,t._,t.ko.ninjaEvents,t.ko.ninjaExtend)}(this,function(t,e,n,i){var o=function(t){t=t||{},this._setupObservables.call(this,t),this._setup&&this._setup(t),this.initialize(t)};return e.extend(o.prototype,n,{_setupObservables:function(n){if(this.observables&&n){var i;e.each(n,function(t,i){e.isUndefined(this.observables[i])||(this.observables[i]=t,delete n[i])},this),i=e.functions(this.observables),i=e.reduce(this.observables,function(t,n,i){return e.isObject(n)&&!e.isArray(n)&&(n.read||n.write)&&t.push(i),t},i),e.each(e.omit(this.observables,i),function(i,o){this[o]=e.isArray(i)?t.isObservable(n[o])?n[o]:t.observableArray((n[o]||i).slice(0)):t.isObservable(n[o])?n[o]:t.observable(n[o]||i),this[o].subscribe(function(t){this.trigger("change:"+o,t)},this)},this),e.each(e.pick(this.observables,i),function(e,n){this[n]=t.computed({read:this.observables[n],write:function(){},owner:this},this)},this)}},initialize:function(){}}),o.extend=i,"function"==typeof define&&define.amd?o:(t.ninjaClass=o,void 0)}),function(t,e){"function"==typeof define&&define.amd?define("ko.ninja.baseStorage",["underscore","ko.ninja.events","ko.ninja.extend"],e):e(t._,t.ko.ninjaEvents,t.ko.ninjaExtend,t.ko)}(this,function(t,e,n,i){var o=function(n){n=n||{},t.isFunction(this.initialize)&&this.initialize(n),t.extend(this,e,{options:{},idAttribute:"id",invalid:function(){return localStorage?this.options.name?void 0:{error:!0,message:"This storage has no name. Every storage needs a name"}:{error:!0,message:"There is no localStorage available in this context"}},save:function(e,n){return e||(e={},t.each(this.observables,function(t,n){e[n]=this[n]()},this)),e[this.idAttribute]?this.update(e[this.idAttribute],e,n):this.insert(e,n)},fetch:function(){}},n)};return o.extend=n,"function"==typeof define&&define.amd?o:(i.ninjaBaseStorage=o,void 0)}),function(t,e){"function"==typeof define&&define.amd?define("ko.ninja.localStorage",["ko.ninja.baseStorage"],e):e(t.ko.ninjaBaseStorage,t.ko)}(this,function(t,e){var n=t.extend({find:function(t,e){var n,i=[];if(e=e||t,this.invalid())e(this.invalid(t));else{for(var o in localStorage)if(~o.indexOf(this.options.name+"-")){n=!0;for(var s in t)t[s]!==JSON.parse(localStorage[o])[s]&&(n=!1);n&&i.push(JSON.parse(localStorage.getItem(o)))}e(i)}},findOne:function(t,e){this.invalid()?e(this.invalid()):e(JSON.parse(localStorage[this.options.name+"-"+t]||"{}"))},insert:function(t,e){e=e||function(){},t[this.idAttribute]=(new Date).getTime(),this.invalid(t)?e(this.invalid(t)):(localStorage[this.options.name+"-"+t.id]=JSON.stringify(t),e(t))},remove:function(t,e){e=e||function(){},t=t||this.getId(),this.invalid()?e(this.invalid()):(delete localStorage[this.options.name+"-"+t],e(null))},update:function(t,e,n){n=n||function(){},this.invalid(e)?n(this.invalid(e)):(e[this.idAttribute]=t,localStorage[this.options.name+"-"+t]=JSON.stringify(e),n(e))},initialize:function(t){t&&t.options&&(this.options=t.options)}});return"function"==typeof define&&define.amd?n:(e.ninjaLocalStorageModel=n,void 0)}),function(t,e){"function"==typeof define&&define.amd?define("ko.ninja.ajax",[],e):e(t.ko)}(this,function(t){var e=function(t,e){var n,i=this;this.updating=!1,this.abort=function(){i.updating&&(i.updating=!1,i.AJAX.abort(),i.AJAX=null)},this.update=function(t,e){return i.updating?!1:(i.AJAX=null,i.AJAX=window.XMLHttpRequest?new XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP"),null===i.AJAX?!1:(i.AJAX.onreadystatechange=function(){4===i.AJAX.readyState&&(i.updating=!1,i.callback(i.AJAX.responseText,i.AJAX.status,i.AJAX.responseXML),i.AJAX=null)},i.updating=new Date,/post/i.test(e)?(n=o+"?"+i.updating.getTime(),i.AJAX.open("POST",n,!0),i.AJAX.setRequestHeader("Content-type","application/x-www-form-urlencoded"),i.AJAX.send(t)):/put/i.test(e)?(n=o+"?"+i.updating.getTime(),i.AJAX.open("PUT",n,!0),i.AJAX.setRequestHeader("Content-type","application/x-www-form-urlencoded"),i.AJAX.send(t)):/delete/i.test(e)?(n=o+"?"+i.updating.getTime(),i.AJAX.open("DELETE",n,!0),i.AJAX.setRequestHeader("Content-type","application/x-www-form-urlencoded"),i.AJAX.send(t)):(n=o+"?"+t+"×tamp="+i.updating.getTime(),i.AJAX.open("GET",n,!0),i.AJAX.send(null)),!0))};var o=t;this.callback=e||function(){}};return"function"==typeof define&&define.amd?e:(t.ninjaAjax=e,void 0)}),function(t,e){"function"==typeof define&&define.amd?define("ko.ninja.httpStorage",["ko.ninja.baseStorage","ko.ninja.ajax","underscore"],e):e(t.ko.ninjaBaseStorage,t.ko.ninjaAjax,t._,t.ko)}(this,function(t,e,n,i){var o=t.extend({suffix:"",urlRoot:function(){return(this.options.name?"/"+this.options.name:"")+"/"},ajax:function(t){var i;"function"==typeof $&&$.ajax?$.ajax(n.extend({success:function(e){t.complete(e)}.bind(this),error:function(e){t.complete({error:!0,message:e})}.bind(this)},t)):(i=new e(t.url,function(e){n.isFunction(t.complete)&&t.complete(JSON.parse(e))}),i.update((JSON.stringify(t.data)||"").replace(/:/g,"=").replace(/"/g,"").replace(/,/g,"&").replace(/{/g,"").replace(/}/g,""),t.method))},find:function(t,e){this.ajax({url:this.urlRoot()+this.suffix,method:"GET",data:t,complete:e||t})},findOne:function(t,e){this.ajax({url:this.urlRoot()+t+this.suffix,method:"GET",complete:e})},insert:function(t,e){e=e||function(){},this.ajax({url:this.urlRoot()+this.suffix,method:"POST",data:t,complete:e})},remove:function(t,e){e=e||function(){},t||(t=this.getId()),this.ajax({url:this.urlRoot()+t+this.suffix,method:"DELETE",complete:e})},update:function(t,e,n){n=n||function(){},t||(t=this.getId()),this.ajax({url:this.urlRoot()+t+this.suffix,method:"PUT",data:e,complete:n})}});return"function"==typeof define&&define.amd?o:(i.ninjaHttpStorage=o,void 0)}),function(t,e){"function"==typeof define&&define.amd?define("ko.ninja.socketStorage",["ko.ninja.baseStorage","underscore"],e):e(t.ko.ninjaBaseStorage,t._,t.ko)}(this,function(t,e,n){var i=t.extend({find:function(t,e){e||(e=t),this.socket.emit(this.messageNames.find,{data:t},e)},findOne:function(t,e){t||(t=this.getId()),this.socket.emit(this.messageNames.findOne,{id:t},e)},insert:function(t,e){this.socket.emit(this.messageNames.insert,{data:t},e)},remove:function(t,e){t||(t=this.getId()),this.socket.emit(this.messageNames.remove,{id:t},e)},update:function(t,e,n){t||(t=this.getId()),this.socket.emit(this.messageNames.update,{id:t,data:e},n)},initialize:function(t){t=t||{},this.options=e.extend({protocol:"http",hostName:"localhost",port:8080,name:"list"},t.options||{}),this.socket=io.connect(this.options.protocol+"://"+this.options.hostName+":"+this.options.port),this.messageNames=e.extend({update:this.options.name+"-update",insert:this.options.name+"-insert",find:this.options.name+"-find",findOne:this.options.name+"-findOne",remove:this.options.name+"-remove"},this.options.messageNames||{})}});return"function"==typeof define&&define.amd?i:(n.ninjaSocketStorage=i,void 0)}),function(t,e){"function"==typeof define&&define.amd?define("ko.ninja.storage",["ko.ninja.extend","ko.ninja.localStorage","ko.ninja.httpStorage","ko.ninja.socketStorage"],e):e(t.ko.ninjaExtend,t.ko.ninjaLocalStorageStorage,t.ko.ninjaSocketStorage)}(this,function(t,e,n,i){var o=function(t){var o;if(!t||!t.options||!t.options.storage)return{};switch(t.options.storage){case"http":o=new n(t);break;case"socket.io":o=new i(t);break;case"localStorage":o=new e(t)}return o};return o.extend=t,"function"==typeof define&&define.amd?o:(ko.ninjaStorage=o,void 0)}),function(t,e){"function"==typeof define&&define.amd?define("ko.ninja.validation",["knockout","underscore"],e):e(t.ko,t._)}(this,function(t,e){var n={validationTypes:{_custom:function(t,e){return e.validator.call(this,t,e)},_maxLength:function(t,e){return t.length>e.value},_minLength:function(t,e){return t.lengthi[t]()})},autoSync:function(t){var e=this._models();t?(this.fetch(),s=!0):s&&(s=!1);for(var n=0;n li > div { - -moz-box-sizing: border-box; /* firefox */ - -ms-box-sizing: border-box; /* ie */ - -webkit-box-sizing: border-box; /* webkit */ - -khtml-box-sizing: border-box; /* konqueror */ - box-sizing: border-box; /* css3 */ -} - - -/*---------------------- Jump Page -----------------------------*/ -#jump_to, #jump_page { - margin: 0; - background: white; - -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; - -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; - font: 16px Arial; - cursor: pointer; - text-align: right; - list-style: none; -} - -#jump_to a { - text-decoration: none; -} - -#jump_to a.large { - display: none; -} -#jump_to a.small { - font-size: 22px; - font-weight: bold; - color: #676767; -} - -#jump_to, #jump_wrapper { - position: fixed; - right: 0; top: 0; - padding: 10px 15px; - margin:0; -} - -#jump_wrapper { - display: none; - padding:0; -} - -#jump_to:hover #jump_wrapper { - display: block; -} - -#jump_page { - padding: 5px 0 3px; - margin: 0 0 25px 25px; -} - -#jump_page .source { - display: block; - padding: 15px; - text-decoration: none; - border-top: 1px solid #eee; -} - -#jump_page .source:hover { - background: #f5f5ff; -} - -#jump_page .source:first-child { -} - -/*---------------------- Low resolutions (> 320px) ---------------------*/ -@media only screen and (min-width: 320px) { - .pilwrap { display: none; } - - ul.sections > li > div { - display: block; - padding:5px 10px 0 10px; - } - - ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { - padding-left: 30px; - } - - ul.sections > li > div.content { - background: #f5f5ff; - overflow-x:auto; - -webkit-box-shadow: inset 0 0 5px #e5e5ee; - box-shadow: inset 0 0 5px #e5e5ee; - border: 1px solid #dedede; - margin:5px 10px 5px 10px; - padding-bottom: 5px; - } - - ul.sections > li > div.annotation pre { - margin: 7px 0 7px; - padding-left: 15px; - } - - ul.sections > li > div.annotation p tt, .annotation code { - background: #f8f8ff; - border: 1px solid #dedede; - font-size: 12px; - padding: 0 0.2em; - } -} - -/*---------------------- (> 481px) ---------------------*/ -@media only screen and (min-width: 481px) { - #container { - position: relative; - } - body { - background-color: #F5F5FF; - font-size: 15px; - line-height: 21px; - } - pre, tt, code { - line-height: 18px; - } - p, ul, ol { - margin: 0 0 15px; - } - - - #jump_to { - padding: 5px 10px; - } - #jump_wrapper { - padding: 0; - } - #jump_to, #jump_page { - font: 10px Arial; - text-transform: uppercase; - } - #jump_page .source { - padding: 5px 10px; - } - #jump_to a.large { - display: inline-block; - } - #jump_to a.small { - display: none; - } - - - - #background { - position: absolute; - top: 0; bottom: 0; - width: 350px; - background: #fff; - border-right: 1px solid #e5e5ee; - z-index: -1; - } - - ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { - padding-left: 40px; - } - - ul.sections > li { - white-space: nowrap; - } - - ul.sections > li > div { - display: inline-block; - } - - ul.sections > li > div.annotation { - max-width: 350px; - min-width: 350px; - min-height: 5px; - padding: 13px; - overflow-x: hidden; - white-space: normal; - vertical-align: top; - text-align: left; - } - ul.sections > li > div.annotation pre { - margin: 15px 0 15px; - padding-left: 15px; - } - - ul.sections > li > div.content { - padding: 13px; - vertical-align: top; - background: #f5f5ff; - border: none; - -webkit-box-shadow: none; - box-shadow: none; - } - - .pilwrap { - position: relative; - display: inline; - } - - .pilcrow { - font: 12px Arial; - text-decoration: none; - color: #454545; - position: absolute; - top: 3px; left: -20px; - padding: 1px 2px; - opacity: 0; - -webkit-transition: opacity 0.2s linear; - } - .for-h1 .pilcrow { - top: 47px; - } - .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow { - top: 35px; - } - - ul.sections > li > div.annotation:hover .pilcrow { - opacity: 1; - } -} - -/*---------------------- (> 1025px) ---------------------*/ -@media only screen and (min-width: 1025px) { - - body { - font-size: 16px; - line-height: 24px; - } - - #background { - width: 525px; - } - ul.sections > li > div.annotation { - max-width: 525px; - min-width: 525px; - padding: 10px 25px 1px 50px; - } - ul.sections > li > div.content { - padding: 9px 15px 16px 25px; - } -} - -/*---------------------- Syntax Highlighting -----------------------------*/ - -td.linenos { background-color: #f0f0f0; padding-right: 10px; } -span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } -/* - -github.com style (c) Vasily Polovnyov - -*/ - -pre code { - display: block; padding: 0.5em; - color: #000; - background: #f8f8ff -} - -pre .comment, -pre .template_comment, -pre .diff .header, -pre .javadoc { - color: #408080; - font-style: italic -} - -pre .keyword, -pre .assignment, -pre .literal, -pre .css .rule .keyword, -pre .winutils, -pre .javascript .title, -pre .lisp .title, -pre .subst { - color: #954121; - /*font-weight: bold*/ -} - -pre .number, -pre .hexcolor { - color: #40a070 -} - -pre .string, -pre .tag .value, -pre .phpdoc, -pre .tex .formula { - color: #219161; -} - -pre .title, -pre .id { - color: #19469D; -} -pre .params { - color: #00F; -} - -pre .javascript .title, -pre .lisp .title, -pre .subst { - font-weight: normal -} - -pre .class .title, -pre .haskell .label, -pre .tex .command { - color: #458; - font-weight: bold -} - -pre .tag, -pre .tag .title, -pre .rules .property, -pre .django .tag .keyword { - color: #000080; - font-weight: normal -} - -pre .attribute, -pre .variable, -pre .instancevar, -pre .lisp .body { - color: #008080 -} - -pre .regexp { - color: #B68 -} - -pre .class { - color: #458; - font-weight: bold -} - -pre .symbol, -pre .ruby .symbol .string, -pre .ruby .symbol .keyword, -pre .ruby .symbol .keymethods, -pre .lisp .keyword, -pre .tex .special, -pre .input_number { - color: #990073 -} - -pre .builtin, -pre .constructor, -pre .built_in, -pre .lisp .title { - color: #0086b3 -} - -pre .preprocessor, -pre .pi, -pre .doctype, -pre .shebang, -pre .cdata { - color: #999; - font-weight: bold -} - -pre .deletion { - background: #fdd -} - -pre .addition { - background: #dfd -} - -pre .diff .change { - background: #0086b3 -} - -pre .chunk { - color: #aaa -} - -pre .tex .formula { - opacity: 0.5; -} diff --git a/docs/httpStorage.md b/docs/httpStorage.md new file mode 100644 index 0000000..1a1b6ae --- /dev/null +++ b/docs/httpStorage.md @@ -0,0 +1,29 @@ +# http Storage + +HTTP expects a REST API and can be defined like this: + +```js +ko.Model.extend({ + + // The name of your model. If the urlRoot is not specified, + // this will be used to build the urlRoot as well. + name: 'list' + + options: { + + // The root that all ajax calls are made relative to + urlRoot: function () { + return '/list/' + }, + + // If you have a suffix appended to each URL, this can + // be used. It defaults to an empty string. + suffix: '.json', + + // For HTTP, this should always be http + storage: 'http' + + } + +}); +``` \ No newline at end of file diff --git a/docs/ko.ninja.html b/docs/ko.ninja.html deleted file mode 100644 index 6a7d329..0000000 --- a/docs/ko.ninja.html +++ /dev/null @@ -1,532 +0,0 @@ - - - - - ko.ninja.js - - - - - -
-
- -
    - -
  • -
    -

    ko.ninja.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    /*! ko.ninja - v0.0.1 - 2013-08-14
    -* Copyright (c) 2013 ; Licensed  */
    -(function (root, factory) {
    -    if (typeof define === "function" && define.amd) {
    -        define(["knockout", "underscore"], factory);
    -    } else if (typeof exports === "object") {
    -        module.exports = factory(require("knockout"), require("underscore"));
    -    } else {
    -        root.ko = factory(root.ko, root._);
    -    }
    -}(this, function (ko, _) {
    -
    -var extend = 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;
    -};
    -
    -/**
    -* 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;
    -    };
    -});
    - -
  • - - -
  • -
    - -
    - -
    -

    ko.ViewModel

    - -
    - -
    var ViewModel = function ViewModel(options) {   
    -    options = options || {};
    -
    -    setupObservables.call(this, options);
    -
    -    if (this.validation) {
    -        setupValidation.call(this);
    -    }
    -
    -    this.initialize.call(this, options);
    -};
    -
    -_.extend(ViewModel.prototype, Events, {
    -    initialize: function() {}
    -});
    -
    -var setupObservables = function(options) {
    -    var self = this,        
    -        computedObservables = _.functions(this.observables);
    -
    -    computedObservables = _.reduce(this.observables, function(memo, value, prop) {
    -        if (_.isObject(value) && !_.isArray(value) && (value.read || value.write)) {
    -            memo.push(prop);
    -        }
    -        return memo;
    -    }, computedObservables);
    - -
  • - - -
  • -
    - -
    - -
    -

    Process the observables first

    - -
    - -
        _.each(_.omit(this.observables, computedObservables), function (value, prop) {
    -        if (_.isArray(value)) {
    -            if (ko.isObservable(options[prop])) {
    -                self[prop] = options[prop];
    -            }
    -            else {
    -                self[prop] = ko.observableArray((options[prop] || value).slice(0));
    -            }
    -        }
    -        else {
    -            if (ko.isObservable(options[prop])) {
    -                self[prop] = options[prop];
    -            }
    -            else {
    -                self[prop] = ko.observable(options[prop] || value);
    -            }
    -        }
    -    });
    - -
  • - - -
  • -
    - -
    - -
    -

    Now process the computedObservables

    - -
    - -
        _.each(_.pick(this.observables, computedObservables), function(value, prop) {
    -        self[prop] = ko.computed(self.observables[prop], self);
    -    });
    -};
    -
    -var setupValidation = function() {
    -
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    ko.Model

    - -
    - -
    var Model = function() {
    -    
    -};
    -
    -_.extend(Model.prototype, Events);
    -
    -ko.Model = Model;
    -ko.ViewModel = ViewModel;
    -
    -ko.Model.extend = ko.ViewModel.extend = extend;
    -
    -return ko;
    -
    -}));
    - -
  • - -
-
- - diff --git a/docs/localStorage.md b/docs/localStorage.md new file mode 100644 index 0000000..23ab3ef --- /dev/null +++ b/docs/localStorage.md @@ -0,0 +1,23 @@ +# local Storage + +This saves your collections or models to localStorage. Here is an example in the model: + +```js +ko.Model.extend({ + name: 'myModelName', + options: { + storage: 'localStorage' + } +}) +``` + +This is how to set it up in a collection: + +```js +ko.Collection.extend({ + name: 'myCollectionName', + options: { + storage: 'localStorage' + } +}); +``` \ No newline at end of file diff --git a/docs/models.md b/docs/models.md new file mode 100644 index 0000000..3528425 --- /dev/null +++ b/docs/models.md @@ -0,0 +1,220 @@ +# Models + +## Options + +#### idAttribute {String} +The id attribute on the viewModel. This is used for syncing with databases. Defaults to `id`. + +```js +ko.Model.extend({ + name: 'friend', + options: { + idAttribute: '_id' + } +}); +``` + +#### storage {String} + +The storage type to use with the model. If the model is inside a collection, the model will inherit the storage type from the collection. + +```js +ko.Model.extend({ + name: 'friend', + options: { + storage: 'localStorage' + } +}); +``` +Current storage types include: + +- [HTTP Storage](httpStorage.md) +- [Local Storage](localStorage.md) +- [Socket IO Storage](socketIoStorage.md) + +## Methods + +#### autoSync ({Boolean} autoSync) + +This makes the model automatically sync with the backend anytime there is a change. To turn autoSync on, pass in `true`: + +```js +dog.autoSync(true); +``` + +#### clear () + +Removes all of the data from the observables in the model: + +```js +dog.clear(); +``` + +#### get ({String} name) + +Returns one of the observable in the model: + +```js +dog.get('firstName'); // Fido +``` + +#### getId () + +Returns the id attribute of the observables. By default, the ID attribute is an observable named "id", but this can be overridden using the "idAttribute" option. + +```js +var Dog = ko.Model.extend({ + observables: { + id: 111 + } +}); +var fido = new Dog(); +console.log(fido.getId()); // 111 +``` + +#### has ({String} name) + +If the requested observable returns a truthy value, this will be true: + +```js +fido.set('firstName', 'Fido'); +console.log(fido.has('firstName')); // true +fido.set('firstName', null); +console.log(fido.has('firstName')); // false +``` + +#### toJSON () + +Returns the JSON values for the observables in the model. + +#### insert (data) +Inserts new data into the database. + +```js +var Model = ko.Model.extend({ + name: 'friend', + observables: { + firstName: '', + lastName: '', + id: null + } +}); +var model = new Model(); + +model.insert({ firstName: 'Milo', lastName: 'Cadenhead' }, function (data) { + console.log(data); + // { firstName: 'Milo', lastName: 'Cadenhead', id: 1382539084406 } +}); +``` + +#### update (id, data) +Updates the data in the database. + +```js +var Model = ko.Model.extend({ + name: 'friend', + observables: { + firstName: '', + lastName: '', + id: null + } +}); +var model = new Model(); + +model.update(1382539084406, { firstName: 'Linus' }, function () { + model.findOne(1382539084406, function (data) { + console.log(data); + // { firstName: 'Linus', lastName: 'Cadenhead', id: 1382539084406 } + }); +}); +``` + +#### save (data) +Instead of using update and insert, you can use save. If there is an id attribute, save will do an update, otherwise, it will do an insert. + +```js +var Model = ko.Model.extend({ + name: 'friend', + observables: { + firstName: '', + lastName: '', + id: null + } +}); +var model = new Model(); + +model.save({ firstName: 'Jonathan', lastName: 'Creamer', id: 1 }, function () { + console.log('Saved him!'); +}); +``` + +#### remove (id) +Removes a record from the database. + +```js +var Model = ko.Model.extend({ + name: 'friend', + observables: { + firstName: '', + lastName: '', + id: null + } +}); +var model = new Model(); + +model.remove(1382539084406, function () { + console.log('1382539084406 was removed'); +}); +``` + +#### findOne (id) +Returns a single viewModel with the matching id. + +```js +var Model = ko.Model.extend({ + name: 'friend', + observables: { + firstName: '', + lastName: '', + id: null + } +}); +var model = new Model(); + +model.save({ firstName: 'Jonathan', lastName: 'Creamer', id: 1 }, function () { + models.findOne(1, function (data) { + console.log(data); + // { firstName: 'Jonathan', lastName: 'Creamer', id: 1 } + }); +}); +``` + +#### find (query) +Search the data for any matches. All matches are returned in the promise. + +```js +var Model = ko.Model.extend({ + name: 'friends', + observables: { + firstName: '', + lastName: '', + id: null + } +}); +var model = new Model(); + +model.save({ firstName: 'Jonathan', lastName: 'Creamer', id: 1 }); +model.save({ firstName: 'Tyson', lastName: 'Cadenhead', id: 2 }); +model.save({ firstName: 'Linus', lastName: 'Cadenhead', id: 3 }); + +model.find({ + lastName: 'Cadenhead' +}, function (data) { + console.log(data); + // [{ firstName: 'Tyson', lastName: 'Cadenhead', id: 2 }, { firstName: 'Linus', lastName: 'Cadenhead', id: 3 }] +}); +``` + +## [Validations](validations.md) + +There are a ton of validations you can do on the model before it is allowed to save to a backend service. \ No newline at end of file diff --git a/docs/public/fonts/aller-bold.eot b/docs/public/fonts/aller-bold.eot deleted file mode 100755 index 1b32532..0000000 Binary files a/docs/public/fonts/aller-bold.eot and /dev/null differ diff --git a/docs/public/fonts/aller-bold.ttf b/docs/public/fonts/aller-bold.ttf deleted file mode 100755 index dc4cc9c..0000000 Binary files a/docs/public/fonts/aller-bold.ttf and /dev/null differ diff --git a/docs/public/fonts/aller-bold.woff b/docs/public/fonts/aller-bold.woff deleted file mode 100755 index fa16fd0..0000000 Binary files a/docs/public/fonts/aller-bold.woff and /dev/null differ diff --git a/docs/public/fonts/aller-light.eot b/docs/public/fonts/aller-light.eot deleted file mode 100755 index 40bd654..0000000 Binary files a/docs/public/fonts/aller-light.eot and /dev/null differ diff --git a/docs/public/fonts/aller-light.ttf b/docs/public/fonts/aller-light.ttf deleted file mode 100755 index c2c7290..0000000 Binary files a/docs/public/fonts/aller-light.ttf and /dev/null differ diff --git a/docs/public/fonts/aller-light.woff b/docs/public/fonts/aller-light.woff deleted file mode 100755 index 81a09d1..0000000 Binary files a/docs/public/fonts/aller-light.woff and /dev/null differ diff --git a/docs/public/fonts/novecento-bold.eot b/docs/public/fonts/novecento-bold.eot deleted file mode 100755 index 98a9a7f..0000000 Binary files a/docs/public/fonts/novecento-bold.eot and /dev/null differ diff --git a/docs/public/fonts/novecento-bold.ttf b/docs/public/fonts/novecento-bold.ttf deleted file mode 100755 index 2af39b0..0000000 Binary files a/docs/public/fonts/novecento-bold.ttf and /dev/null differ diff --git a/docs/public/fonts/novecento-bold.woff b/docs/public/fonts/novecento-bold.woff deleted file mode 100755 index de558b5..0000000 Binary files a/docs/public/fonts/novecento-bold.woff and /dev/null differ diff --git a/docs/public/stylesheets/normalize.css b/docs/public/stylesheets/normalize.css deleted file mode 100644 index 73abb76..0000000 --- a/docs/public/stylesheets/normalize.css +++ /dev/null @@ -1,375 +0,0 @@ -/*! normalize.css v2.0.1 | MIT License | git.io/normalize */ - -/* ========================================================================== - HTML5 display definitions - ========================================================================== */ - -/* - * Corrects `block` display not defined in IE 8/9. - */ - -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -nav, -section, -summary { - display: block; -} - -/* - * Corrects `inline-block` display not defined in IE 8/9. - */ - -audio, -canvas, -video { - display: inline-block; -} - -/* - * Prevents modern browsers from displaying `audio` without controls. - * Remove excess height in iOS 5 devices. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/* - * Addresses styling for `hidden` attribute not present in IE 8/9. - */ - -[hidden] { - display: none; -} - -/* ========================================================================== - Base - ========================================================================== */ - -/* - * 1. Sets default font family to sans-serif. - * 2. Prevents iOS text size adjust after orientation change, without disabling - * user zoom. - */ - -html { - font-family: sans-serif; /* 1 */ - -webkit-text-size-adjust: 100%; /* 2 */ - -ms-text-size-adjust: 100%; /* 2 */ -} - -/* - * Removes default margin. - */ - -body { - margin: 0; -} - -/* ========================================================================== - Links - ========================================================================== */ - -/* - * Addresses `outline` inconsistency between Chrome and other browsers. - */ - -a:focus { - outline: thin dotted; -} - -/* - * Improves readability when focused and also mouse hovered in all browsers. - */ - -a:active, -a:hover { - outline: 0; -} - -/* ========================================================================== - Typography - ========================================================================== */ - -/* - * Addresses `h1` font sizes within `section` and `article` in Firefox 4+, - * Safari 5, and Chrome. - */ - -h1 { - font-size: 2em; -} - -/* - * Addresses styling not present in IE 8/9, Safari 5, and Chrome. - */ - -abbr[title] { - border-bottom: 1px dotted; -} - -/* - * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome. - */ - -b, -strong { - font-weight: bold; -} - -/* - * Addresses styling not present in Safari 5 and Chrome. - */ - -dfn { - font-style: italic; -} - -/* - * Addresses styling not present in IE 8/9. - */ - -mark { - background: #ff0; - color: #000; -} - - -/* - * Corrects font family set oddly in Safari 5 and Chrome. - */ - -code, -kbd, -pre, -samp { - font-family: monospace, serif; - font-size: 1em; -} - -/* - * Improves readability of pre-formatted text in all browsers. - */ - -pre { - white-space: pre; - white-space: pre-wrap; - word-wrap: break-word; -} - -/* - * Sets consistent quote types. - */ - -q { - quotes: "\201C" "\201D" "\2018" "\2019"; -} - -/* - * Addresses inconsistent and variable font size in all browsers. - */ - -small { - font-size: 80%; -} - -/* - * Prevents `sub` and `sup` affecting `line-height` in all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sup { - top: -0.5em; -} - -sub { - bottom: -0.25em; -} - -/* ========================================================================== - Embedded content - ========================================================================== */ - -/* - * Removes border when inside `a` element in IE 8/9. - */ - -img { - border: 0; -} - -/* - * Corrects overflow displayed oddly in IE 9. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* ========================================================================== - Figures - ========================================================================== */ - -/* - * Addresses margin not present in IE 8/9 and Safari 5. - */ - -figure { - margin: 0; -} - -/* ========================================================================== - Forms - ========================================================================== */ - -/* - * Define consistent border, margin, and padding. - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/* - * 1. Corrects color not being inherited in IE 8/9. - * 2. Remove padding so people aren't caught out if they zero out fieldsets. - */ - -legend { - border: 0; /* 1 */ - padding: 0; /* 2 */ -} - -/* - * 1. Corrects font family not being inherited in all browsers. - * 2. Corrects font size not being inherited in all browsers. - * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome - */ - -button, -input, -select, -textarea { - font-family: inherit; /* 1 */ - font-size: 100%; /* 2 */ - margin: 0; /* 3 */ -} - -/* - * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in - * the UA stylesheet. - */ - -button, -input { - line-height: normal; -} - -/* - * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` - * and `video` controls. - * 2. Corrects inability to style clickable `input` types in iOS. - * 3. Improves usability and consistency of cursor style between image-type - * `input` and others. - */ - -button, -html input[type="button"], /* 1 */ -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; /* 2 */ - cursor: pointer; /* 3 */ -} - -/* - * Re-set default cursor for disabled elements. - */ - -button[disabled], -input[disabled] { - cursor: default; -} - -/* - * 1. Addresses box sizing set to `content-box` in IE 8/9. - * 2. Removes excess padding in IE 8/9. - */ - -input[type="checkbox"], -input[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/* - * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. - * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome - * (include `-moz` to future-proof). - */ - -input[type="search"] { - -webkit-appearance: textfield; /* 1 */ - -moz-box-sizing: content-box; - -webkit-box-sizing: content-box; /* 2 */ - box-sizing: content-box; -} - -/* - * Removes inner padding and search cancel button in Safari 5 and Chrome - * on OS X. - */ - -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/* - * Removes inner padding and border in Firefox 4+. - */ - -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} - -/* - * 1. Removes default vertical scrollbar in IE 8/9. - * 2. Improves readability and alignment in all browsers. - */ - -textarea { - overflow: auto; /* 1 */ - vertical-align: top; /* 2 */ -} - -/* ========================================================================== - Tables - ========================================================================== */ - -/* - * Remove most spacing between table cells. - */ - -table { - border-collapse: collapse; - border-spacing: 0; -} \ No newline at end of file diff --git a/docs/socketIoStorage.md b/docs/socketIoStorage.md new file mode 100644 index 0000000..2df915c --- /dev/null +++ b/docs/socketIoStorage.md @@ -0,0 +1,44 @@ +# socket.io Storage + +The socket.io storage expects a few different parameters. A socket.io setup might look something like this: + +```js +ko.ViewModel.extend({ + model: { + + // For socket.io, the storage should always be set to socket.io + storage: 'socket.io', + + // The name of the model. If message names are not specified, this will be used to generate the message names. This is required. + name: 'list', + + // The http protocol to use for socket.io messages. This is set to "http" by default + protocol: 'http', + + // The domain name to use for socket.io messages. This is set to "localhost" by default + domainName: 'localhost', + + // The port number to use for socket.io messages. This is set to 8080 by default + port: 3000, + + // The message names can be updated to be anything you want. These are all defaulted and not required. + messageNames: { + 'update': 'update-myList', // Defaults to {{name}}-update + 'insert': 'insert-intoMyList', // Defaults to {{name}}-insert + 'find': 'find-stuffInMyList', // Defaults to {{name}}-find + 'findOne': 'find-aThing', // Defaults to {{name}}-findOne + 'remove': 'remove-aThing' // Defaults to {{name}}-remove + } + + }, + .... +}); +``` + +It also important to note that ko.ninja does not require the socket.io JavaScript file. You will need to add it to your document like this: + +```html + +``` + +For more information on Socket.io and getting it set up, checkout out the [Socket.io documentation](http://socket.io/#how-to-use). \ No newline at end of file diff --git a/docs/validations.md b/docs/validations.md new file mode 100644 index 0000000..ae4cb79 --- /dev/null +++ b/docs/validations.md @@ -0,0 +1,201 @@ +# Model Validation + +The Ninja Model can handle all of your client-side validations with the `validation` object. Adding a validation object to your Model will look something like this: + +```js +var Person = ko.Model.extend({ + + observables: { + firstName: '', + lastName: '', + email: '', + phone: '', + answer: '' + }, + + validation: { + firstName: { + required: 'Your first name is required', + minLength: { + message: 'Please make sure your name is at least 3 characters long.', + value: 3 + } + }, + email: { + required: 'Your email address is required', + email: 'Please enter a valid email address' + }, + phone: { + number: 'Please enter a valid number', + length: { + message: 'Please make sure your phone number has 9 digits', + value: 9 + } + }, + answer: { + maxLength: { + message: 'You have entered more than 2 characters... there is no way you are typing "44"!', + value: 2 + }, + custom: { + message: 'Please enter "44"', + validator: function (value) { + return value !== '44'; + } + } + } + }, + + submitPerson: function () { + var errors = this.validate(); + if (!errors) { + alert('Your form has been submitted. Just kidding!') + } + } + +}); + +var tyson = new Person({}); +tyson.submitPerson(); +``` + +[Here is a JSFiddle](http://jsfiddle.net/tysoncadenhead/QUPg8/) showing the code above in action. + +As your observables are updated, there will also be an observable called [observableName].error that will be populated with errors on the observable. + +For example, if you want to watch for errors on the first name, your template would look like this: + +```html +

+ + +

+
+``` + +You never need to change the error because ko.ninja will keep track of updates for you and populate the error observable if needed. + +Each viewModel also has an `errors` observable array with all of the errors in the viewModel. To show a list of all of the errors in the form, you could do something like this: + +```html +
+

Here are the fields that have errors:

+
    +
  • + - + +
  • +
+
+``` + +Out of the box, ko.ninja comes with a few validators. You can also use the `custom` validator to create your own validation. + +##### required + +Checks to see if there is a value for the observable. + +```js +{ + required: 'This is required' +}, + +// Or +{ + required: { + message: 'This is required' + } +} +``` + +##### email + +Checks to see if the value is a valid email address. + +```js +{ + email: 'This is not an email address' +}, + +// Or +{ + email: { + message: 'This is not an email address' + } +} +``` + +##### number + +Checks to make sure that all of the characters in the observable are numbers. + +```js +{ + number: 'This is not a number' +}, + +// Or +{ + number: { + message: 'This is not a number' + } +} +``` + +##### maxLength + +Checks to make sure that the length is not more than the value passed in. + +```js +{ + length: { + message: 'This is more than 5 characters long', + value: 5 + } +} +``` + +##### minLength + +Checks to make sure that the length is not less than the value passed in. + +```js +{ + length: { + message: 'This is less than 5 characters long', + value: 5 + } +} +``` + +##### length + +Checks to make sure that the length is the same as the passed in value + +```js +{ + length: { + message: 'This is not 5 characters long', + value: 5 + } +} +``` + +##### custom + +If you want to create your own validation, use the custom validator. If the method returns a truthy value, the ViewModel will assume that there is an error. + +##### maxLength + +Checks to make sure that the length is not more than the value passed in. + +```js +{ + custom: { + message: 'Your name is not Tyson', + validator: function (value) { + return value !== 'Tyson'; + } + } +} +``` \ No newline at end of file diff --git a/docs/viewModels.md b/docs/viewModels.md new file mode 100644 index 0000000..cf9b364 --- /dev/null +++ b/docs/viewModels.md @@ -0,0 +1,47 @@ +# ViewModels + +The `ko.ViewModel` is a constructor to define ViewModels. + +```js +var Person = ko.ViewModel.extend({ + observables: { + firstName: "", + lastName: "", + fullName: function() { + return this.firstName() + " " + this.lastName()); + }, + friends: [] + } +}); + +var me = new Person({ + firstName: "Jonathan", + lastName: "Creamer" +}); + +me.firstName(); // "Jonathan" +me.fullName(); // "Jonathan Creamer" + +me.friends.push(new Person({ + firstName: "Tyson", + lastName: "Cadenhead" +})); +``` + +Some of the advantages are, you don't have to type `ko.observable()` all the time, and if you define a function in your `observables` property, it will create a `ko.computed` that is automatically scoped correctly so `this` points to the right place. + +### Events +Changing properties also trigger's events on the ViewModels. + +```js +var me = new Person({ + firstName: "Jonathan", + lastName: "Creamer" +}); + +me.on("change:firstName", function(value) { + // value === "foo" +}); + +me.firstName("foo"); +``` \ No newline at end of file diff --git a/examples/localstorage/index.html b/examples/localstorage/index.html index 3cf1ee9..77c3411 100644 --- a/examples/localstorage/index.html +++ b/examples/localstorage/index.html @@ -72,6 +72,8 @@

Local Storage Example

} }); + console.log(new Meal()); + var ListViewModel = ko.ViewModel.extend({ el: '#list',