spec/dummy/tmp/ember-rails/ember.js in cadenero-0.0.2.a3 vs spec/dummy/tmp/ember-rails/ember.js in cadenero-0.0.2.b1

- old
+ new

@@ -1,7 +1,7 @@ -// Version: v1.0.0-rc.5-3-g835b8e5 -// Last commit: 835b8e5 (2013-06-01 14:23:38 -0400) +// Version: v1.0.0-rc.6 +// Last commit: 893bbc4 (2013-06-23 15:14:46 -0400) (function() { /*global __fail__*/ @@ -47,11 +47,16 @@ the text of the Error thrown if the assertion fails. @param {Boolean} test Must be truthy for the assertion to pass. If falsy, an exception will be thrown. */ Ember.assert = function(desc, test) { - if (!test) throw new Error("assertion failed: "+desc); + Ember.Logger.assert(test, desc); + + if (Ember.testing && !test) { + // when testing, ensure test failures when assertions fail + throw new Error("Assertion Failed: " + desc); + } }; /** Display a warning with the provided message. Ember build tools will @@ -93,16 +98,16 @@ @param {String} message A description of the deprecation. @param {Boolean} test An optional boolean. If falsy, the deprecation will be displayed. */ Ember.deprecate = function(message, test) { - if (Ember && Ember.TESTING_DEPRECATION) { return; } + if (Ember.TESTING_DEPRECATION) { return; } if (arguments.length === 1) { test = false; } if (test) { return; } - if (Ember && Ember.ENV.RAISE_ON_DEPRECATION) { throw new Error(message); } + if (Ember.ENV.RAISE_ON_DEPRECATION) { throw new Error(message); } var error; // When using new Error, we can't do the arguments check for Chrome. Alternatives are welcome try { __fail__.fail(); } catch (e) { error = e; } @@ -149,12 +154,12 @@ }; }; })(); -// Version: v1.0.0-rc.5-3-g835b8e5 -// Last commit: 835b8e5 (2013-06-01 14:23:38 -0400) +// Version: v1.0.0-rc.6 +// Last commit: 893bbc4 (2013-06-23 15:14:46 -0400) (function() { var define, requireModule; @@ -217,11 +222,11 @@ The core Runtime framework is based on the jQuery API with a number of performance optimizations. @class Ember @static - @version 1.0.0-rc.5 + @version 1.0.0-rc.6 */ if ('undefined' === typeof Ember) { // Create core object. Make it act like an instance of Ember.Namespace so that // objects assigned to it are given a sane string representation. @@ -244,14 +249,14 @@ /** @property VERSION @type String - @default '1.0.0-rc.5' + @default '1.0.0-rc.6' @final */ -Ember.VERSION = '1.0.0-rc.5'; +Ember.VERSION = '1.0.0-rc.6'; /** Standard environmental variables. You can define these in a global `ENV` variable before loading Ember to control various configuration settings. @@ -362,10 +367,23 @@ }; } } } +function assertPolyfill(test, message) { + if (!test) { + try { + // attempt to preserve the stack + throw new Error("assertion failed: " + message); + } catch(error) { + setTimeout(function(){ + throw error; + }, 0); + } + } +} + /** Inside Ember-Metal, simply uses the methods from `imports.console`. Override this to provide more robust logging functionality. @class Logger @@ -374,11 +392,12 @@ Ember.Logger = { log: consoleMethod('log') || Ember.K, warn: consoleMethod('warn') || Ember.K, error: consoleMethod('error') || Ember.K, info: consoleMethod('info') || Ember.K, - debug: consoleMethod('debug') || consoleMethod('info') || Ember.K + debug: consoleMethod('debug') || consoleMethod('info') || Ember.K, + assert: consoleMethod('assert') || assertPolyfill }; // .......................................................... // ERROR HANDLING @@ -1642,10 +1661,11 @@ if (!keyName && 'string'===typeof obj) { keyName = obj; obj = null; } + Ember.assert("Cannot call get with "+ keyName +" key.", !!keyName); Ember.assert("Cannot call get with '"+ keyName +"' on an undefined object.", obj !== undefined); if (obj === null || keyName.indexOf('.') !== -1) { return getPath(obj, keyName); } @@ -1674,34 +1694,43 @@ Ember.get = get; Ember.config.overrideAccessors(); get = Ember.get; } -function firstKey(path) { - return path.match(FIRST_KEY)[0]; -} +/** + @private -// assumes path is already normalized -function normalizeTuple(target, path) { + Normalizes a target/path pair to reflect that actual target/path that should + be observed, etc. This takes into account passing in global property + paths (i.e. a path beginning with a captial letter not defined on the + target) and * separators. + + @method normalizeTuple + @for Ember + @param {Object} target The current target. May be `null`. + @param {String} path A path on the target or a global property path. + @return {Array} a temporary array with the normalized target/path pair. +*/ +var normalizeTuple = Ember.normalizeTuple = function(target, path) { var hasThis = HAS_THIS.test(path), isGlobal = !hasThis && IS_GLOBAL_PATH.test(path), key; if (!target || isGlobal) target = Ember.lookup; if (hasThis) path = path.slice(5); if (target === Ember.lookup) { - key = firstKey(path); + key = path.match(FIRST_KEY)[0]; target = get(target, key); path = path.slice(key.length+1); } // must return some kind of path to be valid else other things will break. if (!path || path.length===0) throw new Error('Invalid Path'); return [ target, path ]; -} +}; var getPath = Ember._getPath = function(root, path) { var hasThis, parts, tuple, idx, len; // If there is no root and path is a key name, return that @@ -1726,28 +1755,10 @@ if (root && root.isDestroyed) { return undefined; } } return root; }; -/** - @private - - Normalizes a target/path pair to reflect that actual target/path that should - be observed, etc. This takes into account passing in global property - paths (i.e. a path beginning with a captial letter not defined on the - target) and * separators. - - @method normalizeTuple - @for Ember - @param {Object} target The current target. May be `null`. - @param {String} path A path on the target or a global property path. - @return {Array} a temporary array with the normalized target/path pair. -*/ -Ember.normalizeTuple = function(target, path) { - return normalizeTuple(target, path); -}; - Ember.getWithDefault = function(root, key, defaultValue) { var value = get(root, key); if (value === undefined) { return defaultValue; } return value; @@ -2463,10 +2474,12 @@ value = keyName; keyName = obj; obj = null; } + Ember.assert("Cannot call set with "+ keyName +" key.", !!keyName); + if (!obj || keyName.indexOf('.') !== -1) { return setPath(obj, keyName, value, tolerant); } Ember.assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); @@ -3114,13 +3127,13 @@ Set a list of properties on an object. These properties are set inside a single `beginPropertyChanges` and `endPropertyChanges` batch, so observers will be buffered. @method setProperties - @param target - @param {Hash} properties - @return target + @param self + @param {Object} hash + @return self */ Ember.setProperties = function(self, hash) { changeProperties(function(){ for(var prop in hash) { if (hash.hasOwnProperty(prop)) { set(self, prop, hash[prop]); } @@ -4756,19 +4769,21 @@ hasTimers: function() { return !!timers.length || autorun; }, cancel: function(timer) { - if (typeof timer === 'object' && timer.queue && timer.method) { // we're cancelling a deferOnce + if (timer && typeof timer === 'object' && timer.queue && timer.method) { // we're cancelling a deferOnce return timer.queue.cancel(timer); } else if (typeof timer === 'function') { // we're cancelling a setTimeout for (var i = 0, l = timers.length; i < l; i += 2) { if (timers[i + 1] === timer) { timers.splice(i, 2); // remove the two elements return true; } } + } else { + return; // timer was null or not a timer } } }; Backburner.prototype.schedule = Backburner.prototype.defer; @@ -4857,11 +4872,11 @@ outerloop: while (queueNameIndex < numberOfQueues) { queueName = queueNames[queueNameIndex]; queue = queues[queueName]; - queueItems = queue._queue.slice(); + queueItems = queue._queueBeingFlushed = queue._queue.slice(); queue._queue = []; var options = queue.options, before = options && options.before, after = options && options.after, @@ -4875,19 +4890,23 @@ args = queueItems[queueIndex+2]; stack = queueItems[queueIndex+3]; // Debugging assistance if (typeof method === 'string') { method = target[method]; } - // TODO: error handling - if (args && args.length > 0) { - method.apply(target, args); - } else { - method.call(target); + // method could have been nullified / canceled during flush + if (method) { + // TODO: error handling + if (args && args.length > 0) { + method.apply(target, args); + } else { + method.call(target); + } } queueIndex += 4; } + queue._queueBeingFlushed = null; if (numberOfQueueItems && after) { after(); } if ((priorQueueNameIndex = indexOfPriorQueueWithActions(this, queueNameIndex)) !== -1) { queueNameIndex = priorQueueNameIndex; continue outerloop; @@ -4908,10 +4927,11 @@ } return -1; } + __exports__.DeferredActionQueues = DeferredActionQueues; }); define("backburner/queue", ["exports"], @@ -4997,16 +5017,34 @@ if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { queue.splice(i, 4); return true; } } + + // if not found in current queue + // could be in the queue that is being flushed + queue = this._queueBeingFlushed; + if (!queue) { + return; + } + for (i = 0, l = queue.length; i < l; i += 4) { + currentTarget = queue[i]; + currentMethod = queue[i+1]; + + if (currentTarget === actionToCancel.target && currentMethod === actionToCancel.method) { + // don't mess with array during flush + // just nullify the method + queue[i+1] = null; + return true; + } + } } }; + __exports__.Queue = Queue; }); - })(); (function() { @@ -5110,11 +5148,11 @@ @param {Object} [target] target of method to call @param {Function|String} method Method to invoke. May be a function or a string. If you pass a string then it will be looked up on the passed target. @param {Object} [args*] Any additional arguments you wish to pass to the method. - @return {Object} return value from invoking the passed function. Please note, + @return {Object} return value from invoking the passed function. Please note, when called within an existing loop, no return value is possible. */ Ember.run.join = function(target, method) { if (!Ember.run.currentRunLoop) { return Ember.run.apply(Ember.run, arguments); @@ -5246,11 +5284,13 @@ @method sync @return {void} */ Ember.run.sync = function() { - backburner.currentInstance.queues.sync.flush(); + if (backburner.currentInstance) { + backburner.currentInstance.queues.sync.flush(); + } }; /** Invokes the passed target/method and optional arguments after a specified period if time. The last parameter of this method must always be a number @@ -5439,10 +5479,42 @@ */ Ember.run.cancel = function(timer) { return backburner.cancel(timer); }; +/** + Execute the passed method in a specified amount of time, reset timer + upon additional calls. + + ```javascript + var myFunc = function() { console.log(this.name + ' ran.'); }; + var myContext = {name: 'debounce'}; + + Ember.run.debounce(myContext, myFunc, 150); + + // less than 150ms passes + + Ember.run.debounce(myContext, myFunc, 150); + + // 150ms passes + // myFunc is invoked with context myContext + // console logs 'debounce ran.' one time. + ``` + + @method debounce + @param {Object} [target] target of method to invoke + @param {Function|String} method The method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Optional arguments to pass to the timeout. + @param {Number} wait Number of milliseconds to wait. + @return {void} +*/ +Ember.run.debounce = function() { + return backburner.debounce.apply(backburner, arguments); +}; + // Make sure it's not an autorun during testing function checkAutoRun() { if (!Ember.run.currentRunLoop) { Ember.assert("You have turned on testing mode, which disabled the run-loop's autorun. You will need to wrap any code with asynchronous side-effects in an Ember.run", !Ember.testing); } @@ -7228,10 +7300,12 @@ __exports__.configure = configure; __exports__.resolve = resolve; __exports__.reject = reject; }); + + })(); (function() { define("container", [], @@ -7352,10 +7426,14 @@ } return value; }, + lookupFactory: function(fullName) { + return factoryFor(this, fullName); + }, + has: function(fullName) { if (this.cache.has(fullName)) { return true; } @@ -8072,12 +8150,12 @@ 'css-class-name'.capitalize() // 'Css-class-name' 'my favorite items'.capitalize() // 'My favorite items' ``` @method capitalize - @param {String} str - @return {String} + @param {String} str The string to capitalize. + @return {String} The capitalized string. */ capitalize: function(str) { return str.charAt(0).toUpperCase() + str.substr(1); } @@ -9446,19 +9524,19 @@ /** Adds an array observer to the receiving array. The array observer object normally must implement two methods: - * `arrayWillChange(start, removeCount, addCount)` - This method will be + * `arrayWillChange(observedObj, start, removeCount, addCount)` - This method will be called just before the array is modified. - * `arrayDidChange(start, removeCount, addCount)` - This method will be + * `arrayDidChange(observedObj, start, removeCount, addCount)` - This method will be called just after the array is modified. - Both callbacks will be passed the starting index of the change as well a - a count of the items to be removed and added. You can use these callbacks - to optionally inspect the array during the change, clear caches, or do - any other bookkeeping necessary. + Both callbacks will be passed the observed object, starting index of the + change as well a a count of the items to be removed and added. You can use + these callbacks to optionally inspect the array during the change, clear + caches, or do any other bookkeeping necessary. In addition to passing a target, you can also include an options hash which you can use to override the method names that will be invoked on the target. @@ -13462,21 +13540,44 @@ Specifies the arrangedContent's sort direction @property {Boolean} sortAscending */ sortAscending: true, + + /** + The function used to compare two values. You can override this if you + want to do custom comparisons.Functions must be of the type expected by + Array#sort, i.e. + return 0 if the two parameters are equal, + return a negative value if the first parameter is smaller than the second or + return a positive value otherwise: + ```javascript + function(x,y){ // These are assumed to be integers + if(x === y) + return 0; + return x < y ? -1 : 1; + } + ``` + + @property sortFunction + @type {Function} + @default Ember.compare + */ + sortFunction: Ember.compare, + orderBy: function(item1, item2) { var result = 0, sortProperties = get(this, 'sortProperties'), - sortAscending = get(this, 'sortAscending'); + sortAscending = get(this, 'sortAscending'), + sortFunction = get(this, 'sortFunction'); Ember.assert("you need to define `sortProperties`", !!sortProperties); forEach(sortProperties, function(propertyName) { if (result === 0) { - result = Ember.compare(get(item1, propertyName), get(item2, propertyName)); + result = sortFunction(get(item1, propertyName), get(item2, propertyName)); if ((result !== 0) && !sortAscending) { result = (-1) * result; } } }); @@ -13910,11 +14011,11 @@ @module ember @submodule ember-views */ var jQuery = Ember.imports.jQuery; -Ember.assert("Ember Views require jQuery 1.8, 1.9, 1.10, or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(8|9|10))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); +Ember.assert("Ember Views require jQuery 1.7, 1.8, 1.9, 1.10, or 2.0", jQuery && (jQuery().jquery.match(/^((1\.(7|8|9|10))|2.0)(\.\d+)?(pre|rc\d?)?/) || Ember.ENV.FORCE_JQUERY)); /** Alias for jQuery @method $ @@ -14609,10 +14710,51 @@ @extends Ember.Object */ Ember.EventDispatcher = Ember.Object.extend(/** @scope Ember.EventDispatcher.prototype */{ /** + The set of events names (and associated handler function names) to be setup + and dispatched by the `EventDispatcher`. Custom events can added to this list at setup + time, generally via the `Ember.Application.customEvents` hash. Only override this + default set to prevent the EventDispatcher from listening on some events all together. + + This set will be modified by `setup` to also include any events added at that time. + + @property events + @type Object + */ + events: { + touchstart : 'touchStart', + touchmove : 'touchMove', + touchend : 'touchEnd', + touchcancel : 'touchCancel', + keydown : 'keyDown', + keyup : 'keyUp', + keypress : 'keyPress', + mousedown : 'mouseDown', + mouseup : 'mouseUp', + contextmenu : 'contextMenu', + click : 'click', + dblclick : 'doubleClick', + mousemove : 'mouseMove', + focusin : 'focusIn', + focusout : 'focusOut', + mouseenter : 'mouseEnter', + mouseleave : 'mouseLeave', + submit : 'submit', + input : 'input', + change : 'change', + dragstart : 'dragStart', + drag : 'drag', + dragenter : 'dragEnter', + dragleave : 'dragLeave', + dragover : 'dragOver', + drop : 'drop', + dragend : 'dragEnd' + }, + + /** @private The root DOM element to which event listeners should be attached. Event listeners will be attached to the document unless this is overridden. @@ -14639,39 +14781,11 @@ @method setup @param addedEvents {Hash} */ setup: function(addedEvents, rootElement) { - var event, events = { - touchstart : 'touchStart', - touchmove : 'touchMove', - touchend : 'touchEnd', - touchcancel : 'touchCancel', - keydown : 'keyDown', - keyup : 'keyUp', - keypress : 'keyPress', - mousedown : 'mouseDown', - mouseup : 'mouseUp', - contextmenu : 'contextMenu', - click : 'click', - dblclick : 'doubleClick', - mousemove : 'mouseMove', - focusin : 'focusIn', - focusout : 'focusOut', - mouseenter : 'mouseEnter', - mouseleave : 'mouseLeave', - submit : 'submit', - input : 'input', - change : 'change', - dragstart : 'dragStart', - drag : 'drag', - dragenter : 'dragEnter', - dragleave : 'dragLeave', - dragover : 'dragOver', - drop : 'drop', - dragend : 'dragEnd' - }; + var event, events = get(this, 'events'); Ember.$.extend(events, addedEvents || {}); if (!Ember.isNone(rootElement)) { @@ -14914,10 +15028,19 @@ @for Ember @type Hash */ Ember.TEMPLATES = {}; +/** + `Ember.CoreView` is + + @class CoreView + @namespace Ember + @extends Ember.Object + @uses Ember.Evented +*/ + Ember.CoreView = Ember.Object.extend(Ember.Evented, { isView: true, states: states, @@ -15617,12 +15740,14 @@ See `Handlebars.helpers.action`. ### Event Names - Possible events names for any of the responding approaches described above - are: + All of the event handling approaches described above respond to the same set + of events. The names of the built-in events are listed below. (The hash of + built-in events exists in `Ember.EventDispatcher`.) Additional, custom events + can be registered by using `Ember.Application.customEvents`. Touch events: * `touchStart` * `touchMove` @@ -15671,12 +15796,11 @@ using the `{{view}}` Handlebars helper. See `Handlebars.helpers.view` for additional information. @class View @namespace Ember - @extends Ember.Object - @uses Ember.Evented + @extends Ember.CoreView */ Ember.View = Ember.CoreView.extend( /** @scope Ember.View.prototype */ { concatenatedProperties: ['classNames', 'classNameBindings', 'attributeBindings'], @@ -15853,11 +15977,11 @@ @private If a value that affects template rendering changes, the view should be re-rendered to reflect the new value. - @method _displayPropertyDidChange + @method _contextDidChange */ _contextDidChange: Ember.observer(function() { this.rerender(); }, 'context'), @@ -15983,10 +16107,12 @@ @method _parentViewDidChange */ _parentViewDidChange: Ember.observer(function() { if (this.isDestroying) { return; } + this.trigger('parentViewDidChange'); + if (get(this, 'parentView.controller') && !get(this, 'controller')) { this.notifyPropertyChange('controller'); } }, '_parentView'), @@ -16254,13 +16380,13 @@ as its buffer. For example, calling `view.$('li')` will return a jQuery object containing all of the `li` elements inside the DOM element of this view. - @property $ + @method $ @param {String} [selector] a jQuery-compatible selector string - @return {jQuery} the CoreQuery object for the DOM node + @return {jQuery} the jQuery object for the DOM node */ $: function(sel) { return this.currentState.$(this, sel); }, @@ -16937,35 +17063,36 @@ @param {Class} viewClass @param {Hash} [attrs] Attributes to add @return {Ember.View} new instance */ createChildView: function(view, attrs) { - if (view.isView && view._parentView === this) { return view; } + if (view.isView && view._parentView === this && view.container === this.container) { + return view; + } + attrs = attrs || {}; + attrs._parentView = this; + attrs.container = this.container; + if (Ember.CoreView.detect(view)) { - attrs = attrs || {}; - attrs._parentView = this; - attrs.container = this.container; attrs.templateData = attrs.templateData || get(this, 'templateData'); view = view.create(attrs); // don't set the property on a virtual view, as they are invisible to // consumers of the view API - if (view.viewName) { set(get(this, 'concreteView'), view.viewName, view); } + if (view.viewName) { + set(get(this, 'concreteView'), view.viewName, view); + } } else { Ember.assert('You must pass instance or subclass of View', view.isView); - if (attrs) { - view.setProperties(attrs); - } + Ember.setProperties(view, attrs); if (!get(view, 'templateData')) { set(view, 'templateData', get(this, 'templateData')); } - - set(view, '_parentView', this); } return view; }, @@ -17308,12 +17435,12 @@ if (name !== 'value' && (type === 'string' || (type === 'number' && !isNaN(value)))) { if (value !== elem.attr(name)) { elem.attr(name, value); } } else if (name === 'value' || type === 'boolean') { - // We can't set properties to undefined - if (value === undefined) { value = null; } + // We can't set properties to undefined or null + if (!value) { value = ''; } if (value !== elem.prop(name)) { // value and booleans should always be properties elem.prop(name, value); } @@ -18009,10 +18136,11 @@ }, initializeViews: function(views, parentView, templateData) { forEach(views, function(view) { set(view, '_parentView', parentView); + set(view, 'container', parentView && parentView.container); if (!get(view, 'templateData')) { set(view, 'templateData', templateData); } }); @@ -18475,16 +18603,113 @@ })(); (function() { +/** +@module ember +@submodule ember-views +*/ +/** + An `Ember.Component` is a view that is completely + isolated. Property access in its templates go + to the view object and actions are targeted at + the view object. There is no access to the + surrounding context or outer controller; all + contextual information is passed in. + + The easiest way to create an `Ember.Component` is via + a template. If you name a template + `controls/my-foo`, you will be able to use + `{{my-foo}}` in other templates, which will make + an instance of the isolated control. + + ```html + {{app-profile person=currentUser}} + ``` + + ```html + <!-- app-profile template --> + <h1>{{person.title}}</h1> + <img {{bindAttr src=person.avatar}}> + <p class='signature'>{{person.signature}}</p> + ``` + + You can also use `yield` inside a template to + include the **contents** of the custom tag: + + ```html + {{#my-profile person=currentUser}} + <p>Admin mode</p> + {{/my-profile}} + ``` + + ```html + <!-- app-profile template --> + + <h1>{{person.title}}</h1> + {{yield}} <!-- block contents --> + ``` + + If you want to customize the control, in order to + handle events or actions, you implement a subclass + of `Ember.Component` named after the name of the + control. + + For example, you could implement the action + `hello` for the `app-profile` control: + + ```js + App.AppProfileComponent = Ember.Component.extend({ + hello: function(name) { + console.log("Hello", name) + } + }); + ``` + + And then use it in the control's template: + + ```html + <!-- app-profile template --> + + <h1>{{person.title}}</h1> + {{yield}} <!-- block contents --> + + <button {{action 'hello' person.name}}> + Say Hello to {{person.name}} + </button> + ``` + + Components must have a `-` in their name to avoid + conflicts with built-in controls that wrap HTML + elements. This is consistent with the same + requirement in web components. + + @class Component + @namespace Ember + @extends Ember.View +*/ +Ember.Component = Ember.View.extend({ + init: function() { + this._super(); + this.set('context', this); + this.set('controller', this); + } +}); + })(); (function() { + +})(); + + + +(function() { /** `Ember.ViewTargetActionSupport` is a mixin that can be included in a view class to add a `triggerAction` method with semantics similar to the Handlebars `{{action}}` helper. It provides intelligent defaults for the action's target: the view's controller; and the context that is @@ -18544,11 +18769,10 @@ })(); (function() { -/*globals jQuery*/ /** Ember Views @module ember @submodule ember-views @@ -19069,11 +19293,75 @@ delete hashType[prop]; } } } +/** + Register a bound helper or custom view helper. + + ## Simple bound helper example + + ```javascript + Ember.Handlebars.helper('capitalize', function(value) { + return value.toUpperCase(); + }); + ``` + + The above bound helper can be used inside of templates as follows: + + ```handlebars + {{capitalize name}} + ``` + + In this case, when the `name` property of the template's context changes, + the rendered value of the helper will update to reflect this change. + + For more examples of bound helpers, see documentation for + `Ember.Handlebars.registerBoundHelper`. + + ## Custom view helper example + + Assuming a view subclass named `App.CalenderView` were defined, a helper + for rendering instances of this view could be registered as follows: + + ```javascript + Ember.Handlebars.helper('calendar', App.CalendarView): + ``` + + The above bound helper can be used inside of templates as follows: + + ```handlebars + {{calendar}} + ``` + + Which is functionally equivalent to: + + ```handlebars + {{view App.CalendarView}} + ``` + + Options in the helper will be passed to the view in exactly the same + manner as with the `view` helper. + + @method helper + @for Ember.Handlebars + @param {String} name + @param {Function|Ember.View} function or view class constructor + @param {String} dependentKeys* +*/ Ember.Handlebars.helper = function(name, value) { + if (Ember.Component.detect(value)) { + Ember.assert("You tried to register a component named '" + name + "', but component names must include a '-'", name.match(/-/)); + + var proto = value.proto(); + if (!proto.layoutName && !proto.templateName) { + value.reopen({ + layoutName: 'components/' + name + }); + } + } + if (Ember.View.detect(value)) { Ember.Handlebars.registerHelper(name, function(options) { Ember.assert("You can only pass attributes as parameters (not values) to a application-defined helper", arguments.length < 2); makeBindings(options); return Ember.Handlebars.helpers.view.call(this, value, options); @@ -19830,11 +20118,11 @@ Ember._MetamorphView = Ember.View.extend(Ember._Metamorph); /** @class _SimpleMetamorphView @namespace Ember - @extends Ember.View + @extends Ember.CoreView @uses Ember._Metamorph @private */ Ember._SimpleMetamorphView = Ember.CoreView.extend(Ember._Metamorph); @@ -22646,11 +22934,10 @@ Would result in the following HTML: ```html <select class="ember-select"> - <option value>Please Select</option> <option value="1">Yehuda</option> <option value="2">Tom</option> </select> ``` @@ -22679,11 +22966,10 @@ Would result in the following HTML with a selected option: ```html <select class="ember-select"> - <option value>Please Select</option> <option value="1">Yehuda</option> <option value="2" selected="selected">Tom</option> </select> ``` @@ -22717,11 +23003,10 @@ Would result in the following HTML with a selected option: ```html <select class="ember-select"> - <option value>Please Select</option> <option value="1">Yehuda</option> <option value="2" selected="selected">Tom</option> </select> ``` @@ -22985,12 +23270,12 @@ _triggerChange: function() { var selection = get(this, 'selection'); var value = get(this, 'value'); - if (selection) { this.selectionDidChange(); } - if (value) { this.valueDidChange(); } + if (!Ember.isNone(selection)) { this.selectionDidChange(); } + if (!Ember.isNone(value)) { this.valueDidChange(); } this._change(); }, _changeSingle: function() { @@ -23183,10 +23468,35 @@ function bootstrap() { Ember.Handlebars.bootstrap( Ember.$(document) ); } +function registerComponents(container) { + var templates = Ember.TEMPLATES, match; + if (!templates) { return; } + + for (var prop in templates) { + if (match = prop.match(/^components\/(.*)$/)) { + registerComponent(container, match[1]); + } + } +} + +function registerComponent(container, name) { + Ember.assert("You provided a template named 'components/" + name + "', but custom components must include a '-'", name.match(/-/)); + + var className = name.replace(/-/g, '_'); + var Component = container.lookupFactory('component:' + className) || container.lookupFactory('component:' + name); + var View = Component || Ember.Component.extend(); + + View.reopen({ + layoutName: 'components/' + name + }); + + Ember.Handlebars.helper(name, View); +} + /* We tie this to application.load to ensure that we've at least attempted to bootstrap at the point that the application is loaded. We also tie this to document ready since we're guaranteed that all @@ -23194,12 +23504,28 @@ There's no harm to running this twice, since we remove the templates from the DOM after processing. */ -Ember.onLoad('application', bootstrap); +Ember.onLoad('Ember.Application', function(Application) { + if (Application.initializer) { + Application.initializer({ + name: 'domTemplates', + initialize: bootstrap + }); + Application.initializer({ + name: 'registerComponents', + after: 'domTemplates', + initialize: registerComponents + }); + } else { + // for ember-old-router + Ember.onLoad('application', bootstrap); + } +}); + })(); (function() { @@ -23729,12 +24055,12 @@ (function() { define("router", - ["route-recognizer"], - function(RouteRecognizer) { + ["route-recognizer", "rsvp"], + function(RouteRecognizer, RSVP) { "use strict"; /** @private This file references several internal structures: @@ -23751,15 +24077,152 @@ * `{Object} handler`: a handler object * `{Object} context`: the active context for the handler */ + var slice = Array.prototype.slice; + + + + /** + @private + + A Transition is a thennable (a promise-like object) that represents + an attempt to transition to another route. It can be aborted, either + explicitly via `abort` or by attempting another transition while a + previous one is still underway. An aborted transition can also + be `retry()`d later. + */ + + function Transition(router, promise) { + this.router = router; + this.promise = promise; + this.data = {}; + this.resolvedModels = {}; + this.providedModels = {}; + this.providedModelsArray = []; + this.sequence = ++Transition.currentSequence; + this.params = {}; + } + + Transition.currentSequence = 0; + + Transition.prototype = { + targetName: null, + urlMethod: 'update', + providedModels: null, + resolvedModels: null, + params: null, + + /** + The Transition's internal promise. Calling `.then` on this property + is that same as calling `.then` on the Transition object itself, but + this property is exposed for when you want to pass around a + Transition's promise, but not the Transition object itself, since + Transition object can be externally `abort`ed, while the promise + cannot. + */ + promise: null, + + /** + Custom state can be stored on a Transition's `data` object. + This can be useful for decorating a Transition within an earlier + hook and shared with a later hook. Properties set on `data` will + be copied to new transitions generated by calling `retry` on this + transition. + */ + data: null, + + /** + A standard promise hook that resolves if the transition + succeeds and rejects if it fails/redirects/aborts. + + Forwards to the internal `promise` property which you can + use in situations where you want to pass around a thennable, + but not the Transition itself. + + @param {Function} success + @param {Function} failure + */ + then: function(success, failure) { + return this.promise.then(success, failure); + }, + + /** + Aborts the Transition. Note you can also implicitly abort a transition + by initiating another transition while a previous one is underway. + */ + abort: function() { + if (this.isAborted) { return this; } + log(this.router, this.sequence, this.targetName + ": transition was aborted"); + this.isAborted = true; + this.router.activeTransition = null; + return this; + }, + + /** + Retries a previously-aborted transition (making sure to abort the + transition if it's still active). Returns a new transition that + represents the new attempt to transition. + */ + retry: function() { + this.abort(); + + var recogHandlers = this.router.recognizer.handlersFor(this.targetName), + newTransition = performTransition(this.router, recogHandlers, this.providedModelsArray, this.params, this.data); + + return newTransition; + }, + + /** + Sets the URL-changing method to be employed at the end of a + successful transition. By default, a new Transition will just + use `updateURL`, but passing 'replace' to this method will + cause the URL to update using 'replaceWith' instead. Omitting + a parameter will disable the URL change, allowing for transitions + that don't update the URL at completion (this is also used for + handleURL, since the URL has already changed before the + transition took place). + + @param {String} method the type of URL-changing method to use + at the end of a transition. Accepted values are 'replace', + falsy values, or any other non-falsy value (which is + interpreted as an updateURL transition). + + @return {Transition} this transition + */ + method: function(method) { + this.urlMethod = method; + return this; + } + }; + function Router() { this.recognizer = new RouteRecognizer(); } + + /** + Promise reject reasons passed to promise rejection + handlers for failed transitions. + */ + Router.UnrecognizedURLError = function(message) { + this.message = (message || "UnrecognizedURLError"); + this.name = "UnrecognizedURLError"; + }; + + Router.TransitionAborted = function(message) { + this.message = (message || "TransitionAborted"); + this.name = "TransitionAborted"; + }; + + function errorTransition(router, reason) { + return new Transition(router, RSVP.reject(reason)); + } + + Router.prototype = { /** The main entry point into the router. The API is essentially the same as the `map` method in `route-recognizer`. @@ -23786,20 +24249,24 @@ Clears the current and target route handlers and triggers exit on each of them starting at the leaf and traversing up through its ancestors. */ reset: function() { - eachHandler(this.currentHandlerInfos || [], function(handler) { + eachHandler(this.currentHandlerInfos || [], function(handlerInfo) { + var handler = handlerInfo.handler; if (handler.exit) { handler.exit(); } }); this.currentHandlerInfos = null; this.targetHandlerInfos = null; }, + activeTransition: null, + /** + var handler = handlerInfo.handler; The entry point for handling a change to the URL (usually via the back and forward button). Returns an Array of handlers and the parameters associated with those parameters. @@ -23807,17 +24274,13 @@ @param {String} url a URL to process @return {Array} an Array of `[handler, parameter]` tuples */ handleURL: function(url) { - var results = this.recognizer.recognize(url); - - if (!results) { - throw new Error("No route matched the URL '" + url + "'"); - } - - collectObjects(this, results, 0, []); + // Perform a URL-based transition, but don't change + // the URL afterward, since it already happened. + return doTransition(this, arguments).method(null); }, /** Hook point for updating the URL. @@ -23845,12 +24308,11 @@ that are no longer represented by the target route. @param {String} name the name of the route */ transitionTo: function(name) { - var args = Array.prototype.slice.call(arguments, 1); - doTransition(this, name, this.updateURL, args); + return doTransition(this, arguments); }, /** Identical to `transitionTo` except that the current URL will be replaced if possible. @@ -23858,12 +24320,11 @@ This method is intended primarily for use with `replaceState`. @param {String} name the name of the route */ replaceWith: function(name) { - var args = Array.prototype.slice.call(arguments, 1); - doTransition(this, name, this.replaceURL, args); + return doTransition(this, arguments).method('replace'); }, /** @private @@ -23873,12 +24334,11 @@ @param {String} handlerName @param {Array[Object]} contexts @return {Object} a serialized parameter hash */ paramsForHandler: function(handlerName, callback) { - var output = this._paramsForHandler(handlerName, [].slice.call(arguments, 1)); - return output.params; + return paramsForHandler(this, handlerName, slice.call(arguments, 1)); }, /** Take a named route and context objects and generate a URL. @@ -23888,113 +24348,21 @@ @param {...Object} objects a list of objects to serialize @return {String} a URL */ generate: function(handlerName) { - var params = this.paramsForHandler.apply(this, arguments); + var params = paramsForHandler(this, handlerName, slice.call(arguments, 1)); return this.recognizer.generate(handlerName, params); }, - /** - @private - - Used internally by `generate` and `transitionTo`. - */ - _paramsForHandler: function(handlerName, objects, doUpdate) { - var handlers = this.recognizer.handlersFor(handlerName), - params = {}, - toSetup = [], - startIdx = handlers.length, - objectsToMatch = objects.length, - object, objectChanged, handlerObj, handler, names, i; - - // Find out which handler to start matching at - for (i=handlers.length-1; i>=0 && objectsToMatch>0; i--) { - if (handlers[i].names.length) { - objectsToMatch--; - startIdx = i; - } - } - - if (objectsToMatch > 0) { - throw "More context objects were passed than there are dynamic segments for the route: "+handlerName; - } - - // Connect the objects to the routes - for (i=0; i<handlers.length; i++) { - handlerObj = handlers[i]; - handler = this.getHandler(handlerObj.handler); - names = handlerObj.names; - objectChanged = false; - - // If it's a dynamic segment - if (names.length) { - // If we have objects, use them - if (i >= startIdx) { - object = objects.shift(); - objectChanged = true; - // Otherwise use existing context - } else { - object = handler.context; - } - - // Serialize to generate params - if (handler.serialize) { - merge(params, handler.serialize(object, names)); - } - // If it's not a dynamic segment and we're updating - } else if (doUpdate) { - // If we've passed the match point we need to deserialize again - // or if we never had a context - if (i > startIdx || !handler.hasOwnProperty('context')) { - if (handler.deserialize) { - object = handler.deserialize({}); - objectChanged = true; - } - // Otherwise use existing context - } else { - object = handler.context; - } - } - - // Make sure that we update the context here so it's available to - // subsequent deserialize calls - if (doUpdate && objectChanged) { - // TODO: It's a bit awkward to set the context twice, see if we can DRY things up - setContext(handler, object); - } - - toSetup.push({ - isDynamic: !!handlerObj.names.length, - name: handlerObj.handler, - handler: handler, - context: object - }); - - if (i === handlers.length - 1) { - var lastHandler = toSetup[toSetup.length - 1], - additionalHandler; - - if (additionalHandler = lastHandler.handler.additionalHandler) { - handlers.push({ - handler: additionalHandler.call(lastHandler.handler), - names: [] - }); - } - } - } - - return { params: params, toSetup: toSetup }; - }, - isActive: function(handlerName) { - var contexts = [].slice.call(arguments, 1); + var contexts = slice.call(arguments, 1); var targetHandlerInfos = this.targetHandlerInfos, found = false, names, object, handlerInfo, handlerObj; - if (!targetHandlerInfos) { return; } + if (!targetHandlerInfos) { return false; } for (var i=targetHandlerInfos.length-1; i>=0; i--) { handlerInfo = targetHandlerInfos[i]; if (handlerInfo.name === handlerName) { found = true; } @@ -24010,169 +24378,173 @@ return contexts.length === 0 && found; }, trigger: function(name) { - var args = [].slice.call(arguments); - trigger(this, args); - } - }; + var args = slice.call(arguments); + trigger(this.currentHandlerInfos, false, args); + }, - function merge(hash, other) { - for (var prop in other) { - if (other.hasOwnProperty(prop)) { hash[prop] = other[prop]; } - } - } + /** + Hook point for logging transition status updates. - function isCurrent(currentHandlerInfos, handlerName) { - return currentHandlerInfos[currentHandlerInfos.length - 1].name === handlerName; - } + @param {String} message The message to log. + */ + log: null + }; /** @private - This function is called the first time the `collectObjects` - function encounters a promise while converting URL parameters - into objects. + Used internally for both URL and named transition to determine + a shared pivot parent route and other data necessary to perform + a transition. + */ + function getMatchPoint(router, handlers, objects, inputParams) { - It triggers the `enter` and `setup` methods on the `loading` - handler. + var objectsToMatch = objects.length, + matchPoint = handlers.length, + providedModels = {}, i, + currentHandlerInfos = router.currentHandlerInfos || [], + params = {}, + oldParams = router.currentParams || {}, + activeTransition = router.activeTransition, + handlerParams = {}; - @param {Router} router - */ - function loading(router) { - if (!router.isLoading) { - router.isLoading = true; - var handler = router.getHandler('loading'); + merge(params, inputParams); + + for (i = handlers.length - 1; i >= 0; i--) { + var handlerObj = handlers[i], + handlerName = handlerObj.handler, + oldHandlerInfo = currentHandlerInfos[i], + hasChanged = false; - if (handler) { - if (handler.enter) { handler.enter(); } - if (handler.setup) { handler.setup(); } - } + // Check if handler names have changed. + if (!oldHandlerInfo || oldHandlerInfo.name !== handlerObj.handler) { hasChanged = true; } + + if (handlerObj.isDynamic) { + // URL transition. + + if (objectsToMatch > 0) { + hasChanged = true; + providedModels[handlerName] = objects[--objectsToMatch]; + } else { + handlerParams[handlerName] = {}; + for (var prop in handlerObj.params) { + if (!handlerObj.params.hasOwnProperty(prop)) { continue; } + var newParam = handlerObj.params[prop]; + if (oldParams[prop] !== newParam) { hasChanged = true; } + handlerParams[handlerName][prop] = params[prop] = newParam; + } + } + } else if (handlerObj.hasOwnProperty('names') && handlerObj.names.length) { + // Named transition. + + if (objectsToMatch > 0) { + hasChanged = true; + providedModels[handlerName] = objects[--objectsToMatch]; + } else if (activeTransition && activeTransition.providedModels[handlerName]) { + + // Use model from previous transition attempt, preferably the resolved one. + hasChanged = true; + providedModels[handlerName] = activeTransition.providedModels[handlerName] || + activeTransition.resolvedModels[handlerName]; + } else { + var names = handlerObj.names; + handlerParams[handlerName] = {}; + for (var j = 0, len = names.length; j < len; ++j) { + var name = names[j]; + handlerParams[handlerName][name] = params[name] = oldParams[name]; + } + } + } + + if (hasChanged) { matchPoint = i; } } + + if (objectsToMatch > 0) { + throw "More context objects were passed than there are dynamic segments for the route: " + handlers[handlers.length - 1].handler; + } + + return { matchPoint: matchPoint, providedModels: providedModels, params: params, handlerParams: handlerParams }; } /** @private - This function is called if a promise was previously - encountered once all promises are resolved. + This method takes a handler name and a list of contexts and returns + a serialized parameter hash suitable to pass to `recognizer.generate()`. - It triggers the `exit` method on the `loading` handler. - @param {Router} router + @param {String} handlerName + @param {Array[Object]} objects + @return {Object} a serialized parameter hash */ - function loaded(router) { - router.isLoading = false; - var handler = router.getHandler('loading'); - if (handler && handler.exit) { handler.exit(); } - } + function paramsForHandler(router, handlerName, objects) { - /** - @private + var handlers = router.recognizer.handlersFor(handlerName), + params = {}, + matchPoint = getMatchPoint(router, handlers, objects).matchPoint, + object, handlerObj, handler, names, i; - This function is called if any encountered promise - is rejected. + for (i=0; i<handlers.length; i++) { + handlerObj = handlers[i]; + handler = router.getHandler(handlerObj.handler); + names = handlerObj.names; - It triggers the `exit` method on the `loading` handler, - the `enter` method on the `failure` handler, and the - `setup` method on the `failure` handler with the - `error`. + // If it's a dynamic segment + if (names.length) { + // If we have objects, use them + if (i >= matchPoint) { + object = objects.shift(); + // Otherwise use existing context + } else { + object = handler.context; + } - @param {Router} router - @param {Object} error the reason for the promise - rejection, to pass into the failure handler's - `setup` method. - */ - function failure(router, error) { - loaded(router); - var handler = router.getHandler('failure'); - if (handler) { - if (handler.enter) { handler.enter(); } - if (handler.setup) { handler.setup(error); } + // Serialize to generate params + merge(params, serialize(handler, object, names)); + } } + return params; } + function merge(hash, other) { + for (var prop in other) { + if (other.hasOwnProperty(prop)) { hash[prop] = other[prop]; } + } + } + /** @private */ - function doTransition(router, name, method, args) { - var output = router._paramsForHandler(name, args, true); - var params = output.params, toSetup = output.toSetup; + function createNamedTransition(router, args) { + var handlers = router.recognizer.handlersFor(args[0]); - var url = router.recognizer.generate(name, params); - method.call(router, url); + log(router, "Attempting transition to " + args[0]); - setupContexts(router, toSetup); + return performTransition(router, handlers, slice.call(args, 1), router.currentParams); } /** @private - - This function is called after a URL change has been handled - by `router.handleURL`. - - Takes an Array of `RecognizedHandler`s, and converts the raw - params hashes into deserialized objects by calling deserialize - on the handlers. This process builds up an Array of - `HandlerInfo`s. It then calls `setupContexts` with the Array. - - If the `deserialize` method on a handler returns a promise - (i.e. has a method called `then`), this function will pause - building up the `HandlerInfo` Array until the promise is - resolved. It will use the resolved value as the context of - `HandlerInfo`. */ - function collectObjects(router, results, index, objects) { - if (results.length === index) { - var lastObject = objects[objects.length - 1], - lastHandler = lastObject && lastObject.handler; + function createURLTransition(router, url) { - if (lastHandler && lastHandler.additionalHandler) { - var additionalResult = { - handler: lastHandler.additionalHandler(), - params: {}, - isDynamic: false - }; - results.push(additionalResult); - } else { - loaded(router); - setupContexts(router, objects); - return; - } - } + var results = router.recognizer.recognize(url), + currentHandlerInfos = router.currentHandlerInfos; - var result = results[index]; - var handler = router.getHandler(result.handler); - var object = handler.deserialize && handler.deserialize(result.params); + log(router, "Attempting URL transition to " + url); - if (object && typeof object.then === 'function') { - loading(router); - - // The chained `then` means that we can also catch errors that happen in `proceed` - object.then(proceed).then(null, function(error) { - failure(router, error); - }); - } else { - proceed(object); + if (!results) { + return errorTransition(router, new Router.UnrecognizedURLError(url)); } - function proceed(value) { - if (handler.context !== object) { - setContext(handler, object); - } - - var updatedObjects = objects.concat([{ - context: value, - name: result.handler, - handler: router.getHandler(result.handler), - isDynamic: result.isDynamic - }]); - collectObjects(router, results, index + 1, updatedObjects); - } + return performTransition(router, results, [], {}); } + /** @private Takes an Array of `HandlerInfo`s, figures out which ones are exiting, entering, or changing contexts, and calls the @@ -24191,11 +24563,11 @@ ``` Consider the following transitions: 1. A URL transition to `/posts/1`. - 1. Triggers the `deserialize` callback on the + 1. Triggers the `*model` callbacks on the `index`, `posts`, and `showPost` handlers 2. Triggers the `enter` callback on the same 3. Triggers the `setup` callback on the same 2. A direct transition to `newPost` 1. Triggers the `exit` callback on `showPost` @@ -24207,54 +24579,73 @@ and `posts` 2. Triggers the `serialize` callback on `about` 3. Triggers the `enter` callback on `about` 4. Triggers the `setup` callback on `about` - @param {Router} router + @param {Transition} transition @param {Array[HandlerInfo]} handlerInfos */ - function setupContexts(router, handlerInfos) { - var partition = - partitionHandlers(router.currentHandlerInfos || [], handlerInfos); + function setupContexts(transition, handlerInfos) { + var router = transition.router, + partition = partitionHandlers(router.currentHandlerInfos || [], handlerInfos); router.targetHandlerInfos = handlerInfos; - eachHandler(partition.exited, function(handler, context) { + eachHandler(partition.exited, function(handlerInfo) { + var handler = handlerInfo.handler; delete handler.context; if (handler.exit) { handler.exit(); } }); var currentHandlerInfos = partition.unchanged.slice(); router.currentHandlerInfos = currentHandlerInfos; - eachHandler(partition.updatedContext, function(handler, context, handlerInfo) { - setContext(handler, context); - if (handler.setup) { handler.setup(context); } - currentHandlerInfos.push(handlerInfo); + eachHandler(partition.updatedContext, function(handlerInfo) { + handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, false); }); - var aborted = false; - eachHandler(partition.entered, function(handler, context, handlerInfo) { - if (aborted) { return; } - if (handler.enter) { handler.enter(); } + eachHandler(partition.entered, function(handlerInfo) { + handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, true); + }); + + if (router.didTransition) { + router.didTransition(handlerInfos); + } + } + + /** + @private + + Helper method used by setupContexts. Handles errors or redirects + that may happen in enter/setup. + */ + function handlerEnteredOrUpdated(transition, currentHandlerInfos, handlerInfo, enter) { + var handler = handlerInfo.handler, + context = handlerInfo.context; + + try { + if (enter && handler.enter) { handler.enter(); } + checkAbort(transition); + setContext(handler, context); - if (handler.setup) { - if (false === handler.setup(context)) { - aborted = true; - } - } - if (!aborted) { - currentHandlerInfos.push(handlerInfo); + if (handler.setup) { handler.setup(context); } + checkAbort(transition); + } catch(e) { + if (!(e instanceof Router.TransitionAborted)) { + // Trigger the `error` event starting from this failed handler. + trigger(currentHandlerInfos.concat(handlerInfo), true, ['error', e, transition]); } - }); - if (!aborted && router.didTransition) { - router.didTransition(handlerInfos); + // Propagate the error so that the transition promise will reject. + throw e; } + + currentHandlerInfos.push(handlerInfo); } + /** @private Iterates over an array of `HandlerInfo`s, passing the handler and context into the callback. @@ -24262,15 +24653,11 @@ @param {Array[HandlerInfo]} handlerInfos @param {Function(Object, Object)} callback */ function eachHandler(handlerInfos, callback) { for (var i=0, l=handlerInfos.length; i<l; i++) { - var handlerInfo = handlerInfos[i], - handler = handlerInfo.handler, - context = handlerInfo.context; - - callback(handler, context, handlerInfo); + callback(handlerInfos[i]); } } /** @private @@ -24346,23 +24733,23 @@ } return handlers; } - function trigger(router, args) { - var currentHandlerInfos = router.currentHandlerInfos; + function trigger(handlerInfos, ignoreFailure, args) { var name = args.shift(); - if (!currentHandlerInfos) { + if (!handlerInfos) { + if (ignoreFailure) { return; } throw new Error("Could not trigger event '" + name + "'. There are no active handlers"); } var eventWasHandled = false; - for (var i=currentHandlerInfos.length-1; i>=0; i--) { - var handlerInfo = currentHandlerInfos[i], + for (var i=handlerInfos.length-1; i>=0; i--) { + var handlerInfo = handlerInfos[i], handler = handlerInfo.handler; if (handler.events && handler.events[name]) { if (handler.events[name].apply(handler, args) === true) { eventWasHandled = true; @@ -24370,23 +24757,385 @@ return; } } } - if (!eventWasHandled) { + if (!eventWasHandled && !ignoreFailure) { throw new Error("Nothing handled the event '" + name + "'."); } } function setContext(handler, context) { handler.context = context; if (handler.contextDidChange) { handler.contextDidChange(); } } + + /** + @private + + Creates, begins, and returns a Transition. + */ + function performTransition(router, recogHandlers, providedModelsArray, params, data) { + + var matchPointResults = getMatchPoint(router, recogHandlers, providedModelsArray, params), + targetName = recogHandlers[recogHandlers.length - 1].handler, + wasTransitioning = false; + + // Check if there's already a transition underway. + if (router.activeTransition) { + if (transitionsIdentical(router.activeTransition, targetName, providedModelsArray)) { + return router.activeTransition; + } + router.activeTransition.abort(); + wasTransitioning = true; + } + + var deferred = RSVP.defer(), + transition = new Transition(router, deferred.promise); + + transition.targetName = targetName; + transition.providedModels = matchPointResults.providedModels; + transition.providedModelsArray = providedModelsArray; + transition.params = matchPointResults.params; + transition.data = data || {}; + router.activeTransition = transition; + + var handlerInfos = generateHandlerInfos(router, recogHandlers); + + // Fire 'willTransition' event on current handlers, but don't fire it + // if a transition was already underway. + if (!wasTransitioning) { + trigger(router.currentHandlerInfos, true, ['willTransition', transition]); + } + + log(router, transition.sequence, "Beginning validation for transition to " + transition.targetName); + validateEntry(transition, handlerInfos, 0, matchPointResults.matchPoint, matchPointResults.handlerParams) + .then(transitionSuccess, transitionFailure); + + return transition; + + function transitionSuccess() { + checkAbort(transition); + + try { + finalizeTransition(transition, handlerInfos); + + // Resolve with the final handler. + deferred.resolve(handlerInfos[handlerInfos.length - 1].handler); + } catch(e) { + deferred.reject(e); + } + + // Don't nullify if another transition is underway (meaning + // there was a transition initiated with enter/setup). + if (!transition.isAborted) { + router.activeTransition = null; + } + } + + function transitionFailure(reason) { + deferred.reject(reason); + } + } + + /** + @private + + Accepts handlers in Recognizer format, either returned from + recognize() or handlersFor(), and returns unified + `HandlerInfo`s. + */ + function generateHandlerInfos(router, recogHandlers) { + var handlerInfos = []; + for (var i = 0, len = recogHandlers.length; i < len; ++i) { + var handlerObj = recogHandlers[i], + isDynamic = handlerObj.isDynamic || (handlerObj.names && handlerObj.names.length); + + handlerInfos.push({ + isDynamic: !!isDynamic, + name: handlerObj.handler, + handler: router.getHandler(handlerObj.handler) + }); + } + return handlerInfos; + } + + /** + @private + */ + function transitionsIdentical(oldTransition, targetName, providedModelsArray) { + + if (oldTransition.targetName !== targetName) { return false; } + + var oldModels = oldTransition.providedModelsArray; + if (oldModels.length !== providedModelsArray.length) { return false; } + + for (var i = 0, len = oldModels.length; i < len; ++i) { + if (oldModels[i] !== providedModelsArray[i]) { return false; } + } + return true; + } + + /** + @private + + Updates the URL (if necessary) and calls `setupContexts` + to update the router's array of `currentHandlerInfos`. + */ + function finalizeTransition(transition, handlerInfos) { + + var router = transition.router, + seq = transition.sequence, + handlerName = handlerInfos[handlerInfos.length - 1].name; + + log(router, seq, "Validation succeeded, finalizing transition;"); + + // Collect params for URL. + var objects = []; + for (var i = 0, len = handlerInfos.length; i < len; ++i) { + var handlerInfo = handlerInfos[i]; + if (handlerInfo.isDynamic) { + objects.push(handlerInfo.context); + } + } + + var params = paramsForHandler(router, handlerName, objects); + + transition.providedModelsArray = []; + transition.providedContexts = {}; + router.currentParams = params; + + var urlMethod = transition.urlMethod; + if (urlMethod) { + var url = router.recognizer.generate(handlerName, params); + + if (urlMethod === 'replace') { + router.replaceURL(url); + } else { + // Assume everything else is just a URL update for now. + router.updateURL(url); + } + } + + setupContexts(transition, handlerInfos); + log(router, seq, "TRANSITION COMPLETE."); + } + + /** + @private + + Internal function used to construct the chain of promises used + to validate a transition. Wraps calls to `beforeModel`, `model`, + and `afterModel` in promises, and checks for redirects/aborts + between each. + */ + function validateEntry(transition, handlerInfos, index, matchPoint, handlerParams) { + + if (index === handlerInfos.length) { + // No more contexts to resolve. + return RSVP.resolve(transition.resolvedModels); + } + + var router = transition.router, + handlerInfo = handlerInfos[index], + handler = handlerInfo.handler, + handlerName = handlerInfo.name, + seq = transition.sequence, + errorAlreadyHandled = false, + resolvedModel; + + if (index < matchPoint) { + log(router, seq, handlerName + ": using context from already-active handler"); + + // We're before the match point, so don't run any hooks, + // just use the already resolved context from the handler. + resolvedModel = handlerInfo.handler.context; + return proceed(); + } + + return RSVP.resolve().then(handleAbort) + .then(beforeModel) + .then(null, handleError) + .then(handleAbort) + .then(model) + .then(null, handleError) + .then(handleAbort) + .then(afterModel) + .then(null, handleError) + .then(handleAbort) + .then(proceed); + + function handleAbort(result) { + + if (transition.isAborted) { + log(transition.router, transition.sequence, "detected abort."); + errorAlreadyHandled = true; + return RSVP.reject(new Router.TransitionAborted()); + } + + return result; + } + + function handleError(reason) { + + if (errorAlreadyHandled) { return RSVP.reject(reason); } + errorAlreadyHandled = true; + transition.abort(); + + log(router, seq, handlerName + ": handling error: " + reason); + + // An error was thrown / promise rejected, so fire an + // `error` event from this handler info up to root. + trigger(handlerInfos.slice(0, index + 1), true, ['error', reason, transition]); + + if (handler.error) { + handler.error(reason, transition); } + + + // Propagate the original error. + return RSVP.reject(reason); + } + + function beforeModel() { + + log(router, seq, handlerName + ": calling beforeModel hook"); + + return handler.beforeModel && handler.beforeModel(transition); + } + + function model() { + log(router, seq, handlerName + ": resolving model"); + + return getModel(handlerInfo, transition, handlerParams[handlerName], index >= matchPoint); + } + + function afterModel(context) { + + log(router, seq, handlerName + ": calling afterModel hook"); + + // Pass the context and resolved parent contexts to afterModel, but we don't + // want to use the value returned from `afterModel` in any way, but rather + // always resolve with the original `context` object. + + resolvedModel = context; + return handler.afterModel && handler.afterModel(resolvedModel, transition); + } + + function proceed() { + log(router, seq, handlerName + ": validation succeeded, proceeding"); + + handlerInfo.context = transition.resolvedModels[handlerInfo.name] = resolvedModel; + return validateEntry(transition, handlerInfos, index + 1, matchPoint, handlerParams); + } + } + + /** + @private + + Throws a TransitionAborted if the provided transition has been aborted. + */ + function checkAbort(transition) { + if (transition.isAborted) { + log(transition.router, transition.sequence, "detected abort."); + throw new Router.TransitionAborted(); + } + } + + /** + @private + + Encapsulates the logic for whether to call `model` on a route, + or use one of the models provided to `transitionTo`. + */ + function getModel(handlerInfo, transition, handlerParams, needsUpdate) { + + var handler = handlerInfo.handler, + handlerName = handlerInfo.name; + + if (!needsUpdate && handler.hasOwnProperty('context')) { + return handler.context; + } + + if (handlerInfo.isDynamic && transition.providedModels.hasOwnProperty(handlerName)) { + var providedModel = transition.providedModels[handlerName]; + return typeof providedModel === 'function' ? providedModel() : providedModel; + } + + return handler.model && handler.model(handlerParams || {}, transition); + } + + /** + @private + */ + function log(router, sequence, msg) { + + if (!router.log) { return; } + + if (arguments.length === 3) { + router.log("Transition #" + sequence + ": " + msg); + } else { + msg = sequence; + router.log(msg); + } + } + + /** + @private + + Begins and returns a Transition based on the provided + arguments. Accepts arguments in the form of both URL + transitions and named transitions. + + @param {Router} router + @param {Array[Object]} args arguments passed to transitionTo, + replaceWith, or handleURL + */ + function doTransition(router, args) { + // Normalize blank transitions to root URL transitions. + var name = args[0] || '/'; + + if (name.charAt(0) === '/') { + return createURLTransition(router, name); + } else { + return createNamedTransition(router, args); + } + } + + /** + @private + + Serializes a handler using its custom `serialize` method or + by a default that looks up the expected property name from + the dynamic segment. + + @param {Object} handler a router handler + @param {Object} model the model to be serialized for this handler + @param {Array[Object]} names the names array attached to an + handler object returned from router.recognizer.handlersFor() + */ + function serialize(handler, model, names) { + + // Use custom serialize if it exists. + if (handler.serialize) { + return handler.serialize(model, names); + } + + if (names.length !== 1) { return; } + + var name = names[0], object = {}; + + if (/_id$/.test(name)) { + object[name] = model.id; + } else { + object[name] = model; + } + return object; + } + return Router; }); - })(); (function() { @@ -24568,11 +25317,11 @@ */ Ember.Router = Ember.Object.extend({ location: 'hash', init: function() { - this.router = this.constructor.router; + this.router = this.constructor.router || this.constructor.map(Ember.K); this._activeViews = {}; setupLocation(this); }, url: Ember.computed(function() { @@ -24613,38 +25362,25 @@ Ember.Logger.log("Transitioned into '" + path + "'"); } }, handleURL: function(url) { - this.router.handleURL(url); - this.notifyPropertyChange('url'); - }, + scheduleLoadingStateEntry(this); - /** - Transition to another route via the `routeTo` event which - will by default be handled by ApplicationRoute. + var self = this; - @method routeTo - @param {TransitionEvent} transitionEvent - */ - routeTo: function(transitionEvent) { - var handlerInfos = this.router.currentHandlerInfos; - if (handlerInfos) { - transitionEvent.sourceRoute = handlerInfos[handlerInfos.length - 1].handler; - } - - this.send('routeTo', transitionEvent); + return this.router.handleURL(url).then(function() { + transitionCompleted(self); + }); }, - transitionTo: function(name) { - var args = [].slice.call(arguments); - doTransition(this, 'transitionTo', args); + transitionTo: function() { + return doTransition(this, 'transitionTo', arguments); }, replaceWith: function() { - var args = [].slice.call(arguments); - doTransition(this, 'replaceWith', args); + return doTransition(this, 'replaceWith', arguments); }, generate: function() { var url = this.router.generate.apply(this.router, arguments); return this.location.formatURL(url); @@ -24694,21 +25430,10 @@ this._activeViews[templateName] = [view, disconnect]; view.one('willDestroyElement', this, disconnect); } }); -Ember.Router.reopenClass({ - defaultFailureHandler: { - setup: function(error) { - Ember.Logger.error('Error while loading route:', error); - - // Using setTimeout allows us to escape from the Promise's try/catch block - setTimeout(function() { throw error; }); - } - } -}); - function getHandlerFunction(router) { var seen = {}, container = router.container, DefaultRoute = container.resolve('route:basic'); return function(name) { @@ -24719,31 +25444,38 @@ seen[name] = true; if (!handler) { if (name === 'loading') { return {}; } - if (name === 'failure') { return router.constructor.defaultFailureHandler; } container.register(routeName, DefaultRoute.extend()); handler = container.lookup(routeName); if (get(router, 'namespace.LOG_ACTIVE_GENERATION')) { Ember.Logger.info("generated -> " + routeName, { fullName: routeName }); } } if (name === 'application') { - // Inject default `routeTo` handler. + // Inject default `error` handler. handler.events = handler.events || {}; - handler.events.routeTo = handler.events.routeTo || Ember.TransitionEvent.defaultHandler; + handler.events.error = handler.events.error || defaultErrorHandler; } handler.routeName = name; return handler; }; } +function defaultErrorHandler(error, transition) { + Ember.Logger.error('Error while loading route:', error); + + // Using setTimeout allows us to escape from the Promise's try/catch block + setTimeout(function() { throw error; }); +} + + function routePath(handlerInfos) { var path = []; for (var i=1, l=handlerInfos.length; i<l; i++) { var name = handlerInfos[i].name, @@ -24784,28 +25516,80 @@ emberRouter.didTransition(infos); }; } function doTransition(router, method, args) { + // Normalize blank route to root URL. + args = [].slice.call(args); + args[0] = args[0] || '/'; + var passedName = args[0], name; - if (!router.router.hasRoute(args[0])) { - name = args[0] = passedName + '.index'; - } else { + if (passedName.charAt(0) === '/') { name = passedName; + } else { + if (!router.router.hasRoute(passedName)) { + name = args[0] = passedName + '.index'; + } else { + name = passedName; + } + + Ember.assert("The route " + passedName + " was not found", router.router.hasRoute(name)); } - Ember.assert("The route " + passedName + " was not found", router.router.hasRoute(name)); + scheduleLoadingStateEntry(router); - router.router[method].apply(router.router, args); + var transitionPromise = router.router[method].apply(router.router, args); + transitionPromise.then(function() { + transitionCompleted(router); + }); + + // We want to return the configurable promise object + // so that callers of this function can use `.method()` on it, + // which obviously doesn't exist for normal RSVP promises. + return transitionPromise; +} + +function scheduleLoadingStateEntry(router) { + if (router._loadingStateActive) { return; } + router._shouldEnterLoadingState = true; + Ember.run.scheduleOnce('routerTransitions', null, enterLoadingState, router); +} + +function enterLoadingState(router) { + if (router._loadingStateActive || !router._shouldEnterLoadingState) { return; } + + var loadingRoute = router.router.getHandler('loading'); + if (loadingRoute) { + if (loadingRoute.enter) { loadingRoute.enter(); } + if (loadingRoute.setup) { loadingRoute.setup(); } + router._loadingStateActive = true; + } +} + +function exitLoadingState(router) { + router._shouldEnterLoadingState = false; + if (!router._loadingStateActive) { return; } + + var loadingRoute = router.router.getHandler('loading'); + if (loadingRoute && loadingRoute.exit) { loadingRoute.exit(); } + router._loadingStateActive = false; +} + +function transitionCompleted(router) { router.notifyPropertyChange('url'); + exitLoadingState(router); } Ember.Router.reopenClass({ map: function(callback) { var router = this.router = new Router(); + if (get(this, 'namespace.LOG_TRANSITIONS_INTERNAL')) { + router.log = Ember.Logger.debug; + } + var dsl = Ember.RouterDSL.map(function() { this.resource('application', { path: "/" }, function() { callback.call(this); }); }); @@ -24813,10 +25597,11 @@ router.map(dsl.generate()); return router; } }); + })(); (function() { @@ -24825,11 +25610,13 @@ @submodule ember-routing */ var get = Ember.get, set = Ember.set, classify = Ember.String.classify, - fmt = Ember.String.fmt; + fmt = Ember.String.fmt, + a_forEach = Ember.EnumerableUtils.forEach, + a_replace = Ember.EnumerableUtils.replace; /** The `Ember.Route` class is used to define individual routes. Refer to the [routing guide](http://emberjs.com/guides/routing/) for documentation. @@ -24843,11 +25630,11 @@ @method exit */ exit: function() { this.deactivate(); - teardownView(this); + this.teardownViews(); }, /** @private @@ -24867,10 +25654,108 @@ Events can also be invoked from other parts of your application via `Route#send` or `Controller#send`. The context of the event will be this route. + ## Bubbling + + By default, an event will stop bubbling once a handler defined + on the `events` hash handles it. To continue bubbling the event, + you must return `true` from the handler. + + ## Built-in events + + There are a few built-in events pertaining to transitions that you + can use to customize transition behavior: `willTransition` and + `error`. + + ### `willTransition` + + The `willTransition` event is fired at the beginning of any + attempted transition with a `Transition` object as the sole + argument. This event can be used for aborting, redirecting, + or decorating the transition from the currently active routes. + + A good example is preventing navigation when a form is + half-filled out: + + ```js + App.ContactFormRoute = Ember.Route.extend({ + events: { + willTransition: function(transition) { + if (this.controller.get('userHasEnteredData')) { + this.controller.displayNavigationConfirm(); + transition.abort(); + } + } + } + }); + ``` + + You can also redirect elsewhere by calling + `this.transitionTo('elsewhere')` from within `willTransition`. + Note that `willTransition` will not be fired for the + redirecting `transitionTo`, since `willTransition` doesn't + fire when there is already a transition underway. If you want + subsequent `willTransition` events to fire for the redirecting + transition, you must first explicitly call + `transition.abort()`. + + ### `error` + + When attempting to transition into a route, any of the hooks + may throw an error, or return a promise that rejects, at which + point an `error` event will be fired on the partially-entered + routes, allowing for per-route error handling logic, or shared + error handling logic defined on a parent route. + + Here is an example of an error handler that will be invoked + for rejected promises / thrown errors from the various hooks + on the route, as well as any unhandled errors from child + routes: + + ```js + App.AdminRoute = Ember.Route.extend({ + beforeModel: function() { + throw "bad things!"; + // ...or, equivalently: + return Ember.RSVP.reject("bad things!"); + }, + + events: { + error: function(error, transition) { + // Assuming we got here due to the error in `beforeModel`, + // we can expect that error === "bad things!", + // but a promise model rejecting would also + // call this hook, as would any errors encountered + // in `afterModel`. + + // The `error` hook is also provided the failed + // `transition`, which can be stored and later + // `.retry()`d if desired. + + this.transitionTo('login'); + } + } + }); + ``` + + `error` events that bubble up all the way to `ApplicationRoute` + will fire a default error handler that logs the error. You can + specify your own global default error handler by overriding the + `error` handler on `ApplicationRoute`: + + ```js + App.ApplicationRoute = Ember.Route.extend({ + events: { + error: function(error, transition) { + this.controllerFor('banner').displayError(error.message); + } + } + }); + ``` + @see {Ember.Route#send} @see {Handlebars.helpers.action} @property events @type Hash @@ -24893,38 +25778,20 @@ @method activate */ activate: Ember.K, /** - Transition to another route via the `routeTo` event which - will by default be handled by ApplicationRoute. - - @method routeTo - @param {TransitionEvent} transitionEvent - */ - routeTo: function(transitionEvent) { - this.router.routeTo(transitionEvent); - }, - - /** Transition into another route. Optionally supply a model for the route in question. The model will be serialized into the URL using the `serialize` hook. @method transitionTo @param {String} name the name of the route @param {...Object} models the */ transitionTo: function(name, context) { var router = this.router; - - // If the transition is a no-op, just bail. - if (router.isActive.apply(router, arguments)) { - return; - } - - if (this._checkingRedirect) { this._redirected[this._redirectDepth] = true; } return router.transitionTo.apply(router, arguments); }, /** Transition into another route while replacing the current URL if @@ -24937,93 +25804,25 @@ @param {String} name the name of the route @param {...Object} models the */ replaceWith: function() { var router = this.router; - - // If the transition is a no-op, just bail. - if (router.isActive.apply(router, arguments)) { - return; - } - - if (this._checkingRedirect) { this._redirected[this._redirectDepth] = true; } return this.router.replaceWith.apply(this.router, arguments); }, send: function() { return this.router.send.apply(this.router, arguments); }, /** - @private - - Internal counter for tracking whether a route handler has - called transitionTo or replaceWith inside its redirect hook. - - */ - _redirectDepth: 0, - - /** @private This hook is the entry point for router.js @method setup */ setup: function(context) { - // Determine if this is the top-most transition. - // If so, we'll set up a data structure to track - // whether `transitionTo` or replaceWith gets called - // inside our `redirect` hook. - // - // This is necessary because we set a flag on the route - // inside transitionTo/replaceWith to determine afterwards - // if they were called, but `setup` can be called - // recursively and we need to disambiguate where in the - // call stack the redirect happened. - - // Are we the first call to setup? If so, set up the - // redirect tracking data structure, and remember that - // we're the top-most so we can clean it up later. - var isTop; - if (!this._redirected) { - isTop = true; - this._redirected = []; - } - - // Set a flag on this route saying that we are interested in - // tracking redirects, and increment the depth count. - this._checkingRedirect = true; - var depth = ++this._redirectDepth; - - // Check to see if context is set. This check preserves - // the correct arguments.length inside the `redirect` hook. - if (context === undefined) { - this.redirect(); - } else { - this.redirect(context); - } - - // After the call to `redirect` returns, decrement the depth count. - this._redirectDepth--; - this._checkingRedirect = false; - - // Save off the data structure so we can reset it on the route but - // still reference it later in this method. - var redirected = this._redirected; - - // If this is the top `setup` call in the call stack, clear the - // redirect tracking data structure. - if (isTop) { this._redirected = null; } - - // If we were redirected, there is nothing left for us to do. - // Returning false tells router.js not to continue calling setup - // on any children route handlers. - if (redirected[depth]) { - return false; - } - var controller = this.controllerFor(this.routeName, context); // Assign the route's controller so that it can more easily be // referenced in event handlers this.controller = controller; @@ -25042,33 +25841,135 @@ this.renderTemplate(controller, context); } }, /** + @deprecated + A hook you can implement to optionally redirect to another route. If you call `this.transitionTo` from inside of this hook, this route will not be entered in favor of the other hook. + This hook is deprecated in favor of using the `afterModel` hook + for performing redirects after the model has resolved. + @method redirect @param {Object} model the model for this route */ redirect: Ember.K, /** - @private + This hook is the first of the route entry validation hooks + called when an attempt is made to transition into a route + or one of its children. It is called before `model` and + `afterModel`, and is appropriate for cases when: - The hook called by `router.js` to convert parameters into the context - for this handler. The public Ember hook is `model`. + 1) A decision can be made to redirect elsewhere without + needing to resolve the model first. + 2) Any async operations need to occur first before the + model is attempted to be resolved. - @method deserialize + This hook is provided the current `transition` attempt + as a parameter, which can be used to `.abort()` the transition, + save it for a later `.retry()`, or retrieve values set + on it from a previous hook. You can also just call + `this.transitionTo` to another route to implicitly + abort the `transition`. + + You can return a promise from this hook to pause the + transition until the promise resolves (or rejects). This could + be useful, for instance, for retrieving async code from + the server that is required to enter a route. + + ```js + App.PostRoute = Ember.Route.extend({ + beforeModel: function(transition) { + if (!App.Post) { + return Ember.$.getScript('/models/post.js'); + } + } + }); + ``` + + If `App.Post` doesn't exist in the above example, + `beforeModel` will use jQuery's `getScript`, which + returns a promise that resolves after the server has + successfully retrieved and executed the code from the + server. Note that if an error were to occur, it would + be passed to the `error` hook on `Ember.Route`, but + it's also possible to handle errors specific to + `beforeModel` right from within the hook (to distinguish + from the shared error handling behavior of the `error` + hook): + + ```js + App.PostRoute = Ember.Route.extend({ + beforeModel: function(transition) { + if (!App.Post) { + var self = this; + return Ember.$.getScript('post.js').then(null, function(e) { + self.transitionTo('help'); + + // Note that the above transitionTo will implicitly + // halt the transition. If you were to return + // nothing from this promise reject handler, + // according to promise semantics, that would + // convert the reject into a resolve and the + // transition would continue. To propagate the + // error so that it'd be handled by the `error` + // hook, you would have to either + return Ember.RSVP.reject(e); + // or + throw e; + }); + } + } + }); + ``` + + @param {Transition} transition + @return {Promise} if the value returned from this hook is + a promise, the transition will pause until the transition + resolves. Otherwise, non-promise return values are not + utilized in any way. */ - deserialize: function(params) { - var model = this.model(params); - return this.currentModel = model; + beforeModel: Ember.K, + + /** + This hook is called after this route's model has resolved. + It follows identical async/promise semantics to `beforeModel` + but is provided the route's resolved model in addition to + the `transition`, and is therefore suited to performing + logic that can only take place after the model has already + resolved. + + ```js + App.PostRoute = Ember.Route.extend({ + afterModel: function(posts, transition) { + if (posts.length === 1) { + this.transitionTo('post.show', posts[0]); + } + } + }); + ``` + + Refer to documentation for `beforeModel` for a description + of transition-pausing semantics when a promise is returned + from this hook. + + @param {Transition} transition + @return {Promise} if the value returned from this hook is + a promise, the transition will pause until the transition + resolves. Otherwise, non-promise return values are not + utilized in any way. + */ + afterModel: function(resolvedModel, transition) { + this.redirect(resolvedModel, transition); }, + /** @private Called when the context is changed by router.js. @@ -25102,14 +26003,19 @@ through a transition (e.g. when using the `linkTo` Handlebars helper), then a model context is already provided and this hook is not called. Routes without dynamic segments will always execute the model hook. + This hook follows the asynchronous/promise semantics + described in the documentation for `beforeModel`. In particular, + if a promise returned from `model` fails, the error will be + handled by the `error` hook on `Ember.Route`. + @method model @param {Object} params the parameters extracted from the URL */ - model: function(params) { + model: function(params, resolvedParentModels) { var match, name, sawParams, value; for (var prop in params) { if (match = prop.match(/^(.*)_id$/)) { name = match[1]; @@ -25263,11 +26169,23 @@ @method modelFor @param {String} name the name of the route @return {Object} the model object */ modelFor: function(name) { - var route = this.container.lookup('route:' + name); + + var route = this.container.lookup('route:' + name), + transition = this.router.router.activeTransition; + + // If we are mid-transition, we want to try and look up + // resolved parent contexts on the current transitionEvent. + if (transition) { + var modelLookupName = (route && route.routeName) || name; + if (transition.resolvedModels.hasOwnProperty(modelLookupName)) { + return transition.resolvedModels[modelLookupName]; + } + } + return route && route.currentModel; }, /** A hook you can use to render the template for the current route. @@ -25366,11 +26284,26 @@ appendView(this, view, options); }, willDestroy: function() { - teardownView(this); + this.teardownViews(); + }, + + teardownViews: function() { + // Tear down the top level view + if (this.teardownTopLevelView) { this.teardownTopLevelView(); } + + // Tear down any outlets rendered with 'into' + var teardownOutletViews = this.teardownOutletViews || []; + a_forEach(teardownOutletViews, function(teardownOutletView) { + teardownOutletView(); + }); + + delete this.teardownTopLevelView; + delete this.teardownOutletViews; + delete this.lastRenderedTemplate; } }); function parentRoute(route) { var handlerInfos = route.router.router.targetHandlerInfos; @@ -25455,109 +26388,45 @@ } function appendView(route, view, options) { if (options.into) { var parentView = route.router._lookupActiveView(options.into); - route.teardownView = teardownOutlet(parentView, options.outlet); + var teardownOutletView = generateOutletTeardown(parentView, options.outlet); + if (!route.teardownOutletViews) { route.teardownOutletViews = []; } + a_replace(route.teardownOutletViews, 0, 0, [teardownOutletView]); parentView.connectOutlet(options.outlet, view); } else { var rootElement = get(route, 'router.namespace.rootElement'); // tear down view if one is already rendered - if (route.teardownView) { - route.teardownView(); + if (route.teardownTopLevelView) { + route.teardownTopLevelView(); } route.router._connectActiveView(options.name, view); - route.teardownView = teardownTopLevel(view); + route.teardownTopLevelView = generateTopLevelTeardown(view); view.appendTo(rootElement); } } -function teardownTopLevel(view) { +function generateTopLevelTeardown(view) { return function() { view.destroy(); }; } -function teardownOutlet(parentView, outlet) { +function generateOutletTeardown(parentView, outlet) { return function() { parentView.disconnectOutlet(outlet); }; } -function teardownView(route) { - if (route.teardownView) { route.teardownView(); } - - delete route.teardownView; - delete route.lastRenderedTemplate; -} - })(); (function() { -/** -@module ember -@submodule ember-routing -*/ - -/* - A TransitionEvent is passed as the argument for `transitionTo` - events and contains information about an attempted transition - that can be modified or decorated by leafier `transitionTo` event - handlers before the actual transition is committed by ApplicationRoute. - - @class TransitionEvent - @namespace Ember - @extends Ember.Deferred - */ -Ember.TransitionEvent = Ember.Object.extend({ - - /* - The Ember.Route method used to perform the transition. Presently, - the only valid values are 'transitionTo' and 'replaceWith'. - */ - transitionMethod: 'transitionTo', - destinationRouteName: null, - sourceRoute: null, - contexts: null, - - init: function() { - this._super(); - this.contexts = this.contexts || []; - }, - - /* - Convenience method that returns an array that can be used for - legacy `transitionTo` and `replaceWith`. - */ - transitionToArgs: function() { - return [this.destinationRouteName].concat(this.contexts); - } -}); - - -Ember.TransitionEvent.reopenClass({ - /* - This is the default transition event handler that will be injected - into ApplicationRoute. The context, like all route event handlers in - the events hash, will be an `Ember.Route`. - */ - defaultHandler: function(transitionEvent) { - var router = this.router; - router[transitionEvent.transitionMethod].apply(router, transitionEvent.transitionToArgs()); - } -}); - })(); (function() { - -})(); - - - -(function() { Ember.onLoad('Ember.Handlebars', function() { var handlebarsResolve = Ember.Handlebars.resolveParams, map = Ember.ArrayPolyfills.map, get = Ember.get; @@ -25620,38 +26489,137 @@ var ret = [ routeName ]; return ret.concat(resolvedPaths(linkView.parameters)); } /** + `Ember.LinkView` renders an element whose `click` event triggers a + transition of the application's instance of `Ember.Router` to + a supplied route by name. + + Instances of `LinkView` will most likely be created through + the `linkTo` Handlebars helper, but properties of this class + can be overridden to customize application-wide behavior. + @class LinkView @namespace Ember @extends Ember.View + @see {Handlebars.helpers.linkTo} **/ var LinkView = Ember.LinkView = Ember.View.extend({ tagName: 'a', namedRoute: null, currentWhen: null, + + /** + Sets the `title` attribute of the `LinkView`'s HTML element. + + @property title + @default null + **/ title: null, + + /** + The CSS class to apply to `LinkView`'s element when its `active` + property is `true`. + + @property activeClass + @type String + @default active + **/ activeClass: 'active', + + /** + The CSS class to apply to a `LinkView`'s element when its `disabled` + property is `true`. + + @property disabledClass + @type String + @default disabled + **/ disabledClass: 'disabled', _isDisabled: false, + + /** + Determines whether the `LinkView` will trigger routing via + the `replaceWith` routing strategy. + + @type Boolean + @default false + **/ replace: false, attributeBindings: ['href', 'title'], classNameBindings: ['active', 'disabled'], - // Even though this isn't a virtual view, we want to treat it as if it is - // so that you can access the parent with {{view.prop}} + /** + By default the `{{linkTo}}` helper responds to the `click` event. You + can override this globally by setting this property to your custom + event name. + + This is particularly useful on mobile when one wants to avoid the 300ms + click delay using some sort of custom `tap` event. + + @property eventName + @type String + @default click + */ + eventName: 'click', + + // this is doc'ed here so it shows up in the events + // section of the API documentation, which is where + // people will likely go looking for it. + /** + Triggers the `LinkView`'s routing behavior. If + `eventName` is changed to a value other than `click` + the routing behavior will trigger on that custom event + instead. + + @event click + **/ + + init: function() { + this._super(); + // Map desired event name to invoke function + var eventName = get(this, 'eventName'); + this.on(eventName, this, this._invoke); + }, + + /** + @private + + Even though this isn't a virtual view, we want to treat it as if it is + so that you can access the parent with {{view.prop}} + + @method concreteView + **/ concreteView: Ember.computed(function() { return get(this, 'parentView'); }).property('parentView'), + /** + + Accessed as a classname binding to apply the `LinkView`'s `disabledClass` + CSS `class` to the element when the link is disabled. + + When `true` interactions with the element will not trigger route changes. + @property disabled + */ disabled: Ember.computed(function(key, value) { if (value !== undefined) { this.set('_isDisabled', value); } return value ? this.get('disabledClass') : false; }), + /** + Accessed as a classname binding to apply the `LinkView`'s `activeClass` + CSS `class` to the element when the link is active. + + A `LinkView` is considered active when its `currentWhen` property is `true` + or the application's current route is the route the `LinkView` would trigger + transitions into. + + @property active + **/ active: Ember.computed(function() { var router = this.get('router'), params = resolvedPaths(this.parameters), currentWithIndex = this.currentWhen + '.index', isActive = router.isActive.apply(router, [this.currentWhen].concat(params)) || @@ -25662,38 +26630,45 @@ router: Ember.computed(function() { return this.get('controller').container.lookup('router:main'); }), - click: function(event) { + /** + @private + + Event handler that invokes the link, activating the associated route. + + @method _invoke + @param {Event} event + */ + _invoke: function(event) { if (!isSimpleClick(event)) { return true; } event.preventDefault(); if (this.bubbles === false) { event.stopPropagation(); } if (get(this, '_isDisabled')) { return false; } - var router = this.get('router'); + var router = this.get('router'), + routeArgs = args(this, router); - if (Ember.ENV.ENABLE_ROUTE_TO) { - - var routeArgs = args(this, router); - - router.routeTo(Ember.TransitionEvent.create({ - transitionMethod: this.get('replace') ? 'replaceWith' : 'transitionTo', - destinationRouteName: routeArgs[0], - contexts: routeArgs.slice(1) - })); + if (this.get('replace')) { + router.replaceWith.apply(router, routeArgs); } else { - if (this.get('replace')) { - router.replaceWith.apply(router, args(this, router)); - } else { - router.transitionTo.apply(router, args(this, router)); - } + router.transitionTo.apply(router, routeArgs); } }, + /** + Sets the element's `href` attribute to the url for + the `LinkView`'s targeted route. + + If the `LinkView`'s `tagName` is changed to a value other + than `a`, this property will be ignored. + + @property href + **/ href: Ember.computed(function() { if (this.get('tagName') !== 'a') { return false; } var router = this.get('router'); return router.generate.apply(router, args(this, router)); @@ -25850,10 +26825,19 @@ activeClass: "is-active", tagName: 'li' }) ``` + It is also possible to override the default event in + this manner: + + ``` javascript + Ember.LinkView.reopen({ + eventName: 'customEventName' + }); + ``` + @method linkTo @for Ember.Handlebars.helpers @param {String} routeName @param {Object} [context]* @return {String} HTML string @@ -26041,12 +27025,16 @@ Ember.assert("You can only use the {{render}} helper once without a model object as its second argument, as in {{render \"post\" post}}.", context || !router || !router._lookupActiveView(name)); view = container.lookup('view:' + name) || container.lookup('view:default'); - if (controller = options.hash.controller) { - controller = container.lookup('controller:' + controller, lookupOptions); + var controllerName = options.hash.controller; + + // Look up the controller by name, if provided. + if (controllerName) { + controller = container.lookup('controller:' + controllerName, lookupOptions); + Ember.assert("The controller name you supplied '" + controllerName + "' did not resolve to a controller.", !!controller); } else { controller = Ember.controllerFor(container, name, context, lookupOptions); } if (controller && context) { @@ -26463,14 +27451,16 @@ var model = Ember.Handlebars.get(this, modelPath, options); set(childController, 'model', model); childView.rerender(); } - Ember.addObserver(this, modelPath, observer); - childView.one('willDestroyElement', this, function() { - Ember.removeObserver(this, modelPath, observer); - }); + if (modelPath) { + Ember.addObserver(this, modelPath, observer); + childView.one('willDestroyElement', this, function() { + Ember.removeObserver(this, modelPath, observer); + }); + } Ember.Handlebars.helpers.view.call(this, childView, options); }); } @@ -26591,11 +27581,11 @@ }, _hasEquivalentView: function(outletName, view) { var existingView = get(this, '_outlets.'+outletName); return existingView && - existingView.prototype === view.prototype && + existingView.constructor === view.constructor && existingView.get('template') === view.get('template') && existingView.get('context') === view.get('context'); }, disconnectOutlet: function(outletName) { @@ -26620,11 +27610,24 @@ })(); (function() { +/** +@module ember +@submodule ember-views +*/ +// Add a new named queue after the 'actions' queue (where RSVP promises +// resolve), which is used in router transitions to prevent unnecessary +// loading state entry if all context promises resolve on the +// 'actions' queue first. + +var queues = Ember.run.queues, + indexOf = Ember.ArrayPolyfills.indexOf; +queues.splice(indexOf.call(queues, 'actions') + 1, 0, 'routerTransitions'); + })(); (function() { @@ -27503,15 +28506,18 @@ ### Routing In addition to creating your application's router, `Ember.Application` is also responsible for telling the router when to start routing. Transitions - between routes can be logged with the LOG_TRANSITIONS flag: + between routes can be logged with the LOG_TRANSITIONS flag, and more + detailed intra-transition logging can be logged with + the LOG_TRANSITIONS_INTERNAL flag: ```javascript window.App = Ember.Application.create({ - LOG_TRANSITIONS: true + LOG_TRANSITIONS: true, // basic logging of successful transitions + LOG_TRANSITIONS_INTERNAL: true // detailed logging of all routing steps }); ``` By default, the router will begin trying to translate the current URL into application state once the browser emits the `DOMContentReady` event. If you @@ -28214,10 +29220,12 @@ ```javascript this.get('controllers.post'); // instance of App.PostController ``` + This is only available for singleton controllers. + @property {Array} needs @default [] */ needs: [], @@ -28276,12 +29284,10 @@ @extends Ember.Object @uses Ember.Evented */ Ember.State = Ember.Object.extend(Ember.Evented, /** @scope Ember.State.prototype */{ - isState: true, - /** A reference to the parent state. @property parentState @type Ember.State @@ -28387,24 +29393,28 @@ return pathsCacheForManager[path]; }, setupChild: function(states, name, value) { if (!value) { return false; } + var instance; - if (value.isState) { + if (value instanceof Ember.State) { set(value, 'name', name); + instance = value; + instance.container = this.container; } else if (Ember.State.detect(value)) { - value = value.create({ - name: name + instance = value.create({ + name: name, + container: this.container }); } - if (value.isState) { - set(value, 'parentState', this); - get(this, 'childStates').pushObject(value); - states[name] = value; - return value; + if (instance instanceof Ember.State) { + set(instance, 'parentState', this); + get(this, 'childStates').pushObject(instance); + states[name] = instance; + return instance; } }, lookupEventTransition: function(name) { var path, state = this; @@ -29629,11 +30639,11 @@ var promise = new Ember.RSVP.Promise(resolver); var thenable = { chained: false }; thenable.then = function(onSuccess, onFailure) { - var self = this, thenPromise, nextPromise; + var thenPromise, nextPromise; thenable.chained = true; thenPromise = promise.then(onSuccess, onFailure); // this is to ensure all downstream fulfillment // handlers are wrapped in the error handling nextPromise = Ember.Test.promise(function(resolve) { @@ -29802,82 +30812,88 @@ (function() { var get = Ember.get, - helper = Ember.Test.registerHelper, - pendingAjaxRequests = 0, + Test = Ember.Test, + helper = Test.registerHelper, countAsync = 0; +Test.pendingAjaxRequests = 0; -Ember.Test.onInjectHelpers(function() { +Test.onInjectHelpers(function() { Ember.$(document).ajaxStart(function() { - pendingAjaxRequests++; + Test.pendingAjaxRequests++; }); Ember.$(document).ajaxStop(function() { - pendingAjaxRequests--; + Test.pendingAjaxRequests--; }); }); function visit(app, url) { - Ember.run(app, app.handleURL, url); app.__container__.lookup('router:main').location.setURL(url); + Ember.run(app, app.handleURL, url); return wait(app); } function click(app, selector, context) { - var $el = find(app, selector, context); - Ember.run(function() { - $el.click(); - }); + var $el = findWithAssert(app, selector, context); + Ember.run($el, 'click'); return wait(app); } function fillIn(app, selector, context, text) { var $el; if (typeof text === 'undefined') { text = context; context = null; } - $el = find(app, selector, context); + $el = findWithAssert(app, selector, context); Ember.run(function() { $el.val(text).change(); }); return wait(app); } +function findWithAssert(app, selector, context) { + var $el = find(app, selector, context); + if ($el.length === 0) { + throw("Element " + selector + " not found."); + } + return $el; +} + function find(app, selector, context) { var $el; context = context || get(app, 'rootElement'); $el = app.$(selector, context); - if ($el.length === 0) { - throw("Element " + selector + " not found."); - } + return $el; } function wait(app, value) { - var promise, obj = {}, helperName; + var promise; - promise = Ember.Test.promise(function(resolve) { + promise = Test.promise(function(resolve) { if (++countAsync === 1) { - Ember.Test.adapter.asyncStart(); + Test.adapter.asyncStart(); } var watcher = setInterval(function() { var routerIsLoading = app.__container__.lookup('router:main').router.isLoading; if (routerIsLoading) { return; } - if (pendingAjaxRequests) { return; } + if (Test.pendingAjaxRequests) { return; } if (Ember.run.hasScheduledTimers() || Ember.run.currentRunLoop) { return; } + clearInterval(watcher); + if (--countAsync === 0) { - Ember.Test.adapter.asyncEnd(); + Test.adapter.asyncEnd(); } - Ember.run(function() { - resolve(value); - }); + + Ember.run(null, resolve, value); }, 10); }); return buildChainObject(app, promise); } @@ -29928,9 +30944,10 @@ // expose these methods as test helpers helper('visit', visit); helper('click', click); helper('fillIn', fillIn); helper('find', find); +helper('findWithAssert', findWithAssert); helper('wait', wait); })();