diff --git a/.mailmap b/.mailmap index efd15a1605..705934494b 100644 --- a/.mailmap +++ b/.mailmap @@ -27,6 +27,7 @@ GITHUB: codeinthehole GITHUB: dandv GITHUB: davegonzalez GITHUB: ducdigital +GITHUB: duckspeaker GITHUB: emgee3 GITHUB: felixrabe GITHUB: FredericoC @@ -75,3 +76,4 @@ METEOR: sixolet METEOR: Slava METEOR: stubailo METEOR: ekatek +METEOR: mariapacana diff --git a/History.md b/History.md index 6856a8c0dd..0c02250709 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,4 @@ -## v.NEXT.NEXT +## v.NEXT * The `appcache` package now defaults to functioning on all browsers that support the AppCache API, rather than a whitelist of browsers. You can still @@ -6,16 +6,90 @@ change is that `appcache` is now enabled by default on Firefox, because Firefox no longer makes a confusing popup. #2241 +* When a call to `match` fails in a method or subscription, log the + failure on the server. (This matches the behavior described in our docs) -## v.NEXT +## v0.8.3 + +#### Blaze + +* Refactor Blaze to simplify internals while preserving the public + API. `UI.Component` has been replaced with `Blaze.View.` + +* Fix performance issues and memory leaks concerning event handlers. + +* Add `UI.remove`, which removes a template after `UI.render`/`UI.insert`. + +* Add `this.autorun` to the template instance, which is like `Deps.autorun` + but is automatically stopped when the template is destroyed. + +* Create `` tags as SVG elements when they have `xlink:href` + attributes. (Previously, `` tags inside SVGs were never created as + SVG elements.) #2178 + +* Throw an error in `{{foo bar}}` if `foo` is missing or not a function. + +* Cursors returned from template helpers for #each should implement + the `observeChanges` method and don't have to be Minimongo cursors + (allowing new custom data stores for Blaze like Miniredis). + +* Remove warnings when {{#each}} iterates over a list of strings, + numbers, or other items that contains duplicates. #1980 + +#### Meteor Accounts + +* Fix regression in 0.8.2 where an exception would be thrown if + `Meteor.loginWithPassword` didn't have a callback. Callbacks to + `Meteor.loginWithPassword` are now optional again. #2255 + +* Fix OAuth popup flow in mobile apps that don't support + `window.opener`. #2302 + +* Fix "Email already exists" error with MongoDB 2.6. #2238 + + +#### mongo-livedata and minimongo * Fix performance issue where a large batch of oplog updates could block the node event loop for long periods. #2299. +* Fix oplog bug resulting in error message "Buffer inexplicably empty". #2274 + +* Fix regression from 0.8.2 that caused collections to appear empty in + reactive `findOne()` or `fetch` queries that run before a mutator + returns. #2275 + + +#### Miscellaneous + +* Stop including code by default that automatically refreshes the page + if JavaScript and CSS don't load correctly. While this code is useful + in some multi-server deployments, it can cause infinite refresh loops + if there are errors on the page. Add the `reload-safetybelt` package + to your app if you want to include this code. + +* On the server, `Meteor.startup(c)` now calls `c` immediately if the + server has already started up, matching the client behavior. #2239 + +* Add support for server-side source maps when debugging with + `node-inspector`. + +* Add `WebAppInternals.addStaticJs()` for adding static JavaScript code + to be served in the app, inline if allowed by `browser-policy`. + +* Make the `tinytest/run` method return immediately, so that `wait` + method calls from client tests don't block on server tests completing. + +* Log errors from method invocations on the client if there is no + callback provided. + * Upgraded dependencies: + - node: 0.10.29 (from 0.10.28) - less: 1.7.1 (from 1.6.1) +Patches contributed by GitHub users Cangit, cmather, duckspeaker, zol. + ## v0.8.2 diff --git a/docs/.meteor/release b/docs/.meteor/release index 100435be13..ee94dd834b 100644 --- a/docs/.meteor/release +++ b/docs/.meteor/release @@ -1 +1 @@ -0.8.2 +0.8.3 diff --git a/docs/client/api.html b/docs/client/api.html index 5e8a7b30f6..f1ac161bb2 100644 --- a/docs/client/api.html +++ b/docs/client/api.html @@ -2189,6 +2189,12 @@ This property provides access to the data context at the top level of the template. It is updated each time the template is re-rendered. Access is read-only and non-reactive. +{{> api_box template_autorun}} + +You can use `this.autorun` from a [`created`](#template_created) or +[`rendered`](#template_rendered) callback to reactively update the DOM +or the template instance. The Computation is automatically stopped +when the template is destroyed.

Template utilities

@@ -2205,6 +2211,15 @@ any part of the DOM for finer control than just using template inclusions. You can define helpers and event maps on `UI.body` just like on any `Template.myTemplate` object. +Helpers on `UI.body` are only available in the `` tags of your +app. To register a global helper, use +[UI.registerHelper](#ui_registerhelper). + +Event maps on `UI.body` don't apply to elements added to the body via +`UI.insert`, jQuery, or the DOM API, or to the body element itself. +To handle events on the body, window, or document, use jQuery or the +DOM API. + {{> api_box ui_render}} This returns an "rendered template" object, which can be passed to @@ -2238,7 +2253,15 @@ changes. {{> api_box ui_getelementdata}} +{{> api_box ui_dynamic}} +`UI.dynamic` allows you to include a template by name, where the name +may be calculated by a helper and may change reactively. The `data` +argument is optional, and if it is omitted, the current data context +is used. + +For example, if there is a template named "foo", `{{dstache}}> UI.dynamic +template="foo"}}` is equivalent to `{{dstache}}> foo}}`. {{#api_box eventmaps}} @@ -2281,8 +2304,8 @@ Example: // Fires when any element with the 'accept' class is clicked 'click .accept': function (event) { ... }, - // Fires when 'accept' is clicked, or a key is pressed - 'keydown, click .accept': function (event) { ... } + // Fires when 'accept' is clicked or focused, or a key is pressed + 'click .accept, focus .accept, keypress': function (event) { ... } } Most events bubble up the document tree from their originating diff --git a/docs/client/api.js b/docs/client/api.js index a4c9a3a828..cc49d6fd32 100644 --- a/docs/client/api.js +++ b/docs/client/api.js @@ -1186,6 +1186,11 @@ Template.api.accounts_ui_config = { type: "Object", descr: "To ask the user for permission to act on their behalf when offline, map the relevant external service to `true`. Currently only supported with Google. See [Meteor.loginWithExternalService](#meteor_loginwithexternalservice) for more details." }, + { + name: "forceApprovalPrompt", + type: "Boolean", + descr: "If true, forces the user to approve the app's permissions, even if previously approved. Currently only supported with Google." + }, { name: "passwordSignupFields", type: "String", @@ -1845,6 +1850,17 @@ Template.api.template_data = { descr: ["The data context of this instance's latest invocation."] }; +Template.api.template_autorun = { + id: "template_autorun", + name: "this.autorun(runFunc)", + locus: "Client", + descr: ["A version of [Deps.autorun](#deps_autorun) that is stopped when the template is destroyed."], + args: [ + {name: "runFunc", + type: "Function", + descr: "The function to run. It receives one argument: a Deps.Computation object."} + ] +}; Template.api.ui_registerhelper = { id: "ui_registerhelper", @@ -1862,6 +1878,22 @@ Template.api.ui_registerhelper = { }] }; +Template.api.ui_dynamic = { + id: "ui_dynamic", + name: "{{> UI.dynamic template=templateName [data=dataContext]}}", + locus: "Client", + descr: ["Choose a template to include dynamically, by name."], + args: [ + {name: "templateName", + type: "String", + descr: "The name of the template to include." + }, + {name: "dataContext", + type: "Object", + descr: "Optional. The data context in which to include the template." + }] +}; + Template.api.ui_body = { id: "ui_body", name: "UI.body", diff --git a/docs/client/concepts.html b/docs/client/concepts.html index 1ef9bbfd4b..ea2f5809e6 100644 --- a/docs/client/concepts.html +++ b/docs/client/concepts.html @@ -33,7 +33,7 @@ packages that most any app will use (for example `webapp`, which handles incoming HTTP connections, and `templating`, which lets you make HTML templates that automatically update live as data changes). Then there are optional packages like `email`, which lets your app -send emails, or the Meteor Accounts series (`account-password`, +send emails, or the Meteor Accounts series (`accounts-password`, `accounts-facebook`, `accounts-ui`, and others) which provide a full-featured user account system that you can drop right into your app. And beyond these "official" packages, there are hundreds of diff --git a/docs/client/docs.js b/docs/client/docs.js index bb3fc8eec8..f76df144bf 100644 --- a/docs/client/docs.js +++ b/docs/client/docs.js @@ -253,7 +253,8 @@ var toc = [ {instance: "this", name: "find", id: "template_find"}, {instance: "this", name: "firstNode", id: "template_firstNode"}, {instance: "this", name: "lastNode", id: "template_lastNode"}, - {instance: "this", name: "data", id: "template_data"} + {instance: "this", name: "data", id: "template_data"}, + {instance: "this", name: "autorun", id: "template_autorun"} ], "UI", [ "UI.registerHelper", @@ -262,7 +263,8 @@ var toc = [ "UI.renderWithData", "UI.insert", "UI.remove", - "UI.getElementData" + "UI.getElementData", + {name: "{{> UI.dynamic}}", id: "ui_dynamic"} ], {type: "spacer"}, {name: "Event maps", style: "noncode"} diff --git a/docs/lib/release-override.js b/docs/lib/release-override.js index d0fc420710..6bfbd0e614 100644 --- a/docs/lib/release-override.js +++ b/docs/lib/release-override.js @@ -1,5 +1,5 @@ // While galaxy apps are on their own special meteor releases, override // Meteor.release here. if (Meteor.isClient) { - Meteor.release = Meteor.release ? "0.8.2" : undefined; + Meteor.release = Meteor.release ? "0.8.3" : undefined; } diff --git a/examples/clock/.meteor/release b/examples/clock/.meteor/release index 100435be13..ee94dd834b 100644 --- a/examples/clock/.meteor/release +++ b/examples/clock/.meteor/release @@ -1 +1 @@ -0.8.2 +0.8.3 diff --git a/examples/leaderboard/.meteor/release b/examples/leaderboard/.meteor/release index 100435be13..ee94dd834b 100644 --- a/examples/leaderboard/.meteor/release +++ b/examples/leaderboard/.meteor/release @@ -1 +1 @@ -0.8.2 +0.8.3 diff --git a/examples/parties/.meteor/release b/examples/parties/.meteor/release index 100435be13..ee94dd834b 100644 --- a/examples/parties/.meteor/release +++ b/examples/parties/.meteor/release @@ -1 +1 @@ -0.8.2 +0.8.3 diff --git a/examples/todos/.meteor/release b/examples/todos/.meteor/release index 100435be13..ee94dd834b 100644 --- a/examples/todos/.meteor/release +++ b/examples/todos/.meteor/release @@ -1 +1 @@ -0.8.2 +0.8.3 diff --git a/examples/unfinished/benchmark/benchmark.js b/examples/unfinished/benchmark/benchmark.js index abbbde6ad8..1a206ea094 100644 --- a/examples/unfinished/benchmark/benchmark.js +++ b/examples/unfinished/benchmark/benchmark.js @@ -35,6 +35,16 @@ var randomString = function (length) { return ret; }; +var preCall = function (name) { + console.log('> ' + name); +}; + +var postCall = function (name) { + return function (err, callback) { + console.log('< ' + name + ' ' + (err ? 'ERR' : 'OK')); + }; +}; + var pickCollection = function () { return Random.choice(Collections); }; @@ -96,7 +106,8 @@ if (Meteor.isServer) { Meteor.setInterval(function () { var when = +(new Date) - PARAMS.maxAgeSeconds*1000; _.each(Collections, function (C) { - C.remove({when: {$lt: when}}); + preCall('removeMaxAge'); + C.remove({when: {$lt: when}}, postCall('removeMaxAge')); }); // Clear out 5% of the DB each time, steady state. XXX parameterize? }, 1000*PARAMS.maxAgeSeconds / 20); @@ -121,7 +132,8 @@ if (Meteor.isServer) { doc.when = +(new Date); var C = pickCollection(); - C.insert(doc); + preCall('insert'); + C.insert(doc, postCall('insert')); }, update: function (processId, field, value) { check([processId, field, value], [String]); @@ -130,15 +142,18 @@ if (Meteor.isServer) { var C = pickCollection(); // update one message. - C.update({fromProcess: processId}, {$set: modifer}, {multi: false}); + preCall('update'); + C.update({fromProcess: processId}, {$set: modifer}, {multi: false}, postCall('update')); }, remove: function (processId) { check(processId, String); var C = pickCollection(); // remove one message. var obj = C.findOne({fromProcess: processId}); - if (obj) - C.remove(obj._id); + if (obj) { + preCall('remove'); + C.remove(obj._id, postCall('remove')); + } } }); diff --git a/examples/unfinished/benchmark/run-local.sh b/examples/unfinished/benchmark/run-local.sh index 3c533ad54a..9fecfcaba9 100755 --- a/examples/unfinished/benchmark/run-local.sh +++ b/examples/unfinished/benchmark/run-local.sh @@ -1,8 +1,12 @@ #!/bin/bash PORT=9000 -NUM_CLIENTS=10 -DURATION=120 +if [ -z "$NUM_CLIENTS" ]; then + NUM_CLIENTS=10 +fi +if [ -z "$DURATION" ]; then + DURATION=120 +fi REPORT_INTERVAL=10 set -e @@ -20,7 +24,7 @@ pkill -f "$PROJDIR/.meteor/local/db" || true ../../../meteor reset || true # start the benchmark app -../../../meteor --production --settings "scenarios/${SCENARIO}.json" --port 9000 & +../../../meteor --production --settings "scenarios/${SCENARIO}.json" --port ${PORT} & OUTER_PID=$! echo "Waiting for server to come up" @@ -30,12 +34,13 @@ function wait_for_port { sleep 1 N=$(($N+1)) if [ $N -ge $2 ] ; then + curl -v "$1" || true echo "Timed out waiting for port $1" exit 2 fi done } -wait_for_port "http://localhost:9001" 60 +wait_for_port "http://localhost:${PORT}" 60 echo "Starting phantoms" diff --git a/examples/unfinished/benchmark/scenarios/scale10.json b/examples/unfinished/benchmark/scenarios/scale10.json new file mode 100644 index 0000000000..5962540e04 --- /dev/null +++ b/examples/unfinished/benchmark/scenarios/scale10.json @@ -0,0 +1,11 @@ + { + "params": { + "numCollections": 1, + "maxAgeSeconds": 60, + "insertsPerSecond": 10, + "updatesPerSecond": 10, + "removesPerSecond": 1, + "documentSize": 128, + "documentNumFields": 2 + } +} diff --git a/examples/unfinished/benchmark/scenarios/scale100.json b/examples/unfinished/benchmark/scenarios/scale100.json new file mode 100644 index 0000000000..ec0f4853e9 --- /dev/null +++ b/examples/unfinished/benchmark/scenarios/scale100.json @@ -0,0 +1,11 @@ + { + "params": { + "numCollections": 1, + "maxAgeSeconds": 60, + "insertsPerSecond": 100, + "updatesPerSecond": 100, + "removesPerSecond": 10, + "documentSize": 128, + "documentNumFields": 2 + } +} diff --git a/examples/unfinished/benchmark/scenarios/scale20.json b/examples/unfinished/benchmark/scenarios/scale20.json new file mode 100644 index 0000000000..db357f2994 --- /dev/null +++ b/examples/unfinished/benchmark/scenarios/scale20.json @@ -0,0 +1,11 @@ + { + "params": { + "numCollections": 1, + "maxAgeSeconds": 60, + "insertsPerSecond": 20, + "updatesPerSecond": 20, + "removesPerSecond": 2, + "documentSize": 128, + "documentNumFields": 2 + } +} diff --git a/examples/unfinished/benchmark/scenarios/scale40.json b/examples/unfinished/benchmark/scenarios/scale40.json new file mode 100644 index 0000000000..53cab32bd1 --- /dev/null +++ b/examples/unfinished/benchmark/scenarios/scale40.json @@ -0,0 +1,11 @@ + { + "params": { + "numCollections": 1, + "maxAgeSeconds": 60, + "insertsPerSecond": 40, + "updatesPerSecond": 40, + "removesPerSecond": 4, + "documentSize": 128, + "documentNumFields": 2 + } +} diff --git a/examples/unfinished/benchmark/scenarios/scale50.json b/examples/unfinished/benchmark/scenarios/scale50.json new file mode 100644 index 0000000000..0e4df53c12 --- /dev/null +++ b/examples/unfinished/benchmark/scenarios/scale50.json @@ -0,0 +1,11 @@ + { + "params": { + "numCollections": 1, + "maxAgeSeconds": 60, + "insertsPerSecond": 50, + "updatesPerSecond": 50, + "removesPerSecond": 5, + "documentSize": 128, + "documentNumFields": 2 + } +} diff --git a/examples/wordplay/.meteor/release b/examples/wordplay/.meteor/release index 100435be13..ee94dd834b 100644 --- a/examples/wordplay/.meteor/release +++ b/examples/wordplay/.meteor/release @@ -1 +1 @@ -0.8.2 +0.8.3 diff --git a/packages/accounts-ui-unstyled/accounts_ui.js b/packages/accounts-ui-unstyled/accounts_ui.js index 5f241ab997..6a0f0851c2 100644 --- a/packages/accounts-ui-unstyled/accounts_ui.js +++ b/packages/accounts-ui-unstyled/accounts_ui.js @@ -2,12 +2,14 @@ Accounts.ui = {}; Accounts.ui._options = { requestPermissions: {}, - requestOfflineToken: {} + requestOfflineToken: {}, + forceApprovalPrompt: {} }; +// XXX refactor duplicated code in this function Accounts.ui.config = function(options) { // validate options keys - var VALID_KEYS = ['passwordSignupFields', 'requestPermissions', 'requestOfflineToken']; + var VALID_KEYS = ['passwordSignupFields', 'requestPermissions', 'requestOfflineToken', 'forceApprovalPrompt']; _.each(_.keys(options), function (key) { if (!_.contains(VALID_KEYS, key)) throw new Error("Accounts.ui.config: Invalid key: " + key); @@ -56,6 +58,20 @@ Accounts.ui.config = function(options) { } }); } + + // deal with `forceApprovalPrompt` + if (options.forceApprovalPrompt) { + _.each(options.forceApprovalPrompt, function (value, service) { + if (service !== 'google') + throw new Error("Accounts.ui.config: `forceApprovalPrompt` only supported for Google login at the moment."); + + if (Accounts.ui._options.forceApprovalPrompt[service]) { + throw new Error("Accounts.ui.config: Can't set `forceApprovalPrompt` more than once for " + service); + } else { + Accounts.ui._options.forceApprovalPrompt[service] = value; + } + }); + } }; passwordSignupFields = function () { diff --git a/packages/accounts-ui-unstyled/accounts_ui_tests.js b/packages/accounts-ui-unstyled/accounts_ui_tests.js index a8f4fc40f5..5e4bef9ea0 100644 --- a/packages/accounts-ui-unstyled/accounts_ui_tests.js +++ b/packages/accounts-ui-unstyled/accounts_ui_tests.js @@ -5,7 +5,7 @@ // XXX it'd be cool to also test that the right thing happens if options -// *are* validated, but Accouns.ui._options is global state which makes this hard +// *are* validated, but Accounts.ui._options is global state which makes this hard // (impossible?) Tinytest.add('accounts-ui - config validates keys', function (test) { test.throws(function () { @@ -19,4 +19,8 @@ Tinytest.add('accounts-ui - config validates keys', function (test) { test.throws(function () { Accounts.ui.config({requestPermissions: {facebook: "not an array"}}); }); + + test.throws(function () { + Accounts.ui.config({forceApprovalPrompt: {facebook: "only google"}}); + }); }); diff --git a/packages/accounts-ui-unstyled/login_buttons_single.js b/packages/accounts-ui-unstyled/login_buttons_single.js index 79d35a4190..96e1e65624 100644 --- a/packages/accounts-ui-unstyled/login_buttons_single.js +++ b/packages/accounts-ui-unstyled/login_buttons_single.js @@ -29,6 +29,8 @@ Template._loginButtonsLoggedOutSingleLoginButton.events({ options.requestPermissions = Accounts.ui._options.requestPermissions[serviceName]; if (Accounts.ui._options.requestOfflineToken[serviceName]) options.requestOfflineToken = Accounts.ui._options.requestOfflineToken[serviceName]; + if (Accounts.ui._options.forceApprovalPrompt[serviceName]) + options.forceApprovalPrompt = Accounts.ui._options.forceApprovalPrompt[serviceName]; loginWithService(options, callback); } diff --git a/packages/autoupdate/package.js b/packages/autoupdate/package.js index 306a177c54..ebcdf04e57 100644 --- a/packages/autoupdate/package.js +++ b/packages/autoupdate/package.js @@ -11,7 +11,7 @@ Cordova.depends({ Package.on_use(function (api) { api.use('webapp', 'server'); api.use(['deps', 'retry'], 'client'); - api.use(['livedata', 'mongo-livedata'], ['client', 'server']); + api.use(['livedata', 'mongo-livedata', 'underscore'], ['client', 'server']); api.use('deps', 'client'); api.use('reload', 'client', {weak: true}); api.use('http', 'client.cordova'); diff --git a/packages/blaze/builtins.js b/packages/blaze/builtins.js index 524171af48..d65b33b2a2 100644 --- a/packages/blaze/builtins.js +++ b/packages/blaze/builtins.js @@ -169,6 +169,7 @@ Blaze.InOuterTemplateScope = function (templateView, contentFunc) { parentView = parentView.parentView; view.onCreated(function () { + this.originalParentView = this.parentView; this.parentView = parentView; }); return view; diff --git a/packages/blaze/domrange.js b/packages/blaze/domrange.js index 055e71ec52..ba71fc20d3 100644 --- a/packages/blaze/domrange.js +++ b/packages/blaze/domrange.js @@ -29,6 +29,28 @@ Blaze.DOMRange = function (nodeAndRangeArray) { }; var DOMRange = Blaze.DOMRange; +// In IE 8, don't use empty text nodes as placeholders +// in empty DOMRanges, use comment nodes instead. Using +// empty text nodes in modern browsers is great because +// it doesn't clutter the web inspector. In IE 8, however, +// it seems to lead in some roundabout way to the OAuth +// pop-up crashing the browser completely. In the past, +// we didn't use empty text nodes on IE 8 because they +// don't accept JS properties, so just use the same logic +// even though we don't need to set properties on the +// placeholder anymore. +DOMRange._USE_COMMENT_PLACEHOLDERS = (function () { + var result = false; + var textNode = document.createTextNode(""); + try { + textNode.someProp = true; + } catch (e) { + // IE 8 + result = true; + } + return result; +})(); + // static methods DOMRange._insert = function (rangeOrNode, parentElement, nextNode, _isMove) { var m = rangeOrNode; @@ -118,7 +140,10 @@ DOMRange.prototype.attach = function (parentElement, nextNode, _isMove) { DOMRange._insert(members[i], parentElement, nextNode, _isMove); } } else { - var placeholder = document.createTextNode(""); + var placeholder = ( + DOMRange._USE_COMMENT_PLACEHOLDERS ? + document.createComment("") : + document.createTextNode("")); this.emptyRangePlaceholder = placeholder; parentElement.insertBefore(placeholder, nextNode || null); } diff --git a/packages/blaze/lookup.js b/packages/blaze/lookup.js index 31f5701fde..3bc97a5f6b 100644 --- a/packages/blaze/lookup.js +++ b/packages/blaze/lookup.js @@ -6,7 +6,9 @@ var bindIfIsFunction = function (x, target) { }; }; -var bindToCurrentDataIfIsFunction = function (x) { +// If `x` is a function, binds the value of `this` for that function +// to the current data context. +var bindDataContext = function (x) { if (typeof x === 'function') { return function () { var data = Blaze.getCurrentData(); @@ -22,6 +24,8 @@ var wrapHelper = function (f) { return Blaze.wrapCatchingExceptions(f, 'template helper'); }; +// !!! FIX THIS COMMENT !!! +// // Implements {{foo}} where `name` is "foo" // and `component` is the component the tag is found in // (the lexical "self," on which to look for methods). @@ -45,11 +49,11 @@ Blaze.View.prototype.lookup = function (name, _options) { return Blaze._parentData(name.length - 1, true /*_functionWrapped*/); } else if (template && (name in template)) { - return wrapHelper(bindToCurrentDataIfIsFunction(template[name])); + return wrapHelper(bindDataContext(template[name])); } else if (lookupTemplate && Template.__lookup__(name)) { return Template.__lookup__(name); } else if (UI._globalHelpers[name]) { - return wrapHelper(bindToCurrentDataIfIsFunction(UI._globalHelpers[name])); + return wrapHelper(bindDataContext(UI._globalHelpers[name])); } else { return function () { var isCalledAsFunction = (arguments.length > 0); diff --git a/packages/blaze/materializer.js b/packages/blaze/materializer.js index 84b95003dd..fc71f11f66 100644 --- a/packages/blaze/materializer.js +++ b/packages/blaze/materializer.js @@ -49,13 +49,19 @@ Blaze.DOMMaterializer.def({ var rawAttrs = tag.attrs; var children = tag.children; - if (tagName === 'textarea' && ! (rawAttrs && ('value' in rawAttrs))) { - // turn TEXTAREA contents into a value attribute. - // Reactivity in the form of nested Views won't work here - // because the Views have already been instantiated. To - // get Views in a textarea they need to be wrapped in a - // function and provided as the "value" attribute by the - // compiler. + if (tagName === 'textarea' && tag.children.length && + ! (rawAttrs && ('value' in rawAttrs))) { + // Provide very limited support for TEXTAREA tags with children + // rather than a "value" attribute. + // Reactivity in the form of Views nested in the tag's children + // won't work. Compilers should compile textarea contents into + // the "value" attribute of the tag, wrapped in a function if there + // is reactivity. + if (typeof rawAttrs === 'function' || + HTML.isArray(rawAttrs)) { + throw new Error("Can't have reactive children of TEXTAREA node; " + + "use the 'value' attribute instead."); + } rawAttrs = _.extend({}, rawAttrs || null); rawAttrs.value = Blaze._expand(children, self.parentView); children = []; diff --git a/packages/blaze/view.js b/packages/blaze/view.js index 9b4e703e91..7597f97a63 100644 --- a/packages/blaze/view.js +++ b/packages/blaze/view.js @@ -312,14 +312,21 @@ Blaze.HTMLJSExpander.def({ } }); +// Return Blaze.currentView, but only if it is being rendered +// (i.e. we are in its render() method). +var currentViewIfRendering = function () { + var view = Blaze.currentView; + return (view && view.isInRender) ? view : null; +}; + Blaze._expand = function (htmljs, parentView) { - parentView = parentView || Blaze.currentView; + parentView = parentView || currentViewIfRendering(); return (new Blaze.HTMLJSExpander( {parentView: parentView})).visit(htmljs); }; Blaze._expandAttributes = function (attrs, parentView) { - parentView = parentView || Blaze.currentView; + parentView = parentView || currentViewIfRendering(); return (new Blaze.HTMLJSExpander( {parentView: parentView})).visitAttributes(attrs); }; @@ -383,7 +390,7 @@ Blaze.runTemplate = function (t/*, args*/) { }; Blaze.render = function (content, parentView) { - parentView = parentView || Blaze.currentView; + parentView = parentView || currentViewIfRendering(); var view; if (typeof content === 'function') { @@ -401,7 +408,7 @@ Blaze.render = function (content, parentView) { Blaze.toHTML = function (htmljs, parentView) { if (typeof htmljs === 'function') throw new Error("Blaze.toHTML doesn't take a function, just HTMLjs"); - parentView = parentView || Blaze.currentView; + parentView = parentView || currentViewIfRendering(); return HTML.toHTML(Blaze._expand(htmljs, parentView)); }; @@ -414,7 +421,7 @@ Blaze.toText = function (htmljs, parentView, textMode) { textMode = parentView; parentView = null; } - parentView = parentView || Blaze.currentView; + parentView = parentView || currentViewIfRendering(); if (! textMode) throw new Error("textMode required"); @@ -529,7 +536,11 @@ Blaze._addEventMap = function (view, eventMap, thisInHandler) { function (evt) { if (! range.containsElement(evt.currentTarget)) return null; - return handler.apply(thisInHandler || this, arguments); + var handlerThis = thisInHandler || this; + var handlerArgs = arguments; + return Blaze.withCurrentView(view, function () { + return handler.apply(handlerThis, handlerArgs); + }); }, range, function (r) { return r.parentRange; diff --git a/packages/boilerplate-generator/boilerplate_client.browser.html b/packages/boilerplate-generator/boilerplate_web.browser.html similarity index 100% rename from packages/boilerplate-generator/boilerplate_client.browser.html rename to packages/boilerplate-generator/boilerplate_web.browser.html diff --git a/packages/boilerplate-generator/boilerplate_client.cordova.html b/packages/boilerplate-generator/boilerplate_web.cordova.html similarity index 100% rename from packages/boilerplate-generator/boilerplate_client.cordova.html rename to packages/boilerplate-generator/boilerplate_web.cordova.html diff --git a/packages/boilerplate-generator/package.js b/packages/boilerplate-generator/package.js index 4f9a0dbb94..06a357eda1 100644 --- a/packages/boilerplate-generator/package.js +++ b/packages/boilerplate-generator/package.js @@ -12,8 +12,8 @@ Package.on_use(function (api) { // spacebars compiler rather than letting the 'templating' package (which // isn't fully supported on the server yet) handle it. That also means that // they don't contain the outer "