├── .gitignore ├── .travis.yml ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── RELEASE_NOTES.md ├── bower.json ├── component.json ├── config ├── amd │ └── gulp-options.coffee ├── browserify │ └── tests.coffee ├── builds │ ├── library │ │ ├── knockback-core-stack.webpack.config.coffee │ │ ├── knockback-core.webpack.config.coffee │ │ ├── knockback-full-stack.webpack.config.coffee │ │ └── knockback.webpack.config.coffee │ └── test │ │ ├── knockback-core-lodash.tests.webpack.config.coffee │ │ ├── knockback-core.tests.webpack.config.coffee │ │ ├── knockback-lodash.tests.webpack.config.coffee │ │ └── knockback.tests.webpack.config.coffee ├── files.coffee ├── karma │ ├── config-amd.coffee │ ├── config-base.coffee │ ├── generate.coffee │ └── run.coffee ├── test_groups.coffee └── webpack │ └── base-config.coffee ├── gulpfile.coffee ├── knockback-core-stack.js ├── knockback-core-stack.min.js ├── knockback-core.js ├── knockback-core.min.js ├── knockback-full-stack.js ├── knockback-full-stack.min.js ├── knockback.js ├── knockback.min.js ├── media └── logo.png ├── package-lock.json ├── package.json ├── packages ├── npm │ ├── README.md │ ├── RELEASE_NOTES.md │ ├── bower.json │ ├── component.json │ ├── knockback-core-stack.js │ ├── knockback-core-stack.min.js │ ├── knockback-core.js │ ├── knockback-core.min.js │ ├── knockback-full-stack.js │ ├── knockback-full-stack.min.js │ ├── knockback.js │ ├── knockback.min.js │ └── package.json └── nuget │ ├── Content │ └── Scripts │ │ ├── README.md │ │ ├── RELEASE_NOTES.md │ │ ├── knockback-core-stack.js │ │ ├── knockback-core-stack.min.js │ │ ├── knockback-core.js │ │ ├── knockback-core.min.js │ │ ├── knockback-full-stack.js │ │ ├── knockback-full-stack.min.js │ │ ├── knockback.js │ │ └── knockback.min.js │ └── package.nuspec ├── src ├── core │ ├── collection-observable.coffee │ ├── configure.coffee │ ├── event-watcher.coffee │ ├── factory.coffee │ ├── functions │ │ ├── collapse_options.coffee │ │ ├── extend.coffee │ │ ├── unwrap_models.coffee │ │ └── wrapped_destroy.coffee │ ├── index.coffee │ ├── inject.coffee │ ├── kb.coffee │ ├── monkey-patches.coffee │ ├── observable.coffee │ ├── orms │ │ ├── backbone-associations.coffee │ │ └── backbone-relational.coffee │ ├── statistics.coffee │ ├── store.coffee │ ├── typed-value.coffee │ ├── utils.coffee │ └── view-model.coffee ├── defaults │ ├── default-observable.coffee │ └── extensions.coffee ├── formatting │ └── formatted-observable.coffee ├── localization │ └── localized-observable.coffee ├── triggering │ └── triggered-observable.coffee └── validation │ ├── validation.coffee │ └── validators.coffee ├── test ├── issues │ ├── 2013_04_04 │ │ ├── issue_2013_04_04.coffee │ │ ├── issue_2013_04_04.html │ │ └── issue_2013_04_04.js │ ├── issue141 │ │ ├── issue.html │ │ └── issue.js │ ├── issue145 │ │ ├── issue.html │ │ └── issue.js │ ├── issue157 │ │ ├── issue.html │ │ └── issue.js │ ├── issue161 │ │ ├── issue-0.19.0.html │ │ ├── issue-0.19.0.js │ │ ├── issue.html │ │ └── issue.js │ ├── issue34 │ │ ├── issue34.html │ │ └── issue34.js │ ├── issue35 │ │ ├── issue35.html │ │ └── issue35.js │ ├── issue37 │ │ ├── issue37.html │ │ └── issue37.js │ ├── issue40 │ │ ├── knockback-amd.js │ │ ├── require.js │ │ ├── require_fixed.html │ │ └── require_problem.html │ ├── issue44 │ │ ├── issue44.html │ │ └── issue44.js │ ├── issue46 │ │ ├── issue46.html │ │ └── issue46.js │ ├── issue47 │ │ ├── issue47.html │ │ └── issue47.js │ ├── issue89 │ │ ├── issue89.coffee │ │ ├── issue89.html │ │ └── issue89.js │ └── issue98 │ │ ├── issue.coffee │ │ ├── issue.html │ │ └── issue.js ├── lib │ ├── globalize.culture.en-GB.js │ ├── globalize.culture.fr-FR.js │ └── globalize.js ├── mocha.opts └── spec │ ├── core │ ├── collection-observable.tests.coffee │ ├── core.tests.coffee │ ├── inject.tests.coffee │ ├── memory-management.tests.coffee │ ├── monkey-patches.tests.coffee │ ├── observable.tests.coffee │ ├── utils.tests.coffee │ └── view-model.tests.coffee │ ├── ecosystem │ ├── backbone-associations.tests.coffee │ ├── backbone-modelref.tests.coffee │ ├── backbone-orm.tests.coffee │ ├── backbone-relational.memory-management.tests.coffee │ └── backbone-relational.tests.coffee │ ├── issues │ └── 159.tests.coffee │ ├── parameters.coffee │ └── plugins │ ├── formatting.tests.coffee │ ├── localization_and_defaults.tests.coffee │ ├── triggering.tests.coffee │ └── validation.tests.coffee └── vendor ├── backbone-0.5.1.js ├── knockout-2.1.0.js ├── lodash-0.3.2.js ├── parse-1.2.0.js └── underscore-1.1.7.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | *.map 4 | 5 | node_modules/ 6 | doc/ 7 | _temp/ 8 | 9 | # web site noise 10 | tutorials/* 11 | stylesheets/* 12 | javascripts/* 13 | images/* 14 | 15 | components/* 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "6" 5 | env: 6 | - CXX=g++-4.8 7 | addons: 8 | apt: 9 | sources: 10 | - ubuntu-toolchain-r-test 11 | packages: 12 | - g++-4.8 13 | 14 | notifications: 15 | email: 16 | on_failure: always # [always|never|change] default: always 17 | 18 | before_script: 19 | - "export PATH=node_modules/.bin:node_modules/easy-bake/node_modules/.bin:$PATH" 20 | - "export DISPLAY=:99.0" 21 | - "sh -e /etc/init.d/xvfb start" -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "GulpTester", 6 | "type": "node", 7 | "request": "launch", 8 | "program": "${workspaceRoot}/node_modules/gulp/bin/gulp.js", 9 | "stopOnEntry": false, 10 | "args": [], 11 | "cwd": "${workspaceRoot}", 12 | "runtimeArgs": ["--nolazy"], 13 | "console": "internalConsole" 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2016 Kevin Malakoff 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/kmalakoff/knockback.png)](http://travis-ci.org/kmalakoff/knockback#master) 2 | 3 | ![logo](https://github.com/kmalakoff/knockback/raw/master/media/logo.png) 4 | 5 | Knockback.js provides Knockout.js magic for Backbone.js Models and Collections. 6 | 7 | # Why Knockback? 8 | 9 | * Make amazingly dynamic applications by applying a small number of simple principles 10 | * Leverage the wonderful work from both the Backbone and Knockout communities 11 | * Easily view and edit relationships between Models using an ORM of your choice: 12 | * [BackboneORM](http://vidigami.github.io/backbone-orm/) 13 | * [Backbone-Relational.js](http://backbonerelational.org/) 14 | * Simplify program control flow by configuring your application from your HTML Views. It's like Angular.js but without memorizing all of the special purpose ng-{something} attributes. See the [Inject Tutorial](http://kmalakoff.github.com/knockback/tutorial_inject.html) for live examples! 15 | 16 | # Examples 17 | 18 | ### Simple 19 | 20 | ###### The HTML: 21 | 22 | ```html 23 | 24 | 25 | ``` 26 | 27 | ###### And...engage: 28 | 29 | ```coffeescript 30 | model = new Backbone.Model({first_name: 'Bob', last_name: 'Smith'}) 31 | ko.applyBindings(kb.viewModel(model)) 32 | ``` 33 | 34 | When you type in the input boxes, the values are properly transferred bi-directionally to the model and all other bound view models! 35 | 36 | 37 | ### Advanced 38 | 39 | ###### The View Model: 40 | 41 | **Javascript** 42 | 43 | ```javascript 44 | var ContactViewModel = kb.ViewModel.extend({ 45 | constructor: function(model) { 46 | kb.ViewModel.prototype.constructor.call(this, model); 47 | 48 | this.full_name = ko.computed(function() { 49 | return this.first_name() + " " + this.last_name(); 50 | }, this); 51 | }); 52 | 53 | ``` 54 | 55 | **or Coffeescript** 56 | 57 | ```coffeescript 58 | class ContactViewModel extends kb.ViewModel 59 | constructor: (model) -> 60 | super model 61 | 62 | @full_name = ko.computed => "#{@first_name()} #{@last_name()}" 63 | ``` 64 | 65 | ###### The HTML: 66 | 67 | ```html 68 |

69 | 70 | 71 | ``` 72 | 73 | ###### And...engage: 74 | 75 | ```coffeescript 76 | model = new Backbone.Model({first_name: 'Bob', last_name: 'Smith'}) 77 | view_model = new ContactViewModel(model) 78 | ko.applyBindings(view_model) 79 | 80 | # ... do stuff then clean up 81 | kb.release(view_model) 82 | ``` 83 | 84 | Now, the greeting updates as you type! 85 | 86 | 87 | # Getting Started 88 | 89 | * [Website](http://kmalakoff.github.com/knockback/) - explore everything Knockback and connect to the community 90 | * [Tutorials](http://kmalakoff.github.io/knockback/tutorials_introduction.html) - try some live examples 91 | * [API Docs](http://kmalakoff.github.com/knockback/doc/index.html) - dig into the API 92 | * [TodoMVC App (Live!)](http://kmalakoff.github.com/knockback-todos-app/) - compare client-side framworks 93 | 94 | # Download Latest (1.2.3): 95 | 96 | Please see the [release notes](https://github.com/kmalakoff/knockback/blob/master/RELEASE_NOTES.md) for upgrade pointers. 97 | 98 | * Full Library [(dev, 64k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback.js) or [(min+gzip, 8k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback.min.js) 99 | * Full Stack [(dev, 330k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-full-stack.js) or [(min+gzip, 32k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-full-stack.min.js) 100 | 101 | * Core Library [(dev, 54k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-core.js) or [(min+gzip, 7k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-core.min.js) 102 | * Core Stack [(dev, 315k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-core-stack.js) or [(min+gzip, 31k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-core-stack.min.js) 103 | 104 | The **full versions** bundle advanced features. 105 | 106 | The **core versions** remove advanced features that can be included separately: localization, formatting, triggering, defaults, and validation. 107 | 108 | The **stack versions** provide Underscore.js + Backbone.js + Knockout.js + Knockback.js in a single file. 109 | 110 | ###Distributions 111 | 112 | You can also find Knockback on your favorite distributions: 113 | 114 | * **npm**: npm install knockback 115 | * **Bower**: bower install knockback 116 | * [NuGet](http://nuget.org/packages/Knockback.js) - install right in Visual Studio 117 | 118 | ###Dependencies 119 | 120 | * [Backbone.js](http://backbonejs.org/) - provides the Model layer 121 | * [Knockout.js](http://knockoutjs.com/) - provides the ViewModel layer foundations for Knockback 122 | * [Underscore.js](http://underscorejs.org/) - provides an awesome JavaScript utility belt 123 | * [LoDash](http://lodash.com/) - optionally replaces Underscore.js with a library optimized for consistent performance 124 | * [Parse](https://www.parse.com/) - optionally replaces Backbone.js and Underscore.js 125 | 126 | ###Compatible Components 127 | 128 | * [BackboneORM](http://vidigami.github.io/backbone-orm/) - A polystore ORM for Node.js and the browser 129 | * [Backbone-Relational.js](http://backbonerelational.org/) - Get and set relations (one-to-one, one-to-many, many-to-one) for Backbone models 130 | * [Backbone Associations](http://dhruvaray.github.io/backbone-associations/) - Create object hierarchies with Backbone models. Respond to hierarchy changes using regular Backbone events 131 | * [BackboneModelRef.js](https://github.com/kmalakoff/backbone-modelref/) - provides a reference to a Backbone.Model that can be bound to your view before the model is loaded from the server (along with relevant load state notifications). 132 | 133 | 134 | # Contributing 135 | 136 | To build the library for Node.js and browsers: 137 | 138 | ``` 139 | $ gulp build 140 | ``` 141 | 142 | Please run tests before submitting a pull request: 143 | 144 | ``` 145 | $ gulp test --quick 146 | ``` 147 | 148 | and eventually all tests: 149 | 150 | ``` 151 | $ npm test 152 | ``` 153 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knockback", 3 | "version": "1.2.3", 4 | "main": "knockback.js", 5 | "ignore": [ 6 | "**/.*", 7 | "config", 8 | "components", 9 | "doc", 10 | "media", 11 | "node_modules", 12 | "packages", 13 | "src", 14 | "test", 15 | "vendor", 16 | "bower_components" 17 | ], 18 | "homepage": "https://github.com/kmalakoff/knockback", 19 | "authors": [ 20 | "Kevin Malakoff " 21 | ], 22 | "description": "Knockback.js provides Knockout.js magic for Backbone.js Models and Collections", 23 | "keywords": [ 24 | "knockback", 25 | "knockbackjs", 26 | "backbone", 27 | "backbonejs", 28 | "knockout", 29 | "knockoutjs" 30 | ], 31 | "moduleType": [ 32 | "amd", 33 | "globals", 34 | "node" 35 | ], 36 | "dependencies": { 37 | "underscore": ">=1.1.7", 38 | "backbone": ">=0.5.1", 39 | "knockout": ">=2.2.1" 40 | }, 41 | 42 | "license": "MIT" 43 | } 44 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knockback", 3 | "author": "Kevin Malakoff (https://github.com/kmalakoff)", 4 | "version": "1.2.3", 5 | "description": "Knockback.js provides Knockout.js magic for Backbone.js Models and Collections", 6 | "keywords" : ["knockback", "knockbackjs", "backbone", "backbonejs", "knockout", "knockoutjs"], 7 | "repo": "kmalakoff/knockback", 8 | "dependencies": { 9 | "jashkenas/underscore": "*", 10 | "jashkenas/backbone": "*", 11 | "kmalakoff/knockout": "*" 12 | }, 13 | "main": "knockback.js", 14 | "scripts": [ 15 | "knockback.js" 16 | ], 17 | "license": "MIT" 18 | } -------------------------------------------------------------------------------- /config/amd/gulp-options.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | karma: true, 3 | shims: 4 | underscore: {exports: '_'} 5 | backbone: {exports: 'Backbone', deps: ['underscore']} 6 | knockback: {deps: ['backbone', 'knockout']} 7 | 'globalize.culture.en-GB': {deps: ['globalize']} 8 | 'globalize.culture.fr-FR': {deps: ['globalize']} 9 | post_load: 'this._ = this.Backbone = this.ko = this.kb = null;' # clear window references so amd version is used 10 | aliases: {'knockback-core': 'knockback', 'lodash': 'underscore', 'knockout-latest.debug': 'knockout'} 11 | -------------------------------------------------------------------------------- /config/browserify/tests.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | module.exports = 4 | full: 5 | output: './_temp/browserify/knockback.tests.js' 6 | files: ['./test/spec/core/**/*.tests.coffee', './test/spec/plugins/**/*.tests.coffee'] 7 | options: 8 | # basedir: path.resolve(__dirname, '../..') 9 | shim: 10 | knockback: {path: './knockback.js', exports: 'kb', depends: {underscore: '_', backbone: 'Backbone', knockout: 'ko'}} 11 | 12 | core: 13 | output: './_temp/browserify/knockback-core.tests.js' 14 | files: ['./test/spec/core/**/*.tests.coffee'] 15 | options: 16 | # basedir: path.resolve(__dirname, '../..') 17 | shim: 18 | knockback: {path: './knockback.js', exports: 'kb', depends: {underscore: '_', backbone: 'Backbone', knockout: 'ko'}} 19 | -------------------------------------------------------------------------------- /config/builds/library/knockback-core-stack.webpack.config.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | _ = require 'underscore' 4 | 5 | module.exports = _.extend _.clone(require '../../webpack/base-config.coffee'), { 6 | entry: _.flatten([require('../../files').src_core, './src/core/index.coffee']) 7 | output: 8 | library: 'kb' 9 | libraryTarget: 'umd2' 10 | filename: 'knockback-core-stack.js' 11 | 12 | externals: [ 13 | {'backbone-relational': {optional: true, root: 'Backbone', amd: 'backbone-relational', commonjs: 'backbone-relational', commonjs2: 'backbone-relational'}} 14 | {'backbone-associations': {optional: true, root: 'Backbone', amd: 'backbone-associations', commonjs: 'backbone-associations', commonjs2: 'backbone-associations'}} 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /config/builds/library/knockback-core.webpack.config.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | _ = require 'underscore' 4 | 5 | module.exports = _.extend _.clone(require '../../webpack/base-config.coffee'), { 6 | entry: _.flatten([require('../../files').src_core, './src/core/index.coffee']) 7 | output: 8 | library: 'kb' 9 | libraryTarget: 'umd2' 10 | filename: 'knockback-core.js' 11 | 12 | externals: [ 13 | {underscore: {root: '_', amd: 'underscore', commonjs: 'underscore', commonjs2: 'underscore'}} 14 | {backbone: {root: 'Backbone', amd: 'backbone', commonjs: 'backbone', commonjs2: 'backbone'}} 15 | {knockout: {root: 'ko', amd: 'knockout', commonjs: 'knockout', commonjs2: 'knockout'}} 16 | 17 | {'backbone-relational': {optional: true, root: 'Backbone', amd: 'backbone-relational', commonjs: 'backbone-relational', commonjs2: 'backbone-relational'}} 18 | {'backbone-associations': {optional: true, root: 'Backbone', amd: 'backbone-associations', commonjs: 'backbone-associations', commonjs2: 'backbone-associations'}} 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /config/builds/library/knockback-full-stack.webpack.config.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | _ = require 'underscore' 4 | 5 | module.exports = _.extend _.clone(require '../../webpack/base-config.coffee'), { 6 | entry: _.flatten([require('../../files').src_core, require('../../files').src_plugin, './src/core/index.coffee']) 7 | output: 8 | library: 'kb' 9 | libraryTarget: 'umd2' 10 | filename: 'knockback-full-stack.js' 11 | 12 | externals: [ 13 | {'backbone-relational': {optional: true, root: 'Backbone', amd: 'backbone-relational', commonjs: 'backbone-relational', commonjs2: 'backbone-relational'}} 14 | {'backbone-associations': {optional: true, root: 'Backbone', amd: 'backbone-associations', commonjs: 'backbone-associations', commonjs2: 'backbone-associations'}} 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /config/builds/library/knockback.webpack.config.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | _ = require 'underscore' 4 | 5 | module.exports = _.extend _.clone(require '../../webpack/base-config.coffee'), { 6 | entry: _.flatten([require('../../files').src_core, require('../../files').src_plugin, './src/core/index.coffee']) 7 | output: 8 | library: 'kb' 9 | libraryTarget: 'umd2' 10 | filename: 'knockback.js' 11 | 12 | externals: [ 13 | {underscore: {root: '_', amd: 'underscore', commonjs: 'underscore', commonjs2: 'underscore'}} 14 | {backbone: {root: 'Backbone', amd: 'backbone', commonjs: 'backbone', commonjs2: 'backbone'}} 15 | {knockout: {root: 'ko', amd: 'knockout', commonjs: 'knockout', commonjs2: 'knockout'}} 16 | 17 | {'backbone-relational': {optional: true, root: 'Backbone', amd: 'backbone-relational', commonjs: 'backbone-relational', commonjs2: 'backbone-relational'}} 18 | {'backbone-associations': {optional: true, root: 'Backbone', amd: 'backbone-associations', commonjs: 'backbone-associations', commonjs2: 'backbone-associations'}} 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /config/builds/test/knockback-core-lodash.tests.webpack.config.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | _ = require 'underscore' 3 | 4 | module.exports = _.extend _.clone(require '../../webpack/base-config.coffee'), { 5 | entry: 6 | 'knockback-core-lodash.tests': require('../../files').tests_core 7 | } 8 | 9 | module.exports.resolve.alias = 10 | underscore: require.resolve('lodash') 11 | knockback: path.resolve('./knockback-core.js') 12 | -------------------------------------------------------------------------------- /config/builds/test/knockback-core.tests.webpack.config.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | _ = require 'underscore' 3 | 4 | module.exports = _.extend _.clone(require '../../webpack/base-config.coffee'), { 5 | entry: 6 | 'knockback-core.tests': require('../../files').tests_core 7 | } 8 | 9 | module.exports.resolve.alias = 10 | knockback: path.resolve('./knockback-core.js') 11 | -------------------------------------------------------------------------------- /config/builds/test/knockback-lodash.tests.webpack.config.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | _ = require 'underscore' 3 | 4 | module.exports = _.extend _.clone(require '../../webpack/base-config.coffee'), { 5 | entry: 6 | 'knockback-lodash.tests': _.flatten([require('../../files').tests_core, require('../../files').tests_plugin]) 7 | } 8 | 9 | module.exports.resolve.alias = 10 | underscore: require.resolve('lodash') 11 | knockback: path.resolve('./knockback.js') 12 | -------------------------------------------------------------------------------- /config/builds/test/knockback.tests.webpack.config.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | _ = require 'underscore' 3 | 4 | module.exports = _.extend _.clone(require '../../webpack/base-config.coffee'), { 5 | entry: 6 | 'knockback.tests': _.flatten([require('../../files').tests_core, require('../../files').tests_plugin]) 7 | } 8 | 9 | module.exports.resolve.alias = 10 | knockback: path.resolve('./knockback.js') 11 | -------------------------------------------------------------------------------- /config/files.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | _ = require 'underscore' 4 | readdirSyncRecursive = require 'fs-readdir-recursive' 5 | 6 | PLUGIN_ENTRIES = 7 | defaults: './src/defaults/default-observable.coffee' 8 | formatting: './src/formatting/formatted-observable.coffee' 9 | localization: './src/localization/localized-observable.coffee' 10 | triggering: './src/triggering/triggered-observable.coffee' 11 | validation: './src/validation/validation.coffee' 12 | 13 | module.exports = 14 | libraries: _.flatten(_.map(_.filter(fs.readdirSync('./config/builds/library'), (file) -> path.extname(file) is '.coffee' and file.indexOf('webpack.config.coffee') >= 0), (file) -> [file.replace('webpack.config.coffee', 'js'), file.replace('webpack.config.coffee', 'min.js')])) 15 | 16 | src_core: _.map(_.filter(fs.readdirSync('./src/core'), (file) -> path.extname(file) is '.coffee' and file isnt 'index.coffee'), (file) -> "./src/core/#{file}") 17 | src_plugin: _.values(PLUGIN_ENTRIES) 18 | 19 | tests_core: ("./test/spec/core/#{filename}" for filename in readdirSyncRecursive(__dirname + '/../test/spec/core') when /\.tests.coffee$/.test(filename)) 20 | tests_plugin: ("./test/spec/plugins/#{filename}" for filename in readdirSyncRecursive(__dirname + '/../test/spec/plugins') when /\.tests.coffee$/.test(filename)) 21 | 22 | tests_webpack: _.map(_.filter(fs.readdirSync('./config/builds/test'), (file) -> path.extname(file) is '.coffee' and file.indexOf('.tests.webpack.config.coffee') >= 0), (file) -> "_temp/webpack/#{file.replace('.tests.webpack.config.coffee', '.tests.js')}") 23 | -------------------------------------------------------------------------------- /config/karma/config-amd.coffee: -------------------------------------------------------------------------------- 1 | _ = require 'lodash' 2 | 3 | module.exports = _.clone(require './config-base') 4 | module.exports.frameworks = ['mocha'] 5 | -------------------------------------------------------------------------------- /config/karma/config-base.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | basePath: '.' 3 | frameworks: ['mocha', 'chai'] 4 | preprocessors: {'**/*.coffee': ['coffee']} 5 | 6 | coffeePreprocessor: 7 | options: {bare: true, sourceMap: false} 8 | transformPath: (path) -> path.replace(/\.coffee$/, '.js') 9 | 10 | reporters: ['dots'] 11 | port: 9876 12 | colors: true 13 | logLevel: 'INFO' 14 | 15 | browsers: ['ChromeHeadless'] 16 | singleRun: true 17 | 18 | # browsers: ['ChromeHeadless', 'Chrome, 'Firefox', 'Chrome', 'Safari'] 19 | # singleRun: true 20 | 21 | # browsers: ['Chrome'] 22 | # singleRun: false 23 | -------------------------------------------------------------------------------- /config/karma/generate.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs-extra' 2 | path = require 'path' 3 | _ = require 'underscore' 4 | Queue = require 'queue-async' 5 | es = require 'event-stream' 6 | 7 | gulp = require 'gulp' 8 | gutil = require 'gulp-util' 9 | shell = require 'gulp-shell' 10 | coffee = require 'gulp-coffee' 11 | concat = require 'gulp-concat' 12 | wrapAMD = require 'gulp-wrap-amd-infer' 13 | webpack = require 'gulp-webpack-config' 14 | browserify = require 'gulp-browserify' 15 | 16 | TEST_GROUPS = require('../test_groups') 17 | 18 | module.exports = (options={}, callback) -> 19 | return callback() if options.tags.indexOf('@quick') >= 0 20 | 21 | queue = new Queue(1) 22 | 23 | # install knockback 24 | queue.defer (callback) -> 25 | gulp.src(['./knockback.js', './package.json']) 26 | .pipe(gulp.dest('node_modules/knockback')) 27 | .on('end', callback) 28 | 29 | # build webpack 30 | queue.defer (callback) -> 31 | gulp.src(['config/builds/test/**/*.webpack.config.coffee'], {read: false, buffer: false}) 32 | .pipe(webpack()) 33 | .pipe(gulp.dest('_temp/webpack')) 34 | .on('end', callback) 35 | 36 | # build test browserify 37 | for test in TEST_GROUPS.browserify or [] 38 | do (test) -> queue.defer (callback) -> 39 | gulp.src(test.build.files) 40 | .pipe(coffee({bare: true})) 41 | .pipe(concat(path.basename(test.build.destination))) 42 | .pipe(browserify(test.build.options)) 43 | .pipe(gulp.dest(path.dirname(test.build.destination))) 44 | .on('end', callback) 45 | 46 | # wrap AMD tests 47 | for test in TEST_GROUPS.amd or [] 48 | do (test) -> queue.defer (callback) -> 49 | gulp.src(test.build.files) 50 | .pipe(coffee({bare: true, header: false})) 51 | .pipe(wrapAMD(test.build.options)) 52 | .pipe(gulp.dest(test.build.destination)) 53 | .on('end', callback) 54 | 55 | # uninstall knockback 56 | queue.await (err) -> 57 | fs.removeSync('node_modules/knockback') 58 | callback(err) 59 | -------------------------------------------------------------------------------- /config/karma/run.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs-extra' 2 | path = require 'path' 3 | _ = require 'underscore' 4 | Queue = require 'queue-async' 5 | {Server} = require 'karma' 6 | gutil = require 'gulp-util' 7 | generate = require './generate' 8 | 9 | KARMA_CONFIG_BASE = require './config-base' 10 | KARMA_CONFIG_AMD = require './config-amd' 11 | 12 | module.exports = (options={}, callback) -> 13 | fs.removeSync('./_temp') 14 | fs.removeSync('node_modules/knockback') 15 | queue = new Queue(1) 16 | queue.defer (callback) -> generate(options, callback) 17 | 18 | TEST_GROUPS = require '../test_groups' 19 | TEST_GROUPS = {browser_globals: TEST_GROUPS.browser_globals.slice(0, 1)} if options.tags.indexOf('@quick') >= 0 20 | 21 | # TODO: disabled AMD tests until can troubleshoot knockout not loading in AMD (it stoped loading at 3.5.0) 22 | # TEST_GROUPS = {amd: TEST_GROUPS.amd} 23 | delete TEST_GROUPS.amd 24 | 25 | for name, tests of TEST_GROUPS then do (name, tests) -> 26 | for test in tests then do (test) -> 27 | # return unless test.name is 'amd_backbone_lodash_latest_browser_globals' 28 | 29 | queue.defer (callback) -> 30 | gutil.log "RUNNING TESTS: #{name} #{test.name}" 31 | gutil.log "#{JSON.stringify test.files}" 32 | karma_config = if (name == 'amd') then KARMA_CONFIG_AMD else KARMA_CONFIG_BASE 33 | args = if options.tags then ['--grep', options.tags] else [] 34 | new Server( 35 | _.defaults({files: test.files, client: {args}}, karma_config), 36 | (return_value) -> 37 | console.log("DONE TESTS: #{name} #{test.name}. Return value: #{return_value}") 38 | callback(new Error "Tests failed: #{return_value}" if return_value) 39 | ).start() 40 | 41 | queue.await (err) -> 42 | fs.removeSync('./_temp') unless err 43 | callback(err) 44 | -------------------------------------------------------------------------------- /config/test_groups.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | _ = require 'underscore' 4 | gutil = require 'gulp-util' 5 | 6 | resolveModule = (module_name) -> path.relative('.', require.resolve(module_name)) 7 | 8 | KNOCKBACK = 9 | browser_globals: ['./knockback.js'] 10 | browser_globals_min: ['./knockback.min.js'] 11 | browser_globals_stack: ['./knockback-browser_globals-stack.js'] 12 | core: ['./knockback-core.js'] 13 | core_min: ['./knockback-core.min.js'] 14 | core_stack: ['./knockback-core-stack.js'] 15 | 16 | REQUIRED_DEPENDENCIES = 17 | backbone_underscore_latest: (resolveModule(module_name) for module_name in ['jquery', 'underscore', 'backbone', 'knockout']) 18 | backbone_underscore_legacy: ['./vendor/underscore-1.1.7.js', './vendor/backbone-0.5.1.js', './vendor/knockout-2.1.0.js'] 19 | backbone_lodash_latest: (resolveModule(module_name) for module_name in ['lodash', 'backbone', 'knockout']) 20 | backbone_lodash_legacy: ['./vendor/lodash-0.3.2.js', './vendor/backbone-0.5.1.js', './vendor/knockout-2.1.0.js'] 21 | parse_latest_compatibile: (resolveModule(module_name) for module_name in ['knockout', 'parse']) 22 | parse_legacy: (resolveModule(module_name) for module_name in ['knockout']).concat(['./vendor/parse-1.2.0.js']) 23 | 24 | LOCALIZATION_DEPENCIES = ['./test/lib/globalize.js', './test/lib/globalize.culture.en-GB.js', './test/lib/globalize.culture.fr-FR.js'] 25 | 26 | FILES = require './files' 27 | 28 | module.exports = TEST_GROUPS = {} 29 | 30 | ############################### 31 | # Full Library 32 | ############################### 33 | TEST_GROUPS.browser_globals = [] 34 | for library_name, library_files of KNOCKBACK when (library_name.indexOf('browser_globals') >= 0 and library_name.indexOf('stack') < 0) 35 | for dep_name, dep_files of REQUIRED_DEPENDENCIES 36 | if dep_name.indexOf('backbone') >= 0 # Backbone 37 | TEST_GROUPS.browser_globals.push({name: "#{dep_name}_#{library_name}", files: _.flatten([dep_files, library_files, LOCALIZATION_DEPENCIES, resolveModule('backbone-modelref'), './test/spec/core/**/*.tests.coffee', './test/spec/plugins/**/*.tests.coffee', './test/spec/issues/**/*.tests.coffee'])}) 38 | else # Parse 39 | TEST_GROUPS.browser_globals.push({name: "#{dep_name}_#{library_name}", files: _.flatten([dep_files, library_files, LOCALIZATION_DEPENCIES, './test/spec/core/**/*.tests.coffee', './test/spec/plugins/**/*.tests.coffee'])}) 40 | 41 | ############################### 42 | # Core Library 43 | ############################### 44 | TEST_GROUPS.core = [] 45 | for test_name, library_files of KNOCKBACK when (test_name.indexOf('core') >= 0 and test_name.indexOf('stack') < 0) 46 | TEST_GROUPS.core.push({name: "core_#{test_name}", files: _.flatten([REQUIRED_DEPENDENCIES.backbone_underscore_latest, library_files, './test/spec/core/**/*.tests.coffee'])}) 47 | 48 | ############################### 49 | # ORM 50 | ############################### 51 | ORM_TESTS = 52 | backbone_orm: [KNOCKBACK.browser_globals, resolveModule('backbone-orm'), './test/spec/ecosystem/**/backbone-orm*.tests.coffee'] 53 | backbone_relational: [KNOCKBACK.browser_globals, resolveModule('backbone-relational'), './test/spec/ecosystem/**/backbone-relational*.tests.coffee'] 54 | backbone_associations: [KNOCKBACK.browser_globals, resolveModule('backbone-associations'), './test/spec/ecosystem/**/backbone-associations*.tests.coffee'] 55 | 56 | TEST_GROUPS.orm = [] 57 | for dep_name, dep_files of _.pick(REQUIRED_DEPENDENCIES, 'backbone_underscore_latest') 58 | TEST_GROUPS.orm.push({name: "#{dep_name}_#{test_name}", files: _.flatten([dep_files, test_files])}) for test_name, test_files of ORM_TESTS 59 | 60 | ############################### 61 | # AMD 62 | ############################### 63 | AMD_OPTIONS = require './amd/gulp-options' 64 | TEST_GROUPS.amd = [] 65 | for test in TEST_GROUPS.browser_globals.concat(TEST_GROUPS.core) when (test.name.indexOf('_min') < 0 and test.name.indexOf('legacy_') < 0 and test.name.indexOf('parse_') < 0) 66 | test_files = test.files.concat(['./node_modules/chai/chai.js', './node_modules/jquery/dist/jquery.js']); files = []; test_patterns = []; path_files = [] 67 | files.push({pattern: './node_modules/requirejs/require.js'}) 68 | for file in test_files 69 | (test_patterns.push(file); continue) if file.indexOf('.tests.') >= 0 70 | files.push({pattern: file, included: false}) 71 | path_files.push(file) 72 | files.push("_temp/amd/#{test.name}/**/*.js") 73 | TEST_GROUPS.amd.push({name: "amd_#{test.name}", files: files, build: {files: test_patterns, destination: "_temp/amd/#{test.name}", options: _.extend({path_files: path_files}, AMD_OPTIONS)}}) 74 | 75 | ############################### 76 | # Webpack 77 | ############################### 78 | TEST_GROUPS.webpack = [] 79 | for file in FILES.tests_webpack 80 | TEST_GROUPS.webpack.push({name: "webpack_#{file.replace('.js', '')}", files: _.flatten([(if file.indexOf('core') >= 0 then [] else LOCALIZATION_DEPENCIES), file])}) 81 | 82 | ############################### 83 | # Browserify 84 | ############################### 85 | TEST_GROUPS.browserify = [] 86 | for test_name, test_info of require('./browserify/tests') 87 | TEST_GROUPS.browserify.push({name: "browserify_#{test_name}", files: _.flatten([(if file.indexOf('core') >= 0 then [] else LOCALIZATION_DEPENCIES), test_info.output]), build: {destination: test_info.output, options: test_info.options, files: test_info.files}}) 88 | -------------------------------------------------------------------------------- /config/webpack/base-config.coffee: -------------------------------------------------------------------------------- 1 | module.exports = 2 | module: 3 | rules: [{test: /\.coffee$/, loader: 'coffee-loader'}] 4 | 5 | resolve: 6 | extensions: ['.coffee', '.js'] 7 | modules: ['node_modules'] 8 | -------------------------------------------------------------------------------- /gulpfile.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | _ = require 'underscore' 3 | Queue = require 'queue-async' 4 | Async = require 'async' 5 | es = require 'event-stream' 6 | 7 | gulp = require 'gulp' 8 | gutil = require 'gulp-util' 9 | webpack = require 'gulp-webpack-config' 10 | rename = require 'gulp-rename' 11 | uglify = require 'gulp-uglify' 12 | header = require 'gulp-header' 13 | mocha = require 'gulp-mocha' 14 | nuget = require 'nuget' 15 | nugetGulp = -> es.map (file, callback) -> 16 | nuget.pack file, (err, nupkg_file) -> 17 | return callback(err) if err 18 | nuget.push nupkg_file, (err) -> if err then gutil.log(err) else callback() 19 | 20 | HEADER = module.exports = """ 21 | /* 22 | <%= file.path.split('/').splice(-1)[0].replace('.min', '') %> <%= pkg.version %> 23 | Copyright (c) 2011-#{(new Date()).getFullYear()} Kevin Malakoff. 24 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 25 | Source: https://github.com/kmalakoff/knockback 26 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 27 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 28 | */\n 29 | """ 30 | LIBRARY_FILES = require('./config/files').libraries 31 | 32 | gulp.task 'build', buildLibraries = (callback) -> 33 | errors = [] 34 | gulp.src('config/builds/library/**/*.webpack.config.coffee') 35 | .pipe(webpack()) 36 | .pipe(header(HEADER, {pkg: require('./package.json')})) 37 | .pipe(gulp.dest('.')) 38 | .on('end', callback) 39 | return # promises workaround: https://github.com/gulpjs/gulp/issues/455 40 | 41 | gulp.task 'watch', ['build'], -> 42 | gulp.watch './src/**/*.coffee', -> buildLibraries(->) 43 | return # promises workaround: https://github.com/gulpjs/gulp/issues/455 44 | 45 | gulp.task 'minify', ['build'], (callback) -> 46 | gulp.src(['*.js', '!*.min.js', '!_temp/**/*.js', '!node_modules/']) 47 | .pipe(uglify()) 48 | .pipe(rename({suffix: '.min'})) 49 | .pipe(header(HEADER, {pkg: require('./package.json')})) 50 | .pipe(gulp.dest((file) -> file.base)) 51 | .on('end', callback) 52 | return # promises workaround: https://github.com/gulpjs/gulp/issues/455 53 | 54 | testNode = (callback) -> 55 | tags = ("@#{tag.replace(/^[-]+/, '')}" for tag in process.argv.slice(3)).join(' ') 56 | 57 | mochaOptions = {reporter: 'dot', compilers: 'coffee:coffee-script/register'} 58 | mochaOptions.grep = tags if tags 59 | gutil.log "Running Node.js tests #{tags}" 60 | gulp.src('test/spec/**/*.tests.coffee') 61 | .pipe(mocha(mochaOptions)) 62 | .pipe es.writeArray callback 63 | return # promises workaround: https://github.com/gulpjs/gulp/issues/455 64 | 65 | testBrowsers = (callback) -> 66 | tags = ("@#{tag.replace(/^[-]+/, '')}" for tag in process.argv.slice(3)).join(' ') 67 | 68 | gutil.log "Running Browser tests #{tags}" 69 | (require './config/karma/run')({tags}, callback) 70 | return # promises workaround: https://github.com/gulpjs/gulp/issues/455 71 | 72 | gulp.task 'test-node', ['build'], testNode 73 | 74 | # gulp.task 'test-browsers', testBrowsers 75 | gulp.task 'test-browsers', ['minify'], testBrowsers 76 | 77 | gulp.task 'test', ['minify'], (callback) -> 78 | Async.series [testNode, testBrowsers], (err) -> not err || console.log(err); process.exit(if err then 1 else 0) 79 | return # promises workaround: https://github.com/gulpjs/gulp/issues/455 80 | 81 | gulp.task 'publish', ['minify'], (callback) -> 82 | copyLibraryFiles = (destination, others, callback) -> 83 | gulp.src(LIBRARY_FILES.concat(['README.md', 'RELEASE_NOTES.md'].concat(others))) 84 | .pipe(gulp.dest((file) -> path.join(destination, path.dirname(file.path).replace(__dirname, '')))) 85 | .on('end', callback) 86 | 87 | queue = new Queue(1) 88 | queue.defer (callback) -> Async.series [testNode, testBrowsers], callback 89 | queue.defer (callback) -> copyLibraryFiles('packages/npm', ['component.json', 'bower.json'], callback) 90 | queue.defer (callback) -> copyLibraryFiles('packages/nuget/Content/Scripts', [], callback) 91 | queue.defer (callback) -> 92 | gulp.src('packages/nuget/*.nuspec') 93 | .pipe(nugetGulp()) 94 | .on('end', callback) 95 | queue.await (err) -> not err || console.log(err); process.exit(if err then 1 else 0) 96 | return # promises workaround: https://github.com/gulpjs/gulp/issues/455 97 | 98 | gulp.task 'default', ['test-node'] 99 | -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kmalakoff/knockback/8ac1f5f8d013f1eede74f1abc38ecffc8e4bd9f8/media/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knockback", 3 | "version": "1.2.3", 4 | "description": "Knockback.js provides Knockout.js magic for Backbone.js Models and Collections", 5 | "main": "knockback.js", 6 | "author": { 7 | "name": "Kevin Malakoff", 8 | "url": "https://github.com/kmalakoff" 9 | }, 10 | "license": "MIT", 11 | "keywords": [ 12 | "knockback", 13 | "knockbackjs", 14 | "backbone", 15 | "backbonejs", 16 | "knockout", 17 | "knockoutjs" 18 | ], 19 | "private": true, 20 | "url": "http://kmalakoff.github.com/knockback/", 21 | "homepage": "http://kmalakoff.github.com/knockback/", 22 | "repository": { 23 | "type": "git", 24 | "url": "http://github.com/kmalakoff/knockback.git" 25 | }, 26 | "scripts": { 27 | "test": "gulp test", 28 | "docs": "./node_modules/.bin/codo -n Knockback.js -o doc src/." 29 | }, 30 | "dependencies": { 31 | "backbone": ">=0.5.1", 32 | "knockout": ">=2.1.0", 33 | "underscore": ">=1.1.7" 34 | }, 35 | "devDependencies": { 36 | "async": "*", 37 | "chai": "*", 38 | "codo": "2.x", 39 | "coffee-loader": "0.6.x", 40 | "coffee-script": "^1.6.3", 41 | "event-stream": "*", 42 | "fs-extra": "*", 43 | "fs-readdir-recursive": "*", 44 | "gulp": "^3.9.1", 45 | "gulp-browserify": "*", 46 | "gulp-coffee": "*", 47 | "gulp-concat": "*", 48 | "gulp-header": "*", 49 | "gulp-mocha": "3.x", 50 | "gulp-rename": "*", 51 | "gulp-shell": "*", 52 | "gulp-uglify": "*", 53 | "gulp-util": "*", 54 | "gulp-webpack-config": "*", 55 | "gulp-wrap-amd-infer": "*", 56 | "gulp-zip": "*", 57 | "imports-loader": "*", 58 | "jsdom": "*", 59 | "karma": "*", 60 | "karma-chai": "*", 61 | "karma-chrome-launcher": "^2.2.0", 62 | "karma-cli": "*", 63 | "karma-coffee-preprocessor": "*", 64 | "karma-firefox-launcher": "*", 65 | "karma-mocha": "*", 66 | "karma-safari-launcher": "*", 67 | "karma-spec-reporter": "*", 68 | "minimatch": "^3.0.4", 69 | "mocha": "*", 70 | "nuget": "*", 71 | "puppeteer": "^1.15.0", 72 | "queue-async": "*", 73 | "requirejs": "*", 74 | "webpack": "*" 75 | }, 76 | "optionalDependencies": { 77 | "backbone-relational": "*", 78 | "backbone-modelref": "*", 79 | "backbone.localstorage": "*", 80 | "lodash": "*", 81 | "backbone-orm": "*", 82 | "jquery": "*", 83 | "moment": "*", 84 | "backbone-associations": "*", 85 | "parse": "1.5.x" 86 | }, 87 | "jam": { 88 | "dependencies": { 89 | "underscore": ">=1.1.7", 90 | "backbone": ">=0.5.1", 91 | "knockout": ">=2.2.1" 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /packages/npm/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/kmalakoff/knockback.png)](http://travis-ci.org/kmalakoff/knockback#master) 2 | 3 | ![logo](https://github.com/kmalakoff/knockback/raw/master/media/logo.png) 4 | 5 | Knockback.js provides Knockout.js magic for Backbone.js Models and Collections. 6 | 7 | # Why Knockback? 8 | 9 | * Make amazingly dynamic applications by applying a small number of simple principles 10 | * Leverage the wonderful work from both the Backbone and Knockout communities 11 | * Easily view and edit relationships between Models using an ORM of your choice: 12 | * [BackboneORM](http://vidigami.github.io/backbone-orm/) 13 | * [Backbone-Relational.js](http://backbonerelational.org/) 14 | * Simplify program control flow by configuring your application from your HTML Views. It's like Angular.js but without memorizing all of the special purpose ng-{something} attributes. See the [Inject Tutorial](http://kmalakoff.github.com/knockback/tutorial_inject.html) for live examples! 15 | 16 | # Examples 17 | 18 | ### Simple 19 | 20 | ###### The HTML: 21 | 22 | ```html 23 | 24 | 25 | ``` 26 | 27 | ###### And...engage: 28 | 29 | ```coffeescript 30 | model = new Backbone.Model({first_name: 'Bob', last_name: 'Smith'}) 31 | ko.applyBindings(kb.viewModel(model)) 32 | ``` 33 | 34 | When you type in the input boxes, the values are properly transferred bi-directionally to the model and all other bound view models! 35 | 36 | 37 | ### Advanced 38 | 39 | ###### The View Model: 40 | 41 | **Javascript** 42 | 43 | ```javascript 44 | var ContactViewModel = kb.ViewModel.extend({ 45 | constructor: function(model) { 46 | kb.ViewModel.prototype.constructor.call(this, model); 47 | 48 | this.full_name = ko.computed(function() { 49 | return this.first_name() + " " + this.last_name(); 50 | }, this); 51 | }); 52 | 53 | ``` 54 | 55 | **or Coffeescript** 56 | 57 | ```coffeescript 58 | class ContactViewModel extends kb.ViewModel 59 | constructor: (model) -> 60 | super model 61 | 62 | @full_name = ko.computed => "#{@first_name()} #{@last_name()}" 63 | ``` 64 | 65 | ###### The HTML: 66 | 67 | ```html 68 |

69 | 70 | 71 | ``` 72 | 73 | ###### And...engage: 74 | 75 | ```coffeescript 76 | model = new Backbone.Model({first_name: 'Bob', last_name: 'Smith'}) 77 | view_model = new ContactViewModel(model) 78 | ko.applyBindings(view_model) 79 | 80 | # ... do stuff then clean up 81 | kb.release(view_model) 82 | ``` 83 | 84 | Now, the greeting updates as you type! 85 | 86 | 87 | # Getting Started 88 | 89 | * [Website](http://kmalakoff.github.com/knockback/) - explore everything Knockback and connect to the community 90 | * [Tutorials](http://kmalakoff.github.io/knockback/tutorials_introduction.html) - try some live examples 91 | * [API Docs](http://kmalakoff.github.com/knockback/doc/index.html) - dig into the API 92 | * [TodoMVC App (Live!)](http://kmalakoff.github.com/knockback-todos-app/) - compare client-side framworks 93 | 94 | # Download Latest (1.2.3): 95 | 96 | Please see the [release notes](https://github.com/kmalakoff/knockback/blob/master/RELEASE_NOTES.md) for upgrade pointers. 97 | 98 | * Full Library [(dev, 64k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback.js) or [(min+gzip, 8k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback.min.js) 99 | * Full Stack [(dev, 330k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-full-stack.js) or [(min+gzip, 32k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-full-stack.min.js) 100 | 101 | * Core Library [(dev, 54k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-core.js) or [(min+gzip, 7k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-core.min.js) 102 | * Core Stack [(dev, 315k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-core-stack.js) or [(min+gzip, 31k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-core-stack.min.js) 103 | 104 | The **full versions** bundle advanced features. 105 | 106 | The **core versions** remove advanced features that can be included separately: localization, formatting, triggering, defaults, and validation. 107 | 108 | The **stack versions** provide Underscore.js + Backbone.js + Knockout.js + Knockback.js in a single file. 109 | 110 | ###Distributions 111 | 112 | You can also find Knockback on your favorite distributions: 113 | 114 | * **npm**: npm install knockback 115 | * **Bower**: bower install knockback 116 | * [NuGet](http://nuget.org/packages/Knockback.js) - install right in Visual Studio 117 | 118 | ###Dependencies 119 | 120 | * [Backbone.js](http://backbonejs.org/) - provides the Model layer 121 | * [Knockout.js](http://knockoutjs.com/) - provides the ViewModel layer foundations for Knockback 122 | * [Underscore.js](http://underscorejs.org/) - provides an awesome JavaScript utility belt 123 | * [LoDash](http://lodash.com/) - optionally replaces Underscore.js with a library optimized for consistent performance 124 | * [Parse](https://www.parse.com/) - optionally replaces Backbone.js and Underscore.js 125 | 126 | ###Compatible Components 127 | 128 | * [BackboneORM](http://vidigami.github.io/backbone-orm/) - A polystore ORM for Node.js and the browser 129 | * [Backbone-Relational.js](http://backbonerelational.org/) - Get and set relations (one-to-one, one-to-many, many-to-one) for Backbone models 130 | * [Backbone Associations](http://dhruvaray.github.io/backbone-associations/) - Create object hierarchies with Backbone models. Respond to hierarchy changes using regular Backbone events 131 | * [BackboneModelRef.js](https://github.com/kmalakoff/backbone-modelref/) - provides a reference to a Backbone.Model that can be bound to your view before the model is loaded from the server (along with relevant load state notifications). 132 | 133 | 134 | # Contributing 135 | 136 | To build the library for Node.js and browsers: 137 | 138 | ``` 139 | $ gulp build 140 | ``` 141 | 142 | Please run tests before submitting a pull request: 143 | 144 | ``` 145 | $ gulp test --quick 146 | ``` 147 | 148 | and eventually all tests: 149 | 150 | ``` 151 | $ npm test 152 | ``` 153 | -------------------------------------------------------------------------------- /packages/npm/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knockback", 3 | "version": "1.2.3", 4 | "main": "knockback.js", 5 | "ignore": [ 6 | "**/.*", 7 | "config", 8 | "components", 9 | "doc", 10 | "media", 11 | "node_modules", 12 | "packages", 13 | "src", 14 | "test", 15 | "vendor", 16 | "bower_components" 17 | ], 18 | "homepage": "https://github.com/kmalakoff/knockback", 19 | "authors": [ 20 | "Kevin Malakoff " 21 | ], 22 | "description": "Knockback.js provides Knockout.js magic for Backbone.js Models and Collections", 23 | "keywords": [ 24 | "knockback", 25 | "knockbackjs", 26 | "backbone", 27 | "backbonejs", 28 | "knockout", 29 | "knockoutjs" 30 | ], 31 | "moduleType": [ 32 | "amd", 33 | "globals", 34 | "node" 35 | ], 36 | "dependencies": { 37 | "underscore": ">=1.1.7", 38 | "backbone": ">=0.5.1", 39 | "knockout": ">=2.2.1" 40 | }, 41 | 42 | "license": "MIT" 43 | } 44 | -------------------------------------------------------------------------------- /packages/npm/component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knockback", 3 | "author": "Kevin Malakoff (https://github.com/kmalakoff)", 4 | "version": "1.2.3", 5 | "description": "Knockback.js provides Knockout.js magic for Backbone.js Models and Collections", 6 | "keywords" : ["knockback", "knockbackjs", "backbone", "backbonejs", "knockout", "knockoutjs"], 7 | "repo": "kmalakoff/knockback", 8 | "dependencies": { 9 | "jashkenas/underscore": "*", 10 | "jashkenas/backbone": "*", 11 | "kmalakoff/knockout": "*" 12 | }, 13 | "main": "knockback.js", 14 | "scripts": [ 15 | "knockback.js" 16 | ], 17 | "license": "MIT" 18 | } -------------------------------------------------------------------------------- /packages/npm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knockback", 3 | "version": "1.2.3", 4 | "description": "Knockback.js provides Knockout.js magic for Backbone.js Models and Collections", 5 | "main": "knockback.js", 6 | "author": { 7 | "name": "Kevin Malakoff", 8 | "url": "https://github.com/kmalakoff" 9 | }, 10 | "license": "MIT", 11 | "keywords": [ 12 | "knockback", 13 | "knockbackjs", 14 | "backbone", 15 | "backbonejs", 16 | "knockout", 17 | "knockoutjs" 18 | ], 19 | "url": "http://kmalakoff.github.com/knockback/", 20 | "homepage": "http://kmalakoff.github.com/knockback/", 21 | "contributors": [], 22 | "repository": { 23 | "type": "git", 24 | "url": "http://github.com/kmalakoff/knockback.git" 25 | }, 26 | "bugs": "https://github.com/kmalakoff/knockback/issues", 27 | "dependencies": {}, 28 | "optionalDependencies": { 29 | "underscore": ">=1.1.7", 30 | "backbone": ">=0.5.1", 31 | "knockout": ">=2.1.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/nuget/Content/Scripts/README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/kmalakoff/knockback.png)](http://travis-ci.org/kmalakoff/knockback#master) 2 | 3 | ![logo](https://github.com/kmalakoff/knockback/raw/master/media/logo.png) 4 | 5 | Knockback.js provides Knockout.js magic for Backbone.js Models and Collections. 6 | 7 | # Why Knockback? 8 | 9 | * Make amazingly dynamic applications by applying a small number of simple principles 10 | * Leverage the wonderful work from both the Backbone and Knockout communities 11 | * Easily view and edit relationships between Models using an ORM of your choice: 12 | * [BackboneORM](http://vidigami.github.io/backbone-orm/) 13 | * [Backbone-Relational.js](http://backbonerelational.org/) 14 | * Simplify program control flow by configuring your application from your HTML Views. It's like Angular.js but without memorizing all of the special purpose ng-{something} attributes. See the [Inject Tutorial](http://kmalakoff.github.com/knockback/tutorial_inject.html) for live examples! 15 | 16 | # Examples 17 | 18 | ### Simple 19 | 20 | ###### The HTML: 21 | 22 | ```html 23 | 24 | 25 | ``` 26 | 27 | ###### And...engage: 28 | 29 | ```coffeescript 30 | model = new Backbone.Model({first_name: 'Bob', last_name: 'Smith'}) 31 | ko.applyBindings(kb.viewModel(model)) 32 | ``` 33 | 34 | When you type in the input boxes, the values are properly transferred bi-directionally to the model and all other bound view models! 35 | 36 | 37 | ### Advanced 38 | 39 | ###### The View Model: 40 | 41 | **Javascript** 42 | 43 | ```javascript 44 | var ContactViewModel = kb.ViewModel.extend({ 45 | constructor: function(model) { 46 | kb.ViewModel.prototype.constructor.call(this, model); 47 | 48 | this.full_name = ko.computed(function() { 49 | return this.first_name() + " " + this.last_name(); 50 | }, this); 51 | }); 52 | 53 | ``` 54 | 55 | **or Coffeescript** 56 | 57 | ```coffeescript 58 | class ContactViewModel extends kb.ViewModel 59 | constructor: (model) -> 60 | super model 61 | 62 | @full_name = ko.computed => "#{@first_name()} #{@last_name()}" 63 | ``` 64 | 65 | ###### The HTML: 66 | 67 | ```html 68 |

69 | 70 | 71 | ``` 72 | 73 | ###### And...engage: 74 | 75 | ```coffeescript 76 | model = new Backbone.Model({first_name: 'Bob', last_name: 'Smith'}) 77 | view_model = new ContactViewModel(model) 78 | ko.applyBindings(view_model) 79 | 80 | # ... do stuff then clean up 81 | kb.release(view_model) 82 | ``` 83 | 84 | Now, the greeting updates as you type! 85 | 86 | 87 | # Getting Started 88 | 89 | * [Website](http://kmalakoff.github.com/knockback/) - explore everything Knockback and connect to the community 90 | * [Tutorials](http://kmalakoff.github.io/knockback/tutorials_introduction.html) - try some live examples 91 | * [API Docs](http://kmalakoff.github.com/knockback/doc/index.html) - dig into the API 92 | * [TodoMVC App (Live!)](http://kmalakoff.github.com/knockback-todos-app/) - compare client-side framworks 93 | 94 | # Download Latest (1.2.3): 95 | 96 | Please see the [release notes](https://github.com/kmalakoff/knockback/blob/master/RELEASE_NOTES.md) for upgrade pointers. 97 | 98 | * Full Library [(dev, 64k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback.js) or [(min+gzip, 8k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback.min.js) 99 | * Full Stack [(dev, 330k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-full-stack.js) or [(min+gzip, 32k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-full-stack.min.js) 100 | 101 | * Core Library [(dev, 54k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-core.js) or [(min+gzip, 7k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-core.min.js) 102 | * Core Stack [(dev, 315k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-core-stack.js) or [(min+gzip, 31k)](https://raw.github.com/kmalakoff/knockback/1.2.3/knockback-core-stack.min.js) 103 | 104 | The **full versions** bundle advanced features. 105 | 106 | The **core versions** remove advanced features that can be included separately: localization, formatting, triggering, defaults, and validation. 107 | 108 | The **stack versions** provide Underscore.js + Backbone.js + Knockout.js + Knockback.js in a single file. 109 | 110 | ###Distributions 111 | 112 | You can also find Knockback on your favorite distributions: 113 | 114 | * **npm**: npm install knockback 115 | * **Bower**: bower install knockback 116 | * [NuGet](http://nuget.org/packages/Knockback.js) - install right in Visual Studio 117 | 118 | ###Dependencies 119 | 120 | * [Backbone.js](http://backbonejs.org/) - provides the Model layer 121 | * [Knockout.js](http://knockoutjs.com/) - provides the ViewModel layer foundations for Knockback 122 | * [Underscore.js](http://underscorejs.org/) - provides an awesome JavaScript utility belt 123 | * [LoDash](http://lodash.com/) - optionally replaces Underscore.js with a library optimized for consistent performance 124 | * [Parse](https://www.parse.com/) - optionally replaces Backbone.js and Underscore.js 125 | 126 | ###Compatible Components 127 | 128 | * [BackboneORM](http://vidigami.github.io/backbone-orm/) - A polystore ORM for Node.js and the browser 129 | * [Backbone-Relational.js](http://backbonerelational.org/) - Get and set relations (one-to-one, one-to-many, many-to-one) for Backbone models 130 | * [Backbone Associations](http://dhruvaray.github.io/backbone-associations/) - Create object hierarchies with Backbone models. Respond to hierarchy changes using regular Backbone events 131 | * [BackboneModelRef.js](https://github.com/kmalakoff/backbone-modelref/) - provides a reference to a Backbone.Model that can be bound to your view before the model is loaded from the server (along with relevant load state notifications). 132 | 133 | 134 | # Contributing 135 | 136 | To build the library for Node.js and browsers: 137 | 138 | ``` 139 | $ gulp build 140 | ``` 141 | 142 | Please run tests before submitting a pull request: 143 | 144 | ``` 145 | $ gulp test --quick 146 | ``` 147 | 148 | and eventually all tests: 149 | 150 | ``` 151 | $ npm test 152 | ``` 153 | -------------------------------------------------------------------------------- /packages/nuget/package.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 1.2.3 5 | Kevin Malakoff 6 | 7 | https://github.com/kmalakoff/knockback/blob/master/LICENSE 8 | http://kmalakoff.github.com/knockback/ 9 | 10 | 11 | 12 | 13 | 14 | Knockback.js 15 | Knockback.js 16 | false 17 | Knockback.js provides Knockout.js magic for Backbone.js Models and Collections. 18 | Knockback.js provides Knockout.js magic for Backbone.js Models and Collections. 19 | Copyright (c) 2011-2016 Kevin Malakoff 20 | Kockout KnockoutJS Knockout.js Knockback Backbone BackboneJS Bakcbone.js Javascript 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/core/configure.coffee: -------------------------------------------------------------------------------- 1 | {_, ko} = kb = require './kb' 2 | 3 | ALL_ORMS = 4 | 'default': null 5 | 'backbone-orm': null 6 | 'backbone-associations': require './orms/backbone-associations' 7 | 'backbone-relational': require './orms/backbone-relational' 8 | 9 | # @nodoc 10 | kb.settings = {orm: ALL_ORMS.default} 11 | (kb.settings.orm = value; break) for key, value of ALL_ORMS when value and value.isAvailable() 12 | 13 | # @nodoc 14 | module.exports = (options={}) -> 15 | for key, value of options 16 | switch key 17 | when 'orm' 18 | # set by name 19 | if _.isString(value) 20 | (console.log "Knockback configure: could not find orm: #{value}. Available: #{_.keys(ALL_ORMS).join(', ')}"; continue) unless ALL_ORMS.hasOwnProperty(value) 21 | (console.log "Knockback configure: could not enable orm #{value}. Make sure it is included before Knockback"; continue) if (orm = ALL_ORMS[value]) and not orm.isAvailable() 22 | kb.settings.orm = orm 23 | continue 24 | 25 | # set by functions 26 | else 27 | kb.settings.orm = value 28 | 29 | else 30 | kb.settings[key] = value 31 | return 32 | -------------------------------------------------------------------------------- /src/core/event-watcher.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_, ko} = kb = require './kb' 11 | 12 | # Used to provide a central place to aggregate registered Model events rather than having all kb.Observables register for updates independently. 13 | # 14 | class kb.EventWatcher 15 | 16 | # Used to either register yourself with the existing emitter watcher or to create a new one. 17 | # 18 | # @param [Object] options please pass the options from your constructor to the register method. For example, constructor(emitter, options) 19 | # @param [Model|ModelRef] obj the Model that will own or register with the store 20 | # @param [ko.observable|Object] emitter the emitters of the event watcher 21 | # @param [Object] callback_options information about the event and callback to register 22 | # @option options [Function] emitter callback for when the emitter changes (eg. is loaded). Signature: function(new_emitter) 23 | # @option options [Function] update callback for when the registered event is triggered. Signature: function(new_value) 24 | # @option options [String] event_selector the name or names of events. 25 | # @option options [String] key the optional key to filter update attribute events. 26 | @useOptionsOrCreate: (options, emitter, obj, callback_options) -> 27 | if options.event_watcher 28 | kb._throwUnexpected(@, 'emitter not matching') unless (options.event_watcher.emitter() is emitter or (options.event_watcher.model_ref is emitter)) 29 | return kb.utils.wrappedEventWatcher(obj, options.event_watcher).registerCallbacks(obj, callback_options) 30 | else 31 | kb.utils.wrappedEventWatcherIsOwned(obj, true) 32 | return kb.utils.wrappedEventWatcher(obj, new kb.EventWatcher(emitter)).registerCallbacks(obj, callback_options) 33 | 34 | constructor: (emitter, obj, callback_options) -> 35 | @__kb or= {} 36 | @__kb.callbacks = {} 37 | 38 | @ee = null 39 | @registerCallbacks(obj, callback_options) if callback_options 40 | @emitter(emitter) if emitter 41 | 42 | # Required clean up function to break cycles, release view emitters, etc. 43 | # Can be called directly, via kb.release(object) or as a consequence of ko.releaseNode(element). 44 | destroy: -> 45 | @emitter(null); @__kb.callbacks = null 46 | kb.utils.wrappedDestroy(@) 47 | 48 | # Dual-purpose getter/setter for the observed emitter. 49 | # 50 | # @overload emitter() 51 | # Gets the emitter or emitter reference 52 | # @return [Model|ModelRef] the emitter whose attributes are being observed (can be null) 53 | # @overload emitter(new_emitter) 54 | # Sets the emitter or emitter reference 55 | # @param [Model|ModelRef] new_emitter the emitter whose attributes will be observed (can be null) 56 | emitter: (new_emitter) -> 57 | # get or no change 58 | return @ee if (arguments.length is 0) or (@ee is new_emitter) 59 | 60 | # clear and unbind previous 61 | if @model_ref 62 | @model_ref.unbind('loaded', @_onModelLoaded) 63 | @model_ref.unbind('unloaded', @_onModelUnloaded) 64 | @model_ref.release(); @model_ref = null 65 | 66 | # set up current 67 | if kb.Backbone and kb.Backbone.ModelRef and (new_emitter instanceof kb.Backbone.ModelRef) 68 | @model_ref = new_emitter; @model_ref.retain() 69 | @model_ref.bind('loaded', @_onModelLoaded) 70 | @model_ref.bind('unloaded', @_onModelUnloaded) 71 | new_emitter = @model_ref.model() or null 72 | else 73 | delete @model_ref 74 | 75 | # switch bindings 76 | if @ee isnt new_emitter 77 | if new_emitter then @_onModelLoaded(new_emitter) else @_onModelUnloaded(@ee) 78 | return new_emitter 79 | 80 | # Used to register callbacks for an emitter. 81 | # 82 | # @param [Object] obj the owning object. 83 | # @param [Object] callback_info the callback information 84 | # @option options [Function] emitter callback for when the emitter changes (eg. is loaded). Signature: function(new_emitter) 85 | # @option options [Function] update callback for when the registered emitter is triggered. Signature: function(new_value) 86 | # @option options [String] emitter_name the name of the emitter. 87 | # @option options [String] key the optional key to filter update attribute events. 88 | registerCallbacks: (obj, callback_info) -> 89 | obj or kb._throwMissing(this, 'obj') 90 | callback_info or kb._throwMissing(this, 'callback_info') 91 | event_names = if callback_info.event_selector then callback_info.event_selector.split(' ') else ['change'] 92 | model = @ee 93 | 94 | for event_name in event_names 95 | continue unless event_name # extra spaces 96 | do (event_name) => 97 | unless callbacks = @__kb.callbacks[event_name] 98 | callbacks = @__kb.callbacks[event_name] = { 99 | model: null 100 | list: [] 101 | fn: (model) => 102 | for info in callbacks.list 103 | continue unless info.update 104 | continue if model and info.key and (model.hasChanged and not model.hasChanged(ko.utils.unwrapObservable(info.key))) # key doesn't match 105 | not kb.statistics or kb.statistics.addModelEvent({name: event_name, model: model, key: info.key, path: info.path}) 106 | info.update() # trigger update 107 | return null 108 | } 109 | 110 | callbacks.list.push(info = _.defaults({obj: obj}, callback_info)) # store the callback information 111 | @_onModelLoaded(model) if model 112 | return @ 113 | 114 | releaseCallbacks: (obj) -> 115 | @ee = null 116 | @_unbindCallbacks(event_name, callbacks, kb.wasReleased(obj)) for event_name, callbacks of @__kb.callbacks # unbind all events 117 | delete @__kb.callbacks 118 | 119 | #################################################### 120 | # Internal 121 | #################################################### 122 | 123 | # @nodoc 124 | # NOTE: this is called by registerCallbacks so the model could already be bound and we just want to bind the new info 125 | # NOTE: this is called by emitter so it may be used to clear a previous emitter without triggering an intermediate change 126 | _onModelLoaded: (model) => 127 | @ee = model 128 | for event_name, callbacks of @__kb.callbacks # bind all events 129 | @_unbindCallbacks(event_name, callbacks, true) if callbacks.model and (callbacks.model isnt model) 130 | 131 | (callbacks.model = model; model.bind(event_name, callbacks.fn)) unless callbacks.model 132 | for info in callbacks.list 133 | info.unbind_fn or= kb.settings.orm?.bind(model, info.key, info.update, info.path) 134 | (info.emitter(model) if info.emitter) 135 | return 136 | 137 | # @nodoc 138 | _onModelUnloaded: (model) => 139 | return if @ee isnt model 140 | @ee = null 141 | @_unbindCallbacks(event_name, callbacks) for event_name, callbacks of @__kb.callbacks # unbind all events 142 | return 143 | 144 | # @nodoc 145 | _unbindCallbacks: (event_name, callbacks, skip_emitter) => 146 | (callbacks.model.unbind(event_name, callbacks.fn); callbacks.model = null) if callbacks.model 147 | for info in callbacks.list 148 | (info.unbind_fn(); info.unbind_fn = null) if info.unbind_fn 149 | info.emitter(null) if info.emitter and not skip_emitter and not kb.wasReleased(info.obj) 150 | return 151 | 152 | # factory function 153 | kb.emitterObservable = (emitter, observable) -> return new kb.EventWatcher(emitter, observable) 154 | -------------------------------------------------------------------------------- /src/core/factory.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_} = kb = require './kb' 11 | 12 | # Used to share the hierachy of constructors and create functions by path to allow for custom creation per Model attribute. 13 | # 14 | # @example Create an instance by path. 15 | # var factory = new kb.Factory(); 16 | # factory.addPathMapping('bob.the.builder', kb.ViewModel); 17 | # view_model = factory.createForPath(new Backbone.Model({name: 'Bob'}), 'bob.the.builder'); // creates kb.ViewModel 18 | class kb.Factory 19 | 20 | # Used to either register yourself with the existing factory or to create a new factory. 21 | # 22 | # @param [Object] options please pass the options from your constructor to the register method. For example, constructor(model, options) 23 | # @option options [Object] factories a map of dot-deliminated paths; for example 'models.owner': kb.ViewModel to either constructors or create functions. Signature: 'some.path': function(object, options) 24 | # @param [Instance] obj the instance that will own or register with the store 25 | # @param [String] owner_path the path to the owning object for turning relative scoping of the factories to absolute paths. 26 | @useOptionsOrCreate: (options, obj, owner_path) -> 27 | # share 28 | if options.factory and (not options.factories or (options.factories and options.factory.hasPathMappings(options.factories, owner_path))) 29 | return kb.utils.wrappedFactory(obj, options.factory) 30 | 31 | # create a new factory 32 | factory = kb.utils.wrappedFactory(obj, new kb.Factory(options.factory)) 33 | factory.addPathMappings(options.factories, owner_path) if options.factories 34 | return factory 35 | 36 | constructor: (parent_factory) -> @paths = {}; @parent_factory = parent_factory if parent_factory 37 | 38 | hasPath: (path) -> return @paths.hasOwnProperty(path) or @parent_factory?.hasPath(path) 39 | 40 | addPathMapping: (path, create_info) -> @paths[path] = create_info 41 | 42 | addPathMappings: (factories, owner_path) -> 43 | @paths[kb.utils.pathJoin(owner_path, path)] = create_info for path, create_info of factories 44 | return 45 | 46 | hasPathMappings: (factories, owner_path) -> 47 | all_exist = true 48 | for path, creator of factories 49 | all_exist &= ((existing_creator = @creatorForPath(null, kb.utils.pathJoin(owner_path, path))) and (creator is existing_creator)) 50 | return all_exist 51 | 52 | # If possible, creates an observable for an object using a dot-deliminated path. 53 | # 54 | # @example Create an instance by path. 55 | # var factory = new kb.Factory(); 56 | # factory.addPathMapping('bob.the.builder', kb.ViewModel); 57 | # view_model = factory.createForPath(new Backbone.Model({name: 'Bob'}), 'bob.the.builder'); // creates kb.ViewModel 58 | creatorForPath: (obj, path) -> 59 | return (if creator.view_model then creator.view_model else creator) if creator = @paths[path] 60 | return creator if creator = @parent_factory?.creatorForPath(obj, path) 61 | return null 62 | -------------------------------------------------------------------------------- /src/core/functions/collapse_options.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_} = require '../kb' 11 | 12 | # @nodoc 13 | _mergeArray = (result, key, value) -> 14 | result[key] or= [] 15 | value = [value] unless _.isArray(value) 16 | result[key] = if result[key].length then _.union(result[key], value) else value 17 | return result 18 | 19 | # @nodoc 20 | _mergeObject = (result, key, value) -> result[key] or= {}; return _.extend(result[key], value) 21 | 22 | # @nodoc 23 | _keyArrayToObject = (value) -> result = {}; result[item] = {key: item} for item in value; return result 24 | 25 | _mergeOptions = (result, options) -> 26 | return result unless options 27 | for key, value of options 28 | switch key 29 | when 'internals', 'requires', 'excludes', 'statics' then _mergeArray(result, key, value) 30 | when 'keys' 31 | # an object 32 | if (_.isObject(value) and not _.isArray(value)) or (_.isObject(result[key]) and not _.isArray(result[key])) 33 | value = [value] unless _.isObject(value) 34 | value = _keyArrayToObject(value) if _.isArray(value) 35 | result[key] = _keyArrayToObject(result[key]) if _.isArray(result[key]) 36 | _mergeObject(result, key, value) 37 | 38 | # an array 39 | else 40 | _mergeArray(result, key, value) 41 | 42 | when 'factories' 43 | if _.isFunction(value) then result[key] = value else _mergeObject(result, key, value) 44 | when 'static_defaults' then _mergeObject(result, key, value) 45 | when 'options' then 46 | else 47 | result[key] = value 48 | 49 | return _mergeOptions(result, options.options) 50 | 51 | # @nodoc 52 | module.exports = (options) -> _mergeOptions({}, options) 53 | -------------------------------------------------------------------------------- /src/core/functions/extend.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | # From Backbone.js (https:github.com/documentcloud/backbone) 11 | copyProps = (dest, source) -> dest[key] = value for key, value of source; return dest 12 | 13 | `// Shared empty constructor function to aid in prototype-chain creation. 14 | var ctor = function(){}; 15 | 16 | // Helper function to correctly set up the prototype chain, for subclasses. 17 | // Similar to 'goog.inherits', but uses a hash of prototype properties and 18 | // class properties to be extended. 19 | var inherits = function(parent, protoProps, staticProps) { 20 | var child; 21 | 22 | // The constructor function for the new subclass is either defined by you 23 | // (the "constructor" property in your extend definition), or defaulted 24 | // by us to simply call the parent's constructor. 25 | if (protoProps && protoProps.hasOwnProperty('constructor')) { 26 | child = protoProps.constructor; 27 | } else { 28 | child = function(){ parent.apply(this, arguments); }; 29 | } 30 | 31 | // Inherit class (static) properties from parent. 32 | copyProps(child, parent); 33 | 34 | // Set the prototype chain to inherit from parent, without calling 35 | // parent's constructor function. 36 | ctor.prototype = parent.prototype; 37 | child.prototype = new ctor(); 38 | 39 | // Add prototype properties (instance properties) to the subclass, 40 | // if supplied. 41 | if (protoProps) copyProps(child.prototype, protoProps); 42 | 43 | // Add static properties to the constructor function, if supplied. 44 | if (staticProps) copyProps(child, staticProps); 45 | 46 | // Correctly set child's 'prototype.constructor'. 47 | child.prototype.constructor = child; 48 | 49 | // Set a convenience property in case the parent's prototype is needed later. 50 | child.__super__ = parent.prototype; 51 | 52 | return child; 53 | }; 54 | 55 | // The self-propagating extend function that BacLCone classes use. 56 | var extend = function (protoProps, classProps) { 57 | var child = inherits(this, protoProps, classProps); 58 | child.extend = this.extend; 59 | return child; 60 | }; 61 | ` 62 | 63 | module.exports = extend 64 | -------------------------------------------------------------------------------- /src/core/functions/unwrap_models.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_} = require '../kb' 11 | 12 | # @nodoc 13 | module.exports = unwrapModels = (obj) -> 14 | return obj unless obj 15 | 16 | return (if obj.__kb.hasOwnProperty('object') then obj.__kb.object else obj) if obj.__kb 17 | return _.map(obj, (test) -> return unwrapModels(test)) if _.isArray(obj) 18 | if _.isObject(obj) and (obj.constructor is {}.constructor) # a simple object 19 | result = {} 20 | result[key] = unwrapModels(value) for key, value of obj 21 | return result 22 | 23 | return obj 24 | -------------------------------------------------------------------------------- /src/core/functions/wrapped_destroy.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_} = require '../kb' 11 | 12 | # @nodoc 13 | module.exports = wrappedDestroy = (obj) -> 14 | return unless obj.__kb 15 | obj.__kb.event_watcher.releaseCallbacks(obj) if obj.__kb.event_watcher 16 | 17 | __kb = obj.__kb; obj.__kb = null # clear now to break cycles 18 | 19 | if __kb.observable 20 | __kb.observable.destroy = __kb.observable.release = null 21 | wrappedDestroy(__kb.observable) 22 | __kb.observable = null 23 | 24 | __kb.factory = null 25 | 26 | __kb.event_watcher.destroy() if __kb.event_watcher_is_owned # release the event_watcher 27 | __kb.event_watcher = null 28 | 29 | __kb.store.destroy() if __kb.store_is_owned # release the store 30 | __kb.store = null 31 | if __kb.stores_references 32 | while store_references = __kb.stores_references.pop() 33 | store_references.store.release(obj) unless store_references.store.__kb_released 34 | return 35 | -------------------------------------------------------------------------------- /src/core/index.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | module.exports = kb = require './kb' 11 | 12 | kb.configure = require './configure' 13 | 14 | # re-expose modules 15 | kb.modules = {underscore: kb._, backbone: kb.Parse or kb.Backbone, knockout: kb.ko} 16 | -------------------------------------------------------------------------------- /src/core/inject.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | window = if window? then window else global 11 | 12 | {_, ko} = kb = require './kb' 13 | 14 | kb.RECUSIVE_AUTO_INJECT = true 15 | 16 | # custom Knockout `inject` binding 17 | ko.bindingHandlers['inject'] = 18 | 'init': (element, value_accessor, all_bindings_accessor, view_model) -> 19 | kb.Inject.inject(ko.utils.unwrapObservable(value_accessor()), view_model, element, value_accessor, all_bindings_accessor) 20 | 21 | # Used to inject ViewModels and observables dynamically from your HTML Views. For both the `'kb-inject'` attribute and the data-bind `'inject'` custom binding, the following properties are reserved: 22 | # 23 | # * `'view_model'` class used to create a new ViewModel instance 24 | # * `'create'` function used to manually add observables to a view model 25 | # * `'options'` to pass to ko.applyBindings 26 | # * `'afterBinding'` callback (can alternatively be in the options) 27 | # * `'beforeBinding'` callback (can alternatively be in the options) 28 | # 29 | # Each function/constructor gets called with the following signature `'function(view_model, element)'`. 30 | # 31 | # @example Bind your application automatically when the DOM is loaded. 32 | #
33 | # @example Bind your application with properties. 34 | #
35 | # @example Bind your application creating a specific ViewModel instance when the DOM is loaded. 36 | #
37 | # var MyViewModel = function(view_model, el) { 38 | # this.message = ko.observable('Hello World!'); 39 | # } 40 | # @example Bind your application using a function when the DOM is loaded (like Angular.js controllers). 41 | #
42 | # var MyController = function(view_model, el) { 43 | # view_model.message = ko.observable('Hello World!'); 44 | # } 45 | # @example Bind your application with a specific ViewModel instance and a callback before and after the binding. 46 | #
47 | # var MyViewModel = function(view_model, el) { 48 | # this.message = ko.observable('Hello World!'); 49 | # this.beforeBinding = function() {alert('before'); }; 50 | # this.afterBinding = function() {alert('after'); }; 51 | # } 52 | # @example Dynamically inject new properties into your ViewModel. 53 | #
54 | #
55 | # 56 | # 57 | #
58 | #
59 | # var MyViewModel = function(view_model, el) { 60 | # // site will be dynamically attached to this ViewModel 61 | # } 62 | # @example Dynamically bind a form. 63 | #
64 | #
65 | #
66 | # 67 | # 68 | #
69 | #
70 | # 71 | # 72 | #
73 | #
74 | #
75 | # var MyViewModel = kb.ViewModel.extend({ 76 | # constructor: -> 77 | # model = new Backbone.Model({name: '', site: 'http://your.url.com'}); 78 | # kb.ViewModel.prototype.constructor.call(this, model); 79 | # }); 80 | class kb.Inject 81 | # @private 82 | @inject: (data, view_model, element, value_accessor, all_bindings_accessor, nested) -> 83 | inject = (data) -> 84 | if _.isFunction(data) 85 | view_model = new data(view_model, element, value_accessor, all_bindings_accessor) # use 'new' to allow for classes in addition to functions 86 | kb.releaseOnNodeRemove(view_model, element) 87 | else 88 | # view_model constructor causes a scope change 89 | if (data.view_model) 90 | # specifying a view_model changes the scope so we need to bind a destroy 91 | view_model = new data.view_model(view_model, element, value_accessor, all_bindings_accessor) 92 | kb.releaseOnNodeRemove(view_model, element) 93 | 94 | # resolve and merge in each key 95 | for key, value of data 96 | continue if (key is 'view_model') 97 | 98 | # create function 99 | if (key is 'create') 100 | value(view_model, element, value_accessor, all_bindings_accessor) 101 | 102 | # resolve nested with assign or not 103 | else if _.isObject(value) and not _.isFunction(value) 104 | target = if nested or (value and value.create) then {} else view_model 105 | view_model[key] = kb.Inject.inject(value, target, element, value_accessor, all_bindings_accessor, true) 106 | 107 | # simple set 108 | else 109 | view_model[key] = value 110 | 111 | return view_model 112 | 113 | # in recursive calls, we are already protected from propagating dependencies to the template 114 | return if nested then inject(data) else kb.ignore(-> inject(data)) 115 | 116 | # Searches the DOM from root or document for elements with the `'kb-inject'` attribute and create/customizes ViewModels for the DOM tree when encountered. Also, used with the data-bind `'inject'` custom binding. 117 | # @param [DOM element] root the root DOM element to start searching for `'kb-inject'` attributes. 118 | # @return [Array] array of Objects with the DOM elements and ViewModels that were bound in the form `{el: DOM element, view_model: ViewModel}`. 119 | @injectViewModels: (root) -> 120 | # find all of the app elements 121 | results = [] 122 | findElements = (el) -> 123 | unless el.__kb_injected # already injected -> skip, but still process children in case they were added afterwards 124 | if el.attributes and (attr = _.find(el.attributes, (attr)-> attr.name is 'kb-inject')) 125 | el.__kb_injected = true # mark injected 126 | results.push({el: el, view_model: {}, binding: attr.value}) 127 | findElements(child_el) for child_el in el.childNodes 128 | return 129 | root = window.document if not root and window?.document 130 | findElements(root) 131 | 132 | # bind the view models 133 | for app in results 134 | # evaluate the app data 135 | if expression = app.binding 136 | (expression.search(/[:]/) < 0) or (expression = "{#{expression}}") # wrap if is an object 137 | data = (new Function("", "return ( #{expression} )"))() 138 | data or (data = {}) # no data 139 | (not data.options) or (options = data.options; delete data.options) # extract options 140 | options or (options={}) 141 | app.view_model = kb.Inject.inject(data, app.view_model, app.el, null, null, true) 142 | afterBinding = app.view_model.afterBinding or options.afterBinding 143 | beforeBinding = app.view_model.beforeBinding or options.beforeBinding 144 | 145 | # auto-bind 146 | beforeBinding.call(app.view_model, app.view_model, app.el, options) if beforeBinding 147 | kb.applyBindings(app.view_model, app.el, options) 148 | afterBinding.call(app.view_model, app.view_model, app.el, options) if afterBinding 149 | return results 150 | 151 | # auto-inject recursively 152 | _ko_applyBindings = ko.applyBindings 153 | ko.applyBindings = (context, element) -> 154 | results = if kb.RECUSIVE_AUTO_INJECT then kb.injectViewModels(element) else [] 155 | _ko_applyBindings.apply(@, arguments) unless results.length 156 | 157 | ############################# 158 | # Aliases 159 | ############################# 160 | kb.injectViewModels = kb.Inject.injectViewModels 161 | 162 | ############################# 163 | # Auto Inject results 164 | ############################# 165 | if document? 166 | # use simple ready check 167 | (onReady = -> 168 | return setTimeout(onReady, 0) unless document.readyState is 'complete' # keep waiting for the document to load 169 | kb.injectViewModels() # the document is loaded 170 | )() 171 | -------------------------------------------------------------------------------- /src/core/monkey-patches.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {ko} = kb = require './kb' 11 | 12 | # Allow for dependent release until is resolved https://github.com/knockout/knockout/issues/1464 13 | if ko.subscribable?.fn?.extend 14 | _extend = ko.subscribable.fn.extend 15 | ko.subscribable.fn.extend = -> 16 | target = _extend.apply(@, arguments) 17 | 18 | # release the extended observable 19 | if target isnt @ and kb.isReleaseable(@) 20 | _dispose = target.dispose 21 | target.dispose = => _dispose?.apply(target, arguments); kb.release(@) 22 | 23 | return target 24 | -------------------------------------------------------------------------------- /src/core/observable.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_, ko} = kb = require './kb' 11 | TypedValue = require './typed-value' 12 | 13 | KEYS_PUBLISH = ['value', 'valueType', 'destroy'] 14 | KEYS_INFO = ['args', 'read', 'write'] 15 | 16 | # Base class for observing model attributes. 17 | # 18 | # @example How to create a ko.CollectionObservable using the ko.collectionObservable factory. 19 | # var ContactViewModel = function(model) { 20 | # this.name = kb.observable(model, 'name'); 21 | # this.number = kb.observable(model, { key: 'number'}); 22 | # }; 23 | # var model = new Contact({ name: 'Ringo', number: '555-555-5556' }); 24 | # var view_model = new ContactViewModel(model); 25 | # 26 | # @example How to create a kb.Observable with a default value. 27 | # var model = Backbone.Model({name: 'Bob'}); 28 | # var name = kb.observable(model, {key:'name', default: '(none)'}); // name is Bob 29 | # name.setToDefault(); // name is (none) 30 | # 31 | # @method #model() 32 | # Dual-purpose getter/setter ko.computed for the observed model. 33 | # @return [Model|ModelRef|void] getter: the model whose attributes are being observed (can be null) OR setter: void 34 | # @example 35 | # var observable = kb.observable(new Backbone.Model({name: 'bob'}), 'name'); 36 | # var the_model = observable.model(); // get 37 | # observable.model(new Backbone.Model({name: 'fred'})); // set 38 | # 39 | class kb.Observable 40 | 41 | # Used to create a new kb.Observable. 42 | # 43 | # @param [Model] model the model to observe (can be null) 44 | # @param [String|Array|Object] options the create options. String is a single attribute name, Array is an array of attribute names. 45 | # @option options [String] key the name of the attribute. 46 | # @option options [Function] read a function used to provide transform the attribute value before passing it to the caller. Signature: read() 47 | # @option options [Function] write a function used to provide transform the value before passing it to the model set function. Signature: write(value) 48 | # @option options [Array] args arguments to pass to the read and write functions (they can be ko.observables). Can be useful for passing arguments to a locale manager. 49 | # @option options [Constructor] localizer a concrete kb.LocalizedObservable constructor for localization. 50 | # @option options [Data|ko.observable] default the default value. Can be a value, string or ko.observable. 51 | # @option options [String] path the path to the value (used to create related observables from the factory). 52 | # @option options [kb.Store] store a store used to cache and share view models. 53 | # @option options [kb.Factory] factory a factory used to create view models. 54 | # @option options [Object] options a set of options merge into these options. Useful for extending options when deriving classes rather than merging them by hand. 55 | # @return [ko.observable] the constructor does not return 'this' but a ko.observable 56 | # @note the constructor does not return 'this' but a ko.observable 57 | constructor: (model, key_or_info, options, @_vm={}) -> return kb.ignore => 58 | key_or_info or kb._throwMissing(this, 'key_or_info') 59 | @key = key_or_info.key or key_or_info 60 | @[key] = key_or_info[key] for key in KEYS_INFO when key_or_info[key] 61 | 62 | create_options = kb.utils.collapseOptions(options) 63 | event_watcher = create_options.event_watcher 64 | delete create_options.event_watcher 65 | 66 | # set up basics 67 | @_value = new TypedValue(create_options) 68 | @_model = ko.observable() 69 | observable = kb.utils.wrappedObservable @, ko.computed { 70 | read: => 71 | _model = @_model(); ko.utils.unwrapObservable(arg) for arg in args = [@key].concat(@args or []) 72 | kb.utils.wrappedEventWatcher(@)?.emitter(_model or null) # update the event watcher 73 | if @read 74 | @update(@read.apply(@_vm, args)) 75 | else if !_.isUndefined(_model) 76 | kb.ignore => @update(kb.getValue(_model, kb.peek(@key), @args)) 77 | return @_value.value() 78 | 79 | write: (new_value) => kb.ignore => 80 | unwrapped_new_value = kb.utils.unwrapModels(new_value) # unwrap for set (knockout may pass view models which are required for the observable but not the model) 81 | _model = kb.peek(@_model) 82 | if @write 83 | @write.call(@_vm, unwrapped_new_value) 84 | new_value = kb.getValue(_model, kb.peek(@key), @args) 85 | else if _model 86 | kb.setValue(_model, kb.peek(@key), unwrapped_new_value) 87 | @update(new_value) 88 | 89 | owner: @_vm 90 | } 91 | 92 | observable.__kb_is_o = true # mark as a kb.Observable 93 | create_options.store = kb.utils.wrappedStore(observable, create_options.store) 94 | create_options.path = kb.utils.pathJoin(create_options.path, @key) 95 | if create_options.factories and ((typeof(create_options.factories) is 'function') or create_options.factories.create) 96 | create_options.factory = kb.utils.wrappedFactory(observable, new kb.Factory(create_options.factory)) 97 | create_options.factory.addPathMapping(create_options.path, create_options.factories) 98 | else 99 | create_options.factory = kb.Factory.useOptionsOrCreate(create_options, observable, create_options.path) 100 | delete create_options.factories 101 | 102 | # publish public interface on the observable and return instead of this 103 | kb.publishMethods(observable, @, KEYS_PUBLISH) 104 | 105 | # use external model observable or create 106 | observable.model = @model = ko.computed { 107 | read: => ko.utils.unwrapObservable(@_model) 108 | write: (new_model) => kb.ignore => 109 | return if @__kb_released or (kb.peek(@_model) is new_model) # destroyed or no change 110 | 111 | # update references 112 | new_value = kb.getValue(new_model, kb.peek(@key), @args) 113 | @_model(new_model) 114 | if not new_model 115 | @update(null) 116 | else if not _.isUndefined(new_value) 117 | @update(new_value) 118 | } 119 | kb.EventWatcher.useOptionsOrCreate({event_watcher: event_watcher}, model or null, @, {emitter: @model, update: (=> kb.ignore => @update()), key: @key, path: create_options.path}) 120 | @_value.rawValue() or @_value.update() # wasn't loaded so create 121 | 122 | observable = new key_or_info.localizer(observable) if kb.LocalizedObservable and key_or_info.localizer # wrap ourselves with a localizer 123 | observable = kb.defaultObservable(observable, key_or_info.default) if kb.DefaultObservable and key_or_info.hasOwnProperty('default') # wrap ourselves with a default value 124 | 125 | return observable 126 | 127 | # Required clean up function to break cycles, release view models, etc. 128 | # Can be called directly, via kb.release(object) or as a consequence of ko.releaseNode(element). 129 | destroy: -> 130 | observable = kb.utils.wrappedObservable(@) 131 | @__kb_released = true 132 | @_value.destroy(); @_value = null 133 | @model.dispose(); @model = observable.model = null 134 | kb.utils.wrappedDestroy(@) 135 | 136 | # @return [kb.CollectionObservable|kb.ViewModel|ko.observable] exposes the raw value inside the kb.observable. For example, if your attribute is a Collection, it will hold a CollectionObservable. 137 | value: -> @_value.rawValue() 138 | 139 | # @return [kb.TYPE_UNKNOWN|kb.TYPE_SIMPLE|kb.TYPE_ARRAY|kb.TYPE_MODEL|kb.TYPE_COLLECTION] provides the type of the wrapped value. 140 | valueType: -> @_value.valueType(kb.peek(@_model), kb.peek(@key)) 141 | 142 | #################################################### 143 | # Internal 144 | #################################################### 145 | # @nodoc 146 | update: (new_value) -> 147 | return if @__kb_released # destroyed, nothing to do 148 | new_value = kb.getValue(kb.peek(@_model), kb.peek(@key)) unless arguments.length 149 | @_value.update(new_value) 150 | 151 | kb.observable = (model, key, options, view_model) -> new kb.Observable(model, key, options, view_model) 152 | -------------------------------------------------------------------------------- /src/core/orms/backbone-associations.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_, Backbone} = kb = require '../kb' 11 | 12 | AssociatedModel = null # lazy check 13 | 14 | # @nodoc 15 | module.exports = class BackboneAssociations 16 | @isAvailable: -> return !!AssociatedModel = Backbone?.AssociatedModel # or require?('backbone-associations')?.AssociatedModel # webpack optionals 17 | 18 | @keys: (model) -> 19 | return null unless model instanceof AssociatedModel 20 | return _.map(model.relations, (test) -> test.key) 21 | 22 | @relationType: (model, key) -> 23 | return null unless model instanceof AssociatedModel 24 | return null unless relation = _.find(model.relations, (test) -> return test.key is key) 25 | return if (relation.type is 'Many') then kb.TYPE_COLLECTION else kb.TYPE_MODEL 26 | 27 | @useFunction: -> false 28 | -------------------------------------------------------------------------------- /src/core/orms/backbone-relational.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_, Backbone} = kb = require '../kb' 11 | 12 | RelationalModel = null # lazy check 13 | 14 | # @nodoc 15 | module.exports = class BackboneRelational 16 | @isAvailable: -> return !!RelationalModel = Backbone?.RelationalModel # or require?('backbone-relational')?.RelationalModel # webpack optionals 17 | 18 | @relationType: (model, key) -> 19 | return null unless model instanceof RelationalModel 20 | return null unless relation = _.find(model.getRelations(), (test) -> return test.key is key) 21 | return if (relation.collectionType or _.isArray(relation.keyContents)) then kb.TYPE_COLLECTION else kb.TYPE_MODEL 22 | 23 | @bind: (model, key, update, path) -> 24 | return null unless type = @relationType(model, key) 25 | rel_fn = (model) -> 26 | not kb.statistics or kb.statistics.addModelEvent({name: 'update (relational)', model: model, key: key, path: path}) 27 | update() 28 | 29 | # VERSIONING: pre Backbone-Relational 0.8.0 30 | events = if kb.Backbone.Relation.prototype.sanitizeOptions then ['update', 'add', 'remove'] else ['change', 'add', 'remove'] 31 | if type is kb.TYPE_COLLECTION 32 | model.bind("#{event}:#{key}", rel_fn) for event in events 33 | else 34 | model.bind("#{events[0]}:#{key}", rel_fn) 35 | 36 | return -> 37 | if type is kb.TYPE_COLLECTION 38 | model.unbind("#{event}:#{key}", rel_fn) for event in events 39 | else 40 | model.unbind("#{events[0]}:#{key}", rel_fn) 41 | return 42 | 43 | @useFunction: -> false 44 | -------------------------------------------------------------------------------- /src/core/statistics.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_} = kb = require './kb' 11 | 12 | # kb.Statistics is an optional components that is useful for measuring your application's performance. You can record all of the Backbone.Events that have triggered ko.observable subscription updates and the memory footprint (instance count-only) of your ViewModels and collection observables. 13 | # 14 | # kb.Statistics is not included in `knockback.js` nor `knockback-core.js` so you need to manually include it from the `lib` directory. 15 | # 16 | module.exports = class kb.Statistics 17 | constructor: -> 18 | @model_events_tracker = [] 19 | @registered_tracker = {} 20 | 21 | # Clear the tracked model events (but keep the registered objects intact) 22 | clear: -> 23 | @model_events_tracker = [] 24 | 25 | ############################### 26 | # Registered Events 27 | ############################### 28 | 29 | # Register a model event 30 | addModelEvent: (event) -> 31 | @model_events_tracker.push(event) 32 | 33 | # A debug helper to summarize the registered events in human-readable form 34 | modelEventsStatsString: -> 35 | stats_string = '' 36 | stats_string += "Total Count: #{@model_events_tracker.length}" 37 | event_groups = _.groupBy(@model_events_tracker, (test) -> return "event name: '#{test.name}', attribute name: '#{test.key}'") 38 | for key, value of event_groups 39 | stats_string += "\n #{key}, count: #{value.length}" 40 | return stats_string 41 | 42 | ############################### 43 | # Registered Observables and View Models 44 | ############################### 45 | 46 | # Register an object by key 47 | register: (key, obj) -> 48 | @registeredTracker(key).push(obj) 49 | 50 | # Unregister an object by key 51 | unregister: (key, obj) -> 52 | type_tracker = @registeredTracker(key) 53 | return console?.log("kb.Statistics: failed to unregister type: #{key}") if (index = _.indexOf(type_tracker, obj)) < 0 54 | type_tracker.splice(index, 1) 55 | 56 | # @return [Integer] the number of registered objects by type 57 | registeredCount: (type) -> 58 | return @registeredTracker(type).length if type 59 | count = 0 60 | count += type_tracker.length for type, type_tracker of @registered_tracker[type] 61 | return count 62 | 63 | # A debug helper to summarize the current registered objects by key 64 | # 65 | # @param [String] success_message a message to return if there are no registered objects 66 | # @return [String] a human readable string summarizing the currently registered objects or success_message 67 | registeredStatsString: (success_message) -> 68 | stats_string = '' 69 | for type, type_tracker of @registered_tracker 70 | continue unless type_tracker.length 71 | stats_string += '\n ' if written 72 | stats_string += "#{if type then type else 'No Name'}: #{type_tracker.length}" 73 | written = true 74 | return if stats_string then stats_string else success_message 75 | 76 | # @nodoc 77 | registeredTracker: (key) -> 78 | return @registered_tracker[key] if @registered_tracker.hasOwnProperty(key) 79 | type_tracker = []; @registered_tracker[key] = type_tracker 80 | return type_tracker 81 | 82 | @eventsStats: (obj, key) -> 83 | stats = {count: 0} 84 | 85 | events = obj._events or obj._callbacks or {} 86 | for key in (if key then [key] else _.keys(events)) when node = events[key] 87 | if _.isArray(node) 88 | stats[key] = _.compact(node).length 89 | else 90 | stats[key] = 0; tail = node.tail 91 | stats[key]++ while ((node = node.next) isnt tail) 92 | stats.count += stats[key] 93 | return stats 94 | -------------------------------------------------------------------------------- /src/core/typed-value.coffee: -------------------------------------------------------------------------------- 1 | {_, ko} = kb = require './kb' 2 | 3 | # @nodoc 4 | module.exports = class TypedValue 5 | constructor: (@create_options) -> 6 | @_vo = ko.observable(null) # create a value observable for the first dependency 7 | 8 | destroy: -> 9 | @__kb_released = true 10 | if previous_value = @__kb_value 11 | @__kb_value = null 12 | if @create_options.store and kb.utils.wrappedCreator(previous_value) then @create_options.store.release(previous_value) else kb.release(previous_value) 13 | @create_options = null 14 | 15 | value: -> ko.utils.unwrapObservable(@_vo()) 16 | rawValue: -> return @__kb_value 17 | 18 | valueType: (model, key) -> 19 | new_value = kb.getValue(model, key) 20 | @value_type or @_updateValueObservable(new_value) # create so we can check the type 21 | return @value_type 22 | 23 | update: (new_value) -> 24 | return if @__kb_released # destroyed, nothing to do 25 | 26 | # determine the new type 27 | (new_value isnt undefined) or (new_value = null) # ensure null instead of undefined 28 | new_type = kb.utils.valueType(new_value) 29 | 30 | (@__kb_value = @value_type = undefined) if @__kb_value?.__kb_released 31 | value = @__kb_value 32 | 33 | switch @value_type 34 | when kb.TYPE_COLLECTION 35 | return value(new_value) if @value_type is kb.TYPE_COLLECTION and new_type is kb.TYPE_ARRAY 36 | if new_type is kb.TYPE_COLLECTION or _.isNull(new_value) 37 | # use the provided CollectionObservable 38 | if new_value and new_value instanceof kb.CollectionObservable 39 | @_updateValueObservable(kb.utils.wrappedObject(new_value), new_value) 40 | else 41 | value.collection(new_value) if kb.peek(value.collection) isnt new_value # collection observables are allocated once 42 | return 43 | 44 | when kb.TYPE_MODEL 45 | if new_type is kb.TYPE_MODEL or _.isNull(new_value) 46 | # use the provided ViewModel 47 | if new_value and not kb.isModel(new_value) 48 | @_updateValueObservable(kb.utils.wrappedObject(new_value), new_value) 49 | else 50 | @_updateValueObservable(new_value) if kb.utils.wrappedObject(value) isnt kb.utils.resolveModel(new_value) 51 | return 52 | 53 | if @value_type is new_type and not _.isUndefined(@value_type) 54 | value(new_value) if kb.peek(value) isnt new_value 55 | else 56 | @_updateValueObservable(new_value) if kb.peek(value) isnt new_value 57 | 58 | _updateValueObservable: (new_value, new_observable) -> 59 | create_options = @create_options 60 | creator = kb.utils.inferCreator(new_value, create_options.factory, create_options.path) 61 | 62 | # retain previous type 63 | if (new_value is null) and not creator 64 | if @value_type is kb.TYPE_MODEL 65 | creator = kb.ViewModel 66 | else if @value_type is kb.TYPE_COLLECTION 67 | creator = kb.CollectionObservable 68 | create_options.creator = creator 69 | 70 | value_type = kb.TYPE_UNKNOWN 71 | [previous_value, @__kb_value] = [@__kb_value, undefined] 72 | 73 | if new_observable 74 | value = new_observable 75 | create_options.store.retain(new_observable, new_value, creator) if create_options.store 76 | 77 | # found a creator 78 | else if creator 79 | # have the store, use it to create 80 | if create_options.store 81 | value = create_options.store.retainOrCreate(new_value, create_options, true) 82 | 83 | # create manually 84 | else 85 | if creator.models_only 86 | value = new_value 87 | value_type = kb.TYPE_SIMPLE 88 | else if creator.create 89 | value = creator.create(new_value, create_options) 90 | else 91 | value = new creator(new_value, create_options) 92 | 93 | # create and cache the type 94 | else 95 | if _.isArray(new_value) 96 | value_type = kb.TYPE_ARRAY 97 | value = ko.observableArray(new_value) 98 | else 99 | value_type = kb.TYPE_SIMPLE 100 | value = ko.observable(new_value) 101 | 102 | # determine the type 103 | if (@value_type = value_type) is kb.TYPE_UNKNOWN 104 | if not ko.isObservable(value) # a view model, recognize view_models as non-observable 105 | @value_type = kb.TYPE_MODEL 106 | kb.utils.wrappedObject(value, kb.utils.resolveModel(new_value)) 107 | else if value.__kb_is_co 108 | @value_type = kb.TYPE_COLLECTION 109 | kb.utils.wrappedObject(value, new_value) 110 | else if not @value_type 111 | @value_type = kb.TYPE_SIMPLE 112 | 113 | # release previous 114 | if previous_value 115 | if @create_options.store then @create_options.store.release(previous_value) else kb.release(previous_value) 116 | 117 | # store the value 118 | @__kb_value = value 119 | @_vo(value) 120 | 121 | _inferType: (value) -> 122 | -------------------------------------------------------------------------------- /src/core/view-model.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_, ko} = kb = require './kb' 11 | 12 | # @nodoc 13 | assignViewModelKey = (vm, key) -> 14 | vm_key = if vm.__kb.internals and ~_.indexOf(vm.__kb.internals, key) then "_#{key}" else key 15 | return if vm.__kb.view_model.hasOwnProperty(vm_key) # already exists, skip 16 | vm.__kb.view_model[vm_key] = null 17 | return vm_key 18 | 19 | # @nodoc 20 | createObservable = (vm, model, key, create_options) -> 21 | return if vm.__kb.excludes and ~_.indexOf(vm.__kb.excludes, key) 22 | return if vm.__kb.statics and ~_.indexOf(vm.__kb.statics, key) 23 | return unless vm_key = assignViewModelKey(vm, key) 24 | vm[vm_key] = vm.__kb.view_model[vm_key] = kb.observable(model, key, create_options, vm) 25 | 26 | # @nodoc 27 | createStaticObservables = (vm, model) -> 28 | for key in vm.__kb.statics when vm_key = assignViewModelKey(vm, key) 29 | if model.has(vm_key) 30 | vm[vm_key] = vm.__kb.view_model[vm_key] = model.get(vm_key) 31 | else if vm.__kb.static_defaults and vm_key of vm.__kb.static_defaults 32 | vm[vm_key] = vm.__kb.view_model[vm_key] = vm.__kb.static_defaults[vm_key] 33 | else 34 | delete vm.__kb.view_model[vm_key] 35 | return 36 | 37 | KEYS_OPTIONS = ['keys', 'internals', 'excludes', 'statics', 'static_defaults'] 38 | 39 | # Base class for ViewModels for Models. 40 | # 41 | # @example How to create a ViewModel with first_name and last_name observables. 42 | # var view_model = kb.viewModel(new Backbone.Model({first_name: "Planet", last_name: "Earth"})); 43 | # 44 | # @example Bulk kb.Observable create using 'key' Object to customize the kb.Observable created per attribute. 45 | # var ContactViewModel = function(model) { 46 | # this.loading_message = new kb.LocalizedStringLocalizer(new LocalizedString('loading')); 47 | # this._auto = kb.viewModel(model, { 48 | # keys: { 49 | # name: { key: 'name', 'default': this.loading_message }, 50 | # number: { key: 'number', 'default': this.loading_message }, 51 | # date: { key: 'date', 'default': this.loading_message, localizer: kb.ShortDateLocalizer } 52 | # } 53 | # }, this); 54 | # return this; 55 | # }; 56 | # 57 | # @example Creating ko.Observables on a target ViewModel 58 | # var view_model = {}; 59 | # kb.viewModel(model, ['name', 'date'], view_model); // observables are added to view_model 60 | # 61 | # @method .extend(prototype_properties, class_properties) 62 | # Class method for JavaScript inheritance. 63 | # @param [Object] prototype_properties the properties to add to the prototype 64 | # @param [Object] class_properties the properties to add to the class 65 | # @return [kb.ViewModel] the constructor returns 'this' 66 | # @example 67 | # var ContactViewModel = kb.ViewModel.extend({ 68 | # constructor: function(model) { 69 | # kb.ViewModel.prototype.constructor.call(this, model, {internals: ['email', 'date']}); // call super constructor: @name, @_email, and @_date created in super from the model attributes 70 | # this.email = kb.defaultObservable(this._email, 'your.name@yourplace.com'); 71 | # this.date = new LongDateLocalizer(this._date); 72 | # return this; 73 | # } 74 | # }); 75 | # @example 76 | # var ViewModel = kb.ViewModel.extend({ 77 | # constructor: function(model){ 78 | # kb.ViewModel.prototype.constructor.apply(this, arguments); 79 | # this.full_name = ko.computed(function() { return this.first_name() + " " + this.last_name(); }, this); 80 | # } 81 | # }); 82 | # var view_model = new ViewModel(model); 83 | # 84 | # @method #model() 85 | # Dual-purpose getter/setter ko.computed for the observed model. 86 | # @return [Model|ModelRef|void] getter: the model whose attributes are being observed (can be null) OR setter: void 87 | # @example 88 | # var view_model = kb.viewModel(new Backbone.Model({name: 'bob'})); 89 | # var the_model = view_model.model(); // get 90 | # view_model.model(new Backbone.Model({name: 'fred'})); // set 91 | # 92 | class kb.ViewModel 93 | # @nodoc 94 | @extend = kb.extend # for Backbone non-Coffeescript inheritance (use "kb.SuperClass.extend({})" in Javascript instead of "class MyClass extends kb.SuperClass") 95 | 96 | # Used to create a new kb.ViewModel. 97 | # 98 | # @param [Model|ModelRef] model the model to observe (can be null) 99 | # @param [Object] options the create options 100 | # @option options [Array|String] internals an array of atttributes that should be scoped with an underscore, eg. name -> _name 101 | # @option options [Array|String] requires an array of atttributes that will have kb.Observables created even if they do not exist on the Model. Useful for binding Views that require specific observables to exist 102 | # @option options [Array|String] keys restricts the keys used on a model. Useful for reducing the number of kb.Observables created from a limited set of Model attributes 103 | # @option options [Object|Array|String] excludes if an array is supplied, excludes keys to exclude on the view model; for example, if you want to provide a custom implementation. If an Object, it provides options to the kb.Observable constructor. 104 | # @option options [Array] statics creates non-observable properties on your view model for Model attributes that do not need to be observed for changes. 105 | # @option options [Object] static_defaults provides default values for statics. 106 | # @option options [String] path the path to the value (used to create related observables from the factory). 107 | # @option options [kb.Store] store a store used to cache and share view models. 108 | # @option options [Object] factories a map of dot-deliminated paths; for example `{'models.name': kb.ViewModel}` to either constructors or create functions. Signature: `{'some.path': function(object, options)}` 109 | # @option options [kb.Factory] factory a factory used to create view models. 110 | # @option options [Object] options a set of options merge into these options. Useful for extending options when deriving classes rather than merging them by hand. 111 | # @return [ko.observable] the constructor returns 'this' 112 | # @param [Object] view_model a view model to also set the kb.Observables on. Useful when batch creating observable on an owning view model. 113 | constructor: (model, options={}, view_model) -> args = Array.prototype.slice.call(if _.isArguments(model) then model else arguments); return kb.ignore => 114 | not (model = args.shift()) or kb.isModel(model) or kb._throwUnexpected(@, 'not a model') 115 | args[0] = {keys: args[0]} if _.isArray(args[0]) 116 | @__kb or= {}; @__kb.view_model = (if args.length > 1 then args.pop() else @) 117 | options = {}; _.extend(options, arg) for arg in args; options = kb.utils.collapseOptions(options) 118 | @__kb[key] = options[key] for key in KEYS_OPTIONS when options.hasOwnProperty(key) 119 | 120 | # always use a store to ensure recursive view models are handled correctly 121 | kb.Store.useOptionsOrCreate(options, model, @) 122 | 123 | # view model factory 124 | @__kb.path = options.path 125 | kb.Factory.useOptionsOrCreate(options, @, options.path) 126 | 127 | _model = kb.utils.set(@, '_model', ko.observable()) 128 | @model = ko.computed { 129 | read: => ko.utils.unwrapObservable(_model) 130 | write: (new_model) => kb.ignore => 131 | return if kb.wasReleased(@) or not event_watcher 132 | 133 | @__kb.store.reuse(@, kb.utils.resolveModel(new_model)) 134 | event_watcher.emitter(new_model); _model(event_watcher.ee) 135 | not event_watcher.ee or @createObservables(event_watcher.ee) 136 | } 137 | event_watcher = kb.utils.wrappedEventWatcher(@, new kb.EventWatcher(model, @, {emitter: @_model, update: (=> kb.ignore => not event_watcher?.ee or @createObservables(event_watcher?.ee))})) 138 | kb.utils.wrappedObject(@, model = event_watcher.ee); _model(event_watcher.ee) 139 | 140 | # update the observables 141 | @__kb.create_options = {store: kb.utils.wrappedStore(@), factory: kb.utils.wrappedFactory(@), path: @__kb.path, event_watcher: kb.utils.wrappedEventWatcher(@)} 142 | not options.requires or @createObservables(model, options.requires) 143 | not @__kb.internals or @createObservables(model, @__kb.internals) 144 | not options.mappings or @createObservables(model, options.mappings) 145 | not @__kb.statics or createStaticObservables(@, model) 146 | @createObservables(model, @__kb.keys) 147 | 148 | not kb.statistics or kb.statistics.register('ViewModel', @) # collect memory management statistics 149 | return @ 150 | 151 | # Required clean up function to break cycles, release view models, etc. 152 | # Can be called directly, via kb.release(object) or as a consequence of ko.releaseNode(element). 153 | destroy: -> 154 | @__kb_released = true 155 | (@__kb.view_model[vm_key] = null for vm_key of @__kb.vm_keys) if @__kb.view_model isnt @ # clear the external references 156 | @__kb.view_model = @__kb.create_options = null 157 | kb.releaseKeys(@) 158 | kb.utils.wrappedDestroy(@) 159 | 160 | not kb.statistics or kb.statistics.unregister('ViewModel', @) # collect memory management statistics 161 | 162 | # Get the options for a new view model that can be used for sharing view models. 163 | shareOptions: -> {store: kb.utils.wrappedStore(@), factory: kb.utils.wrappedFactory(@)} 164 | 165 | # create observables manually 166 | createObservables: (model, keys) -> 167 | if not keys 168 | return if @__kb.keys or not model # only use the keys provided 169 | createObservable(@, model, key, @__kb.create_options) for key of model.attributes 170 | (createObservable(@, model, key, @__kb.create_options) for key in rel_keys) if rel_keys = kb.settings.orm?.keys?(model) 171 | else if _.isArray(keys) 172 | createObservable(@, model, key, @__kb.create_options) for key in keys 173 | else 174 | for key, mapping_info of keys when vm_key = assignViewModelKey(@, key) 175 | mapping_info.key or= vm_key unless _.isString(mapping_info) 176 | @[vm_key] = @__kb.view_model[vm_key] = kb.observable(model, mapping_info, @__kb.create_options, @) 177 | return 178 | 179 | # Factory function to create a kb.ViewModel. 180 | kb.viewModel = (model, options, view_model) -> return new kb.ViewModel(arguments) -------------------------------------------------------------------------------- /src/defaults/default-observable.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_, ko} = kb = require '../core/kb' 11 | require './extensions' 12 | 13 | KEYS_PUBLISH = ['destroy', 'setToDefault'] 14 | 15 | # Used to provide a default value when an observable is null, undefined, or the empty string. 16 | # 17 | # @example Provide a observable with observable and/or non observable default argument in the form of: 18 | # var wrapped_name = kb.defaultObservable(kb.observable(model, 'name'), '(no name)'); 19 | module.exports = class kb.DefaultObservable 20 | 21 | # Used to create a new kb.DefaultObservable. 22 | # 23 | # @param [ko.observable] target_observable the observable to check for null, undefined, or the empty string 24 | # @param [Any] default_value the default value. Can be a value, string or ko.observable 25 | # @return [ko.observable] the constructor does not return 'this' but a ko.observable 26 | # @note the constructor does not return 'this' but a ko.observable 27 | constructor: (target_observable, @dv) -> # @dv is default value 28 | observable = kb.utils.wrappedObservable @, ko.computed { 29 | read: => 30 | current_target = ko.utils.unwrapObservable(target_observable()) 31 | return if _.isNull(current_target) or _.isUndefined(current_target) then ko.utils.unwrapObservable(@dv) else current_target 32 | write: (value) -> target_observable(value) 33 | } 34 | 35 | # publish public interface on the observable and return instead of this 36 | kb.publishMethods(observable, @, KEYS_PUBLISH) 37 | 38 | return observable 39 | 40 | # Required clean up function to break cycles, release view models, etc. 41 | # Can be called directly, via kb.release(object) or as a consequence of ko.releaseNode(element). 42 | destroy: -> kb.utils.wrappedDestroy(@) 43 | 44 | # Forces the observable to take the default value. 45 | # @note Can be used with kb.utils.setToDefault, kb.Observable.setToDefault, kb.ViewModel.setToDefault 46 | setToDefault: -> kb.utils.wrappedObservable(@)(@dv) 47 | 48 | kb.defaultObservable = (target, default_value) -> return new kb.DefaultObservable(target, default_value) 49 | kb.observableDefault = kb.defaultObservable 50 | -------------------------------------------------------------------------------- /src/defaults/extensions.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_, ko} = kb = require '../core/kb' 11 | 12 | kb.Observable::setToDefault = -> @__kb_value?.setToDefault?(); return 13 | kb.ViewModel::setToDefault = -> @[vm_key]?.setToDefault?() for vm_key of @__kb.vm_keys; return 14 | 15 | # @example 16 | # var model = new Backbone.Model({name: 'Bob'}); 17 | # var view_model = { 18 | # wrapped_name: kb.defaultWrapper(kb.observable(model, 'name'), '(no name)') 19 | # }; // view_model.wrapped name: Bob 20 | # kb.utils.setToDefault(view_model); // view_model.wrapped name: (no name) 21 | kb.utils.setToDefault = (obj) -> 22 | return unless obj 23 | 24 | # observable 25 | if ko.isObservable(obj) 26 | obj.setToDefault?() 27 | 28 | # view model 29 | else if _.isObject(obj) 30 | for key, value of obj 31 | @setToDefault(value) if value and (ko.isObservable(value) or (typeof(value) isnt 'function')) and ((key[0] isnt '_') or key.search('__kb')) 32 | return obj -------------------------------------------------------------------------------- /src/formatting/formatted-observable.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_, ko} = kb = require '../core/kb' 11 | 12 | arraySlice = Array.prototype.slice 13 | 14 | kb.toFormattedString = (format) -> 15 | result = format.slice() 16 | args = arraySlice.call(arguments, 1) 17 | for index, arg of args 18 | value = ko.utils.unwrapObservable(arg) 19 | value = '' if _.isUndefined(value) or _.isNull(value) 20 | 21 | parameter_index = format.indexOf("\{#{index}\}") 22 | while (parameter_index>=0) 23 | result = result.replace("{#{index}}", value) 24 | parameter_index = format.indexOf("\{#{index}\}", parameter_index+1) 25 | return result 26 | 27 | kb.parseFormattedString = (string, format) -> 28 | regex_string = format.slice(); index = 0; parameter_count = 0; positions = {} 29 | while(regex_string.search("\\{#{index}\\}")>=0) 30 | # store the positions of the replacements 31 | parameter_index = format.indexOf("\{#{index}\}") 32 | while (parameter_index>=0) 33 | regex_string = regex_string.replace("\{#{index}\}", '(.*)') 34 | positions[parameter_index] = index; parameter_count++ 35 | parameter_index = format.indexOf("\{#{index}\}", parameter_index+1) 36 | index++ 37 | count = index 38 | 39 | regex = new RegExp(regex_string) 40 | matches = regex.exec(string) 41 | matches.shift() if (matches) 42 | # return fake empty data 43 | if not matches or (matches.length isnt parameter_count) 44 | result = [] 45 | result.push('') while count-- > 0 46 | return result 47 | 48 | # sort the matches since the parameters could be requested unordered 49 | sorted_positions = _.sortBy(_.keys(positions), (parameter_index, format_index) -> return parseInt(parameter_index,10)) 50 | format_indices_to_matched_indices = {} 51 | for match_index, parameter_index of sorted_positions 52 | index = positions[parameter_index] 53 | continue if format_indices_to_matched_indices.hasOwnProperty(index) 54 | format_indices_to_matched_indices[index] = match_index 55 | 56 | results = []; index=0 57 | while (index 75 | # being called by the factory function 76 | if _.isArray(args) 77 | format = format 78 | observable_args = args 79 | else 80 | observable_args = arraySlice.call(arguments, 1) 81 | 82 | observable = kb.utils.wrappedObservable @, ko.computed { 83 | read: -> 84 | args = [ko.utils.unwrapObservable(format)] 85 | args.push(ko.utils.unwrapObservable(arg)) for arg in observable_args 86 | return kb.toFormattedString.apply(null, args) 87 | write: (value) -> 88 | matches = kb.parseFormattedString(value, ko.utils.unwrapObservable(format)) 89 | max_count = Math.min(observable_args.length, matches.length); index = 0 90 | while (index kb.utils.wrappedDestroy(@) 101 | 102 | kb.formattedObservable = (format, args) -> return new kb.FormattedObservable(format, arraySlice.call(arguments, 1)) 103 | kb.observableFormatted = kb.formattedObservable 104 | -------------------------------------------------------------------------------- /src/localization/localized-observable.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_, ko} = kb = require '../core/kb' 11 | 12 | KEYS_PUBLISH = ['destroy', 'observedValue', 'resetToCurrent'] 13 | 14 | # Locale Manager - if you are using localization, set this property. 15 | # It must have Backbone.Events mixed in and implement a get method like Backbone.Model, eg. get: (attribute_name) -> return somthing 16 | kb.locale_manager or= undefined 17 | 18 | # @abstract You must provide the following two methods: 19 | # * read: function(value, observable) called to get the value and each time the locale changes 20 | # * write: function(localized_string, value, observable) called to set the value (optional) 21 | # 22 | # Base class for observing localized data that changes when the locale changes. 23 | # 24 | # @example How to create a ko.CollectionObservable using the ko.collectionObservable factory. 25 | # kb.ShortDateLocalizer = kb.LocalizedObservable.extend({ 26 | # constructor: function(value, options, view_model) { 27 | # kb.LocalizedObservable.prototype.constructor.apply(this, arguments); 28 | # return kb.utils.wrappedObservable(this); 29 | # }, 30 | # read: function(value) { 31 | # return Globalize.format(value, Globalize.cultures[kb.locale_manager.getLocale()].calendars.standard.patterns.d, kb.locale_manager.getLocale()); 32 | # }, 33 | # write: function(localized_string, value) { 34 | # var new_value; 35 | # new_value = Globalize.parseDate(localized_string, Globalize.cultures[kb.locale_manager.getLocale()].calendars.standard.patterns.d, kb.locale_manager.getLocale()); 36 | # if (!(new_value && _.isDate(new_value))) { 37 | # return kb.utils.wrappedObservable(this).resetToCurrent(); 38 | # } 39 | # return value.setTime(new_value.valueOf()); 40 | # } 41 | # }); 42 | # var ViewModel = function(model) { 43 | # this.localized_date = kb.observable(model, { 44 | # key: 'date', 45 | # 'default': this.loading_message, 46 | # localizer: ShortDateLocalizer 47 | # }, this); 48 | # }; 49 | # var view_model = new ViewModel(new Backbone.Model({date: new Date()})); 50 | # 51 | # @method .extend(prototype_properties, class_properties) 52 | # Class method for JavaScript inheritance. 53 | # @param [Object] prototype_properties the properties to add to the prototype 54 | # @param [Object] class_properties the properties to add to the class 55 | # @return [ko.observable] the constructor does not return 'this' but a ko.observable 56 | # @example 57 | # var MyLocalizedObservable = kb.LocalizedObservable.extend({ 58 | # constructor: function(value, options, view_model) { 59 | # // the constructor does not return 'this' but a ko.observable 60 | # return kb.LocalizedObservable.prototype.constructor.apply(this, arguments); 61 | # } 62 | # }); 63 | module.exports = class kb.LocalizedObservable 64 | @extend = kb.extend # for Backbone non-Coffeescript inheritance (use "kb.SuperClass.extend({})" in Javascript instead of "class MyClass extends kb.SuperClass") 65 | 66 | # Used to create a new kb.LocalizedObservable. This an abstract class. 67 | # 68 | # @param [Data|ko.observable] value the value to localize 69 | # @param [Object] options the create options 70 | # @option options [Data|ko.observable] default a default value to present when the value is null, an empty string, etc. 71 | # @option options [Function] onChange a notification that gets called when the locale changes. Signature: function(localized_string, value, observable) 72 | # @return [ko.observable] the constructor does not return 'this' but a ko.observable 73 | # @note the constructor does not return 'this' but a ko.observable 74 | constructor: (@value, options, @vm) -> # @vm is view_model 75 | options or= {}; @vm or= {} 76 | @read or kb._throwMissing(this, 'read') 77 | kb.locale_manager or kb._throwMissing(this, 'kb.locale_manager') 78 | 79 | # bind callbacks 80 | @__kb or= {} 81 | @__kb._onLocaleChange = _.bind(@_onLocaleChange, @) 82 | @__kb._onChange = options.onChange 83 | 84 | # internal state 85 | value = ko.utils.unwrapObservable(@value) if @value 86 | @vo = ko.observable(if not value then null else @read(value, null)) 87 | observable = kb.utils.wrappedObservable @, ko.computed { 88 | read: => 89 | ko.utils.unwrapObservable(@value) if @value 90 | @vo() # create a depdenency 91 | return @read(ko.utils.unwrapObservable(@value)) 92 | 93 | write: (value) => 94 | @write or kb._throwUnexpected(@, 'writing to read-only') 95 | @write(value, ko.utils.unwrapObservable(@value)) 96 | @vo(value) 97 | @__kb._onChange(value) if @__kb._onChange 98 | 99 | owner: @vm 100 | } 101 | 102 | # publish public interface on the observable and return instead of this 103 | kb.publishMethods(observable, @, KEYS_PUBLISH) 104 | 105 | # start 106 | kb.locale_manager.bind('change', @__kb._onLocaleChange) 107 | 108 | # wrap ourselves with a default value 109 | observable = kb.DefaultObservable and ko.defaultObservable(observable, options.default) if options.hasOwnProperty('default') 110 | 111 | return observable 112 | 113 | # Required clean up function to break cycles, release view models, etc. 114 | # Can be called directly, via kb.release(object) or as a consequence of ko.releaseNode(element). 115 | destroy: -> 116 | kb.locale_manager.unbind('change', @__kb._onLocaleChange) 117 | @vm = null 118 | kb.utils.wrappedDestroy(@) 119 | 120 | # Used to reset the value if localization is not possible. 121 | resetToCurrent: -> 122 | observable = kb.utils.wrappedObservable(@) 123 | current_value = if @value then @read(ko.utils.unwrapObservable(@value)) else null 124 | return if observable() is current_value 125 | observable(current_value) 126 | 127 | # Dual purpose set/get 128 | observedValue: (value) -> 129 | return @value if arguments.length == 0 130 | @value = value; @_onLocaleChange() 131 | return 132 | 133 | #################################################### 134 | # Internal 135 | #################################################### 136 | 137 | # @nodoc 138 | _onLocaleChange: -> 139 | value = @read(ko.utils.unwrapObservable(@value)) 140 | @vo(value) 141 | @__kb._onChange(value) if @__kb._onChange 142 | 143 | # factory function 144 | kb.localizedObservable = (value, options, view_model) -> return new kb.LocalizedObservable(value, options, view_model) 145 | kb.observableLocalized = kb.localizedObservable 146 | -------------------------------------------------------------------------------- /src/triggering/triggered-observable.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_, ko} = kb = require '../core/kb' 11 | 12 | KEYS_PUBLISH = ['destroy'] 13 | 14 | # Class for observing emitter events. 15 | # 16 | # @example create an observable whose subscriptions are notified with the change event is triggered. 17 | # var triggered_observable = kb.triggeredObservable(name, 'change'); 18 | # 19 | # @example How to watch a emitter for events. 20 | # var trigger_count = 0; 21 | # var emitter = new Backbone.Model(); 22 | # var view_emitter = { 23 | # triggered_observable: kb.triggeredObservable(emitter, 'change') 24 | # }; 25 | # view_emitter.counter = ko.computed(function() { 26 | # view_emitter.triggered_observable() // add a dependency 27 | # return trigger_count++ 28 | # }); 29 | # emitter.set(name: 'bob'); # trigger_count: 1 30 | # emitter.set(name: 'george'); # trigger_count: 2 31 | # emitter.set(last: 'smith'); # trigger_count: 3 32 | module.exports = class kb.TriggeredObservable 33 | 34 | # Used to create a new kb.Observable. 35 | # 36 | # @param [Model] emitter the emitter to observe (can be null) 37 | # @param [String] event_selector the event name to trigger Knockout subscriptions on. 38 | # @return [ko.observable] the constructor does not return 'this' but a ko.observable 39 | # @note the constructor does not return 'this' but a ko.observable 40 | constructor: (emitter, @event_selector) -> 41 | emitter or kb._throwMissing(this, 'emitter') 42 | @event_selector or kb._throwMissing(this, 'event_selector') 43 | 44 | # internal state 45 | @vo = ko.observable() 46 | observable = kb.utils.wrappedObservable(@, ko.computed(=> @vo())) 47 | 48 | # publish public interface on the observable and return instead of this 49 | kb.publishMethods(observable, @, KEYS_PUBLISH) 50 | 51 | # create emitter observable 52 | kb.utils.wrappedEventWatcher(@, new kb.EventWatcher(emitter, @, {emitter: _.bind(@emitter, @), update: _.bind(@update, @), event_selector: @event_selector})) 53 | 54 | return observable 55 | 56 | # Required clean up function to break cycles, release view models, etc. 57 | # Can be called directly, via kb.release(object) or as a consequence of ko.releaseNode(element). 58 | destroy: -> kb.utils.wrappedDestroy(@) 59 | 60 | # Dual-purpose getter/setter for the observed emitter. 61 | # 62 | # @overload emitter() 63 | # Gets the emitter or emitter reference 64 | # @return [Model|ModelRef|Collection] the emitter whose events are being bound (can be null) 65 | # @overload emitter(new_emitter) 66 | # Sets the emitter or emitter reference 67 | # @param [Model|ModelRef|Collection] new_emitter the emitter whose events will be bound (can be null) 68 | emitter: (new_emitter) -> 69 | # get or no change 70 | return @ee if (arguments.length == 0) or (@ee is new_emitter) 71 | @update() if (@ee = new_emitter) 72 | 73 | #################################################### 74 | # Internal 75 | #################################################### 76 | # @nodoc 77 | update: -> 78 | return unless @ee # do not trigger if there is no emitter 79 | if @vo() isnt @ee then @vo(@ee) else @vo.valueHasMutated() # manually trigger the dependable 80 | 81 | # factory function 82 | kb.triggeredObservable = (emitter, event_selector) -> return new kb.TriggeredObservable(emitter, event_selector) 83 | kb.observableTriggered = kb.triggeredObservable 84 | -------------------------------------------------------------------------------- /src/validation/validators.coffee: -------------------------------------------------------------------------------- 1 | ### 2 | knockback.js 1.2.3 3 | Copyright (c) 2011-2016 Kevin Malakoff. 4 | License: MIT (http://www.opensource.org/licenses/mit-license.php) 5 | Source: https://github.com/kmalakoff/knockback 6 | Dependencies: Knockout.js, Backbone.js, and Underscore.js (or LoDash.js). 7 | Optional dependencies: Backbone.ModelRef.js and BackboneORM. 8 | ### 9 | 10 | {_, ko} = kb = require '../core/kb' 11 | 12 | # Regular expressions from Angular.js: https://github.com/angular/angular.js 13 | URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/ 14 | EMAIL_REGEXP = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/ 15 | NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/ 16 | 17 | # A validator should return true if there are errors (similar to the binding check in HTML, eg. $name().required). 18 | kb.valid = 19 | required: (value) -> !value 20 | url: (value) -> !URL_REGEXP.test(value) 21 | email: (value) -> !EMAIL_REGEXP.test(value) 22 | number: (value) -> !NUMBER_REGEXP.test(value) 23 | 24 | # Convention is that if they end in Fn then returns a function pointer based on parameters passed. 25 | kb.hasChangedFn = (model) -> 26 | m = null; attributes = null 27 | return -> 28 | if m isnt (current_model = ko.utils.unwrapObservable(model)) # change in model 29 | m = current_model 30 | attributes = (if m then m.toJSON() else null) 31 | return false 32 | return false if not (m and attributes) 33 | return !_.isEqual(m.toJSON(), attributes) 34 | 35 | kb.minLengthFn = (length) -> 36 | return (value) -> return not value or value.length < length 37 | 38 | kb.uniqueValueFn = (model, key, collection) -> 39 | return (value) -> 40 | m = ko.utils.unwrapObservable(model); k = ko.utils.unwrapObservable(key); c = ko.utils.unwrapObservable(collection) 41 | return false if not (m and k and c) 42 | return !!_.find(c.models, (test) => (test isnt m) and test.get(k) is value) 43 | 44 | kb.untilTrueFn = (stand_in, fn, model) -> 45 | was_true = false 46 | model.subscribe(-> was_true = false) if model and ko.isObservable(model) # reset if the model changes 47 | return (value) -> 48 | return ko.utils.unwrapObservable(stand_in) unless (f = ko.utils.unwrapObservable(fn)) 49 | was_true |= !!(result = f(ko.utils.unwrapObservable(value))) 50 | return (if was_true then result else ko.utils.unwrapObservable(stand_in)) 51 | 52 | kb.untilFalseFn = (stand_in, fn, model) -> 53 | was_false = false 54 | model.subscribe(-> was_false = false) if model and ko.isObservable(model) # reset if the model changes 55 | return (value) -> 56 | return ko.utils.unwrapObservable(stand_in) unless (f = ko.utils.unwrapObservable(fn)) 57 | was_false |= !(result = f(ko.utils.unwrapObservable(value))) 58 | return (if was_false then result else ko.utils.unwrapObservable(stand_in)) -------------------------------------------------------------------------------- /test/issues/2013_04_04/issue_2013_04_04.coffee: -------------------------------------------------------------------------------- 1 | window.SingleExecutionViewModel = kb.ViewModel.extend({ 2 | constructor : (model,options)-> 3 | kb.ViewModel.prototype.constructor.call(@,model,{},options ) 4 | }) 5 | 6 | window.SingleExecutionCollection = kb.CollectionObservable.extend({ 7 | constructor: (collection, options) -> 8 | return kb.CollectionObservable.prototype.constructor.call(@, collection, {view_model: SingleExecutionViewModel, options: options}) 9 | }) 10 | 11 | 12 | window.WorkspaceViewModel = kb.ViewModel.extend({ 13 | constructor: (model,options)-> 14 | kb.ViewModel.prototype.constructor.call(@,model,{ 15 | requires: ['singleExecutions'] 16 | factories: 17 | 'singleExecutions': SingleExecutionCollection 18 | },options) 19 | console.log("calling workspace view model") 20 | return 21 | 22 | 23 | init: -> 24 | @subscribeEvents() 25 | 26 | subscribeEvents: -> @singleExecutions.subscribe( (executionViewModels) => alert(executionViewModels) ) 27 | }) 28 | 29 | class window.Workspace1 extends Backbone.Model 30 | defaults: 31 | singleExecutions: new Backbone.Collection() 32 | batchExecutions: new Backbone.Collection() 33 | 34 | constructor: -> super() 35 | 36 | workspace = new Workspace1() 37 | workspaceViewModel= new WorkspaceViewModel(workspace) 38 | workspaceViewModel.init() 39 | workspace.get('singleExecutions').add(new Backbone.Model()) -------------------------------------------------------------------------------- /test/issues/2013_04_04/issue_2013_04_04.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 2013_04_04 Test 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/issues/2013_04_04/issue_2013_04_04.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.2 2 | (function() { 3 | var workspace, workspaceViewModel, 4 | __hasProp = {}.hasOwnProperty, 5 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 6 | 7 | window.SingleExecutionViewModel = kb.ViewModel.extend({ 8 | constructor: function(model, options) { 9 | return kb.ViewModel.prototype.constructor.call(this, model, {}, options); 10 | } 11 | }); 12 | 13 | window.SingleExecutionCollection = kb.CollectionObservable.extend({ 14 | constructor: function(collection, options) { 15 | return kb.CollectionObservable.prototype.constructor.call(this, collection, { 16 | view_model: SingleExecutionViewModel, 17 | options: options 18 | }); 19 | } 20 | }); 21 | 22 | window.WorkspaceViewModel = kb.ViewModel.extend({ 23 | constructor: function(model, options) { 24 | kb.ViewModel.prototype.constructor.call(this, model, { 25 | requires: ['singleExecutions'], 26 | factories: { 27 | 'singleExecutions': SingleExecutionCollection 28 | } 29 | }, options); 30 | console.log("calling workspace view model"); 31 | }, 32 | init: function() { 33 | return this.subscribeEvents(); 34 | }, 35 | subscribeEvents: function() { 36 | var _this = this; 37 | 38 | return this.singleExecutions.subscribe(function(executionViewModels) { 39 | return alert(executionViewModels); 40 | }); 41 | } 42 | }); 43 | 44 | window.Workspace1 = (function(_super) { 45 | __extends(Workspace1, _super); 46 | 47 | Workspace1.prototype.defaults = { 48 | singleExecutions: new Backbone.Collection(), 49 | batchExecutions: new Backbone.Collection() 50 | }; 51 | 52 | function Workspace1() { 53 | Workspace1.__super__.constructor.call(this); 54 | } 55 | 56 | return Workspace1; 57 | 58 | })(Backbone.Model); 59 | 60 | workspace = new Workspace1(); 61 | 62 | workspaceViewModel = new WorkspaceViewModel(workspace); 63 | 64 | workspaceViewModel.init(); 65 | 66 | workspace.get('singleExecutions').add(new Backbone.Model()); 67 | 68 | }).call(this); 69 | -------------------------------------------------------------------------------- /test/issues/issue141/issue.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 141 Test 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
...
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/issues/issue141/issue.js: -------------------------------------------------------------------------------- 1 | var Model = Backbone.RelationalModel.extend({ 2 | url: '...' 3 | }); 4 | 5 | 6 | var ListModel = Backbone.RelationalModel.extend({ 7 | url: '...', 8 | relations: [{ 9 | type: Backbone.HasMany, 10 | key: 'coll', 11 | relatedModel: Model 12 | } 13 | ] 14 | }); 15 | 16 | ModelViewModel = kb.ViewModel.extend({ 17 | constructor: function (model) { 18 | kb.ViewModel.prototype.constructor.apply(this, arguments); 19 | this.cancel = function () {this.model().destroy();} 20 | } 21 | }); 22 | 23 | var listModel = new ListModel(); 24 | listModel.get('coll').reset([new Model(), new Model()]); 25 | 26 | var viewModel = new kb.viewModel(listModel, { 27 | factories: { 28 | 'coll.models': ModelViewModel 29 | } 30 | }); 31 | 32 | 33 | ko.applyBindings(viewModel, $('#some-div')[0]); 34 | -------------------------------------------------------------------------------- /test/issues/issue145/issue.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 141 Test 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/issues/issue145/issue.js: -------------------------------------------------------------------------------- 1 | // MODIFIED from: https://stackoverflow.com/questions/15111361/backbone-localstorage-saves-but-does-not-fetch-collection-using-knockback 2 | 3 | var Player = Backbone.Model.extend({ 4 | urlRoot: '/players', 5 | localStorage: new Backbone.LocalStorage('players'), 6 | defaults: {name: 'Unnamed', team: 'Unassigned'} 7 | }); 8 | var Players = Backbone.Collection.extend({ 9 | url: '/players', 10 | model: Player, 11 | localStorage: new Backbone.LocalStorage('players') 12 | }); 13 | 14 | function GameViewModel() { 15 | var self = this; 16 | 17 | this.players = kb.collectionObservable(new Players()); 18 | 19 | console.log('BEFORE fetch: ', self.players.collection().models); 20 | this.players.collection().fetch({success: function() { 21 | console.log('AFTER fetch: ', self.players.collection().models); 22 | }}); 23 | }; 24 | 25 | // save one if none exist yet 26 | var players = new Players(); 27 | if (!players.localStorage.findAll().length) { 28 | new Player().save({name: _.uniqueId('Player ')}); 29 | } 30 | 31 | ko.applyBindings(new GameViewModel()); 32 | -------------------------------------------------------------------------------- /test/issues/issue157/issue.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 157 Test 10 | 11 | 12 | 13 | 14 |
15 | 16 |

17 | 18 | This collection is sortable:
19 |
    20 |
  • 21 | 22 |
  • 23 |
24 | 25 | This is a clone which should stay the same:
26 |
    27 |
  • 28 | 29 |
  • 30 |
31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /test/issues/issue157/issue.js: -------------------------------------------------------------------------------- 1 | var SortItemModel = Backbone.Model.extend({}); 2 | 3 | var ViewModel = kb.ViewModel.extend({ 4 | i: 1, 5 | 6 | addItem: function () 7 | { 8 | var obj = {name: "test" + (this.i++)}; 9 | var line = new SortItemModel(obj); 10 | this.model().get('lines').push(line); 11 | console.log("CollectionObservable length:", this.lines().length); 12 | console.log("Collection length:", this.model().get('lines').length); 13 | } 14 | }); 15 | 16 | ko.applyBindings(view_model, $('#some-div')[0]); 17 | -------------------------------------------------------------------------------- /test/issues/issue161/issue-0.19.0.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 161 Test 10 | 11 | 12 | 13 | 14 |
15 |

Parent

16 | 17 | 18 |

Children

19 |
    20 |
  • 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/issues/issue161/issue-0.19.0.js: -------------------------------------------------------------------------------- 1 | var children = new Backbone.Collection([ 2 | new Backbone.Model({name:"Charles"}), 3 | new Backbone.Model({name:"Eve"}) 4 | ]); 5 | 6 | var parent = new Backbone.Model({ 7 | name:"Bob", children:children 8 | }); 9 | 10 | var subFactory = function (model) { 11 | var subVm = new kb.ViewModel(model); 12 | subVm.cid = kb.ko.computed(function () { 13 | return model.cid; 14 | }); 15 | return subVm; 16 | }; 17 | 18 | var vm = new kb.ViewModel(null, {excludes : ["children"]}); 19 | 20 | vm.shareOptions().factory.addPathMapping("children.models", subFactory); 21 | vm.createObservables(null, ["children"]); 22 | 23 | vm.model(parent); 24 | 25 | ko.applyBindings(vm, document.getElementById("some-div")); 26 | 27 | console.log(vm.name()); 28 | console.log(vm.children()[0].cid()); 29 | -------------------------------------------------------------------------------- /test/issues/issue161/issue.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 161 Test 10 | 11 | 12 | 13 | 14 |
15 |

Parent

16 | 17 | 18 |

Children

19 |
    20 |
  • 21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/issues/issue161/issue.js: -------------------------------------------------------------------------------- 1 | var children = new Backbone.Collection([ 2 | new Backbone.Model({name:"Charles"}), 3 | new Backbone.Model({name:"Eve"}) 4 | ]); 5 | 6 | var parent = new Backbone.Model({ 7 | name:"Bob", children:children 8 | }); 9 | 10 | var subFactory = function (model) { 11 | var subVm = new kb.ViewModel(model); 12 | subVm.cid = kb.ko.computed(function () { 13 | return model.cid; 14 | }); 15 | return subVm; 16 | }; 17 | 18 | var vm = new kb.ViewModel(null, { 19 | excludes : ["children"], 20 | factories: {"children.models": subFactory} 21 | }); 22 | 23 | // Passing in parent instead of null works but in my case I don't have parent 24 | // here so I must be able to set it later using vm.model(parent). 25 | vm.children = kb.observable(null, "children", vm.shareOptions()); 26 | 27 | vm.model(parent); 28 | 29 | ko.applyBindings(vm, document.getElementById("some-div")); 30 | 31 | console.log(vm.name()); 32 | console.log(vm.children()[0].cid()); 33 | -------------------------------------------------------------------------------- /test/issues/issue34/issue34.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 34 Test 10 | 11 | 12 | 13 | 14 |
    15 | 16 |
  • 17 | 18 | by 19 | 20 |
  • 21 | 22 | 23 |
  • 24 | 25 | 26 | 27 |
  • 28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/issues/issue34/issue34.js: -------------------------------------------------------------------------------- 1 | var 2 | Book = Backbone.RelationalModel.extend({ 3 | defaults:{ 4 | name: 'untitled' 5 | }, 6 | idAttribute: "_id" 7 | }), 8 | Author = Backbone.RelationalModel.extend({ 9 | defaults:{ 10 | name: 'untitled' 11 | }, 12 | idAttribute: "_id", 13 | relations:[ 14 | { 15 | type: 'HasMany', 16 | key: 'books', 17 | relatedModel: Book, 18 | includeInJSON: "_id", 19 | reverseRelation: { 20 | key: 'author', 21 | includeInJSON: "_id" 22 | }} 23 | ] 24 | }), 25 | BookStore = Backbone.RelationalModel.extend({ 26 | relations:[ 27 | { 28 | type: 'HasMany', 29 | key: 'books', 30 | relatedModel: Book 31 | }, 32 | { 33 | type: 'HasMany', 34 | key: 'authors', 35 | relatedModel: Author 36 | } 37 | ] 38 | }); 39 | 40 | Author.setup(); 41 | BookStore.setup(); 42 | 43 | 44 | var 45 | bs = new BookStore({ 46 | books:[ 47 | { 48 | _id:"b1", 49 | name: "Book One", 50 | author: "a1" 51 | }, 52 | { 53 | _id:"b2", 54 | name: "Book Two", 55 | author: "a1" // does not work 56 | // author: "a2" // works 57 | }, 58 | ], 59 | authors:[ 60 | { 61 | name: 'fred', 62 | _id: "a1", 63 | books: ["b1","b2"] // does not work 64 | // books: ["b1"] // works 65 | }, 66 | { 67 | name: 'ted', 68 | _id: "a2", 69 | books: [] // does not work 70 | // books: ["b2"] // works 71 | 72 | } 73 | ] 74 | }); 75 | 76 | var bookViewModel = kb.ViewModel.extend({ 77 | constructor:function(model){ 78 | kb.ViewModel.prototype.constructor.apply(this, arguments); 79 | this.editMode = ko.observable(); 80 | this.author = ko.computed(function(){ 81 | 82 | var a = model.get('author'); 83 | if(a) return a.get('name'); 84 | return null; 85 | }); 86 | this.edit = function(){ 87 | model._save = model.toJSON(); 88 | this.editMode(true); 89 | }; 90 | this.confirm = function(){ 91 | model._save = null; 92 | this.editMode(false); 93 | }; 94 | this.cancel= function(){ 95 | model.set(model._save); 96 | this.editMode(false); 97 | }; 98 | } 99 | }); 100 | 101 | function bookVmcreate(model){ 102 | return { 103 | name: kb.observable(model,'name'), 104 | author: ko.computed(function(){ 105 | var a = model.get('author'); 106 | if(a) return a.get('name'); 107 | return null; 108 | }), 109 | editMode: ko.observable(), 110 | edit:function(){ 111 | model._save = model.toJSON(); 112 | this.editMode(true);}, 113 | confirm:function(){ 114 | model._save = null; 115 | this.editMode(false);}, 116 | cancel:function(){ 117 | model.set(model._save); 118 | this.editMode(false); 119 | } 120 | }; 121 | }; 122 | 123 | var view_model = { books: kb.collectionObservable(bs.get('books'), { 124 | view_model: bookViewModel, 125 | factories: { 126 | 'models.author.books.models': bookViewModel 127 | } 128 | }) }; // does not work when two books refer same author 129 | //view_model_create: bookVmcreate}) }; // fixes not working 130 | ko.applyBindings(view_model); 131 | -------------------------------------------------------------------------------- /test/issues/issue35/issue35.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 35 Test 10 | 11 | 12 | 13 | 14 |
    15 | 16 |
  • 17 | 18 | by 19 | 20 |
  • 21 | 22 | 23 |
  • 24 | 25 | 29 | 30 | 31 |
  • 32 | 33 |
34 | 35 |
    36 |
  • 37 | 38 | 43 |
  • 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /test/issues/issue35/issue35.js: -------------------------------------------------------------------------------- 1 | var 2 | Book = Backbone.RelationalModel.extend({ 3 | defaults:{ 4 | name: 'untitled' 5 | }, 6 | idAttribute: "_id" 7 | }), 8 | Author = Backbone.RelationalModel.extend({ 9 | defaults:{ 10 | name: 'untitled' 11 | }, 12 | idAttribute: "_id", 13 | relations:[ 14 | { 15 | type: 'HasMany', 16 | key: 'books', 17 | relatedModel: Book, 18 | includeInJSON: "_id", 19 | reverseRelation: { 20 | key: 'author', 21 | includeInJSON: "_id" 22 | }} 23 | ] 24 | }), 25 | BookStore = Backbone.RelationalModel.extend({ 26 | relations:[ 27 | { 28 | type: 'HasMany', 29 | key: 'books', 30 | relatedModel: Book 31 | }, 32 | { 33 | type: 'HasMany', 34 | key: 'authors', 35 | relatedModel: Author 36 | } 37 | ] 38 | }); 39 | 40 | Author.setup(); 41 | BookStore.setup(); 42 | 43 | 44 | var 45 | bs = new BookStore({ 46 | books:[ 47 | { 48 | _id:"b1", 49 | name: "Book One", 50 | author: "a1" 51 | }, 52 | { 53 | _id:"b2", 54 | name: "Book Two", 55 | author: "a1" // does not work 56 | // author: "a2" // works 57 | }, 58 | ], 59 | authors:[ 60 | { 61 | name: 'fred', 62 | _id: "a1", 63 | books: ["b1","b2"] // does not work 64 | // books: ["b1"] // works 65 | }, 66 | { 67 | name: 'ted', 68 | _id: "a2", 69 | books: [] // does not work 70 | // books: ["b2"] // works 71 | 72 | } 73 | ] 74 | }); 75 | 76 | function bookVmcreate(model){ 77 | var vm = { 78 | name: kb.observable(model,'name'), 79 | author: kb.observable(model,'author'), 80 | editMode: ko.observable(), 81 | edit:function(){ 82 | model._save = model.toJSON(); 83 | this.editMode(true);}, 84 | confirm:function(){ 85 | model._save = null; 86 | this.editMode(false);}, 87 | cancel:function(){ 88 | model.set(model._save); 89 | this.editMode(false); 90 | }, 91 | model: model 92 | }; 93 | vm.authorName = vm.author().name 94 | return vm; 95 | }; 96 | 97 | function authorVMcreate(model){ 98 | return { 99 | name: kb.observable(model,'name'), 100 | books: kb.collectionObservable(model.get('books'), {models_only: true}), 101 | // books: kb.observable(model,{ 102 | // key: 'books', 103 | // read: function(){ 104 | // return model.get('books').models; 105 | // }, 106 | // write: function(val){ 107 | // model.get('books').reset(val); 108 | // } 109 | // }), 110 | model: model 111 | 112 | } 113 | } 114 | 115 | view_model = { books: kb.collectionObservable(bs.get('books'), { 116 | create: bookVmcreate}) , 117 | authors: kb.collectionObservable(bs.get('authors'), { 118 | create: authorVMcreate}), 119 | showBS: function(){ 120 | alert(JSON.stringify(bs.toJSON())); 121 | }, 122 | changeInCode:function(){ 123 | bs.get('books').at(0).set('author',bs.get('authors').at(1)); 124 | } 125 | }; 126 | ko.applyBindings(view_model); -------------------------------------------------------------------------------- /test/issues/issue37/issue37.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 37 Test 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 |
18 | 19 |
20 |
    21 |
  • 22 |
23 | 24 |
25 | 26 |
​ 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/issues/issue37/issue37.js: -------------------------------------------------------------------------------- 1 | var SubThing = Backbone.Model.extend({}), 2 | SubThings = Backbone.Collection.extend({ 3 | model:SubThing 4 | }), 5 | Thing = Backbone.Model.extend({ 6 | initialize: function() { 7 | this.subthings = new SubThings; 8 | } 9 | }), 10 | Things = Backbone.Collection.extend({ 11 | model:Thing 12 | }); 13 | 14 | var things = new Things([ 15 | {'name':'Thing One', 'id':1}, {'name':'Thing Two', 'id':2} 16 | ]), 17 | thing = new Thing(); 18 | 19 | var SubthingViewModel = function(model) { 20 | this.id = kb.observable(model, 'id'); 21 | this.name = kb.observable(model, 'name'); 22 | }; 23 | 24 | var ThingViewModel = function(model) { 25 | this.subthings = kb.collectionObservable(model.subthings, { 26 | view_model: SubthingViewModel 27 | }); 28 | this.name = kb.observable(model, 'name'); 29 | 30 | this.allThings = kb.collectionObservable(things, { 31 | view_model: SubthingViewModel 32 | }); 33 | 34 | this.onSubmit = function(el) { 35 | document.getElementById('theJson').innerText = JSON.stringify(thing.subthings.toJSON()); 36 | console.log(this.subthings()); 37 | }; 38 | }; 39 | 40 | var pageViewModel = { 41 | thing: new ThingViewModel(thing), 42 | 43 | } 44 | ko.applyBindings(pageViewModel); -------------------------------------------------------------------------------- /test/issues/issue40/require_fixed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 34 Test 10 | 11 | 12 | 13 | 14 | 15 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/issues/issue40/require_problem.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 34 Test 10 | 11 | 12 | 13 | 14 | 15 | 42 | 43 | 44 | 45 | ​ -------------------------------------------------------------------------------- /test/issues/issue44/issue44.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 34 Test 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/issues/issue44/issue44.js: -------------------------------------------------------------------------------- 1 | var model = new Backbone.Model({ value : 5 }); 2 | 3 | var viewModel = kb.viewModel(model); 4 | 5 | viewModel.countDown = function () { 6 | model.set("value", model.get("value") - 1); 7 | }; 8 | viewModel.reset = function () { 9 | model.set("value", 5); 10 | }; 11 | 12 | ko.applyBindings(viewModel, document.body); -------------------------------------------------------------------------------- /test/issues/issue46/issue46.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 46 Test 10 | 11 | 12 | 13 | 14 |
15 | 16 |
    17 |
  • 18 | * 19 | 20 | has age: 21 | 22 |
  • 23 |
24 | 25 |
26 | Clear Selection 27 |

28 | 29 |
30 | You have selected: 31 | and his age is 32 |
33 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/issues/issue46/issue46.js: -------------------------------------------------------------------------------- 1 | var bob = new Backbone.Model({id: 1, name:'bob', age:20}); 2 | var fred = new Backbone.Model({id: 2, name:'fred', age:30}); 3 | var alice = new Backbone.Model({id: 3, name:'alice', age:34}); 4 | var people = new Backbone.Collection([bob,fred,alice]); 5 | 6 | var inviteForm = new Backbone.Model({ 7 | people: people, 8 | invite: bob 9 | }); 10 | 11 | var MyViewModel = function(model) { 12 | this.people = kb.observable(model, 'people'); 13 | this.invite = kb.observable(model, 'invite'); 14 | 15 | this.clearSelection = function() { 16 | model.set("invite", null); 17 | }; 18 | 19 | this.setSelection = function() { 20 | model.set("invite", this.model()); 21 | }; 22 | }; 23 | 24 | $(function() { 25 | window.vm = new MyViewModel(inviteForm); 26 | ko.applyBindings(window.vm); 27 | }); 28 | -------------------------------------------------------------------------------- /test/issues/issue47/issue47.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 47 Test 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/issues/issue47/issue47.js: -------------------------------------------------------------------------------- 1 | var ForumTopic = Backbone.RelationalModel.extend({}); 2 | 3 | var Forum = Backbone.RelationalModel.extend({ 4 | urlRoot: '/forums', 5 | relations: [{ 6 | type: Backbone.HasMany, 7 | key: 'topics', 8 | relatedModel: 'ForumTopic', 9 | collectionType: 'ForumTopicCollection', 10 | reverseRelation: { 11 | key: 'forum', 12 | keySource: 'forum_id', 13 | includeInJSON: 'id' 14 | } 15 | }]}); 16 | var ForumViewModel = kb.ViewModel.extend({ 17 | constructor: function(model, options, view_model) { 18 | kb.ViewModel.prototype.constructor.call(this, model, {requires: ['id'], options: options}); 19 | this.forum_link = ko.computed(function() { 20 | return '#forum/' + this.id(); 21 | }, this); 22 | 23 | return this; 24 | } 25 | }); 26 | 27 | var TopicViewModel = kb.ViewModel.extend({ 28 | //new functions definitions, non override any existing functions 29 | }); 30 | 31 | var topic = new ForumTopic(); 32 | var view_model = new TopicViewModel(topic, { 33 | requires: ['title', 'body'], 34 | factories: { 35 | 'forum': ForumViewModel 36 | } 37 | }); 38 | 39 | ko.applyBindings(view_model); -------------------------------------------------------------------------------- /test/issues/issue89/issue89.coffee: -------------------------------------------------------------------------------- 1 | class RecordViewModel extends kb.ViewModel 2 | constructor: (model, options={}) -> 3 | common_requires = ['_selected', '_for_delete', 'active', 'name', 'type', '_change_type', 'zone', 'value'] 4 | _.extend(options, {requires: common_requires}) 5 | super(model, options) 6 | 7 | class Record extends Backbone.Model 8 | class RecordCollection extends Backbone.Collection 9 | model: Record 10 | 11 | records = kb.collectionObservable(new RecordCollection([new Record({id: 1}), new Record({id: 2})]), {view_model: RecordViewModel}) 12 | _.last(records()).destroy() 13 | 14 | kb.release(records) -------------------------------------------------------------------------------- /test/issues/issue89/issue89.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 47 Test 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/issues/issue89/issue89.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | (function() { 3 | var Record, RecordCollection, RecordViewModel, records, _ref, _ref1, 4 | __hasProp = {}.hasOwnProperty, 5 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 6 | 7 | RecordViewModel = (function(_super) { 8 | __extends(RecordViewModel, _super); 9 | 10 | function RecordViewModel(model, options) { 11 | var common_requires; 12 | if (options == null) { 13 | options = {}; 14 | } 15 | common_requires = ['_selected', '_for_delete', 'active', 'name', 'type', '_change_type', 'zone', 'value']; 16 | _.extend(options, { 17 | requires: common_requires 18 | }); 19 | RecordViewModel.__super__.constructor.call(this, model, options); 20 | } 21 | 22 | return RecordViewModel; 23 | 24 | })(kb.ViewModel); 25 | 26 | Record = (function(_super) { 27 | __extends(Record, _super); 28 | 29 | function Record() { 30 | _ref = Record.__super__.constructor.apply(this, arguments); 31 | return _ref; 32 | } 33 | 34 | return Record; 35 | 36 | })(Backbone.Model); 37 | 38 | RecordCollection = (function(_super) { 39 | __extends(RecordCollection, _super); 40 | 41 | function RecordCollection() { 42 | _ref1 = RecordCollection.__super__.constructor.apply(this, arguments); 43 | return _ref1; 44 | } 45 | 46 | RecordCollection.prototype.model = Record; 47 | 48 | return RecordCollection; 49 | 50 | })(Backbone.Collection); 51 | 52 | records = kb.collectionObservable(new RecordCollection([ 53 | new Record({ 54 | id: 1 55 | }), new Record({ 56 | id: 2 57 | }) 58 | ]), { 59 | view_model: RecordViewModel 60 | }); 61 | 62 | _.last(records()).destroy(); 63 | 64 | kb.release(records); 65 | 66 | }).call(this); 67 | -------------------------------------------------------------------------------- /test/issues/issue98/issue.coffee: -------------------------------------------------------------------------------- 1 | MainClass = Backbone.RelationalModel.extend 2 | defaults: 3 | number1: 1 4 | relations: [ 5 | { 6 | type: Backbone.HasMany 7 | key: 'categories' 8 | relatedModel: 'Category' 9 | collectionType: 'Categories' 10 | reverseRelation: 11 | key: 'main', 12 | includeInJSON: false 13 | } 14 | ] 15 | 16 | window.Category = Backbone.RelationalModel.extend 17 | relations: [ 18 | { 19 | type: Backbone.HasMany 20 | key: 'subcategories' 21 | relatedModel: 'Subcategory' 22 | collectionType: 'Subcategories' 23 | reverseRelation: 24 | key: 'category', 25 | includeInJSON: false 26 | } 27 | ] 28 | 29 | window.Subcategory = Backbone.RelationalModel.extend 30 | defaults: 31 | number1: 1 32 | number2: 1 33 | 34 | window.Categories = Backbone.Collection.extend 35 | model: Category 36 | 37 | window.Subcategories = Backbone.Collection.extend 38 | model: Subcategory 39 | 40 | SubcategoryVm = null 41 | CategoryVm = null 42 | MainClassVm = null 43 | 44 | class SubcategoryVm extends kb.ViewModel 45 | constructor: (model, options) -> 46 | super model, 47 | factories: 48 | 'category': CategoryVm 49 | options: options 50 | @computed = ko.computed => @category().main().number1() + @number1() + @number2() 51 | 52 | class CategoryVm extends kb.ViewModel 53 | constructor: (model, options) -> 54 | super model, 55 | requires: ['main'] 56 | factories: 57 | 'subcategories.models': SubcategoryVm 58 | 'main': MainClassVm 59 | options: options 60 | 61 | class MainClassVm extends kb.ViewModel 62 | constructor: (model, options) -> 63 | super model, 64 | factories: 65 | 'categories.models': CategoryVm 66 | options: options 67 | 68 | model = new MainClass 69 | categories: [{subcategories: [{}]}] 70 | 71 | try 72 | model_vm = new MainClassVm(model); 73 | catch error 74 | jQuery('#errorPanel').html(error) -------------------------------------------------------------------------------- /test/issues/issue98/issue.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Knockback Issue 98 Test 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/issues/issue98/issue.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.3 2 | (function() { 3 | var CategoryVm, MainClass, MainClassVm, SubcategoryVm, error, model, model_vm, 4 | __hasProp = {}.hasOwnProperty, 5 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 6 | 7 | MainClass = Backbone.RelationalModel.extend({ 8 | defaults: { 9 | number1: 1 10 | }, 11 | relations: [ 12 | { 13 | type: Backbone.HasMany, 14 | key: 'categories', 15 | relatedModel: 'Category', 16 | collectionType: 'Categories', 17 | reverseRelation: { 18 | key: 'main', 19 | includeInJSON: false 20 | } 21 | } 22 | ] 23 | }); 24 | 25 | window.Category = Backbone.RelationalModel.extend({ 26 | relations: [ 27 | { 28 | type: Backbone.HasMany, 29 | key: 'subcategories', 30 | relatedModel: 'Subcategory', 31 | collectionType: 'Subcategories', 32 | reverseRelation: { 33 | key: 'category', 34 | includeInJSON: false 35 | } 36 | } 37 | ] 38 | }); 39 | 40 | window.Subcategory = Backbone.RelationalModel.extend({ 41 | defaults: { 42 | number1: 1, 43 | number2: 1 44 | } 45 | }); 46 | 47 | window.Categories = Backbone.Collection.extend({ 48 | model: Category 49 | }); 50 | 51 | window.Subcategories = Backbone.Collection.extend({ 52 | model: Subcategory 53 | }); 54 | 55 | SubcategoryVm = null; 56 | 57 | CategoryVm = null; 58 | 59 | MainClassVm = null; 60 | 61 | SubcategoryVm = (function(_super) { 62 | __extends(SubcategoryVm, _super); 63 | 64 | function SubcategoryVm(model, options) { 65 | var _this = this; 66 | SubcategoryVm.__super__.constructor.call(this, model, { 67 | factories: { 68 | 'category': CategoryVm 69 | }, 70 | options: options 71 | }); 72 | this.computed = ko.computed(function() { 73 | return _this.category().main().number1() + _this.number1() + _this.number2(); 74 | }); 75 | } 76 | 77 | return SubcategoryVm; 78 | 79 | })(kb.ViewModel); 80 | 81 | CategoryVm = (function(_super) { 82 | __extends(CategoryVm, _super); 83 | 84 | function CategoryVm(model, options) { 85 | CategoryVm.__super__.constructor.call(this, model, { 86 | requires: ['main'], 87 | factories: { 88 | 'subcategories.models': SubcategoryVm, 89 | 'main': MainClassVm 90 | }, 91 | options: options 92 | }); 93 | } 94 | 95 | return CategoryVm; 96 | 97 | })(kb.ViewModel); 98 | 99 | MainClassVm = (function(_super) { 100 | __extends(MainClassVm, _super); 101 | 102 | function MainClassVm(model, options) { 103 | MainClassVm.__super__.constructor.call(this, model, { 104 | factories: { 105 | 'categories.models': CategoryVm 106 | }, 107 | options: options 108 | }); 109 | } 110 | 111 | return MainClassVm; 112 | 113 | })(kb.ViewModel); 114 | 115 | model = new MainClass({ 116 | categories: [ 117 | { 118 | subcategories: [{}] 119 | } 120 | ] 121 | }); 122 | 123 | try { 124 | model_vm = new MainClassVm(model); 125 | } catch (_error) { 126 | error = _error; 127 | jQuery('#errorPanel').html(error); 128 | } 129 | 130 | }).call(this); 131 | -------------------------------------------------------------------------------- /test/lib/globalize.culture.en-GB.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Globalize Culture en-GB 3 | * 4 | * http://github.com/jquery/globalize 5 | * 6 | * Copyright Software Freedom Conservancy, Inc. 7 | * Dual licensed under the MIT or GPL Version 2 licenses. 8 | * http://jquery.org/license 9 | * 10 | * This file was generated by the Globalize Culture Generator 11 | * Translation: bugs found in this file need to be fixed in the generator 12 | */ 13 | 14 | (function( window, undefined ) { 15 | 16 | var Globalize; 17 | 18 | if ( typeof require !== "undefined" 19 | && typeof exports !== "undefined" 20 | && typeof module !== "undefined" ) { 21 | // Assume CommonJS 22 | Globalize = require( "./globalize" ); 23 | } else { 24 | // Global variable 25 | Globalize = window.Globalize; 26 | } 27 | 28 | Globalize.addCultureInfo( "en-GB", "default", { 29 | name: "en-GB", 30 | englishName: "English (United Kingdom)", 31 | nativeName: "English (United Kingdom)", 32 | numberFormat: { 33 | currency: { 34 | pattern: ["-$n","$n"], 35 | symbol: "£" 36 | } 37 | }, 38 | calendars: { 39 | standard: { 40 | firstDay: 1, 41 | patterns: { 42 | d: "dd/MM/yyyy", 43 | D: "dd MMMM yyyy", 44 | t: "HH:mm", 45 | T: "HH:mm:ss", 46 | f: "dd MMMM yyyy HH:mm", 47 | F: "dd MMMM yyyy HH:mm:ss", 48 | M: "dd MMMM", 49 | Y: "MMMM yyyy" 50 | } 51 | } 52 | } 53 | }); 54 | 55 | }( this )); 56 | -------------------------------------------------------------------------------- /test/lib/globalize.culture.fr-FR.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Globalize Culture fr-FR 3 | * 4 | * http://github.com/jquery/globalize 5 | * 6 | * Copyright Software Freedom Conservancy, Inc. 7 | * Dual licensed under the MIT or GPL Version 2 licenses. 8 | * http://jquery.org/license 9 | * 10 | * This file was generated by the Globalize Culture Generator 11 | * Translation: bugs found in this file need to be fixed in the generator 12 | */ 13 | 14 | (function( window, undefined ) { 15 | 16 | var Globalize; 17 | 18 | if ( typeof require !== "undefined" 19 | && typeof exports !== "undefined" 20 | && typeof module !== "undefined" ) { 21 | // Assume CommonJS 22 | Globalize = require( "./globalize" ); 23 | } else { 24 | // Global variable 25 | Globalize = window.Globalize; 26 | } 27 | 28 | Globalize.addCultureInfo( "fr-FR", "default", { 29 | name: "fr-FR", 30 | englishName: "French (France)", 31 | nativeName: "français (France)", 32 | language: "fr", 33 | numberFormat: { 34 | ",": " ", 35 | ".": ",", 36 | NaN: "Non Numérique", 37 | negativeInfinity: "-Infini", 38 | positiveInfinity: "+Infini", 39 | percent: { 40 | ",": " ", 41 | ".": "," 42 | }, 43 | currency: { 44 | pattern: ["-n $","n $"], 45 | ",": " ", 46 | ".": ",", 47 | symbol: "€" 48 | } 49 | }, 50 | calendars: { 51 | standard: { 52 | firstDay: 1, 53 | days: { 54 | names: ["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"], 55 | namesAbbr: ["dim.","lun.","mar.","mer.","jeu.","ven.","sam."], 56 | namesShort: ["di","lu","ma","me","je","ve","sa"] 57 | }, 58 | months: { 59 | names: ["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre",""], 60 | namesAbbr: ["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc.",""] 61 | }, 62 | AM: null, 63 | PM: null, 64 | eras: [{"name":"ap. J.-C.","start":null,"offset":0}], 65 | patterns: { 66 | d: "dd/MM/yyyy", 67 | D: "dddd d MMMM yyyy", 68 | t: "HH:mm", 69 | T: "HH:mm:ss", 70 | f: "dddd d MMMM yyyy HH:mm", 71 | F: "dddd d MMMM yyyy HH:mm:ss", 72 | M: "d MMMM", 73 | Y: "MMMM yyyy" 74 | } 75 | } 76 | } 77 | }); 78 | 79 | }( this )); 80 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers coffee:coffee-script/register 2 | --timeout 10000 3 | --grep @no_options 4 | -------------------------------------------------------------------------------- /test/spec/core/core.tests.coffee: -------------------------------------------------------------------------------- 1 | assert = assert or require?('chai').assert 2 | 3 | describe 'knockback_core utils @quick @core', -> 4 | kb = window?.kb; try kb or= require?('knockback') catch; try kb or= require?('../../../knockback') 5 | {_, ko} = kb 6 | $ = window?.$ 7 | 8 | it 'TEST DEPENDENCY MISSING', (done) -> 9 | assert.ok(!!ko, 'ko') 10 | assert.ok(!!_, '_') 11 | assert.ok(!!kb.Model, 'kb.Model') 12 | assert.ok(!!kb.Collection, 'kb.Collection') 13 | assert.ok(!!kb, 'kb') 14 | done() 15 | 16 | it 'kb.renderTemplate', (done) -> 17 | return done() unless ($ and window?.document) 18 | 19 | kb.statistics = new kb.Statistics() # turn on stats 20 | 21 | class ViewModel 22 | constructor: -> 23 | @name = ko.observable('Bob') 24 | afterRender: -> 25 | @was_called = true 26 | 27 | # test without options override 28 | view_model = new ViewModel() 29 | assert.ok(!view_model.was_called, 'afterRender not called yet') 30 | el = $('') 31 | $('body').append(el) 32 | kb.renderTemplate('the_template1', view_model) 33 | assert.ok(view_model.was_called, 'afterRender was called') 34 | 35 | # test with options override 36 | was_called = false 37 | view_model = new ViewModel() 38 | assert.ok(!view_model.was_called, 'afterRender (options) not called yet') 39 | assert.ok(!view_model.was_called, 'afterRender not called yet') 40 | el = $('') 41 | $('body').append(el) 42 | kb.renderTemplate('the_template2', view_model, {afterRender: -> was_called = true}) 43 | assert.ok(was_called, 'afterRender (options) was called') 44 | assert.ok(!view_model.was_called, 'afterRender was not called') 45 | 46 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 47 | done() 48 | 49 | it 'kb.ignore', (done) -> 50 | kb.statistics = new kb.Statistics() # turn on stats 51 | 52 | counter = 0 53 | counter_ignore = 0 54 | 55 | name = ko.observable('Bob') 56 | counter_computed = ko.computed => 57 | name() 58 | ++counter 59 | 60 | counter_computed_ignore = ko.computed => 61 | value = kb.ignore -> 62 | name() 63 | ++counter_ignore 64 | assert.equal(value, counter_ignore) 65 | 66 | assert.equal(counter, 1) 67 | assert.equal(counter_ignore, 1) 68 | 69 | name('Fred') 70 | assert.equal(counter, 2) 71 | assert.equal(counter_ignore, 1) 72 | 73 | # ignore with arguments 74 | kb.ignore ((arg1, arg2) -> assert.equal(arg1, 1); assert.equal(arg2, 2)), null, [1, 2] 75 | 76 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 77 | done() -------------------------------------------------------------------------------- /test/spec/core/monkey-patches.tests.coffee: -------------------------------------------------------------------------------- 1 | assert = assert or require?('chai').assert 2 | 3 | describe 'money-patches @quick @monkey', -> 4 | kb = window?.kb; try kb or= require?('knockback') catch; try kb or= require?('../../../knockback') 5 | {_, ko} = kb 6 | 7 | it 'TEST DEPENDENCY MISSING', (done) -> 8 | assert.ok(!!ko, 'ko') 9 | assert.ok(!!_, '_') 10 | assert.ok(!!kb.Model, 'kb.Model') 11 | assert.ok(!!kb.Collection, 'kb.Collection') 12 | assert.ok(!!kb, 'kb') 13 | done() 14 | 15 | Contact = if kb.Parse then kb.Model.extend('Contact', { defaults: {name: '', number: 0, date: new Date()} }) else kb.Model.extend({ defaults: {name: '', number: 0, date: new Date()} }) 16 | Contacts = kb.Collection.extend({model: Contact}) 17 | 18 | ## https://github.com/kmalakoff/knockback/issues/124 19 | it 'fixes memory management for extend on kb.observable', (done) -> 20 | return done() unless ko.subscribable?.fn?.extend 21 | kb.statistics = new kb.Statistics() # turn on stats 22 | 23 | model = new Contact({name: 'Bob'}) 24 | observable = kb.observable(model, {key: 'name'}) 25 | assert.ok !kb.wasReleased(observable), 'observable not released' 26 | 27 | extended_observable = observable.extend({throttle: 100}) 28 | assert.ok !kb.wasReleased(observable), 'observable not released' 29 | assert.ok !kb.wasReleased(extended_observable), 'observable not released' 30 | 31 | kb.release(extended_observable) 32 | assert.ok !!kb.wasReleased(observable), 'observable released' 33 | assert.ok !!kb.wasReleased(extended_observable), 'observable released' 34 | 35 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 36 | done() 37 | 38 | ## https://github.com/kmalakoff/knockback/issues/124 39 | it 'fixes memory management for extend on kb.CollectionObservable', (done) -> 40 | return done() unless ko.subscribable?.fn?.extend 41 | kb.statistics = new kb.Statistics() # turn on stats 42 | 43 | ko.extenders.lazyArray = (target, timeout) -> 44 | addTimeout = null 45 | target.elementsToDisplay = ko.observable(0) 46 | 47 | return ko.computed -> 48 | all = target() 49 | if (target.elementsToDisplay() > all.length) 50 | target.elementsToDisplay(all.length) 51 | else if (addTimeout is null && target.elementsToDisplay() < all.length) 52 | addTimeout = setTimeout (-> 53 | addTimeout = null 54 | target.elementsToDisplay(target.elementsToDisplay() + 1) 55 | ), timeout 56 | return all.slice(0, target.elementsToDisplay()) 57 | 58 | collection_observable = kb.collectionObservable(collection = new Contacts([{name: 'Bob'}])) 59 | assert.ok !kb.wasReleased(collection_observable), 'collection_observable not released' 60 | 61 | extended_collection_observable = collection_observable.extend({lazyArray: 10}) 62 | assert.ok !kb.wasReleased(collection_observable), 'collection_observable not released' 63 | assert.ok !kb.wasReleased(extended_collection_observable), 'collection_observable not released' 64 | 65 | kb.release(extended_collection_observable) 66 | assert.ok !!kb.wasReleased(collection_observable), 'collection_observable released' 67 | assert.ok !!kb.wasReleased(extended_collection_observable), 'collection_observable released' 68 | 69 | delete ko.extenders.lazyArray 70 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 71 | done() 72 | 73 | # https://github.com/kmalakoff/knockback/issues/127 74 | it 'extend monkey patch does not cause arrays to destroy', (done) -> 75 | kb.statistics = new kb.Statistics() # turn on stats 76 | 77 | class ViewModel 78 | constructor: -> 79 | @list1 = ko.observableArray(['sds1', 'sdsd1']) 80 | @list2 = ko.observableArray(['sds2', 'sdsd2']) 81 | @totalNumberOfItems = ko.computed => @list1().length + @list2().length 82 | 83 | view_model = new ViewModel() 84 | assert.deepEqual(view_model.list1(), ['sds1', 'sdsd1']) 85 | assert.deepEqual(view_model.list2(), ['sds2', 'sdsd2']) 86 | assert.equal view_model.totalNumberOfItems(), 4 87 | 88 | assert.doesNotThrow -> kb.release(view_model) 89 | assert.ok !view_model.list1 90 | assert.ok !view_model.list2 91 | assert.ok !view_model.totalNumberOfItems 92 | 93 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 94 | done() 95 | -------------------------------------------------------------------------------- /test/spec/core/utils.tests.coffee: -------------------------------------------------------------------------------- 1 | assert = assert or require?('chai').assert 2 | 3 | describe 'knockback_core utils @quick @utils', -> 4 | 5 | kb = window?.kb; try kb or= require?('knockback') catch; try kb or= require?('../../../knockback') 6 | {_, ko} = kb 7 | 8 | it 'TEST DEPENDENCY MISSING', (done) -> 9 | assert.ok(!!ko, 'ko') 10 | assert.ok(!!_, '_') 11 | assert.ok(!!kb.Model, 'kb.Model') 12 | assert.ok(!!kb.Collection, 'kb.Collection') 13 | assert.ok(!!kb, 'kb') 14 | done() 15 | 16 | it 'kb.utils.wrappedObservable', (done) -> 17 | kb.statistics = new kb.Statistics() # turn on stats 18 | 19 | observable = ko.observable() 20 | instance = {} 21 | kb.utils.wrappedObservable(instance, observable) # set 22 | assert.equal(kb.utils.wrappedObservable(instance), observable, "observable was wrapped") # get 23 | 24 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 25 | done() 26 | 27 | it 'kb.utils.wrappedModel', (done) -> 28 | kb.statistics = new kb.Statistics() # turn on stats 29 | 30 | model = new kb.Model({name: 'Bob'}) 31 | instance = {} 32 | assert.equal(kb.utils.wrappedModel(instance), instance, "no model was wrapped so return the instance") # get 33 | 34 | kb.utils.wrappedModel(instance, model) # set 35 | assert.equal(kb.utils.wrappedModel(instance), model, "model was wrapped") # get 36 | 37 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 38 | done() 39 | 40 | it 'kb.utils.wrappedStore', (done) -> 41 | kb.statistics = new kb.Statistics() # turn on stats 42 | 43 | collection_observable = kb.collectionObservable(new kb.Collection()) 44 | assert.ok(!!kb.utils.wrappedStore(collection_observable), 'Store is available on a collection observable') 45 | 46 | # can get and share store 47 | collection_observable_shared = kb.collectionObservable(new kb.Collection(), {store: kb.utils.wrappedStore(collection_observable)}) 48 | assert.equal(kb.utils.wrappedStore(collection_observable), kb.utils.wrappedStore(collection_observable_shared), 'Store is shared between collection observables') 49 | kb.release(collection_observable_shared) # clean up 50 | 51 | view_model = kb.viewModel(new kb.Model({name: 'Bob'})) 52 | assert.ok(!!kb.utils.wrappedStore(view_model), 'Store is available on a view model') 53 | 54 | # can get and share store 55 | collection_observable_shared = kb.collectionObservable(new kb.Collection(), {store: kb.utils.wrappedStore(view_model)}) 56 | 57 | assert.equal(kb.utils.wrappedStore(view_model), kb.utils.wrappedStore(collection_observable_shared), 'Store is shared between collection observable and view model') 58 | 59 | # clean up 60 | kb.release(collection_observable) 61 | kb.release(collection_observable_shared) 62 | kb.release(view_model) 63 | 64 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 65 | done() 66 | 67 | it 'kb.utils.valueType', (done) -> 68 | kb.statistics = new kb.Statistics() # turn on stats 69 | 70 | co = kb.collectionObservable(new kb.Collection()) 71 | assert.equal(kb.utils.valueType(co), kb.TYPE_COLLECTION, "kb.CollectionObservable is a collection type") 72 | kb.release(co) # clean up 73 | 74 | o = kb.observable(new kb.Model({name: 'name1'}), 'name') 75 | assert.equal(kb.utils.valueType(o), kb.TYPE_SIMPLE, "kb.Observable is a kb.TYPE_SIMPLE") 76 | kb.release(o) # clean up 77 | 78 | model = new kb.Model({simple_type: 3, model_type: new kb.Model(), collection_type: new kb.Collection()}) 79 | view_model = kb.viewModel(model) 80 | 81 | assert.equal(kb.utils.valueType(view_model.simple_type), kb.TYPE_SIMPLE, "simple is kb.TYPE_SIMPLE") 82 | assert.equal(kb.utils.valueType(view_model.model_type), kb.TYPE_MODEL, "model is kb.TYPE_MODEL") 83 | assert.equal(kb.utils.valueType(view_model.collection_type), kb.TYPE_COLLECTION, "collection is kb.TYPE_COLLECTION") 84 | kb.release(view_model) # clean up 85 | 86 | view_model = kb.viewModel(new kb.Model({simple_attr: null, model_attr: null}), {factories: model_attr: kb.ViewModel}) 87 | assert.equal(kb.utils.valueType(view_model.simple_attr), kb.TYPE_SIMPLE, 'simple_attr is kb.TYPE_SIMPLE') 88 | assert.equal(kb.utils.valueType(view_model.model_attr), kb.TYPE_MODEL, 'model_attr is kb.TYPE_MODEL') 89 | kb.release(view_model) # clean up 90 | 91 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 92 | done() 93 | 94 | it 'kb.utils.path', (done) -> 95 | kb.statistics = new kb.Statistics() # turn on stats 96 | 97 | assert.equal(kb.utils.pathJoin(null, 'key'), 'key', "key path joined") 98 | assert.equal(kb.utils.pathJoin('bob', 'key'), 'bob.key', "bob.key path joined") 99 | assert.equal(kb.utils.pathJoin('bob.', 'key'), 'bob.key', "bob.key path joined") 100 | assert.equal(kb.utils.pathJoin('bob.harry', 'key'), 'bob.harry.key', "bob.harry.key path joined") 101 | assert.equal(kb.utils.pathJoin('bob.harry.', 'key'), 'bob.harry.key', "bob.harry.key path joined") 102 | 103 | assert.equal(kb.utils.optionsPathJoin({}, 'key').path, 'key', "key path joined") 104 | assert.equal(kb.utils.optionsPathJoin({path: 'bob'}, 'key').path, 'bob.key', "bob.key path joined") 105 | assert.equal(kb.utils.optionsPathJoin({path: 'bob.'}, 'key').path, 'bob.key', "bob.key path joined") 106 | assert.equal(kb.utils.optionsPathJoin({path: 'bob.harry'}, 'key').path, 'bob.harry.key', "bob.harry.key path joined") 107 | assert.equal(kb.utils.optionsPathJoin({path: 'bob.harry.'}, 'key').path, 'bob.harry.key', "bob.harry.key path joined") 108 | 109 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 110 | done() 111 | 112 | # https://github.com/kmalakoff/knockback/issues/103 113 | it 'kb.release handling type changes', (done) -> 114 | kb.statistics = new kb.Statistics() # turn on stats 115 | 116 | model = new kb.Model() 117 | model.set({foo: [1,2,3]}) 118 | observable = kb.viewModel(model) 119 | model.set({foo: null}) 120 | kb.release(observable) 121 | 122 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 123 | done() 124 | 125 | # https://github.com/kmalakoff/knockback/issues/101 126 | it 'kb.release releases all events', (done) -> 127 | kb.statistics = new kb.Statistics() # turn on stats 128 | 129 | model = new kb.Model({id: 1, name: "Zebra", age: 22, genus: "Equus"}); 130 | assert.ok(kb.Statistics.eventsStats(model).count is 0, 'No events yet') 131 | 132 | view_model = {model: kb.viewModel(model)} 133 | assert.ok(kb.Statistics.eventsStats(model).count is 1, "There is 1 event. Expected: 1. Actual: #{JSON.stringify(kb.Statistics.eventsStats(model))}") 134 | 135 | kb.release(view_model) 136 | 137 | assert.ok(kb.Statistics.eventsStats(model).count is 0, "All events cleared. Expected: 0. Actual: #{JSON.stringify(kb.Statistics.eventsStats(model))}") 138 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 139 | done() 140 | -------------------------------------------------------------------------------- /test/spec/ecosystem/backbone-modelref.tests.coffee: -------------------------------------------------------------------------------- 1 | root = if window? then window else global 2 | assert = assert or require?('chai').assert 3 | 4 | describe 'Knockback.js with Backbone.ModelRef.js @backbone-modelref', -> 5 | 6 | # import Underscore (or Lo-Dash with precedence), Backbone, Knockout, and Knockback 7 | kb = window?.kb; try kb or= require?('knockback') catch; try kb or= require?('../../../knockback') 8 | {_, Backbone, ko} = kb 9 | root.Backbone?.ModelRef or= require?('backbone-modelref') 10 | 11 | it 'TEST DEPENDENCY MISSING', (done) -> 12 | assert.ok(!!ko, 'ko') 13 | assert.ok(!!_, '_') 14 | assert.ok(!!Backbone, 'Backbone') 15 | assert.ok(!!Backbone.ModelRef, 'Backbone.ModelRef') 16 | assert.ok(!!kb, 'kb') 17 | done() 18 | 19 | return unless root.Backbone?.ModelRef 20 | 21 | Contact = if kb.Parse then kb.Model.extend('Contact', { defaults: {name: '', number: 0, date: new Date()} }) else kb.Model.extend({ defaults: {name: '', number: 0, date: new Date()} }) 22 | Contacts = kb.Collection.extend({model: Contact}) 23 | 24 | it 'Standard use case: just enough to get the picture', (done) -> 25 | kb.statistics = new kb.Statistics() # turn on stats 26 | 27 | ContactViewModel = (model) -> 28 | @_auto = kb.viewModel(model, {keys: { 29 | name: {key:'name'} 30 | number: {key:'number'} 31 | date: {key:'date'} 32 | }}, this) 33 | return 34 | 35 | collection = new Contacts() 36 | model_ref = new Backbone.ModelRef(collection, 'b4') 37 | view_model = new ContactViewModel(model_ref) 38 | 39 | assert.equal(view_model.name(), null, "Is that what we want to convey?") 40 | 41 | collection.add(collection.parse({id: 'b4', name: 'John', number: '555-555-5558', date: new Date(1940, 10, 9)})) 42 | model = collection.get('b4') 43 | 44 | # get 45 | assert.equal(view_model.name(), 'John', "It is a name") 46 | assert.equal(view_model.number(), '555-555-5558', "Not so interesting number") 47 | assert.equal(view_model.date().toString(), new Date(1940, 10, 9).toString(), "John's birthdate matches") 48 | 49 | # set from the view model 50 | assert.equal(model.get('name'), 'John', "Name not changed") 51 | assert.equal(view_model.name(), 'John', "Name not changed") 52 | view_model.number('9222-222-222') 53 | assert.equal(model.get('number'), '9222-222-222', "Number was changed") 54 | assert.equal(view_model.number(), '9222-222-222', "Number was changed") 55 | view_model.date(new Date(1963, 11, 10)) 56 | current_date = model.get('date') 57 | assert.equal(current_date.getFullYear(), 1963, "year is good") 58 | assert.equal(current_date.getMonth(), 11, "month is good") 59 | assert.equal(current_date.getDate(), 10, "day is good") 60 | 61 | # set from the model 62 | model.set({name: 'Yoko', number: '818-818-8181'}) 63 | assert.equal(view_model.name(), 'Yoko', "Name changed") 64 | assert.equal(view_model.number(), '818-818-8181', "Number was changed") 65 | model.set({date: new Date(1940, 10, 9)}) 66 | assert.equal(view_model.date().toString(), (new Date(1940, 10, 9)).toString(), "John's birthdate") 67 | view_model.date(new Date(1940, 10, 10)) 68 | current_date = model.get('date') 69 | assert.equal(current_date.getFullYear(), 1940, "year is good") 70 | assert.equal(current_date.getMonth(), 10, "month is good") 71 | assert.equal(current_date.getDate(), 10, "day is good") 72 | 73 | # and cleanup after yourself when you are done. 74 | kb.release(view_model) 75 | 76 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 77 | done() 78 | 79 | it 'Standard use case with kb.ViewModels', (done) -> 80 | kb.statistics = new kb.Statistics() # turn on stats 81 | 82 | class ContactViewModel extends kb.ViewModel 83 | constructor: (model) -> 84 | super(model, {requires: ['name', 'number', 'date']}) 85 | 86 | collection = new Contacts() 87 | model_ref = new Backbone.ModelRef(collection, 'b4') 88 | view_model = new ContactViewModel(model_ref) 89 | 90 | assert.equal(view_model.name(), null, "Is that what we want to convey?") 91 | 92 | collection.add(collection.parse({id: 'b4', name: 'John', number: '555-555-5558', date: new Date(1940, 10, 9)})) 93 | model = collection.get('b4') 94 | 95 | # get 96 | assert.equal(view_model.name(), 'John', "It is a name") 97 | assert.equal(view_model.number(), '555-555-5558', "Not so interesting number") 98 | assert.equal(view_model.date().toString(), (new Date(1940, 10, 9)).toString(), "John's birthdate matches") 99 | 100 | # set from the view model 101 | assert.equal(model.get('name'), 'John', "Name not changed") 102 | assert.equal(view_model.name(), 'John', "Name not changed") 103 | view_model.number('9222-222-222') 104 | assert.equal(model.get('number'), '9222-222-222', "Number was changed") 105 | assert.equal(view_model.number(), '9222-222-222', "Number was changed") 106 | view_model.date(new Date(1963, 11, 10)) 107 | current_date = model.get('date') 108 | assert.equal(current_date.getFullYear(), 1963, "year is good") 109 | assert.equal(current_date.getMonth(), 11, "month is good") 110 | assert.equal(current_date.getDate(), 10, "day is good") 111 | 112 | # set from the model 113 | model.set({name: 'Yoko', number: '818-818-8181'}) 114 | assert.equal(view_model.name(), 'Yoko', "Name changed") 115 | assert.equal(view_model.number(), '818-818-8181', "Number was changed") 116 | model.set({date: new Date(1940, 10, 9)}) 117 | assert.equal(view_model.date().toString(), (new Date(1940, 10, 9)).toString(), "John's birthdate") 118 | view_model.date(new Date(1940, 10, 10)) 119 | current_date = model.get('date') 120 | assert.equal(current_date.getFullYear(), 1940, "year is good") 121 | assert.equal(current_date.getMonth(), 10, "month is good") 122 | assert.equal(current_date.getDate(), 10, "day is good") 123 | 124 | # and cleanup after yourself when you are done. 125 | kb.release(view_model) 126 | 127 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 128 | done() 129 | 130 | it 'CLEANUP', -> delete Backbone.ModeRef 131 | -------------------------------------------------------------------------------- /test/spec/ecosystem/backbone-relational.memory-management.tests.coffee: -------------------------------------------------------------------------------- 1 | root = if window? then window else global 2 | assert = assert or require?('chai').assert 3 | 4 | describe 'Knockback.js with Backbone-Relational.js @backbone-relational', -> 5 | 6 | # after -> delete root.Person 7 | 8 | # import Underscore (or Lo-Dash with precedence), Backbone, Knockout, and Knockback 9 | kb = window?.kb; try kb or= require?('knockback') catch; try kb or= require?('../../../knockback') 10 | {_, Backbone, ko} = kb 11 | require?('backbone-relational') unless Backbone?.Relational 12 | 13 | it 'TEST DEPENDENCY MISSING', (done) -> 14 | assert.ok(!!ko, 'ko') 15 | assert.ok(!!_, '_') 16 | assert.ok(!!Backbone, 'Backbone') 17 | assert.ok(!!kb, 'kb') 18 | assert.ok(!!Backbone.Relational, 'Backbone.Relational') 19 | kb.configure({orm: 'backbone-relational'}) 20 | done() 21 | 22 | return unless Backbone?.Relational 23 | Backbone.Relational.store = new Backbone.Store(); Backbone.Relational.store.addModelScope?(root) 24 | 25 | root.Person = Person = Backbone.RelationalModel.extend({ 26 | relations: [{ 27 | type: Backbone.HasMany 28 | key: 'friends' 29 | relatedModel: 'Person' 30 | }] 31 | }) 32 | 33 | # ref counted view model 34 | class RefCountableViewModel 35 | constructor: -> 36 | RefCountableViewModel.view_models.push(this) 37 | @ref_count = 1 38 | 39 | refCount: -> return @ref_count 40 | retain: -> 41 | @ref_count++ 42 | return @ 43 | release: -> 44 | --@ref_count 45 | throw "ref count is corrupt" if @ref_count < 0 46 | unless @ref_count 47 | @is_destroyed = true 48 | @__destroy() 49 | return @ 50 | 51 | __destroy: -> 52 | RefCountableViewModel.view_models.splice(_.indexOf(RefCountableViewModel.view_models, this), 1) 53 | 54 | @view_models: [] 55 | 56 | # destroyable view model 57 | class DestroyableViewModel 58 | constructor: -> 59 | DestroyableViewModel.view_models.push(this) 60 | 61 | destroy: -> 62 | DestroyableViewModel.view_models.splice(_.indexOf(DestroyableViewModel.view_models, this), 1) 63 | 64 | @view_models: [] 65 | 66 | # simple view model 67 | class SimpleViewModel 68 | constructor: -> 69 | @prop = ko.observable() 70 | SimpleViewModel.view_models.push(this) 71 | 72 | @view_models: [] 73 | 74 | it 'kb.CollectionObservable with recursive view models', (done) -> 75 | kb.statistics = new kb.Statistics() # turn on stats 76 | 77 | john = new Person({ 78 | id: 'person-1-1' 79 | name: 'John' 80 | friends: ['person-1-2', 'person-1-3', 'person-1-4'] 81 | }) 82 | paul = new Person({ 83 | id: 'person-1-2' 84 | name: 'Paul' 85 | friends: ['person-1-1', 'person-1-3', 'person-1-4'] 86 | }) 87 | george = new Person({ 88 | id: 'person-1-3' 89 | name: 'George' 90 | friends: ['person-1-1', 'person-1-2', 'person-1-4'] 91 | }) 92 | ringo = new Person({ 93 | id: 'person-1-4' 94 | name: 'Ringo' 95 | friends: ['person-1-1', 'person-1-2', 'person-1-3'] 96 | }) 97 | 98 | band = new kb.Collection([john, paul, george, ringo]) 99 | 100 | # ref counted view model 101 | RefCountableViewModel.view_models = [] 102 | collection_observable = kb.collectionObservable(band, {view_model: RefCountableViewModel}) 103 | assert.equal(RefCountableViewModel.view_models.length, 4, "Created: 4") 104 | 105 | instance = collection_observable()[0].retain() 106 | 107 | kb.release(collection_observable) 108 | assert.equal(RefCountableViewModel.view_models.length, 1, "Still one reference") 109 | assert.equal(instance.refCount(), 1, "All instances were destroyed in the collection's store") 110 | 111 | # destroyable view model 112 | DestroyableViewModel.view_models = [] 113 | collection_observable = kb.collectionObservable(band, {view_model: DestroyableViewModel}) 114 | assert.equal(DestroyableViewModel.view_models.length, 4, "Created: 4") 115 | 116 | kb.release(collection_observable) 117 | assert.equal(DestroyableViewModel.view_models.length, 0, "All destroyed") 118 | 119 | # simple view model 120 | SimpleViewModel.view_models = [] 121 | collection_observable = kb.collectionObservable(band, {view_model: SimpleViewModel}) 122 | assert.equal(SimpleViewModel.view_models.length, 4, "Created: 4") 123 | 124 | kb.release(collection_observable) 125 | assert.equal(SimpleViewModel.view_models.length, 4, "Destroyed: 4") 126 | assert.ok(!view_model.prop, "Prop destroyed") for view_model in SimpleViewModel.view_models 127 | 128 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 129 | done() 130 | 131 | it 'kb.CollectionObservable with recursive view models and external store', (done) -> 132 | kb.statistics = new kb.Statistics() # turn on stats 133 | 134 | john = new Person({ 135 | id: 'person-2-1' 136 | name: 'John' 137 | friends: ['person-2-2', 'person-2-3', 'person-2-4'] 138 | }) 139 | paul = new Person({ 140 | id: 'person-2-2' 141 | name: 'Paul' 142 | friends: ['person-2-1', 'person-2-3', 'person-2-4'] 143 | }) 144 | george = new Person({ 145 | id: 'person-2-3' 146 | name: 'George' 147 | friends: ['person-2-1', 'person-2-2', 'person-2-4'] 148 | }) 149 | ringo = new Person({ 150 | id: 'person-2-4' 151 | name: 'Ringo' 152 | friends: ['person-2-1', 'person-2-2', 'person-2-3'] 153 | }) 154 | 155 | band = new kb.Collection([john, paul, george, ringo]) 156 | 157 | # ref counted view model 158 | store = new kb.Store() 159 | RefCountableViewModel.view_models = [] 160 | collection_observable = kb.collectionObservable(band, {view_model: RefCountableViewModel, store: store}) 161 | assert.equal(RefCountableViewModel.view_models.length, 4, "Created: 4") 162 | 163 | instance = collection_observable()[0].retain() 164 | 165 | kb.release(collection_observable) 166 | assert.equal(RefCountableViewModel.view_models.length, 4, "Remaining: 4") 167 | 168 | assert.equal(instance.refCount(), 2, "One instance retained and one in the store") 169 | 170 | store.destroy(); store = null 171 | 172 | assert.equal(RefCountableViewModel.view_models.length, 1, "Still one reference") 173 | assert.equal(instance.refCount(), 1, "All instances were destroyed in the collection's store") 174 | 175 | # destroyable view model 176 | store = new kb.Store() 177 | DestroyableViewModel.view_models = [] 178 | collection_observable = kb.collectionObservable(band, {view_model: DestroyableViewModel, store: store}) 179 | assert.equal(DestroyableViewModel.view_models.length, 4, "Created: 4") 180 | 181 | kb.release(collection_observable) 182 | assert.equal(DestroyableViewModel.view_models.length, 4, "All destroyed") 183 | 184 | store.destroy(); store = null 185 | 186 | # all instances in the collection's store were released when it was destroyed (to remove potential cycles) 187 | assert.equal(DestroyableViewModel.view_models.length, 0, "All destroyed") 188 | 189 | # simple view model 190 | store = new kb.Store() 191 | SimpleViewModel.view_models = [] 192 | collection_observable = kb.collectionObservable(band, {view_model: SimpleViewModel, store: store}) 193 | assert.equal(SimpleViewModel.view_models.length, 4, "Created: 4") 194 | 195 | kb.release(collection_observable) 196 | assert.equal(SimpleViewModel.view_models.length, 4, "Remaining: 4") 197 | assert.ok(view_model.prop, "Prop destroyed") for view_model in SimpleViewModel.view_models 198 | 199 | store.destroy(); store = null 200 | 201 | # all instances in the collection's store were released when it was destroyed (to remove potential cycles) 202 | assert.equal(SimpleViewModel.view_models.length, 4, "Destroyed: 4") 203 | assert.ok(!view_model.prop, "Prop destroyed") for view_model in SimpleViewModel.view_models 204 | 205 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 206 | done() 207 | 208 | it 'CLEANUP', -> kb.configure({orm: 'default'}) 209 | -------------------------------------------------------------------------------- /test/spec/issues/159.tests.coffee: -------------------------------------------------------------------------------- 1 | assert = assert or require?('chai').assert 2 | 3 | # https://github.com/kmalakoff/knockback/issues/159 4 | describe 'issue 159 @issue159 @quick', -> 5 | kb = window?.kb; try kb or= require?('knockback') catch; try kb or= require?('../../../knockback') 6 | {_, ko} = kb 7 | $ = window?.$ 8 | 9 | it 'TEST DEPENDENCY MISSING', (done) -> 10 | assert.ok(!!ko, 'ko') 11 | assert.ok(!!_, '_') 12 | assert.ok(!!kb.Model, 'kb.Model') 13 | assert.ok(!!kb.Collection, 'kb.Collection') 14 | assert.ok(!!kb, 'kb') 15 | done() 16 | 17 | it 'has no issue', (done) -> 18 | return done() unless ($ and window?.document) 19 | 20 | kb.statistics = new kb.Statistics() # turn on stats 21 | 22 | class ViewModel 23 | afterRender: -> @was_called = true 24 | 25 | # test without options override 26 | view_model = new ViewModel() 27 | assert.ok(!view_model.was_called, 'afterRender not called yet') 28 | el = $("""""") 36 | $('body').append(el) 37 | kb.renderTemplate('the_template1', view_model) 38 | assert.ok(view_model.was_called, 'afterRender was called') 39 | 40 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 41 | done() 42 | -------------------------------------------------------------------------------- /test/spec/parameters.coffee: -------------------------------------------------------------------------------- 1 | kb = window?.kb; try kb or= require?('knockback') catch; try kb or= require?('../../../knockback') 2 | 3 | exports = 4 | LocaleManager: require('./lib/locale_manager') 5 | 6 | (if window? then window else if global? then global).__test__parameters = exports; module?.exports = exports 7 | -------------------------------------------------------------------------------- /test/spec/plugins/triggering.tests.coffee: -------------------------------------------------------------------------------- 1 | assert = assert or require?('chai').assert 2 | 3 | describe 'triggered-observable @quick @triggering', -> 4 | 5 | kb = window?.kb; try kb or= require?('knockback') catch; try kb or= require?('../../../knockback') 6 | {_, ko} = kb 7 | 8 | it 'TEST DEPENDENCY MISSING', (done) -> 9 | assert.ok(!!ko, 'ko') 10 | assert.ok(!!_, '_') 11 | assert.ok(!!kb.Model, 'kb.Model') 12 | assert.ok(!!kb.Collection, 'kb.Collection') 13 | assert.ok(!!kb, 'kb') 14 | done() 15 | 16 | it '1. Standard use case: simple events notifications', (done) -> 17 | kb.statistics = new kb.Statistics() # turn on stats 18 | model = new kb.Model 19 | model.setLocale = -> model.trigger('change', model) 20 | 21 | trigger_count = 0 22 | 23 | view_model = 24 | triggered_observable: kb.triggeredObservable(model, 'change') 25 | view_model.counter = ko.computed(-> 26 | view_model.triggered_observable() # add a dependency 27 | return trigger_count++ 28 | ) 29 | 30 | assert.equal(trigger_count, 1, "1: was set up") 31 | 32 | model.trigger('change', model) 33 | assert.equal(trigger_count, 2, "2: changed") 34 | 35 | model.setLocale('fr-FR') 36 | assert.equal(trigger_count, 3, "3: changed") 37 | 38 | # and cleanup after yourself when you are done. 39 | kb.release(view_model) 40 | assert.equal(trigger_count, 3, "3: no change") 41 | 42 | # KO doesn't have a dispose for ko.observable 43 | model.setLocale('fr-FR') 44 | assert.equal(trigger_count, 3, "3: no change") 45 | 46 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 47 | done() 48 | 49 | it '2. Standard use case: simple events notifications (kb.observableTriggered)', (done) -> 50 | kb.statistics = new kb.Statistics() # turn on stats 51 | model = new kb.Model 52 | model.setLocale = -> model.trigger('change', model) 53 | 54 | trigger_count = 0 55 | 56 | view_model = 57 | triggered_observable: kb.observableTriggered(model, 'change') 58 | view_model.counter = ko.computed(-> 59 | view_model.triggered_observable() # add a dependency 60 | return trigger_count++ 61 | ) 62 | 63 | assert.equal(trigger_count, 1, "1: was set up") 64 | 65 | model.trigger('change', model) 66 | assert.equal(trigger_count, 2, "2: changed") 67 | 68 | model.setLocale('fr-FR') 69 | assert.equal(trigger_count, 3, "3: changed") 70 | 71 | # and cleanup after yourself when you are done. 72 | kb.release(view_model) 73 | assert.equal(trigger_count, 3, "3: no change") 74 | 75 | # KO doesn't have a dispose for ko.observable 76 | model.setLocale('fr-FR') 77 | assert.equal(trigger_count, 3, "3: no change") 78 | 79 | assert.equal(kb.statistics.registeredStatsString('all released'), 'all released', "Cleanup: stats"); kb.statistics = null 80 | done() 81 | --------------------------------------------------------------------------------