├── .gitignore ├── .meteor ├── .finished-upgraders ├── .gitignore ├── .id ├── packages ├── platforms ├── release └── versions ├── LICENSE ├── README.md ├── client ├── main.coffee ├── main.css └── main.jade ├── collections └── nodes.coffee ├── package.json ├── packages └── meteor-template2 │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── package.js │ └── src │ ├── _export.coffee │ ├── model-map.coffee │ ├── reactive-object.js │ └── template2.coffee └── server └── main.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | packages/meteor-template-two-way-binding 3 | -------------------------------------------------------------------------------- /.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 | 1.3.0-split-minifiers-package 14 | -------------------------------------------------------------------------------- /.meteor/.gitignore: -------------------------------------------------------------------------------- 1 | local 2 | -------------------------------------------------------------------------------- /.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 | 1ltrk781ii5t2u1jtq24h 8 | -------------------------------------------------------------------------------- /.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 | meteor-base # Packages every Meteor app needs to have 8 | mobile-experience # Packages for a great mobile UX 9 | mongo # The database Meteor supports right now 10 | blaze-html-templates # Compile .html files into Meteor Blaze views 11 | reactive-var # Reactive variable for tracker 12 | jquery # Helpful client-side library 13 | tracker # Meteor's client-side reactive programming library 14 | 15 | standard-minifier-css # CSS minifier run for production mode 16 | standard-minifier-js # JS minifier run for production mode 17 | es5-shim # ECMAScript 5 compatibility for older browsers. 18 | ecmascript # Enable ECMAScript2015+ syntax in app code 19 | 20 | autopublish # Publish all data to the clients (for prototyping) 21 | insecure # Allow all DB writes from clients (for prototyping) 22 | 23 | coffeescript 24 | mquandalle:jade 25 | aldeed:simple-schema 26 | aldeed:collection2 27 | comerc:template2 28 | 29 | # moberegger:validated-template 30 | # voidale:helpers-everywhere 31 | # useful:blaze-state 32 | # ouk:template-destruct 33 | # mpowaga:template-schema 34 | # themeteorites:blaze-magic-events 35 | # useful:forms 36 | # comerc:autoform-fixtures 37 | -------------------------------------------------------------------------------- /.meteor/platforms: -------------------------------------------------------------------------------- 1 | server 2 | browser 3 | -------------------------------------------------------------------------------- /.meteor/release: -------------------------------------------------------------------------------- 1 | METEOR@1.3.2.4 2 | -------------------------------------------------------------------------------- /.meteor/versions: -------------------------------------------------------------------------------- 1 | aldeed:collection2@2.9.1 2 | aldeed:collection2-core@1.1.1 3 | aldeed:schema-deny@1.0.1 4 | aldeed:schema-index@1.0.1 5 | aldeed:simple-schema@1.5.3 6 | allow-deny@1.0.4 7 | autopublish@1.0.7 8 | autoupdate@1.2.9 9 | babel-compiler@6.6.4 10 | babel-runtime@0.1.8 11 | base64@1.0.8 12 | binary-heap@1.0.8 13 | blaze@2.1.7 14 | blaze-html-templates@1.0.4 15 | blaze-tools@1.0.8 16 | boilerplate-generator@1.0.8 17 | caching-compiler@1.0.4 18 | caching-html-compiler@1.0.6 19 | callback-hook@1.0.8 20 | check@1.2.1 21 | coffeescript@1.0.17 22 | comerc:template-two-way-binding@1.6.1 23 | comerc:template2@1.5.3 24 | ddp@1.2.5 25 | ddp-client@1.2.7 26 | ddp-common@1.2.5 27 | ddp-server@1.2.6 28 | deps@1.0.12 29 | diff-sequence@1.0.5 30 | ecmascript@0.4.3 31 | ecmascript-runtime@0.2.10 32 | ejson@1.0.11 33 | es5-shim@4.5.10 34 | fastclick@1.0.11 35 | geojson-utils@1.0.8 36 | hot-code-push@1.0.4 37 | html-tools@1.0.9 38 | htmljs@1.0.9 39 | http@1.1.5 40 | id-map@1.0.7 41 | insecure@1.0.7 42 | jquery@1.11.8 43 | launch-screen@1.0.11 44 | livedata@1.0.18 45 | logging@1.0.12 46 | mdg:validation-error@0.2.0 47 | meteor@1.1.14 48 | meteor-base@1.0.4 49 | minifier-css@1.1.11 50 | minifier-js@1.1.11 51 | minifiers@1.1.7 52 | minimongo@1.0.16 53 | mobile-experience@1.0.4 54 | mobile-status-bar@1.0.12 55 | modules@0.6.1 56 | modules-runtime@0.6.3 57 | mongo@1.1.7 58 | mongo-id@1.0.4 59 | mquandalle:jade@0.4.9 60 | mquandalle:jade-compiler@0.4.5 61 | npm-mongo@1.4.43 62 | observe-sequence@1.0.11 63 | ordered-dict@1.0.7 64 | promise@0.6.7 65 | raix:eventemitter@0.1.3 66 | random@1.0.9 67 | reactive-var@1.0.9 68 | reload@1.1.8 69 | retry@1.0.7 70 | routepolicy@1.0.10 71 | spacebars@1.0.11 72 | spacebars-compiler@1.0.11 73 | standard-minifier-css@1.0.6 74 | standard-minifier-js@1.0.6 75 | templating@1.1.9 76 | templating-tools@1.0.4 77 | tracker@1.0.13 78 | ui@1.0.11 79 | underscore@1.0.8 80 | url@1.0.9 81 | webapp@1.2.8 82 | webapp-hashing@1.0.9 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 comerc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Template2 2 | 3 | `comerc:template2` 4 | 5 | [MVVM](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel) for Meteor with [Two-Way Binding](https://github.com/comerc/meteor-template-two-way-binding) via [Model Schema](https://github.com/aldeed/meteor-simple-schema). 6 | 7 | ## Table of Contents 8 | 9 | 10 | 11 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 12 | 13 | - [Intro](#intro) 14 | - [Key features](#key-features) 15 | - [How to run Demo](#how-to-run-demo) 16 | - [Installation](#installation) 17 | - [Basic Usage](#basic-usage) 18 | - [API](#api) 19 | - [`onCreated`, `onRendered`, `onDestroyed`](#oncreated-onrendered-ondestroyed) 20 | - [`events`, `helpers`](#events-helpers) 21 | - [`propsSchema: { clean: Function, validate: Function }`](#propsschema--clean-function-validate-function-) 22 | - [`modelSchema: { schema: Function, newContext: Function }`](#modelschema--schema-function-newcontext-function-) 23 | - [`states: { myProperty: defaultValue, … }`](#states--myproperty-defaultvalue-%E2%80%A6-) 24 | - [`viewDoc(callback)`](#viewdoccallback) 25 | - [`modelDoc(doc)`](#modeldocdoc) 26 | - [Two-Way Binding features](#two-way-binding-features) 27 | - [Configuration](#configuration) 28 | - [`Template2Config.propsClean`](#template2configpropsclean) 29 | - [`Template2Config.modelClean`](#template2configmodelclean) 30 | - [TODO](#todo) 31 | - [Inspired by](#inspired-by) 32 | - [License](#license) 33 | 34 | 35 | 36 | ## Intro 37 | 38 | Fork of TemplateController ([what difference](https://github.com/meteor-space/template-controller/issues/35)): 39 | 40 | >**Supports the best practices of writing Blaze templates** 41 | > 42 | >**Blaze is awesome** but writing the Js part of templates always 43 | felt a bit awkward. This package just provides a very thin layer of syntactic 44 | sugar on top of the standard API, so you can follow best practices outlined 45 | in the [Blaze guide](http://guide.meteor.com/blaze.html#reusable-components) 46 | more easily. 47 | 48 | **Now you can turn this:** 49 | 50 | ```handlebars 51 | You have clicked the button {{counter}} times. 52 | 53 | ``` 54 | 55 | ```javascript 56 | Template.hello.onCreated(function helloOnCreated() { 57 | // counter starts at 0 58 | this.counter = new ReactiveVar(0); 59 | }); 60 | 61 | Template.hello.helpers({ 62 | counter() { 63 | return Template.instance().counter.get(); 64 | }, 65 | }); 66 | 67 | Template.hello.events({ 68 | 'click button'(event, instance) { 69 | // increment the counter when button is clicked 70 | instance.counter.set(instance.counter.get() + 1); 71 | }, 72 | }); 73 | ``` 74 | 75 | **into that:** 76 | 77 | ```handlebars 78 | You have clicked the button {{state.counter}} times. 79 | 80 | ``` 81 | 82 | ```javascript 83 | Template2('hello', { 84 | states: { 85 | counter: 0 // default value 86 | }, 87 | events: { 88 | 'click button'() { 89 | // increment the counter when button is clicked 90 | this.state.counter += 1; 91 | } 92 | } 93 | }); 94 | ``` 95 | 96 | >Yeah i have used [ViewModel](http://viewmodel.org) before – it's also nice, and there are also [Blaze Components](https://github.com/peerlibrary/meteor-blaze-components). My only "problem" with these existing packages is that they introduce new concepts on top of the standard Blaze api. I just wanted less boilerplate and that best practices like setting up ReactiveVars, validating properties passed to a template or accessing Template.instance() become a no-brainer for the whole team. 97 | > 98 | >The idea for this package came up during a Meteor training with some Devs where realized how complicated it is to explain the best practices with Blaze and that they had a ton of questions like "how can i access the template instance in helpers / event handlers" or "how does a template manage state" – which is so basic that it should be the easiest thing in the world. 99 | 100 | ## Key features 101 | - Compatible with Blaze Template - we love it. 102 | - Minimum changes for migration your great project to Template2. 103 | - One time declaration of variables to Model via `` attribute. 104 | - Validate input data and get doc for writing to Model without coding. 105 | - Support of [SimpleSchema](https://github.com/aldeed/meteor-simple-schema), may be extended for support any other Model ([Astronomy](https://github.com/jagi/meteor-astronomy) etc.) 106 | - Usage of Two-Way Binding Features without Model. 107 | 108 | ## How to run Demo 109 | 110 | ``` 111 | $ git clone https://github.com/comerc/meteor-template2.git 112 | $ cd meteor-template2 113 | $ meteor 114 | ``` 115 | 116 | ...then [http://localhost:3000](http://localhost:3000) 117 | 118 | ## Installation 119 | 120 | In your Meteor app directory, enter: 121 | 122 | ``` 123 | $ meteor add comerc:template2 124 | ``` 125 | 126 | ## Basic Usage 127 | 128 | ``` 129 | $ meteor add aldeed:simple-schema 130 | $ meteor add aldeed:collection2 131 | ``` 132 | 133 | ```javascript 134 | Posts = new Mongo.Collection('posts'); 135 | 136 | PostSchema = new SimpleSchema({ 137 | myValue: { 138 | type: String, 139 | min: 3, 140 | defaultValue: '777' 141 | } 142 | }); 143 | 144 | Posts.attachSchema(PostSchema); 145 | ``` 146 | 147 | ```handlebars 148 | 149 | {{> hello myParam="123"}} 150 | 151 | 152 | 161 | ``` 162 | 163 | ```javascript 164 | Template2('hello', { 165 | // Validate the properties passed to the template from parents 166 | propsSchema: new SimpleSchema({ 167 | param: { type: String } 168 | }), 169 | // Setup Model Schema 170 | modelSchema: Posts.simpleSchema(), 171 | // Setup reactive template states 172 | states: {}, 173 | // Helpers & Events work like before but is always the template instance! 174 | helpers: {}, events: {}, 175 | // Lifecycle callbacks work exactly like with standard Blaze 176 | onCreated() {}, onRendered() {}, onDestroyed() {}, 177 | }); 178 | 179 | // events declaration by old style, but with context by Template.instance() 180 | Template.hello.eventsByInstance({ 181 | 'submit form': function(e) { 182 | e.preventDefault(); 183 | // Get doc after clean and validation for save to model 184 | this.viewDoc(function(error, doc) { 185 | if (error) return; 186 | Posts.insert(doc); 187 | }); 188 | } 189 | }); 190 | 191 | // onRendered declaration by old style may also be used 192 | Template.hello.onRendered(function() { 193 | var self = this; 194 | this.autorun(function() { 195 | var doc = Posts.findOne(); 196 | if (doc) { 197 | // Set doc from model to view 198 | self.modelDoc(doc); 199 | } 200 | }); 201 | }); 202 | ``` 203 | 204 | ## API 205 | 206 | ### `onCreated`, `onRendered`, `onDestroyed` 207 | Work exactly the same as with standard Blaze. 208 | 209 | ### `events`, `helpers` 210 | Work exactly the same as normal but `this` inside the handlers is always 211 | a reference to the `Template.instance()`. In most cases that's what you want 212 | and would expect. You can still access the data context via `this.data`. 213 | 214 | ### `propsSchema: { clean: Function, validate: Function }` 215 | 216 | Any data passed to your component should be validated to avoid UI bugs 217 | that are hard to find. You can pass any object to the `propsSchema` option, which 218 | provides a `clean` and `validate` function. `clean` is called first and can 219 | be used to cleanup the data context before validating it (e.g: adding default 220 | properties, transforming values etc.). `validate` is called directly after 221 | and should throw validation errors if something does not conform the schema. 222 | This api is compatible but not limited to 223 | [SimpleSchema](https://github.com/aldeed/meteor-simple-schema). 224 | 225 | This is a best practice outlined in the 226 | [Blaze guide - validate data context](http://guide.meteor.com/blaze.html#validate-data-context) 227 | section. `Template2` does provide a bit more functionality though: 228 | any property you define in the schema is turned into a template helper 229 | that can be used as a reactive getter, also in the html template: 230 | 231 | ```javascript 232 | Template2('hello', { 233 | propsSchema: new SimpleSchema({ 234 | messageCount: { 235 | type: Number, // allows only integers! 236 | defaultValue: 0 237 | } 238 | }) 239 | }); 240 | ``` 241 | 242 | ```handlebars 243 | 246 | ``` 247 | 248 | … and you can access the value of `messageCount` anywhere in helpers etc. with 249 | `this.props.messageCount` 250 | 251 | a parent template can provide the `messageCount` prop with standard Blaze: 252 | ```handlebars 253 | 256 | ``` 257 | 258 | If the parent passes down anything else than an integer value for `messageCount` 259 | our component will throw a nice validation error. 260 | 261 | ### `modelSchema: { schema: Function, newContext: Function }` 262 | 263 | You can pass any object to the `modelSchema` option, which provides a `schema` and `newContext` function. 264 | 265 | This api is compatible but not limited to 266 | [SimpleSchema](https://github.com/aldeed/meteor-simple-schema). 267 | 268 | ### `states: { myProperty: defaultValue, … }` 269 | 270 | Each state property you define is turned into a `ReactiveVar` and you can get 271 | the value with `this.state.myProperty` and set it like a normal property 272 | `this.state.myProperty = newValue`. The reason why we are not using 273 | `ReactiveVar` directly is simple: we need a template helper to render it in 274 | our html template! So `Template2` actually adds a `state` template 275 | helper which returns `this.state` and thus you can render any state var in 276 | your templates like this: 277 | 278 | ```handlebars 279 | You have clicked the button {{state.counter}} times. 280 | ``` 281 | 282 | But you can also modify the state var easily in your Js code like this: 283 | 284 | ```javascript 285 | events: { 286 | 'click button'() { 287 | this.state.counter += 1; 288 | } 289 | } 290 | ``` 291 | 292 | Since each state var is turned into a separate reactive var you do not run 293 | into any reactivity issues with re-rendering too much portions of your template. 294 | 295 | ### `viewDoc(callback)` 296 | 297 | Get doc via callback after clean and validation for save to model. 298 | 299 | ```javascript 300 | var t = Template.instance(); 301 | t.viewDoc(function(error, doc) { 302 | if (error) return; 303 | Posts.insert(doc); 304 | }); 305 | ``` 306 | 307 | ### `modelDoc(doc)` 308 | 309 | Set doc from model to view. 310 | 311 | ```javascript 312 | var t = Template.instance(); 313 | var doc = Posts.findOne(); 314 | t.modelDoc(doc); 315 | ``` 316 | 317 | ## Two-Way Binding features 318 | 319 | Described [here](https://github.com/comerc/meteor-template-two-way-binding). 320 | 321 | ## Configuration 322 | 323 | ### `Template2Config.propsClean` 324 | Enables you to configure the props cleaning operation of libs like SimpleSchema. Checkout [SimpleSchema clean docs](https://github.com/aldeed/meteor-simple-schema#cleaning-data) to 325 | see your options. 326 | 327 | Here is one example why `removeEmptyStrings: true` is the default config: 328 | 329 | ```handlebars 330 | {{> button label=(i18n 'button_label') }} 331 | ``` 332 | `i18n` might initially return an empty string for your translation. 333 | This would cause an validation error because SimpleSchema removes empty strings by default when cleaning the data. 334 | 335 | ### `Template2Config.modelClean` 336 | 337 | The same as [previos](https://github.com/comerc/meteor-template2#template2configpropsclean), but for model. 338 | 339 | ## TODO 340 | - [ ] AstronomySchema (for compatible with SimpleSchema) 341 | - [ ] Add `original` and `error`* helpers, like [useful:forms](https://github.com/usefulio/forms) 342 | - [ ] Wait for throttle & debounce before form submit 343 | - [ ] Demo with [meteor7](https://github.com/daveeel/meteor7) 344 | - [ ] Actions, like [blaze-magic-events](https://github.com/themeteorites/blaze-magic-events) 345 | - [ ] Remove underscore dependence 346 | - [x] [Demo](https://github.com/comerc/meteor-mvvm-mdl-demo) with [Material Design Lite](https://getmdl.io/) 347 | - [x] Remove helpers `propsOf` and `stateOf`, we may use `{{state.[field-name]}}` 348 | - [ ] [Dependencies](https://github.com/ermouth/jQuery.my/#dependencies) 349 | - [ ] [Conditional formatting and disabling](https://github.com/ermouth/jQuery.my/#conditional-formatting-and-disabling) 350 | 351 | ## Change Log 352 | ### 1.5.0 353 | - `Template2.mixin()` rename to `Template2()` 354 | - `Template2.setPropsCleanConfiguration(Object)` rename to `Template2Config.propsClean` 355 | - `Template2.setModelCleanConfiguration(Object)` rename to `Template2Config.modelClean` 356 | 357 | ## Inspired by 358 | 359 | - [jQuery.my](https://github.com/ermouth/jQuery.my) 360 | - [Aurelia](http://aurelia.io/) 361 | - [Vue](https://vuejs.org/guide/#Two-way-Binding) 362 | - [ReactLink](https://facebook.github.io/react/docs/two-way-binding-helpers.html) 363 | - [Blaze](https://github.com/meteor/meteor/tree/devel/packages/blaze) 364 | - [aldeed:autoform](https://github.com/aldeed/meteor-autoform) 365 | - [manuel:viewmodel](https://github.com/ManuelDeLeon/viewmodel) 366 | - [nov1n:reactive-bind](https://github.com/nov1n/reactive-bind) 367 | - [space:template-controller](https://github.com/meteor-space/template-controller) 368 | - [themeteorites:blaze-magic-events](https://github.com/themeteorites/blaze-magic-events) 369 | - [moberegger:validated-template](https://github.com/MichaelOber/validated-template) 370 | - [voidale:helpers-everywhere](https://github.com/voidale/meteor-helpers-everywhere) 371 | - [useful:blaze-state](https://github.com/usefulio/blaze-state) 372 | - [useful:forms](https://github.com/usefulio/forms) 373 | - [ouk:template-destruct](https://github.com/andrejsm/meteor-template-destruct) 374 | - [mpowaga:template-schema](https://github.com/mpowaga/meteor-template-schema) 375 | - [peerlibrary:blaze-components](https://github.com/peerlibrary/meteor-blaze-components) 376 | - [aldeed:template-extension](https://github.com/aldeed/meteor-template-extension) 377 | - [meteorhacks:flow-components](https://github.com/meteorhacks/flow-components) 378 | - [kadira:blaze-plus](https://github.com/kadirahq/blaze-plus) 379 | - [templates:forms](https://github.com/meteortemplates/forms) 380 | 381 | 384 | 385 | ## License 386 | MIT 387 | -------------------------------------------------------------------------------- /client/main.coffee: -------------------------------------------------------------------------------- 1 | { Template } = require 'meteor/templating' 2 | 3 | require './main.jade' 4 | 5 | Template2 'hello', 6 | propsSchema: new SimpleSchema(test: type: String) 7 | modelSchema: Nodes.simpleSchema() 8 | states: 9 | nodeId: false 10 | submitMessage: '' 11 | helpers: 12 | nodes: -> 13 | Nodes.find() 14 | events: 15 | 'click a.node': (e) -> 16 | e.preventDefault() 17 | @state.nodeId = $(e.target).data('node-id') or false 18 | 'submit form': (e) -> 19 | e.preventDefault() 20 | @viewDoc (error, doc) -> 21 | return console.log error.message if error 22 | # save data 23 | if @state.nodeId 24 | Nodes.update @state.nodeId, $set: doc, 25 | => @state.submitMessage = 'updated' 26 | else 27 | @state.nodeId = Nodes.insert doc, 28 | => @state.submitMessage = 'inserted' 29 | 30 | Template.hello.onRendered -> 31 | @autorun => 32 | if @state.nodeId 33 | @modelDoc Nodes.findOne @state.nodeId 34 | else 35 | @modelDoc false 36 | @state.submitMessage = '' 37 | 38 | Template.hello.eventsByInstance 39 | 'click #reset': (e) -> 40 | e.preventDefault() 41 | @modelDoc false 42 | @state.submitMessage = '' 43 | -------------------------------------------------------------------------------- /client/main.css: -------------------------------------------------------------------------------- 1 | /* CSS declarations go here */ 2 | -------------------------------------------------------------------------------- /client/main.jade: -------------------------------------------------------------------------------- 1 | head 2 | title simple 3 | 4 | body 5 | +hello(test="123") 6 | +info 7 | 8 | template(name="hello") 9 | h1 Welcome to Template2! 10 | div props: 11 | = props.test 12 | div 13 | a.node(href="#") NEW 14 | form 15 | input(type="text" value-bind="fieldHead@throttle:500") 16 | = state.fieldHead 17 | input(type="text" value-bind="fieldBody@debounce:500") 18 | = state.fieldBody 19 | button(type="submit") SUBMIT 20 | button#reset RESET 21 | = state.submitMessage 22 | = state.errorMessages 23 | ul 24 | +each node in nodes 25 | li 26 | a.node(href="#" data-node-id=node._id) 27 | = node.fieldHead 28 | | / 29 | = node.fieldBody 30 | 31 | template(name='info') 32 | h2 See also 33 | a(href='https://github.com/comerc/meteor-mvvm-mdl-demo') Demo 34 | | with  35 | a(href='https://getmdl.io/') Material Design Lite 36 | -------------------------------------------------------------------------------- /collections/nodes.coffee: -------------------------------------------------------------------------------- 1 | # { Random } = require 'meteor/random' 2 | 3 | @Nodes = new (Mongo.Collection)('nodes') 4 | 5 | @NodeSchema = new SimpleSchema 6 | fieldHead: 7 | type: String 8 | label: 'My Head' 9 | max: 3 10 | # defaultValue: '111' 11 | fieldBody: 12 | type: String 13 | label: 'My Body' 14 | min: 3 15 | defaultValue: '777' 16 | 17 | Nodes.attachSchema(NodeSchema) 18 | 19 | # data = fieldHead: '123', fieldBody: '321' 20 | # id = Random.id() 21 | # console.log data 22 | # 23 | # Nodes.update id, $set: data, 24 | # upsert: true 25 | # console.log Nodes.find().count() 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meteor-template2", 3 | "private": true, 4 | "scripts": { 5 | "start": "meteor run" 6 | }, 7 | "dependencies": { 8 | "meteor-node-stubs": "^0.2.3" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/meteor-template2/.gitignore: -------------------------------------------------------------------------------- 1 | .meteor/local 2 | .meteor/meteorite 3 | -------------------------------------------------------------------------------- /packages/meteor-template2/LICENSE: -------------------------------------------------------------------------------- 1 | ../../LICENSE -------------------------------------------------------------------------------- /packages/meteor-template2/README.md: -------------------------------------------------------------------------------- 1 | ../../README.md -------------------------------------------------------------------------------- /packages/meteor-template2/package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | summary: 'MVVM for Meteor with Two-Way Binding via Model Schema', 3 | name: 'comerc:template2', 4 | version: '1.5.4', 5 | git: 'https://github.com/comerc/meteor-template2.git' 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | 10 | // Have to stay on Meteor 1.2.1 to be compatible with all Meteor versions. 11 | api.versionsFrom('1.2.1'); 12 | 13 | api.use([ 14 | 'coffeescript', 15 | 'underscore', 16 | 'ecmascript', 17 | 'reactive-var', 18 | 'templating', 19 | 'blaze-html-templates', 20 | 'comerc:template-two-way-binding@1.6.1' 21 | ], 'client'); 22 | 23 | api.addFiles([ 24 | 'src/_export.coffee', 25 | 'src/reactive-object.js', 26 | 'src/template2.coffee', 27 | 'src/model-map.coffee' 28 | ], 'client'); 29 | 30 | api.export('Template2', 'client'); 31 | api.export('Template2Config', 'client'); 32 | }); 33 | -------------------------------------------------------------------------------- /packages/meteor-template2/src/_export.coffee: -------------------------------------------------------------------------------- 1 | Template2Config = {} 2 | -------------------------------------------------------------------------------- /packages/meteor-template2/src/model-map.coffee: -------------------------------------------------------------------------------- 1 | # for custom override 2 | Blaze.TemplateInstance.prototype.validateOne = (doc, variable) -> 3 | result = @validationContext.validateOne(doc, variable) 4 | @state.errorMessages = _.map @validationContext.getErrorObject().invalidKeys, (error) -> 5 | return " #{error.message}" 6 | return result 7 | 8 | # for custom override 9 | Blaze.TemplateInstance.prototype.validate = (doc) -> 10 | result = @validationContext.validate(doc) 11 | if result 12 | @state.errorMessages = [] 13 | else 14 | @state.errorMessages = _.map @validationContext.getErrorObject().invalidKeys, (error) -> 15 | return " #{error.message}" 16 | return result 17 | 18 | checkModelSchema = (templateInstance) -> 19 | if not templateInstance.__modelSchema 20 | error = new Error 'Model Schema Required' 21 | error.name = 'ModelSchemaRequired' 22 | throw error 23 | 24 | Blaze.TemplateInstance.prototype.modelSchema = (schema, validationContext) -> 25 | if not schema 26 | throw new Error 'property schema required' 27 | @__modelSchema = schema 28 | if validationContext 29 | @validationContext = validationContext 30 | else 31 | @validationContext = schema.newContext() 32 | states = {} 33 | for variable, field of @__modelSchema.schema() 34 | states[variable] = field.defaultValue or '' 35 | states['errorMessages'] = [] 36 | @states(states) 37 | return @ 38 | 39 | Blaze.TemplateInstance.prototype.modelDoc = (doc) -> 40 | checkModelSchema @ 41 | @validationContext.resetValidation() 42 | @state.errorMessages = [] 43 | if doc 44 | for variable of @__modelSchema.schema() 45 | @state[variable] = if doc[variable] == 'undefined' then '' else doc[variable] 46 | else 47 | for variable, field of @__modelSchema.schema() 48 | @state[variable] = field.defaultValue or '' 49 | return @ 50 | 51 | Blaze.TemplateInstance.prototype.viewDoc = (callback) -> 52 | checkModelSchema @ 53 | error = null 54 | doc = {} 55 | for variable of @__modelSchema.schema() 56 | doc[variable] = @state[variable] 57 | @__modelSchema.clean doc, Template2Config.modelClean 58 | if not @validate(doc) 59 | error = new Error 'Validation Error' 60 | error.name = 'ValidationError' 61 | doc = null 62 | callback.call @, error, doc 63 | return @ 64 | 65 | TemplateTwoWayBinding.getter = (variable) -> 66 | return @state[variable] 67 | 68 | TemplateTwoWayBinding.setter = (variable, value) -> 69 | if @__modelSchema # we may use TemplateTwoWayBinding wo model 70 | doc = {} 71 | doc[variable] = value 72 | @validateOne.call @, doc, variable 73 | @state[variable] = value 74 | return 75 | 76 | # oldConstructView = Template.prototype.constructView 77 | # 78 | # Blaze.Template.prototype.constructView = -> 79 | # 80 | # return oldConstructView.apply @, arguments 81 | -------------------------------------------------------------------------------- /packages/meteor-template2/src/reactive-object.js: -------------------------------------------------------------------------------- 1 | ReactiveObject = class ReactiveObject { 2 | constructor(properties = {}) { 3 | this.addProperties(properties); 4 | } 5 | addProperty(key, defaultValue = null) { 6 | const property = new ReactiveVar(defaultValue); 7 | Object.defineProperty(this, key, { 8 | get: () => { return property.get(); }, 9 | set: (value) => { property.set(value); } 10 | }); 11 | } 12 | addProperties(properties = {}) { 13 | for (let key of Object.keys(properties)) { 14 | this.addProperty(key, properties[key]); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/meteor-template2/src/template2.coffee: -------------------------------------------------------------------------------- 1 | bindToTemplateInstance = (handler) -> 2 | return -> 3 | handler.apply Template.instance(), arguments 4 | 5 | bindAllToTemplateInstance = (handlers) -> 6 | for key of handlers 7 | handlers[key] = bindToTemplateInstance(handlers[key]); 8 | return handlers 9 | 10 | propertyValidatorRequired = -> 11 | error = new Error(' must be a validator with #clean and #validate methods (see: SimpleSchema)') 12 | error.name = 'PropertyValidatorRequired' 13 | return error 14 | 15 | propertyValidationError = (error, templateName) -> 16 | error.name = 'PropertyValidationError' 17 | error.message = "in <#{templateName}> #{error.message}" 18 | return error 19 | 20 | # Init props 21 | Blaze.TemplateInstance.prototype.propsSchema = (schema) -> 22 | # Setup validated reactive props passed from the outside 23 | if not (schema and schema.validate and schema.clean) 24 | throw propertyValidatorRequired() 25 | @__propsSchema = schema 26 | if not @props 27 | @props = new ReactiveObject 28 | helpers = {} 29 | helpers.props = -> 30 | Template.instance().props 31 | @view.template.helpers helpers 32 | @autorun => 33 | currentData = Template.currentData() or {} 34 | @__propsSchema.clean currentData, Template2Config.propsClean 35 | try 36 | @__propsSchema.validate currentData 37 | catch error 38 | throw propertyValidationError(error, @view.name) 39 | for key, value of currentData 40 | if @props.hasOwnProperty(key) 41 | @props[key] = value 42 | else 43 | @props.addProperty key, value 44 | return @ 45 | 46 | # Init state 47 | Blaze.TemplateInstance.prototype.states = (states) -> 48 | if not states 49 | throw new Error('property states required') 50 | if not @state 51 | @state = new ReactiveObject 52 | helpers = {} 53 | helpers.state = -> 54 | Template.instance().state 55 | @view.template.helpers helpers 56 | for key, value of states 57 | if @state.hasOwnProperty(key) 58 | @state[key] = value 59 | else 60 | @state.addProperty key, value 61 | return @ 62 | 63 | # Init actions 64 | Blaze.TemplateInstance.prototype.actions = (actions) -> 65 | # not implemented yet 66 | return @ 67 | 68 | Blaze.TemplateInstance.prototype.helpers = (helpers) -> 69 | @view.template.helpers bindAllToTemplateInstance(helpers) 70 | return @ 71 | 72 | Blaze.TemplateInstance.prototype.events = (eventMap) -> 73 | @view.template.events bindAllToTemplateInstance(eventMap) 74 | return @ 75 | 76 | Blaze.Template.prototype.helpersByInstance = (helpers) -> 77 | @helpers bindAllToTemplateInstance(helpers) 78 | return 79 | 80 | Blaze.Template.prototype.eventsByInstance = (eventMap) -> 81 | @events bindAllToTemplateInstance(eventMap) 82 | return 83 | 84 | Template2 = (template, config) -> 85 | if template instanceof Blaze.Template 86 | else 87 | if typeof template is 'string' 88 | template = Blaze.Template[template] 89 | else 90 | throw new Error 'template not found' 91 | template.onCreated -> 92 | @propsSchema config.propsSchema if config.propsSchema 93 | @modelSchema config.modelSchema if config.modelSchema 94 | @states config.states if config.states 95 | # @actions config.actions if config.actions 96 | return 97 | template.helpers bindAllToTemplateInstance(config.helpers) if config.helpers 98 | template.events bindAllToTemplateInstance(config.events) if config.events 99 | template.onCreated config.onCreated if config.onCreated 100 | template.onRendered config.onRendered if config.onRendered 101 | template.onRendered -> 102 | TemplateTwoWayBinding.rendered @ 103 | return 104 | # XXX then Template.hello.onRendered -> 105 | template.onDestroyed config.onDestroyed if config.onDestroyed 106 | -------------------------------------------------------------------------------- /server/main.js: -------------------------------------------------------------------------------- 1 | import { Meteor } from 'meteor/meteor'; 2 | 3 | Meteor.startup(() => { 4 | // code to run on server at startup 5 | }); 6 | --------------------------------------------------------------------------------