├── .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 | {{text}} {{myFieldValue}}
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 |
102 |
103 |
104 |
105 | {{#if show}}
106 |
All examples
4 | 5 |childValue: {{childValue}}
3 | 4 | {{destroy}} 5 | 6 | {{#unless destroy}} 7 |{{> fullValue}}
8 |{{> fullThrottled}}
9 |{{> fullStartValue startValue='yo'}}
10 |{{> fullDate}}
11 |{{> fullChecked}}
12 |{{> fullClick}}
13 |{{> fullToggle}}
14 |{{> fullDisabled}}
15 |{{> fullFocused}}
16 |{{> fullHovered}}
17 |{{> fullEnterKey}}
18 |{{> fullKey}}
19 |{{> fullValue}}
20 |{{> fullFiles}}
21 |{{> fullTextarea}}
22 |{{> fullSelect}}
23 |{{> fullRadio}}
24 |{{> fullClass}}
25 | {{/unless}} 26 | 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 | 2 | 3 | 4 | {{#if checked}} 5 | 6 | {{else}} 7 | {{value}} 8 | {{/if}} 9 | 10 | -------------------------------------------------------------------------------- /examples/minimalist/client/views/bindings/disabled.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{value}} 5 | 6 | -------------------------------------------------------------------------------- /examples/minimalist/client/views/bindings/files.html: -------------------------------------------------------------------------------- 1 | 2 | count: {{files.length}} 3 | 4 | -------------------------------------------------------------------------------- /examples/minimalist/client/views/bindings/focused.html: -------------------------------------------------------------------------------- 1 | 2 | {{focused}} 3 | 4 | -------------------------------------------------------------------------------- /examples/minimalist/client/views/bindings/hovered.html: -------------------------------------------------------------------------------- 1 | 2 | {{hovered}} 3 | 4 | -------------------------------------------------------------------------------- /examples/minimalist/client/views/bindings/toggle.html: -------------------------------------------------------------------------------- 1 | 2 | {{toggled}} 3 | 4 | -------------------------------------------------------------------------------- /examples/minimalist/client/views/bindings/value.html: -------------------------------------------------------------------------------- 1 | 2 | {{value}} 3 | 4 | -------------------------------------------------------------------------------- /examples/minimalist/client/views/minimalist.html: -------------------------------------------------------------------------------- 1 | 2 |{{> minimalistValue}}
3 |{{> minimalistValue throttle=1500}}
4 |{{> minimalistChecked}}
5 |{{> minimalistToggle}}
6 |{{> minimalistDisabled}}
7 |{{> minimalistFiles}}
8 |{{> minimalistFocused}}
9 |{{> minimalistHovered}}
10 | 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 | 2 |Date: {{date}}
3 | 4 |{{text}}
7 | 8 | {{/if}} 9 | 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 | 2 | 3 | 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 | 2 |{{myFieldValue}}
3 | 4 | {{> usageField startValue='Hello world'}} 5 | 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 | --------------------------------------------------------------------------------