steal("can/util", "can/view/callbacks","can/view/elements.js","can/view/bindings","can/control", "can/observe", "can/view/mustache", "can/util/view_model", function (can, viewCallbacks, elements, bindings) {This implements the can.Component which allows you to create widgets
that use a template, a view-model and custom tags.
can.Component implements most of it’s functionality in the can.Component.setup
and the can.Component.prototype.setup functions.
can.Component.setup prepares everything needed by the can.Component.prototype.setup
to hookup the component.
steal("can/util", "can/view/callbacks","can/view/elements.js","can/view/bindings","can/control", "can/observe", "can/view/mustache", "can/util/view_model", function (can, viewCallbacks, elements, bindings) { var paramReplacer = /\{([^\}]+)\}/g;
/**
* @add can.Component
*/
var Component = can.Component = can.Construct.extend( /**
* @static
*/
{When a component is extended, this sets up the component’s internal constructor functions and templates for later fast initialization.
setup: function () {
can.Construct.setup.apply(this, arguments);When can.Component.setup function is ran for the first time, can.Component doesn’t exist yet
which ensures that the following code is ran only in constructors that extend can.Component.
if (can.Component) {
var self = this,
protoViewModel = this.prototype.scope || this.prototype.viewModel;Define a control using the events prototype property.
this.Control = ComponentControl.extend( this.prototype.events );Look to convert protoViewModel to a Map constructor function.
if (!protoViewModel || (typeof protoViewModel === "object" && ! (protoViewModel instanceof can.Map) ) ) {If protoViewModel is an object, use that object as the prototype of an extended Map constructor function. A new instance of that Map constructor function will be created and set a the constructor instance’s viewModel.
this.Map = can.Map.extend(protoViewModel || {});
}
else if (protoViewModel.prototype instanceof can.Map) {If viewModel is a can.Map constructor function, just use that.
this.Map = protoViewModel;
}Look for default @ values. If a @ is found, these
attributes string values will be set and 2-way bound on the
component instance’s viewModel.
this.attributeScopeMappings = {};
can.each(this.Map ? this.Map.defaults : {}, function (val, prop) {
if (val === "@") {
self.attributeScopeMappings[prop] = prop;
}
});Convert the template into a renderer function.
if (this.prototype.template) {If this.prototype.template is a function create renderer from it by
wrapping it with the anonymous function that will pass it the arguments,
otherwise create the render from the string
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);
}
}Register this component to be created when its tag is found.
can.view.tag(this.prototype.tag, function (el, options) {
new self(el, options);
});
}
}
}, { /**
* @prototype
*/ setup: function (el, componentTagData) {Setup values passed to component
var initialViewModelData = {},
component = this,If a template is not provided, we fall back to dynamic scoping regardless of settings.
lexicalContent = ((typeof this.leakScope === "undefined" ?
false :
!this.leakScope) &&
!!this.template),the object added to the scope
viewModel,
frag,an array of teardown stuff that should happen when the element is removed
teardownFunctions = [],
callTeardownFunctions = function(){
for(var i = 0, len = teardownFunctions.length ; i < len; i++) {
teardownFunctions[i]();
}
},
$el = can.$(el),
setupBindings = !can.data($el,"preventDataBindings");Add viewModel prototype properties marked with an “@” to the initialViewModelData object
can.each(this.constructor.attributeScopeMappings, function (val, prop) {
initialViewModelData[prop] = el.getAttribute(can.hyphenate(val));
});
if(setupBindings) {
teardownFunctions.push(bindings.behaviors.viewModel(el, componentTagData, function(initialViewModelData){Make %root available on the viewModel.
initialViewModelData["%root"] = componentTagData.scope.attr("%root");Create the component’s viewModel.
var protoViewModel = component.scope || component.viewModel;
if (component.constructor.Map) {If Map property is set on the constructor use it to wrap the initialViewModelData
viewModel = new component.constructor.Map(initialViewModelData);
} else if (protoViewModel instanceof can.Map) {If component.viewModel is instance of can.Map assign it to the viewModel
viewModel = protoViewModel;
} else if (can.isFunction(protoViewModel)) {If component.viewModel is a function, call the function and
var scopeResult = protoViewModel.call(component, initialViewModelData, componentTagData.scope, el);
if (scopeResult instanceof can.Map) {If the function returns a can.Map, use that as the viewModel
viewModel = scopeResult;
} else if (scopeResult.prototype instanceof can.Map) {If scopeResult is of a can.Map type, use it to wrap the initialViewModelData
viewModel = new scopeResult(initialViewModelData);
} else {Otherwise extend can.Map with the scopeResult and initialize it with the initialViewModelData
viewModel = new(can.Map.extend(scopeResult))(initialViewModelData);
}
}
var oldSerialize = viewModel.serialize;
viewModel.serialize = function () {
var result = oldSerialize.apply(this, arguments);
delete result["%root"];
return result;
};
return viewModel;
}, initialViewModelData));
}Set viewModel to this.viewModel and set it to the element’s data object as a viewModel property
this.scope = this.viewModel = viewModel;
can.data($el, "scope", this.viewModel);
can.data($el, "viewModel", this.viewModel);
can.data($el,"preventDataBindings", true);Create a real Scope object out of the viewModel property The scope used to render the component’s template. However, if there is no template, the “light” dom is rendered with this anyway.
var shadowScope;
if(lexicalContent) {
shadowScope = can.view.Scope.refsScope().add(this.viewModel,{viewModel: true});
} else {if this component has a template, render the template with it’s own Refs scope otherwise, just add this component’s viewModel.
shadowScope = ( this.constructor.renderer ?
componentTagData.scope.add( new can.view.Scope.Refs() ) :
componentTagData.scope )
.add(this.viewModel,{viewModel: true});
}
var options = {
helpers: {}
},
addHelper = function(name, fn) {
options.helpers[name] = function() {
return fn.apply(viewModel, arguments);
};
};Setup helpers to callback with this as the component
can.each(this.helpers || {}, function (val, prop) {
if (can.isFunction(val)) {
addHelper(prop, val);
}
});Setup simple helpers
can.each(this.simpleHelpers || {}, function(val, prop) {!steal-remove-start
if(options.helpers[prop]) {
can.dev.warn('Component ' + component.tag +
' already has a helper called ' + prop);
}!steal-remove-end
Convert the helper
addHelper(prop, can.view.simpleHelper(val));
});events controlCreate a control to listen to events
this._control = new this.constructor.Control(el, {Pass the viewModel to the control so we can listen to it’s changes from the controller.
scope: this.viewModel,
viewModel: this.viewModel,
destroy: callTeardownFunctions
});Keep a nodeList so we can kill any directly nested nodeLists within this component
var nodeList = can.view.nodeLists.register([], undefined, componentTagData.parentNodeList || true, false);
nodeList.expression = "<"+this.tag+">";
teardownFunctions.push(function(){
can.view.nodeLists.unregister(nodeList);
});If this component has a template (that we’ve already converted to a renderer)
if (this.constructor.renderer) {If options.tags doesn’t exist set it to an empty object.
if (!options.tags) {
options.tags = {};
}We need be alerted to when a
options.tags.content = function contentHookup(el, contentTagData) {First check if there was content within the custom tag
otherwise, render what was within componentTagData.subtemplate is the content inside this component
var subtemplate = componentTagData.subtemplate || contentTagData.subtemplate,
renderingLightContent = subtemplate === componentTagData.subtemplate;
if (subtemplate) {contentTagData.options is a viewModel of helpers where <content> was found, so
the right helpers should already be available.
However, _tags.content is going to point to this current content callback. We need to
remove that so it will walk up the chain
delete options.tags.content;By default, light dom scoping is
dynamic. This means that any {{foo}}
bindings inside the “light dom” content of
the component will have access to the
internal viewModel. This can be overridden to be
lexical with the leakScope option.
var lightTemplateData;
if( renderingLightContent ) {
if(lexicalContent) {render with the same scope the component was found within.
lightTemplateData = componentTagData;
} else {render with the component’s viewModel mixed in, however we still want the outer refs to be used, NOT the component’s refs
lightTemplateData = {
scope: contentTagData.scope.cloneFromRef(),
options: contentTagData.options
};
}
} else {we are rendering default content so this content should
use the same scope as the
lightTemplateData = contentTagData;
}
if(contentTagData.parentNodeList) {
var frag = subtemplate( lightTemplateData.scope, lightTemplateData.options, contentTagData.parentNodeList );
elements.replace([el], frag);
} else {
can.view.live.replace([el], subtemplate( lightTemplateData.scope, lightTemplateData.options ));
}Restore the content tag so it could potentially be used again (as in lists)
options.tags.content = contentHookup;
}
};Render the component’s template
frag = this.constructor.renderer(shadowScope, componentTagData.options.add(options), nodeList);
} else {Otherwise render the contents between the element
if(componentTagData.templateType === "legacy") {
frag = can.view.frag(componentTagData.subtemplate ? componentTagData.subtemplate(shadowScope, componentTagData.options.add(options)) : "");
} else {we need to be the parent … or we need to
frag = componentTagData.subtemplate ?
componentTagData.subtemplate(shadowScope, componentTagData.options.add(options), nodeList) :
document.createDocumentFragment();
}
}Append the resulting document fragment to the element
can.appendChild(el, frag, can.document);update the nodeList with the new children so the mapping gets applied
can.view.nodeLists.update(nodeList, can.childNodes(el));
}
});
var ComponentControl = can.Control.extend({Change lookup to first look in the viewModel.
_lookup: function (options) {
return [options.scope, options, window];
},
_action: function (methodName, options, controlInstance ) {
var hasObjectLookup, readyCompute;
paramReplacer.lastIndex = 0;
hasObjectLookup = paramReplacer.test(methodName);If we don’t have options (a control instance), we’ll run this
later.
if( !controlInstance && hasObjectLookup) {
return;
} else if( !hasObjectLookup ) {
return can.Control._action.apply(this, arguments);
} else {We have hasObjectLookup and controlInstance.
readyCompute = can.compute(function(){
var delegate;Set the delegate target and get the name of the event we’re listening to.
var name = methodName.replace(paramReplacer, function(matched, key){
var value;If we are listening directly on the viewModel set it as a delegate target.
if(key === "scope" || key === "viewModel") {
delegate = options.viewModel;
return "";
}Remove viewModel. from the start of the key and read the value from the viewModel.
key = key.replace(/^(scope|^viewModel)\./,"");
value = can.compute.read(options.viewModel, can.compute.read.reads(key), {if we find a compute, we should bind on that and not read it
readCompute: false
}).value;If value is undefined use can.getObject to get the value.
if(value === undefined) {
value = can.getObject(key);
}If value is a string we just return it, otherwise we set it as a delegate target.
if(typeof value === "string") {
return value;
} else {
delegate = value;
return "";
}
});Get the name of the event we’re listening to.
var parts = name.split(/\s+/g),
event = parts.pop();Return everything needed to handle the event we’re listening to.
return {
processor: this.processors[event] || this.processors.click,
parts: [name, parts.join(" "), event],
delegate: delegate || undefined
};
}, this);Create a handler function that we’ll use to handle the change event on the readyCompute.
var handler = function(ev, ready){unbinds the old binding
controlInstance._bindings.control[methodName](controlInstance.element);binds the new
controlInstance._bindings.control[methodName] = ready.processor(
ready.delegate || controlInstance.element,
ready.parts[2], ready.parts[1], methodName, controlInstance);
};
readyCompute.bind("change", handler);
controlInstance._bindings.readyComputes[methodName] = {
compute: readyCompute,
handler: handler
};
return readyCompute();
}
}
},Extend events with a setup method that listens to changes in viewModel and
rebinds all templated event handlers.
{
setup: function (el, options) {
this.scope = options.scope;
this.viewModel = options.viewModel;
return can.Control.prototype.setup.call(this, el, options);
},
off: function(){If this._bindings exists we need to go through it’s readyComputes and manually
unbind change event listeners set by the controller.
if( this._bindings ) {
can.each(this._bindings.readyComputes || {}, function (value) {
value.compute.unbind("change", value.handler);
});
}Call can.Control.prototype.off function on this instance to cleanup the bindings.
can.Control.prototype.off.apply(this, arguments);
this._bindings.readyComputes = {};
},
destroy: function() {
can.Control.prototype.destroy.apply( this, arguments );
if(typeof this.options.destroy === 'function') {
this.options.destroy.apply(this, arguments);
}
}
});
/**
* @description Read and write a component element's viewModel.
*
* @function can.viewModel
* @parent can.util
* @signature `can.viewModel(el[, attr[, value]])`
* @param {HTMLElement|NodeList} el can.Component element to get viewModel of.
* @param {String} [attr] Attribute name to access.
* @param {*} [val] Value to write to the viewModel attribute.
*
* @return {*} If only one argument, returns the viewModel itself. If two
* arguments are given, returns the attribute value. If three arguments
* are given, returns the element itself after assigning the value (for
* chaining).
*
* @body
*
* `can.viewModel` can be used to directly access a [can.Component]'s
* viewModel. Depending on how it's called, it can be used to get the
* entire viewModel object, read a specific property from it, or write a
* property. The property read and write features can be seen as a
* shorthand for code such as `$("my-thing").viewModel().attr("foo", val);`
*
* If using jQuery, this function is accessible as a jQuery plugin,
* with one fewer argument to the call. For example,
* `$("my-element").viewModel("name", "Whatnot");`
*
*/Define the can.viewModel function that can be used to retrieve the
viewModel from the element
var $ = can.$;If $ has an fn object create the
scope plugin that returns the scope object.
if ($.fn) {
$.fn.scope = $.fn.viewModel = function () {Just use can.scope as the base for this function instead
of repeating ourselves.
return can.viewModel.apply(can, [this].concat(can.makeArray(arguments)));
};
}
return Component;
});