/*jshint evil:true*/
minispade = {
root: null,
modules: {},
loaded: {},
globalEval: function(data) {
if ( data ) {
// We use execScript on Internet Explorer
// We use an anonymous function so that context is window
// rather than jQuery in Firefox
( window.execScript || function( data ) {
window[ "eval" ].call( window, data );
} )( data );
}
},
require: function(name) {
var loaded = minispade.loaded[name];
var mod = minispade.modules[name];
if (!loaded) {
if (mod) {
minispade.loaded[name] = true;
if (typeof mod === "string") {
this.globalEval(mod);
} else {
mod();
}
} else {
if (minispade.root && name.substr(0,minispade.root.length) !== minispade.root) {
return minispade.require(minispade.root+name);
} else {
throw "The module '" + name + "' could not be found";
}
}
}
return loaded;
},
requireAll: function(regex) {
for (var module in this.modules) {
if (!this.modules.hasOwnProperty(module)) { continue; }
if (regex && !regex.test(module)) { continue; }
minispade.require(module);
}
},
register: function(name, callback) {
minispade.modules[name] = callback;
}
};
/*! jQuery v1.7.2 jquery.com | jquery.org/license */
(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"":"")+"
"&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/
And associate it by name using a view's `templateName` property:
AView = Ember.View.extend({
templateName: 'some-template'
})
Using a value for `templateName` that does not have a Handlebars template with a
matching `data-template-name` attribute will throw an error.
Assigning a value to both `template` and `templateName` properties will throw an error.
For views classes that may have a template later defined (e.g. as the block portion of a `{{view}}`
Handlebars helper call in another template or in a subclass), you can provide a `defaultTemplate`
property set to compiled template function. If a template is not later provided for the view
instance the `defaultTemplate` value will be used:
AView = Ember.View.extend({
defaultTemplate: Ember.Handlebars.compile('I was the default'),
template: null,
templateName: null
})
Will result in instances with an HTML representation of:
I was the default
If a `template` or `templateName` is provided it will take precedence over `defaultTemplate`:
AView = Ember.View.extend({
defaultTemplate: Ember.Handlebars.compile('I was the default')
})
aView = AView.create({
template: Ember.Handlebars.compile('I was the template, not default')
})
Will result in the following HTML representation when rendered:
I was the template, not default
## Layouts
Views can have a secondary template that wraps their main template. Like
primary templates, layouts can be any function that accepts an optional context
parameter and returns a string of HTML that will be inserted inside view's tag. Views whose HTML
element is self closing (e.g. ``) cannot have a layout and this property will be ignored.
Most typically in Ember a layout will be a compiled Ember.Handlebars template.
A view's layout can be set directly with the `layout` property or reference an
existing Handlebars template by name with the `layoutName` property.
A template used as a layout must contain a single use of the Handlebars `{{yield}}`
helper. The HTML contents of a view's rendered `template` will be inserted at this location:
AViewWithLayout = Ember.View.extend({
layout: Ember.Handlebars.compile("
{{yield}}
")
template: Ember.Handlebars.compile("I got wrapped"),
})
Will result in view instances with an HTML representation of:
I got wrapped
See `Handlebars.helpers.yield` for more information.
## Responding to Browser Events
Views can respond to user-initiated events in one of three ways: method implementation,
through an event manager, and through `{{action}}` helper use in their template or layout.
### Method Implementation
Views can respond to user-initiated events by implementing a method that matches the
event name. A `jQuery.Event` object will be passed as the argument to this method.
AView = Ember.View.extend({
click: function(event){
// will be called when when an instance's
// rendered element is clicked
}
})
### Event Managers
Views can define an object as their `eventManager` property. This object can then
implement methods that match the desired event names. Matching events that occur
on the view's rendered HTML or the rendered HTML of any of its DOM descendants
will trigger this method. A `jQuery.Event` object will be passed as the first
argument to the method and an `Ember.View` object as the second. The `Ember.View`
will be the view whose rendered HTML was interacted with. This may be the view with
the `eventManager` property or one of its descendent views.
AView = Ember.View.extend({
eventManager: Ember.Object.create({
doubleClick: function(event, view){
// will be called when when an instance's
// rendered element or any rendering
// of this views's descendent
// elements is clicked
}
})
})
An event defined for an event manager takes precedence over events of the same
name handled through methods on the view.
AView = Ember.View.extend({
mouseEnter: function(event){
// will never trigger.
},
eventManager: Ember.Object.create({
mouseEnter: function(event, view){
// takes presedence over AView#mouseEnter
}
})
})
Similarly a view's event manager will take precedence for events of any views
rendered as a descendent. A method name that matches an event name will not be called
if the view instance was rendered inside the HTML representation of a view that has
an `eventManager` property defined that handles events of the name. Events not handled
by the event manager will still trigger method calls on the descendent.
OuterView = Ember.View.extend({
template: Ember.Handlebars.compile("outer {{#view InnerView}}inner{{/view}} outer"),
eventManager: Ember.Object.create({
mouseEnter: function(event, view){
// view might be instance of either
// OutsideView or InnerView depending on
// where on the page the user interaction occured
}
})
})
InnerView = Ember.View.extend({
click: function(event){
// will be called if rendered inside
// an OuterView because OuterView's
// eventManager doesn't handle click events
},
mouseEnter: function(event){
// will never be called if rendered inside
// an OuterView.
}
})
### Handlebars `{{action}}` Helper
See `Handlebars.helpers.action`.
### Event Names
Possible events names for any of the responding approaches described above are:
Touch events: 'touchStart', 'touchMove', 'touchEnd', 'touchCancel'
Keyboard events: 'keyDown', 'keyUp', 'keyPress'
Mouse events: 'mouseDown', 'mouseUp', 'contextMenu', 'click', 'doubleClick', 'mouseMove',
'focusIn', 'focusOut', 'mouseEnter', 'mouseLeave'
Form events: 'submit', 'change', 'focusIn', 'focusOut', 'input'
HTML5 drag and drop events: 'dragStart', 'drag', 'dragEnter', 'dragLeave', 'drop', 'dragEnd'
## Handlebars `{{view}}` Helper
Other `Ember.View` instances can be included as part of a view's template by using the `{{view}}`
Handlebars helper. See `Handlebars.helpers.view` for additional information.
@extends Ember.Object
*/
Ember.View = Ember.Object.extend(Ember.Evented,
/** @scope Ember.View.prototype */ {
/** @private */
concatenatedProperties: ['classNames', 'classNameBindings', 'attributeBindings'],
/**
@type Boolean
@default true
@constant
*/
isView: true,
// ..........................................................
// TEMPLATE SUPPORT
//
/**
The name of the template to lookup if no template is provided.
Ember.View will look for a template with this name in this view's
`templates` object. By default, this will be a global object
shared in `Ember.TEMPLATES`.
@type String
@default null
*/
templateName: null,
/**
The name of the layout to lookup if no layout is provided.
Ember.View will look for a template with this name in this view's
`templates` object. By default, this will be a global object
shared in `Ember.TEMPLATES`.
@type String
@default null
*/
layoutName: null,
/**
The hash in which to look for `templateName`.
@type Ember.Object
@default Ember.TEMPLATES
*/
templates: Ember.TEMPLATES,
/**
The template used to render the view. This should be a function that
accepts an optional context parameter and returns a string of HTML that
will be inserted into the DOM relative to its parent view.
In general, you should set the `templateName` property instead of setting
the template yourself.
@field
@type Function
*/
template: Ember.computed(function(key, value) {
if (value !== undefined) { return value; }
var templateName = get(this, 'templateName'),
template = this.templateForName(templateName, 'template');
return template || get(this, 'defaultTemplate');
}).property('templateName').cacheable(),
/**
The controller managing this view. If this property is set, it will be
made available for use by the template.
@type Object
*/
controller: Ember.computed(function(key, value) {
var parentView;
if (arguments.length === 2) {
return value;
} else {
parentView = get(this, 'parentView');
return parentView ? get(parentView, 'controller') : null;
}
}).property().cacheable(),
/**
A view may contain a layout. A layout is a regular template but
supersedes the `template` property during rendering. It is the
responsibility of the layout template to retrieve the `template`
property from the view (or alternatively, call `Handlebars.helpers.yield`,
`{{yield}}`) to render it in the correct location.
This is useful for a view that has a shared wrapper, but which delegates
the rendering of the contents of the wrapper to the `template` property
on a subclass.
@field
@type Function
*/
layout: Ember.computed(function(key, value) {
if (arguments.length === 2) { return value; }
var layoutName = get(this, 'layoutName'),
layout = this.templateForName(layoutName, 'layout');
return layout || get(this, 'defaultLayout');
}).property('layoutName').cacheable(),
templateForName: function(name, type) {
if (!name) { return; }
var templates = get(this, 'templates'),
template = get(templates, name);
if (!template) {
throw new Ember.Error(fmt('%@ - Unable to find %@ "%@".', [this, type, name]));
}
return template;
},
/**
The object from which templates should access properties.
This object will be passed to the template function each time the render
method is called, but it is up to the individual function to decide what
to do with it.
By default, this will be the view itself.
@type Object
*/
context: Ember.computed(function(key, value) {
if (arguments.length === 2) {
set(this, '_context', value);
return value;
} else {
return get(this, '_context');
}
}).cacheable(),
/**
@private
Private copy of the view's template context. This can be set directly
by Handlebars without triggering the observer that causes the view
to be re-rendered.
*/
_context: Ember.computed(function(key, value) {
var parentView, controller;
if (arguments.length === 2) {
return value;
}
if (VIEW_PRESERVES_CONTEXT) {
if (controller = get(this, 'controller')) {
return controller;
}
parentView = get(this, '_parentView');
if (parentView) {
return get(parentView, '_context');
}
}
return this;
}).cacheable(),
/**
If a value that affects template rendering changes, the view should be
re-rendered to reflect the new value.
@private
*/
_displayPropertyDidChange: Ember.observer(function() {
this.rerender();
}, 'context', 'controller'),
/**
If the view is currently inserted into the DOM of a parent view, this
property will point to the parent of the view.
@type Ember.View
@default null
*/
parentView: Ember.computed(function() {
var parent = get(this, '_parentView');
if (parent && parent.isVirtual) {
return get(parent, 'parentView');
} else {
return parent;
}
}).property('_parentView').volatile(),
_parentView: null,
// return the current view, not including virtual views
concreteView: Ember.computed(function() {
if (!this.isVirtual) { return this; }
else { return get(this, 'parentView'); }
}).property('_parentView').volatile(),
/**
If false, the view will appear hidden in DOM.
@type Boolean
@default null
*/
isVisible: true,
/**
Array of child views. You should never edit this array directly.
Instead, use appendChild and removeFromParent.
@private
@type Array
@default []
*/
childViews: childViewsProperty,
_childViews: [],
/**
When it's a virtual view, we need to notify the parent that their
childViews will change.
*/
_childViewsWillChange: Ember.beforeObserver(function() {
if (this.isVirtual) {
var parentView = get(this, 'parentView');
if (parentView) { Ember.propertyWillChange(parentView, 'childViews'); }
}
}, 'childViews'),
/**
When it's a virtual view, we need to notify the parent that their
childViews did change.
*/
_childViewsDidChange: Ember.observer(function() {
if (this.isVirtual) {
var parentView = get(this, 'parentView');
if (parentView) { Ember.propertyDidChange(parentView, 'childViews'); }
}
}, 'childViews'),
/**
Return the nearest ancestor that is an instance of the provided
class.
@param {Class} klass Subclass of Ember.View (or Ember.View itself)
@returns Ember.View
*/
nearestInstanceOf: function(klass) {
var view = get(this, 'parentView');
while (view) {
if(view instanceof klass) { return view; }
view = get(view, 'parentView');
}
},
/**
Return the nearest ancestor that has a given property.
@param {String} property A property name
@returns Ember.View
*/
nearestWithProperty: function(property) {
var view = get(this, 'parentView');
while (view) {
if (property in view) { return view; }
view = get(view, 'parentView');
}
},
/**
Return the nearest ancestor whose parent is an instance of
`klass`.
@param {Class} klass Subclass of Ember.View (or Ember.View itself)
@returns Ember.View
*/
nearestChildOf: function(klass) {
var view = get(this, 'parentView');
while (view) {
if(get(view, 'parentView') instanceof klass) { return view; }
view = get(view, 'parentView');
}
},
/**
Return the nearest ancestor that is an Ember.CollectionView
@returns Ember.CollectionView
*/
collectionView: Ember.computed(function() {
return this.nearestInstanceOf(Ember.CollectionView);
}).cacheable(),
/**
Return the nearest ancestor that is a direct child of
an Ember.CollectionView
@returns Ember.View
*/
itemView: Ember.computed(function() {
return this.nearestChildOf(Ember.CollectionView);
}).cacheable(),
/**
Return the nearest ancestor that has the property
`content`.
@returns Ember.View
*/
contentView: Ember.computed(function() {
return this.nearestWithProperty('content');
}).cacheable(),
/**
@private
When the parent view changes, recursively invalidate
collectionView, itemView, and contentView
*/
_parentViewDidChange: Ember.observer(function() {
if (this.isDestroying) { return; }
this.invokeRecursively(function(view) {
view.propertyDidChange('collectionView');
view.propertyDidChange('itemView');
view.propertyDidChange('contentView');
});
if (getPath(this, 'parentView.controller') && !get(this, 'controller')) {
this.notifyPropertyChange('controller');
}
}, '_parentView'),
_controllerDidChange: Ember.observer(function() {
if (this.isDestroying) { return; }
this.forEachChildView(function(view) {
view.propertyDidChange('controller');
});
}, 'controller'),
cloneKeywords: function() {
var templateData = get(this, 'templateData');
var keywords = templateData ? Ember.copy(templateData.keywords) : {};
keywords.view = get(this, 'concreteView');
keywords.controller = get(this, 'controller');
return keywords;
},
/**
Called on your view when it should push strings of HTML into a
Ember.RenderBuffer. Most users will want to override the `template`
or `templateName` properties instead of this method.
By default, Ember.View will look for a function in the `template`
property and invoke it with the value of `context`. The value of
`context` will be the view's controller unless you override it.
@param {Ember.RenderBuffer} buffer The render buffer
*/
render: function(buffer) {
// If this view has a layout, it is the responsibility of the
// the layout to render the view's template. Otherwise, render the template
// directly.
var template = get(this, 'layout') || get(this, 'template');
if (template) {
var context = get(this, '_context');
var keywords = this.cloneKeywords();
var data = {
view: this,
buffer: buffer,
isRenderData: true,
keywords: keywords
};
// Invoke the template with the provided template context, which
// is the view by default. A hash of data is also passed that provides
// the template with access to the view and render buffer.
Ember.assert('template must be a function. Did you mean to call Ember.Handlebars.compile("...") or specify templateName instead?', typeof template === 'function');
// The template should write directly to the render buffer instead
// of returning a string.
var output = template(context, { data: data });
// If the template returned a string instead of writing to the buffer,
// push the string onto the buffer.
if (output !== undefined) { buffer.push(output); }
}
},
invokeForState: function(name) {
var stateName = this.state, args, fn;
// try to find the function for the state in the cache
if (fn = invokeForState[stateName][name]) {
args = a_slice.call(arguments);
args[0] = this;
return fn.apply(this, args);
}
// otherwise, find and cache the function for this state
var parent = this, states = parent.states, state;
while (states) {
state = states[stateName];
while (state) {
fn = state[name];
if (fn) {
invokeForState[stateName][name] = fn;
args = a_slice.call(arguments, 1);
args.unshift(this);
return fn.apply(this, args);
}
state = state.parentState;
}
states = states.parent;
}
},
/**
Renders the view again. This will work regardless of whether the
view is already in the DOM or not. If the view is in the DOM, the
rendering process will be deferred to give bindings a chance
to synchronize.
If children were added during the rendering process using `appendChild`,
`rerender` will remove them, because they will be added again
if needed by the next `render`.
In general, if the display of your view changes, you should modify
the DOM element directly instead of manually calling `rerender`, which can
be slow.
*/
rerender: function() {
return this.invokeForState('rerender');
},
clearRenderedChildren: function() {
var lengthBefore = this.lengthBeforeRender,
lengthAfter = this.lengthAfterRender;
// If there were child views created during the last call to render(),
// remove them under the assumption that they will be re-created when
// we re-render.
// VIEW-TODO: Unit test this path.
var childViews = get(this, '_childViews');
for (var i=lengthAfter-1; i>=lengthBefore; i--) {
if (childViews[i]) { childViews[i].destroy(); }
}
},
/**
@private
Iterates over the view's `classNameBindings` array, inserts the value
of the specified property into the `classNames` array, then creates an
observer to update the view's element if the bound property ever changes
in the future.
*/
_applyClassNameBindings: function() {
var classBindings = get(this, 'classNameBindings'),
classNames = get(this, 'classNames'),
elem, newClass, dasherizedClass;
if (!classBindings) { return; }
// Loop through all of the configured bindings. These will be either
// property names ('isUrgent') or property paths relative to the view
// ('content.isUrgent')
a_forEach(classBindings, function(binding) {
// Variable in which the old class value is saved. The observer function
// closes over this variable, so it knows which string to remove when
// the property changes.
var oldClass, property;
// Set up an observer on the context. If the property changes, toggle the
// class name.
var observer = function() {
// Get the current value of the property
newClass = this._classStringForProperty(binding);
elem = this.$();
// If we had previously added a class to the element, remove it.
if (oldClass) {
elem.removeClass(oldClass);
// Also remove from classNames so that if the view gets rerendered,
// the class doesn't get added back to the DOM.
classNames.removeObject(oldClass);
}
// If necessary, add a new class. Make sure we keep track of it so
// it can be removed in the future.
if (newClass) {
elem.addClass(newClass);
oldClass = newClass;
} else {
oldClass = null;
}
};
// Get the class name for the property at its current value
dasherizedClass = this._classStringForProperty(binding);
if (dasherizedClass) {
// Ensure that it gets into the classNames array
// so it is displayed when we render.
classNames.push(dasherizedClass);
// Save a reference to the class name so we can remove it
// if the observer fires. Remember that this variable has
// been closed over by the observer.
oldClass = dasherizedClass;
}
// Extract just the property name from bindings like 'foo:bar'
property = binding.split(':')[0];
addObserver(this, property, observer);
}, this);
},
/**
Iterates through the view's attribute bindings, sets up observers for each,
then applies the current value of the attributes to the passed render buffer.
@param {Ember.RenderBuffer} buffer
*/
_applyAttributeBindings: function(buffer) {
var attributeBindings = get(this, 'attributeBindings'),
attributeValue, elem, type;
if (!attributeBindings) { return; }
a_forEach(attributeBindings, function(binding) {
var split = binding.split(':'),
property = split[0],
attributeName = split[1] || property;
// Create an observer to add/remove/change the attribute if the
// JavaScript property changes.
var observer = function() {
elem = this.$();
attributeValue = get(this, property);
Ember.View.applyAttributeBindings(elem, attributeName, attributeValue);
};
addObserver(this, property, observer);
// Determine the current value and add it to the render buffer
// if necessary.
attributeValue = get(this, property);
Ember.View.applyAttributeBindings(buffer, attributeName, attributeValue);
}, this);
},
/**
@private
Given a property name, returns a dasherized version of that
property name if the property evaluates to a non-falsy value.
For example, if the view has property `isUrgent` that evaluates to true,
passing `isUrgent` to this method will return `"is-urgent"`.
*/
_classStringForProperty: function(property) {
var split = property.split(':'),
className = split[1];
property = split[0];
// TODO: Remove this `false` when the `getPath` globals support is removed
var val = Ember.getPath(this, property, false);
if (val === undefined && Ember.isGlobalPath(property)) {
val = Ember.getPath(window, property);
}
// If the value is truthy and we're using the colon syntax,
// we should return the className directly
if (!!val && className) {
return className;
// If value is a Boolean and true, return the dasherized property
// name.
} else if (val === true) {
// Normalize property path to be suitable for use
// as a class name. For exaple, content.foo.barBaz
// becomes bar-baz.
var parts = property.split('.');
return Ember.String.dasherize(parts[parts.length-1]);
// If the value is not false, undefined, or null, return the current
// value of the property.
} else if (val !== false && val !== undefined && val !== null) {
return val;
// Nothing to display. Return null so that the old class is removed
// but no new class is added.
} else {
return null;
}
},
// ..........................................................
// ELEMENT SUPPORT
//
/**
Returns the current DOM element for the view.
@field
@type DOMElement
*/
element: Ember.computed(function(key, value) {
if (value !== undefined) {
return this.invokeForState('setElement', value);
} else {
return this.invokeForState('getElement');
}
}).property('_parentView').cacheable(),
/**
Returns a jQuery object for this view's element. If you pass in a selector
string, this method will return a jQuery object, using the current element
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.
@param {String} [selector] a jQuery-compatible selector string
@returns {Ember.CoreQuery} the CoreQuery object for the DOM node
*/
$: function(sel) {
return this.invokeForState('$', sel);
},
/** @private */
mutateChildViews: function(callback) {
var childViews = get(this, '_childViews'),
idx = get(childViews, 'length'),
view;
while(--idx >= 0) {
view = childViews[idx];
callback.call(this, view, idx);
}
return this;
},
/** @private */
forEachChildView: function(callback) {
var childViews = get(this, '_childViews');
if (!childViews) { return this; }
var len = get(childViews, 'length'),
view, idx;
for(idx = 0; idx < len; idx++) {
view = childViews[idx];
callback.call(this, view);
}
return this;
},
/**
Appends the view's element to the specified parent element.
If the view does not have an HTML representation yet, `createElement()`
will be called automatically.
Note that this method just schedules the view to be appended; the DOM
element will not be appended to the given element until all bindings have
finished synchronizing.
This is not typically a function that you will need to call directly
when building your application. You might consider using Ember.ContainerView
instead. If you do need to use appendTo, be sure that the target element you
are providing is associated with an Ember.Application and does not have an
ancestor element that is associated with an Ember view.
@param {String|DOMElement|jQuery} A selector, element, HTML string, or jQuery object
@returns {Ember.View} receiver
*/
appendTo: function(target) {
Ember.assert("You cannot append to an existing Ember.View. Consider using Ember.ContainerView instead.", !Ember.$(target).is('.ember-view') && !Ember.$(target).parents().is('.ember-view'));
// Schedule the DOM element to be created and appended to the given
// element after bindings have synchronized.
this._insertElementLater(function() {
this.$().appendTo(target);
});
return this;
},
/**
Replaces the content of the specified parent element with this view's element.
If the view does not have an HTML representation yet, `createElement()`
will be called automatically.
Note that this method just schedules the view to be appended; the DOM
element will not be appended to the given element until all bindings have
finished synchronizing
@param {String|DOMElement|jQuery} A selector, element, HTML string, or jQuery object
@returns {Ember.View} received
*/
replaceIn: function(target) {
Ember.assert("You cannot replace an existing Ember.View. Consider using Ember.ContainerView instead.", !Ember.$(target).is('.ember-view') && !Ember.$(target).parents().is('.ember-view'));
this._insertElementLater(function() {
Ember.$(target).empty();
this.$().appendTo(target);
});
return this;
},
/**
@private
Schedules a DOM operation to occur during the next render phase. This
ensures that all bindings have finished synchronizing before the view is
rendered.
To use, pass a function that performs a DOM operation..
Before your function is called, this view and all child views will receive
the `willInsertElement` event. After your function is invoked, this view
and all of its child views will receive the `didInsertElement` event.
view._insertElementLater(function() {
this.createElement();
this.$().appendTo('body');
});
@param {Function} fn the function that inserts the element into the DOM
*/
_insertElementLater: function(fn) {
this._lastInsert = Ember.guidFor(fn);
Ember.run.schedule('render', this, this.invokeForState, 'insertElement', fn);
},
/**
Appends the view's element to the document body. If the view does
not have an HTML representation yet, `createElement()` will be called
automatically.
Note that this method just schedules the view to be appended; the DOM
element will not be appended to the document body until all bindings have
finished synchronizing.
@returns {Ember.View} receiver
*/
append: function() {
return this.appendTo(document.body);
},
/**
Removes the view's element from the element to which it is attached.
@returns {Ember.View} receiver
*/
remove: function() {
// What we should really do here is wait until the end of the run loop
// to determine if the element has been re-appended to a different
// element.
// In the interim, we will just re-render if that happens. It is more
// important than elements get garbage collected.
this.destroyElement();
this.invokeRecursively(function(view) {
view.clearRenderedChildren();
});
},
/**
The ID to use when trying to locate the element in the DOM. If you do not
set the elementId explicitly, then the view's GUID will be used instead.
This ID must be set at the time the view is created.
@type String
@readOnly
*/
elementId: Ember.computed(function(key, value) {
return value !== undefined ? value : Ember.guidFor(this);
}).cacheable(),
/**
@private
TODO: Perhaps this should be removed from the production build somehow.
*/
_elementIdDidChange: Ember.beforeObserver(function() {
throw "Changing a view's elementId after creation is not allowed.";
}, 'elementId'),
/**
Attempts to discover the element in the parent element. The default
implementation looks for an element with an ID of elementId (or the view's
guid if elementId is null). You can override this method to provide your
own form of lookup. For example, if you want to discover your element
using a CSS class name instead of an ID.
@param {DOMElement} parentElement The parent's DOM element
@returns {DOMElement} The discovered element
*/
findElementInParentElement: function(parentElem) {
var id = "#" + get(this, 'elementId');
return Ember.$(id)[0] || Ember.$(id, parentElem)[0];
},
/**
Creates a new renderBuffer with the passed tagName. You can override this
method to provide further customization to the buffer if needed. Normally
you will not need to call or override this method.
@returns {Ember.RenderBuffer}
*/
renderBuffer: function(tagName) {
tagName = tagName || get(this, 'tagName');
// Explicitly check for null or undefined, as tagName
// may be an empty string, which would evaluate to false.
if (tagName === null || tagName === undefined) {
tagName = 'div';
}
return Ember.RenderBuffer(tagName);
},
/**
Creates a DOM representation of the view and all of its
child views by recursively calling the `render()` method.
After the element has been created, `didInsertElement` will
be called on this view and all of its child views.
@returns {Ember.View} receiver
*/
createElement: function() {
if (get(this, 'element')) { return this; }
var buffer = this.renderToBuffer();
set(this, 'element', buffer.element());
return this;
},
/**
Called when a view is going to insert an element into the DOM.
*/
willInsertElement: Ember.K,
/**
Called when the element of the view has been inserted into the DOM.
Override this function to do any set up that requires an element in the
document body.
*/
didInsertElement: Ember.K,
/**
Called when the view is about to rerender, but before anything has
been torn down. This is a good opportunity to tear down any manual
observers you have installed based on the DOM state
*/
willRerender: Ember.K,
/**
Run this callback on the current view and recursively on child views.
@private
*/
invokeRecursively: function(fn) {
fn.call(this, this);
this.forEachChildView(function(view) {
view.invokeRecursively(fn);
});
},
/**
Invalidates the cache for a property on all child views.
*/
invalidateRecursively: function(key) {
this.forEachChildView(function(view) {
view.propertyDidChange(key);
});
},
/**
@private
Invokes the receiver's willInsertElement() method if it exists and then
invokes the same on all child views.
NOTE: In some cases this was called when the element existed. This no longer
works so we let people know. We can remove this warning code later.
*/
_notifyWillInsertElement: function() {
this.invokeRecursively(function(view) {
view.trigger('willInsertElement');
});
},
/**
@private
Invokes the receiver's didInsertElement() method if it exists and then
invokes the same on all child views.
*/
_notifyDidInsertElement: function() {
this.invokeRecursively(function(view) {
view.trigger('didInsertElement');
});
},
/**
@private
Invokes the receiver's willRerender() method if it exists and then
invokes the same on all child views.
*/
_notifyWillRerender: function() {
this.invokeRecursively(function(view) {
view.trigger('willRerender');
});
},
/**
Destroys any existing element along with the element for any child views
as well. If the view does not currently have a element, then this method
will do nothing.
If you implement willDestroyElement() on your view, then this method will
be invoked on your view before your element is destroyed to give you a
chance to clean up any event handlers, etc.
If you write a willDestroyElement() handler, you can assume that your
didInsertElement() handler was called earlier for the same element.
Normally you will not call or override this method yourself, but you may
want to implement the above callbacks when it is run.
@returns {Ember.View} receiver
*/
destroyElement: function() {
return this.invokeForState('destroyElement');
},
/**
Called when the element of the view is going to be destroyed. Override
this function to do any teardown that requires an element, like removing
event listeners.
*/
willDestroyElement: function() {},
/**
@private
Invokes the `willDestroyElement` callback on the view and child views.
*/
_notifyWillDestroyElement: function() {
this.invokeRecursively(function(view) {
view.trigger('willDestroyElement');
});
},
/** @private (nodoc) */
_elementWillChange: Ember.beforeObserver(function() {
this.forEachChildView(function(view) {
Ember.propertyWillChange(view, 'element');
});
}, 'element'),
/**
@private
If this view's element changes, we need to invalidate the caches of our
child views so that we do not retain references to DOM elements that are
no longer needed.
@observes element
*/
_elementDidChange: Ember.observer(function() {
this.forEachChildView(function(view) {
Ember.propertyDidChange(view, 'element');
});
}, 'element'),
/**
Called when the parentView property has changed.
@function
*/
parentViewDidChange: Ember.K,
/**
@private
Invoked by the view system when this view needs to produce an HTML
representation. This method will create a new render buffer, if needed,
then apply any default attributes, such as class names and visibility.
Finally, the `render()` method is invoked, which is responsible for
doing the bulk of the rendering.
You should not need to override this method; instead, implement the
`template` property, or if you need more control, override the `render`
method.
@param {Ember.RenderBuffer} buffer the render buffer. If no buffer is
passed, a default buffer, using the current view's `tagName`, will
be used.
*/
renderToBuffer: function(parentBuffer, bufferOperation) {
var buffer;
Ember.run.sync();
// Determine where in the parent buffer to start the new buffer.
// By default, a new buffer will be appended to the parent buffer.
// The buffer operation may be changed if the child views array is
// mutated by Ember.ContainerView.
bufferOperation = bufferOperation || 'begin';
// If this is the top-most view, start a new buffer. Otherwise,
// create a new buffer relative to the original using the
// provided buffer operation (for example, `insertAfter` will
// insert a new buffer after the "parent buffer").
if (parentBuffer) {
var tagName = get(this, 'tagName');
if (tagName === null || tagName === undefined) {
tagName = 'div';
}
buffer = parentBuffer[bufferOperation](tagName);
} else {
buffer = this.renderBuffer();
}
this.buffer = buffer;
this.transitionTo('inBuffer', false);
this.lengthBeforeRender = get(get(this, '_childViews'), 'length');
this.beforeRender(buffer);
this.render(buffer);
this.afterRender(buffer);
this.lengthAfterRender = get(get(this, '_childViews'), 'length');
return buffer;
},
beforeRender: function(buffer) {
this.applyAttributesToBuffer(buffer);
},
afterRender: Ember.K,
/**
@private
*/
applyAttributesToBuffer: function(buffer) {
// Creates observers for all registered class name and attribute bindings,
// then adds them to the element.
this._applyClassNameBindings();
// Pass the render buffer so the method can apply attributes directly.
// This isn't needed for class name bindings because they use the
// existing classNames infrastructure.
this._applyAttributeBindings(buffer);
a_forEach(get(this, 'classNames'), function(name){ buffer.addClass(name); });
buffer.id(get(this, 'elementId'));
var role = get(this, 'ariaRole');
if (role) {
buffer.attr('role', role);
}
if (get(this, 'isVisible') === false) {
buffer.style('display', 'none');
}
},
// ..........................................................
// STANDARD RENDER PROPERTIES
//
/**
Tag name for the view's outer element. The tag name is only used when
an element is first created. If you change the tagName for an element, you
must destroy and recreate the view element.
By default, the render buffer will use a `
` tag for views.
@type String
@default null
*/
// We leave this null by default so we can tell the difference between
// the default case and a user-specified tag.
tagName: null,
/**
The WAI-ARIA role of the control represented by this view. For example, a
button may have a role of type 'button', or a pane may have a role of
type 'alertdialog'. This property is used by assistive software to help
visually challenged users navigate rich web applications.
The full list of valid WAI-ARIA roles is available at:
http://www.w3.org/TR/wai-aria/roles#roles_categorization
@type String
@default null
*/
ariaRole: null,
/**
Standard CSS class names to apply to the view's outer element. This
property automatically inherits any class names defined by the view's
superclasses as well.
@type Array
@default ['ember-view']
*/
classNames: ['ember-view'],
/**
A list of properties of the view to apply as class names. If the property
is a string value, the value of that string will be applied as a class
name.
// Applies the 'high' class to the view element
Ember.View.create({
classNameBindings: ['priority']
priority: 'high'
});
If the value of the property is a Boolean, the name of that property is
added as a dasherized class name.
// Applies the 'is-urgent' class to the view element
Ember.View.create({
classNameBindings: ['isUrgent']
isUrgent: true
});
If you would prefer to use a custom value instead of the dasherized
property name, you can pass a binding like this:
// Applies the 'urgent' class to the view element
Ember.View.create({
classNameBindings: ['isUrgent:urgent']
isUrgent: true
});
This list of properties is inherited from the view's superclasses as well.
@type Array
@default []
*/
classNameBindings: [],
/**
A list of properties of the view to apply as attributes. If the property is
a string value, the value of that string will be applied as the attribute.
// Applies the type attribute to the element
// with the value "button", like
Ember.View.create({
attributeBindings: ['type'],
type: 'button'
});
If the value of the property is a Boolean, the name of that property is
added as an attribute.
// Renders something like
Removing a view
aContainer.get('childViews') // [aContainer.aView, aContainer.bView]
aContainer.get('childViews').removeObject(aContainer.get('bView'))
aContainer.get('childViews') // [aContainer.aView]
Will result in the following HTML
A
Similarly, adding a child view is accomplished by adding `Ember.View` instances to the
container's `childViews` property.
Given an empty `` the following code
aContainer = Ember.ContainerView.create({
classNames: ['the-container'],
childViews: ['aView', 'bView'],
aView: Ember.View.create({
template: Ember.Handlebars.compile("A")
}),
bView: Ember.View.create({
template: Ember.Handlebars.compile("B")
})
})
aContainer.appendTo('body')
Results in the HTML
A
B
Adding a view
AnotherViewClass = Ember.View.extend({
template: Ember.Handlebars.compile("Another view")
})
aContainer.get('childViews') // [aContainer.aView, aContainer.bView]
aContainer.get('childViews').pushObject(AnotherViewClass.create())
aContainer.get('childViews') // [aContainer.aView, aContainer.bView, ]
Will result in the following HTML
A
B
Another view
Direct manipulation of childViews presence or absence in the DOM via calls to
`remove` or `removeFromParent` or calls to a container's `removeChild` may not behave
correctly.
Calling `remove()` on a child view will remove the view's HTML, but it will remain as part of its
container's `childView`s property.
Calling `removeChild()` on the container will remove the passed view instance from the container's
`childView`s but keep its HTML within the container's rendered view.
Calling `removeFromParent()` behaves as expected but should be avoided in favor of direct
manipulation of a container's `childViews` property.
aContainer = Ember.ContainerView.create({
classNames: ['the-container'],
childViews: ['aView', 'bView'],
aView: Ember.View.create({
template: Ember.Handlebars.compile("A")
}),
bView: Ember.View.create({
template: Ember.Handlebars.compile("B")
})
})
aContainer.appendTo('body')
Results in the HTML
A
B
Calling `aContainer.get('aView').removeFromParent()` will result in the following HTML
B
And the `Ember.View` instance stored in `aContainer.aView` will be removed from `aContainer`'s
`childViews` array.
## Templates and Layout
A `template`, `templateName`, `defaultTemplate`, `layout`, `layoutName` or `defaultLayout`
property on a container view will not result in the template or layout being rendered.
The HTML contents of a `Ember.ContainerView`'s DOM representation will only be the rendered HTML
of its child views.
## Binding a View to Display
If you would like to display a single view in your ContainerView, you can set its `currentView`
property. When the `currentView` property is set to a view instance, it will be added to the
ContainerView's `childViews` array. If the `currentView` property is later changed to a
different view, the new view will replace the old view. If `currentView` is set to `null`, the
last `currentView` will be removed.
This functionality is useful for cases where you want to bind the display of a ContainerView to
a controller or state manager. For example, you can bind the `currentView` of a container to
a controller like this:
// Controller
App.appController = Ember.Object.create({
view: Ember.View.create({
templateName: 'person_template'
})
});
// Handlebars template
{{view Ember.ContainerView currentViewBinding="App.appController.view"}}
@extends Ember.View
*/
Ember.ContainerView = Ember.View.extend({
init: function() {
var childViews = get(this, 'childViews');
Ember.defineProperty(this, 'childViews', childViewsProperty);
this._super();
var _childViews = get(this, '_childViews');
forEach(childViews, function(viewName, idx) {
var view;
if ('string' === typeof viewName) {
view = get(this, viewName);
view = this.createChildView(view);
set(this, viewName, view);
} else {
view = this.createChildView(viewName);
}
_childViews[idx] = view;
}, this);
// Make the _childViews array observable
Ember.A(_childViews);
// Sets up an array observer on the child views array. This
// observer will detect when child views are added or removed
// and update the DOM to reflect the mutation.
get(this, 'childViews').addArrayObserver(this, {
willChange: 'childViewsWillChange',
didChange: 'childViewsDidChange'
});
},
/**
Instructs each child view to render to the passed render buffer.
@param {Ember.RenderBuffer} buffer the buffer to render to
@private
*/
render: function(buffer) {
this.forEachChildView(function(view) {
view.renderToBuffer(buffer);
});
},
/**
When the container view is destroyed, tear down the child views
array observer.
@private
*/
willDestroy: function() {
get(this, 'childViews').removeArrayObserver(this, {
willChange: 'childViewsWillChange',
didChange: 'childViewsDidChange'
});
this._super();
},
/**
When a child view is removed, destroy its element so that
it is removed from the DOM.
The array observer that triggers this action is set up in the
`renderToBuffer` method.
@private
@param {Ember.Array} views the child views array before mutation
@param {Number} start the start position of the mutation
@param {Number} removed the number of child views removed
**/
childViewsWillChange: function(views, start, removed) {
if (removed === 0) { return; }
var changedViews = views.slice(start, start+removed);
this.initializeViews(changedViews, null, null);
this.invokeForState('childViewsWillChange', views, start, removed);
},
/**
When a child view is added, make sure the DOM gets updated appropriately.
If the view has already rendered an element, we tell the child view to
create an element and insert it into the DOM. If the enclosing container view
has already written to a buffer, but not yet converted that buffer into an
element, we insert the string representation of the child into the appropriate
place in the buffer.
@private
@param {Ember.Array} views the array of child views afte the mutation has occurred
@param {Number} start the start position of the mutation
@param {Number} removed the number of child views removed
@param {Number} the number of child views added
*/
childViewsDidChange: function(views, start, removed, added) {
var len = get(views, 'length');
// No new child views were added; bail out.
if (added === 0) return;
var changedViews = views.slice(start, start+added);
this.initializeViews(changedViews, this, get(this, 'templateData'));
// Let the current state handle the changes
this.invokeForState('childViewsDidChange', views, start, added);
},
initializeViews: function(views, parentView, templateData) {
forEach(views, function(view) {
set(view, '_parentView', parentView);
if (!get(view, 'templateData')) {
set(view, 'templateData', templateData);
}
});
},
/**
Schedules a child view to be inserted into the DOM after bindings have
finished syncing for this run loop.
@param {Ember.View} view the child view to insert
@param {Ember.View} prev the child view after which the specified view should
be inserted
@private
*/
_scheduleInsertion: function(view, prev) {
if (prev) {
prev.domManager.after(prev, view);
} else {
this.domManager.prepend(this, view);
}
},
currentView: null,
_currentViewWillChange: Ember.beforeObserver(function() {
var childViews = get(this, 'childViews'),
currentView = get(this, 'currentView');
if (currentView) {
childViews.removeObject(currentView);
}
}, 'currentView'),
_currentViewDidChange: Ember.observer(function() {
var childViews = get(this, 'childViews'),
currentView = get(this, 'currentView');
if (currentView) {
childViews.pushObject(currentView);
}
}, 'currentView')
});
// Ember.ContainerView extends the default view states to provide different
// behavior for childViewsWillChange and childViewsDidChange.
Ember.ContainerView.states = {
parent: Ember.View.states,
inBuffer: {
childViewsDidChange: function(parentView, views, start, added) {
var buffer = parentView.buffer,
startWith, prev, prevBuffer, view;
// Determine where to begin inserting the child view(s) in the
// render buffer.
if (start === 0) {
// If views were inserted at the beginning, prepend the first
// view to the render buffer, then begin inserting any
// additional views at the beginning.
view = views[start];
startWith = start + 1;
view.renderToBuffer(buffer, 'prepend');
} else {
// Otherwise, just insert them at the same place as the child
// views mutation.
view = views[start - 1];
startWith = start;
}
for (var i=startWith; i` and the following code:
someItemsView = Ember.CollectionView.create({
classNames: ['a-collection'],
content: ['A','B','C'],
itemViewClass: Ember.View.extend({
template: Ember.Handlebars.compile("the letter: {{view.content}}")
})
})
someItemsView.appendTo('body')
Will result in the following HTML structure
the letter: A
the letter: B
the letter: C
## Automatic matching of parent/child tagNames
Setting the `tagName` property of a `CollectionView` to any of
"ul", "ol", "table", "thead", "tbody", "tfoot", "tr", or "select" will result
in the item views receiving an appropriately matched `tagName` property.
Given an empty `` and the following code:
anUndorderedListView = Ember.CollectionView.create({
tagName: 'ul',
content: ['A','B','C'],
itemViewClass: Ember.View.extend({
template: Ember.Handlebars.compile("the letter: {{view.content}}")
})
})
anUndorderedListView.appendTo('body')
Will result in the following HTML structure
the letter: A
the letter: B
the letter: C
Additional tagName pairs can be provided by adding to `Ember.CollectionView.CONTAINER_MAP `
Ember.CollectionView.CONTAINER_MAP['article'] = 'section'
## Empty View
You can provide an `Ember.View` subclass to the `Ember.CollectionView` instance as its
`emptyView` property. If the `content` property of a `CollectionView` is set to `null`
or an empty array, an instance of this view will be the `CollectionView`s only child.
aListWithNothing = Ember.CollectionView.create({
classNames: ['nothing']
content: null,
emptyView: Ember.View.extend({
template: Ember.Handlebars.compile("The collection is empty")
})
})
aListWithNothing.appendTo('body')
Will result in the following HTML structure
')
})
viewStates = Ember.StateManager.create({
showingPeople: Ember.ViewState.create({
view: ContactListView
}),
showingPhotos: Ember.ViewState.create({
view: PhotoListView
})
})
viewStates.transitionTo('showingPeople')
The above code will change the rendered HTML from
to
People
Changing the current state via `transitionTo` from `showingPeople` to
`showingPhotos` will remove the `showingPeople` view and add the `showingPhotos` view:
viewStates.transitionTo('showingPhotos')
will change the rendered HTML to
Photos
When entering nested `ViewState`s, each state's view will be draw into the the StateManager's
`rootView` or `rootElement` as siblings.
ContactListView = Ember.View.extend({
classNames: ['my-contacts-css-class'],
template: Ember.Handlebars.compile('
People
')
})
EditAContactView = Ember.View.extend({
classNames: ['editing-a-contact-css-class'],
template: Ember.Handlebars.compile('Editing...')
})
viewStates = Ember.StateManager.create({
showingPeople: Ember.ViewState.create({
view: ContactListView,
withEditingPanel: Ember.ViewState.create({
view: EditAContactView
})
})
})
viewStates.transitionTo('showingPeople.withEditingPanel')
Will result in the following rendered HTML:
People
Editing...
ViewState views are added and removed from their StateManager's view via their
`enter` and `exit` methods. If you need to override these methods, be sure to call
`_super` to maintain the adding and removing behavior:
viewStates = Ember.StateManager.create({
aState: Ember.ViewState.create({
view: Ember.View.extend({}),
enter: function(manager){
// calling _super ensures this view will be
// properly inserted
this._super(manager);
// now you can do other things
}
})
})
## Managing Multiple Sections of A Page With States
Multiple StateManagers can be combined to control multiple areas of an application's rendered views.
Given the following HTML body:
" ],
map: [ 1, "" ],
_default: [ 0, "", "" ]
};
/**
* Given a parent node and some HTML, generate a set of nodes. Return the first
* node, which will allow us to traverse the rest using nextSibling.
*
* We need to do this because innerHTML in IE does not really parse the nodes.
**/
var firstNodeFor = function(parentNode, html) {
var arr = wrapMap[parentNode.tagName.toLowerCase()] || wrapMap._default;
var depth = arr[0], start = arr[1], end = arr[2];
if (needsShy) { html = ''+html; }
var element = document.createElement('div');
element.innerHTML = start + html + end;
for (var i=0; i<=depth; i++) {
element = element.firstChild;
}
// Look for to remove it.
if (needsShy) {
var shyElement = element;
// Sometimes we get nameless elements with the shy inside
while (shyElement.nodeType === 1 && !shyElement.nodeName) {
shyElement = shyElement.firstChild;
}
// At this point it's the actual unicode character.
if (shyElement.nodeType === 3 && shyElement.nodeValue.charAt(0) === "\u00AD") {
shyElement.nodeValue = shyElement.nodeValue.slice(1);
}
}
return element;
};
/**
* In some cases, Internet Explorer can create an anonymous node in
* the hierarchy with no tagName. You can create this scenario via:
*
* div = document.createElement("div");
* div.innerHTML = "
­
hi
";
* div.firstChild.firstChild.tagName //=> ""
*
* If our script markers are inside such a node, we need to find that
* node and use *it* as the marker.
**/
var realNode = function(start) {
while (start.parentNode.tagName === "") {
start = start.parentNode;
}
return start;
};
/**
* When automatically adding a tbody, Internet Explorer inserts the
* tbody immediately before the first
. Other browsers create it
* before the first node, no matter what.
*
* This means the the following code:
*
* div = document.createElement("div");
* div.innerHTML = "
### parentView setting
The `parentView` property of the new `Ember.View` instance created through `{{view}}`
will be set to the `Ember.View` instance of the template where `{{view}}` was called.
aView = Ember.View.create({
template: Ember.Handlebars.compile("{{#view}} my parent: {{parentView.elementId}} {{/view}}")
})
aView.appendTo('body')
Will result in HTML structure:
my parent: ember1
### Setting CSS id and class attributes
The HTML `id` attribute can be set on the `{{view}}`'s resulting element with the `id` option.
This option will _not_ be passed to `Ember.View.create`.
Results in the following HTML structure:
hello.
The HTML `class` attribute can be set on the `{{view}}`'s resulting element with
the `class` or `classNameBindings` options. The `class` option
will directly set the CSS `class` attribute and will not be passed to
`Ember.View.create`. `classNameBindings` will be passed to `create` and use
`Ember.View`'s class name binding functionality:
Results in the following HTML structure:
hello.
### Supplying a different view class
`{{view}}` can take an optional first argument before its supplied options to specify a
path to a custom view class.
The first argument can also be a relative path. Ember will search for the view class
starting at the `Ember.View` of the template where `{{view}}` was used as the root object:
MyApp = Ember.Application.create({})
MyApp.OuterView = Ember.View.extend({
innerViewClass: Ember.View.extend({
classNames: ['a-custom-view-class-as-property']
}),
template: Ember.Handlebars.compile('{{#view "innerViewClass"}} hi {{/view}}')
})
MyApp.OuterView.create().appendTo('body')
Will result in the following HTML:
### Blockless Use
If you provide an `itemViewClass` option that has its own `template` you can omit
the block.
The following template:
And application code
App = Ember.Application.create()
App.items = [
Ember.Object.create({name: 'Dave'}),
Ember.Object.create({name: 'Mary'}),
Ember.Object.create({name: 'Sara'})
]
App.AnItemView = Ember.View.extend({
template: Ember.Handlebars.compile("Greetings {{content.name}}")
})
Will result in the HTML structure below
Greetings Dave
Greetings Mary
Greetings Sara
### Specifying a CollectionView subclass
By default the `{{collection}}` helper will create an instance of `Ember.CollectionView`.
You can supply a `Ember.CollectionView` subclass to the helper by passing it
as the first argument:
### Forwarded `item.*`-named Options
As with the `{{view}}`, helper options passed to the `{{collection}}` will be set on
the resulting `Ember.CollectionView` as properties. Additionally, options prefixed with
`item` will be applied to the views rendered for each item (note the camelcasing):
Will result in the following HTML structure:
Clicking "click me" will trigger the `anActionName` method of the `aView`
object with a `jQuery.Event` object as its argument. The `jQuery.Event`
object will be extended to include a `view` property that is set to the
original view interacted with (in this case the `aView` object).
### Event Propagation
Events triggered through the action helper will automatically have
`.preventDefault()` called on them. You do not need to do so in your event
handlers. To stop propagation of the event, simply return `false` from your
handler.
If you need the default handler to trigger you should either register your
own event handler, or use event methods on your view class.
### Specifying an Action Target
A `target` option can be provided to change which object will receive the
method call. This option must be a string representing a path to an object:
Clicking "click me" in the rendered HTML of the above template will trigger
the `anActionName` method of the object at `MyApplication.someObject`.
The first argument to this method will be a `jQuery.Event` extended to
include a `view` property that is set to the original view interacted with.
A path relative to the template's `Ember.View` instance can also be used as
a target:
Clicking "click me" in the rendered HTML of the above template will trigger
the `anActionName` method of the view's parent view.
The `{{action}}` helper is `Ember.StateManager` aware. If the target of the
action is an `Ember.StateManager` instance `{{action}}` will use the `send`
functionality of StateManagers. The documentation for `Ember.StateManager`
has additional information about this use.
If an action's target does not implement a method that matches the supplied
action name an error will be thrown.
With the following application code
AView = Ember.View.extend({
templateName; 'a-template',
// note: no method 'aMethodNameThatIsMissing'
anActionName: function(event){}
});
aView = AView.create();
aView.appendTo('body');
Will throw `Uncaught TypeError: Cannot call method 'call' of undefined` when
"click me" is clicked.
### Specifying DOM event type
By default the `{{action}}` helper registers for DOM `click` events. You can
supply an `on` option to the helper to specify a different DOM event name:
See `Ember.EventDispatcher` for a list of acceptable DOM event names.
Because `{{action}}` depends on Ember's event dispatch system it will only
function if an `Ember.EventDispatcher` instance is available. An
`Ember.EventDispatcher` instance will be created when a new
`Ember.Application` is created. Having an instance of `Ember.Application`
will satisfy this requirement.
### Specifying a context
By default the `{{action}}` helper passes the current Handlebars context
along in the `jQuery.Event` object. You may specify an alternative object to
pass as the context by providing a property path:
@name Handlebars.helpers.action
@param {String} actionName
@param {Hash} options
*/
EmberHandlebars.registerHelper('action', function(actionName, options) {
var hash = options.hash,
eventName = hash.on || "click",
view = options.data.view,
target, context, controller;
view = get(view, 'concreteView');
if (hash.target) {
target = getPath(this, hash.target, options);
} else if (controller = options.data.keywords.controller) {
target = get(controller, 'target');
}
target = target || view;
context = hash.context ? getPath(this, hash.context, options) : options.contexts[0];
var output = [], url;
if (hash.href && target.urlForEvent) {
url = target.urlForEvent(actionName, context);
output.push('href="' + url + '"');
}
var actionId = ActionHelper.registerAction(actionName, eventName, target, view, context);
output.push('data-ember-action="' + actionId + '"');
return new EmberHandlebars.SafeString(output.join(" "));
});
})();
(function() {
var get = Ember.get, set = Ember.set;
/**
When used in a Handlebars template that is assigned to an `Ember.View` instance's
`layout` property Ember will render the layout template first, inserting the view's
own rendered output at the `{{ yield }}` location.
An empty `` and the following application code:
AView = Ember.View.extend({
classNames: ['a-view-with-layout'],
layout: Ember.Handlebars.compile('
{{ yield }}
'),
template: Ember.Handlebars.compile('I am wrapped')
})
aView = AView.create()
aView.appendTo('body')
Will result in the following HTML output: