• ¶

    can/component/component.js

    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) {
  • ¶

    Helpers

    Attribute names to ignore for setting viewModel values.

    	var paramReplacer = /\{([^\}]+)\}/g;
    
    	/**
    	 * @add can.Component
    	 */
    	var Component = can.Component = can.Construct.extend(
  • ¶

    Static

    		/**
    		 * @static
    		 */
    
    		{
  • ¶

    setup

    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

    			/**
    			 * @prototype
    			 */
  • ¶

    setup

    When a new component instance is created, setup bindings, render the template, etc.

    			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");
  • ¶

    Scope

  • ¶

    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);
    						};
    					};
  • ¶

    Helpers

  • ¶

    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 control

  • ¶

    Create 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
    				});
  • ¶

    Rendering

  • ¶

    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 element is rendered so we can put the original contents of the widget in its place

    					options.tags.content = function contentHookup(el, contentTagData) {
  • ¶

    First check if there was content within the custom tag otherwise, render what was within , the default code. 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

    {{some value }} To fix this, we walk down the scope to the component’s ref, clone scopes from that point up use that as the new scope.

    									lightTemplateData = {
    										scope: contentTagData.scope.cloneFromRef(),
    										options: contentTagData.options
    									};
    								}
    
    							} else {
  • ¶

    we are rendering default content so this content should use the same scope as the tag was found within.

    								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;
    });