├── .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
--------------------------------------------------------------------------------