├── .gitignore ├── README.md ├── examples ├── all │ ├── .meteor │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ ├── client │ │ ├── index.html │ │ ├── lib │ │ │ ├── blaze-layout.js │ │ │ └── register-bind.js │ │ └── views │ │ │ ├── full │ │ │ ├── jade │ │ │ ├── layout.html │ │ │ ├── minimalist │ │ │ ├── pikaday │ │ │ ├── quickstart │ │ │ └── usage │ ├── lib │ │ └── router.js │ └── packages ├── full │ ├── .meteor │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ ├── client │ │ ├── index.html │ │ └── views │ │ │ ├── bindings │ │ │ ├── checked.html │ │ │ ├── checked.js │ │ │ ├── class.css │ │ │ ├── class.html │ │ │ ├── class.js │ │ │ ├── click.html │ │ │ ├── click.js │ │ │ ├── date.html │ │ │ ├── date.js │ │ │ ├── disabled.html │ │ │ ├── disabled.js │ │ │ ├── enter-key.html │ │ │ ├── enter-key.js │ │ │ ├── files.html │ │ │ ├── files.js │ │ │ ├── focused.html │ │ │ ├── focused.js │ │ │ ├── hovered.html │ │ │ ├── hovered.js │ │ │ ├── key.html │ │ │ ├── key.js │ │ │ ├── radio.html │ │ │ ├── radio.js │ │ │ ├── select.html │ │ │ ├── select.js │ │ │ ├── start-value.html │ │ │ ├── start-value.js │ │ │ ├── textarea.html │ │ │ ├── textarea.js │ │ │ ├── throttled.html │ │ │ ├── throttled.js │ │ │ ├── toggle.html │ │ │ ├── toggle.js │ │ │ ├── value.html │ │ │ └── value.js │ │ │ ├── full.html │ │ │ └── full.js │ └── packages ├── jade │ ├── .meteor │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ ├── client │ │ ├── index.jade │ │ ├── lib │ │ │ └── register-bind.js │ │ └── views │ │ │ ├── bindings │ │ │ ├── jadeChecked.tpl.jade │ │ │ ├── jadeDisabled.tpl.jade │ │ │ ├── jadeFiles.tpl.jade │ │ │ ├── jadeFocused.tpl.jade │ │ │ ├── jadeHovered.tpl.jade │ │ │ ├── jadeToggle.tpl.jade │ │ │ └── jadeValue.tpl.jade │ │ │ └── jade.tpl.jade │ └── packages ├── minimalist │ ├── .meteor │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ ├── client │ │ ├── index.html │ │ ├── lib │ │ │ └── register-bind.js │ │ └── views │ │ │ ├── bindings │ │ │ ├── checked.html │ │ │ ├── disabled.html │ │ │ ├── files.html │ │ │ ├── focused.html │ │ │ ├── hovered.html │ │ │ ├── toggle.html │ │ │ └── value.html │ │ │ └── minimalist.html │ └── packages ├── pikaday │ ├── .meteor │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ ├── client │ │ ├── index.html │ │ ├── lib │ │ │ └── register-bind.js │ │ └── views │ │ │ └── pikaday.html │ └── packages ├── quickstart │ ├── .meteor │ │ ├── .finished-upgraders │ │ ├── .gitignore │ │ ├── .id │ │ ├── packages │ │ ├── platforms │ │ ├── release │ │ └── versions │ ├── client │ │ ├── index.html │ │ ├── lib │ │ │ └── register-bind.js │ │ └── views │ │ │ └── quickstart.html │ └── packages └── usage │ ├── .meteor │ ├── .finished-upgraders │ ├── .gitignore │ ├── .id │ ├── packages │ ├── platforms │ ├── release │ └── versions │ ├── client │ ├── index.html │ └── views │ │ ├── field.html │ │ ├── field.js │ │ ├── usage.html │ │ └── usage.js │ └── packages └── packages └── dalgard_viewmodel ├── .versions ├── bindings ├── checked.js ├── class.js ├── click.js ├── disabled.js ├── enter-key.js ├── files.js ├── focused.js ├── hovered.js ├── key.js ├── pikaday.js ├── radio.js ├── submit.js ├── toggle.js └── value.js ├── lib ├── base.js ├── binding.js ├── list.js ├── nexus.js ├── property.js ├── utils.js └── viewmodel.js └── package.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-workspace 2 | *.sublime-project 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dalgard:viewmodel 1.0.2 2 | ======================= 3 |
4 | 5 | > **Version `1.0.0` has been released** after an extended period without issues. 6 | > 7 | > The new version should be compatible with the previous version `0.9.4`, except that jQuery has been removed as a dependency, meaning that elements and events are no longer wrapped in jQuery. 8 | > 9 | > See the [History](#history) section for more info. 10 | 11 |
12 | Minimalist VM for Meteor – inspired by `manuel:viewmodel` and `nikhizzle:session-bind`. 13 | 14 | - Simple, reactive API 15 | - Easily extensible 16 | - Non-intrusive 17 | - Highly declarative 18 | - Terse syntax 19 | 20 | (6.0 kB minified and gzipped) 21 | 22 | ### Install 23 | 24 | `meteor add dalgard:viewmodel` 25 | 26 | If you are migrating from `manuel:viewmodel` or want to try both packages side by side, read the [Migration](#migration) section. 27 | 28 | ### Contents 29 | 30 | *Generated with [DocToc](https://github.com/thlorenz/doctoc).* 31 | 32 | 33 | 34 | 35 | 36 | - [Intro](#intro) 37 | - [Quickstart](#quickstart) 38 | - [Usage](#usage) 39 | - [Jade](#jade) 40 | - [API](#api) 41 | - [{{bind}}](#bind) 42 | - [Bind expressions](#bind-expressions) 43 | - [Viewmodel instances](#viewmodel-instances) 44 | - [Templates](#templates) 45 | - [Properties](#properties) 46 | - [Serialization](#serialization) 47 | - [Traversal](#traversal) 48 | - [Static methods](#static-methods) 49 | - [Transclude](#transclude) 50 | - [Persistence](#persistence) 51 | - [Shared state](#shared-state) 52 | - [addBinding](#addbinding) 53 | - [Built-in bindings](#built-in-bindings) 54 | - [Value ([throttle][, leading])](#value-throttle-leading) 55 | - [Checked](#checked) 56 | - [Radio](#radio) 57 | - [Pikaday ([position])](#pikaday-position) 58 | - [Click](#click) 59 | - [Toggle](#toggle) 60 | - [Submit ([send])](#submit-send) 61 | - [Disabled](#disabled) 62 | - [Focused](#focused) 63 | - [Hovered ([delay[Enter]][, delayLeave])](#hovered-delayenter-delayleave) 64 | - [Enter key](#enter-key) 65 | - [Key (keyCode)](#key-keycode) 66 | - [Class](#class) 67 | - [Files](#files) 68 | - [Migration](#migration) 69 | - [History](#history) 70 | 71 | 72 | 73 | 74 | ## Intro 75 | 76 | A modern webapp typically consists of various components, tied together in a view hierarchy. Some of these components have state, some of them expose a value, and some have actions. 77 | 78 | Examples: 79 | 80 | - A filter panel, which might be folded or unfolded and expose a regex depending on an input field. 81 | - A pagination widget, which might have a currently selected page, expose an index range, and have the ability to change page. 82 | - A login form with username, password, and a submit button, which logs in the user. 83 | 84 | Traditionally, the state of a component is held implicitly in the DOM. An element that is hidden simply has `display: none`. Values are retrieved manually upon use, and events are registered manually – in both cases through an element's class or id. 85 | 86 | With the viewmodel pattern, the state, value, and methods of a component is stored in an object – the component's **viewmodel** – which can be persisted across sessions or routes and read or written to by other components. The state and values in the viewmodel are automatically synchronized between this object and the DOM through something called **bindings**. 87 | 88 | This principle reduces the amount of code in a project, because bindings are declarative, and at the same time makes components more loosely coupled, because other parts of the view hierarchy don't have to know about a component's actual markup. 89 | 90 | The goal of `dalgard:viewmodel` is to cut down to the core of this pattern and provide the leanest possible API for gaining the largest possible advantage from it. 91 | 92 | 93 | ## Quickstart 94 | 95 | ```js 96 | // All the code you need to get started 97 | ViewModel.registerHelper("bind"); 98 | ``` 99 | 100 | ```html 101 | 110 | ``` 111 | 112 | **Note:** This example depends on the package `dalgard:get-helper-reactively` for using the `red` helper *before* it is actually declared. 113 | 114 | Check out this example and others in the `/examples` directory and at [dalgard-viewmodel.meteor.com](http://dalgard-viewmodel.meteor.com/). 115 | 116 | 117 | ## Usage 118 | 119 | The example below demonstrates the core features of the package. 120 | 121 | Viewmodel declarations may sometimes be omitted altogether – the `{{bind}}` helper automatically creates what it needs, if registered globally (like in the quickstart example). 122 | 123 | ```html 124 | 129 | 130 | 133 | ``` 134 | 135 | ```js 136 | // Declare a viewmodel on this template (all properties are registered as Blaze helpers) 137 | Template.page.viewmodel({ 138 | // Computed property from child viewmodel 139 | myFieldValue() { 140 | // Get child viewmodel reactively by name 141 | const field = this.child("field"); 142 | 143 | // Get the value of myValue reactively when the field is rendered 144 | return field && field.myValue(); 145 | } 146 | }, {}); // An options object may be passed 147 | 148 | // Instead of a definition object, a factory function may be used. 149 | // Unrelated to the factory, this viewmodel is given a name. 150 | Template.field.viewmodel("field", function (data) { 151 | // Return the new viewmodel definition 152 | return { 153 | // Primitive property 154 | myValue: data && data.startValue || "", 155 | 156 | // Computed property 157 | regex() { 158 | // Get the value of myValue reactively 159 | const value = this.myValue(); 160 | 161 | return new RegExp(value); 162 | }, 163 | 164 | // React to changes in dependencies such as viewmodel properties 165 | // – can be an array of functions 166 | autorun() { 167 | // Log every time the computed regex property changes 168 | console.log("New value of regex:", this.regex()); 169 | } 170 | }; 171 | }); 172 | ``` 173 | 174 | When a viewmodel is created on a template – either implicitly or explicitly – existing Blaze helpers on the template become properties of the viewmodel. The helpers preserve their normal context and arguments when called. 175 | 176 | The viewmodel of a template instance may be accessed inside lifetime hooks, helpers, and events, through the `viewmodel` property on the template instance: 177 | 178 | ```js 179 | Template.example.viewmodel({ 180 | myValue: "Hello world" 181 | }); 182 | 183 | Template.example.onRendered(function () { 184 | console.log(this.viewmodel.myValue()); // "Hello world" 185 | }); 186 | 187 | Template.example.events({ 188 | "click button"(event, template_instance) { 189 | console.log(template_instance.viewmodel.myValue()); // "Hello world" 190 | } 191 | }); 192 | 193 | // Additional helpers shouldn't be needed in practice, since all viewmodel 194 | // properties are also registered as Blaze helpers 195 | Template.example.helpers({ 196 | myHelper() { 197 | return Template.instance().viewmodel.myValue(); // "Hello world" 198 | } 199 | }); 200 | 201 | // If no name is specified for a viewmodel, it is named after its view 202 | Template.other.helpers({ 203 | otherHelper() { 204 | return ViewModel.findOne("Template.example").myValue(); // "Hello world" 205 | } 206 | }); 207 | ``` 208 | 209 | ### Jade 210 | 211 | To bind an element in a Jade template, when using the `mquandalle:jade` package, the slightly convoluted embedded Blaze syntax is used: 212 | 213 | ```jade 214 | input(type='text' $dyn='{{bind "value: value" throttle=500}}') 215 | ``` 216 | 217 | A more elegant syntax can be achieved by using the [`dalgard:jade`](https://github.com/dalgard/meteor-jade) package instead of `mquandalle:jade`. This package is a direct fork of the latter one, which adds a few extensions to the syntax, allowing this syntax for binding elements: 218 | 219 | ```jade 220 | input(type='text' $bind('value: value' throttle=500)) 221 | ``` 222 | 223 | Check out the Jade example in `/examples/jade`. 224 | 225 | ## API 226 | 227 | This is an extract of the full API – take five minutes to explore the ViewModel class with `dir(ViewModel)` and viewmodel instances with `ViewModel.find()` in your dev tools of choice. 228 | 229 | ### {{bind}} 230 | 231 | To begin with, the Blaze bind helper only gets registered on templates with a declared viewmodel. The name of the helper may be changed like this: 232 | 233 | ```js 234 | ViewModel.helperName = "myBind"; 235 | ``` 236 | 237 | However, you may choose to register the helper globally: 238 | 239 | ```js 240 | ViewModel.registerHelper(name); // name is optional 241 | ``` 242 | 243 | The advantage of registering `{{bind}}` globally is that you may use it in any template without first declaring a viewmodel on it. 244 | 245 | The helper then automatically creates a new viewmodel instance (if none existed) and registers any bound properties as Blaze helpers. 246 | 247 | **Note:** The newly created helper may be used anywhere after the bind expression in the template. Using it *before* the call to `{{bind}}` is enabled by adding the package [`dalgard:get-helper-reactively`](https://github.com/dalgard/meteor-get-helper-reactively). 248 | 249 | ##### Bind expressions 250 | 251 | The basic syntax of the bind helper looks like this: 252 | 253 | ```html 254 | {{bind expression ...}} 255 | ``` 256 | 257 | ... where `expression` is a string, formatted as a key/value pair: 258 | 259 | ```js 260 | 'binding: key' 261 | ``` 262 | 263 | You may pass multiple bind expressions to the helper – either as one string, separated by commas, or as multiple positional arguments. 264 | 265 | There are cases, like with the `class` binding, where the key may be omitted or where multiple keys may be given. 266 | 267 | Any space separated values after the colon inside the bind expression are passed as arguments to the binding – for instance, key and delay: 268 | 269 | ```html 270 | 271 | ``` 272 | 273 | ### Viewmodel instances 274 | 275 | ViewModel can be used more or less programmatically, but below are the methods that are recommended for use inside computed properties, autoruns etc. when sticking to the more declarative approach. 276 | 277 | (Optional arguments are written in brackets below) 278 | 279 | ```js 280 | // Reactively get or set the name of the viewmodel 281 | this.name([new_name]); 282 | ``` 283 | 284 | ```js 285 | // Reactively get or set an option on the viewmodel 286 | this.option(name[, new_value]); 287 | ``` 288 | 289 | ##### Templates 290 | 291 | ```js 292 | // Get the current template instance 293 | this.templateInstance(); 294 | ``` 295 | 296 | ```js 297 | // Reactively get the data context of the current template instance 298 | this.getData(); 299 | ``` 300 | 301 | ##### Properties 302 | 303 | Primitive viewmodel properties are converted to reactive accessor methods. Call a property name with a new value to reactively *set* the value, and without arguments to reactively *get* the value. 304 | 305 | ```js 306 | // Reactively get or set the property value 307 | this.myProp([new_value]); 308 | ``` 309 | 310 | ```js 311 | // Get or set the property value non-reactively 312 | this.myProp.nonreactive([new_value]); 313 | ``` 314 | 315 | ```js 316 | // Reset the property to its initial value 317 | this.myProp.reset(); 318 | ``` 319 | 320 | If the viewmodel shares its state (`share` flag is set), setting a new value – reactively or non-reactively – automatically sets the new value on all other instances of the same viewmodel (as a rule, you should never set a new value non-reactively). 321 | 322 | All viewmodel methods have an internal value that can be accessed reactively through a pair of `set` and `get` methods on the methods themselves: 323 | 324 | ```js 325 | Template.example.viewmodel({ 326 | counter(addend) { 327 | if (_.isNumber(addend)) 328 | // this.counter is a reference to the same method that we are in 329 | this.counter.set(addend + (this.counter.nonreactive() || 0)); 330 | else 331 | return this.counter.get() || 0; 332 | } 333 | }); 334 | ``` 335 | 336 | ##### Serialization 337 | 338 | ```js 339 | // Get a snapshot of the viewmodel, ready for serialization 340 | this.serialize(); 341 | ``` 342 | 343 | ```js 344 | // Apply a snapshot to the viewmodel 345 | this.deserialize(object); 346 | ``` 347 | 348 | ```js 349 | // Reset all properties to their initial values 350 | this.reset(); 351 | ``` 352 | 353 | ##### Traversal 354 | 355 | The recommended pattern with this package is to retrieve values from child viewmodels, rather than having the child viewmodels write values to their parent, as well as to use Spacebars keyword arguments to pass values down to children. 356 | 357 | Consequently, the `parent`, `ancestor`, and `ancestors` methods should generally be avoided. 358 | 359 | Each method takes a number of `test`s as optional arguments. A test can be either a **predicate function**, a **DOM element**, a **viewmodel**, a **regex**, or a **string**. The latter two are compared with the name of the viewmodel. 360 | 361 | If no name is specified for a viewmodel, it is named after its view (e.g. `"Template.example"`). 362 | 363 | ```js 364 | // Reactively get a filtered array of child viewmodels 365 | this.children([...tests]); 366 | ``` 367 | 368 | ```js 369 | // Reactively get the first child or the child at index in a filtered array of 370 | // child viewmodels 371 | this.child([...tests][, index=0]); 372 | ``` 373 | 374 | ```js 375 | // Reactively get a filtered array of descendant viewmodels, optionally within 376 | // a depth 377 | this.descendants([...tests][, depth]); 378 | ``` 379 | 380 | ```js 381 | // Reactively get the first descendant or the descendant at index in a filtered 382 | // array of descendant viewmodels, optionally within a depth 383 | this.descendant([...tests][, index=0][, depth]); 384 | ``` 385 | 386 | ```js 387 | // Reactively get the parent viewmodel filtered by tests 388 | this.parent([...tests]); 389 | ``` 390 | 391 | ```js 392 | // Reactively get a filtered array of ancestor viewmodels, optionally within 393 | // a depth 394 | this.ancestors([...tests][, depth]); 395 | ``` 396 | 397 | ```js 398 | // Reactively get the first ancestor or the ancestor at index in a filtered 399 | // array of ancestor viewmodels, optionally within a depth 400 | this.ancestor([...tests][, index=0][, depth]); 401 | ``` 402 | 403 | ### Static methods 404 | 405 | The methods below are mainly for inspection while developing, but may also be used as a convenient way of retrieving a far off component in a complex view hierarchy (see previous section). 406 | 407 | ```js 408 | // Reactively get a filtered array of all the current viewmodels on the page 409 | ViewModel.find([...tests]); 410 | ``` 411 | 412 | ```js 413 | // Reactively get the first item or the item at index in a filtered array of 414 | // all the current viewmodels on the page 415 | ViewModel.findOne([...tests][, index=0]); 416 | ``` 417 | 418 | The bound element-binding pairs – referred to as *nexuses*, with a novel term – that currently resides in a view, may be inspected through the view's `nexuses` property (the name of this property can be changed through `ViewModel.nexusesKey`). 419 | 420 | To get a list of all the current nexuses on the page, use the static `find` and `findOne` methods on the `ViewModel.Nexus` class, which are equivalent to the methods on `ViewModel`. They are useful for finding and updating a property associated with a specific binding on an element (among other things): 421 | 422 | ```js 423 | // Update viewmodel property 424 | ViewModel.Nexus.findOne(dom_element, "value").prop("Hello new world"); 425 | ``` 426 | 427 | Lastly, a utility method for finding the closest template instance from a view or DOM element (traversing upwards in the view hierarchy) is available as a static method on `ViewModel`: 428 | 429 | ```js 430 | // Get the closest template instance 431 | ViewModel.templateInstance(view || dom_elem); 432 | ``` 433 | 434 | ### Transclude 435 | 436 | To take a viewmodel out of the viewmodel hierarchy, set the `transclude` option to `true`: 437 | 438 | ```js 439 | Template.example.viewmodel({ 440 | prop: "" 441 | }, { transclude: true }); 442 | ``` 443 | 444 | A viewmodel that is transcluded becomes "invisible" to its parent and children. Instead, the children of the transcluded viewmodel become children of the transcluded viewmodel's parent. 445 | 446 | This is useful when placing some component in a template, which has its own internal state, but which isn't otherwise relevant to the rest of the view hierarchy. 447 | 448 | ### Persistence 449 | 450 | Values in viewmodel instances are automatically persisted across hot code pushes. 451 | 452 | To persist the state of a viewmodel across re-renderings, including changing to another route and going back to a previous one, set the `persist` option to `true`: 453 | 454 | ```js 455 | Template.example.viewmodel({ 456 | // This property will be restored on re-render 457 | prop: "" 458 | }, { persist: true }); 459 | ``` 460 | 461 | In order to determine whether an instance is the same as a previous one, ViewModel looks at 1) the position of the viewmodel in the view hierarchy, 2) the index of the viewmodel in relation to sibling viewmodels, and 3) the browser location. 462 | 463 | If all these things match, the state of the viewmodel instance will be restored. 464 | 465 | **Important:** Any viewmodel that is a descendant of a viewmodel that has the `persist` flag set, is persisted in the same way. 466 | 467 | ### Shared state 468 | 469 | Multiple instances of the same viewmodel can share their state – set the `share` option to `true`: 470 | 471 | ```js 472 | Template.example.viewmodel({ 473 | prop: "" 474 | }, { share: true }); 475 | ``` 476 | 477 | If a component is repeated on a page, the `share` flag makes sure that the state of the two instances is kept in sync automatically. This is useful for something like a pagination widget that is duplicated at the top and bottom of a page. 478 | 479 | 480 | ## addBinding 481 | 482 | This is the full declaration of the `click` binding: 483 | 484 | ```js 485 | ViewModel.addBinding("click", { 486 | on: "click" 487 | }); 488 | ``` 489 | 490 | The job of a binding is to synchronize data between the DOM and the viewmodel. Bindings are added through definition objects: 491 | 492 | ```js 493 | // All properties are optional 494 | ViewModel.addBinding("name", { 495 | /* Definition */ 496 | 497 | // Run once when the element is rendered, right before the first call to set. 498 | // Used to initalize things like jQuery plugins. When creating a binding that 499 | // only contains init and/or dispose, set the "detached" option to true 500 | init(elem, init_value) { 501 | // For example 502 | this.instance = $(elem).plugin(this.hash.options); 503 | }, 504 | 505 | // Apply the original value and new values to the DOM 506 | set(elem, new_value) { 507 | // For example 508 | elem.value = new_value || ""; 509 | }, 510 | 511 | // Space separated list or array of event types 512 | on: "keyup input change", 513 | 514 | // Get the changed value from the DOM triggered by events 515 | get(event, elem, prop) { 516 | // For example 517 | return elem.value; 518 | }, 519 | 520 | // Run once when the view that contains the element is destroyed. 521 | // Used to tear down things like jQuery plugins. 522 | dispose(prop) { 523 | // For example 524 | this.instance.destroy(); 525 | prop.reset(); 526 | } 527 | }, { 528 | /* Options */ 529 | 530 | // Inherit the properties of one or several other bindings (name or array of names) 531 | extends: "superName", 532 | 533 | // Omitted in most cases. If true, the binding doesn't use a viewmodel, and 534 | // consequently, viewmodels or properties will not be created automatically 535 | detached: false 536 | }); 537 | ``` 538 | 539 | The parameters used for `init`, `set`, `get`, and `dispose` are: 540 | 541 | - `event`  –  the original event object. 542 | - `elem`  –  the DOM element that the `{{bind}}` helper was called on. 543 | - `init_value`/`new_value`  –  the new value that was passed to the property. 544 | - `prop`  –  the property on the viewmodel, if available. 545 | 546 | Each function is called with an object as context (`this`) that is private to each specific bound element-binding pair. This object can be used to store plugin instances or other variables for the lifetime of the element. 547 | 548 | The context object comes with some useful properties: 549 | 550 | - `viewmodel`  –  A reference to the viewmodel, if available. 551 | - `key`  –  The property key, if available. 552 | - `view`  –  The view that the element was bound in. 553 | - `templateInstance`  –  The nearest template instance. 554 | - `data`  –  the current data context of the template instance. 555 | - `args`  –  an array (possibly empty) containing any space separated values after the colon in the bind expression, including the key. 556 | - `hash`  –  the keyword arguments that the `{{bind}}` helper was called with. 557 | 558 | The returned value from the `get` function is written directly to the bound property. However, if the function doesn't return anything (i.e. returns `undefined`), the bound property is not called at all. This is practical in case you only want to call the bound property in *some* cases. 559 | 560 | An example: 561 | 562 | ```js 563 | ViewModel.addBinding("enterKey", { 564 | on: "keyup", 565 | 566 | // This function doesn't return anything but calls the property explicitly instead 567 | get(event, elem, prop) { 568 | if (event.keyCode === 13) 569 | // Call prop with these three arguments as standard 570 | prop(event, this.args, this.hash); 571 | } 572 | }); 573 | ``` 574 | 575 | In the case where you want to call the bound property, but not do so with a new value, simply omit `get` altogether – like with the `click` binding further above. The bound property will then be called with the arguments `event`, `args`, and `hash`. 576 | 577 | If your binding has both `get` and `set`, and you don't want to trigger `set` as a result of calling `prop()` inside `get`, call `this.preventSet()` before calling the property. 578 | 579 | A definition object may also be returned from a factory function, which is called with the same context object as the definition functions: 580 | 581 | ```js 582 | ViewModel.addBinding(name, function () { 583 | // Return definition object 584 | return {}; 585 | }); 586 | ``` 587 | 588 | 589 | ## Built-in bindings 590 | 591 | Several bindings are included with the package, but you are highly encouraged to add more specialized bindings to your project in order to improve the readability of your code. 592 | 593 | Arguments in the built-in bindings can be passed either as part of the bind expression or as keyword arguments to the helper: 594 | 595 | ```html 596 | {{bind 'value: value 100 true'}} 597 | 598 | {{bind 'value: value' throttle=100 leading=true}} 599 | ``` 600 | 601 | (Boilerplate code is omitted below and possible arguments are shown in parentheses) 602 | 603 | #### Value ([throttle][, leading]) 604 | 605 | The property reflects the value of a text input, textarea, or select. An initial value can be set in the viewmodel. The `throttle` argument is a number (in ms) by which the update is [delayed](https://lodash.com/docs#throttle) as long as the user is typing. If the `leading` argument is `true`, the value is updated once before the delay. 606 | 607 | ```html 608 | 609 | ``` 610 | 611 | ```js 612 | { text: "" } 613 | ``` 614 | 615 | #### Checked 616 | 617 | The property reflects the state of the checkbox. The inital state of the checkbox can be set in the viewmodel. 618 | 619 | ```html 620 | 621 | ``` 622 | 623 | ```js 624 | { checked: false } 625 | ``` 626 | 627 | #### Radio 628 | 629 | The property reflects the value of the radio button. The inital state of the group of radiobuttons (i.e. with the same `name` attribute) can be set in the viewmodel. 630 | 631 | ```html 632 | 633 | 634 | ``` 635 | 636 | ```js 637 | { value: "first" } 638 | ``` 639 | 640 | #### Pikaday ([position]) 641 | 642 | This datepicker binding is implemented with [Pikaday](https://github.com/richsilv/Pikaday/), so a package like `richsilv:pikaday` **must** be added for the binding it to work. 643 | 644 | The property reflects the currently selected `Date`. An initial date can be set in the viewmodel. The `position` argument determines where to render the datepicker (default: `bottom left`). 645 | 646 | ```html 647 | 648 | ``` 649 | 650 | ```js 651 | { date: new Date } // Or simply null 652 | ``` 653 | 654 | An additional keyword argument `monthFirst` can be set to `true` if the month should come first in the date format. 655 | 656 | #### Click 657 | 658 | A method on the viewmodel is called when the element is clicked. 659 | 660 | ```html 661 | 662 | ``` 663 | 664 | ```js 665 | { click(event, args, hash) { ... } } 666 | ``` 667 | 668 | #### Toggle 669 | 670 | The property is negated on each `click` of the button. 671 | 672 | ```html 673 | 674 | ``` 675 | 676 | ```js 677 | { toggled: false } 678 | ``` 679 | 680 | #### Submit ([send]) 681 | 682 | A method on the viewmodel is run when the form is submitted. If `true` is passed as the `send` argument, the event does **not** get `event.preventDefault()`, meaning that the form will be sent. 683 | 684 | ```html 685 |
686 | ``` 687 | 688 | ```js 689 | { submit(event, args, hash) { ... } } 690 | ``` 691 | 692 | #### Disabled 693 | 694 | The disabled state of the element reflects a boolean property on the viewmodel. The inital state can be set in the viewmodel. 695 | 696 | ```html 697 | 698 | ``` 699 | 700 | ```js 701 | { disabled: false } 702 | ``` 703 | 704 | #### Focused 705 | 706 | The property reflects whether the element is in focus. An element can be given focus by setting the initial state to `true`. 707 | 708 | ```html 709 | 710 | ``` 711 | 712 | ```js 713 | { focused: true } 714 | ``` 715 | 716 | #### Hovered ([delay[Enter]][, delayLeave]) 717 | 718 | The property reflects whether the mouse hovers over the element. 719 | 720 | If an integer is passed as the only argument in the expression (keyword argument `delay`), this determines a delay on changing the property on both entering and leaving the element. To give each event a different delay, either pass two integers or use the keyword arguments `delayEnter` and `delayLeave`. 721 | 722 | Delaying the leave state is especially useful for not immediately closing a hover menu if the cursor is moved outside the element briefly. 723 | 724 | ```html 725 | 726 | ``` 727 | 728 | ```js 729 | { hovered: false } 730 | ``` 731 | 732 | #### Enter key 733 | 734 | A method on the viewmodel is run when the enter key is pressed on the element. 735 | 736 | ```html 737 | 738 | ``` 739 | 740 | ```js 741 | { press(event, args, hash) { ... } } 742 | ``` 743 | 744 | #### Key (keyCode) 745 | 746 | A method on the viewmodel is run when the specific key, passed as an argument, is pressed on the element. In the example, it's the shift key. 747 | 748 | ```html 749 | 750 | ``` 751 | 752 | ```js 753 | { press(event, args, hash) { ... } } 754 | ``` 755 | 756 | #### Class 757 | 758 | This bind expression takes a number of keys, where each key refers to a keyword argument. The name of the keyword argument represents a class name and the truthyness of its value determines whether the class is toggled. 759 | 760 | If no keys are indicated in the bind expression (the colon should be omitted, too), all keyword arguments are used. 761 | 762 | ```html 763 |

764 | ``` 765 | 766 | ```js 767 | { isRed: true } 768 | ``` 769 | 770 | #### Files 771 | 772 | The property is an array of the currently selected file object(s) from the file picker. The boolean attribute `multiple` is optional on the input element. 773 | 774 | ```html 775 | 776 | ``` 777 | 778 | ```js 779 | { files: [] } 780 | ``` 781 | 782 | 783 | ## Migration 784 | 785 | If you are migrating gradually from `manuel:viewmodel` or any other package that exports a `ViewModel` and/or overwrites `Template.prototype.viewmodel`, there are a couple of steps you need to take to remedy conflicts: 786 | 787 | 1. Make sure `dalgard:viewmodel` is included *before* any package that fits the description above. 788 | 2. Reassign the needed functionality to whichever names you like, directly from the package. 789 | 790 | Like this: 791 | 792 | ```js 793 | // E.g. /client/lib/dalgard-viewmodel.js 794 | 795 | DalgardViewModel = Package["dalgard:viewmodel"].ViewModel; 796 | Template.prototype.dalgardViewmodel = DalgardViewModel.viewmodelHook; 797 | 798 | // Name of viewmodel reference on template instances 799 | DalgardViewModel.viewmodelKey = "dalgardViewmodel"; 800 | ``` 801 | 802 | You can now use the two packages side by side, even on the same template, until everything is migrated. 803 | 804 | Pro tip: Choose unique names that can be search-and-replaced globally, when the time comes. 805 | 806 | 807 | ## History 808 | 809 | - 1.0.2  –  Fixed regression with `this.preventSet()` (inputs with `value` binding constantly moving cursor to end). 810 | - 1.0.1  –  Fixed passing event types as an array in binding definition. 811 | - 1.0.0  –  jQuery was removed as a dependency; consequently, elements and events are no longer wrapped in jQuery. ViewModel class API changes: `ViewModel.nexuses()` → `ViewModel.Nexus.find()`. The `find()` and `findOne()` methods on `ViewModel` and `ViewModel.Nexus`, together with all the traversal methods, now take a number of tests as arguments (besides the usual index and depth arguments, in some cases); a test can be either a predicate function, a DOM element, a viewmodel, a regex, or a string. Added `ViewModel.templateInstance(view || dom_element)` utility method. Viewmodel instance API changes: `vm.isPersisted()`, `vm.restore(hash_id)`, `vm.addChild(vm)`, and `vm.removeChild(vm)` methods now public. Nexus instance API changes: `nexus.getProp()` → `nexus.prop`, `nexus.elem` → `nexus.elem()`, `nexus.setPrevented` → `nexus.isSetPrevented()`, `nexus.inBody()` → `ViewModel.Nexus.isInBody(nexus.elem())`. 812 | - 0.9.4  –  Added `key` to binding context and improved `hovered` built-in binding. 813 | - 0.9.3  –  Bug fixes: Corner case with rebinding on dynamic attribute change; don't put viewmodel on built-in templates. 814 | - 0.9.2  –  API change: `classes` binding is renamed to `class` and changed to take (optionally indicated) keyword arguments as class names and their values as the class' presence. Creating a viewmodel adds existing Blaze template helpers as properties. 815 | - 0.9.1  –  API change: `uniqueId` renamed to `uid`, `bindings` renamed to `nexuses`. Global list of binding nexuses can be inspected through `ViewModel.nexuses([name])`. Fixed bug: Using a predicate with traversal methods was temporarily broken. Updated Jade example to `dalgard:jade@0.5.0`. 816 | - 0.9.0  –  Major refactoring. API change: Signatures and context of the functions in bindings is changed, and `extends` and `detached` are moved to an options object. Viewmodel methods have access to an internal reactive variable. Bound element-binding pairs (termed "nexuses") in a view can be inspected through the view's `bindings` property. Pikaday supports keyboard arrows up/down. 817 | - 0.8.3  –  Don't trigger `set` on normal updates in bindings, i.e. with a return value from `get`. 818 | - 0.8.2  –  If no name is specified for a viewmodel, it is named after its view 819 | - 0.8.1  –  Bug fix: Using implicit helper before `{{bind}}` didn't work when the same template was used multiple times. API change: Changed `referenceName` to `referenceKey`. 820 | - 0.8.0  –  Experimental feature: Helpers in templates without an explicitly declared viewmodel may now be used anywhere in the template, including before the actual call to `{{bind}}` that creates the helper. Added static serialization methods. Improved arguments for built-in bindings. 821 | - 0.7.1  –  Added `nonreactive` get-set method to primitive viewmodel props. Possible to programmatically bind an element outside of the viewmodel's template. `children` method now always returns a copy. 822 | - 0.7.0  –  API change: Removed lifetime hooks and Blaze events from viewmodel definition. Added `reset` method and various optimizations. Added `extends` and `dispose` to binding definition. Added `pikaday` binding. Fixed ongoing bug: Values are now properly restored with bindings nested in block helpers 823 | - 0.6.2  –  Serious bug fix: Events are no longer registered more than once. Bug fix: Corrected signature when calling viewmodel methods (should only get `event`, `args`, and `hash`). API change: Removed `key` as a parameter for binding factories and `bind` method. `onReady` and `ViewModel.uniqueId` now part of public API. 824 | - 0.6.1  –  Bug fix: Bind helpers were sometimes being rerun. `hashId` now part of public API. 825 | - 0.6.0  –  Added `init` function to binding definition. `ViewModel.bindHelper` now part of public API. 826 | - 0.5.9  –  Migration made possible by storing the `viewmodel` hook as a property on `ViewModel`. Multiple comma separated bind expressions in one string (for future Jade extension). 827 | - 0.5.8  –  API change: Passing viewmodel property to `get` function instead of key. 828 | - 0.5.7  –  API change: `args` argument now holds the key as the first value. 829 | - 0.5.0  –  Optionally share state between two instances of the same viewmodel. Only use Object.defineProperties when present (to support 2 | 3 | 4 | 5 | All examples 6 | 7 | -------------------------------------------------------------------------------- /examples/all/client/lib/blaze-layout.js: -------------------------------------------------------------------------------- 1 | BlazeLayout.setRoot("body"); 2 | -------------------------------------------------------------------------------- /examples/all/client/lib/register-bind.js: -------------------------------------------------------------------------------- 1 | ViewModel.registerHelper("bind"); 2 | -------------------------------------------------------------------------------- /examples/all/client/views/full: -------------------------------------------------------------------------------- 1 | ../../../full/client/views -------------------------------------------------------------------------------- /examples/all/client/views/jade: -------------------------------------------------------------------------------- 1 | ../../../jade/client/views -------------------------------------------------------------------------------- /examples/all/client/views/layout.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /examples/all/client/views/minimalist: -------------------------------------------------------------------------------- 1 | ../../../minimalist/client/views -------------------------------------------------------------------------------- /examples/all/client/views/pikaday: -------------------------------------------------------------------------------- 1 | ../../../pikaday/client/views -------------------------------------------------------------------------------- /examples/all/client/views/quickstart: -------------------------------------------------------------------------------- 1 | ../../../quickstart/client/views -------------------------------------------------------------------------------- /examples/all/client/views/usage: -------------------------------------------------------------------------------- 1 | ../../../usage/client/views -------------------------------------------------------------------------------- /examples/all/lib/router.js: -------------------------------------------------------------------------------- 1 | FlowRouter.route("/:route?", { 2 | action(params) { 3 | BlazeLayout.render("layout", { params }); 4 | }, 5 | }); 6 | -------------------------------------------------------------------------------- /examples/all/packages: -------------------------------------------------------------------------------- 1 | ../../packages -------------------------------------------------------------------------------- /examples/full/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | -------------------------------------------------------------------------------- /examples/full/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/full/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 21o36snkarto1wxddzm 8 | -------------------------------------------------------------------------------- /examples/full/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | standard-minifiers 8 | ecmascript 9 | meteor-base 10 | mobile-experience 11 | blaze-html-templates 12 | reload 13 | spacebars 14 | 15 | dalgard:viewmodel 16 | richsilv:pikaday 17 | -------------------------------------------------------------------------------- /examples/full/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/full/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.2.1 2 | -------------------------------------------------------------------------------- /examples/full/.meteor/versions: -------------------------------------------------------------------------------- 1 | autoupdate@1.2.4 2 | babel-compiler@5.8.24_1 3 | babel-runtime@0.1.4 4 | base64@1.0.4 5 | binary-heap@1.0.4 6 | blaze@2.1.3 7 | blaze-html-templates@1.0.1 8 | blaze-tools@1.0.4 9 | boilerplate-generator@1.0.4 10 | caching-compiler@1.0.0 11 | caching-html-compiler@1.0.2 12 | callback-hook@1.0.4 13 | check@1.1.0 14 | dalgard:reactive-map@0.1.0_3 15 | dalgard:viewmodel@1.0.2 16 | ddp@1.2.2 17 | ddp-client@1.2.1 18 | ddp-common@1.2.2 19 | ddp-server@1.2.2 20 | deps@1.0.9 21 | diff-sequence@1.0.1 22 | ecmascript@0.1.6 23 | ecmascript-runtime@0.2.6 24 | ejson@1.0.7 25 | fastclick@1.0.7 26 | geojson-utils@1.0.4 27 | hot-code-push@1.0.0 28 | html-tools@1.0.5 29 | htmljs@1.0.5 30 | http@1.1.1 31 | id-map@1.0.4 32 | jquery@1.11.4 33 | launch-screen@1.0.4 34 | livedata@1.0.15 35 | logging@1.0.8 36 | meteor@1.1.10 37 | meteor-base@1.0.1 38 | minifiers@1.1.7 39 | minimongo@1.0.10 40 | mobile-experience@1.0.1 41 | mobile-status-bar@1.0.6 42 | momentjs:moment@2.9.0 43 | mongo@1.1.3 44 | mongo-id@1.0.1 45 | npm-mongo@1.4.39_1 46 | observe-sequence@1.0.7 47 | ordered-dict@1.0.4 48 | promise@0.5.1 49 | random@1.0.5 50 | reactive-dict@1.1.3 51 | reactive-var@1.0.6 52 | reload@1.1.4 53 | retry@1.0.4 54 | richsilv:pikaday@1.0.1 55 | routepolicy@1.0.6 56 | sha@1.0.4 57 | spacebars@1.0.7 58 | spacebars-compiler@1.0.7 59 | standard-minifiers@1.0.2 60 | stevezhu:lodash@3.10.1 61 | templating@1.1.5 62 | templating-tools@1.0.0 63 | tracker@1.0.9 64 | ui@1.0.8 65 | underscore@1.0.4 66 | url@1.0.5 67 | webapp@1.2.3 68 | webapp-hashing@1.0.5 69 | -------------------------------------------------------------------------------- /examples/full/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | {{> full}} 3 | 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/checked.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/checked.js: -------------------------------------------------------------------------------- 1 | Template.fullChecked.viewmodel({ 2 | checked: false, 3 | value: "yo", 4 | }); 5 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/class.css: -------------------------------------------------------------------------------- 1 | .red { 2 | color: red; 3 | } 4 | 5 | .bold { 6 | font-weight: bold; 7 | } 8 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/class.html: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/class.js: -------------------------------------------------------------------------------- 1 | Template.fullClass.viewmodel({ 2 | red: false, 3 | bold: false, 4 | }); 5 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/click.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/click.js: -------------------------------------------------------------------------------- 1 | Template.fullClick.viewmodel({ 2 | clicked: false, 3 | 4 | click() { 5 | this.clicked(true); 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/date.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/date.js: -------------------------------------------------------------------------------- 1 | Template.fullDate.viewmodel({ 2 | date: null, 3 | }); 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/disabled.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/disabled.js: -------------------------------------------------------------------------------- 1 | Template.fullDisabled.viewmodel({ 2 | disabled: false, 3 | value: "yo", 4 | }); 5 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/enter-key.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/enter-key.js: -------------------------------------------------------------------------------- 1 | Template.fullEnterKey.viewmodel({ 2 | pressed: false, 3 | 4 | press() { 5 | this.pressed(true); 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/files.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/files.js: -------------------------------------------------------------------------------- 1 | Template.fullFiles.viewmodel({ 2 | files: [], 3 | }); 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/focused.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/focused.js: -------------------------------------------------------------------------------- 1 | Template.fullFocused.viewmodel({ 2 | focused: true, 3 | }); 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/hovered.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/hovered.js: -------------------------------------------------------------------------------- 1 | Template.fullHovered.viewmodel({ 2 | hovered: false, 3 | }); 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/key.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/key.js: -------------------------------------------------------------------------------- 1 | Template.fullKey.viewmodel({ 2 | pressed: false, 3 | 4 | key() { 5 | this.pressed(true); 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/radio.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/radio.js: -------------------------------------------------------------------------------- 1 | Template.fullRadio.viewmodel({ 2 | value: "first", 3 | }); 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/select.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/select.js: -------------------------------------------------------------------------------- 1 | Template.fullSelect.viewmodel({ 2 | value: "", 3 | }); 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/start-value.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/start-value.js: -------------------------------------------------------------------------------- 1 | Template.fullStartValue.viewmodel(function (data) { 2 | return { 3 | value: data && data.startValue || "", 4 | }; 5 | }); 6 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/textarea.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/textarea.js: -------------------------------------------------------------------------------- 1 | Template.fullTextarea.viewmodel({ 2 | value: "", 3 | }); 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/throttled.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/throttled.js: -------------------------------------------------------------------------------- 1 | Template.fullThrottled.viewmodel({ 2 | value: "", 3 | }); 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/toggle.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/toggle.js: -------------------------------------------------------------------------------- 1 | Template.fullToggle.viewmodel({ 2 | toggled: false, 3 | }); 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/value.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/full/client/views/bindings/value.js: -------------------------------------------------------------------------------- 1 | Template.fullValue.viewmodel("value", { 2 | value: "", 3 | }, { 4 | // Share values between all instances of this viewmodel 5 | share: true, 6 | }); 7 | -------------------------------------------------------------------------------- /examples/full/client/views/full.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /examples/full/client/views/full.js: -------------------------------------------------------------------------------- 1 | Template.full.viewmodel({ 2 | destroy: false, 3 | 4 | childValue() { 5 | // Get child viewmodel reactively 6 | const child = this.child("value"); 7 | 8 | // Child may not be ready when this value is used 9 | if (child) 10 | return child.value(); 11 | }, 12 | 13 | autorun() { 14 | const child = this.child("value"); 15 | 16 | if (child) 17 | console.log("page autorun", child.value()); 18 | }, 19 | }, { 20 | // Persist this viewmodel and descendant viewmodels across re-rendering 21 | persist: true, 22 | }); 23 | -------------------------------------------------------------------------------- /examples/full/packages: -------------------------------------------------------------------------------- 1 | ../../packages -------------------------------------------------------------------------------- /examples/jade/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | -------------------------------------------------------------------------------- /examples/jade/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/jade/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 21o36snkarto1wxddzm 8 | -------------------------------------------------------------------------------- /examples/jade/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | standard-minifiers 8 | ecmascript 9 | meteor-base 10 | mobile-experience 11 | blaze-html-templates 12 | reload 13 | spacebars 14 | jquery 15 | 16 | dalgard:get-helper-reactively 17 | dalgard:jade 18 | dalgard:viewmodel 19 | -------------------------------------------------------------------------------- /examples/jade/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/jade/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.2.1 2 | -------------------------------------------------------------------------------- /examples/jade/.meteor/versions: -------------------------------------------------------------------------------- 1 | autoupdate@1.2.4 2 | babel-compiler@5.8.24_1 3 | babel-runtime@0.1.4 4 | base64@1.0.4 5 | binary-heap@1.0.4 6 | blaze@2.1.3 7 | blaze-html-templates@1.0.1 8 | blaze-tools@1.0.4 9 | boilerplate-generator@1.0.4 10 | caching-compiler@1.0.0 11 | caching-html-compiler@1.0.2 12 | callback-hook@1.0.4 13 | check@1.1.0 14 | dalgard:get-helper-reactively@0.1.0 15 | dalgard:jade@0.5.4_1 16 | dalgard:jade-compiler@0.5.4_1 17 | dalgard:reactive-map@0.1.0_3 18 | dalgard:viewmodel@1.0.2 19 | ddp@1.2.2 20 | ddp-client@1.2.1 21 | ddp-common@1.2.2 22 | ddp-server@1.2.2 23 | deps@1.0.9 24 | diff-sequence@1.0.1 25 | ecmascript@0.1.6 26 | ecmascript-runtime@0.2.6 27 | ejson@1.0.7 28 | fastclick@1.0.7 29 | geojson-utils@1.0.4 30 | hot-code-push@1.0.0 31 | html-tools@1.0.5 32 | htmljs@1.0.5 33 | http@1.1.1 34 | id-map@1.0.4 35 | jquery@1.11.4 36 | launch-screen@1.0.4 37 | livedata@1.0.15 38 | logging@1.0.8 39 | meteor@1.1.10 40 | meteor-base@1.0.1 41 | minifiers@1.1.7 42 | minimongo@1.0.10 43 | mobile-experience@1.0.1 44 | mobile-status-bar@1.0.6 45 | mongo@1.1.3 46 | mongo-id@1.0.1 47 | npm-mongo@1.4.39_1 48 | observe-sequence@1.0.7 49 | ordered-dict@1.0.4 50 | promise@0.5.1 51 | random@1.0.5 52 | reactive-dict@1.1.3 53 | reactive-var@1.0.6 54 | reload@1.1.4 55 | retry@1.0.4 56 | routepolicy@1.0.6 57 | sha@1.0.4 58 | spacebars@1.0.7 59 | spacebars-compiler@1.0.7 60 | standard-minifiers@1.0.2 61 | stevezhu:lodash@3.10.1 62 | templating@1.1.5 63 | templating-tools@1.0.0 64 | tracker@1.0.9 65 | ui@1.0.8 66 | underscore@1.0.4 67 | url@1.0.5 68 | webapp@1.2.3 69 | webapp-hashing@1.0.5 70 | -------------------------------------------------------------------------------- /examples/jade/client/index.jade: -------------------------------------------------------------------------------- 1 | body 2 | +jade 3 | -------------------------------------------------------------------------------- /examples/jade/client/lib/register-bind.js: -------------------------------------------------------------------------------- 1 | ViewModel.registerHelper("bind"); 2 | -------------------------------------------------------------------------------- /examples/jade/client/views/bindings/jadeChecked.tpl.jade: -------------------------------------------------------------------------------- 1 | label 2 | input(type='checkbox' $bind('checked: checked')) 3 | | checked 4 | 5 | if checked 6 | input(type='text' placeholder='inside if' $bind('value: value')) 7 | else 8 | | #{value} 9 | -------------------------------------------------------------------------------- /examples/jade/client/views/bindings/jadeDisabled.tpl.jade: -------------------------------------------------------------------------------- 1 | label 2 | input(type='checkbox' $bind('checked: disabled')) 3 | | disabled 4 | 5 | input(type='text' $bind('value: value' 'disabled: disabled')) 6 | 7 | | #{value} 8 | -------------------------------------------------------------------------------- /examples/jade/client/views/bindings/jadeFiles.tpl.jade: -------------------------------------------------------------------------------- 1 | input(type='file' multiple $bind('files: files')) 2 | 3 | | count: #{files.length} 4 | -------------------------------------------------------------------------------- /examples/jade/client/views/bindings/jadeFocused.tpl.jade: -------------------------------------------------------------------------------- 1 | input(type='text' placeholder='focused' $bind('focused: focused')) 2 | 3 | | #{focused} 4 | -------------------------------------------------------------------------------- /examples/jade/client/views/bindings/jadeHovered.tpl.jade: -------------------------------------------------------------------------------- 1 | input(type='text' placeholder='hovered' $bind('hovered: hovered')) 2 | 3 | | #{hovered} 4 | -------------------------------------------------------------------------------- /examples/jade/client/views/bindings/jadeToggle.tpl.jade: -------------------------------------------------------------------------------- 1 | button($bind('toggle: toggled')) 2 | | toggle 3 | 4 | | #{toggled} 5 | -------------------------------------------------------------------------------- /examples/jade/client/views/bindings/jadeValue.tpl.jade: -------------------------------------------------------------------------------- 1 | input(type='text' placeholder='value' $bind('value: value')) 2 | 3 | | #{value} 4 | -------------------------------------------------------------------------------- /examples/jade/client/views/jade.tpl.jade: -------------------------------------------------------------------------------- 1 | p 2 | +jadeValue 3 | p 4 | +jadeChecked 5 | p 6 | +jadeToggle 7 | p 8 | +jadeDisabled 9 | p 10 | +jadeFiles 11 | p 12 | +jadeFocused 13 | p 14 | +jadeHovered 15 | -------------------------------------------------------------------------------- /examples/jade/packages: -------------------------------------------------------------------------------- 1 | ../../packages -------------------------------------------------------------------------------- /examples/minimalist/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | -------------------------------------------------------------------------------- /examples/minimalist/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/minimalist/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 21o36snkarto1wxddzm 8 | -------------------------------------------------------------------------------- /examples/minimalist/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | standard-minifiers 8 | ecmascript 9 | meteor-base 10 | mobile-experience 11 | blaze-html-templates 12 | reload 13 | spacebars 14 | 15 | dalgard:get-helper-reactively 16 | dalgard:viewmodel 17 | -------------------------------------------------------------------------------- /examples/minimalist/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/minimalist/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.2.1 2 | -------------------------------------------------------------------------------- /examples/minimalist/.meteor/versions: -------------------------------------------------------------------------------- 1 | autoupdate@1.2.4 2 | babel-compiler@5.8.24_1 3 | babel-runtime@0.1.4 4 | base64@1.0.4 5 | binary-heap@1.0.4 6 | blaze@2.1.3 7 | blaze-html-templates@1.0.1 8 | blaze-tools@1.0.4 9 | boilerplate-generator@1.0.4 10 | caching-compiler@1.0.0 11 | caching-html-compiler@1.0.2 12 | callback-hook@1.0.4 13 | check@1.1.0 14 | dalgard:get-helper-reactively@0.1.0 15 | dalgard:reactive-map@0.1.0_3 16 | dalgard:viewmodel@1.0.2 17 | ddp@1.2.2 18 | ddp-client@1.2.1 19 | ddp-common@1.2.2 20 | ddp-server@1.2.2 21 | deps@1.0.9 22 | diff-sequence@1.0.1 23 | ecmascript@0.1.6 24 | ecmascript-runtime@0.2.6 25 | ejson@1.0.7 26 | fastclick@1.0.7 27 | geojson-utils@1.0.4 28 | hot-code-push@1.0.0 29 | html-tools@1.0.5 30 | htmljs@1.0.5 31 | http@1.1.1 32 | id-map@1.0.4 33 | jquery@1.11.4 34 | launch-screen@1.0.4 35 | livedata@1.0.15 36 | logging@1.0.8 37 | meteor@1.1.10 38 | meteor-base@1.0.1 39 | minifiers@1.1.7 40 | minimongo@1.0.10 41 | mobile-experience@1.0.1 42 | mobile-status-bar@1.0.6 43 | mongo@1.1.3 44 | mongo-id@1.0.1 45 | npm-mongo@1.4.39_1 46 | observe-sequence@1.0.7 47 | ordered-dict@1.0.4 48 | promise@0.5.1 49 | random@1.0.5 50 | reactive-dict@1.1.3 51 | reactive-var@1.0.6 52 | reload@1.1.4 53 | retry@1.0.4 54 | routepolicy@1.0.6 55 | sha@1.0.4 56 | spacebars@1.0.7 57 | spacebars-compiler@1.0.7 58 | standard-minifiers@1.0.2 59 | stevezhu:lodash@3.10.1 60 | templating@1.1.5 61 | templating-tools@1.0.0 62 | tracker@1.0.9 63 | ui@1.0.8 64 | underscore@1.0.4 65 | url@1.0.5 66 | webapp@1.2.3 67 | webapp-hashing@1.0.5 68 | -------------------------------------------------------------------------------- /examples/minimalist/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | {{> minimalist}} 3 | 4 | -------------------------------------------------------------------------------- /examples/minimalist/client/lib/register-bind.js: -------------------------------------------------------------------------------- 1 | ViewModel.registerHelper("bind"); 2 | -------------------------------------------------------------------------------- /examples/minimalist/client/views/bindings/checked.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /examples/minimalist/client/views/bindings/disabled.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /examples/minimalist/client/views/bindings/files.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/minimalist/client/views/bindings/focused.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/minimalist/client/views/bindings/hovered.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/minimalist/client/views/bindings/toggle.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/minimalist/client/views/bindings/value.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/minimalist/client/views/minimalist.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /examples/minimalist/packages: -------------------------------------------------------------------------------- 1 | ../../packages -------------------------------------------------------------------------------- /examples/pikaday/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | -------------------------------------------------------------------------------- /examples/pikaday/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/pikaday/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | 1s6gnnl1fbnawpe9egua 8 | -------------------------------------------------------------------------------- /examples/pikaday/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | standard-minifiers 8 | ecmascript 9 | meteor-base 10 | mobile-experience 11 | blaze-html-templates 12 | reload 13 | spacebars 14 | 15 | dalgard:get-helper-reactively 16 | dalgard:viewmodel 17 | richsilv:pikaday 18 | -------------------------------------------------------------------------------- /examples/pikaday/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/pikaday/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.2.1 2 | -------------------------------------------------------------------------------- /examples/pikaday/.meteor/versions: -------------------------------------------------------------------------------- 1 | autoupdate@1.2.4 2 | babel-compiler@5.8.24_1 3 | babel-runtime@0.1.4 4 | base64@1.0.4 5 | binary-heap@1.0.4 6 | blaze@2.1.3 7 | blaze-html-templates@1.0.1 8 | blaze-tools@1.0.4 9 | boilerplate-generator@1.0.4 10 | caching-compiler@1.0.0 11 | caching-html-compiler@1.0.2 12 | callback-hook@1.0.4 13 | check@1.1.0 14 | dalgard:get-helper-reactively@0.1.0 15 | dalgard:reactive-map@0.1.0_3 16 | dalgard:viewmodel@1.0.2 17 | ddp@1.2.2 18 | ddp-client@1.2.1 19 | ddp-common@1.2.2 20 | ddp-server@1.2.2 21 | deps@1.0.9 22 | diff-sequence@1.0.1 23 | ecmascript@0.1.6 24 | ecmascript-runtime@0.2.6 25 | ejson@1.0.7 26 | fastclick@1.0.7 27 | geojson-utils@1.0.4 28 | hot-code-push@1.0.0 29 | html-tools@1.0.5 30 | htmljs@1.0.5 31 | http@1.1.1 32 | id-map@1.0.4 33 | jquery@1.11.4 34 | launch-screen@1.0.4 35 | livedata@1.0.15 36 | logging@1.0.8 37 | meteor@1.1.10 38 | meteor-base@1.0.1 39 | minifiers@1.1.7 40 | minimongo@1.0.10 41 | mobile-experience@1.0.1 42 | mobile-status-bar@1.0.6 43 | momentjs:moment@2.9.0 44 | mongo@1.1.3 45 | mongo-id@1.0.1 46 | npm-mongo@1.4.39_1 47 | observe-sequence@1.0.7 48 | ordered-dict@1.0.4 49 | promise@0.5.1 50 | random@1.0.5 51 | reactive-dict@1.1.3 52 | reactive-var@1.0.6 53 | reload@1.1.4 54 | retry@1.0.4 55 | richsilv:pikaday@1.0.1 56 | routepolicy@1.0.6 57 | sha@1.0.4 58 | spacebars@1.0.7 59 | spacebars-compiler@1.0.7 60 | standard-minifiers@1.0.2 61 | stevezhu:lodash@3.10.1 62 | templating@1.1.5 63 | templating-tools@1.0.0 64 | tracker@1.0.9 65 | ui@1.0.8 66 | underscore@1.0.4 67 | url@1.0.5 68 | webapp@1.2.3 69 | webapp-hashing@1.0.5 70 | -------------------------------------------------------------------------------- /examples/pikaday/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | {{> pikaday}} 3 | 4 | -------------------------------------------------------------------------------- /examples/pikaday/client/lib/register-bind.js: -------------------------------------------------------------------------------- 1 | ViewModel.registerHelper("bind"); 2 | -------------------------------------------------------------------------------- /examples/pikaday/client/views/pikaday.html: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /examples/pikaday/packages: -------------------------------------------------------------------------------- 1 | ../../packages -------------------------------------------------------------------------------- /examples/quickstart/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | -------------------------------------------------------------------------------- /examples/quickstart/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/quickstart/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | hyjwtc1g62231vlx04i 8 | -------------------------------------------------------------------------------- /examples/quickstart/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | standard-minifiers 8 | ecmascript 9 | meteor-base 10 | mobile-experience 11 | blaze-html-templates 12 | reload 13 | spacebars 14 | 15 | dalgard:get-helper-reactively 16 | dalgard:viewmodel 17 | -------------------------------------------------------------------------------- /examples/quickstart/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/quickstart/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.2.1 2 | -------------------------------------------------------------------------------- /examples/quickstart/.meteor/versions: -------------------------------------------------------------------------------- 1 | autoupdate@1.2.4 2 | babel-compiler@5.8.24_1 3 | babel-runtime@0.1.4 4 | base64@1.0.4 5 | binary-heap@1.0.4 6 | blaze@2.1.3 7 | blaze-html-templates@1.0.1 8 | blaze-tools@1.0.4 9 | boilerplate-generator@1.0.4 10 | caching-compiler@1.0.0 11 | caching-html-compiler@1.0.2 12 | callback-hook@1.0.4 13 | check@1.1.0 14 | dalgard:get-helper-reactively@0.1.0 15 | dalgard:reactive-map@0.1.0_3 16 | dalgard:viewmodel@1.0.2 17 | ddp@1.2.2 18 | ddp-client@1.2.1 19 | ddp-common@1.2.2 20 | ddp-server@1.2.2 21 | deps@1.0.9 22 | diff-sequence@1.0.1 23 | ecmascript@0.1.6 24 | ecmascript-runtime@0.2.6 25 | ejson@1.0.7 26 | fastclick@1.0.7 27 | geojson-utils@1.0.4 28 | hot-code-push@1.0.0 29 | html-tools@1.0.5 30 | htmljs@1.0.5 31 | http@1.1.1 32 | id-map@1.0.4 33 | jquery@1.11.4 34 | launch-screen@1.0.4 35 | livedata@1.0.15 36 | logging@1.0.8 37 | meteor@1.1.10 38 | meteor-base@1.0.1 39 | minifiers@1.1.7 40 | minimongo@1.0.10 41 | mobile-experience@1.0.1 42 | mobile-status-bar@1.0.6 43 | mongo@1.1.3 44 | mongo-id@1.0.1 45 | npm-mongo@1.4.39_1 46 | observe-sequence@1.0.7 47 | ordered-dict@1.0.4 48 | promise@0.5.1 49 | random@1.0.5 50 | reactive-dict@1.1.3 51 | reactive-var@1.0.6 52 | reload@1.1.4 53 | retry@1.0.4 54 | routepolicy@1.0.6 55 | sha@1.0.4 56 | spacebars@1.0.7 57 | spacebars-compiler@1.0.7 58 | standard-minifiers@1.0.2 59 | stevezhu:lodash@3.10.1 60 | templating@1.1.5 61 | templating-tools@1.0.0 62 | tracker@1.0.9 63 | ui@1.0.8 64 | underscore@1.0.4 65 | url@1.0.5 66 | webapp@1.2.3 67 | webapp-hashing@1.0.5 68 | -------------------------------------------------------------------------------- /examples/quickstart/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | {{> quickstart}} 3 | 4 | -------------------------------------------------------------------------------- /examples/quickstart/client/lib/register-bind.js: -------------------------------------------------------------------------------- 1 | ViewModel.registerHelper("bind"); 2 | -------------------------------------------------------------------------------- /examples/quickstart/client/views/quickstart.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /examples/quickstart/packages: -------------------------------------------------------------------------------- 1 | ../../packages -------------------------------------------------------------------------------- /examples/usage/.meteor/.finished-upgraders: -------------------------------------------------------------------------------- 1 | # This file contains information which helps Meteor properly upgrade your 2 | # app when you run 'meteor update'. You should check it into version control 3 | # with your project. 4 | 5 | notices-for-0.9.0 6 | notices-for-0.9.1 7 | 0.9.4-platform-file 8 | notices-for-facebook-graph-api-2 9 | 1.2.0-standard-minifiers-package 10 | 1.2.0-meteor-platform-split 11 | 1.2.0-cordova-changes 12 | 1.2.0-breaking-changes 13 | -------------------------------------------------------------------------------- /examples/usage/.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /examples/usage/.meteor/.id: -------------------------------------------------------------------------------- 1 | # This file contains a token that is unique to your project. 2 | # Check it into your repository along with the rest of this directory. 3 | # It can be used for purposes such as: 4 | # - ensuring you don't accidentally deploy one app on top of another 5 | # - providing package authors with aggregated statistics 6 | 7 | hyjwtc1g62231vlx04i 8 | -------------------------------------------------------------------------------- /examples/usage/.meteor/packages: -------------------------------------------------------------------------------- 1 | # Meteor packages used by this project, one per line. 2 | # Check this file (and the other files in this directory) into your repository. 3 | # 4 | # 'meteor add' and 'meteor remove' will edit this file for you, 5 | # but you can also edit it by hand. 6 | 7 | standard-minifiers 8 | ecmascript 9 | meteor-base 10 | mobile-experience 11 | blaze-html-templates 12 | reload 13 | spacebars 14 | 15 | dalgard:viewmodel 16 | -------------------------------------------------------------------------------- /examples/usage/.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /examples/usage/.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.2.1 2 | -------------------------------------------------------------------------------- /examples/usage/.meteor/versions: -------------------------------------------------------------------------------- 1 | autoupdate@1.2.4 2 | babel-compiler@5.8.24_1 3 | babel-runtime@0.1.4 4 | base64@1.0.4 5 | binary-heap@1.0.4 6 | blaze@2.1.3 7 | blaze-html-templates@1.0.1 8 | blaze-tools@1.0.4 9 | boilerplate-generator@1.0.4 10 | caching-compiler@1.0.0 11 | caching-html-compiler@1.0.2 12 | callback-hook@1.0.4 13 | check@1.1.0 14 | dalgard:reactive-map@0.1.0_3 15 | dalgard:viewmodel@1.0.2 16 | ddp@1.2.2 17 | ddp-client@1.2.1 18 | ddp-common@1.2.2 19 | ddp-server@1.2.2 20 | deps@1.0.9 21 | diff-sequence@1.0.1 22 | ecmascript@0.1.6 23 | ecmascript-runtime@0.2.6 24 | ejson@1.0.7 25 | fastclick@1.0.7 26 | geojson-utils@1.0.4 27 | hot-code-push@1.0.0 28 | html-tools@1.0.5 29 | htmljs@1.0.5 30 | http@1.1.1 31 | id-map@1.0.4 32 | jquery@1.11.4 33 | launch-screen@1.0.4 34 | livedata@1.0.15 35 | logging@1.0.8 36 | meteor@1.1.10 37 | meteor-base@1.0.1 38 | minifiers@1.1.7 39 | minimongo@1.0.10 40 | mobile-experience@1.0.1 41 | mobile-status-bar@1.0.6 42 | mongo@1.1.3 43 | mongo-id@1.0.1 44 | npm-mongo@1.4.39_1 45 | observe-sequence@1.0.7 46 | ordered-dict@1.0.4 47 | promise@0.5.1 48 | random@1.0.5 49 | reactive-dict@1.1.3 50 | reactive-var@1.0.6 51 | reload@1.1.4 52 | retry@1.0.4 53 | routepolicy@1.0.6 54 | sha@1.0.4 55 | spacebars@1.0.7 56 | spacebars-compiler@1.0.7 57 | standard-minifiers@1.0.2 58 | stevezhu:lodash@3.10.1 59 | templating@1.1.5 60 | templating-tools@1.0.0 61 | tracker@1.0.9 62 | ui@1.0.8 63 | underscore@1.0.4 64 | url@1.0.5 65 | webapp@1.2.3 66 | webapp-hashing@1.0.5 67 | -------------------------------------------------------------------------------- /examples/usage/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | {{> usage}} 3 | 4 | -------------------------------------------------------------------------------- /examples/usage/client/views/field.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /examples/usage/client/views/field.js: -------------------------------------------------------------------------------- 1 | // Instead of a definition object, a factory function may be used. Unrelated 2 | // to the factory, this viewmodel is also given a name. 3 | Template.usageField.viewmodel("field", function (data) { 4 | // Return the new viewmodel definition 5 | return { 6 | // Primitive property 7 | myValue: data && data.startValue || "", 8 | 9 | // Computed property 10 | regex() { 11 | // Get the value of myValue reactively 12 | const value = this.myValue(); 13 | 14 | return new RegExp(value); 15 | }, 16 | 17 | // React to changes in dependencies such as viewmodel properties 18 | // – can be an array of functions 19 | autorun() { 20 | // Log every time the computed regex property changes 21 | console.log("New value of regex:", this.regex()); 22 | }, 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /examples/usage/client/views/usage.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /examples/usage/client/views/usage.js: -------------------------------------------------------------------------------- 1 | // Declare a viewmodel on this template (all properties are registered as Blaze helpers) 2 | Template.usage.viewmodel({ 3 | // Computed property from child viewmodel 4 | myFieldValue() { 5 | // Get child viewmodel reactively by name 6 | const field = this.child("field"); 7 | 8 | // Get the value of myValue reactively when the field is rendered 9 | return field && field.myValue(); 10 | }, 11 | }, {}); // An options object may be passed 12 | -------------------------------------------------------------------------------- /examples/usage/packages: -------------------------------------------------------------------------------- 1 | ../../packages -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@5.8.24_1 2 | babel-runtime@0.1.4 3 | base64@1.0.4 4 | blaze@2.1.3 5 | blaze-tools@1.0.4 6 | caching-compiler@1.0.0 7 | caching-html-compiler@1.0.2 8 | check@1.1.0 9 | dalgard:reactive-map@0.1.0 10 | dalgard:viewmodel@1.0.2 11 | deps@1.0.9 12 | diff-sequence@1.0.1 13 | ecmascript@0.1.6 14 | ecmascript-runtime@0.2.6 15 | ejson@1.0.7 16 | html-tools@1.0.5 17 | htmljs@1.0.5 18 | id-map@1.0.4 19 | jquery@1.11.4 20 | meteor@1.1.10 21 | minifiers@1.1.7 22 | mongo-id@1.0.1 23 | observe-sequence@1.0.7 24 | promise@0.5.1 25 | random@1.0.5 26 | reactive-dict@1.1.3 27 | reactive-var@1.0.6 28 | sha@1.0.4 29 | spacebars@1.0.7 30 | spacebars-compiler@1.0.7 31 | stevezhu:lodash@3.10.1 32 | templating@1.1.5 33 | templating-tools@1.0.0 34 | tracker@1.0.9 35 | underscore@1.0.4 36 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/bindings/checked.js: -------------------------------------------------------------------------------- 1 | ViewModel.addBinding("checked", { 2 | set(elem, new_value) { 3 | elem.checked = new_value || false; 4 | }, 5 | 6 | on: "change", 7 | 8 | get(event, elem) { 9 | return elem.checked; 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/bindings/class.js: -------------------------------------------------------------------------------- 1 | ViewModel.addBinding("class", { 2 | set(elem) { 3 | let classes = this.hash; 4 | 5 | // Keyword arguments must be present 6 | if (_.isObject(classes)) { 7 | // Possibly only use indicated keys 8 | if (this.args.length) 9 | classes = _.pick(classes, this.args); 10 | 11 | _.each(classes, (value, name) => (value ? addClass(elem, name) : removeClass(elem, name))); 12 | } 13 | }, 14 | }, { 15 | // This binding doesn't need a viewmodel 16 | detached: true, 17 | }); 18 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/bindings/click.js: -------------------------------------------------------------------------------- 1 | ViewModel.addBinding("click", { 2 | on: "click", 3 | }); 4 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/bindings/disabled.js: -------------------------------------------------------------------------------- 1 | ViewModel.addBinding("disabled", { 2 | set(elem, new_value) { 3 | elem.disabled = new_value || false; 4 | }, 5 | }); 6 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/bindings/enter-key.js: -------------------------------------------------------------------------------- 1 | ViewModel.addBinding("enterKey", { 2 | on: "keyup", 3 | 4 | get(event, elem, prop) { 5 | const key = event.key || event.keyCode || event.keyIdentifier; 6 | 7 | if (key === 13) 8 | prop(event, this.args, this.hash); 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/bindings/files.js: -------------------------------------------------------------------------------- 1 | ViewModel.addBinding("files", { 2 | on: "change", 3 | 4 | get(event, elem) { 5 | return elem.files; 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/bindings/focused.js: -------------------------------------------------------------------------------- 1 | ViewModel.addBinding("focused", { 2 | set(elem, new_value) { 3 | if (new_value) 4 | return elem.focus(); 5 | 6 | return elem.blur(); 7 | }, 8 | 9 | on: "focus blur", 10 | 11 | get(event) { 12 | return event.type === "focus"; 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/bindings/hovered.js: -------------------------------------------------------------------------------- 1 | ViewModel.addBinding("hovered", { 2 | init() { 3 | this.delayEnter = this.args[1]; 4 | this.delayLeave = this.args[2] || this.delayEnter; 5 | 6 | _.each(["delayEnter", "delayLeave"], key => { 7 | if (_.isObject(this.hash)) 8 | this[key] = this.hash[key] || this.hash.delay || this[key]; 9 | 10 | this[key] = parseInt(this[key], 10); 11 | }); 12 | }, 13 | 14 | on: "mouseenter mouseleave", 15 | 16 | get(event, elem, prop) { 17 | clearTimeout(this.enterId); 18 | clearTimeout(this.leaveId); 19 | 20 | if (event.type === "mouseenter") { 21 | if (!this.delayEnter) 22 | return true; 23 | 24 | this.enterId = setTimeout(() => prop(true), this.delayEnter); 25 | } 26 | else { 27 | if (!this.delayLeave) 28 | return false; 29 | 30 | this.leaveId = setTimeout(() => prop(false), this.delayLeave); 31 | } 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/bindings/key.js: -------------------------------------------------------------------------------- 1 | ViewModel.addBinding("key", { 2 | on: "keyup", 3 | 4 | get(event, elem, prop) { 5 | const use_hash = _.isNumber(_.isObject(this.hash) && this.hash.keyCode); 6 | const key_code = use_hash ? this.hash.keyCode : parseInt(this.args[1], 10); 7 | const key = event.key || event.keyCode || event.keyIdentifier; 8 | 9 | if (key === key_code) 10 | prop(event, this.args, this.hash); 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/bindings/pikaday.js: -------------------------------------------------------------------------------- 1 | ViewModel.addBinding("pikaday", { 2 | // Initialize Pikaday instance 3 | init(elem) { 4 | // Pikaday package must be present 5 | if (typeof Pikaday !== "function") 6 | throw new ReferenceError("Pikaday must be present for this binding to work (add richsilv:pikaday)"); 7 | 8 | const position = this.hash.position || (this.args[2] ? this.args[1] + " " + this.args[2] : this.args[1]); 9 | const options = { 10 | field: elem, // Use DOM element 11 | format: this.hash.monthFirst ? "MM-DD-YYYY" : "DD-MM-YYYY", 12 | firstDay: 1, 13 | position: position || "bottom left", 14 | }; 15 | 16 | // Possibly localize 17 | if (_.isObject(this.hash.i18n)) 18 | options.i18n = this.hash.i18n; 19 | 20 | // Save instance on binding context 21 | this.instance = new Pikaday(options); 22 | }, 23 | 24 | set(elem, new_value) { 25 | // Prevent infinite loop, since setDate triggers a change event in spite of silent flag 26 | // https://github.com/dbushell/Pikaday/issues/402 27 | this.isSetting = true; 28 | 29 | this.instance.setDate(new_value, true); 30 | 31 | this.isSetting = false; 32 | 33 | // Clear field when the date is cleared 34 | if (!new_value) 35 | elem.value = ""; 36 | 37 | // Keyboard arrow controls 38 | if (this.isGetting) { 39 | let start = 0; 40 | let end = 2; 41 | 42 | if (this.position >= 3 && this.position <= 5) { 43 | start = 3; 44 | end = 5; 45 | } 46 | else if (this.position > 5) { 47 | start = 6; 48 | end = 10; 49 | } 50 | 51 | elem.setSelectionRange(start, end); 52 | } 53 | }, 54 | 55 | on: "cut paste change keyup keypress keydown", 56 | 57 | get(event, elem, prop) { 58 | // Check whether it is a change 59 | if (_.contains(["cut", "paste", "change"], event.type)) { 60 | if (!this.isSetting) { 61 | return this.instance.getDate(); 62 | } 63 | } 64 | else { 65 | // Check whether setSelectionRange is supported 66 | if (_.isFunction(elem.setSelectionRange)) { 67 | const key = event.key || event.keyCode || event.keyIdentifier; 68 | const delta = 39 - key; 69 | 70 | // Keyboard arrows up/down have keycodes 38/40 71 | if (Math.abs(delta) === 1) { 72 | event.preventDefault(); 73 | 74 | if (event.type === "keyup") { 75 | const date = prop.nonreactive(); 76 | 77 | if (_.isDate(date)) { 78 | this.position = elem.selectionStart; 79 | 80 | if (_.isNumber(this.position)) { 81 | const is_first = this.position <= 2; 82 | const is_second = this.position >= 3 && this.position <= 5; 83 | 84 | if (this.hash.monthFirst ? is_second : is_first) 85 | date.setDate(date.getDate() + delta); 86 | else if (this.hash.monthFirst ? is_first : is_second) 87 | date.setMonth(date.getMonth() + delta); 88 | else 89 | date.setFullYear(date.getFullYear() + delta); 90 | 91 | this.isGetting = true; 92 | 93 | prop(date); 94 | 95 | Tracker.afterFlush(() => this.isGetting = false); 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } 102 | }, 103 | 104 | // Destroy Pikaday instance to avoid memory leak 105 | dispose() { 106 | this.instance.destroy(); 107 | }, 108 | }); 109 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/bindings/radio.js: -------------------------------------------------------------------------------- 1 | ViewModel.addBinding("radio", { 2 | set(elem, new_value) { 3 | if (elem.value === new_value) 4 | elem.checked = true; 5 | }, 6 | 7 | on: "change", 8 | 9 | get(event, elem) { 10 | return elem.value; 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/bindings/submit.js: -------------------------------------------------------------------------------- 1 | ViewModel.addBinding("submit", { 2 | on: "submit", 3 | 4 | get(event, elem, prop) { 5 | const use_hash = _.isBoolean(_.isObject(this.hash) && this.hash.send); 6 | const send = use_hash ? this.hash.send : this.args[1] === "true"; 7 | 8 | if (!send) 9 | event.preventDefault(); 10 | 11 | prop(event, this.args, this.hash); 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/bindings/toggle.js: -------------------------------------------------------------------------------- 1 | ViewModel.addBinding("toggle", { 2 | on: "click", 3 | 4 | get(event, elem, prop) { 5 | return !prop(); 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/bindings/value.js: -------------------------------------------------------------------------------- 1 | ViewModel.addBinding("value", function () { 2 | const use_hash = _.isObject(this.hash); 3 | const throttle = use_hash && this.hash.throttle || parseInt(this.args[1], 10); 4 | const leading = use_hash && _.isBoolean(this.hash.leading) ? this.hash.leading : String(this.args[2]) === "true"; 5 | 6 | let get = function (event, elem, prop) { 7 | this.preventSet(); 8 | 9 | prop(elem.value); 10 | }; 11 | 12 | if (throttle) 13 | get = _.throttle(get, throttle, { leading }); 14 | 15 | return { 16 | set(elem, new_value) { 17 | elem.value = new_value || ""; 18 | }, 19 | 20 | on: "cut paste keyup input change", 21 | 22 | get, 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/lib/base.js: -------------------------------------------------------------------------------- 1 | // Base class for viewmodels and nexuses 2 | Base = class Base { 3 | constructor(view, name) { 4 | check(view, Blaze.View); 5 | check(name, Match.OneOf(String, null)); 6 | 7 | // Static properties on instance 8 | defineProperties(this, { 9 | // Reference to view 10 | view: { value: view }, 11 | 12 | // Instance name 13 | _name: { value: new ReactiveVar(name) }, 14 | }); 15 | } 16 | 17 | 18 | // Reactively get or set the name of the instance 19 | name(name) { 20 | // Ensure type of argument 21 | check(name, Match.Optional(String)); 22 | 23 | // Getter 24 | if (_.isUndefined(name)) 25 | return this._name.get(); 26 | 27 | this._name.set(name); 28 | 29 | // Return name if setter 30 | return name; 31 | } 32 | 33 | // Test this instance 34 | test(test) { 35 | // Predicate function 36 | if (_.isFunction(test)) 37 | return test(this); 38 | 39 | // Test regex with name 40 | if (_.isRegExp(test)) 41 | return test.test(this.name()); 42 | 43 | // Compare with name 44 | if (_.isString(test)) 45 | return test === this.name(); 46 | 47 | // Compare with instance 48 | return test === this; 49 | } 50 | 51 | 52 | // Run callback when view is rendered and after flush 53 | onReady(callback) { 54 | // Ensure type of argument 55 | check(callback, Function); 56 | 57 | // Bind callback to context 58 | callback = callback.bind(this); 59 | 60 | const view = this.view; 61 | 62 | if (view.isRendered) { 63 | if (!view.isDestroyed) { 64 | Tracker.afterFlush(callback); 65 | } 66 | } 67 | else { 68 | view.onViewReady(callback); 69 | } 70 | } 71 | 72 | // Register one or more autoruns 73 | autorun(callback) { 74 | // May be an array of callbacks 75 | if (_.isArray(callback)) 76 | return _.each(callback, this.autorun, this); 77 | 78 | // Ensure type of argument 79 | check(callback, Function); 80 | 81 | // Bind callback to context 82 | callback = callback.bind(this); 83 | 84 | const view = this.view; 85 | 86 | // Wait until the view is rendered and after flush 87 | this.onReady(function () { 88 | view.autorun(callback); 89 | }); 90 | } 91 | 92 | // Run callback when view is refreshed 93 | onRefreshed(callback) { 94 | // Ensure type of argument 95 | check(callback, Function); 96 | 97 | // Bind callback to context 98 | callback = callback.bind(this); 99 | 100 | this.view.onViewReady(function () { 101 | if (this.renderCount > 1) 102 | callback(); 103 | }); 104 | } 105 | 106 | // Run callback when view is destroyed 107 | onDestroyed(callback) { 108 | // Ensure type of argument 109 | check(callback, Function); 110 | 111 | // Bind callback to context 112 | callback = callback.bind(this); 113 | 114 | this.view.onViewDestroyed(callback); 115 | } 116 | 117 | // Run callback when the current computation is invalidated 118 | onInvalidate(callback) { 119 | // Ensure type of argument 120 | check(callback, Function); 121 | 122 | // Bind callback to context 123 | callback = callback.bind(this); 124 | 125 | const computation = Tracker.currentComputation; 126 | 127 | if (computation) 128 | computation.onInvalidate(callback); 129 | } 130 | }; 131 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/lib/binding.js: -------------------------------------------------------------------------------- 1 | // Store for binding definitions 2 | const bindings = new ReactiveMap; 3 | 4 | 5 | // Class for binding definitions 6 | Binding = class Binding { 7 | constructor(name, definition, options) { 8 | // Ensure type of arguments 9 | check(name, String); 10 | check(definition, Match.OneOf(Object, Function)); 11 | check(options, Match.Optional(Object)); 12 | 13 | // Static properties on property instance 14 | defineProperties(this, { 15 | // Binding name 16 | name: { value: name }, 17 | 18 | // Binding definition 19 | _definition: { value: definition }, 20 | 21 | // Configuration options 22 | _options: { value: new ReactiveMap(options) }, 23 | }); 24 | } 25 | 26 | 27 | // Reactively get or set configuration options 28 | option(key, value) { 29 | // Ensure type of argument 30 | check(key, String); 31 | 32 | // Getter 33 | if (_.isUndefined(value)) 34 | return this._options.get(key); 35 | 36 | this._elem.set(key, value); 37 | 38 | // Return value if setter 39 | return value; 40 | } 41 | 42 | // Get resolve binding definition 43 | definition(context = {}, finalize = true) { 44 | // Ensure type of argument 45 | check(context, Object); 46 | 47 | let def = this._definition; 48 | 49 | // May be a factory 50 | if (_.isFunction(def)) 51 | def = def.call(context); 52 | else 53 | def = _.cloneDeep(def); 54 | 55 | check(def, Object); 56 | 57 | 58 | // Add name to definition 59 | def.name = this.name; 60 | 61 | // Convert event types to array 62 | if (_.isString(def.on)) 63 | def.on = def.on.split(/\s+/g); 64 | 65 | // Add options to definition 66 | _.defaults(def, this._options.all()); 67 | 68 | 69 | // Get extends option 70 | const exts = this.option("extends"); 71 | 72 | if (exts) { 73 | check(exts, Match.OneOf(String, [String])); 74 | 75 | // Resolve extends 76 | const defs = _.isArray(exts) 77 | ? _.map(exts, name => Binding.get(name).definition(context, false)) 78 | : [Binding.get(exts).definition(context, false)]; 79 | 80 | // Inherit 81 | _.defaults(def, ...defs); 82 | } 83 | 84 | 85 | // Possibly lock down all properties 86 | if (finalize) { 87 | defineProperties(def, _.mapValues(def, () => ({ 88 | enumerable: false, 89 | writable: false, 90 | configurable: false, 91 | }))); 92 | } 93 | 94 | 95 | return def; 96 | } 97 | 98 | 99 | // Add binding to the global list 100 | static add(name, definition, options) { 101 | const binding = new Binding(name, definition, options); 102 | 103 | // Add to reactive map 104 | bindings.set(name, binding); 105 | 106 | return binding; 107 | } 108 | 109 | // Get binding by name 110 | static get(name) { 111 | // Ensure type of arguments 112 | check(name, String); 113 | 114 | return bindings.get(name) || null; 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/lib/list.js: -------------------------------------------------------------------------------- 1 | // Class for reactive lists 2 | List = class List extends Array { 3 | constructor(...args) { 4 | super(...args); 5 | 6 | // Add dependency to list 7 | defineProperties(this, { 8 | dep: { value: new Tracker.Dependency }, 9 | }); 10 | } 11 | 12 | 13 | // Reactively add an item 14 | add(...items) { 15 | this.push(...items); 16 | 17 | this.dep.changed(); 18 | } 19 | 20 | // Reactively remove an item 21 | remove(...items) { 22 | let result = false; 23 | 24 | _.each(items, item => { 25 | const index = this.indexOf(item); 26 | const is_found = !!~index; 27 | 28 | if (is_found) { 29 | this.splice(index, 1); 30 | 31 | this.dep.changed(); 32 | 33 | result = true; 34 | } 35 | }); 36 | 37 | return result; 38 | } 39 | 40 | 41 | // Reactively get an array of matching items 42 | find(...tests) { 43 | this.dep.depend(); 44 | 45 | // Possibly remove items failing test 46 | if (tests.length) { 47 | return _.filter(this, (item, index, list) => _.every(tests, test => { 48 | if (_.isFunction(test)) 49 | return test(item, index, list); 50 | 51 | if (_.isObject(item) && _.isFunction(item.test)) 52 | return item.test(test); 53 | 54 | return test === item; 55 | })); 56 | } 57 | 58 | // Return copy of array 59 | return this.slice(); 60 | } 61 | 62 | // Reactively get the first current item at index 63 | findOne(...args) { 64 | // Handle trailing number arguments 65 | const tests = _.dropRightWhile(args, _.isNumber); 66 | const index = args.slice(tests.length).pop() || 0; 67 | 68 | // Use slice to allow negative indices 69 | return this.find(...tests).slice(index)[0] || null; 70 | } 71 | 72 | 73 | // Decorate an object with list methods operating on an internal list 74 | static decorate(obj, reference_key) { 75 | // Ensure type of arguments 76 | check(obj, Match.OneOf(Object, Function)); 77 | check(reference_key, Match.Optional(String)); 78 | 79 | // Internal list 80 | const list = new List; 81 | 82 | // Property descriptors 83 | const descriptor = { 84 | add: { value: list.add.bind(list) }, 85 | remove: { value: list.remove.bind(list) }, 86 | find: { value: list.find.bind(list) }, 87 | findOne: { value: list.findOne.bind(list) }, 88 | }; 89 | 90 | // Possibly add a reference to the internal list 91 | if (reference_key) 92 | descriptor[reference_key] = { value: list }; 93 | 94 | // Add bound methods 95 | defineProperties(obj, descriptor); 96 | } 97 | }; 98 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/lib/nexus.js: -------------------------------------------------------------------------------- 1 | // Class for binding nexuses 2 | Nexus = class Nexus extends Base { 3 | constructor(view, selector, binding, context = {}) { 4 | // Ensure type of arguments 5 | check(selector, Match.OneOf(String, Match.Where(_.isElement))); 6 | check(binding, Match.OneOf(String, Binding)); 7 | check(context, Object); 8 | 9 | // Possibly get binding 10 | if (_.isString(binding)) 11 | binding = Binding.get(binding); 12 | 13 | const is_detached = binding.option("detached"); 14 | 15 | let key = null; 16 | let vm = null; 17 | let prop = null; 18 | 19 | // Possibly get key 20 | if (!is_detached && _.isArray(context.args) && _.isString(context.args[0])) 21 | key = context.args[0]; 22 | 23 | // Possibly ensure existence of a viewmodel 24 | if (!is_detached && !(context.viewmodel instanceof ViewModel)) 25 | vm = ViewModel.ensureViewmodel(view, key); 26 | 27 | // Possibly get viewmodel property 28 | if (vm && !_.isUndefined(key) && _.isFunction(vm[key])) 29 | prop = vm[key]; 30 | 31 | 32 | // Call constructor of Base 33 | super(view, binding.name); 34 | 35 | // Possibly create nexuses list 36 | if (!(this.view[ViewModel.nexusesKey] instanceof List)) 37 | this.view[ViewModel.nexusesKey] = new List; 38 | 39 | 40 | // Static properties on context object 41 | defineProperties(context, { 42 | // Reference to view 43 | view: { value: view }, 44 | 45 | // Reference to template instance 46 | templateInstance: { value: templateInstance(view) }, 47 | 48 | // Method bound to instance 49 | preventSet: { value: this.preventSet.bind(this) }, 50 | 51 | // Viewmodel key 52 | key: { value: key }, 53 | 54 | // Reference to viewmodel 55 | viewmodel: { value: vm }, 56 | }); 57 | 58 | 59 | // Static properties on nexus instance 60 | defineProperties(this, { 61 | // Element selector 62 | selector: { value: selector }, 63 | 64 | // Calling context of bind 65 | context: { value: context }, 66 | 67 | // Binding definition resolved with context 68 | binding: { value: binding.definition(context) }, 69 | 70 | // Viewmodel property 71 | prop: { value: prop }, 72 | 73 | // Bound DOM element 74 | _elem: { value: null, writable: true }, 75 | 76 | // Whether to run the set function when updating 77 | _isSetPrevented: { value: null, writable: true }, 78 | }); 79 | 80 | 81 | // Unbind element on view refreshed 82 | this.onRefreshed(this.unbind); 83 | 84 | // Unbind element on view destroyed 85 | this.onDestroyed(this.unbind); 86 | 87 | // Unbind element on computation invalidation 88 | this.onInvalidate(() => this.unbind(true)); 89 | 90 | 91 | // Bind element on view ready 92 | this.onReady(this.bind); 93 | } 94 | 95 | 96 | // Get or set the bound DOM element 97 | elem(elem) { 98 | // Ensure type of argument 99 | check(elem, Match.Optional(Match.Where(_.isElement))); 100 | 101 | // Getter 102 | if (_.isUndefined(elem)) 103 | return this._elem; 104 | 105 | // Set and return element if setter 106 | return this._elem = elem; 107 | } 108 | 109 | // Test the element of this instance or delegate to super 110 | test(test) { 111 | // Compare with element 112 | if (_.isElement(test)) 113 | return test === this.elem(); 114 | 115 | return super(test); 116 | } 117 | 118 | 119 | // Get or set whether to run the set function when updating 120 | isSetPrevented(is_set_prevented) { 121 | // Ensure type of argument 122 | check(is_set_prevented, Match.Optional(Match.OneOf(Boolean, null))); 123 | 124 | // Getter 125 | if (_.isUndefined(is_set_prevented)) 126 | return this._isSetPrevented; 127 | 128 | // Set and return is_set_prevented if setter 129 | return this._isSetPrevented = is_set_prevented; 130 | } 131 | 132 | // Change prevented state of nexus 133 | preventSet(state = true) { 134 | // Ensure type of argument 135 | check(state, Match.OneOf(Boolean, null)); 136 | 137 | this.isSetPrevented(state); 138 | } 139 | 140 | 141 | // Bind element 142 | bind() { 143 | const binding = this.binding; 144 | const prop = this.prop; 145 | 146 | // Get element (possibly set) 147 | const elem = this.elem() || this.elem(document.querySelector(this.selector)); 148 | 149 | // Don't bind if element is no longer present 150 | if (!Nexus.isInBody(elem)) 151 | return false; 152 | 153 | 154 | if (binding.init) { 155 | // Ensure type of definition property 156 | check(binding.init, Function); 157 | 158 | const init_value = prop && prop(); 159 | 160 | // Run init function immediately 161 | binding.init.call(this.context, elem, init_value); 162 | } 163 | 164 | 165 | if (binding.on) { 166 | // Ensure type of definition property 167 | check(binding.on, Array); 168 | 169 | const listener = event => { 170 | if (binding.get) { 171 | // Ensure type of definition property 172 | check(binding.get, Function); 173 | 174 | const result = binding.get.call(this.context, event, elem, prop); 175 | 176 | // Call property if get returned a value other than undefined 177 | if (prop && !_.isUndefined(result)) { 178 | // Don't trigger set function from updating property 179 | if (this.isSetPrevented() !== false) 180 | this.preventSet(); 181 | 182 | prop.call(this.context.viewmodel, result); 183 | } 184 | } 185 | else if (prop) { 186 | // Call property if get was omitted in the binding definition 187 | prop.call(this.context.viewmodel, event, this.context.args, this.context.hash); 188 | } 189 | 190 | // Mark that we are exiting the update cycle 191 | Tracker.afterFlush(this.preventSet.bind(this, null)); 192 | }; 193 | 194 | // Save listener for unbind 195 | defineProperties(this, { 196 | listener: { value: listener }, 197 | }); 198 | 199 | // Register event listeners 200 | _.each(binding.on, type => elem.addEventListener(type, listener)); 201 | } 202 | 203 | 204 | if (binding.set) { 205 | // Ensure type of definition property 206 | check(binding.set, Function); 207 | 208 | // Wrap set function and add it to list of autoruns 209 | this.autorun(comp => { 210 | if (comp.firstRun) { 211 | // Save computation for unbind 212 | defineProperties(this, { 213 | comp: { value: comp }, 214 | }); 215 | } 216 | 217 | const new_value = prop && prop(); 218 | 219 | if (!this.isSetPrevented()) 220 | binding.set.call(this.context, elem, new_value); 221 | }); 222 | } 223 | 224 | 225 | // Add to view list 226 | this.view[ViewModel.nexusesKey].add(this); 227 | 228 | // Add to global list 229 | Nexus.add(this); 230 | 231 | return true; 232 | } 233 | 234 | // Unbind element 235 | unbind(do_unbind = this.view.isDestroyed || !Nexus.isInBody(this.elem())) { 236 | // Unbind elements that are no longer part of the DOM 237 | if (do_unbind) { 238 | const binding = this.binding; 239 | const prop = this.prop; 240 | 241 | 242 | // Possibly unregister event listener 243 | if (this.listener) { 244 | const elem = this.elem(); 245 | 246 | _.each(binding.on, type => elem.removeEventListener(type, this.listener)); 247 | } 248 | 249 | // Possibly stop set autorun 250 | if (this.comp) 251 | this.comp.stop(); 252 | 253 | 254 | // Possibly run dispose function 255 | if (binding.dispose) { 256 | // Ensure type of definition property 257 | check(binding.dispose, Function); 258 | 259 | binding.dispose.call(this.context, prop); 260 | } 261 | 262 | 263 | // Remove from global list 264 | Nexus.remove(this); 265 | 266 | // Remove from view list 267 | this.view[ViewModel.nexusesKey].remove(this); 268 | } 269 | 270 | return do_unbind; 271 | } 272 | 273 | 274 | // Whether an element is present in the document body 275 | static isInBody(elem) { 276 | // Using the DOM contains method 277 | return document.body.contains(elem); 278 | } 279 | }; 280 | 281 | // Decorate Nexus class with static list methods operating on an internal list 282 | List.decorate(Nexus); 283 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/lib/property.js: -------------------------------------------------------------------------------- 1 | // Class for viewmodel properties 2 | Property = class Property { 3 | constructor(vm, key, init_value) { 4 | // Ensure type of arguments 5 | check(vm, ViewModel); 6 | check(key, String); 7 | 8 | const is_primitive = !_.isFunction(init_value); 9 | const accessor = is_primitive ? this.accessor.bind(this) : init_value.bind(vm); 10 | 11 | 12 | // Static properties on property instance 13 | defineProperties(this, { 14 | // Property owner 15 | viewmodel: { value: vm }, 16 | 17 | // Property name 18 | key: { value: key }, 19 | 20 | // Reactive value store 21 | value: { value: new ReactiveVar }, 22 | 23 | // Bound accessor method 24 | accessor: { value: accessor }, 25 | }); 26 | 27 | 28 | // Property methods bound to instance 29 | defineProperties(accessor, { 30 | // Get value 31 | get: { value: this.get.bind(this) }, 32 | 33 | // Set new value 34 | set: { value: this.set.bind(this) }, 35 | 36 | // Reset value 37 | reset: { value: this.reset.bind(this) }, 38 | 39 | // Nonreactive accessor 40 | nonreactive: { value: this.nonreactive.bind(this) }, 41 | }); 42 | 43 | 44 | if (is_primitive) { 45 | // Save initial value 46 | this.initial = init_value; 47 | 48 | // Apply initial value 49 | this.reset(); 50 | } 51 | } 52 | 53 | 54 | // Get the property value 55 | get() { 56 | return this.value.get(); 57 | } 58 | 59 | // Set a new property value 60 | set(value, share = true) { 61 | this.value.set(value); 62 | 63 | // Write to other viewmodels if shared 64 | if (share && this.viewmodel.option("share")) { 65 | const shared = ViewModel.find(vm => vm._id === this.viewmodel._id); 66 | 67 | _.each(shared, vm => vm[this.key].set(value, false)); 68 | } 69 | } 70 | 71 | // Reset the value of the property 72 | reset() { 73 | // Clone initial value to avoid sharing objects and arrays between instances 74 | // of the same viewmodel 75 | this.value.set(_.cloneDeep(this.initial)); 76 | } 77 | 78 | 79 | // Reactive accessor function bound to property instance 80 | accessor(value) { 81 | // Getter 82 | if (_.isUndefined(value)) 83 | return this.get(); 84 | 85 | this.set(value); 86 | 87 | // Return value if setter 88 | return value; 89 | } 90 | 91 | // Get the value of the property nonreactively 92 | nonreactive(...args) { 93 | const accessor = this.accessor.bind(this, ...args); 94 | 95 | return Tracker.nonreactive(accessor); 96 | } 97 | 98 | 99 | // Factory for Blaze property helpers bound to a key 100 | static helper(key) { 101 | // Helper function 102 | const helper = function (...args) { 103 | const vm = ViewModel.ensureViewmodel(Blaze.getView(), key); 104 | 105 | return vm[key](...args); 106 | }; 107 | 108 | // Mark as viewmodel property helper 109 | helper.isPropertyHelper = true; 110 | 111 | return helper; 112 | } 113 | }; 114 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/lib/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | Private package utility functions 3 | */ 4 | 5 | // Use ES5 property definitions when available 6 | defineProperties = function (obj, props) { 7 | if (_.isFunction(Object.defineProperties)) 8 | Object.defineProperties(obj, props); 9 | else 10 | _.each(props, (prop, key) => obj[key] = prop.value); 11 | }; 12 | 13 | // Get closest template instance for view 14 | templateInstance = function (view) { 15 | // A DOM element may be passed instead of a view 16 | if (_.isElement(view)) 17 | view = Blaze.getView(view); 18 | 19 | if (view) { 20 | do if (view.template && view.name !== "(contentBlock)" && view.name !== "Template.__dynamic" && view.name !== "Template.__dynamicWithDataContext") 21 | return view.templateInstance(); 22 | while (view = view.parentView); 23 | } 24 | 25 | return null; 26 | }; 27 | 28 | // Get the current path, taking FlowRouter into account 29 | // https://github.com/kadirahq/flow-router/issues/293 30 | getPath = function () { 31 | if (typeof FlowRouter !== "undefined") 32 | return FlowRouter.current().path; 33 | 34 | return location.pathname + location.search; 35 | }; 36 | 37 | 38 | /* 39 | Stand-alone versions of jQuery methods (http://youmightnotneedjquery.com/) 40 | */ 41 | 42 | hasClass = function (elem, class_name) { 43 | if (false && elem.classList) 44 | return elem.classList.contains(class_name); 45 | 46 | return elem.className.match(new RegExp("(^|\\s)" + class_name + "(\\s|$)")); 47 | }; 48 | 49 | addClass = function (elem, class_name) { 50 | if (false && elem.classList) 51 | return elem.classList.add(class_name); 52 | 53 | if (!hasClass(elem, class_name)) 54 | elem.className += " " + class_name; 55 | }; 56 | 57 | removeClass = function (elem, class_name) { 58 | if (false && elem.classList) 59 | return elem.classList.remove(class_name); 60 | 61 | if (hasClass(elem, class_name)) { 62 | elem.className = elem.className.replace(new RegExp("(^|\\s)" + class_name + "(\\s|$)", "g"), " "); 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/lib/viewmodel.js: -------------------------------------------------------------------------------- 1 | // Counter for unique ids 2 | let uid = 0; 3 | 4 | // Whether we are in the middle of a hot code push 5 | let is_hcp = true; 6 | 7 | // Whether the bind helper has been registered globally 8 | let is_global = false; 9 | 10 | // ReactiveDict for persistence after hot code push and across re-rendering 11 | const persist = new ReactiveDict("dalgard:viewmodel"); 12 | 13 | 14 | // Exported class for viewmodels 15 | ViewModel = class ViewModel extends Base { 16 | constructor(view, name = view.name, id = ViewModel.uid(), definition, options) { 17 | // Ensure type of arguments 18 | check(id, Match.Integer); 19 | check(definition, Match.Optional(Match.OneOf(Object, Function))); 20 | check(options, Match.Optional(Object)); 21 | 22 | if (!(view.template instanceof Template)) 23 | throw new TypeError("The view passed to ViewModel must be a template view"); 24 | 25 | // Call constructor of Base 26 | super(view, name, options); 27 | 28 | 29 | // Static properties on instance 30 | defineProperties(this, { 31 | // Viewmodel id 32 | _id: { value: id }, 33 | 34 | // List of child viewmodels 35 | _children: { value: new List }, 36 | 37 | // Configuration options 38 | _options: { value: new ReactiveMap(options) }, 39 | }); 40 | 41 | // Attach to template instance 42 | view.templateInstance()[ViewModel.viewmodelKey] = this; 43 | 44 | 45 | // Experimental feature: Add existing Blaze helpers as viewmodel methods that are 46 | // bound to the normal helper context 47 | _.each(view.template.__helpers, (helper, key) => { 48 | if (key.charAt(0) === " " && _.isFunction(helper) && helper !== ViewModel.bindHelper && !helper.isPropertyHelper) { 49 | key = key.slice(1); 50 | 51 | const property = new Property(this, key, function (...args) { 52 | return helper.call(this.getData(), ...args); 53 | }); 54 | 55 | // Save accessor as viewmodel property 56 | this[key] = property.accessor; 57 | } 58 | }); 59 | 60 | // Possibly add properties 61 | if (definition) 62 | this.addProps(definition); 63 | 64 | 65 | // Get parent for non-transcluded viewmodels 66 | const parent = this.parent(); 67 | 68 | // Possibly register with parent 69 | if (parent) 70 | parent.addChild(this); 71 | 72 | // Add to global list 73 | ViewModel.add(this); 74 | 75 | // Tear down viewmodel 76 | this.onDestroyed(function () { 77 | // Remove from global list 78 | ViewModel.remove(this); 79 | 80 | // Possibly remove from parent 81 | if (parent) 82 | parent.removeChild(this); 83 | }); 84 | 85 | 86 | const hash_id = this.hashId(true); 87 | const is_hcp_restore = ViewModel.restoreAfterHCP && is_hcp; 88 | 89 | // Possibly restore viewmodel instance from the last time the template was rendered 90 | // or after a hot code push 91 | if (this.isPersisted() || is_hcp_restore) 92 | this.restore(hash_id); 93 | 94 | // Always save viewmodel state so it can be restored after a hot code push 95 | this.autorun(function (comp) { 96 | // Always register dependencies 97 | const map = this.serialize(); 98 | 99 | // Wait for actual changes to arrive 100 | if (!comp.firstRun) 101 | persist.set(hash_id, map); 102 | }); 103 | 104 | // Remove viewmodel from store if not persisted 105 | this.onDestroyed(function () { 106 | if (!this.isPersisted()) 107 | delete persist.keys[hash_id]; 108 | }); 109 | } 110 | 111 | 112 | // Reactively get or set configuration options 113 | option(key, value) { 114 | // Ensure type of argument 115 | check(key, String); 116 | 117 | // Getter 118 | if (_.isUndefined(value)) 119 | return this._options.get(key); 120 | 121 | this._elem.set(key, value); 122 | 123 | // Return value if setter 124 | return value; 125 | } 126 | 127 | // Add properties to the viewmodel 128 | addProps(definition) { 129 | // Ensure type of argument 130 | check(definition, Match.Optional(Match.OneOf(Object, Function))); 131 | 132 | // Definition may be a factory 133 | if (_.isFunction(definition)) 134 | definition = definition.call(this, this.getData()); 135 | 136 | const is_object = _.isObject(definition); 137 | 138 | if (is_object) { 139 | // Possibly add autoruns 140 | if (definition.autorun) 141 | this.autorun(definition.autorun); 142 | 143 | const template = this.templateInstance().view.template; 144 | 145 | _.each(definition, (init_value, key) => { 146 | if (key !== "autorun") { 147 | const property = new Property(this, key, init_value); 148 | 149 | // Save accessor as viewmodel property 150 | this[key] = property.accessor; 151 | 152 | // Register Blaze helper 153 | template.helpers({ [key]: Property.helper(key) }); 154 | } 155 | }); 156 | } 157 | 158 | return is_object; 159 | } 160 | 161 | // Bind an element programmatically 162 | bind(selector, binding, ...args) { 163 | // Context object for resolving bindings 164 | const context = {}; 165 | 166 | defineProperties(context, { 167 | // Reference to viewmodel 168 | viewmodel: { value: this }, 169 | 170 | // Data context of template instance 171 | data: { value: this.getData() }, 172 | 173 | // Arguments for binding 174 | args: { value: args }, 175 | }); 176 | 177 | // Create binding nexus 178 | new Nexus(this.view, selector, binding, context); 179 | } 180 | 181 | 182 | // Reactively get template instance 183 | templateInstance() { 184 | return this.view.templateInstance(); 185 | } 186 | 187 | // Reactively get the template's data context 188 | getData() { 189 | return this.templateInstance().data; 190 | } 191 | 192 | // Test whether element is in same template instance or delegate to super 193 | test(test) { 194 | // Compare with template instance 195 | if (_.isElement(test)) 196 | return ViewModel.templateInstance(test) === this.templateInstance(); 197 | 198 | return super(test); 199 | } 200 | 201 | 202 | // Get a hash based on the position of the viewmodel in the view hierarchy, 203 | // the index of the viewmodel in relation to sibling viewmodels, and, optionally, 204 | // the current browser location 205 | hashId(use_path) { 206 | const path = use_path ? getPath() : ""; 207 | const parent = this.parent(); 208 | const index = parent ? _.indexOf(parent.children(), this) : ""; 209 | const parent_hash_id = parent ? parent.hashId() : ""; 210 | const view_names = []; 211 | 212 | let view = this.view; 213 | 214 | do view_names.push(view.name); 215 | while (view = view.parentView && !view.templateInstance()[ViewModel.viewmodelKey]); 216 | 217 | return SHA256(path + index + view_names.join("/") + parent_hash_id); 218 | } 219 | 220 | // Reactively get properties for serialization 221 | serialize() { 222 | return _.mapValues(this, prop => prop.get()); 223 | } 224 | 225 | // Restore serialized values 226 | deserialize(map) { 227 | // Ensure type of argument 228 | check(map, Match.Optional(Object)); 229 | 230 | _.each(map, (value, key) => { 231 | // Set value on viewmodel or create missing property with value 232 | if (_.isFunction(this[key])) 233 | this[key].set(value); 234 | else 235 | this.addProps({ [key]: value }); 236 | }); 237 | } 238 | 239 | // Check whether this viewmodel or any ancestor is persisted across re-rendering 240 | isPersisted() { 241 | let persist = this.option("persist"); 242 | 243 | if (!persist) { 244 | const parent = this.parent(); 245 | 246 | persist = parent && parent.isPersisted(); 247 | } 248 | 249 | return persist; 250 | } 251 | 252 | // Restore persisted viewmodel values to instance 253 | restore(hash_id = this.hashId(true)) { 254 | // Ensure type of argument 255 | check(hash_id, String); 256 | 257 | // Get non-reactively 258 | let map = persist.keys[hash_id]; 259 | 260 | if (_.isString(map)) 261 | map = EJSON.parse(map); 262 | 263 | this.deserialize(map); 264 | } 265 | 266 | // Reset all properties to their initial value 267 | reset() { 268 | _.each(this, prop => prop.reset()); 269 | } 270 | 271 | 272 | // Reactively add a child viewmodel to the _children list 273 | addChild(vm) { 274 | // Ensure type of argument 275 | check(vm, ViewModel); 276 | 277 | return this._children.add(vm); 278 | } 279 | 280 | // Reactively remove a child viewmodel from the _children list 281 | removeChild(vm) { 282 | // Ensure type of argument 283 | check(vm, ViewModel); 284 | 285 | return this._children.remove(vm); 286 | } 287 | 288 | // Reactively get a filtered array of child viewmodels 289 | children(...tests) { 290 | return this._children.find(...tests); 291 | } 292 | 293 | // Reactively get the first child or the child at index in a filtered array of 294 | // child viewmodels 295 | child(...args) { 296 | return this._children.findOne(...args); 297 | } 298 | 299 | // Reactively get a filtered array of descendant viewmodels, optionally within 300 | // a depth 301 | descendants(...args) { 302 | // Handle trailing number arguments 303 | const tests = _.dropRightWhile(args, _.isNumber); 304 | const numbers = args.slice(tests.length).slice(-1); 305 | const depth = _.isNumber(numbers[0]) ? numbers.shift() : Infinity; 306 | 307 | let descendants = []; 308 | 309 | if (depth > 0) { 310 | const children = this.children(...tests); 311 | 312 | _.each(children, child => { 313 | descendants.push(child); 314 | 315 | descendants = descendants.concat(child.descendants(depth - 1)); 316 | }); 317 | } 318 | 319 | return descendants; 320 | } 321 | 322 | // Reactively get the first descendant or the descendant at index in a filtered 323 | // array of descendant viewmodels, optionally within a depth 324 | descendant(...args) { 325 | // Handle trailing number arguments 326 | const tests = _.dropRightWhile(args, _.isNumber); 327 | const numbers = args.slice(tests.length).slice(-2); 328 | const index = numbers.shift() || 0; 329 | 330 | // Add depth to the end of tests again 331 | if (_.isNumber(numbers[0])) 332 | tests.push(numbers.shift()); 333 | 334 | // Use slice to allow negative indices 335 | return this.descendants(...tests).slice(index)[0] || null; 336 | } 337 | 338 | // Reactively get the parent viewmodel filtered by tests 339 | parent(...tests) { 340 | // Transcluded viewmodels have no ancestors 341 | if (!this.option("transclude")) { 342 | let parent_view = this.view.parentView; 343 | 344 | do if (parent_view.template) { 345 | const vm = parent_view.templateInstance()[ViewModel.viewmodelKey]; 346 | 347 | // Transcluded viewmodels are taken out of the hierarchy 348 | if (vm && !vm.option("transclude")) { 349 | if (tests.length) { 350 | const is_every = _.every(tests, test => { 351 | if (_.isFunction(test)) 352 | return test(vm); 353 | 354 | return vm.test(test); 355 | }); 356 | 357 | if (!is_every) 358 | return null; 359 | } 360 | 361 | return vm; 362 | } 363 | } 364 | while (parent_view = parent_view.parentView); 365 | } 366 | 367 | return null; 368 | } 369 | 370 | // Reactively get a filtered array of ancestor viewmodels, optionally within 371 | // a depth 372 | ancestors(...args) { 373 | // Handle trailing number arguments 374 | const tests = _.dropRightWhile(args, _.isNumber); 375 | const numbers = args.slice(tests.length).slice(-1); 376 | const depth = _.isNumber(numbers[0]) ? numbers.shift() : Infinity; 377 | 378 | let ancestors = []; 379 | 380 | if (depth > 0) { 381 | const parent = this.parent(...tests); 382 | 383 | if (parent) { 384 | ancestors.push(parent); 385 | 386 | ancestors = ancestors.concat(parent.ancestors(depth - 1)); 387 | } 388 | } 389 | 390 | return ancestors; 391 | } 392 | 393 | // Reactively get the first ancestor or the ancestor at index in a filtered 394 | // array of ancestor viewmodels, optionally within a depth 395 | ancestor(...args) { 396 | // Handle trailing number arguments 397 | const tests = _.dropRightWhile(args, _.isNumber); 398 | const numbers = args.slice(tests.length).slice(-2); 399 | const index = numbers.shift() || 0; 400 | 401 | // Add depth to the end of tests again 402 | if (_.isNumber(numbers[0])) 403 | tests.push(numbers.shift()); 404 | 405 | // Use slice to allow negative indices 406 | return this.ancestors(...tests).slice(index)[0] || null; 407 | } 408 | 409 | 410 | // Get next unique id 411 | static uid() { 412 | return ++uid; 413 | } 414 | 415 | // Add a binding to the global list 416 | static addBinding(...args) { 417 | return Binding.add(...args); 418 | } 419 | 420 | 421 | // Reactively get an array of serialized current viewmodels, optionally filtered by name 422 | static serialize(name) { 423 | const viewmodels = this.find(name); 424 | 425 | return _.map(viewmodels, vm => vm.serialize()); 426 | } 427 | 428 | // Restore an array of serialized values on the current viewmodels, optionally filtered by name 429 | static deserialize(maps, name) { 430 | // Ensure type of argument 431 | check(maps, Array); 432 | 433 | const viewmodels = this.find(name); 434 | 435 | _.each(viewmodels, (vm, index) => vm.deserialize(maps[index])); 436 | } 437 | 438 | 439 | // Ensure existence of a viewmodel with optional property 440 | static ensureViewmodel(view, key) { 441 | // Ensure type of arguments 442 | check(view, Blaze.View); 443 | check(key, Match.Optional(String)); 444 | 445 | const template_instance = templateInstance(view); 446 | 447 | let vm = template_instance[ViewModel.viewmodelKey]; 448 | 449 | // Possibly create new viewmodel instance on view 450 | if (!(vm instanceof ViewModel)) 451 | vm = new ViewModel(template_instance.view); 452 | 453 | // Possibly create missing property on viewmodel 454 | if (_.isString(key) && !_.isFunction(vm[key])) { 455 | // Initialize as undefined 456 | const definition = _.zipObject([key]); 457 | 458 | vm.addProps(definition); 459 | } 460 | 461 | return vm; 462 | } 463 | 464 | // The {{bind}} Blaze helper 465 | static bindHelper(...args) { 466 | const view = Blaze.getView(); 467 | const data = Blaze.getData(); 468 | 469 | // Unique bind id for current element 470 | const bind_id = ViewModel.uid(); 471 | 472 | let hash = args.pop(); // Keyword arguments 473 | let bind_exps = []; 474 | 475 | // Possibly use hash of Spacebars keywords arguments object 476 | if (hash instanceof Spacebars.kw) 477 | hash = hash.hash; 478 | 479 | // Support multiple bind expressions separated by comma 480 | _.each(args, arg => bind_exps = bind_exps.concat(arg.split(/\s*,\s*/g))); 481 | 482 | 483 | // Loop through bind expressions 484 | _.each(bind_exps, exp => { 485 | // Split bind expression at first colon 486 | exp = exp.trim().split(/\s*:\s*/); 487 | 488 | const args = _.isString(exp[1]) ? exp[1].split(/\s+/g) : []; 489 | const selector = "[" + ViewModel.bindAttrName + "='" + bind_id + "']"; 490 | const binding_name = exp[0]; 491 | 492 | // Context object for resolving bindings 493 | const context = {}; 494 | 495 | defineProperties(context, { 496 | // Current data context 497 | data: { value: data }, 498 | 499 | // Space separated strings after the colon in bind expressions 500 | args: { value: args }, 501 | 502 | // Hash object of Spacebars keyword arguments 503 | hash: { value: hash }, 504 | }); 505 | 506 | // Create binding nexus 507 | new Nexus(view, selector, binding_name, context); 508 | }); 509 | 510 | 511 | // Set the dynamic bind id attribute on the element in order to select it after rendering 512 | return { [ViewModel.bindAttrName]: bind_id }; 513 | } 514 | 515 | // Register the bind helper globally 516 | static registerHelper(name = ViewModel.helperName) { 517 | // Ensure type of argument 518 | check(name, String); 519 | 520 | // Global helper 521 | Template.registerHelper(name, ViewModel.bindHelper); 522 | 523 | // Save name 524 | ViewModel.helperName = name; 525 | 526 | // Indicate that the helper has been registered globally 527 | is_global = true; 528 | } 529 | 530 | 531 | // Viewmodel declaration hook 532 | static viewmodelHook(name, definition, options) { 533 | // Must be called in the context of a template 534 | if (!(this instanceof Template)) 535 | throw new TypeError("viewmodelHook must be attached to Template.prototype to work"); 536 | 537 | // Name argument may be omitted 538 | if (_.isObject(name)) 539 | options = definition, definition = name, name = this.viewName; 540 | 541 | // Ensure type of arguments 542 | check(name, String); 543 | check(definition, Match.OneOf(Object, Function)); 544 | check(options, Match.Optional(Object)); 545 | 546 | 547 | // Give all instances of this viewmodel the same id (used when sharing state) 548 | const id = ViewModel.uid(); 549 | 550 | // Create viewmodel instance – a function is added to the template's onCreated 551 | // hook, wherein a viewmodel instance is created on the view with the properties 552 | // from the definition object 553 | this.onCreated(function () { 554 | const template = this.view.template; 555 | 556 | // If the helper hasn't been registered globally 557 | if (!is_global) { 558 | // Register the Blaze bind helper on this template 559 | template.helpers({ 560 | [ViewModel.helperName]: ViewModel.bindHelper, 561 | }); 562 | } 563 | 564 | 565 | // Check existing viewmodel on template instance 566 | const vm = this[ViewModel.viewmodelKey]; 567 | 568 | // Create new viewmodel instance on view or add properties to existing viewmodel 569 | if (!(vm instanceof ViewModel)) { 570 | new ViewModel(this.view, name, id, definition, options); 571 | } 572 | else { 573 | if (name !== this.viewName) 574 | vm.name(name); 575 | 576 | vm.addProps(definition); 577 | } 578 | }); 579 | } 580 | }; 581 | 582 | // Static properties on class 583 | defineProperties(ViewModel, { 584 | // Name of bind helper 585 | helperName: { value: "bind", writable: true, enumerable: true }, 586 | 587 | // Name of attribute used by bind helper 588 | bindAttrName: { value: "vm-bind-id", writable: true, enumerable: true }, 589 | 590 | // Name of bindings reference on views 591 | nexusesKey: { value: "nexuses", writable: true, enumerable: true }, 592 | 593 | // Name of viewmodel reference on template instances 594 | viewmodelKey: { value: "viewmodel", writable: true, enumerable: true }, 595 | 596 | // Whether to try to restore viewmodels in this project after a hot code push 597 | restoreAfterHCP: { value: true, writable: true, enumerable: true }, 598 | 599 | // Utility method 600 | templateInstance: { value: templateInstance }, 601 | 602 | // Nexus class 603 | Nexus: { value: Nexus }, 604 | }); 605 | 606 | // Decorate ViewModel class with static list methods operating on an internal list 607 | List.decorate(ViewModel); 608 | 609 | 610 | /* 611 | Blaze stuff 612 | */ 613 | 614 | // Attach declaration hook to Blaze templates 615 | Template.prototype.viewmodel = ViewModel.viewmodelHook; 616 | 617 | // Hot code push is finished when body is rendered 618 | Template.body.onRendered(() => is_hcp = false); 619 | -------------------------------------------------------------------------------- /packages/dalgard_viewmodel/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: "dalgard:viewmodel", 3 | version: "1.0.2", 4 | summary: "Minimalist VM for Meteor", 5 | git: "https://github.com/dalgard/meteor-viewmodel", 6 | documentation: "../../README.md", 7 | }); 8 | 9 | Package.onUse(function (api) { 10 | api.versionsFrom("METEOR@1.2.0.2"); 11 | 12 | api.use("kadira:flow-router@2.0.0", "client", { weak: true }); 13 | 14 | api.use([ 15 | "ecmascript", 16 | "sha", 17 | "check", 18 | "blaze", 19 | "templating", 20 | "tracker", 21 | "ejson", 22 | "reactive-var", 23 | "reactive-dict", 24 | "stevezhu:lodash@3.10.1", 25 | "dalgard:reactive-map@0.1.0", 26 | ], "client"); 27 | 28 | api.addFiles([ 29 | "lib/utils.js", 30 | "lib/list.js", 31 | "lib/base.js", 32 | "lib/binding.js", 33 | "lib/property.js", 34 | "lib/nexus.js", 35 | "lib/viewmodel.js", 36 | ], "client"); 37 | 38 | api.addFiles([ 39 | "bindings/checked.js", 40 | "bindings/class.js", 41 | "bindings/click.js", 42 | "bindings/disabled.js", 43 | "bindings/enter-key.js", 44 | "bindings/files.js", 45 | "bindings/focused.js", 46 | "bindings/hovered.js", 47 | "bindings/key.js", 48 | "bindings/pikaday.js", 49 | "bindings/radio.js", 50 | "bindings/submit.js", 51 | "bindings/toggle.js", 52 | "bindings/value.js", 53 | ], "client"); 54 | 55 | api.export("ViewModel", "client"); 56 | }); 57 | --------------------------------------------------------------------------------