├── .gitignore ├── .versions ├── HISTORY.md ├── LICENSE.txt ├── README.md ├── lib ├── bindings.coffee ├── lzstring.js ├── migration.coffee ├── template.coffee ├── viewmodel-onUrl.coffee ├── viewmodel-parseBind.coffee ├── viewmodel-property.js └── viewmodel.coffee ├── package.js ├── test.bat └── tests ├── bindings.coffee ├── jquery-patch.js ├── sinon-restore.js ├── template.coffee ├── viewmodel-check.coffee ├── viewmodel-instance.coffee ├── viewmodel-parseBind.coffee ├── viewmodel-property.coffee └── viewmodel.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | /.meteor/local 2 | /.git 3 | /.build 4 | /packages 5 | /c 6 | /p 7 | /*.iml 8 | /.idea 9 | /smart.lock -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@7.0.7 2 | babel-runtime@1.2.0 3 | base64@1.0.10 4 | blaze@2.3.2 5 | blaze-tools@1.0.10 6 | caching-compiler@1.1.9 7 | caching-html-compiler@1.0.6 8 | check@1.3.0 9 | coffeescript@2.0.3_4 10 | coffeescript-compiler@2.0.3_4 11 | deps@1.0.12 12 | diff-sequence@1.1.0 13 | dynamic-import@0.3.0 14 | ecmascript@0.10.6 15 | ecmascript-runtime@0.5.0 16 | ecmascript-runtime-client@0.6.0 17 | ecmascript-runtime-server@0.5.0 18 | ejson@1.1.0 19 | html-tools@1.0.11 20 | htmljs@1.0.11 21 | http@1.4.0 22 | id-map@1.1.0 23 | jquery@1.11.10 24 | local-test:manuel:viewmodel@6.3.7 25 | manuel:isdev@1.0.0 26 | manuel:reactivearray@1.0.9 27 | manuel:viewmodel@6.3.7 28 | manuel:viewmodel-debug@2.7.2 29 | meteor@1.8.2 30 | modules@0.11.3 31 | modules-runtime@0.9.1 32 | mongo-id@1.0.6 33 | observe-sequence@1.0.16 34 | ordered-dict@1.1.0 35 | promise@0.10.1 36 | random@1.1.0 37 | reactive-dict@1.2.0 38 | reactive-var@1.0.11 39 | reload@1.2.0 40 | sha@1.0.9 41 | spacebars@1.0.15 42 | spacebars-compiler@1.1.2 43 | templating@1.1.12_1 44 | templating-tools@1.1.1 45 | tracker@1.1.3 46 | underscore@1.0.10 47 | url@1.2.0 48 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## 6.3.8 2 | 3 | - Fix issue when getting/setting values in nested properties. 4 | 5 | ## 6.3.3 6 | 7 | - Fix arrays returning more than their items. 8 | 9 | ## 6.3.2 10 | 11 | - style binding now removes styles when set to null 12 | 13 | ## 6.3.0 14 | 15 | - Add vmRef global helper. See: https://viewmodel.org/docs/misc#usingcontrols 16 | 17 | ## 6.2.1 18 | 19 | - fix validation for undefined values 20 | 21 | ## 6.2.0 22 | 23 | - Add global properties 24 | 25 | ## 6.1.5 26 | 27 | - Fix value/throttle with complex binds 28 | 29 | ## 6.1.4 30 | 31 | - Fix the value/throttle issues by saving the next value when throttled 32 | 33 | ## 6.1.3 34 | 35 | - Hidden elements now respond to both input and change events 36 | 37 | ## 6.1.2 38 | 39 | - Use change event for select elements 40 | 41 | ## 6.1.1 42 | 43 | - value binding now only uses 'input' event 44 | 45 | ## 6.1.0 46 | 47 | - optionsText can now point to a vm function 48 | 49 | ## 6.0.0 50 | 51 | - Short circuit && || operators in bindings. 52 | - Prevent using "b" as a view model property. It conflicts with Blaze. 53 | - Throw error if you try to use a reserved property. 54 | 55 | vmId 56 | vmPathToParent 57 | vmOnCreated 58 | vmOnRendered 59 | vmOnDestroyed 60 | vmAutorun 61 | vmEvents 62 | vmInitial 63 | vmProp 64 | templateInstance 65 | templateName 66 | parent 67 | children 68 | child 69 | reset 70 | data 71 | b 72 | 73 | ## 5.0.1 74 | 75 | - Bindings parser can now handle negative numbers inside parenthesis operations 76 | 77 | ## 5.0.0 78 | 79 | - Before: If a mixin A used another mixin B and you scope A to a property, mixin B would attach to the root of view model. 80 | - Now: mixin B gets attached to the same property as A 81 | 82 | ## 4.1.9 83 | 84 | - Don't save on url in older browsers 85 | 86 | ## 4.1.8 87 | 88 | - enable/disable bindings add disabled attribute to select elements 89 | 90 | ## 4.1.7 91 | 92 | - Fix throttle with input value starting blank then deleting the text before the time is up. 93 | 94 | ## 4.1.6 95 | 96 | - Check for null/undefined in value binding when the property starts with a null/undefined 97 | 98 | ## 4.1.5 99 | 100 | - Check for null/undefined in value binding 101 | 102 | ## 4.1.4 103 | 104 | - ViewModel.property.array is now reactive 105 | 106 | ## 4.1.3 107 | 108 | - Fix array validator 109 | - Lower ecmascript version 110 | 111 | ## 4.1.2 112 | 113 | - Add automatic array validator 114 | 115 | ## 4.1.1 116 | 117 | - Fixed issue with onUrl 118 | 119 | ## 4.1.0 120 | 121 | - Added validations. See https://viewmodel.org/docs/viewmodels#validation 122 | - Added .templateName() to view models. 123 | 124 | ## 4.0.14 125 | 126 | - Throw an error if a bind can't be parsed. 127 | 128 | ## 4.0.13 129 | 130 | - Allow .children to use a template name plus a function 131 | 132 | ## 4.0.12 133 | 134 | - Check that a view model property exists before using it as a template helper. 135 | 136 | ## 4.0.11 137 | 138 | - I don't even know what to say... 139 | 140 | ## 4.0.10 141 | 142 | - Only process bindings once 143 | 144 | ## 4.0.9 145 | 146 | - Don't execute onCreated/onRendered/autoruns if the template instance is destroyed 147 | 148 | ## 4.0.8 149 | 150 | - Don't try to bind to an element if its view is destroyed 151 | 152 | ## 4.0.7 153 | 154 | - Use afterFlush instead of onViewReady (so it can work better with packages like jagi:astronomy and tap:i18n) 155 | 156 | ## 4.0.6 157 | 158 | - Fix automatic state save across hot code push when using appcache package. 159 | 160 | ## 4.0.5 161 | 162 | - Don't check if an event is supported (the code was buggy) 163 | 164 | ## 4.0.4 165 | 166 | - Update docs url (now that \*.meteor.com is going down) 167 | 168 | ## 4.0.3 169 | 170 | - AddAttributeBinding is now case sensitive 171 | 172 | ## 4.0.2 173 | 174 | - Make attributes case sensitive 175 | 176 | ## 4.0.1 177 | 178 | - Fix binding conditionals when it starts with a negative 179 | 180 | ## 4.0.0 181 | 182 | - New ViewModel.addAttributeBinding to add attribute as bindings. See https://viewmodel.org/docs/bindings#attr 183 | 184 | ### BREAKING CHANGES 185 | 186 | - src, readonly, and href used to be default bindings which mapped to their corresponding attributes. Now they're not. If you use these bindings you now have to add them with ViewModel.addAttributeBinding( ['src','href','readonly'] ) 187 | 188 | ## 3.4.10 189 | 190 | - Fix signals with Firefox 191 | 192 | ## 3.4.9 193 | 194 | - Events are now loaded from mixin/share/load too 195 | 196 | ## 3.4.8 197 | 198 | - Check for parent node missing when calculating template's path. 199 | 200 | ## 3.4.7 201 | 202 | - Fix hot code push with view models with an id property. 203 | 204 | ## 3.4.6 205 | 206 | - Only run defining function once. 207 | 208 | ## 3.4.5 209 | 210 | - Change doesn't trigger on first run. 211 | 212 | ## 3.4.4 213 | 214 | - Load context onCreated so Blaze helpers can use inherited properties. 215 | 216 | ## 3.4.3 217 | 218 | - Style strings now accept semi-colons. 219 | 220 | ## 3.4.2 221 | 222 | - Fix onCreated. Delay loading data when running in a simulation 223 | 224 | ## 3.4.1 225 | 226 | - Fix this.parent() from onRendered 227 | 228 | ## 3.4.0 229 | 230 | - Add refGroup binding. see https://viewmodel.org/docs/bindings#refgroup 231 | 232 | ## 3.3.5 233 | 234 | - Simplify override priority. 235 | 236 | ## 3.3.4 237 | 238 | - Context properties override even functions. 239 | 240 | ## 3.3.3 241 | 242 | - Initial load order overrides even functions. 243 | 244 | ## 3.3.2 245 | 246 | - Throw nice error when trying to access a non property in your bindings 247 | 248 | ## 3.3.1 249 | 250 | - Allow .load to load an array of objects with hooks (like onRendered) 251 | 252 | ## 3.3.0 253 | 254 | - Add throttle to signals. 255 | 256 | ## 3.2.0 257 | 258 | - Add a way to transform signals. See: https://viewmodel.org/docs/viewmodels#signal 259 | 260 | ## 3.1.2 261 | 262 | - Fix onCreated/onRendered/onDestroyed/autorun when using an array of functions 263 | 264 | ## 3.1.1 265 | 266 | - Fix onRendered so it happens after bindings are in place. 267 | 268 | ## 3.1.0 269 | 270 | - Add viewmodel.child method which returns the first child it finds with the given criteria. See the docs. 271 | 272 | ## 3.0.0 273 | 274 | - Add signals to capture stream of events that happen outside the view models. 275 | - Fix options binding on Firefox 276 | - Set order of load priority: context props, direct props, from load, mixin, share, signal 277 | - Return undefined when ViewModel.find and .findOne can't find the given template 278 | - onCreated now runs when the template is created. 279 | 280 | ### BREAKING CHANGES 281 | 282 | - onCreated now runs when the template is created. This means, by the time onCreated is called, the view model will not have properties automatically added from the markup. I don't expect this to affect many people, if at all. You should be able to upgrade without a problem. Check where you use onCreated just to make sure. 283 | - The order of load priority is now: context props, direct props, from load, mixin, share, signal. This will only affect you if you use the same property name multiple times in the same view model. For example if you have a mixin with a property `name` and a view model that uses that mixin and also has `name` defined for itself. In those cases check that you're getting the expected result. 284 | 285 | ## 2.9.3 286 | 287 | - Fix cleanup when a template is destroyed. It was leaving a reference to the view model on ViewModel.byTemplate 288 | - Give a better error when trying to access a property of undefined/null in the template. 289 | 290 | ## 2.9.2 291 | 292 | - Warn if you try to put \_id on the url without specifying a vmTag property. 293 | 294 | ## 2.9.1 295 | 296 | - Fix ViewModel.find 297 | 298 | ## 2.9.0 299 | 300 | - Add ViewModel.find and ViewModel.findOne - They both take an optional string with the name of the template and an optional function to find a template. 301 | 302 | ## 2.8.1 303 | 304 | - .children() uses the .parent instead of its own logic. 305 | 306 | ## 2.8.0 307 | 308 | - You can now add an array of strings and objects to mixin and share properties. 309 | 310 | ## 2.7.8 311 | 312 | - onCreated/onRendered/onDestroyed/autorun now work when defining the view model with a function. 313 | 314 | ## 2.7.7 315 | 316 | - .parent() now searches for the first view model up the chain. Not just the parent template. 317 | 318 | ## 2.7.6 319 | 320 | - Using viewmodel-debug@2.5.1 321 | 322 | ## 2.7.5 323 | 324 | - Missed underscore 1.0.3 325 | 326 | ## 2.7.4 327 | 328 | - Reduce version requirements so it works with Meteor 1.1 329 | 330 | ## 2.7.3 331 | 332 | - options binding now works with mongo collections 333 | 334 | ## 2.7.2 335 | 336 | - Order of events are now correct: onCreated -> onRendered -> autorun 337 | 338 | ## 2.7.1 339 | 340 | - View model methods used as template helpers now receive the parameters from the template. 341 | 342 | ## 2.7.0 343 | 344 | - mixins and shares can be scoped to a view model property. So instead of adding all share/mixin properties to the view model, you can specify under which property they should fall. See: https://viewmodel.org/docs/viewmodels#sharemixinscope 345 | 346 | ## 2.6.0 347 | 348 | - .load now accepts an array of objects 349 | - You can now load multiple objects when you define a view model( .viewmodel({ load: objOrArray }) ) 350 | - Loaded objects can have their own autorun/onCreated/onRendered/onDestroyed properties. 351 | 352 | ## 2.5.5 353 | 354 | - Fix to the fix 355 | 356 | ## 2.5.4 357 | 358 | - Fix issue when using Iron Router's contentFor blocks 359 | 360 | ## 2.5.3 361 | 362 | - Fix issue with blaze helpers not being wired up correctly when using nested #each blocks. 363 | 364 | ## 2.5.2 365 | 366 | - autoruns now receive a computation parameter (as they should). 367 | 368 | ## 2.5.1 369 | 370 | - Trim parameters used when using functions declared in bindings 371 | 372 | ## 2.5.0 373 | 374 | - Properties are automatically added to the view models when used in the markup. This alleviates the problem of inheriting from Mongo documents where one might be missing a field. 375 | 376 | ## 2.4.2 377 | 378 | - Update readme 379 | 380 | ## 2.4.1 381 | 382 | - Make `this` reactive when used in a binding inside an `#each` block 383 | 384 | ## 2.4.0 385 | 386 | - Add error messages when the bind/event doesn't exist. 387 | - Add href, src, readonly bindings. 388 | - Check .viewmodel args 389 | 390 | ## 2.3.2 391 | 392 | - Fix inherited contexts 393 | 394 | ## 2.3.1 395 | 396 | - Fix autorun when given an array of functions 397 | 398 | ## 2.3.0 399 | 400 | - Add ViewModel.elementBind for testing 401 | 402 | ## 2.2.2 403 | 404 | - Fix setVmValue when the value to set is taken from the view model 405 | 406 | ## 2.2.1 407 | 408 | - Add events 409 | 410 | ## 2.1.1 411 | 412 | - Fix issue when using {{b and {{on at the same time 413 | 414 | ## 2.1.0 415 | 416 | - Add persist option for individual view models 417 | 418 | ## 2.0.0 419 | 420 | - Hello World! 421 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Manuel DeLeon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ViewModel 2 | ## A new level of simplicity 3 | 4 | ViewModel is a view layer for Meteor. You can think of it as Angular, Knockout, Aurelia, Vue, etc. but without the boilerplate code required to make those work. 5 | 6 | Here are some things it can do to make your life easier: 7 | 8 | - [Less code to get things done][1] 9 | - [State is automatically saved for you across hot code pushes][2] 10 | - [Save the state on the url][3] 11 | - [Components can be used as controls][4] 12 | - [Share state between components][5] 13 | - [Compose view elements via mixins][6] 14 | - [Inline/scoped styles][7] 15 | - [Better error messages][8] 16 | 17 | Go to [viewmodel.org][9] for examples and full documentation. 18 | 19 | Go to the [help section][10] if you have any questions, comments, feedback, or just want to talk about anything related to ViewModel and Meteor. 20 | 21 | [1]:https://viewmodel.org/docs#comparison 22 | [2]:https://viewmodel.org/docs/misc#hotcodepush 23 | [3]:https://viewmodel.org/docs/misc#stateonurl 24 | [4]:https://viewmodel.org/docs/misc#controls 25 | [5]:https://viewmodel.org/docs/viewmodels#share 26 | [6]:https://viewmodel.org/docs/viewmodels#mixin 27 | [7]:https://viewmodel.org/docs/misc#inlinestyles 28 | [8]:https://viewmodel.org/docs/misc#bettererrors 29 | [9]:https://viewmodel.org/ 30 | [10]:https://viewmodel.org/help 31 | -------------------------------------------------------------------------------- /lib/bindings.coffee: -------------------------------------------------------------------------------- 1 | isArray = (obj) -> obj instanceof Array or Array.isArray(obj) 2 | 3 | addBinding = ViewModel.addBinding 4 | 5 | 6 | addBinding 7 | name: 'default' 8 | bind: (bindArg) -> 9 | bindArg.element.on bindArg.bindName, (event) -> 10 | bindArg.setVmValue(event) 11 | return 12 | return 13 | 14 | addBinding 15 | name: 'toggle' 16 | events: 17 | click: (bindArg) -> 18 | value = bindArg.getVmValue() 19 | bindArg.setVmValue(!value) 20 | 21 | showHide = (reverse) -> 22 | (bindArg) -> 23 | show = bindArg.getVmValue() 24 | show = !show if reverse 25 | if show 26 | bindArg.element.show() 27 | else 28 | bindArg.element.hide() 29 | 30 | addBinding 31 | name: 'if' 32 | autorun: showHide(false) 33 | 34 | addBinding 35 | name: 'visible' 36 | autorun: showHide(false) 37 | 38 | addBinding 39 | name: 'unless' 40 | autorun: showHide(true) 41 | 42 | addBinding 43 | name: 'hide' 44 | autorun: showHide(true) 45 | 46 | valueEvent = (bindArg) -> 47 | newVal = bindArg.element.val() 48 | vmVal = bindArg.getVmValue() 49 | vmVal = if `vmVal == null` then "" else vmVal.toString() 50 | if bindArg.elementBind.throttle and !bindArg.viewmodel.hasOwnProperty(bindArg.bindValue) 51 | bindArg.viewmodel[bindArg.bindValue] = {} 52 | if newVal isnt vmVal or (bindArg.elementBind.throttle and (!bindArg.viewmodel[bindArg.bindValue].hasOwnProperty('nextVal') or newVal isnt bindArg.viewmodel[bindArg.bindValue].nextVal )) 53 | if bindArg.elementBind.throttle 54 | bindArg.viewmodel[bindArg.bindValue].nextVal = newVal 55 | bindArg.setVmValue(newVal) 56 | else 57 | bindArg.setVmValue(newVal) 58 | return 59 | 60 | valueAutorun = (bindArg) -> 61 | newVal = bindArg.getVmValue() 62 | newVal = if `newVal == null` then "" else newVal.toString() 63 | bindArg.element.val(newVal) if newVal isnt bindArg.element.val() 64 | return 65 | 66 | addBinding 67 | name: 'value' 68 | events: 69 | 'input change': valueEvent 70 | autorun: valueAutorun 71 | 72 | addBinding 73 | name: 'text' 74 | autorun: (bindArg) -> 75 | bindArg.element.text bindArg.getVmValue() 76 | return 77 | 78 | addBinding 79 | name: 'html' 80 | autorun: (bindArg) -> 81 | bindArg.element.html bindArg.getVmValue() 82 | 83 | changeBinding = (eb) -> 84 | eb.value or eb.check or eb.text or eb.html or eb.focus or eb.hover or eb.toggle or eb.if or eb.visible or eb.unless or eb.hide or eb.enable or eb.disable 85 | 86 | addBinding 87 | name: 'change' 88 | bind: (bindArg)-> 89 | bindValue = changeBinding(bindArg.elementBind) 90 | bindArg.autorun (bindArg, c) -> 91 | newValue = bindArg.getVmValue(bindValue) 92 | bindArg.setVmValue newValue if not c.firstRun 93 | 94 | bindIf: (bindArg)-> changeBinding(bindArg.elementBind) 95 | 96 | addBinding 97 | name: 'enter' 98 | events: 99 | 'keyup': (bindArg, event) -> 100 | if event.which is 13 or event.keyCode is 13 101 | bindArg.setVmValue(event) 102 | 103 | addBinding 104 | name: 'attr' 105 | bind: (bindArg) -> 106 | for attr of bindArg.bindValue 107 | do (attr) -> 108 | bindArg.autorun -> 109 | bindArg.element[0].setAttribute attr, bindArg.getVmValue(bindArg.bindValue[attr]) 110 | return 111 | 112 | addBinding 113 | name: 'check' 114 | events: 115 | 'change': (bindArg) -> 116 | bindArg.setVmValue bindArg.element.is(':checked') 117 | return 118 | 119 | autorun: (bindArg) -> 120 | vmValue = bindArg.getVmValue() 121 | elementCheck = bindArg.element.is(':checked') 122 | bindArg.element.prop 'checked', vmValue if elementCheck isnt vmValue 123 | 124 | addBinding 125 | name: 'check' 126 | selector: 'input[type=radio]' 127 | events: 128 | 'change': (bindArg) -> 129 | checked = bindArg.element.is(':checked') 130 | bindArg.setVmValue checked 131 | rawElement = bindArg.element[0] 132 | if checked and name = rawElement.name 133 | bindArg.templateInstance.$("input[type=radio][name=#{name}]").each -> 134 | $(this).trigger('change') if rawElement isnt this 135 | return 136 | 137 | autorun: (bindArg) -> 138 | vmValue = bindArg.getVmValue() 139 | elementCheck = bindArg.element.is(':checked') 140 | bindArg.element.prop 'checked', vmValue if elementCheck isnt vmValue 141 | 142 | addBinding 143 | name: 'group' 144 | selector: 'input[type=checkbox]' 145 | events: 146 | 'change': (bindArg) -> 147 | vmValue = bindArg.getVmValue() 148 | elementValue = bindArg.element.val() 149 | if bindArg.element.is(':checked') 150 | vmValue.push elementValue if elementValue not in vmValue 151 | else 152 | vmValue.remove elementValue 153 | 154 | autorun: (bindArg) -> 155 | vmValue = bindArg.getVmValue() 156 | elementCheck = bindArg.element.is(':checked') 157 | elementValue = bindArg.element.val() 158 | newValue = elementValue in vmValue 159 | bindArg.element.prop 'checked', newValue if elementCheck isnt newValue 160 | 161 | addBinding 162 | name: 'group' 163 | selector: 'input[type=radio]' 164 | events: 165 | 'change': (bindArg) -> 166 | checked = bindArg.element.is(':checked') 167 | if checked 168 | bindArg.setVmValue bindArg.element.val() 169 | rawElement = bindArg.element[0] 170 | if name = rawElement.name 171 | bindArg.templateInstance.$("input[type=radio][name=#{name}]").each -> 172 | $(this).trigger('change') if rawElement isnt this 173 | return 174 | 175 | autorun: (bindArg) -> 176 | vmValue = bindArg.getVmValue() 177 | elementValue = bindArg.element.val() 178 | bindArg.element.prop 'checked', vmValue is elementValue 179 | 180 | addBinding 181 | name: 'class' 182 | bindIf: (bindArg) -> _.isString(bindArg.bindValue) 183 | bind: (bindArg) -> 184 | bindArg.prevValue = '' 185 | autorun: (bindArg) -> 186 | newValue = bindArg.getVmValue() 187 | bindArg.element.removeClass bindArg.prevValue 188 | bindArg.element.addClass newValue 189 | bindArg.prevValue = newValue 190 | 191 | addBinding 192 | name: 'class' 193 | bindIf: (bindArg) -> not _.isString(bindArg.bindValue) 194 | bind: (bindArg) -> 195 | for cssClass of bindArg.bindValue 196 | do (cssClass) -> 197 | bindArg.autorun -> 198 | if bindArg.getVmValue(bindArg.bindValue[cssClass]) 199 | bindArg.element.addClass cssClass 200 | else 201 | bindArg.element.removeClass cssClass 202 | return 203 | return 204 | 205 | addBinding 206 | name: 'style' 207 | priority: 2 208 | bindIf: (bindArg) -> _.isString(bindArg.bindValue) and bindArg.bindValue.charAt(0) is '[' 209 | autorun: (bindArg) -> 210 | itemString = bindArg.bindValue.substr(1, bindArg.bindValue.length - 2) 211 | items = itemString.split(',') 212 | for item in items 213 | value = bindArg.getVmValue($.trim(item)) 214 | for style of value 215 | bindArg.element[0].style[style] = value[style] 216 | return 217 | 218 | addBinding 219 | name: 'style' 220 | bindIf: (bindArg) -> _.isString(bindArg.bindValue) 221 | autorun: (bindArg) -> 222 | newValue = bindArg.getVmValue() 223 | if _.isString(newValue) 224 | if ~newValue.indexOf(";") 225 | newValue = newValue.split(";").join(",") 226 | newValue = ViewModel.parseBind(newValue) 227 | for style of newValue 228 | bindArg.element[0].style[style] = newValue[style] 229 | 230 | addBinding 231 | name: 'style' 232 | bindIf: (bindArg) -> not _.isString(bindArg.bindValue) 233 | bind: (bindArg) -> 234 | for style of bindArg.bindValue 235 | do (style) -> 236 | bindArg.autorun -> 237 | bindArg.element[0].style[style] = bindArg.getVmValue(bindArg.bindValue[style]) 238 | return 239 | return 240 | 241 | addBinding 242 | name: 'hover' 243 | bind: (bindArg) -> 244 | setBool = (val) -> 245 | return -> bindArg.setVmValue(val) 246 | bindArg.element.hover setBool(true), setBool(false) 247 | return 248 | 249 | addBinding 250 | name: 'focus' 251 | events: 252 | focus: (bindArg) -> 253 | bindArg.setVmValue(true) if not bindArg.getVmValue() 254 | return 255 | blur: (bindArg) -> 256 | bindArg.setVmValue(false) if bindArg.getVmValue() 257 | return 258 | autorun: (bindArg) -> 259 | value = bindArg.getVmValue() 260 | if bindArg.element.is(':focus') isnt value 261 | if value 262 | bindArg.element.focus() 263 | else 264 | bindArg.element.blur() 265 | return 266 | 267 | canDisable = (elem) -> elem.is('button') or elem.is('input') or elem.is('textarea') or elem.is('select') 268 | 269 | enable = (elem) -> 270 | if canDisable(elem) 271 | elem.removeAttr('disabled') 272 | else 273 | elem.removeClass('disabled') 274 | 275 | disable = (elem) -> 276 | if canDisable(elem) 277 | elem.attr('disabled', 'disabled') 278 | else 279 | elem.addClass('disabled') 280 | 281 | enableDisable = (reverse) -> 282 | (bindArg) -> 283 | isEnable = bindArg.getVmValue() 284 | isEnable = !isEnable if reverse 285 | if isEnable 286 | enable bindArg.element 287 | else 288 | disable bindArg.element 289 | 290 | addBinding 291 | name: 'enable' 292 | autorun: enableDisable(false) 293 | 294 | addBinding 295 | name: 'disable' 296 | autorun: enableDisable(true) 297 | 298 | addBinding 299 | name: 'options' 300 | selector: 'select:not([multiple])' 301 | autorun: (bindArg) -> 302 | optionsText = bindArg.elementBind.optionsText 303 | optionsValue = bindArg.elementBind.optionsValue 304 | selection = bindArg.getVmValue(bindArg.elementBind.value) 305 | bindArg.element.find('option').remove() 306 | defaultText = bindArg.elementBind.defaultText 307 | defaultValue = bindArg.elementBind.defaultValue 308 | if defaultText? or defaultValue? 309 | itemText = _.escape(defaultText? and bindArg.getVmValue(defaultText) or '') 310 | itemValue = _.escape(defaultValue? and bindArg.getVmValue(defaultValue) or '') 311 | bindArg.element.append("") 312 | source = bindArg.getVmValue() 313 | collection = if source instanceof Mongo.Cursor then source.fetch() else source 314 | for item in collection 315 | itemTextRaw = if optionsText 316 | if item.hasOwnProperty(optionsText) 317 | item[optionsText] 318 | else if _.isFunction(bindArg.viewmodel[optionsText]) 319 | bindArg.viewmodel[optionsText](item) 320 | else 321 | undefined 322 | else 323 | item 324 | itemText = _.escape(itemTextRaw) 325 | itemValue = _.escape(if optionsValue then item[optionsValue] else item) 326 | selected = if selection is itemValue then "selected='selected'" else "" 327 | bindArg.element.append("") 328 | return 329 | 330 | addBinding 331 | name: 'options' 332 | selector: 'select[multiple]' 333 | autorun: (bindArg) -> 334 | optionsText = bindArg.elementBind.optionsText 335 | optionsValue = bindArg.elementBind.optionsValue 336 | selection = bindArg.getVmValue(bindArg.elementBind.value) 337 | bindArg.element.find('option').remove() 338 | source = bindArg.getVmValue() 339 | collection = if source instanceof Mongo.Cursor then source.fetch() else source 340 | for item in collection 341 | itemTextRaw = if optionsText 342 | if item.hasOwnProperty(optionsText) 343 | item[optionsText] 344 | else if _.isFunction(bindArg.viewmodel[optionsText]) 345 | bindArg.viewmodel[optionsText](item) 346 | else 347 | undefined 348 | else 349 | item 350 | itemText = _.escape(itemTextRaw) 351 | itemValue = _.escape(if optionsValue then item[optionsValue] else item) 352 | selected = if itemValue in selection then "selected='selected'" else "" 353 | bindArg.element.append("") 354 | return 355 | 356 | addBinding 357 | name: 'value' 358 | selector: 'select[multiple]' 359 | events: 360 | change: (bindArg) -> 361 | elementValues = bindArg.element.val() 362 | selected = bindArg.getVmValue() 363 | if isArray(selected) 364 | selected.clear() 365 | if isArray(elementValues) 366 | selected.push v for v in elementValues 367 | return 368 | 369 | addBinding 370 | name: 'ref' 371 | bind: (bindArg) -> 372 | ViewModel.check "refBinding", bindArg 373 | bindArg.viewmodel[bindArg.bindValue] = bindArg.element 374 | return 375 | 376 | addBinding 377 | name: 'refGroup' 378 | bind: (bindArg) -> 379 | if not bindArg.viewmodel[bindArg.bindValue] 380 | bindArg.viewmodel[bindArg.bindValue] = $() 381 | group = bindArg.viewmodel[bindArg.bindValue] 382 | group.push.apply group, bindArg.element 383 | return 384 | 385 | addBinding 386 | name: 'value' 387 | selector: 'input[type=file]:not([multiple])' 388 | events: 389 | change: (bindArg, event) -> 390 | file = if event.target.files?.length then event.target.files[0] else null 391 | bindArg.setVmValue(file) 392 | return 393 | 394 | addBinding 395 | name: 'value' 396 | selector: 'input[type=file][multiple]' 397 | events: 398 | change: (bindArg, event) -> 399 | files = bindArg.getVmValue() 400 | files.clear() 401 | files.push(file) for file in event.target.files 402 | -------------------------------------------------------------------------------- /lib/lzstring.js: -------------------------------------------------------------------------------- 1 | LZString=function(){function o(o,r){if(!t[o]){t[o]={};for(var n=0;ne;e++){var s=r.charCodeAt(e);n[2*e]=s>>>8,n[2*e+1]=s%256}return n},decompressFromUint8Array:function(o){if(null===o||void 0===o)return i.decompress(o);for(var n=new Array(o.length/2),e=0,t=n.length;t>e;e++)n[e]=256*o[2*e]+o[2*e+1];var s=[];return n.forEach(function(o){s.push(r(o))}),i.decompress(s.join(""))},compressToEncodedURIComponent:function(o){return null==o?"":i._compress(o,6,function(o){return e.charAt(o)})},decompressFromEncodedURIComponent:function(r){return null==r?"":""==r?null:(r=r.replace(/ /g,"+"),i._decompress(r.length,32,function(n){return o(e,r.charAt(n))}))},compress:function(o){return i._compress(o,16,function(o){return r(o)})},_compress:function(o,r,n){if(null==o)return"";var e,t,i,s={},p={},u="",c="",a="",l=2,f=3,h=2,d=[],m=0,v=0;for(i=0;ie;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++),s[c]=f++,a=String(u)}if(""!==a){if(Object.prototype.hasOwnProperty.call(p,a)){if(a.charCodeAt(0)<256){for(e=0;h>e;e++)m<<=1,v==r-1?(v=0,d.push(n(m)),m=0):v++;for(t=a.charCodeAt(0),e=0;8>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}else{for(t=1,e=0;h>e;e++)m=m<<1|t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t=0;for(t=a.charCodeAt(0),e=0;16>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1}l--,0==l&&(l=Math.pow(2,h),h++),delete p[a]}else for(t=s[a],e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;l--,0==l&&(l=Math.pow(2,h),h++)}for(t=2,e=0;h>e;e++)m=m<<1|1&t,v==r-1?(v=0,d.push(n(m)),m=0):v++,t>>=1;for(;;){if(m<<=1,v==r-1){d.push(n(m));break}v++}return d.join("")},decompress:function(o){return null==o?"":""==o?null:i._decompress(o.length,32768,function(r){return o.charCodeAt(r)})},_decompress:function(o,n,e){var t,i,s,p,u,c,a,l,f=[],h=4,d=4,m=3,v="",w=[],A={val:e(0),position:n,index:1};for(i=0;3>i;i+=1)f[i]=i;for(p=0,c=Math.pow(2,2),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(t=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;l=r(p);break;case 2:return""}for(f[3]=l,s=l,w.push(l);;){if(A.index>o)return"";for(p=0,c=Math.pow(2,m),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;switch(l=p){case 0:for(p=0,c=Math.pow(2,8),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 1:for(p=0,c=Math.pow(2,16),a=1;a!=c;)u=A.val&A.position,A.position>>=1,0==A.position&&(A.position=n,A.val=e(A.index++)),p|=(u>0?1:0)*a,a<<=1;f[d++]=r(p),l=d-1,h--;break;case 2:return w.join("")}if(0==h&&(h=Math.pow(2,m),m++),f[l])v=f[l];else{if(l!==d)return null;v=s+s.charAt(0)}w.push(v),f[d++]=s+v.charAt(0),h--,s=v,0==h&&(h=Math.pow(2,m),m++)}}};return i}();"function"==typeof define&&define.amd?define(function(){return LZString}):"undefined"!=typeof module&&null!=module&&(module.exports=LZString); -------------------------------------------------------------------------------- /lib/migration.coffee: -------------------------------------------------------------------------------- 1 | @Migration = new ReactiveDict("ViewModel_Migration") 2 | Reload._onMigrate -> 3 | return [true] if not ViewModel.persist 4 | migrated = {} 5 | for vmId, viewmodel of ViewModel.byId when !viewmodel.persist or viewmodel.persist() 6 | vmHash = viewmodel.vmHash() 7 | if migrated[vmHash] 8 | templateName = ViewModel.templateName(viewmodel.templateInstance) 9 | console.error "Could not create unique identifier for an instance of template '#{templateName}'. This can usually be resolved by wrapping a plain text in a div or adding a vmTag to the view model. Now you need to manually refresh the page. See https://viewmodel.org/docs/misc#hotcodepush for more information." 10 | return [false] 11 | migrated[vmHash] = 1 12 | Migration.set vmHash, viewmodel.data() 13 | return [true] 14 | -------------------------------------------------------------------------------- /lib/template.coffee: -------------------------------------------------------------------------------- 1 | Template.registerHelper 'b', ViewModel.bindHelper 2 | Template.registerHelper 'on', ViewModel.eventHelper 3 | 4 | Blaze.Template.prototype.viewmodel = (initial) -> 5 | template = this 6 | ViewModel.check 'T#viewmodel', initial, template 7 | ViewModel.check 'T#viewmodelArgs', template, arguments 8 | template.viewmodelInitial = initial 9 | template.onCreated ViewModel.onCreated(template, initial) 10 | template.onRendered ViewModel.onRendered(initial) 11 | template.onDestroyed ViewModel.onDestroyed(initial) 12 | initialObject = ViewModel.getInitialObject initial 13 | viewmodel = new ViewModel() 14 | viewmodel.load initialObject, true 15 | for eventGroup in viewmodel.vmEvents 16 | for event, eventFunction of eventGroup 17 | do (event, eventFunction) -> 18 | eventObj = {} 19 | eventObj[event] = (e, t) -> 20 | templateInstance = Template.instance() 21 | viewmodel = templateInstance.viewmodel 22 | eventFunction.call viewmodel, e, t 23 | template.events eventObj 24 | return 25 | 26 | Blaze.Template.prototype.createViewModel = (context) -> 27 | template = this 28 | initial = ViewModel.getInitialObject template.viewmodelInitial, context 29 | viewmodel = new ViewModel(initial) 30 | viewmodel.vmInitial = initial 31 | viewmodel 32 | 33 | htmls = { } 34 | Blaze.Template.prototype.elementBind = (selector, data) -> 35 | name = this.viewName 36 | html = null 37 | if data 38 | html = $("
").append($(Blaze.toHTMLWithData(this, data))) 39 | else if htmls[name] 40 | html = htmls[name] 41 | else 42 | html = $("
").append($(Blaze.toHTML(this))) 43 | htmls[name] = html 44 | 45 | bindId = html.find(selector).attr("b-id") 46 | bindOject = ViewModel.bindObjects[bindId] 47 | return bindOject 48 | 49 | Template.registerHelper 'vmRef', (prop) -> 50 | instance = Template.instance() 51 | return () -> 52 | return instance.viewmodel[prop] -------------------------------------------------------------------------------- /lib/viewmodel-onUrl.coffee: -------------------------------------------------------------------------------- 1 | isArray = (obj) -> obj instanceof Array or Array.isArray(obj) 2 | 3 | ((history) -> 4 | pushState = history.pushState 5 | replaceState = history.replaceState 6 | 7 | if (pushState) 8 | history.pushState = (state, title, url) -> 9 | if typeof history.onstatechange is 'function' 10 | history.onstatechange state, title, url 11 | pushState.apply history, arguments 12 | history.replaceState = (state, title, url) -> 13 | if typeof history.onstatechange is 'function' 14 | history.onstatechange state, title, url 15 | replaceState.apply history, arguments 16 | else 17 | history.pushState = -> 18 | history.replaceState = -> 19 | return 20 | ) window.history 21 | 22 | parseUri = (str) -> 23 | o = parseUri.options 24 | m = o.parser[(if o.strictMode then "strict" else "loose")].exec(str) 25 | uri = {} 26 | i = 14 27 | uri[o.key[i]] = m[i] or "" while i-- 28 | uri[o.q.name] = {} 29 | uri[o.key[12]].replace o.q.parser, ($0, $1, $2) -> 30 | uri[o.q.name][$1] = $2 if $1 31 | return 32 | 33 | uri 34 | 35 | parseUri.options = 36 | strictMode: false 37 | key: [ 38 | "source" 39 | "protocol" 40 | "authority" 41 | "userInfo" 42 | "user" 43 | "password" 44 | "host" 45 | "port" 46 | "relative" 47 | "path" 48 | "directory" 49 | "file" 50 | "query" 51 | "anchor" 52 | ] 53 | q: 54 | name: "queryKey" 55 | parser: /(?:^|&)([^&=]*)=?([^&]*)/g 56 | 57 | parser: 58 | strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/ 59 | loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ 60 | 61 | getUrl = (target = document.URL) -> parseUri(target) 62 | 63 | updateQueryString = (key, value, url) -> 64 | if !url 65 | url = window.location.href 66 | re = new RegExp('([?&])' + key + '=.*?(&|#|$)(.*)', 'gi') 67 | hash = undefined 68 | if re.test(url) 69 | if typeof value != 'undefined' and value != null 70 | url.replace re, '$1' + key + '=' + value + '$2$3' 71 | else 72 | hash = url.split('#') 73 | url = hash[0].replace(re, '$1$3').replace(/(&|\?)$/, '') 74 | if typeof hash[1] != 'undefined' and hash[1] != null 75 | url += '#' + hash[1] 76 | url 77 | else 78 | if typeof value != 'undefined' and value != null 79 | separator = if url.indexOf('?') != -1 then '&' else '?' 80 | hash = url.split('#') 81 | url = hash[0] + separator + key + '=' + value 82 | if typeof hash[1] != 'undefined' and hash[1] != null 83 | url += '#' + hash[1] 84 | url 85 | else 86 | url 87 | 88 | getSavedData = (url = document.URL) -> 89 | urlData = getUrl(url).queryKey.data 90 | return if not urlData 91 | dataString = LZString.decompressFromEncodedURIComponent(urlData) 92 | obj = {} 93 | try 94 | obj = JSON.parse(dataString) 95 | finally 96 | return obj 97 | 98 | ViewModel.saveUrl = (viewmodel) -> 99 | viewmodel.templateInstance.autorun (c) -> 100 | ViewModel.check '@saveUrl', viewmodel 101 | vmHash = viewmodel.vmHash() 102 | url = window.location.href 103 | savedData = getSavedData() or {} 104 | fields = if isArray(viewmodel.onUrl()) then viewmodel.onUrl() else [viewmodel.onUrl()] 105 | data = viewmodel.data(fields) 106 | savedData[vmHash] = data 107 | dataString = JSON.stringify savedData 108 | dataCompressed = LZString.compressToEncodedURIComponent dataString 109 | url = updateQueryString "data", dataCompressed, url 110 | window.history.pushState(null, null, url) if not c.firstRun and document.URL isnt url 111 | 112 | ViewModel.loadUrl = (viewmodel) -> 113 | updateFromUrl = (state, title, url = document.URL) -> 114 | data = getSavedData(url) 115 | return if not data 116 | vmHash = viewmodel.vmHash() 117 | savedData = data[vmHash] 118 | if savedData 119 | viewmodel.load savedData 120 | window.onpopstate = window.history.onstatechange = updateFromUrl 121 | updateFromUrl() -------------------------------------------------------------------------------- /lib/viewmodel-parseBind.coffee: -------------------------------------------------------------------------------- 1 | stringDouble = '"(?:[^"\\\\]|\\\\.)*"' 2 | stringSingle = '\'(?:[^\'\\\\]|\\\\.)*\'' 3 | stringRegexp = '/(?:[^/\\\\]|\\\\.)*/w*' 4 | specials = ',"\'{}()/:[\\]' 5 | everyThingElse = '[^\\s:,/][^' + specials + ']*[^\\s' + specials + ']' 6 | oneNotSpace = '[^\\s]' 7 | _bindingToken = RegExp(stringDouble + '|' + stringSingle + '|' + stringRegexp + '|' + everyThingElse + '|' + oneNotSpace, 'g') 8 | 9 | _divisionLookBehind = /[\])"'A-Za-z0-9_$]+$/ 10 | _keywordRegexLookBehind = 11 | in: 1 12 | return: 1 13 | typeof: 1 14 | 15 | _operators = "+-*/&|=><" 16 | 17 | ViewModel.parseBind = (objectLiteralString) -> 18 | str = $.trim(objectLiteralString) 19 | str = str.slice(1, -1) if str.charCodeAt(0) is 123 20 | result = {} 21 | toks = str.match(_bindingToken) 22 | depth = 0 23 | key = undefined 24 | values = undefined 25 | if toks 26 | toks.push ',' 27 | i = -1 28 | tok = undefined 29 | while tok = toks[++i] 30 | c = tok.charCodeAt(0) 31 | if c is 44 32 | if depth <= 0 33 | if key 34 | unless values 35 | throw new Error("Error parsing: " + objectLiteralString) 36 | else 37 | v = values.join '' 38 | v = @parseBind(v) if v.indexOf('{') is 0 39 | result[key] = v 40 | key = values = depth = 0 41 | continue 42 | else if c is 58 43 | unless values 44 | continue 45 | else if c is 47 and i and tok.length > 1 46 | match = toks[i-1].match(_divisionLookBehind) 47 | if match and not _keywordRegexLookBehind[match[0]] 48 | str = str.substr(str.indexOf(tok) + 1) 49 | toks = str.match(_bindingToken) 50 | toks.push(',') 51 | i = -1 52 | tok = '/' 53 | else if c is 40 or c is 123 or c is 91 54 | ++depth 55 | else if c is 41 or c is 125 or c is 93 56 | --depth 57 | else if not key and not values 58 | key = (if (c is 34 or c is 39) then tok.slice(1, -1) else tok) 59 | continue 60 | 61 | if ~_operators.indexOf(tok[0]) 62 | tok = ' ' + tok 63 | 64 | if ~_operators.indexOf(tok[tok.length - 1]) 65 | tok += ' ' 66 | 67 | if values 68 | values.push tok 69 | else 70 | values = [tok] 71 | if objectLiteralString and !Object.getOwnPropertyNames(result).length 72 | throw new Error("Error parsing: " + objectLiteralString) 73 | else 74 | return result -------------------------------------------------------------------------------- /lib/viewmodel-property.js: -------------------------------------------------------------------------------- 1 | const ValueTypes = { 2 | string: 1, 3 | number: 2, 4 | integer: 3, 5 | boolean: 4, 6 | object: 5, 7 | date: 6, 8 | array: 7, 9 | any: 8 10 | }; 11 | 12 | const isNull = function(obj) { 13 | return obj === null; 14 | }; 15 | 16 | const isUndefined = function(obj) { 17 | return typeof obj === "undefined"; 18 | }; 19 | 20 | const isNumber = function(obj) { 21 | // jQuery's isNumeric 22 | return !_.isArray(obj) && (obj - parseFloat(obj) + 1) >= 0; 23 | }; 24 | 25 | const isInteger = function(n) { 26 | if ( 27 | !isNumber(n) 28 | || ~n.toString().indexOf('.') 29 | ) return false; 30 | 31 | var value = parseFloat(n); 32 | return value === +value && value === (value | 0); 33 | }; 34 | 35 | const isObject = function(obj) { return (typeof obj === "object") && (obj !== null) && !( obj instanceof Date) ; }; 36 | 37 | class Property { 38 | constructor(){ 39 | this.checks = []; 40 | this.checksAsync = []; 41 | this.convertIns = []; 42 | this.convertOuts = []; 43 | this.beforeUpdates = []; 44 | this.afterUpdates = []; 45 | this.defaultValue = undefined; 46 | this.validMessageValue = ""; 47 | this.invalidMessageValue = ""; 48 | this.valueType = ValueTypes.any; 49 | 50 | } 51 | verify(value, context){ 52 | for(var check of this.checks) { 53 | if (!check.call(context, value)) return false; 54 | } 55 | return true; 56 | } 57 | verifyAsync(value, done, context){ 58 | for(var check of this.checksAsync) { 59 | check.call(context, value, done); 60 | } 61 | } 62 | hasAsync(){ 63 | return this.checksAsync.length; 64 | } 65 | setDefault(value){ 66 | if(typeof this.defaultValue === "undefined") this.defaultValue = value; 67 | } 68 | 69 | convertIn(fun){ 70 | this.convertIns.push(fun); 71 | return this; 72 | } 73 | convertOut(fun){ 74 | this.convertOuts.push(fun); 75 | return this; 76 | } 77 | 78 | beforeUpdate(fun){ 79 | this.beforeUpdates.push(fun); 80 | return this; 81 | } 82 | afterUpdate(fun){ 83 | this.afterUpdates.push(fun); 84 | return this; 85 | } 86 | 87 | convertValueIn(value, context){ 88 | let final = value; 89 | for(var convert of this.convertIns) { 90 | final = convert.call(context, final); 91 | } 92 | return final; 93 | } 94 | 95 | convertValueOut(value, context){ 96 | let final = value; 97 | for(var convert of this.convertOuts) { 98 | final = convert.call(context, final); 99 | } 100 | return final; 101 | } 102 | 103 | beforeValueUpdate(value, context){ 104 | for(var fun of this.beforeUpdates) { 105 | fun.call(context, value); 106 | } 107 | return final; 108 | } 109 | 110 | afterValueUpdate(value, context){ 111 | for(var fun of this.afterUpdates) { 112 | fun.call(context, value); 113 | } 114 | return final; 115 | } 116 | 117 | 118 | min(minValue) { 119 | this.checks.push((value) => { 120 | if (this.valueType === ValueTypes.string && _.isString(value) ) { 121 | return value.length >= minValue 122 | } else { 123 | return value >= minValue 124 | } 125 | }); 126 | return this; 127 | } 128 | 129 | max(maxValue) { 130 | this.checks.push((value) => { 131 | if (this.valueType === ValueTypes.string && _.isString(value) ) { 132 | return value.length <= maxValue 133 | } else { 134 | return value <= maxValue 135 | } 136 | }); 137 | return this; 138 | } 139 | 140 | equal(value) { 141 | this.checks.push( (v) => v === value); 142 | return this; 143 | } 144 | notEqual(value) { 145 | this.checks.push( (v) => v !== value); 146 | return this; 147 | } 148 | 149 | between(min, max) { 150 | this.checks.push((value) => { 151 | if (this.valueType === ValueTypes.string && _.isString(value) ) { 152 | return value.length >= min && value.length <= max; 153 | } else { 154 | return value >= min && value <= max; 155 | } 156 | }); 157 | return this; 158 | } 159 | notBetween(min, max) { 160 | this.checks.push((value) => { 161 | if (this.valueType === ValueTypes.string && _.isString(value) ) { 162 | return value.length < min || value.length > max; 163 | } else { 164 | return value < min || value > max; 165 | } 166 | }); 167 | return this; 168 | } 169 | 170 | regex(regexp) { 171 | this.checks.push( (v) => regexp.test(v) ); 172 | return this; 173 | } 174 | 175 | validate(fun) { 176 | this.checks.push(fun); 177 | return this; 178 | } 179 | 180 | validateAsync(fun){ 181 | this.checksAsync.push(fun); 182 | return this; 183 | } 184 | 185 | default(value) { 186 | this.defaultValue = value; 187 | return this; 188 | } 189 | validMessage(message) { 190 | this.validMessageValue = message; 191 | return this; 192 | } 193 | invalidMessage(message) { 194 | this.invalidMessageValue = message; 195 | return this; 196 | } 197 | 198 | get notBlank() { 199 | this.checks.push((value) => _.isString(value) && !!value.trim().length); 200 | return this; 201 | } 202 | get string() { 203 | this.setDefault(""); 204 | this.valueType = ValueTypes.string; 205 | this.checks.push((value) => _.isString(value)); 206 | return this; 207 | } 208 | get integer() { 209 | this.setDefault(0); 210 | this.valueType = ValueTypes.integer; 211 | this.checks.push((n) => isInteger(n) ); 212 | return this; 213 | } 214 | get number() { 215 | this.setDefault(0); 216 | this.valueType = ValueTypes.number; 217 | this.checks.push((value) => isNumber(value)); 218 | return this; 219 | } 220 | get boolean() { 221 | this.setDefault(false); 222 | this.valueType = ValueTypes.boolean; 223 | this.checks.push((value) => _.isBoolean(value)); 224 | return this; 225 | } 226 | get object() { 227 | this.setDefault({}); 228 | this.valueType = ValueTypes.object; 229 | this.checks.push((value) => isObject(value)); 230 | return this; 231 | } 232 | get date() { 233 | this.setDefault(new Date()); 234 | this.valueType = ValueTypes.date; 235 | this.checks.push((value) => value instanceof Date); 236 | return this; 237 | } 238 | get array() { 239 | this.setDefault([]); 240 | this.valueType = ValueTypes.array; 241 | this.checks.push((value) => _.isArray(value)); 242 | return this; 243 | } 244 | 245 | get convert(){ 246 | if (this.valueType === ValueTypes.integer){ 247 | this.convertIn( value => parseInt(value) ); 248 | } else if(this.valueType === ValueTypes.string) { 249 | this.convertIn( value => value.toString() ); 250 | } else if(this.valueType === ValueTypes.number) { 251 | this.convertIn( value => parseFloat(value) ); 252 | } else if(this.valueType === ValueTypes.date) { 253 | this.convertIn( value => Date.parse(value) ); 254 | } else if(this.valueType === ValueTypes.boolean) { 255 | this.convertIn( value => !!value ); 256 | } 257 | return this; 258 | } 259 | 260 | 261 | 262 | static validator(value) { 263 | const property = new Property(); 264 | if(_.isString(value)) { 265 | return property.string; 266 | } else if(_.isNumber(value)) { 267 | return property.number; 268 | } else if(_.isDate(value)) { 269 | return property.date; 270 | } else if(_.isBoolean(value)) { 271 | return property.boolean; 272 | } else if(isObject(value)) { 273 | return property.object; 274 | } else if(_.isArray(value)) { 275 | return property.array; 276 | } else { 277 | return property; 278 | } 279 | } 280 | } 281 | 282 | Object.defineProperties(ViewModel, { 283 | "property": { get: function () { return new Property; } } 284 | }); 285 | 286 | ViewModel.Property = Property; -------------------------------------------------------------------------------- /lib/viewmodel.coffee: -------------------------------------------------------------------------------- 1 | isArray = (obj) -> obj instanceof Array or Array.isArray(obj) 2 | 3 | class ViewModel 4 | 5 | #@@@@@@@@@@@@@@ 6 | # Class methods 7 | 8 | _nextId = 1 9 | @nextId = -> _nextId++ 10 | @persist = true 11 | 12 | # These are view model properties the user can use 13 | # but they have special meaning to ViewModel 14 | @properties = 15 | autorun: 1 16 | events: 1 17 | share: 1 18 | mixin: 1 19 | signal: 1 20 | ref: 1 21 | load: 1 22 | onRendered: 1 23 | onCreated: 1 24 | onDestroyed: 1 25 | 26 | # The user can't use these properties 27 | # when defining a view model 28 | @reserved = 29 | vmId: 1 30 | vmPathToParent: 1 31 | vmOnCreated: 1 32 | vmOnRendered: 1 33 | vmOnDestroyed: 1 34 | vmAutorun: 1 35 | vmEvents: 1 36 | vmInitial: 1 37 | vmProp: 1 38 | templateInstance: 1 39 | templateName: 1 40 | parent: 1 41 | children: 1 42 | child: 1 43 | reset: 1 44 | data: 1 45 | b: 1 46 | 47 | 48 | # These are objects used as bindings but do not have 49 | # an implementation 50 | @nonBindings = 51 | throttle: 1 52 | optionsText: 1 53 | optionsValue: 1 54 | defaultText: 1 55 | defaultValue: 1 56 | 57 | # Properties which the user needs to be more explicit in what they want 58 | # e.g. in a bind "if: prop.valid" the assumption is that the user wants to invoke 59 | # "prop.valid", not "prop().valid". If 'valid' is part of the object contained in 60 | # the property then the user must use the parenthesis: "if: prop().valid" 61 | @funPropReserved = 62 | valid: 1 63 | validMessage: 1 64 | invalid: 1 65 | invalidMessage: 1 66 | validating: 1 67 | message: 1 68 | 69 | @bindObjects = {} 70 | 71 | @byId = {} 72 | @byTemplate = {} 73 | @add = (viewmodel) -> 74 | ViewModel.byId[viewmodel.vmId] = viewmodel 75 | templateName = ViewModel.templateName(viewmodel.templateInstance) 76 | if templateName 77 | if not ViewModel.byTemplate[templateName] 78 | ViewModel.byTemplate[templateName] = {} 79 | ViewModel.byTemplate[templateName][viewmodel.vmId] = viewmodel 80 | 81 | @remove = (viewmodel) -> 82 | delete ViewModel.byId[viewmodel.vmId] 83 | templateName = ViewModel.templateName(viewmodel.templateInstance) 84 | if templateName 85 | delete ViewModel.byTemplate[templateName][viewmodel.vmId] 86 | 87 | @find = (templateNameOrPredicate, predicateOrNothing) -> 88 | templateName = _.isString(templateNameOrPredicate) and templateNameOrPredicate 89 | predicate = if templateName then predicateOrNothing else _.isFunction(templateNameOrPredicate) and templateNameOrPredicate 90 | 91 | vmCollection = if templateName then ViewModel.byTemplate[templateName] else ViewModel.byId 92 | return undefined if not vmCollection 93 | vmCollectionValues = _.values(vmCollection) 94 | if predicate 95 | return _.filter(vmCollection, predicate) 96 | else 97 | return vmCollectionValues 98 | 99 | @findOne = (templateNameOrPredicate, predicateOrNothing) -> 100 | return _.first ViewModel.find( templateNameOrPredicate, predicateOrNothing ) 101 | 102 | @check = (key, args...) -> 103 | if Meteor.isDev and not ViewModel.ignoreErrors 104 | Package['manuel:viewmodel-debug']?.VmCheck key, args... 105 | return 106 | 107 | @onCreated = (template) -> 108 | return -> 109 | templateInstance = this 110 | viewmodel = template.createViewModel(templateInstance.data) 111 | templateInstance.viewmodel = viewmodel 112 | viewmodel.templateInstance = templateInstance 113 | ViewModel.add viewmodel 114 | 115 | if templateInstance.data?.ref 116 | parentTemplate = ViewModel.parentTemplate(templateInstance) 117 | if parentTemplate 118 | if not parentTemplate.viewmodel 119 | ViewModel.addEmptyViewModel(parentTemplate) 120 | viewmodel.parent()[templateInstance.data.ref] = viewmodel 121 | 122 | loadData = -> 123 | ViewModel.delay 0, -> 124 | # Don't bother if the template 125 | # gets destroyed by the time it gets here (the next js cycle) 126 | return if templateInstance.isDestroyed 127 | 128 | ViewModel.assignChild(viewmodel) 129 | 130 | for obj in ViewModel.globals 131 | viewmodel.load(obj); 132 | 133 | vmHash = viewmodel.vmHash() 134 | if migrationData = Migration.get(vmHash) 135 | viewmodel.load(migrationData) 136 | ViewModel.removeMigration viewmodel, vmHash 137 | if viewmodel.onUrl 138 | ViewModel.loadUrl viewmodel 139 | ViewModel.saveUrl viewmodel 140 | 141 | autoLoadData = -> 142 | templateInstance.autorun -> 143 | viewmodel.load Template.currentData() 144 | 145 | # Can't use delay in a simulation. 146 | # By default onCreated runs in a computation 147 | if Tracker.currentComputation 148 | loadData() 149 | # Crap, I have no idea why I'm delaying the load 150 | # data from the context. I think Template.currentData() 151 | # blows up if it's called inside a computation ?_? 152 | ViewModel.delay 0, autoLoadData 153 | else 154 | # Loading the context data needs to happen immediately 155 | # so the Blaze helpers can work with inherited values 156 | autoLoadData() 157 | # Running in a simulation 158 | # setup the load data after tracker is done with the current queue 159 | Tracker.afterFlush -> 160 | loadData() 161 | 162 | for fun in viewmodel.vmOnCreated 163 | fun.call viewmodel, templateInstance 164 | 165 | helpers = {} 166 | for prop of viewmodel when not ViewModel.reserved[prop] 167 | do (prop) -> 168 | helpers[prop] = (args...) -> 169 | instanceVm = Template.instance().viewmodel 170 | # We have to check that the view model has the property 171 | # as they may not be present if they're inherited properties 172 | # See: https://github.com/ManuelDeLeon/viewmodel/issues/223 173 | return instanceVm[prop](args...) if instanceVm[prop] 174 | 175 | template.helpers helpers 176 | 177 | return 178 | 179 | @bindIdAttribute = 'b-id' 180 | 181 | @addEmptyViewModel = (templateInstance) -> 182 | template = templateInstance.view.template 183 | template.viewmodelInitial = {} 184 | onCreated = ViewModel.onCreated(template, template.viewmodelInitial) 185 | onCreated.call templateInstance 186 | onRendered = ViewModel.onRendered(template.viewmodelInitial) 187 | onRendered.call templateInstance 188 | onDestroyed = ViewModel.onDestroyed(template.viewmodelInitial) 189 | templateInstance.view.onViewDestroyed -> 190 | onDestroyed.call templateInstance 191 | return 192 | 193 | getBindHelper = (useBindings) -> 194 | bindIdAttribute = ViewModel.bindIdAttribute 195 | bindIdAttribute += "-e" if not useBindings 196 | return (bindString) -> 197 | bindId = ViewModel.nextId() 198 | bindObject = ViewModel.parseBind bindString 199 | ViewModel.bindObjects[bindId] = bindObject 200 | templateInstance = Template.instance() 201 | 202 | if not templateInstance.viewmodel 203 | ViewModel.addEmptyViewModel(templateInstance) 204 | 205 | bindings = if useBindings then ViewModel.bindings else _.pick(ViewModel.bindings, 'default') 206 | 207 | currentView = Blaze.currentView 208 | 209 | # The template on which the element is rendered might not be 210 | # the one where the user puts it on the html. If it sounds confusing 211 | # it's because it IS confusing. The only case I know of is with 212 | # Iron Router's contentFor blocks. 213 | # See https://github.com/ManuelDeLeon/viewmodel/issues/142 214 | currentViewInstance = currentView._templateInstance or templateInstance 215 | 216 | # Blaze.currentView.onViewReady fails for some packages like jagi:astronomy and tap:i18n 217 | Tracker.afterFlush -> 218 | return if currentView.isDestroyed # The element may be removed before it can even be bound/used 219 | element = currentViewInstance.$("[#{bindIdAttribute}='#{bindId}']") 220 | # Don't bind the element because of a context change 221 | if element.length and not element[0].vmBound 222 | return if not element.removeAttr 223 | element[0].vmBound = true 224 | element.removeAttr bindIdAttribute 225 | templateInstance.viewmodel.bind bindObject, templateInstance, element, bindings, bindId, currentView 226 | 227 | bindIdObj = {} 228 | bindIdObj[bindIdAttribute] = bindId 229 | return bindIdObj 230 | 231 | @bindHelper = getBindHelper(true) 232 | @eventHelper = getBindHelper(false) 233 | 234 | @getInitialObject = (initial, context) -> 235 | if _.isFunction(initial) 236 | return initial(context) or {} 237 | else 238 | return initial or {} 239 | 240 | delayed = { } 241 | @delay = (time, nameOrFunc, fn) -> 242 | func = fn || nameOrFunc 243 | name = nameOrFunc if fn 244 | d = delayed[name] if name 245 | Meteor.clearTimeout d if d? 246 | id = Meteor.setTimeout func, time 247 | delayed[name] = id if name 248 | 249 | @makeReactiveProperty = (initial, viewmodel) -> 250 | dependency = new Tracker.Dependency() 251 | initialValue = if initial instanceof ViewModel.Property 252 | initial.defaultValue 253 | else 254 | initial 255 | 256 | _value = undefined 257 | reset = -> 258 | if isArray(initialValue) 259 | _value = new ReactiveArray(initialValue, dependency) 260 | else 261 | _value = initialValue 262 | 263 | reset() 264 | 265 | validator = if initial instanceof ViewModel.Property 266 | initial 267 | else 268 | ViewModel.Property.validator(initial) 269 | 270 | funProp = (value) -> 271 | if arguments.length 272 | if _value isnt value 273 | changeValue = -> 274 | 275 | if validator.beforeUpdates.length 276 | validator.beforeValueUpdate(_value, viewmodel); 277 | 278 | if isArray(value) 279 | _value = new ReactiveArray(value, dependency) 280 | else 281 | _value = value 282 | 283 | if validator.convertIns.length 284 | _value = validator.convertValueIn(_value, viewmodel); 285 | 286 | if validator.afterUpdates.length 287 | validator.afterValueUpdate(_value, viewmodel); 288 | 289 | dependency.changed() 290 | if funProp.delay > 0 291 | ViewModel.delay funProp.delay, funProp.vmProp, changeValue 292 | else 293 | changeValue() 294 | else 295 | dependency.depend() 296 | 297 | if validator.convertOuts.length 298 | return validator.convertValueOut(_value, viewmodel); 299 | else 300 | return _value; 301 | 302 | funProp.reset = -> 303 | reset() 304 | dependency.changed() 305 | 306 | funProp.depend = -> dependency.depend() 307 | funProp.changed = -> dependency.changed() 308 | funProp.delay = 0 309 | funProp.vmProp = ViewModel.nextId() 310 | 311 | 312 | 313 | hasAsync = validator.hasAsync() 314 | validDependency = undefined 315 | validatingItems = undefined 316 | if hasAsync 317 | validDependency = new Tracker.Dependency() 318 | validatingItems = new ReactiveArray() 319 | 320 | validationAsync = {} 321 | 322 | getDone = if hasAsync 323 | (initialValue) -> 324 | validatingItems.push(1) 325 | return (result) -> 326 | validatingItems.pop() 327 | if _value is initialValue and not ((validationAsync.value is _value) or result) 328 | validationAsync = { value: _value } 329 | validDependency.changed() 330 | 331 | funProp.valid = (noAsync) -> 332 | dependency.depend() 333 | if hasAsync 334 | validDependency.depend() 335 | if validationAsync and validationAsync.hasOwnProperty('value') and validationAsync.value is _value 336 | return false 337 | else 338 | if hasAsync and !noAsync 339 | validator.verifyAsync(_value, getDone(_value), viewmodel) 340 | return validator.verify(_value, viewmodel) 341 | 342 | funProp.validMessage = -> validator.validMessageValue 343 | 344 | funProp.invalid = (noAsync) -> not this.valid(noAsync) 345 | funProp.invalidMessage = -> validator.invalidMessageValue 346 | 347 | funProp.validating = -> 348 | return false if not hasAsync 349 | validatingItems.depend() 350 | return !!validatingItems.length 351 | 352 | funProp.message = -> 353 | if this.valid(true) 354 | return validator.validMessageValue 355 | else 356 | return validator.invalidMessageValue 357 | 358 | # to give the feel of non reactivity 359 | Object.defineProperty funProp, 'value', { get: -> _value} 360 | 361 | return funProp 362 | 363 | @bindings = {} 364 | @addBinding = (binding) -> 365 | ViewModel.check "@addBinding", binding 366 | binding.priority = 1 if not binding.priority 367 | binding.priority++ if binding.selector 368 | binding.priority++ if binding.bindIf 369 | 370 | bindings = ViewModel.bindings 371 | if not bindings[binding.name] 372 | bindings[binding.name] = [] 373 | bindingArray = bindings[binding.name] 374 | bindingArray[bindingArray.length] = binding 375 | return 376 | 377 | @addAttributeBinding = (attrs) -> 378 | if isArray(attrs) 379 | for attr in attrs 380 | do (attr) -> 381 | ViewModel.addBinding 382 | name: attr 383 | bind: (bindArg) -> 384 | bindArg.autorun -> 385 | bindArg.element[0].setAttribute attr, bindArg.getVmValue(bindArg.bindValue[attr]) 386 | return 387 | else if _.isString(attrs) 388 | ViewModel.addBinding 389 | name: attrs 390 | bind: (bindArg) -> 391 | bindArg.autorun -> 392 | bindArg.element[0].setAttribute attrs, bindArg.getVmValue(bindArg.bindValue[attrs]) 393 | return 394 | return 395 | 396 | @getBinding = (bindName, bindArg, bindings) -> 397 | binding = null 398 | bindingArray = bindings[bindName] 399 | if bindingArray 400 | if bindingArray.length is 1 and not (bindingArray[0].bindIf or bindingArray[0].selector) 401 | binding = bindingArray[0] 402 | else 403 | binding = _.find(_.sortBy(bindingArray, ((b)-> -b.priority)), (b) -> 404 | not ( (b.bindIf and not b.bindIf(bindArg)) or (b.selector and not bindArg.element.is(b.selector)) ) 405 | ) 406 | return binding or ViewModel.getBinding('default', bindArg, bindings) 407 | 408 | getDelayedSetter = (bindArg, setter, bindId) -> 409 | if bindArg.elementBind.throttle 410 | return (args...) -> 411 | ViewModel.delay bindArg.getVmValue(bindArg.elementBind.throttle), bindId, -> setter(args...) 412 | else 413 | return setter 414 | 415 | @getBindArgument = (templateInstance, element, bindName, bindValue, bindObject, viewmodel, bindId, view) -> 416 | bindArg = 417 | templateInstance: templateInstance 418 | autorun: (f) -> 419 | fun = (c) -> f(bindArg, c) 420 | templateInstance.autorun fun 421 | return 422 | element: element 423 | elementBind: bindObject 424 | getVmValue: ViewModel.getVmValueGetter(viewmodel, bindValue, view) 425 | bindName: bindName 426 | bindValue: bindValue 427 | viewmodel: viewmodel 428 | 429 | bindArg.setVmValue = getDelayedSetter bindArg, ViewModel.getVmValueSetter(viewmodel, bindValue, view), bindId 430 | return bindArg 431 | 432 | @bindSingle = (templateInstance, element, bindName, bindValue, bindObject, viewmodel, bindings, bindId, view) -> 433 | bindArg = ViewModel.getBindArgument templateInstance, element, bindName, bindValue, bindObject, viewmodel, bindId, view 434 | binding = ViewModel.getBinding(bindName, bindArg, bindings) 435 | return if not binding 436 | 437 | if binding.bind 438 | binding.bind bindArg 439 | 440 | if binding.autorun 441 | bindArg.autorun binding.autorun 442 | 443 | if binding.events 444 | for eventName, eventFunc of binding.events 445 | do (eventName, eventFunc) -> 446 | element.bind eventName, (e) -> eventFunc(bindArg, e) 447 | return 448 | 449 | stringRegex = /^(?:"(?:[^"]|\\")*[^\\]"|'(?:[^']|\\')*[^\\]')$/ 450 | quoted = (str) -> stringRegex.test(str) 451 | removeQuotes = (str) -> str.substr(1, str.length - 2) 452 | isPrimitive = (val) -> 453 | val is "true" or val is "false" or val is "null" or val is "undefined" or $.isNumeric(val) 454 | 455 | getPrimitive = (val) -> 456 | switch val 457 | when "true" then true 458 | when "false" then false 459 | when "null" then null 460 | when "undefined" then undefined 461 | else (if $.isNumeric(val) then parseFloat(val) else val) 462 | 463 | tokens = 464 | '**': (a, b) -> a ** b 465 | '*': (a, b) -> a * b 466 | '/': (a, b) -> a / b 467 | '%': (a, b) -> a % b 468 | '+': (a, b) -> a + b 469 | '-': (a, b) -> a - b 470 | '<': (a, b) -> a < b 471 | '<=': (a, b) -> a <= b 472 | '>': (a, b) -> a > b 473 | '>=': (a, b) -> a >= b 474 | '==': (a, b) -> `a == b` 475 | '!==': (a, b) -> `a !== b` 476 | '===': (a, b) -> a is b 477 | '!===': (a, b) -> a isnt b 478 | '&&': (a, b) -> a && b 479 | '||': (a, b) -> a || b 480 | 481 | tokenGroup = {} 482 | for _t of tokens 483 | tokenGroup[_t.length] = {} if not tokenGroup[_t.length] 484 | tokenGroup[_t.length][_t] = 1 485 | 486 | dotRegex = /(\D\.)|(\.\D)/ 487 | 488 | firstToken = (str) -> 489 | tokenIndex = -1 490 | token = null 491 | inQuote = null 492 | parensCount = 0 493 | for c, i in str 494 | break if token 495 | if c is '"' or c is "'" 496 | if inQuote is c 497 | inQuote = null 498 | else if not inQuote 499 | inQuote = c 500 | else if not inQuote and (c is '(' or c is ')') 501 | if c is '(' 502 | parensCount++ 503 | if c is ')' 504 | parensCount-- 505 | else if not inQuote and parensCount is 0 and ~"+-*/%&|><=".indexOf(c) 506 | tokenIndex = i 507 | for length in [4..1] 508 | if str.length > tokenIndex + length 509 | candidateToken = str.substr(tokenIndex, length) 510 | if tokenGroup[length] and tokenGroup[length][candidateToken] 511 | token = candidateToken 512 | break 513 | return [token, tokenIndex] 514 | 515 | getMatchingParenIndex = (bindValue, parenIndexStart) -> 516 | return -1 if !~parenIndexStart 517 | openParenCount = 0 518 | for i in [parenIndexStart + 1 .. bindValue.length] 519 | currentChar = bindValue.charAt(i) 520 | if currentChar is ')' 521 | if openParenCount is 0 522 | return i 523 | else 524 | openParenCount-- 525 | else if currentChar is '(' 526 | openParenCount++ 527 | 528 | throw new Error("Unbalanced parenthesis") 529 | return 530 | 531 | currentView = null 532 | currentContext = -> 533 | if currentView 534 | Blaze.getData(currentView) 535 | else 536 | Template.instance()?.data 537 | 538 | getValue = (container, bindValue, viewmodel, funPropReserved, event) -> 539 | bindValue = bindValue.trim() 540 | return getPrimitive(bindValue) if isPrimitive(bindValue) 541 | [token, tokenIndex] = firstToken(bindValue) 542 | if ~tokenIndex 543 | left = getValue(container, bindValue.substring(0, tokenIndex), viewmodel) 544 | if (token is '&&' and not left) || (token is '||' and left) 545 | value = left 546 | else 547 | right = getValue(container, bindValue.substring(tokenIndex + token.length), viewmodel) 548 | value = tokens[token.trim()]( left, right ) 549 | else if bindValue is "this" 550 | value = currentContext() 551 | else if quoted(bindValue) 552 | value = removeQuotes(bindValue) 553 | else 554 | negate = bindValue.charAt(0) is '!' 555 | bindValue = bindValue.substring 1 if negate 556 | 557 | dotIndex = bindValue.search(dotRegex) 558 | dotIndex += 1 if ~dotIndex and bindValue.charAt(dotIndex) isnt '.' 559 | parenIndexStart = bindValue.indexOf('(') 560 | parenIndexEnd = getMatchingParenIndex(bindValue, parenIndexStart) 561 | 562 | breakOnFirstDot = ~dotIndex and (!~parenIndexStart or dotIndex < parenIndexStart or dotIndex is (parenIndexEnd + 1)) 563 | 564 | if breakOnFirstDot 565 | newBindValue = bindValue.substring(dotIndex + 1) 566 | newBindValueCheck = if newBindValue.endsWith('()') then newBindValue.substr(0, newBindValue.length - 2) else newBindValue 567 | newContainer = getValue container, bindValue.substring(0, dotIndex), viewmodel, ViewModel.funPropReserved[newBindValueCheck] 568 | value = getValue newContainer, newBindValue, viewmodel 569 | else 570 | if `container == null` 571 | value = undefined 572 | else 573 | name = bindValue 574 | args = [] 575 | if ~parenIndexStart 576 | parsed = ViewModel.parseBind(bindValue) 577 | name = Object.keys(parsed)[0] 578 | second = parsed[name] 579 | if second.length > 2 580 | for arg in second.substr(1, second.length - 2).split(',') #remove parenthesis 581 | arg = $.trim(arg) 582 | newArg = undefined 583 | if arg is "this" 584 | newArg = currentContext() 585 | else if quoted(arg) 586 | newArg = removeQuotes(arg) 587 | else 588 | neg = arg.charAt(0) is '!' 589 | arg = arg.substring 1 if neg 590 | 591 | arg = getValue(viewmodel, arg, viewmodel) 592 | if viewmodel and `arg in viewmodel` 593 | newArg = getValue(viewmodel, arg, viewmodel) 594 | else 595 | newArg = arg #getPrimitive(arg) 596 | newArg = !newArg if neg 597 | args.push newArg 598 | 599 | primitive = isPrimitive(name) 600 | if container instanceof ViewModel and not primitive and not container[name] 601 | container[name] = ViewModel.makeReactiveProperty(undefined, viewmodel) 602 | 603 | if !primitive and not (container? and (container[name]? or _.isObject(container))) 604 | errorMsg = "Can't access '#{name}' of '#{container}'." 605 | if viewmodel 606 | templateName = ViewModel.templateName(viewmodel.templateInstance) 607 | errorMsg += " This is for template '#{templateName}'." 608 | throw new Error errorMsg 609 | else if primitive 610 | value = getPrimitive(name) 611 | else if not (`name in container`) 612 | return undefined 613 | else 614 | if !funPropReserved and _.isFunction(container[name]) 615 | args.push(event) if event 616 | value = container[name].apply(container, args) 617 | else 618 | value = container[name] 619 | value = !value if negate 620 | 621 | return value 622 | 623 | @getVmValueGetter = (viewmodel, bindValue, view) -> 624 | return (optBindValue = bindValue) -> 625 | currentView = view 626 | getValue(viewmodel, optBindValue.toString(), viewmodel) 627 | 628 | setValue = (value, container, bindValue, viewmodel, event, initialProp) -> 629 | bindValue = bindValue.trim() 630 | return getPrimitive(bindValue) if isPrimitive(bindValue) 631 | [token, tokenIndex] = firstToken(bindValue) 632 | retValue = undefined 633 | if ~tokenIndex 634 | left = setValue(value, container, bindValue.substring(0, tokenIndex), viewmodel) 635 | return left if token is '&&' and not left 636 | return left if token is '||' and left 637 | right = setValue(value, container, bindValue.substring(tokenIndex + token.length), viewmodel) 638 | retValue = tokens[token.trim()]( left, right ) 639 | else if ~bindValue.indexOf(')', bindValue.length - 1) 640 | retValue = getValue(viewmodel, bindValue, viewmodel, undefined, event) 641 | else if dotRegex.test(bindValue) 642 | i = bindValue.search(dotRegex) 643 | i += 1 if bindValue.charAt(i) isnt '.' 644 | newContainer = getValue container, bindValue.substring(0, i), viewmodel, undefined 645 | newBindValue = bindValue.substring(i + 1) 646 | initProp = initialProp || container[bindValue.substring(0, i)] 647 | retValue = setValue value, newContainer, newBindValue, viewmodel, undefined, initProp 648 | else 649 | if _.isFunction(container[bindValue]) 650 | retValue = container[bindValue](value) 651 | else 652 | container[bindValue] = value 653 | if initialProp && initialProp.changed 654 | initialProp.changed(); 655 | retValue = value 656 | return retValue 657 | 658 | @getVmValueSetter = (viewmodel, bindValue, view) -> 659 | return (->) if not _.isString(bindValue) 660 | return (value) -> 661 | currentView = view 662 | setValue(value, viewmodel, bindValue, viewmodel, value) 663 | 664 | 665 | @parentTemplate = (templateInstance) -> 666 | view = templateInstance.view?.parentView 667 | while view 668 | if view.name.substring(0, 9) is 'Template.' or view.name is 'body' 669 | return view.templateInstance() 670 | view = view.parentView 671 | return 672 | 673 | @assignChild = (viewmodel) -> 674 | viewmodel.parent()?.children().push(viewmodel) 675 | return 676 | 677 | @onRendered = -> 678 | return -> 679 | templateInstance = this 680 | viewmodel = templateInstance.viewmodel 681 | initial = viewmodel.vmInitial 682 | ViewModel.check "@onRendered", initial.autorun, templateInstance 683 | 684 | # onRendered happens before onViewReady 685 | # We want bindings to be in place before we run 686 | # the onRendered functions and autoruns 687 | ViewModel.delay 0, -> 688 | # Don't bother running onRendered or autoruns if the template 689 | # gets destroyed by the time it gets here (the next js cycle) 690 | return if templateInstance.isDestroyed 691 | for fun in viewmodel.vmOnRendered 692 | fun.call viewmodel, templateInstance 693 | 694 | for autorun in viewmodel.vmAutorun 695 | do (autorun) -> 696 | fun = (c) -> autorun.call(viewmodel, c) 697 | templateInstance.autorun fun 698 | return 699 | return 700 | 701 | @loadProperties = (toLoad, container) -> 702 | loadObj = (obj) -> 703 | for key, value of obj when not ViewModel.properties[key] 704 | if ViewModel.reserved[key] 705 | throw new Error "Can't use reserved word '" + key + "' as a view model property." 706 | else 707 | if _.isFunction(value) 708 | # we don't care, just take the new function 709 | container[key] = value 710 | else if container[key] and container[key].vmProp and _.isFunction(container[key]) 711 | # keep the reference to the old property we already have 712 | container[key] value 713 | else 714 | # Create a new property 715 | container[key] = ViewModel.makeReactiveProperty(value, container); 716 | return 717 | if isArray(toLoad) 718 | loadObj obj for obj in toLoad 719 | else 720 | loadObj toLoad 721 | return 722 | 723 | ################## 724 | # Instance methods 725 | 726 | bind: (bindObject, templateInstance, element, bindings, bindId, view) -> 727 | viewmodel = this 728 | for bindName, bindValue of bindObject when not ViewModel.nonBindings[bindName] 729 | if ~bindName.indexOf(' ') 730 | for bindNameSingle in bindName.split(' ') 731 | ViewModel.bindSingle templateInstance, element, bindNameSingle, bindValue, bindObject, viewmodel, bindings, bindId, view 732 | else 733 | ViewModel.bindSingle templateInstance, element, bindName, bindValue, bindObject, viewmodel, bindings, bindId, view 734 | return 735 | 736 | loadMixinShare = (toLoad, collection, viewmodel, onlyEvents) -> 737 | if toLoad 738 | if isArray(toLoad) 739 | for element in toLoad 740 | if _.isString element 741 | #viewmodel.load collection[element], onlyEvents 742 | loadToContainer viewmodel, viewmodel, collection[element], onlyEvents 743 | # if viewmodel instanceof ViewModel 744 | # viewmodel.load collection[element], onlyEvents 745 | # else 746 | # ViewModel.loadProperties collection[element], viewmodel 747 | else 748 | loadMixinShare element, collection, viewmodel, onlyEvents 749 | else if _.isString toLoad 750 | loadToContainer viewmodel, viewmodel, collection[toLoad], onlyEvents 751 | # if viewmodel instanceof ViewModel 752 | # viewmodel.load collection[toLoad], onlyEvents 753 | # else 754 | # ViewModel.loadProperties collection[toLoad], viewmodel 755 | else 756 | for ref of toLoad 757 | container = {} 758 | mixshare = toLoad[ref] 759 | if isArray(mixshare) 760 | for item in mixshare 761 | # loadMixinShare collection[item], container, onlyEvents 762 | loadToContainer container, viewmodel, collection[item], onlyEvents 763 | # ViewModel.loadProperties collection[item], container 764 | else 765 | # loadMixinShare collection[mixshare], container, onlyEvents 766 | loadToContainer container, viewmodel, collection[mixshare], onlyEvents 767 | # ViewModel.loadProperties collection[mixshare], container 768 | viewmodel[ref] = container 769 | return 770 | 771 | loadToContainer = (container, viewmodel, toLoad, onlyEvents) -> 772 | return if not toLoad 773 | 774 | if isArray(toLoad) 775 | loadToContainer( container, viewmodel, item, onlyEvents ) for item in toLoad 776 | 777 | if not onlyEvents 778 | # Signals are loaded 1st 779 | signals = ViewModel.signalToLoad(toLoad.signal, container) 780 | for signal in signals 781 | loadToContainer container, viewmodel, signal, onlyEvents 782 | viewmodel.vmOnCreated.push signal.onCreated 783 | viewmodel.vmOnDestroyed.push signal.onDestroyed 784 | 785 | # Shared are loaded 2nd 786 | loadMixinShare toLoad.share, ViewModel.shared, container, onlyEvents 787 | 788 | # Mixins are loaded 3rd 789 | loadMixinShare toLoad.mixin, ViewModel.mixins, container, onlyEvents 790 | 791 | # Whatever is in 'load' is loaded before direct properties 792 | loadToContainer container, viewmodel, toLoad.load, onlyEvents 793 | 794 | if not onlyEvents 795 | # Direct properties are loaded last. 796 | ViewModel.loadProperties toLoad, container 797 | 798 | if onlyEvents 799 | hooks = 800 | events: 'vmEvents' 801 | else 802 | hooks = 803 | onCreated: 'vmOnCreated' 804 | onRendered: 'vmOnRendered' 805 | onDestroyed: 'vmOnDestroyed' 806 | autorun: 'vmAutorun' 807 | 808 | 809 | for hook, vmProp of hooks when toLoad[hook] 810 | if isArray(toLoad[hook]) 811 | for item in toLoad[hook] 812 | viewmodel[vmProp].push item 813 | else 814 | viewmodel[vmProp].push toLoad[hook] 815 | 816 | load: (toLoad, onlyEvents) -> loadToContainer(this, this, toLoad, onlyEvents) 817 | 818 | parent: (args...) -> 819 | ViewModel.check "#parent", args... 820 | viewmodel = this 821 | instance = viewmodel.templateInstance 822 | while parentTemplate = ViewModel.parentTemplate(instance) 823 | if parentTemplate.viewmodel 824 | return parentTemplate.viewmodel 825 | else 826 | instance = parentTemplate 827 | return 828 | 829 | reset: -> 830 | viewmodel = this 831 | viewmodel[prop].reset() for prop of viewmodel when _.isFunction(viewmodel[prop]?.reset) 832 | 833 | 834 | data: (fields = []) -> 835 | viewmodel = this 836 | js = {} 837 | for prop of viewmodel when viewmodel[prop]?.vmProp and (fields.length is 0 or prop in fields) 838 | viewmodel[prop].depend() 839 | value = viewmodel[prop].value 840 | if value instanceof Array 841 | js[prop] = value.array() 842 | else 843 | js[prop] = value 844 | return js 845 | 846 | valid: (fields = []) -> 847 | viewmodel = this 848 | for prop of viewmodel when viewmodel[prop]?.vmProp and (fields.length is 0 or prop in fields) 849 | return false if not viewmodel[prop].valid(true) 850 | return true 851 | 852 | validMessages: (fields = []) -> 853 | viewmodel = this 854 | messages = [] 855 | for prop of viewmodel when viewmodel[prop]?.vmProp and (fields.length is 0 or prop in fields) 856 | if viewmodel[prop].valid(true) 857 | message = viewmodel[prop].message() 858 | if message 859 | messages.push(message) 860 | return messages 861 | 862 | invalid: (fields = []) -> not this.valid(fields) 863 | invalidMessages: (fields = []) -> 864 | viewmodel = this 865 | messages = [] 866 | for prop of viewmodel when viewmodel[prop]?.vmProp and (fields.length is 0 or prop in fields) 867 | if not viewmodel[prop].valid(true) 868 | message = viewmodel[prop].message() 869 | if message 870 | messages.push(message) 871 | return messages 872 | 873 | templateName: -> ViewModel.templateName(@templateInstance) 874 | 875 | ############# 876 | # Constructor 877 | 878 | childrenProperty = -> 879 | array = new ReactiveArray() 880 | funProp = (search, predicate) -> 881 | array.depend() 882 | if arguments.length 883 | ViewModel.check "#children", search 884 | newPredicate = undefined ; 885 | if _.isString(search) 886 | first = (vm) -> ViewModel.templateName(vm.templateInstance) is search 887 | if predicate 888 | newPredicate = (vm) -> first(vm) and predicate(vm) 889 | else 890 | newPredicate = first 891 | else 892 | newPredicate = search 893 | return _.filter array, newPredicate 894 | else 895 | return array 896 | 897 | return funProp 898 | 899 | @getPathTo = (element) -> 900 | # use ~ and # 901 | if !element or !element.parentNode or element.tagName is 'HTML' or element is document.body 902 | return '/' 903 | 904 | ix = 0 905 | siblings = element.parentNode.childNodes 906 | i = 0 907 | while i < siblings.length 908 | sibling = siblings[i] 909 | if sibling is element 910 | return ViewModel.getPathTo(element.parentNode) + '/' + element.tagName + '[' + (ix + 1) + ']' 911 | if sibling.nodeType is 1 and sibling.tagName is element.tagName 912 | ix++ 913 | i++ 914 | return 915 | 916 | constructor: (initial) -> 917 | ViewModel.check "#constructor", initial 918 | viewmodel = this 919 | viewmodel.vmId = ViewModel.nextId() 920 | 921 | # These will be filled from load/mixin/share/initial 922 | @vmOnCreated = [] 923 | @vmOnRendered = [] 924 | @vmOnDestroyed = [] 925 | @vmAutorun = [] 926 | @vmEvents = [] 927 | 928 | viewmodel.load initial 929 | 930 | @children = childrenProperty() 931 | 932 | viewmodel.vmPathToParent = -> 933 | viewmodelPath = ViewModel.getPathTo(viewmodel.templateInstance.firstNode) 934 | if not viewmodel.parent() 935 | return viewmodelPath 936 | parentPath = ViewModel.getPathTo(viewmodel.parent().templateInstance.firstNode) 937 | i = 0 938 | i++ while parentPath[i] is viewmodelPath[i] and parentPath[i]? 939 | difference = viewmodelPath.substr(i) 940 | return difference 941 | 942 | 943 | return 944 | 945 | child: (args...) -> 946 | children = this.children(args...) 947 | if children?.length 948 | return children[0] 949 | else 950 | return undefined 951 | 952 | @onDestroyed = (initial) -> 953 | return -> 954 | templateInstance = this 955 | initial = initial(templateInstance.data) if _.isFunction(initial) 956 | viewmodel = templateInstance.viewmodel 957 | 958 | for fun in viewmodel.vmOnDestroyed 959 | fun.call viewmodel, templateInstance 960 | 961 | parent = viewmodel.parent() 962 | if parent 963 | children = parent.children() 964 | indexToRemove = -1 965 | for child in children 966 | indexToRemove++ 967 | if child.vmId is viewmodel.vmId 968 | children.splice(indexToRemove, 1) 969 | break 970 | ViewModel.remove viewmodel 971 | return 972 | 973 | @templateName = (templateInstance) -> 974 | name = templateInstance?.view?.name 975 | return '' if not name 976 | if name is 'body' then name else name.substr(name.indexOf('.') + 1) 977 | 978 | vmHash: -> 979 | viewmodel = this 980 | key = ViewModel.templateName(viewmodel.templateInstance) 981 | if viewmodel.parent() 982 | key += viewmodel.parent().vmHash() 983 | 984 | if viewmodel.vmTag 985 | key += viewmodel.vmTag() 986 | else if viewmodel._id 987 | key += viewmodel._id() 988 | else 989 | key += viewmodel.vmPathToParent() 990 | 991 | return SHA256(key).toString() 992 | 993 | @removeMigration = (viewmodel, vmHash) -> 994 | Migration.delete vmHash 995 | 996 | @shared = {} 997 | @share = (obj) -> 998 | for key, value of obj 999 | ViewModel.shared[key] = {} 1000 | for prop, content of value 1001 | if _.isFunction(content) or ViewModel.properties[prop] 1002 | ViewModel.shared[key][prop] = content 1003 | else 1004 | ViewModel.shared[key][prop] = ViewModel.makeReactiveProperty(content) 1005 | 1006 | return 1007 | 1008 | @globals = [] 1009 | @global = (obj) -> 1010 | ViewModel.globals.push(obj) 1011 | 1012 | @mixins = {} 1013 | @mixin = (obj) -> 1014 | for key, value of obj 1015 | ViewModel.mixins[key] = value 1016 | return 1017 | 1018 | @signals = {} 1019 | @signal = (obj) -> 1020 | for key, value of obj 1021 | ViewModel.signals[key] = value 1022 | return 1023 | 1024 | signalContainer = (containerName, container) -> 1025 | all = [] 1026 | return all if not containerName 1027 | signalObject = ViewModel.signals[containerName] 1028 | for key, value of signalObject 1029 | do (key, value) -> 1030 | single = {} 1031 | single[key] = {} 1032 | transform = value.transform or (e) -> e 1033 | boundProp = "_#{key}_Bound" 1034 | single.onCreated = -> 1035 | vmProp = container[key] 1036 | func = (e) -> 1037 | vmProp transform(e) 1038 | funcToUse = if value.throttle then _.throttle( func, value.throttle ) else func 1039 | container[boundProp] = funcToUse 1040 | value.target.addEventListener value.event, funcToUse 1041 | single.onDestroyed = -> 1042 | value.target.removeEventListener value.event, this[boundProp] 1043 | all.push single 1044 | return all 1045 | 1046 | @signalToLoad = (containerName, container) -> 1047 | if isArray(containerName) 1048 | _.flatten( (signalContainer(name, container) for name in containerName), true ) 1049 | else 1050 | signalContainer containerName, container -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "manuel:viewmodel", 3 | summary: 4 | "MVVM, two-way data binding, and components for Meteor. Similar to Angular and Knockout.", 5 | version: "6.3.8", 6 | git: "https://github.com/ManuelDeLeon/viewmodel" 7 | }); 8 | 9 | var CLIENT = "client"; 10 | 11 | Package.onUse(function(api) { 12 | api.use( 13 | [ 14 | "coffeescript@2.0.3_4", 15 | "ecmascript@0.1.6", 16 | "blaze@2.1.2", 17 | "templating@1.1.1", 18 | "jquery@1.11.3_2", 19 | "underscore@1.0.3", 20 | "tracker@1.0.7", 21 | "reload@1.1.3", 22 | "sha@1.0.3", 23 | "reactive-dict@1.1.0", 24 | "manuel:isdev@1.0.0", 25 | "manuel:reactivearray@1.0.9", 26 | "manuel:viewmodel-debug@2.7.2" 27 | ], 28 | CLIENT 29 | ); 30 | 31 | api.addFiles( 32 | [ 33 | "lib/viewmodel.coffee", 34 | "lib/viewmodel-parseBind.coffee", 35 | "lib/bindings.coffee", 36 | "lib/template.coffee", 37 | "lib/migration.coffee", 38 | "lib/viewmodel-onUrl.coffee", 39 | "lib/viewmodel-property.js", 40 | "lib/lzstring.js" 41 | ], 42 | CLIENT 43 | ); 44 | 45 | api.export(["ViewModel"], CLIENT); 46 | }); 47 | 48 | Package.onTest(function(api) { 49 | api.use( 50 | [ 51 | "coffeescript", 52 | "ecmascript", 53 | "blaze", 54 | "templating", 55 | "jquery", 56 | "underscore", 57 | "tracker", 58 | "reload", 59 | "sha", 60 | "reactive-dict", 61 | "manuel:reactivearray", 62 | "cultofcoders:mocha", 63 | "practicalmeteor:sinon", 64 | "practicalmeteor:chai", 65 | "manuel:isdev" 66 | ], 67 | CLIENT 68 | ); 69 | 70 | api.addFiles( 71 | [ 72 | "lib/viewmodel.coffee", 73 | "lib/viewmodel-parseBind.coffee", 74 | "lib/viewmodel-property.js", 75 | "lib/bindings.coffee", 76 | "lib/template.coffee", 77 | "lib/migration.coffee", 78 | "tests/jquery-patch.js", 79 | "tests/sinon-restore.js", 80 | "tests/bindings.coffee", 81 | "tests/viewmodel.coffee", 82 | "tests/viewmodel-instance.coffee", 83 | "tests/viewmodel-check.coffee", 84 | "tests/viewmodel-parseBind.coffee", 85 | "tests/viewmodel-property.coffee", 86 | 87 | "tests/template.coffee" 88 | ], 89 | CLIENT 90 | ); 91 | 92 | api.export(["ViewModel"], CLIENT); 93 | }); 94 | -------------------------------------------------------------------------------- /test.bat: -------------------------------------------------------------------------------- 1 | meteor test-packages ./ --driver-package cultofcoders:mocha --port 4000 -------------------------------------------------------------------------------- /tests/bindings.coffee: -------------------------------------------------------------------------------- 1 | delay = (f) -> 2 | setTimeout(f, 0) 3 | 4 | describe "bindings - input value nested", -> 5 | 6 | beforeEach -> 7 | @viewmodel = new ViewModel 8 | formData: 9 | position: "X" 10 | @element = $("") 11 | @templateInstance = 12 | autorun: Tracker.autorun 13 | 14 | describe "input value nested", -> 15 | beforeEach -> 16 | bindObject = 17 | value: "formData.position" 18 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 19 | 20 | it "gets value", -> 21 | assert.equal "X", @viewmodel.formData().position 22 | 23 | it "sets value from vm", (done) -> 24 | @viewmodel.formData({ position: "Y" }) 25 | delay => 26 | assert.equal "Y", @viewmodel.formData().position 27 | done() 28 | 29 | describe "bindings", -> 30 | 31 | beforeEach -> 32 | @viewmodel = new ViewModel 33 | name: '' 34 | changeName: (v) -> this.name v 35 | on: true 36 | off: false 37 | array: [] 38 | @element = $("") 39 | @templateInstance = 40 | autorun: Tracker.autorun 41 | 42 | describe "input value", -> 43 | beforeEach -> 44 | bindObject = 45 | value: 'name' 46 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 47 | 48 | 49 | it "sets value from vm", (done) -> 50 | @viewmodel.name 'X' 51 | delay => 52 | assert.equal "X", @element.val() 53 | done() 54 | 55 | it "sets value from element", (done) -> 56 | @element.val 'X' 57 | @element.trigger 'input' 58 | delay => 59 | assert.equal "X", @viewmodel.name() 60 | done() 61 | 62 | it "can handle undefined triggered by element", (done) -> 63 | @viewmodel.name undefined 64 | delay => 65 | @element.val 'X' 66 | @element.trigger 'input' 67 | delay => 68 | assert.equal "X", @viewmodel.name() 69 | done() 70 | 71 | it "can handle null triggered by element", (done) -> 72 | @viewmodel.name null 73 | delay => 74 | @element.val 'X' 75 | @element.trigger 'input' 76 | delay => 77 | assert.equal "X", @viewmodel.name() 78 | done() 79 | 80 | it "can handle undefined", (done) -> 81 | @element.val 'X' 82 | @viewmodel.name undefined 83 | delay => 84 | assert.equal "", @element.val() 85 | done() 86 | 87 | it "can handle null", (done) -> 88 | @element.val 'X' 89 | @viewmodel.name null 90 | delay => 91 | assert.equal "", @element.val() 92 | done() 93 | 94 | it "sets value from element (change event)", (done) -> 95 | @element.val 'X' 96 | @element.trigger 'change' 97 | delay => 98 | assert.equal "X", @viewmodel.name() 99 | done() 100 | 101 | describe "input value throttle", -> 102 | beforeEach -> 103 | @clock = sinon.useFakeTimers() 104 | bindObject = 105 | value: 'name' 106 | throttle: '10' 107 | bindId: 1 108 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings, 99, {} 109 | 110 | afterEach -> 111 | @clock.restore() 112 | 113 | it "delays value from element", -> 114 | @element.val 'X' 115 | @element.trigger 'input' 116 | @clock.tick 1 117 | assert.equal '', @viewmodel.name() 118 | @clock.tick 12 119 | assert.equal 'X', @viewmodel.name() 120 | return 121 | 122 | it "throttles the value", -> 123 | @element.val 'X' 124 | @element.trigger 'input' 125 | @clock.tick 8 126 | assert.equal '', @viewmodel.name() 127 | @element.val 'Y' 128 | @element.trigger 'input' 129 | @clock.tick 8 130 | assert.equal '', @viewmodel.name() 131 | @element.val 'Z' 132 | @element.trigger 'input' 133 | @clock.tick 12 134 | assert.equal 'Z', @viewmodel.name() 135 | return 136 | 137 | describe "default", -> 138 | beforeEach -> 139 | bindObject = 140 | click: 'changeName("X")' 141 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 142 | 143 | it "triggers event", (done) -> 144 | @element.trigger 'click' 145 | delay => 146 | assert.equal "X", @viewmodel.name() 147 | done() 148 | 149 | describe "toggle", -> 150 | beforeEach -> 151 | bindObject = 152 | toggle: 'off' 153 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 154 | 155 | it "flips boolean", (done) -> 156 | @element.trigger 'click' 157 | delay => 158 | assert.equal true, @viewmodel.off() 159 | done() 160 | 161 | describe "if", -> 162 | beforeEach -> 163 | bindObject = 164 | if: 'on' 165 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 166 | 167 | it "hides element when true", (done) -> 168 | delay => 169 | assert.equal "inline-block", @element.inlineStyle("display") 170 | done() 171 | 172 | it "hides element when false", (done) -> 173 | @viewmodel.on false 174 | delay => 175 | assert.equal "none", @element.inlineStyle("display") 176 | done() 177 | 178 | describe "visible", -> 179 | beforeEach -> 180 | bindObject = 181 | visible: 'on' 182 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 183 | 184 | it "hides element when true", (done) -> 185 | delay => 186 | assert.equal "inline-block", @element.inlineStyle("display") 187 | done() 188 | 189 | it "hides element when false", (done) -> 190 | @viewmodel.on false 191 | delay => 192 | assert.equal "none", @element.inlineStyle("display") 193 | done() 194 | 195 | describe "unless", -> 196 | beforeEach -> 197 | bindObject = 198 | unless: 'off' 199 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 200 | 201 | it "hides element when true", (done) -> 202 | delay => 203 | assert.equal "inline-block", @element.inlineStyle("display") 204 | done() 205 | 206 | it "hides element when false", (done) -> 207 | @viewmodel.off true 208 | delay => 209 | assert.equal "none", @element.inlineStyle("display") 210 | done() 211 | 212 | describe "hide", -> 213 | beforeEach -> 214 | bindObject = 215 | hide: 'off' 216 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 217 | 218 | it "hides element when true", (done) -> 219 | delay => 220 | assert.equal "inline-block", @element.inlineStyle("display") 221 | done() 222 | 223 | it "hides element when false", (done) -> 224 | @viewmodel.off true 225 | delay => 226 | assert.equal "none", @element.inlineStyle("display") 227 | done() 228 | 229 | describe "text", -> 230 | beforeEach -> 231 | bindObject = 232 | text: 'name' 233 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 234 | 235 | it "sets from vm", (done) -> 236 | @viewmodel.name 'X' 237 | delay => 238 | assert.equal "X", @element.text() 239 | done() 240 | 241 | describe "html", -> 242 | beforeEach -> 243 | bindObject = 244 | html: 'name' 245 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 246 | 247 | it "sets from vm", (done) -> 248 | @viewmodel.name 'X' 249 | delay => 250 | assert.equal "X", @element.html() 251 | done() 252 | 253 | describe "change", -> 254 | 255 | it "uses default without other bindings", (done) -> 256 | bindObject = 257 | change: 'name' 258 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 259 | @element.trigger 'change' 260 | delay => 261 | assert.isTrue @viewmodel.name() instanceof jQuery.Event 262 | done() 263 | 264 | it "uses other bindings", (done) -> 265 | bindObject = 266 | value: 'name' 267 | change: 'on' 268 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 269 | @element.trigger 'change' 270 | delay => 271 | assert.isFalse @viewmodel.name() instanceof jQuery.Event 272 | done() 273 | 274 | describe "enter", -> 275 | beforeEach -> 276 | bindObject = 277 | enter: "changeName('X')" 278 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 279 | 280 | it "uses e.which", (done) -> 281 | e = jQuery.Event("keyup") 282 | e.which = 13 283 | @element.trigger e 284 | delay => 285 | assert.equal 'X', @viewmodel.name() 286 | done() 287 | 288 | it "uses e.keyCode", (done) -> 289 | e = jQuery.Event("keyup") 290 | e.keyCode = 13 291 | @element.trigger e 292 | delay => 293 | assert.equal 'X', @viewmodel.name() 294 | done() 295 | 296 | it "doesn't do anything without key", (done) -> 297 | e = jQuery.Event("keyup") 298 | @element.trigger e 299 | delay => 300 | assert.equal '', @viewmodel.name() 301 | done() 302 | 303 | describe "attr", -> 304 | beforeEach -> 305 | bindObject = 306 | attr: 307 | title: 'name' 308 | viewBox: 'on' 309 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 310 | 311 | it "sets from vm", (done) -> 312 | @viewmodel.name 'X' 313 | @viewmodel.on 'Y' 314 | @viewmodel.viewBox 315 | delay => 316 | assert.equal 'X', @element.attr('title') 317 | assert.equal 'Y', @element[0].getAttribute('viewBox') 318 | done() 319 | 320 | 321 | 322 | describe "addAttributeBinding", -> 323 | it "sets from array", (done) -> 324 | ViewModel.addAttributeBinding( ['href'] ) 325 | bindObject = 326 | href: 'on' 327 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 328 | @viewmodel.on 'Y' 329 | delay => 330 | assert.equal 'Y', @element.attr('href') 331 | done() 332 | 333 | it "sets from string", (done) -> 334 | ViewModel.addAttributeBinding( 'src' ) 335 | bindObject = 336 | src: 'on' 337 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 338 | @viewmodel.on 'Y' 339 | delay => 340 | assert.equal 'Y', @element.attr('src') 341 | done() 342 | 343 | 344 | describe "check", -> 345 | beforeEach -> 346 | bindObject = 347 | check: 'on' 348 | @element = $("") 349 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 350 | 351 | it "has default value", (done) -> 352 | delay => 353 | assert.isTrue @element.is(':checked') 354 | assert.isTrue @viewmodel.on() 355 | done() 356 | 357 | it "sets value from vm", (done) -> 358 | @viewmodel.on false 359 | delay => 360 | assert.isFalse @element.is(':checked') 361 | done() 362 | 363 | it "sets value from element", (done) -> 364 | @element.prop 'checked', false 365 | @element.trigger 'change' 366 | delay => 367 | assert.isFalse @viewmodel.on() 368 | done() 369 | 370 | describe "checkbox group", -> 371 | beforeEach -> 372 | bindObject = 373 | group: 'array' 374 | @element = $("") 375 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 376 | 377 | it "has default value", (done) -> 378 | delay => 379 | assert.equal 0, @viewmodel.array().length 380 | assert.isFalse @element.is(':checked') 381 | done() 382 | 383 | it "sets value from vm", (done) -> 384 | @viewmodel.array().push('A') 385 | delay => 386 | assert.isTrue @element.is(':checked') 387 | done() 388 | 389 | it "sets value from element", (done) -> 390 | @element.prop 'checked', true 391 | @element.trigger 'change' 392 | delay => 393 | assert.equal 1, @viewmodel.array().length 394 | done() 395 | 396 | describe "radio group", -> 397 | beforeEach -> 398 | bindObject = 399 | group: 'name' 400 | @element = $("") 401 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 402 | 403 | it "has default value", (done) -> 404 | delay => 405 | assert.equal '', @viewmodel.name() 406 | assert.isFalse @element.is(':checked') 407 | done() 408 | 409 | it "sets value from vm", (done) -> 410 | @viewmodel.name('A') 411 | delay => 412 | assert.isTrue @element.is(':checked') 413 | done() 414 | 415 | it "sets value from element", (done) -> 416 | triggeredChange = false 417 | @templateInstance.$ = -> 418 | each: -> triggeredChange = true 419 | @element.prop 'checked', true 420 | @element.trigger 'change' 421 | delay => 422 | assert.equal 'A', @viewmodel.name() 423 | assert.isTrue triggeredChange 424 | done() 425 | 426 | describe "style", -> 427 | it "removes the style from string", (done) -> 428 | bindObject = 429 | style: "styleLabel" 430 | @viewmodel.load 431 | styleLabel: 432 | color: "red" 433 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 434 | delay => 435 | assert.equal "red", @element[0].style.color 436 | @viewmodel.styleLabel({ color: null }) 437 | delay => 438 | assert.equal "", @element[0].style.color 439 | done() 440 | return 441 | 442 | it "removes the style from object", (done) -> 443 | bindObject = 444 | style: 445 | color: "color" 446 | @viewmodel.load 447 | color: "red" 448 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 449 | delay => 450 | assert.equal "red", @element[0].style.color 451 | @viewmodel.color(null) 452 | delay => 453 | assert.equal "", @element[0].style.color 454 | done() 455 | return 456 | 457 | it "element has the style from object", (done) -> 458 | bindObject = 459 | style: 460 | color: "'red'" 461 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 462 | delay => 463 | assert.equal "red", @element.inlineStyle("color") 464 | done() 465 | return 466 | 467 | it "element has the style from string", (done) -> 468 | bindObject = 469 | style: "styles.label" 470 | @viewmodel.load 471 | styles: 472 | label: 473 | color: 'red' 474 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 475 | delay => 476 | assert.equal "red", @element.inlineStyle("color") 477 | done() 478 | return 479 | 480 | it "element has the style from string take 2", (done) -> 481 | bindObject = 482 | style: "styleLabel" 483 | @viewmodel.load 484 | styleLabel: "color: red" 485 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 486 | delay => 487 | assert.equal "red", @element.inlineStyle("color") 488 | done() 489 | return 490 | 491 | it "element has the style with commas", (done) -> 492 | bindObject = 493 | style: "styleLabel" 494 | @viewmodel.load 495 | styleLabel: "color: red, border-color: blue" 496 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 497 | delay => 498 | assert.equal "red", @element.inlineStyle("color") 499 | assert.equal "blue", @element.inlineStyle("border-color") 500 | done() 501 | return 502 | 503 | it "element has the style with semi-colons", (done) -> 504 | bindObject = 505 | style: "styleLabel" 506 | @viewmodel.load 507 | styleLabel: "color: red; border-color: blue;" 508 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 509 | delay => 510 | assert.equal "red", @element.inlineStyle("color") 511 | assert.equal "blue", @element.inlineStyle("border-color") 512 | done() 513 | return 514 | 515 | it "element has the style from array", (done) -> 516 | bindObject = 517 | style: "[styles.label, styles.button]" 518 | @viewmodel.load 519 | styles: 520 | label: 521 | color: 'red' 522 | button: 523 | height: '10px' 524 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 525 | delay => 526 | assert.equal "red", @element.inlineStyle("color") 527 | assert.equal "10px", @element.inlineStyle("height") 528 | done() 529 | return 530 | 531 | it "removes the style from array", (done) -> 532 | bindObject = 533 | style: "[styles.label]" 534 | @viewmodel.load 535 | styles: 536 | label: 537 | color: 'red' 538 | @viewmodel.bind bindObject, @templateInstance, @element, ViewModel.bindings 539 | delay => 540 | assert.equal "red", @element.inlineStyle("color") 541 | @viewmodel.styles({ label: { color: null } }) 542 | delay => 543 | assert.equal "", @element[0].style.color 544 | done() 545 | return -------------------------------------------------------------------------------- /tests/jquery-patch.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | $.fn.inlineStyle = function(prop) { 3 | return this.prop('style')[$.camelCase(prop)]; 4 | }; 5 | })(jQuery); -------------------------------------------------------------------------------- /tests/sinon-restore.js: -------------------------------------------------------------------------------- 1 | if (!sinon.patched) { 2 | sinon.patched = true; 3 | sinon.____originalStub = sinon.stub; 4 | sinon.____stubs = []; 5 | sinon.stub = function () { 6 | var stub = sinon.____originalStub.apply(sinon, arguments); 7 | sinon.____stubs.push(stub); 8 | return stub; 9 | }; 10 | 11 | sinon.restoreAll = function () { 12 | sinon.____stubs.forEach(function (stub) { 13 | stub.restore(); 14 | }); 15 | sinon.____stubs = []; 16 | }; 17 | } -------------------------------------------------------------------------------- /tests/template.coffee: -------------------------------------------------------------------------------- 1 | describe "Template", -> 2 | 3 | beforeEach -> 4 | @checkStub = sinon.stub ViewModel, "check" 5 | @vmOnCreatedStub = sinon.stub ViewModel, "onCreated" 6 | @vmOnRenderedStub = sinon.stub ViewModel, "onRendered" 7 | @vmOnDestroyedStub = sinon.stub ViewModel, "onDestroyed" 8 | 9 | afterEach -> 10 | sinon.restoreAll() 11 | 12 | describe "#viewmodel", -> 13 | beforeEach -> 14 | @context = 15 | onCreated: -> 16 | onRendered: -> 17 | onDestroyed: -> 18 | @templateOnCreatedStub = sinon.stub(@context, "onCreated") 19 | @templateOnRenderedStub = sinon.stub(@context, "onRendered") 20 | @templateOnDestroyedStub = sinon.stub(@context, "onDestroyed") 21 | 22 | it "checks the arguments", -> 23 | Template.prototype.viewmodel.call @context, "X" 24 | assert.isTrue @checkStub.calledWithExactly 'T#viewmodel', "X", @context 25 | 26 | it "saves the initial object", -> 27 | Template.prototype.viewmodel.call @context, "X" 28 | assert.equal "X", @context.viewmodelInitial 29 | 30 | it "adds onCreated", -> 31 | @vmOnCreatedStub.returns "Y" 32 | Template.prototype.viewmodel.call @context, "X" 33 | assert.isTrue @vmOnCreatedStub.calledWithExactly(@context, "X") 34 | assert.isTrue @templateOnCreatedStub.calledWithExactly("Y") 35 | 36 | it "adds onRendered", -> 37 | @vmOnRenderedStub.returns "Y" 38 | Template.prototype.viewmodel.call @context, "X" 39 | assert.isTrue @vmOnRenderedStub.calledWithExactly("X") 40 | assert.isTrue @templateOnRenderedStub.calledWithExactly("Y") 41 | 42 | it "adds onDestroyed", -> 43 | @vmOnDestroyedStub.returns "Y" 44 | Template.prototype.viewmodel.call @context, "X" 45 | assert.isTrue @vmOnDestroyedStub.called 46 | assert.isTrue @templateOnDestroyedStub.calledWithExactly("Y") 47 | 48 | it "returns undefined", -> 49 | assert.isUndefined Template.prototype.viewmodel.call(@context, "X") 50 | 51 | it "adds the events", -> 52 | called = [] 53 | initial = 54 | events: 55 | a: null 56 | b: null 57 | @context.events = (eventObj) -> called.push eventObj 58 | Template.prototype.viewmodel.call @context, initial 59 | assert.isFunction called[0].a 60 | assert.isFunction called[1].b 61 | assert.equal called.length, 2 62 | 63 | describe "#createViewModel", -> 64 | beforeEach -> 65 | @createViewModel = Template.prototype.createViewModel 66 | @getInitialObjectStub = sinon.stub ViewModel, 'getInitialObject' 67 | @getInitialObjectStub.returns "X" 68 | @template = 69 | viewmodelInitial: "A" 70 | 71 | it "calls getInitialObject", -> 72 | @createViewModel.call @template, "B" 73 | assert.isTrue @getInitialObjectStub.calledWith("A", "B") 74 | 75 | it "returns a view model", -> 76 | vm = @createViewModel.call @template, "B" 77 | assert.isTrue vm instanceof ViewModel 78 | -------------------------------------------------------------------------------- /tests/viewmodel-check.coffee: -------------------------------------------------------------------------------- 1 | describe "ViewModel", -> 2 | 3 | describe "@check", -> 4 | beforeEach -> 5 | Package['manuel:viewmodel-debug'] = 6 | VmCheck: -> 7 | @vmCheckStub = sinon.stub Package['manuel:viewmodel-debug'], "VmCheck" 8 | 9 | afterEach -> 10 | sinon.restoreAll() 11 | 12 | it "doesn't check if ignoreErrors is true", -> 13 | ViewModel.ignoreErrors = true 14 | ViewModel.check() 15 | ViewModel.ignoreErrors = false 16 | assert.isFalse @vmCheckStub.called 17 | 18 | it "calls VmCheck with parameters", -> 19 | ViewModel.check 1, 2, 3 20 | assert.isTrue @vmCheckStub.calledWithExactly 1, 2, 3 21 | 22 | it "returns undefined", -> 23 | assert.isUndefined ViewModel.check() 24 | -------------------------------------------------------------------------------- /tests/viewmodel-instance.coffee: -------------------------------------------------------------------------------- 1 | describe "ViewModel instance", -> 2 | 3 | beforeEach -> 4 | @checkStub = sinon.stub ViewModel, "check" 5 | @viewmodel = new ViewModel() 6 | 7 | afterEach -> 8 | sinon.restoreAll() 9 | 10 | describe "constructor", -> 11 | it "checks the arguments", -> 12 | obj = { name: 'A'} 13 | vm = new ViewModel obj 14 | assert.isTrue @checkStub.calledWith '#constructor', obj 15 | 16 | it "adds property as function", -> 17 | vm = new ViewModel({ name: 'A'}) 18 | assert.isFunction vm.name 19 | assert.equal 'A', vm.name() 20 | vm.name('B') 21 | assert.equal 'B', vm.name() 22 | 23 | it "adds properties in load object", -> 24 | obj = { name: "A" } 25 | vm = new ViewModel 26 | load: obj 27 | assert.equal 'A', vm.name() 28 | 29 | it "adds properties in load array", -> 30 | arr = [ { name: "A" }, { age: 1 } ] 31 | vm = new ViewModel 32 | load: arr 33 | assert.equal 'A', vm.name() 34 | assert.equal 1, vm.age() 35 | 36 | it "doesn't convert functions", -> 37 | f = -> 38 | vm = new ViewModel 39 | fun: f 40 | assert.equal f, vm.fun 41 | 42 | describe "loading hooks direct", -> 43 | beforeEach -> 44 | ViewModel.mixins = {} 45 | ViewModel.mixin 46 | hooksMixin: 47 | onCreated: -> 'onCreatedMixin' 48 | onRendered: -> 'onRenderedMixin' 49 | onDestroyed: -> 'onDestroyedMixin' 50 | autorun: -> 'autorunMixin' 51 | ViewModel.shared = {} 52 | ViewModel.share 53 | hooksShare: 54 | onCreated: -> 'onCreatedShare' 55 | onRendered: -> 'onRenderedShare' 56 | onDestroyed: -> 'onDestroyedShare' 57 | autorun: -> 'autorunShare' 58 | 59 | @viewmodel = new ViewModel 60 | share: 'hooksShare' 61 | mixin: 'hooksMixin' 62 | load: 63 | onCreated: -> 'onCreatedLoad' 64 | onRendered: -> 'onRenderedLoad' 65 | onDestroyed: -> 'onDestroyedLoad' 66 | autorun: -> 'autorunLoad' 67 | onCreated: -> 'onCreatedBase' 68 | onRendered: -> 'onRenderedBase' 69 | onDestroyed: -> 'onDestroyedBase' 70 | autorun: -> 'autorunBase' 71 | return 72 | 73 | it "adds hooks to onCreated", -> 74 | assert.equal @viewmodel.vmOnCreated.length, 4 75 | assert.equal @viewmodel.vmOnCreated[0](), 'onCreatedShare' 76 | assert.equal @viewmodel.vmOnCreated[1](), 'onCreatedMixin' 77 | assert.equal @viewmodel.vmOnCreated[2](), 'onCreatedLoad' 78 | assert.equal @viewmodel.vmOnCreated[3](), 'onCreatedBase' 79 | it "adds hooks to onRendered", -> 80 | assert.equal @viewmodel.vmOnRendered.length, 4 81 | assert.equal @viewmodel.vmOnRendered[0](), 'onRenderedShare' 82 | assert.equal @viewmodel.vmOnRendered[1](), 'onRenderedMixin' 83 | assert.equal @viewmodel.vmOnRendered[2](), 'onRenderedLoad' 84 | assert.equal @viewmodel.vmOnRendered[3](), 'onRenderedBase' 85 | it "adds hooks to onDestroyed", -> 86 | assert.equal @viewmodel.vmOnDestroyed.length, 4 87 | assert.equal @viewmodel.vmOnDestroyed[0](), 'onDestroyedShare' 88 | assert.equal @viewmodel.vmOnDestroyed[1](), 'onDestroyedMixin' 89 | assert.equal @viewmodel.vmOnDestroyed[2](), 'onDestroyedLoad' 90 | assert.equal @viewmodel.vmOnDestroyed[3](), 'onDestroyedBase' 91 | it "adds hooks to autorun", -> 92 | assert.equal @viewmodel.vmAutorun.length, 4 93 | assert.equal @viewmodel.vmAutorun[0](), 'autorunShare' 94 | assert.equal @viewmodel.vmAutorun[1](), 'autorunMixin' 95 | assert.equal @viewmodel.vmAutorun[2](), 'autorunLoad' 96 | assert.equal @viewmodel.vmAutorun[3](), 'autorunBase' 97 | 98 | describe "loading hooks from array", -> 99 | beforeEach -> 100 | ViewModel.mixins = {} 101 | ViewModel.mixin 102 | hooksMixin: 103 | onCreated: [ (-> 'onCreatedMixin')] 104 | onRendered: [ (-> 'onRenderedMixin')] 105 | onDestroyed: [ (-> 'onDestroyedMixin')] 106 | autorun: [ (-> 'autorunMixin')] 107 | ViewModel.shared = {} 108 | ViewModel.share 109 | hooksShare: 110 | onCreated: [ (-> 'onCreatedShare')] 111 | onRendered: [ (-> 'onRenderedShare')] 112 | onDestroyed: [ (-> 'onDestroyedShare')] 113 | autorun: [ (-> 'autorunShare')] 114 | 115 | @viewmodel = new ViewModel 116 | share: 'hooksShare' 117 | mixin: 'hooksMixin' 118 | load: 119 | onCreated: [ (-> 'onCreatedLoad')] 120 | onRendered: [ (-> 'onRenderedLoad')] 121 | onDestroyed: [ (-> 'onDestroyedLoad')] 122 | autorun: [ (-> 'autorunLoad')] 123 | onCreated: [ (-> 'onCreatedBase')] 124 | onRendered: [ (-> 'onRenderedBase')] 125 | onDestroyed: [ (-> 'onDestroyedBase')] 126 | autorun: [ (-> 'autorunBase')] 127 | return 128 | 129 | it "adds hooks to onCreated", -> 130 | assert.equal @viewmodel.vmOnCreated.length, 4 131 | assert.equal @viewmodel.vmOnCreated[0](), 'onCreatedShare' 132 | assert.equal @viewmodel.vmOnCreated[1](), 'onCreatedMixin' 133 | assert.equal @viewmodel.vmOnCreated[2](), 'onCreatedLoad' 134 | assert.equal @viewmodel.vmOnCreated[3](), 'onCreatedBase' 135 | it "adds hooks to onRendered", -> 136 | assert.equal @viewmodel.vmOnRendered.length, 4 137 | assert.equal @viewmodel.vmOnRendered[0](), 'onRenderedShare' 138 | assert.equal @viewmodel.vmOnRendered[1](), 'onRenderedMixin' 139 | assert.equal @viewmodel.vmOnRendered[2](), 'onRenderedLoad' 140 | assert.equal @viewmodel.vmOnRendered[3](), 'onRenderedBase' 141 | it "adds hooks to onDestroyed", -> 142 | assert.equal @viewmodel.vmOnDestroyed.length, 4 143 | assert.equal @viewmodel.vmOnDestroyed[0](), 'onDestroyedShare' 144 | assert.equal @viewmodel.vmOnDestroyed[1](), 'onDestroyedMixin' 145 | assert.equal @viewmodel.vmOnDestroyed[2](), 'onDestroyedLoad' 146 | assert.equal @viewmodel.vmOnDestroyed[3](), 'onDestroyedBase' 147 | it "adds hooks to autorun", -> 148 | assert.equal @viewmodel.vmAutorun.length, 4 149 | assert.equal @viewmodel.vmAutorun[0](), 'autorunShare' 150 | assert.equal @viewmodel.vmAutorun[1](), 'autorunMixin' 151 | assert.equal @viewmodel.vmAutorun[2](), 'autorunLoad' 152 | assert.equal @viewmodel.vmAutorun[3](), 'autorunBase' 153 | 154 | describe "load order", -> 155 | beforeEach -> 156 | ViewModel.mixins = {} 157 | ViewModel.mixin 158 | name: 159 | name: 'mixin' 160 | ViewModel.shared = {} 161 | ViewModel.share 162 | name: 163 | name: 'share' 164 | 165 | ViewModel.signals = {} 166 | ViewModel.signal 167 | name: 168 | name: 169 | target: document 170 | event: 'keydown' 171 | 172 | it "loads base name last", -> 173 | vm = new ViewModel({ 174 | name: 'base', 175 | load: { 176 | name: 'load' 177 | }, 178 | mixin: 'name', 179 | share: 'name', 180 | signal: 'name' 181 | }) 182 | assert.equal vm.name(), "base" 183 | 184 | it "loads from load 2nd to last", -> 185 | vm = new ViewModel({ 186 | load: { 187 | name: 'load' 188 | }, 189 | mixin: 'name', 190 | share: 'name', 191 | signal: 'name' 192 | }) 193 | assert.equal vm.name(), "load" 194 | 195 | it "loads from mixin 3rd to last", -> 196 | vm = new ViewModel({ 197 | mixin: 'name', 198 | share: 'name', 199 | signal: 'name' 200 | }) 201 | assert.equal vm.name(), "mixin" 202 | 203 | it "loads from share 4th to last", -> 204 | vm = new ViewModel({ 205 | share: 'name', 206 | signal: 'name' 207 | }) 208 | assert.equal vm.name(), "share" 209 | 210 | it "loads from signal first", -> 211 | vm = new ViewModel({ 212 | signal: 'name' 213 | }) 214 | assert.equal _.keys(vm.name()).length, 0 215 | 216 | describe "#bind", -> 217 | 218 | beforeEach -> 219 | @bindSingleStub = sinon.stub ViewModel, 'bindSingle' 220 | 221 | it "calls bindSingle for each entry in bindObject", -> 222 | bindObject = 223 | a: 1 224 | b: 2 225 | vm = {} 226 | bindings = 227 | a: 1 228 | b: 2 229 | @viewmodel.bind.call vm, bindObject, 'templateInstance', 'element', bindings 230 | assert.isTrue @bindSingleStub.calledTwice 231 | assert.isTrue @bindSingleStub.calledWith 'templateInstance', 'element', 'a', 1, bindObject, vm, bindings 232 | assert.isTrue @bindSingleStub.calledWith 'templateInstance', 'element', 'b', 2, bindObject, vm, bindings 233 | 234 | it "returns undefined", -> 235 | bindObject = {} 236 | ret = @viewmodel.bind bindObject, 'templateInstance', 'element', 'bindings' 237 | assert.isUndefined ret 238 | 239 | describe "validation", -> 240 | it "vm is valid with an undefined", -> 241 | @viewmodel.load({ name: undefined }) 242 | assert.equal true, @viewmodel.valid() 243 | return 244 | 245 | describe "#load", -> 246 | 247 | it "adds a property to the view model", -> 248 | @viewmodel.load({ name: 'Alan' }) 249 | assert.equal 'Alan', @viewmodel.name() 250 | 251 | it "adds onRendered from an array", -> 252 | f = -> 253 | @viewmodel.load([ onRendered: f ]) 254 | assert.equal f, @viewmodel.vmOnRendered[0] 255 | 256 | it "adds a properties from an array", -> 257 | @viewmodel.load([{ name: 'Alan' },{ two: 'Brito' }]) 258 | assert.equal 'Alan', @viewmodel.name() 259 | assert.equal 'Brito', @viewmodel.two() 260 | 261 | it "adds function to the view model", -> 262 | f = -> 263 | @viewmodel.load({ fun: f }) 264 | assert.equal f, @viewmodel.fun 265 | 266 | it "doesn't create a new property when extending the same name", -> 267 | @viewmodel.load({ name: 'Alan' }) 268 | old = @viewmodel.name 269 | @viewmodel.load({ name: 'Brito' }) 270 | assert.equal 'Brito', @viewmodel.name() 271 | assert.equal old, @viewmodel.name 272 | 273 | it "overwrite existing functions", -> 274 | @viewmodel.load({ name: -> 'Alan' }) 275 | old = @viewmodel.name 276 | @viewmodel.load({ name: 'Brito' }) 277 | theNew = @viewmodel.name 278 | assert.equal 'Brito', @viewmodel.name() 279 | assert.equal theNew, @viewmodel.name 280 | assert.notEqual old, theNew 281 | 282 | it "doesn't add events", -> 283 | @viewmodel.load({ events: { 'click one' : -> } }) 284 | assert.equal 0, @viewmodel.vmEvents.length 285 | 286 | it "adds events", -> 287 | @viewmodel.load({ events: { 'click one' : -> } }, true) 288 | assert.equal 1, @viewmodel.vmEvents.length 289 | 290 | it "doesn't do anything with null and undefined", -> 291 | @viewmodel.load(undefined ) 292 | @viewmodel.load(null) 293 | 294 | describe "#parent", -> 295 | 296 | beforeEach -> 297 | @viewmodel.templateInstance = 298 | view: 299 | parentView: 300 | name: 'Template.A' 301 | templateInstance: -> 302 | viewmodel: "X" 303 | 304 | it "returns the view model of the parent template", -> 305 | parent = @viewmodel.parent() 306 | assert.equal "X", parent 307 | 308 | it "returns the first view model up the chain", -> 309 | @viewmodel.templateInstance = 310 | view: 311 | parentView: 312 | name: 'Template.something' 313 | templateInstance: -> 314 | view: 315 | parentView: 316 | name: 'Template.A' 317 | templateInstance: -> 318 | viewmodel: "Y" 319 | parent = @viewmodel.parent() 320 | assert.equal "Y", parent 321 | 322 | it "checks the arguments", -> 323 | @viewmodel.parent('X') 324 | assert.isTrue @checkStub.calledWith '#parent', 'X' 325 | 326 | describe "#children", -> 327 | 328 | beforeEach -> 329 | @viewmodel.children().push 330 | age: -> 1 331 | name: -> "AA" 332 | templateInstance: 333 | view: 334 | name: 'Template.A' 335 | @viewmodel.children().push 336 | age: -> 2 337 | name: -> "BB" 338 | templateInstance: 339 | view: 340 | name: 'Template.B' 341 | @viewmodel.children().push 342 | age: -> 1 343 | templateInstance: 344 | view: 345 | name: 'Template.A' 346 | 347 | it "returns all without arguments", -> 348 | assert.equal 3, @viewmodel.children().length 349 | @viewmodel.children().push("X") 350 | assert.equal 4, @viewmodel.children().length 351 | assert.equal "X", @viewmodel.children()[3] 352 | 353 | it "returns by template when passed a string", -> 354 | arr = @viewmodel.children('A') 355 | assert.equal 2, arr.length 356 | assert.equal 1, arr[0].age() 357 | assert.equal 1, arr[1].age() 358 | 359 | it "returns array from a predicate", -> 360 | arr = @viewmodel.children((vm) -> vm.age() is 2) 361 | assert.equal 1, arr.length 362 | assert.equal "BB", arr[0].name() 363 | 364 | it "calls .depend", -> 365 | array = @viewmodel.children() 366 | spy = sinon.spy array, 'depend' 367 | @viewmodel.children() 368 | assert.isTrue spy.called 369 | 370 | it "doesn't check without arguments", -> 371 | @viewmodel.children() 372 | assert.isFalse @checkStub.calledWith '#children' 373 | 374 | it "checks with arguments", -> 375 | @viewmodel.children('X') 376 | assert.isTrue @checkStub.calledWith '#children', 'X' 377 | 378 | describe "#reset", -> 379 | 380 | beforeEach -> 381 | @viewmodel.templateInstance = 382 | view: 383 | name: 'body' 384 | @viewmodel.load 385 | name: 'A' 386 | arr: ['A'] 387 | 388 | it "resets a string", -> 389 | @viewmodel.name('B') 390 | @viewmodel.reset() 391 | assert.equal "A", @viewmodel.name() 392 | 393 | it "resets an array", -> 394 | @viewmodel.arr().push('B') 395 | @viewmodel.reset() 396 | assert.equal 1, @viewmodel.arr().length 397 | assert.equal 'A', @viewmodel.arr()[0] 398 | 399 | describe "#data", -> 400 | 401 | beforeEach -> 402 | @viewmodel.load 403 | name: 'A' 404 | arr: ['B'] 405 | 406 | it "creates js object", -> 407 | obj = @viewmodel.data() 408 | assert.equal 'A', obj.name 409 | assert.equal 'B', obj.arr[0] 410 | return 411 | 412 | it "only loads fields specified", -> 413 | obj = @viewmodel.data(['name']) 414 | assert.equal 'A', obj.name 415 | assert.isUndefined obj.arr 416 | return 417 | 418 | describe "#load", -> 419 | 420 | beforeEach -> 421 | @viewmodel.load 422 | name: 'A' 423 | age: 2 424 | f: -> 'X' 425 | 426 | it "loads js object", -> 427 | @viewmodel.load 428 | name: 'B' 429 | f: -> 'Y' 430 | assert.equal 'B', @viewmodel.name() 431 | assert.equal 2, @viewmodel.age() 432 | assert.equal 'Y', @viewmodel.f() 433 | return 434 | 435 | describe "mixin", -> 436 | 437 | beforeEach -> 438 | ViewModel.mixin 439 | house: 440 | address: 'A' 441 | person: 442 | name: 'X' 443 | glob: 444 | mixin: 'person' 445 | prom: 446 | mixin: 447 | scoped: 'glob' 448 | bland: 449 | mixin: [ { subGlob: 'glob'}, 'house'] 450 | 451 | it "sub-mixin adds property to vm", -> 452 | vm = new ViewModel 453 | mixin: 'glob' 454 | assert.equal 'X', vm.name() 455 | 456 | it "sub-mixin adds sub-property to vm", -> 457 | vm = new ViewModel 458 | mixin: 459 | scoped: 'glob' 460 | assert.equal 'X', vm.scoped.name() 461 | 462 | it "sub-mixin adds sub-property to vm prom", -> 463 | vm = new ViewModel 464 | mixin: 'prom' 465 | assert.equal 'X', vm.scoped.name() 466 | 467 | it "sub-mixin adds sub-property to vm bland", -> 468 | vm = new ViewModel 469 | mixin: 'bland' 470 | assert.equal 'A', vm.address() 471 | assert.equal 'X', vm.subGlob.name() 472 | 473 | it "sub-mixin adds sub-property to vm bland scoped", -> 474 | vm = new ViewModel 475 | mixin: 476 | scoped: 'bland' 477 | assert.equal 'A', vm.scoped.address() 478 | assert.equal 'X', vm.scoped.subGlob.name() 479 | 480 | it "adds property to vm", -> 481 | vm = new ViewModel 482 | mixin: 'house' 483 | assert.equal 'A', vm.address() 484 | 485 | it "adds property to vm from array", -> 486 | vm = new ViewModel 487 | mixin: ['house'] 488 | assert.equal 'A', vm.address() 489 | 490 | it "doesn't share the property", -> 491 | vm1 = new ViewModel 492 | mixin: 'house' 493 | vm2 = new ViewModel 494 | mixin: 'house' 495 | vm2.address 'B' 496 | assert.equal 'A', vm1.address() 497 | assert.equal 'B', vm2.address() 498 | 499 | it "adds object to vm", -> 500 | vm = new ViewModel 501 | mixin: 502 | location: 'house' 503 | assert.equal 'A', vm.location.address() 504 | 505 | it "adds array to vm", -> 506 | vm = new ViewModel 507 | mixin: 508 | location: ['house', 'person'] 509 | assert.equal 'A', vm.location.address() 510 | assert.equal 'X', vm.location.name() 511 | 512 | it "adds mix to vm", -> 513 | vm = new ViewModel 514 | mixin: [ 515 | { location: 'house' }, 516 | 'person' 517 | ] 518 | assert.equal 'A', vm.location.address() 519 | assert.equal 'X', vm.name() 520 | 521 | describe "share", -> 522 | 523 | beforeEach -> 524 | ViewModel.share 525 | house: 526 | address: 'A' 527 | person: 528 | name: 'X' 529 | 530 | it "adds property to vm", -> 531 | vm = new ViewModel 532 | share: 'house' 533 | assert.equal 'A', vm.address() 534 | 535 | it "adds property to vm from array", -> 536 | vm = new ViewModel 537 | share: ['house'] 538 | assert.equal 'A', vm.address() 539 | 540 | it "adds object to vm", -> 541 | vm = new ViewModel 542 | share: 543 | location: 'house' 544 | assert.equal 'A', vm.location.address() 545 | 546 | it "shares the property", -> 547 | vm1 = new ViewModel 548 | share: 'house' 549 | vm2 = new ViewModel 550 | share: 'house' 551 | vm2.address 'B' 552 | assert.equal 'B', vm1.address() 553 | assert.equal 'B', vm2.address() 554 | assert.equal vm1.address, vm1.address 555 | 556 | it "adds array to vm", -> 557 | vm = new ViewModel 558 | share: 559 | location: ['house', 'person'] 560 | assert.equal 'A', vm.location.address() 561 | assert.equal 'X', vm.location.name() 562 | 563 | it "adds mix to vm", -> 564 | vm = new ViewModel 565 | share: [ 566 | { location: 'house' }, 567 | 'person' 568 | ] 569 | assert.equal 'A', vm.location.address() 570 | assert.equal 'X', vm.name() -------------------------------------------------------------------------------- /tests/viewmodel-parseBind.coffee: -------------------------------------------------------------------------------- 1 | describe "ViewModel", -> 2 | 3 | beforeEach -> 4 | @checkStub = sinon.stub ViewModel, "check" 5 | 6 | afterEach -> 7 | sinon.restoreAll() 8 | 9 | describe "@parseBind", -> 10 | 11 | it "parses object", -> 12 | obj = ViewModel.parseBind "text: name, full: first + ' ' + last" 13 | assert.isTrue _.isEqual({ text: "name", full: "first + ' ' + last" }, obj) 14 | 15 | -------------------------------------------------------------------------------- /tests/viewmodel-property.coffee: -------------------------------------------------------------------------------- 1 | describe "ViewModel Properties", -> 2 | describe "string", -> 3 | prop = new ViewModel.Property().string 4 | 5 | it "fails with a number", -> 6 | assert.isFalse prop.verify(1) 7 | 8 | it "fails with an object", -> 9 | assert.isFalse prop.verify({}) 10 | 11 | it "passes with a string", -> 12 | assert.isTrue prop.verify("") 13 | 14 | it "fails with a date", -> 15 | assert.isFalse prop.verify(new Date()) 16 | 17 | it "fails with a boolean", -> 18 | assert.isFalse prop.verify(true) 19 | 20 | describe "number", -> 21 | prop = new ViewModel.Property().number 22 | 23 | it "passes with an integer", -> 24 | assert.isTrue prop.verify(1) 25 | 26 | it "passes with a float", -> 27 | assert.isTrue prop.verify(1.1) 28 | 29 | it "passes with a string/float", -> 30 | assert.isTrue prop.verify("1.0") 31 | 32 | it "fails with a number + string", -> 33 | assert.isFalse prop.verify("1a") 34 | 35 | it "fails with an object", -> 36 | assert.isFalse prop.verify({}) 37 | 38 | it "fails with an empty string", -> 39 | assert.isFalse prop.verify("") 40 | 41 | it "fails with a date", -> 42 | assert.isFalse prop.verify(new Date()) 43 | 44 | it "fails with a boolean", -> 45 | assert.isFalse prop.verify(true) 46 | 47 | describe "integer", -> 48 | prop = new ViewModel.Property().integer 49 | 50 | it "passes with an integer", -> 51 | assert.isTrue prop.verify(1) 52 | 53 | it "fails with a float", -> 54 | assert.isFalse prop.verify(1.1) 55 | 56 | it "passes with a string/integer", -> 57 | assert.isTrue prop.verify("1") 58 | 59 | it "fails with a number + string", -> 60 | assert.isFalse prop.verify("1a") 61 | 62 | it "fails with an object", -> 63 | assert.isFalse prop.verify({}) 64 | 65 | it "fails with an empty string", -> 66 | assert.isFalse prop.verify("") 67 | 68 | it "fails with a date", -> 69 | assert.isFalse prop.verify(new Date()) 70 | 71 | it "fails with a boolean", -> 72 | assert.isFalse prop.verify(true) 73 | 74 | describe "boolean", -> 75 | prop = new ViewModel.Property().boolean 76 | 77 | it "fails with a number", -> 78 | assert.isFalse prop.verify(1) 79 | 80 | it "fails with an object", -> 81 | assert.isFalse prop.verify({}) 82 | 83 | it "fails with a string", -> 84 | assert.isFalse prop.verify("") 85 | 86 | it "fails with a date", -> 87 | assert.isFalse prop.verify(new Date()) 88 | 89 | it "passes with a boolean", -> 90 | assert.isTrue prop.verify(false) 91 | 92 | describe "object", -> 93 | prop = new ViewModel.Property().object 94 | 95 | it "fails with a number", -> 96 | assert.isFalse prop.verify(1) 97 | 98 | it "passes with an object", -> 99 | assert.isTrue prop.verify({}) 100 | 101 | it "fails with a string", -> 102 | assert.isFalse prop.verify("") 103 | 104 | it "fails with a date", -> 105 | assert.isFalse prop.verify(new Date()) 106 | 107 | it "fails with a boolean", -> 108 | assert.isFalse prop.verify(true) 109 | 110 | describe "date", -> 111 | prop = new ViewModel.Property().date 112 | 113 | it "fails with a number", -> 114 | assert.isFalse prop.verify(1) 115 | 116 | it "fails with an object", -> 117 | assert.isFalse prop.verify({}) 118 | 119 | it "fails with a string", -> 120 | assert.isFalse prop.verify("") 121 | 122 | it "passes with a date", -> 123 | assert.isTrue prop.verify(new Date()) 124 | 125 | it "fails with a boolean", -> 126 | assert.isFalse prop.verify(true) 127 | 128 | 129 | describe "min", -> 130 | 131 | describe "string", -> 132 | prop = new ViewModel.Property().string.min(2) 133 | 134 | it "x", -> 135 | assert.isFalse prop.verify("x") 136 | 137 | it "xx", -> 138 | assert.isTrue prop.verify("xx") 139 | 140 | it "xxx", -> 141 | assert.isTrue prop.verify("xxx") 142 | 143 | describe "number", -> 144 | prop = new ViewModel.Property().number.min(2) 145 | 146 | it "1", -> 147 | assert.isFalse prop.verify(1) 148 | 149 | it "2", -> 150 | assert.isTrue prop.verify(2) 151 | 152 | it "3", -> 153 | assert.isTrue prop.verify(3) 154 | 155 | describe "integer", -> 156 | prop = new ViewModel.Property().integer.min(2) 157 | 158 | it "1", -> 159 | assert.isFalse prop.verify(1) 160 | 161 | it "2", -> 162 | assert.isTrue prop.verify(2) 163 | 164 | it "3", -> 165 | assert.isTrue prop.verify(3) 166 | 167 | describe "date", -> 168 | prop = new ViewModel.Property().date.min(new Date(2020, 1, 2)) 169 | 170 | it "new Date(2020, 1, 1)", -> 171 | assert.isFalse prop.verify(new Date(2020, 1, 1)) 172 | 173 | it "new Date(2020, 1, 2)", -> 174 | assert.isTrue prop.verify(new Date(2020, 1, 2)) 175 | 176 | it "new Date(2020, 1, 3)", -> 177 | assert.isTrue prop.verify(new Date(2020, 1, 3)) 178 | 179 | describe "not specified", -> 180 | prop = new ViewModel.Property().min(2) 181 | 182 | it "1", -> 183 | assert.isTrue prop.verify(2) 184 | 185 | 186 | describe "max", -> 187 | 188 | describe "string", -> 189 | prop = new ViewModel.Property().string.max(2) 190 | 191 | it "x", -> 192 | assert.isTrue prop.verify("x") 193 | 194 | it "xx", -> 195 | assert.isTrue prop.verify("xx") 196 | 197 | it "xxx", -> 198 | assert.isFalse prop.verify("xxx") 199 | 200 | describe "number", -> 201 | prop = new ViewModel.Property().number.max(2) 202 | 203 | it "1", -> 204 | assert.isTrue prop.verify(1) 205 | 206 | it "2", -> 207 | assert.isTrue prop.verify(2) 208 | 209 | it "3", -> 210 | assert.isFalse prop.verify(3) 211 | 212 | describe "integer", -> 213 | prop = new ViewModel.Property().integer.max(2) 214 | 215 | it "1", -> 216 | assert.isTrue prop.verify(1) 217 | 218 | it "2", -> 219 | assert.isTrue prop.verify(2) 220 | 221 | it "3", -> 222 | assert.isFalse prop.verify(3) 223 | 224 | describe "date", -> 225 | prop = new ViewModel.Property().date.max(new Date(2020, 1, 2)) 226 | 227 | it "new Date(2020, 1, 1)", -> 228 | assert.isTrue prop.verify(new Date(2020, 1, 1)) 229 | 230 | it "new Date(2020, 1, 2)", -> 231 | assert.isTrue prop.verify(new Date(2020, 1, 2)) 232 | 233 | it "new Date(2020, 1, 3)", -> 234 | assert.isFalse prop.verify(new Date(2020, 1, 3)) 235 | 236 | describe "not specified", -> 237 | prop = new ViewModel.Property().max(2) 238 | 239 | it "1", -> 240 | assert.isTrue prop.verify(1) 241 | 242 | 243 | describe "validate", -> 244 | prop = new ViewModel.Property().validate( ((v) -> v is 2) ) 245 | 246 | it "1", -> 247 | assert.isFalse prop.verify(1) 248 | 249 | it "2", -> 250 | assert.isTrue prop.verify(2) 251 | 252 | 253 | describe "equal", -> 254 | prop = new ViewModel.Property().equal(1) 255 | 256 | it "'1'", -> 257 | assert.isFalse prop.verify("1") 258 | 259 | it "1", -> 260 | assert.isTrue prop.verify(1) 261 | 262 | describe "notEqual", -> 263 | prop = new ViewModel.Property().notEqual(1) 264 | 265 | it "'1'", -> 266 | assert.isTrue prop.verify("1") 267 | 268 | it "1", -> 269 | assert.isFalse prop.verify(1) 270 | 271 | describe "notBlank", -> 272 | prop = new ViewModel.Property().notBlank 273 | 274 | it "' 0 '", -> 275 | assert.isTrue prop.verify(" 0 ") 276 | 277 | it "'0'", -> 278 | assert.isTrue prop.verify("0") 279 | 280 | it "' '", -> 281 | assert.isFalse prop.verify(" ") 282 | 283 | it "null", -> 284 | assert.isFalse prop.verify(null) 285 | 286 | it "undefined", -> 287 | assert.isFalse prop.verify(undefined) 288 | 289 | describe "between", -> 290 | 291 | describe "string", -> 292 | prop = new ViewModel.Property().string.between(2, 4) 293 | 294 | it "x", -> 295 | assert.isFalse prop.verify("x") 296 | 297 | it "xx", -> 298 | assert.isTrue prop.verify("xx") 299 | 300 | it "xxxx", -> 301 | assert.isTrue prop.verify("xxxx") 302 | 303 | it "xxxxx", -> 304 | assert.isFalse prop.verify("xxxxx") 305 | 306 | describe "number", -> 307 | prop = new ViewModel.Property().number.between(2, 4) 308 | 309 | it "1", -> 310 | assert.isFalse prop.verify(1) 311 | 312 | it "2", -> 313 | assert.isTrue prop.verify(2) 314 | 315 | it "4", -> 316 | assert.isTrue prop.verify(4) 317 | 318 | it "5", -> 319 | assert.isFalse prop.verify(5) 320 | 321 | describe "notBetween", -> 322 | 323 | describe "string", -> 324 | prop = new ViewModel.Property().string.notBetween(2, 4) 325 | 326 | it "x", -> 327 | assert.isTrue prop.verify("x") 328 | 329 | it "xx", -> 330 | assert.isFalse prop.verify("xx") 331 | 332 | it "xxxx", -> 333 | assert.isFalse prop.verify("xxxx") 334 | 335 | it "xxxxx", -> 336 | assert.isTrue prop.verify("xxxxx") 337 | 338 | describe "number", -> 339 | prop = new ViewModel.Property().number.notBetween(2, 4) 340 | 341 | it "1", -> 342 | assert.isTrue prop.verify(1) 343 | 344 | it "2", -> 345 | assert.isFalse prop.verify(2) 346 | 347 | it "4", -> 348 | assert.isFalse prop.verify(4) 349 | 350 | it "5", -> 351 | assert.isTrue prop.verify(5) 352 | 353 | describe "regex", -> 354 | prop = new ViewModel.Property().regex(/x/) 355 | 356 | it "axc", -> 357 | assert.isTrue prop.verify("axc") 358 | 359 | it "abc", -> 360 | assert.isFalse prop.verify("abc") -------------------------------------------------------------------------------- /tests/viewmodel.coffee: -------------------------------------------------------------------------------- 1 | describe "ViewModel", -> 2 | 3 | beforeEach -> 4 | @checkStub = sinon.stub ViewModel, "check" 5 | @delay = ViewModel.delay 6 | ViewModel.delay = (t, f) -> f() 7 | 8 | afterEach -> 9 | sinon.restoreAll() 10 | ViewModel.delay = @delay 11 | 12 | describe "@nextId", -> 13 | it "increments the numbers", -> 14 | a = ViewModel.nextId() 15 | b = ViewModel.nextId() 16 | assert.equal b, a + 1 17 | 18 | describe "@reserved", -> 19 | it "has reserved words", -> 20 | assert.ok ViewModel.reserved.vmId 21 | 22 | describe "@onDestroyed", -> 23 | 24 | it "returns a function", -> 25 | assert.isFunction ViewModel.onDestroyed() 26 | 27 | describe "return function", -> 28 | beforeEach -> 29 | @viewmodel = 30 | vmId: 1 31 | vmOnDestroyed: [] 32 | templateInstance: 33 | view: 34 | name: 'Template.A' 35 | parent: -> undefined 36 | @instance = 37 | autorun: (f) -> f() 38 | viewmodel: @viewmodel 39 | 40 | it "removes the view model from ViewModel.byId", -> 41 | ViewModel.byId = {} 42 | ViewModel.add @viewmodel 43 | ViewModel.onDestroyed().call @instance 44 | assert.isUndefined ViewModel.byId[1] 45 | 46 | it "removes the view model from ViewModel.byTemplate", -> 47 | ViewModel.byTemplate = {} 48 | ViewModel.add @viewmodel 49 | assert.ok ViewModel.byTemplate['A'][1] 50 | ViewModel.onDestroyed().call @instance 51 | assert.isUndefined ViewModel.byTemplate['A'][1] 52 | 53 | it "calls viewmodel.onDestroyed", -> 54 | ran = false 55 | @instance.viewmodel = new ViewModel 56 | onDestroyed: -> ran = true 57 | 58 | @instance.viewmodel.templateInstance = 59 | view: 60 | name: 'Template.A' 61 | 62 | ViewModel.onDestroyed({}).call @instance 63 | assert.isTrue ran 64 | 65 | describe "@onRendered", -> 66 | 67 | it "returns a function", -> 68 | assert.isFunction ViewModel.onRendered() 69 | 70 | describe "return function", -> 71 | afterFlush = Tracker.afterFlush 72 | beforeEach -> 73 | @viewmodel = new ViewModel() 74 | @viewmodel.vmInitial = {} 75 | @instance = 76 | autorun: (f) -> f() 77 | viewmodel: @viewmodel 78 | afterFlush = Tracker.afterFlush 79 | Tracker.afterFlush = (f) -> f() 80 | 81 | afterEach -> 82 | Tracker.afterFlush = afterFlush 83 | 84 | it "checks the arguments", -> 85 | @viewmodel.vmInitial.autorun = "X" 86 | ViewModel.onRendered().call @instance 87 | assert.isTrue @checkStub.calledWithExactly('@onRendered', "X", @instance) 88 | 89 | it "sets autorun for single function", -> 90 | ran = false 91 | @viewmodel.vmAutorun.push -> ran = true 92 | ViewModel.onRendered().call @instance 93 | assert.isTrue ran 94 | 95 | it "calls viewmodel.onRendered", -> 96 | ran = false 97 | @viewmodel.vmOnRendered.push -> ran = true 98 | ViewModel.onRendered().call @instance 99 | assert.isTrue ran 100 | 101 | 102 | 103 | describe "@onCreated", -> 104 | 105 | it "returns a function", -> 106 | assert.isFunction ViewModel.onCreated() 107 | 108 | describe "return function", -> 109 | 110 | beforeEach -> 111 | 112 | @helper = null 113 | @template = 114 | createViewModel: -> 115 | vm = new ViewModel() 116 | vm.vmId = 1 117 | vm.id = -> 118 | return vm 119 | helpers: (obj) => @helper = obj 120 | 121 | @assignChildStub = sinon.stub ViewModel, 'assignChild' 122 | @retFun = ViewModel.onCreated(@template) 123 | @helpersSpy = sinon.spy @template, 'helpers' 124 | @currentDataStub = sinon.stub Template , 'currentData' 125 | @afterFlushStub = sinon.stub Tracker, 'afterFlush' 126 | @instance = 127 | data: "A" 128 | autorun: (f) -> f( { firstRun: true }) 129 | view: 130 | name: 'body' 131 | 132 | it "sets the viewmodel property on the template instance", -> 133 | @retFun.call @instance 134 | assert.isTrue @instance.viewmodel instanceof ViewModel 135 | 136 | it "adds the viewmodel to ViewModel.byId", -> 137 | ViewModel.byId = {} 138 | @retFun.call @instance 139 | assert.equal @instance.viewmodel, ViewModel.byId[@instance.viewmodel.vmId] 140 | 141 | it "adds the viewmodel to ViewModel.byTemplate", -> 142 | ViewModel.byTemplate = {} 143 | @retFun.call @instance 144 | assert.equal @instance.viewmodel, ViewModel.byTemplate['body'][@instance.viewmodel.vmId] 145 | 146 | it "adds templateInstance to the view model", -> 147 | @retFun.call @instance 148 | assert.equal @instance.viewmodel.templateInstance, @instance 149 | 150 | it "adds view model properties as helpers", -> 151 | @retFun.call @instance 152 | assert.ok @helper.id 153 | 154 | it "doesn't add reserved words as helpers", -> 155 | @retFun.call @instance 156 | assert.notOk @helper.vmId 157 | 158 | it "extends the view model with the data context", -> 159 | cache = Tracker.afterFlush 160 | Tracker.afterFlush = (f) -> f() 161 | @instance.data = 162 | name: 'Alan' 163 | @currentDataStub.returns @instance.data 164 | @retFun.call @instance 165 | Tracker.afterFlush = cache 166 | assert.equal 'Alan', @instance.viewmodel.name() 167 | 168 | it "assigns viewmodel as child of the parent", -> 169 | cache = Tracker.afterFlush 170 | Tracker.afterFlush = (f) -> f() 171 | @retFun.call @instance 172 | Tracker.afterFlush = cache 173 | assert.isTrue @assignChildStub.calledWithExactly @instance.viewmodel 174 | 175 | 176 | 177 | describe "@bindIdAttribute", -> 178 | it "has has default value", -> 179 | assert.equal "b-id", ViewModel.bindIdAttribute 180 | 181 | describe "@eventHelper", -> 182 | beforeEach -> 183 | @nextIdStub = sinon.stub ViewModel, 'nextId' 184 | @nextIdStub.returns 99 185 | @onViewReadyFunction = null 186 | Blaze.currentView = 187 | onViewReady: (f) => @onViewReadyFunction = f 188 | 189 | it "returns object with the next bind id", -> 190 | instanceStub = sinon.stub Template, 'instance' 191 | templateInstance = 192 | viewmodel: {} 193 | '$': -> "X" 194 | instanceStub.returns templateInstance 195 | ret = ViewModel.eventHelper() 196 | assert.equal ret[ViewModel.bindIdAttribute + '-e'], 99 197 | 198 | describe "@bindHelper", -> 199 | beforeEach -> 200 | @nextIdStub = sinon.stub ViewModel, 'nextId' 201 | @nextIdStub.returns 99 202 | @onViewReadyFunction = null 203 | Blaze.currentView = 204 | onViewReady: (f) => @onViewReadyFunction = f 205 | _templateInstance: 206 | '$': -> 'X' 207 | 208 | it "returns object with the next bind id", -> 209 | instanceStub = sinon.stub Template, 'instance' 210 | templateInstance = 211 | viewmodel: {} 212 | '$': -> "X" 213 | instanceStub.returns templateInstance 214 | ret = ViewModel.bindHelper() 215 | assert.equal ret[ViewModel.bindIdAttribute], 99 216 | 217 | it "adds the binding to ViewModel.bindObjects", -> 218 | viewmodel = new ViewModel() 219 | instanceStub = sinon.stub Template, 'instance' 220 | parseBindStub = sinon.stub ViewModel, 'parseBind' 221 | bindObject = 222 | text: 'name' 223 | parseBindStub.returns bindObject 224 | templateInstance = 225 | viewmodel: viewmodel 226 | '$': -> "X" 227 | instanceStub.returns templateInstance 228 | ViewModel.bindHelper("text: name") 229 | assert.equal ViewModel.bindObjects[99], bindObject 230 | 231 | it "adds a view model if the template doesn't have one", -> 232 | addEmptyViewModelStub = sinon.stub ViewModel, 'addEmptyViewModel' 233 | instanceStub = sinon.stub Template, 'instance' 234 | templateInstance = 235 | '$': -> "X" 236 | instanceStub.returns templateInstance 237 | ViewModel.bindHelper("text: name") 238 | assert.isTrue addEmptyViewModelStub.calledWith templateInstance 239 | 240 | describe "@getInitialObject", -> 241 | it "returns initial when initial is an object", -> 242 | initial = {} 243 | context = "X" 244 | ret = ViewModel.getInitialObject(initial, context) 245 | assert.equal initial, ret 246 | 247 | it "returns the result of the function when initial is a function", -> 248 | initial = (context) -> context + 1 249 | context = 1 250 | ret = ViewModel.getInitialObject(initial, context) 251 | assert.equal 2, ret 252 | 253 | describe "@makeReactiveProperty", -> 254 | it "returns a function", -> 255 | assert.isFunction ViewModel.makeReactiveProperty("X") 256 | it "sets default value", -> 257 | actual = ViewModel.makeReactiveProperty("X") 258 | assert.equal "X", actual() 259 | it "sets and gets values", -> 260 | actual = ViewModel.makeReactiveProperty("X") 261 | actual("Y") 262 | assert.equal "Y", actual() 263 | it "resets the value", -> 264 | actual = ViewModel.makeReactiveProperty("X") 265 | actual("Y") 266 | actual.reset() 267 | assert.equal "X", actual() 268 | it "has depend and changed", -> 269 | actual = ViewModel.makeReactiveProperty("X") 270 | assert.isFunction actual.depend 271 | assert.isFunction actual.changed 272 | it "reactifies arrays", -> 273 | actual = ViewModel.makeReactiveProperty([]) 274 | assert.ok actual().depend 275 | assert.isTrue actual() instanceof Array 276 | 277 | it "resets arrays", -> 278 | actual = ViewModel.makeReactiveProperty([1]) 279 | actual().push(2) 280 | assert.equal 2, actual().length 281 | actual.reset() 282 | assert.equal 1, actual().length 283 | assert.equal 1, actual()[0] 284 | 285 | describe "delay", -> 286 | beforeEach -> 287 | @clock = sinon.useFakeTimers() 288 | ViewModel.delay = @delay 289 | afterEach -> 290 | @clock.restore() 291 | @delay = ViewModel.delay 292 | 293 | it "delays values", -> 294 | actual = ViewModel.makeReactiveProperty("X") 295 | actual.delay = 10 296 | actual("Y") 297 | @clock.tick 8 298 | assert.equal "X", actual() 299 | @clock.tick 4 300 | assert.equal "Y", actual() 301 | return 302 | 303 | describe "validations", -> 304 | it "returns a function", -> 305 | assert.isFunction ViewModel.makeReactiveProperty(ViewModel.property.string) 306 | it "sets default value", -> 307 | actual = ViewModel.makeReactiveProperty(ViewModel.property.string.default("X")) 308 | assert.equal "X", actual() 309 | it "sets and gets values", -> 310 | actual = ViewModel.makeReactiveProperty(ViewModel.property.string.default("X")) 311 | actual("Y") 312 | assert.equal "Y", actual() 313 | it "resets the value", -> 314 | actual = ViewModel.makeReactiveProperty(ViewModel.property.string.default("X")) 315 | actual("Y") 316 | actual.reset() 317 | assert.equal "X", actual() 318 | 319 | it "reactifies arrays", -> 320 | actual = ViewModel.makeReactiveProperty(ViewModel.property.array) 321 | assert.ok actual().depend 322 | assert.isTrue actual() instanceof Array 323 | 324 | it "resets arrays", -> 325 | actual = ViewModel.makeReactiveProperty(ViewModel.property.array.default([1])) 326 | actual().push(2) 327 | assert.equal 2, actual().length 328 | actual.reset() 329 | assert.equal 1, actual().length 330 | assert.equal 1, actual()[0] 331 | 332 | describe "@addBinding", -> 333 | 334 | last = 1 335 | getBindingName = -> "test" + last++ 336 | 337 | it "checks the arguments", -> 338 | ViewModel.addBinding "X" 339 | assert.isTrue @checkStub.calledWithExactly('@addBinding', "X") 340 | 341 | it "returns nothing", -> 342 | ret = ViewModel.addBinding "X" 343 | assert.isUndefined ret 344 | 345 | it "adds the binding to @bindings", -> 346 | name = getBindingName() 347 | ViewModel.addBinding 348 | name: name 349 | bind: -> "X" 350 | assert.equal 1, ViewModel.bindings[name].length 351 | assert.equal "X", ViewModel.bindings[name][0].bind() 352 | 353 | it "adds the binding to @bindings array", -> 354 | name = getBindingName() 355 | ViewModel.addBinding 356 | name: name 357 | bind: -> "X" 358 | ViewModel.addBinding 359 | name: name 360 | bind: -> "Y" 361 | assert.equal 2, ViewModel.bindings[name].length 362 | assert.equal "X", ViewModel.bindings[name][0].bind() 363 | assert.equal "Y", ViewModel.bindings[name][1].bind() 364 | 365 | it "adds default priority 1 to the binding", -> 366 | name = getBindingName() 367 | ViewModel.addBinding 368 | name: name 369 | assert.equal 1, ViewModel.bindings[name][0].priority 370 | 371 | it "adds priority 10 to the binding", -> 372 | name = getBindingName() 373 | ViewModel.addBinding 374 | name: name 375 | priority: 10 376 | assert.equal 10, ViewModel.bindings[name][0].priority 377 | 378 | it "adds priority 2 with a selector", -> 379 | name = getBindingName() 380 | ViewModel.addBinding 381 | name: name 382 | selector: 'A' 383 | assert.equal 2, ViewModel.bindings[name][0].priority 384 | 385 | it "adds priority 2 with a bindIf", -> 386 | name = getBindingName() 387 | ViewModel.addBinding 388 | name: name 389 | bindIf: -> 390 | assert.equal 2, ViewModel.bindings[name][0].priority 391 | 392 | it "adds priority 3 with a selector and bindIf", -> 393 | name = getBindingName() 394 | ViewModel.addBinding 395 | name: name 396 | selector: 'A' 397 | bindIf: -> 398 | assert.equal 3, ViewModel.bindings[name][0].priority 399 | 400 | 401 | describe "@bindSingle", -> 402 | 403 | beforeEach -> 404 | @getBindArgumentStub = sinon.stub ViewModel, 'getBindArgument' 405 | @getBindingStub = sinon.stub ViewModel, 'getBinding' 406 | 407 | it "returns undefined", -> 408 | @getBindingStub.returns 409 | events: { a: 1 } 410 | element = 411 | bind: -> 412 | ret = ViewModel.bindSingle(null, element) 413 | assert.isUndefined ret 414 | 415 | it "uses getBindArgument", -> 416 | 417 | ViewModel.bindSingle 'templateInstance', 'element', 'bindName', 'bindValue', 'bindObject', 'viewmodel', 'bindingArray', 'bindId', 'view' 418 | assert.isTrue @getBindArgumentStub.calledWithExactly 'templateInstance', 'element', 'bindName', 'bindValue', 'bindObject', 'viewmodel', 'bindId', 'view' 419 | 420 | it "uses getBinding", -> 421 | bindArg = {} 422 | @getBindArgumentStub.returns bindArg 423 | ViewModel.bindSingle 'templateInstance', 'element', 'bindName', 'bindValue', 'bindObject', 'viewmodel', 'bindingArray' 424 | assert.isTrue @getBindingStub.calledWithExactly 'bindName', bindArg, 'bindingArray' 425 | 426 | it "executes autorun", -> 427 | bindArg = 428 | autorun: -> 429 | @getBindArgumentStub.returns bindArg 430 | spy = sinon.spy bindArg, 'autorun' 431 | bindingAutorun = -> 432 | @getBindingStub.returns 433 | autorun: bindingAutorun 434 | 435 | ViewModel.bindSingle() 436 | assert.isTrue spy.calledWithExactly bindingAutorun 437 | 438 | it "executes bind", -> 439 | @getBindArgumentStub.returns 'X' 440 | arg = 441 | bind: -> 442 | spy = sinon.spy arg, 'bind' 443 | @getBindingStub.returns arg 444 | 445 | ViewModel.bindSingle() 446 | assert.isTrue spy.calledWithExactly 'X' 447 | 448 | it "binds events", -> 449 | @getBindingStub.returns 450 | events: { a: 1, b: 2 } 451 | element = 452 | bind: -> 453 | spy = sinon.spy element, 'bind' 454 | ViewModel.bindSingle(null, element) 455 | assert.isTrue spy.calledTwice 456 | assert.isTrue spy.calledWith 'a' 457 | assert.isTrue spy.calledWith 'b' 458 | 459 | describe "@getBinding", -> 460 | 461 | it "returns default binding if can't find one", -> 462 | bindName = 'default' 463 | defaultB = 464 | name: bindName 465 | bindings = {} 466 | bindings[bindName] = [defaultB] 467 | 468 | ret = ViewModel.getBinding 'bindName', 'bindArg', bindings 469 | assert.equal ret, defaultB 470 | 471 | it "returns first binding in one element array", -> 472 | bindName = 'one' 473 | oneBinding = 474 | name: bindName 475 | bindings = {} 476 | bindings[bindName] = [oneBinding] 477 | 478 | ret = ViewModel.getBinding bindName, 'bindArg', bindings 479 | assert.equal ret, oneBinding 480 | 481 | it "returns default binding if can't find one that passes bindIf", -> 482 | bindName = 'default' 483 | defaultB = 484 | name: bindName 485 | bindings = {} 486 | bindings[bindName] = [defaultB] 487 | oneBinding = 488 | name: 'none' 489 | bindIf: -> false 490 | bindings['none'] = [oneBinding] 491 | 492 | ret = ViewModel.getBinding 'none', 'bindArg', bindings 493 | assert.equal ret, defaultB 494 | return 495 | 496 | it "returns highest priority binding", -> 497 | oneBinding = 498 | name: 'X' 499 | priority: 1 500 | twoBinding = 501 | name: 'X' 502 | priority: 2 503 | bindings = 504 | X: [oneBinding, twoBinding] 505 | 506 | ret = ViewModel.getBinding 'X', 'bindArg', bindings 507 | assert.equal ret, twoBinding 508 | 509 | it "returns first that passes bindIf", -> 510 | oneBinding = 511 | name: 'X' 512 | priority: 1 513 | bindIf: -> false 514 | twoBinding = 515 | name: 'X' 516 | priority: 1 517 | bindIf: -> true 518 | bindings = 519 | X: [oneBinding, twoBinding] 520 | 521 | ret = ViewModel.getBinding 'X', 'bindArg', bindings 522 | assert.equal ret, twoBinding 523 | 524 | it "returns first that passes selector", -> 525 | oneBinding = 526 | name: 'X' 527 | priority: 1 528 | selector: "A" 529 | twoBinding = 530 | name: 'X' 531 | priority: 1 532 | selector: "B" 533 | bindings = 534 | X: [oneBinding, twoBinding] 535 | 536 | bindArg = 537 | element: 538 | is: (s) -> s is "B" 539 | ret = ViewModel.getBinding 'X', bindArg, bindings 540 | assert.equal ret, twoBinding 541 | 542 | it "returns first that passes bindIf and selector", -> 543 | oneBinding = 544 | name: 'X' 545 | priority: 1 546 | selector: "B" 547 | bindIf: -> false 548 | twoBinding = 549 | name: 'X' 550 | priority: 1 551 | selector: "B" 552 | bindIf: -> true 553 | bindings = 554 | X: [oneBinding, twoBinding] 555 | 556 | bindArg = 557 | element: 558 | is: (s) -> s is "B" 559 | ret = ViewModel.getBinding 'X', bindArg, bindings 560 | assert.equal ret, twoBinding 561 | 562 | it "returns first that passes bindIf and selector with highest priority", -> 563 | oneBinding = 564 | name: 'X' 565 | priority: 1 566 | selector: "B" 567 | bindIf: -> true 568 | twoBinding = 569 | name: 'X' 570 | priority: 2 571 | selector: "B" 572 | bindIf: -> true 573 | bindings = 574 | X: [oneBinding, twoBinding] 575 | 576 | bindArg = 577 | element: 578 | is: (s) -> s is "B" 579 | ret = ViewModel.getBinding 'X', bindArg, bindings 580 | assert.equal ret, twoBinding 581 | 582 | describe "@getBindArgument", -> 583 | 584 | beforeEach -> 585 | @getVmValueGetterStub = sinon.stub ViewModel, 'getVmValueGetter' 586 | @getVmValueSetterStub = sinon.stub ViewModel, 'getVmValueSetter' 587 | 588 | it "returns right object", -> 589 | ret = ViewModel.getBindArgument 'templateInstance', 'element', 'bindName', 'bindValue', 'bindObject', 'viewmodel' 590 | ret = _.omit(ret, 'autorun', 'getVmValue', 'setVmValue') 591 | expected = 592 | templateInstance: 'templateInstance' 593 | element: 'element' 594 | elementBind: 'bindObject' 595 | bindName: 'bindName' 596 | bindValue: 'bindValue' 597 | viewmodel: 'viewmodel' 598 | assert.isTrue _.isEqual(expected, ret) 599 | 600 | it "returns argument with autorun", -> 601 | templateInstance = 602 | autorun: -> 603 | spy = sinon.spy templateInstance, 'autorun' 604 | bindArg = ViewModel.getBindArgument templateInstance, 'element', 'bindName', 'bindValue', 'bindObject', 'viewmodel' 605 | bindArg.autorun -> 606 | assert.isTrue spy.calledOnce 607 | 608 | it "returns argument with vmValueGetter", -> 609 | @getVmValueGetterStub.returns -> "A" 610 | bindArg = ViewModel.getBindArgument 'templateInstance', 'element', 'bindName', 'bindValue', 'bindObject', 'viewmodel' 611 | assert.equal "A", bindArg.getVmValue() 612 | 613 | it "returns argument with vmValueSetter", -> 614 | @getVmValueSetterStub.returns -> "A" 615 | bindArg = ViewModel.getBindArgument 'templateInstance', 'element', 'bindName', 'bindValue', 'bindObject', 'viewmodel' 616 | assert.equal "A", bindArg.setVmValue() 617 | 618 | describe "@getVmValueGetter", -> 619 | 620 | it "returns value from 1 + 'A'", -> 621 | viewmodel = {} 622 | bindValue = ViewModel.parseBind("x: 1 + 'A'").x 623 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 624 | assert.equal "1A", getVmValue() 625 | 626 | it "returns value from name", -> 627 | viewmodel = 628 | name: -> "A" 629 | bindValue = 'name' 630 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 631 | assert.equal "A", getVmValue() 632 | 633 | it "returns short circuits false && true", -> 634 | called = false 635 | viewmodel = 636 | a: -> false 637 | b: -> 638 | called = true 639 | true 640 | bindValue = "a && b" 641 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 642 | assert.equal false, getVmValue() 643 | assert.equal false, called 644 | 645 | it "returns short circuits true || false", -> 646 | called = false 647 | viewmodel = 648 | a: -> true 649 | b: -> 650 | called = true 651 | true 652 | bindValue = "a || b" 653 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 654 | assert.equal true, getVmValue() 655 | assert.equal false, called 656 | 657 | it "returns value from call(1, -2)", -> 658 | viewmodel = 659 | call: (a, b) -> b 660 | bindValue = "call(1, -2)" 661 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 662 | assert.equal -2, getVmValue() 663 | 664 | it "returns value from call(1 - 2)", -> 665 | viewmodel = 666 | call: (a) -> a 667 | bindValue = "call(1 - 2)" 668 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 669 | assert.equal -1, getVmValue() 670 | 671 | it "returns value from call(1, 1 - 2)", -> 672 | viewmodel = 673 | call: (a, b) -> b 674 | bindValue = "call(1, 1 - 2)" 675 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 676 | assert.equal -1, getVmValue() 677 | 678 | it "returns value from name(address.zip)", -> 679 | viewmodel = 680 | name: (val) -> val is 100 681 | address: 682 | zip: 100 683 | bindValue = 'name(address.zip)' 684 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 685 | assert.isTrue getVmValue() 686 | return 687 | 688 | it "returns false from !'A'", -> 689 | viewmodel = 690 | name: -> "A" 691 | bindValue = '!name' 692 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 693 | assert.equal false, getVmValue() 694 | 695 | it "returns value from name.first (first is prop)", -> 696 | viewmodel = 697 | name: -> 698 | first: "A" 699 | bindValue = 'name.first' 700 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 701 | assert.equal "A", getVmValue() 702 | return 703 | 704 | it "returns value from name.first (first is func)", -> 705 | viewmodel = 706 | name: -> 707 | first: -> "A" 708 | bindValue = 'name.first' 709 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 710 | assert.equal "A", getVmValue() 711 | 712 | it "returns value from name()", -> 713 | viewmodel = 714 | name: -> "A" 715 | bindValue = 'name()' 716 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 717 | assert.equal "A", getVmValue() 718 | 719 | it "doesn't give arguments to name()", -> 720 | viewmodel = 721 | name: -> arguments.length 722 | bindValue = 'name()' 723 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 724 | assert.equal 0, getVmValue() 725 | 726 | it "returns value from name('a')", -> 727 | viewmodel = 728 | name: (a) -> a 729 | bindValue = "name('a')" 730 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 731 | assert.equal "a", getVmValue() 732 | 733 | it "returns value from name('a', 1)", -> 734 | viewmodel = 735 | name: (a, b) -> a + b 736 | bindValue = "name('a', 1)" 737 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 738 | assert.equal "a1", getVmValue() 739 | return 740 | 741 | it "returns value from name(first) with string", -> 742 | viewmodel = 743 | name: (v) -> v 744 | first: -> "A" 745 | bindValue = 'name(first)' 746 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 747 | assert.equal "A", getVmValue() 748 | 749 | it "returns value from name(first, second)", -> 750 | viewmodel = 751 | name: (a, b) -> a + b 752 | first: -> "A" 753 | second: -> "B" 754 | bindValue = 'name(first, second)' 755 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 756 | assert.equal "AB", getVmValue() 757 | 758 | it "returns value from name(first, second) with numbers", -> 759 | viewmodel = 760 | name: (a, b) -> a + b 761 | first: -> 1 762 | second: -> 2 763 | bindValue = 'name(first, second)' 764 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 765 | assert.equal 3, getVmValue() 766 | 767 | it "returns value from name(first, second) with booleans", -> 768 | viewmodel = 769 | name: (a, b) -> a or b 770 | first: -> false 771 | second: -> true 772 | bindValue = 'name(first, second)' 773 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 774 | assert.isTrue getVmValue() 775 | 776 | it "returns value from name(first) with null", -> 777 | viewmodel = 778 | name: (a) -> a 779 | first: -> null 780 | bindValue = 'name(first)' 781 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 782 | assert.isNull getVmValue() 783 | 784 | it "returns value from name(first) with undefined", -> 785 | viewmodel = 786 | name: (a) -> a 787 | first: -> undefined 788 | bindValue = 'name(first)' 789 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 790 | assert.isUndefined getVmValue() 791 | 792 | it "returns value from name(1, 2)", -> 793 | viewmodel = 794 | name: (a, b) -> a + b 795 | bindValue = 'name(1, 2)' 796 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 797 | assert.equal 3, getVmValue() 798 | 799 | it "returns value from name(false, true)", -> 800 | viewmodel = 801 | name: (a, b) -> a or b 802 | bindValue = 'name(false, true)' 803 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 804 | assert.isTrue getVmValue() 805 | 806 | it "returns value from name(null)", -> 807 | viewmodel = 808 | name: (a) -> a 809 | bindValue = 'name(null)' 810 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 811 | assert.isNull getVmValue() 812 | 813 | it "returns value from name(undefined)", -> 814 | viewmodel = 815 | name: (a) -> a 816 | bindValue = 'name(undefined)' 817 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 818 | assert.isUndefined getVmValue() 819 | 820 | it "returns value from name(!first, !second) with booleans", -> 821 | viewmodel = 822 | name: (a, b) -> a and b 823 | first: -> false 824 | second: -> false 825 | bindValue = 'name(!first, !second)' 826 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 827 | assert.isTrue getVmValue() 828 | 829 | it "returns value from name().first (first is prop)", -> 830 | viewmodel = 831 | name: -> 832 | first: "A" 833 | bindValue = 'name.first' 834 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 835 | assert.equal "A", getVmValue() 836 | 837 | it "returns value from name().first (first is func)", -> 838 | viewmodel = 839 | name: -> 840 | first: -> "A" 841 | bindValue = 'name.first' 842 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 843 | assert.equal "A", getVmValue() 844 | 845 | it "returns value from name(1).first (first is prop)", -> 846 | viewmodel = 847 | name: (v) -> 848 | if v is 1 849 | first: "A" 850 | bindValue = 'name(1).first' 851 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 852 | assert.equal "A", getVmValue() 853 | return 854 | 855 | it "returns value from name(1)", -> 856 | viewmodel = 857 | name: (a) -> a 858 | bindValue = 'name(1)' 859 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 860 | assert.isTrue 1 is getVmValue() 861 | 862 | it "returns value from name().first()", -> 863 | viewmodel = 864 | name: -> 865 | first: -> "A" 866 | bindValue = 'name().first()' 867 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 868 | assert.equal "A", getVmValue() 869 | 870 | 871 | it "returns value from name().first.second", -> 872 | viewmodel = 873 | name: -> 874 | first: 875 | second: "A" 876 | bindValue = 'name().first.second' 877 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 878 | assert.equal "A", getVmValue() 879 | 880 | it "returns value from name().first.second()", -> 881 | viewmodel = 882 | name: -> 883 | first: 884 | second: -> "A" 885 | bindValue = 'name().first.second()' 886 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 887 | assert.equal "A", getVmValue() 888 | 889 | it "returns value from name().first.second()", -> 890 | viewmodel = 891 | name: -> 892 | first: 893 | second: -> "A" 894 | bindValue = 'name().first.second()' 895 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 896 | assert.equal "A", getVmValue() 897 | 898 | it "returns value from first + second", -> 899 | viewmodel = 900 | first: 1 901 | second: 2 902 | bindValue = ViewModel.parseBind("x: first + second").x 903 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 904 | assert.equal 3, getVmValue() 905 | return 906 | 907 | it "returns value from first + ' - ' + second", -> 908 | viewmodel = 909 | first: 1 910 | second: 2 911 | bindValue = ViewModel.parseBind("x: first + ' - ' + second").x 912 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 913 | assert.equal "1 - 2", getVmValue() 914 | return 915 | 916 | it "returns value from first + second", -> 917 | viewmodel = 918 | first: 1 919 | second: 2 920 | bindValue = ViewModel.parseBind("x: first + second").x 921 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 922 | assert.equal 3, getVmValue() 923 | return 924 | 925 | it "returns value from first - second", -> 926 | viewmodel = 927 | first: 3 928 | second: 2 929 | bindValue = ViewModel.parseBind("x: first - second").x 930 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 931 | assert.equal 1, getVmValue() 932 | return 933 | 934 | it "returns value from first * second", -> 935 | viewmodel = 936 | first: 3 937 | second: 2 938 | bindValue = ViewModel.parseBind("x: first * second").x 939 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 940 | assert.equal 6, getVmValue() 941 | return 942 | 943 | it "returns value from first / second", -> 944 | viewmodel = 945 | first: 6 946 | second: 2 947 | bindValue = ViewModel.parseBind("x: first / second").x 948 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 949 | assert.equal 3, getVmValue() 950 | return 951 | 952 | it "returns value from first && second", -> 953 | viewmodel = 954 | first: true 955 | second: true 956 | bindValue = ViewModel.parseBind("x: first && second").x 957 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 958 | assert.isTrue getVmValue() 959 | return 960 | 961 | it "returns value from first || second", -> 962 | viewmodel = 963 | first: false 964 | second: true 965 | bindValue = ViewModel.parseBind("x: first || second").x 966 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 967 | assert.isTrue getVmValue() 968 | return 969 | 970 | it "returns value from first == second", -> 971 | viewmodel = 972 | first: 1 973 | second: '1' 974 | bindValue = ViewModel.parseBind("x: first == second").x 975 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 976 | assert.isTrue getVmValue() 977 | return 978 | 979 | it "returns value from first === second", -> 980 | viewmodel = 981 | first: 1 982 | second: 1 983 | bindValue = ViewModel.parseBind("x: first === second").x 984 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 985 | assert.isTrue getVmValue() 986 | return 987 | 988 | it "returns value from first !== second", -> 989 | viewmodel = 990 | first: 1 991 | second: 1 992 | bindValue = ViewModel.parseBind("x: first !== second").x 993 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 994 | assert.isFalse getVmValue() 995 | return 996 | 997 | it "returns value from first !=== second", -> 998 | viewmodel = 999 | first: 1 1000 | second: 1 1001 | bindValue = ViewModel.parseBind("x: first !=== second").x 1002 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1003 | assert.isFalse getVmValue() 1004 | return 1005 | 1006 | it "returns value from first > second", -> 1007 | viewmodel = 1008 | first: 1 1009 | second: 0 1010 | bindValue = ViewModel.parseBind("x: first > second").x 1011 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1012 | assert.isTrue getVmValue() 1013 | return 1014 | 1015 | it "returns value from first > second", -> 1016 | viewmodel = 1017 | first: 1 1018 | second: 1 1019 | bindValue = ViewModel.parseBind("x: first > second").x 1020 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1021 | assert.isFalse getVmValue() 1022 | return 1023 | 1024 | it "returns value from first > second", -> 1025 | viewmodel = 1026 | first: 1 1027 | second: 2 1028 | bindValue = ViewModel.parseBind("x: first > second").x 1029 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1030 | assert.isFalse getVmValue() 1031 | return 1032 | 1033 | it "returns value from first >= second", -> 1034 | viewmodel = 1035 | first: 1 1036 | second: 0 1037 | bindValue = ViewModel.parseBind("x: first >= second").x 1038 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1039 | assert.isTrue getVmValue() 1040 | return 1041 | 1042 | it "returns value from first >= second", -> 1043 | viewmodel = 1044 | first: 1 1045 | second: 1 1046 | bindValue = ViewModel.parseBind("x: first >= second").x 1047 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1048 | assert.isTrue getVmValue() 1049 | return 1050 | 1051 | it "returns value from first >= second", -> 1052 | viewmodel = 1053 | first: 1 1054 | second: 2 1055 | bindValue = ViewModel.parseBind("x: first >= second").x 1056 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1057 | assert.isFalse getVmValue() 1058 | return 1059 | 1060 | it "returns value from first < second", -> 1061 | viewmodel = 1062 | first: 1 1063 | second: 0 1064 | bindValue = ViewModel.parseBind("x: first < second").x 1065 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1066 | assert.isFalse getVmValue() 1067 | return 1068 | 1069 | it "returns value from first < second", -> 1070 | viewmodel = 1071 | first: 1 1072 | second: 1 1073 | bindValue = ViewModel.parseBind("x: first < second").x 1074 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1075 | assert.isFalse getVmValue() 1076 | return 1077 | 1078 | it "returns value from first < second", -> 1079 | viewmodel = 1080 | first: 1 1081 | second: 2 1082 | bindValue = ViewModel.parseBind("x: first < second").x 1083 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1084 | assert.isTrue getVmValue() 1085 | return 1086 | 1087 | it "returns value from first <= second", -> 1088 | viewmodel = 1089 | first: 1 1090 | second: 0 1091 | bindValue = ViewModel.parseBind("x: first <= second").x 1092 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1093 | assert.isFalse getVmValue() 1094 | return 1095 | 1096 | it "returns value from first <= second", -> 1097 | viewmodel = 1098 | first: 1 1099 | second: 1 1100 | bindValue = ViewModel.parseBind("x: first <= second").x 1101 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1102 | assert.isTrue getVmValue() 1103 | return 1104 | 1105 | it "returns value from first <= second", -> 1106 | viewmodel = 1107 | first: 1 1108 | second: 2 1109 | bindValue = ViewModel.parseBind("x: first <= second").x 1110 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1111 | assert.isTrue getVmValue() 1112 | return 1113 | 1114 | it "returns value from first(1.1)", -> 1115 | viewmodel = 1116 | first: (v) -> v 1117 | bindValue = 'first(1.1)' 1118 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1119 | assert.equal 1.1, getVmValue() 1120 | return 1121 | 1122 | it "returns value from first1.second", -> 1123 | viewmodel = 1124 | first1: 1125 | second: 2 1126 | bindValue = 'first1.second' 1127 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1128 | assert.equal 2, getVmValue() 1129 | return 1130 | 1131 | it "returns value from first.1second", -> 1132 | viewmodel = 1133 | first: 1134 | '1second': 2 1135 | bindValue = 'first.1second' 1136 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1137 | assert.equal 2, getVmValue() 1138 | return 1139 | 1140 | it "returns value from first(this)", -> 1141 | instance = 1142 | data: 1143 | a: 1 1144 | stub = sinon.stub Template, 'instance' 1145 | stub.returns instance 1146 | viewmodel = 1147 | first: (ins) -> ins.a is 1 1148 | bindValue = 'first(this)' 1149 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1150 | assert.isTrue getVmValue() 1151 | return 1152 | 1153 | it "returns value from first(this.a)", -> 1154 | instance = 1155 | data: 1156 | a: 1 1157 | stub = sinon.stub Template, 'instance' 1158 | stub.returns instance 1159 | viewmodel = 1160 | first: (ins) -> ins is 1 1161 | bindValue = 'first(this.a)' 1162 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1163 | assert.isTrue getVmValue() 1164 | return 1165 | 1166 | it "returns value from parent.first", -> 1167 | viewmodel = 1168 | name: -> 'A' 1169 | parent: -> 1170 | val = this.name() 1171 | first: val 1172 | bindValue = 'parent.first' 1173 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1174 | assert.equal 'A', getVmValue() 1175 | return 1176 | 1177 | it "creates property on view model", -> 1178 | viewmodel = new ViewModel() 1179 | bindValue = 'name' 1180 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1181 | assert.isUndefined getVmValue() 1182 | assert.ok viewmodel.name 1183 | return 1184 | 1185 | it "returns quoted string", -> 1186 | viewmodel = {} 1187 | bindValue = '"Hi"' 1188 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1189 | assert.equal 'Hi', getVmValue() 1190 | return 1191 | 1192 | it "returns single quoted string", -> 1193 | viewmodel = {} 1194 | bindValue = "'Hi'" 1195 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1196 | assert.equal 'Hi', getVmValue() 1197 | return 1198 | 1199 | it "returns value from parent.first.second", -> 1200 | viewmodel = 1201 | parent: 1202 | first: 1203 | second: 'A' 1204 | bindValue = 'parent.first.second' 1205 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1206 | assert.equal 'A', getVmValue() 1207 | return 1208 | 1209 | it "returns value from parent.first(second)", -> 1210 | parent = new ViewModel() 1211 | parent.first = (v) -> v is 'A' 1212 | viewmodel = new ViewModel() 1213 | viewmodel.second = 'A' 1214 | viewmodel.parent = parent 1215 | 1216 | bindValue = 'parent.first(second)' 1217 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1218 | assert.isTrue getVmValue() 1219 | return 1220 | 1221 | it "returns value from first( second )", -> 1222 | viewmodel = new ViewModel() 1223 | viewmodel.load 1224 | first: (v) -> v 1225 | second: 'A' 1226 | bindValue = 'first( second )' 1227 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1228 | assert.equal 'A', getVmValue() 1229 | return 1230 | 1231 | it "returns value from first( second , third )", -> 1232 | viewmodel = new ViewModel() 1233 | viewmodel.load 1234 | first: (a, b) -> a + b 1235 | second: 'A' 1236 | third: 'B' 1237 | bindValue = 'first( second , third )' 1238 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1239 | assert.equal 'AB', getVmValue() 1240 | return 1241 | 1242 | it "returns value from !first && second", -> 1243 | viewmodel = 1244 | first: true 1245 | second: true 1246 | bindValue = ViewModel.parseBind("x: !first && second").x 1247 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1248 | assert.isFalse getVmValue() 1249 | return 1250 | 1251 | it "returns value from !first && second _2", -> 1252 | viewmodel = 1253 | first: false 1254 | second: true 1255 | bindValue = ViewModel.parseBind("x: !first && second").x 1256 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1257 | assert.isTrue getVmValue() 1258 | return 1259 | 1260 | it "returns value from !first && second _3", -> 1261 | viewmodel = 1262 | first: false 1263 | second: false 1264 | bindValue = ViewModel.parseBind("x: !first && second").x 1265 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1266 | assert.isFalse getVmValue() 1267 | return 1268 | 1269 | it "returns value from !first || second", -> 1270 | viewmodel = 1271 | first: false 1272 | second: true 1273 | bindValue = ViewModel.parseBind("x: !first || second").x 1274 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1275 | assert.isTrue getVmValue() 1276 | return 1277 | 1278 | it "returns value from !first || second _2", -> 1279 | viewmodel = 1280 | first: true 1281 | second: false 1282 | bindValue = ViewModel.parseBind("x: !first || second").x 1283 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1284 | assert.isFalse getVmValue() 1285 | return 1286 | 1287 | it "returns value from !first || second _3", -> 1288 | viewmodel = 1289 | first: true 1290 | second: true 1291 | bindValue = ViewModel.parseBind("x: !first || second").x 1292 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1293 | assert.isTrue getVmValue() 1294 | return 1295 | 1296 | it "returns value from 2**3", -> 1297 | viewmodel = {} 1298 | bindValue = ViewModel.parseBind("x: 2**3").x 1299 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1300 | assert.equal getVmValue(), 8 1301 | return 1302 | 1303 | it "returns value from 9%4", -> 1304 | viewmodel = {} 1305 | bindValue = ViewModel.parseBind("x: 9%4").x 1306 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1307 | assert.equal getVmValue(), 1 1308 | return 1309 | 1310 | describe "@getVmValueSetter", -> 1311 | 1312 | it "sets first && second", -> 1313 | firstVal = null 1314 | secondVal = null 1315 | viewmodel = 1316 | first: (v) -> firstVal = v 1317 | second: (v) -> secondVal = v 1318 | bindValue = 'first && second' 1319 | setVmValue = ViewModel.getVmValueSetter(viewmodel, bindValue) 1320 | setVmValue(2) 1321 | assert.equal 2, firstVal 1322 | assert.equal 2, secondVal 1323 | return 1324 | 1325 | it "sets first func", -> 1326 | val = null 1327 | viewmodel = 1328 | first: (v) -> val = v 1329 | bindValue = 'first' 1330 | setVmValue = ViewModel.getVmValueSetter(viewmodel, bindValue) 1331 | setVmValue(2) 1332 | assert.equal 2, val 1333 | return 1334 | 1335 | it "sets first(true)", -> 1336 | val = null 1337 | viewmodel = 1338 | first: (v) -> val = v 1339 | bindValue = 'first(true)' 1340 | setVmValue = ViewModel.getVmValueSetter(viewmodel, bindValue) 1341 | setVmValue(2) 1342 | assert.isTrue val 1343 | return 1344 | 1345 | it "sets first(second)", -> 1346 | val = null 1347 | viewmodel = 1348 | first: (v) -> val = v 1349 | second: 2 1350 | bindValue = 'first(second)' 1351 | setVmValue = ViewModel.getVmValueSetter(viewmodel, bindValue) 1352 | setVmValue() 1353 | assert.equal val , 2 1354 | return 1355 | 1356 | it "sets first(second) with event", -> 1357 | val = null 1358 | evt = null 1359 | viewmodel = 1360 | first: (v, e) -> 1361 | val = v 1362 | evt = e 1363 | second: 2 1364 | bindValue = 'first(second)' 1365 | setVmValue = ViewModel.getVmValueSetter(viewmodel, bindValue) 1366 | setVmValue(3) 1367 | assert.equal val , 2 1368 | assert.equal evt , 3 1369 | return 1370 | 1371 | it "works with sub properties", -> 1372 | viewmodel = 1373 | formData: 1374 | position: "" 1375 | bindValue = 'formData.position' 1376 | getVmValue = ViewModel.getVmValueGetter(viewmodel, bindValue) 1377 | assert.equal getVmValue() , "" 1378 | return 1379 | 1380 | it "doesn't do anything if bindValue isn't a string", -> 1381 | val = null 1382 | viewmodel = 1383 | first: (v) -> val = v 1384 | setVmValue = ViewModel.getVmValueSetter(viewmodel, {}) 1385 | setVmValue(2) 1386 | return 1387 | 1388 | it "sets first prop", -> 1389 | viewmodel = 1390 | first: 1 1391 | bindValue = 'first' 1392 | setVmValue = ViewModel.getVmValueSetter(viewmodel, bindValue) 1393 | setVmValue(2) 1394 | assert.equal 2, viewmodel.first 1395 | return 1396 | 1397 | it "sets first.second func.func", -> 1398 | val = null 1399 | viewmodel = 1400 | first: -> 1401 | second: (v) -> val = v 1402 | bindValue = 'first.second' 1403 | setVmValue = ViewModel.getVmValueSetter(viewmodel, bindValue) 1404 | setVmValue(2) 1405 | assert.equal 2, val 1406 | return 1407 | 1408 | it "sets first().second func.func", -> 1409 | val = null 1410 | viewmodel = 1411 | first: -> 1412 | second: (v) -> val = v 1413 | bindValue = 'first().second' 1414 | setVmValue = ViewModel.getVmValueSetter(viewmodel, bindValue) 1415 | setVmValue(2) 1416 | assert.equal 2, val 1417 | return 1418 | 1419 | it "sets first.second.third p.p.p", -> 1420 | viewmodel = 1421 | first: 1422 | second: 1423 | third: false 1424 | bindValue = 'first.second.third' 1425 | setVmValue = ViewModel.getVmValueSetter(viewmodel, bindValue) 1426 | setVmValue(true) 1427 | assert.isTrue viewmodel.first.second.third 1428 | return 1429 | 1430 | describe "@addEmptyViewModel", -> 1431 | 1432 | it "adds a view model to the template instance", -> 1433 | context = null 1434 | onViewDestroyedCalled = false 1435 | f = -> 1436 | context = this 1437 | onCreatedStub = sinon.stub ViewModel, 'onCreated' 1438 | onCreatedStub.returns f 1439 | vm = new ViewModel() 1440 | vm.vmInitial = {} 1441 | templateInstance = 1442 | viewmodel: vm 1443 | view: 1444 | onViewDestroyed: -> onViewDestroyedCalled = true 1445 | template: {} 1446 | ViewModel.addEmptyViewModel(templateInstance) 1447 | assert.equal context, templateInstance 1448 | assert.isTrue onViewDestroyedCalled 1449 | 1450 | describe "@parentTemplate", -> 1451 | 1452 | it "returns undefined if it doesn't have a parent view", -> 1453 | templateInstance = 1454 | view: {} 1455 | parent = ViewModel.parentTemplate templateInstance 1456 | assert.isUndefined parent 1457 | 1458 | it "returns undefined if parent view isn't a template", -> 1459 | templateInstance = 1460 | view: 1461 | parentView: 1462 | name: 'X' 1463 | parent = ViewModel.parentTemplate templateInstance 1464 | assert.isUndefined parent 1465 | 1466 | it "returns template instance if parent view is a template", -> 1467 | templateInstance = 1468 | view: 1469 | parentView: 1470 | name: 'Template.A' 1471 | templateInstance: -> "X" 1472 | parent = ViewModel.parentTemplate templateInstance 1473 | assert.equal "X", parent 1474 | 1475 | it "returns template instance if parent view is body", -> 1476 | templateInstance = 1477 | view: 1478 | parentView: 1479 | name: 'body' 1480 | templateInstance: -> "X" 1481 | parent = ViewModel.parentTemplate templateInstance 1482 | assert.equal "X", parent 1483 | 1484 | describe "@assignChild", -> 1485 | 1486 | it "adds viewmodel to children", -> 1487 | arr = [] 1488 | vm = 1489 | parent: -> 1490 | children: -> arr 1491 | ViewModel.assignChild vm 1492 | assert.equal 1, arr.length 1493 | assert.equal vm, arr[0] 1494 | 1495 | it "doesn't do anything without a parent template", -> 1496 | vm = 1497 | parent: -> 1498 | ViewModel.assignChild vm 1499 | 1500 | describe "@templateName", -> 1501 | it "returns body if the template is the body", -> 1502 | name = ViewModel.templateName 1503 | view: 1504 | name: 'body' 1505 | assert.equal 'body', name 1506 | 1507 | it "returns name of the template", -> 1508 | name = ViewModel.templateName 1509 | view: 1510 | name: 'Template.mine' 1511 | assert.equal 'mine', name 1512 | 1513 | describe "@find", -> 1514 | before -> 1515 | ViewModel.byId = {} 1516 | ViewModel.byTemplate = {} 1517 | @vm1 = new ViewModel 1518 | name: 'A' 1519 | age: 2 1520 | @vm1.templateInstance = 1521 | view: 1522 | name: 'Template.X' 1523 | ViewModel.add @vm1 1524 | @vm2 = new ViewModel 1525 | name: 'B' 1526 | age: 1 1527 | @vm2.templateInstance = 1528 | view: 1529 | name: 'Template.X' 1530 | ViewModel.add @vm2 1531 | @vm3 = new ViewModel 1532 | name: 'C' 1533 | age: 1 1534 | @vm3.templateInstance = 1535 | view: 1536 | name: 'Template.Y' 1537 | ViewModel.add @vm3 1538 | 1539 | 1540 | it "returns all without parameters", -> 1541 | vms = ViewModel.find() 1542 | assert.isTrue vms instanceof Array 1543 | assert.equal 3, vms.length 1544 | assert.equal @vm1, vms[0] 1545 | assert.equal @vm2, vms[1] 1546 | assert.equal @vm3, vms[2] 1547 | 1548 | it "returns all for template X", -> 1549 | vms = ViewModel.find('X') 1550 | assert.isTrue vms instanceof Array 1551 | assert.equal 2, vms.length 1552 | assert.equal @vm1, vms[0] 1553 | assert.equal @vm2, vms[1] 1554 | 1555 | it "returns all for template X with a predicate", -> 1556 | vms = ViewModel.find('X', (vm) -> vm.name() is 'B') 1557 | assert.isTrue vms instanceof Array 1558 | assert.equal 1, vms.length 1559 | assert.equal @vm2, vms[0] 1560 | 1561 | it "returns all for a predicate", -> 1562 | vms = ViewModel.find((vm) -> vm.age() is 1) 1563 | assert.isTrue vms instanceof Array 1564 | assert.equal 2, vms.length 1565 | assert.equal @vm2, vms[0] 1566 | assert.equal @vm3, vms[1] 1567 | 1568 | describe "@findOne", -> 1569 | 1570 | it "returns first one without params", -> 1571 | vm = ViewModel.findOne() 1572 | assert.equal @vm1, vm 1573 | 1574 | it "returns first for template X", -> 1575 | vm = ViewModel.findOne('X') 1576 | assert.equal @vm1, vm 1577 | 1578 | it "returns first for template X with predicate", -> 1579 | vm = ViewModel.findOne('X', (vm) -> vm.name() is 'B') 1580 | assert.equal @vm2, vm 1581 | 1582 | it "returns first with predicate", -> 1583 | vm = ViewModel.findOne((vm) -> vm.age() is 1) 1584 | assert.equal @vm2, vm --------------------------------------------------------------------------------