todomvc
Version:
> Helping you select an MV\* framework
256 lines (219 loc) • 7.59 kB
JavaScript
/*!
* CanJS - 2.0.3
* http://canjs.us/
* Copyright (c) 2013 Bitovi
* Tue, 26 Nov 2013 18:21:22 GMT
* Licensed MIT
* Includes: CanJS default build
* Download from: http://canjs.us/
*/
define(["can/util/library", "can/control", "can/observe", "can/view/mustache", "can/view/bindings"], function(can){
var ignoreAttributesRegExp = /dataViewId|class|id/i
/**
* @add can.Component
*/
var Component = can.Component = can.Construct.extend(
/**
* @static
*/
{
setup: function(){
can.Construct.setup.apply( this, arguments );
if(can.Component){
var self = this;
this.Control = can.Control.extend({
_lookup: function(options){
return [options.scope, options, window]
}
},can.extend({
setup: function(el, options){
var res = can.Control.prototype.setup.call(this, el, options)
this.scope = options.scope;
// call on() whenever scope changes
var self = this;
this.on(this.scope,"change",function(){
self.on();
self.on(self.scope,"change",arguments.callee);
});
return res;
}
},this.prototype.events));
var attributeScopeMappings = {};
// go through scope and get attribute ones
can.each(this.prototype.scope, function(val, prop){
if(val === "@") {
attributeScopeMappings[prop] = prop;
}
})
this.attributeScopeMappings = attributeScopeMappings;
// If scope is an object,
if(! this.prototype.scope || typeof this.prototype.scope === "object" ){
// use that object as the prototype of an extened Map constructor function.
// A new instance of that Map constructor function will be created and
// set as this.scope.
this.Map = can.Map.extend( this.prototype.scope||{} );
}
// If scope is a can.Map constructor function,
else if(this.prototype.scope.prototype instanceof can.Map) {
// just use that.
this.Map = this.prototype.scope;
}
if(this.prototype.template){
if(typeof this.prototype.template == "function"){
var temp = this.prototype.template
this.renderer = function(){
return can.view.frag(temp.apply(null, arguments))
}
} else {
this.renderer =can.view.mustache( this.prototype.template );
}
}
can.view.Scanner.tag(this.prototype.tag,function(el, options){
new self(el, options)
});
}
}
},{
/**
* @prototype
*/
setup: function(el, hookupOptions){
// Setup values passed to component
var initalScopeData = {},
component = this,
twoWayBindings = {},
// what scope property is currently updating
scopePropertyUpdating,
// the object added to the scope
componentScope;
// scope prototype properties marked with an "@" are added here
can.each(this.constructor.attributeScopeMappings,function(val, prop){
initalScopeData[prop] = el.getAttribute(can.hyphenate(val));
})
// get the value in the scope for each attribute
// the hookup should probably happen after?
can.each(can.makeArray(el.attributes), function(node, index){
var name = can.camelize(node.nodeName.toLowerCase()),
value = node.value;
// ignore attributes already in ScopeMappings
if(component.constructor.attributeScopeMappings[name] || ignoreAttributesRegExp.test(name)){
return;
}
// Cross-bind the value in the scope to this
// component's scope
var computeData = hookupOptions.scope.computeData(value, {args: []}),
compute = computeData.compute;
// bind on this, check it's value, if it has dependencies
var handler = function(ev, newVal){
scopePropertyUpdating = name;
componentScope.attr(name, newVal);
scopePropertyUpdating = null;
}
// compute only returned if bindable
compute.bind("change", handler);
// set the value to be added to the scope
initalScopeData[name] = compute();
if(!compute.hasDependencies) {
compute.unbind("change", handler);
} else {
// make sure we unbind (there's faster ways of doing this)
can.bind.call(el,"removed",function(){
compute.unbind("change", handler);
})
// setup two-way binding
twoWayBindings[name] = computeData
}
})
if(this.constructor.Map){
componentScope = new this.constructor.Map(initalScopeData);
} else if(this.scope instanceof can.Map) {
componentScope = this.scope;
} else if(can.isFunction(this.scope)){
var scopeResult = this.scope(initalScopeData, hookupOptions.scope, el);
// if the function returns a can.Map, use that as the scope
if(scopeResult instanceof can.Map){
componentScope = scopeResult
} else if( scopeResult.prototype instanceof can.Map ){
componentScope = new scopeResult(initalScopeData);
} else {
componentScope = new ( can.Map.extend(scopeResult) )(initalScopeData);
}
}
var handlers = {};
// setup reverse bindings
can.each(twoWayBindings, function(computeData, prop){
handlers[prop] = function(ev, newVal){
// check that this property is not being changed because
// it's source value just changed
if(scopePropertyUpdating !== prop){
computeData.compute(newVal)
}
}
componentScope.bind(prop, handlers[prop])
});
// teardown reverse bindings when element is removed
can.bind.call(el,"removed",function(){
can.each(handlers, function(handler, prop){
componentScope.unbind(prop, handlers[prop])
})
})
this.scope = componentScope;
can.data(can.$(el),"scope", this.scope)
// create a real Scope object out of the scope property
var renderedScope = hookupOptions.scope.add( this.scope ),
// setup helpers to callback with `this` as the component
helpers = {};
can.each(this.helpers || {}, function(val, prop){
if(can.isFunction(val)) {
helpers[prop] = function(){
return val.apply(componentScope, arguments)
}
}
});
// create a control to listen to events
this._control = new this.constructor.Control(el, {scope: this.scope});
// if this component has a template (that we've already converted to a renderer)
if( this.constructor.renderer ) {
// add content to tags
if(!helpers._tags){
helpers._tags = {};
}
// we need be alerted to when a <content> element is rendered so we can put the original contents of the widget in its place
helpers._tags.content = function(el, rendererOptions){
// first check if there was content within the custom tag
// otherwise, render what was within <content>, the default code
var subtemplate = hookupOptions.subtemplate || rendererOptions.subtemplate
if(subtemplate) {
var frag = can.view.frag( subtemplate(rendererOptions.scope, rendererOptions.options.add(helpers) ) );
can.insertBefore(el.parentNode, frag, el);
can.remove( can.$(el) );
}
}
// render the component's template
var frag = this.constructor.renderer( renderedScope, helpers);
} else {
// otherwise render the contents between the
var frag = can.view.frag( hookupOptions.subtemplate ? hookupOptions.subtemplate(renderedScope, hookupOptions.options.add(helpers)) : "");
}
can.appendChild(el, frag);
}
})
if(window.$ && $.fn){
$.fn.scope = function(attr){
if( attr ) {
return this.data("scope").attr(attr)
} else {
return this.data("scope")
}
}
}
can.scope = function(el, attr){
var el = can.$(el);
if( attr ){
return can.data(el,"scope").attr(attr)
} else {
return can.data(el, "scope")
}
}
return Component;
});