• ¶

    can/view/bindings/bindings.js

    This module provides CanJS’s default data and event bindings. It’s broken up into several parts:

    • Behaviors - Binding behaviors that run given an attribute or element.
    • Attribute Syntaxes - Hooks up custom attributes to their behaviors.
    • getComputeFrom - Methods that return a compute cross bound to the scope, viewModel, or element.
    • bind - Methods for setting up cross binding
    • getBindingInfo - A helper that returns the details of a data binding given an attribute.
    • makeDataBinding - A helper method for setting up a data binding.
    • initializeValues - A helper that initializes a data binding.
    steal("can/util",
    	"can/view/stache/expression.js",
    	"can/view/callbacks",
    	"can/view/live",
    	"can/view/scope",
    	"can/view/href", function (can, expression, viewCallbacks, live) {
  • ¶

    Behaviors

    	var behaviors = {
  • ¶

    bindings.behaviors.viewModel

    Sets up all of an element’s data binding attributes to a “soon-to-be-created” viewModel. This is primarily used by can.Component to ensure that its viewModel is initialized with values from the data bindings as quickly as possible. Component could look up the data binding values itself. However, that lookup would have to be duplicated when the bindings are established. Instead, this uses the makeDataBinding helper, which allows creation of the viewModel after scope values have been looked up.

    • makeViewModel(initialViewModelData) - a function that returns the viewModel.
    • initialViewModelData any initial data that should already be added to the viewModel.

    Returns:

    • function - a function that tears all the bindings down. Component wants all the bindings active so cleanup can be done during a component being removed.
    		viewModel: function(el, tagData, makeViewModel, initialViewModelData){
    			initialViewModelData = initialViewModelData || {};
    			
    			var bindingsSemaphore = {},
    				viewModel,
  • ¶

    Stores callbacks for when the viewModel is created.

    				onCompleteBindings = [],
  • ¶

    Stores what needs to be called when the element is removed to prevent memory leaks.

    				onTeardowns = {},
  • ¶

    Track info about each binding, we need this for binding attributes correctly.

    				bindingInfos = {},
    				attributeViewModelBindings = can.extend({}, initialViewModelData);
  • ¶

    For each attribute, we start the binding process, and save what’s returned to be used when the viewModel is created, the element is removed, or the attribute changes values.

    			can.each( can.makeArray(el.attributes), function(node){
    				
    				var dataBinding = makeDataBinding(node, el, {
    					templateType: tagData.templateType,
    					scope: tagData.scope,
    					semaphore: bindingsSemaphore,
    					getViewModel: function(){
    						return viewModel;
    					},
    					attributeViewModelBindings: attributeViewModelBindings,
    					alreadyUpdatedChild: true,
    					nodeList: tagData.parentNodeList
    				});
    				if(dataBinding) {
  • ¶

    For bindings that change the viewModel,

    					if(dataBinding.onCompleteBinding) {
  • ¶

    save the initial value on the viewModel.

    						if(dataBinding.bindingInfo.parentToChild && dataBinding.value !== undefined) {
    							initialViewModelData[cleanVMName(dataBinding.bindingInfo.childName)] = dataBinding.value;
    						}
  • ¶

    Save what needs to happen after the viewModel is created.

    						onCompleteBindings.push(dataBinding.onCompleteBinding);
    					}
    					onTeardowns[node.name] = dataBinding.onTeardown;
    				}
    				
    			});
  • ¶

    Create the viewModel and call what needs to be happen after the viewModel is created.

    			viewModel = makeViewModel(initialViewModelData);
    			
    			for(var i = 0, len = onCompleteBindings.length; i < len; i++) {
    				onCompleteBindings[i]();
    			}
  • ¶

    Listen to attribute changes and re-initialize the bindings.

    			can.bind.call(el, "attributes", function (ev) {
    				
    				var attrName = ev.attributeName,
    					value = el.getAttribute(attrName);
    				
    				if( onTeardowns[attrName] ) {
    					onTeardowns[attrName]();
    				}
  • ¶

    Parent attribute bindings we always re-setup.

    				var parentBindingWasAttribute = bindingInfos[attrName] && bindingInfos[attrName].parent === "attribute";
    				
    				if(value !== null || parentBindingWasAttribute ) {
    					var dataBinding = makeDataBinding({name: attrName, value: value}, el, {
    						templateType: tagData.templateType,
    						scope: tagData.scope,
    						semaphore: {},
    						getViewModel: function(){
    							return viewModel;
    						},
    						attributeViewModelBindings: attributeViewModelBindings,
  • ¶

    always update the viewModel accordingly.

    						initializeValues: true,
    						nodeList: tagData.parentNodeList
    					});
    					if(dataBinding) {
  • ¶

    The viewModel is created, so call callback immediately.

    						if(dataBinding.onCompleteBinding) {
    							dataBinding.onCompleteBinding();
    						}
    						bindingInfos[attrName] = dataBinding.bindingInfo;
    						onTeardowns[attrName] = dataBinding.onTeardown;
    					}
    				}
    			});
    			
    			return function(){
    				for(var attrName in onTeardowns) {
    					onTeardowns[attrName]();
    				}
    			};
    		},
  • ¶

    bindings.behaviors.data

    This is called when an individual data binding attribute is placed on an element. For example {^value}="name".

    		data: function(el, attrData){
    			if(can.data(can.$(el),"preventDataBindings")){
    				return;
    			}
    			var viewModel = can.viewModel(el),
    				semaphore = {},
    				teardown;
  • ¶

    Setup binding

    			var dataBinding = makeDataBinding({
    				name: attrData.attributeName,
    				value: el.getAttribute(attrData.attributeName),
    				nodeList: attrData.nodeList
    			}, el, {
    				templateType: attrData.templateType,
    				scope: attrData.scope,
    				semaphore: semaphore,
    				getViewModel: function(){
    					return viewModel;
    				}
    			});
    			
    			if(dataBinding.onCompleteBinding) {
    				dataBinding.onCompleteBinding();
    			}
    			teardown = dataBinding.onTeardown;
    
    			can.one.call(el, 'removed', function(){
    				teardown();
    			});
  • ¶

    Listen for changes

    			can.bind.call(el, "attributes", function (ev) {
    				var attrName = ev.attributeName,
    					value = el.getAttribute(attrName);
    					
    				if( attrName === attrData.attributeName ) {
    					
    					if( teardown ) {
    						teardown();
    					}
    					
    					if(value !== null  ) {
    						
    						var dataBinding = makeDataBinding({name: attrName, value: value}, el, {
    							templateType: attrData.templateType,
    							scope: attrData.scope,
    							semaphore: semaphore,
    							getViewModel: function(){
    								return viewModel;
    							},
  • ¶

    always update the viewModel accordingly.

    							initializeValues: true,
    							nodeList: attrData.nodeList
    						});
    						if(dataBinding) {
  • ¶

    The viewModel is created, so call callback immediately.

    							if(dataBinding.onCompleteBinding) {
    								dataBinding.onCompleteBinding();
    							}
    							teardown = dataBinding.onTeardown;
    						}
    					}
    					
    				}
    			});
    		},
  • ¶

    bindings.behaviors.reference

    Provides the shorthand *ref behavior that exports the viewModel. For example {^value}="name".

    		reference: function(el, attrData) {
    			if(el.getAttribute(attrData.attributeName)) {
    				console.warn("*reference attributes can only export the view model.");
    			}
    	
    			var name = can.camelize( attrData.attributeName.substr(1).toLowerCase() );
    	
    			var viewModel = can.viewModel(el);
    			var refs = attrData.scope.getRefs();
    			refs._context.attr("*"+name, viewModel);
    		},
  • ¶

    bindings.behaviors.event

    The following section contains code for implementing the can-EVENT attribute. This binds on a wildcard attribute name. Whenever a view is being processed and can-xxx (anything starting with can-), this callback will be run. Inside, its setting up an event handler that calls a method identified by the value of this attribute.

    		event: function(el, data) {
  • ¶

    Get the event name and if we are listening to the element or viewModel. The attribute name is the name of the event.

    			var attributeName = data.attributeName,
  • ¶

    The old way of binding is can-X

    				legacyBinding = attributeName.indexOf('can-') === 0,
    				event = attributeName.indexOf('can-') === 0 ?
    					attributeName.substr("can-".length) :
    					can.camelize(removeBrackets(attributeName, '(', ')')),
    				onBindElement = legacyBinding;
    	
    			if(event.charAt(0) === "$") {
    				event = event.substr(1);
    				onBindElement = true;
    			}
  • ¶

    This is the method that the event will initially trigger. It will look up the method by the string name passed in the attribute and call it.

    			var handler = function (ev) {
    					var attrVal = el.getAttribute(attributeName);
    					if (!attrVal) { return; }
    	
    					var $el = can.$(el),
    						viewModel = can.viewModel($el[0]);
  • ¶

    expression.parse will read the attribute value and parse it identically to how mustache helpers get parsed.

    					var expr = expression.parse(removeBrackets(attrVal),{lookupRule: "method", methodRule: "call"});
    	
    					if(!(expr instanceof expression.Call) && !(expr instanceof expression.Helper)) {
    						var defaultArgs = can.map( [data.scope._context, $el].concat(can.makeArray(arguments) ), function(data){
    							return new expression.Literal(data);
    						});
    						expr = new expression.Call(expr, defaultArgs, {} );
    					}
  • ¶

    make a scope with these things just under

    	
    					var localScope = data.scope.add({
    						"@element": $el,
    						"@event": ev,
    						"@viewModel": viewModel,
    						"@scope": data.scope,
    						"@context": data.scope._context,
    	
    						"%element": this,
    						"$element": $el,
    						"%event": ev,
    						"%viewModel": viewModel,
    						"%scope": data.scope,
    						"%context": data.scope._context
    					},{
    						notContext: true
    					});
  • ¶

    We grab the first item and treat it as a method that we’ll call.

    					var scopeData = localScope.read(expr.methodExpr.key, {
    						isArgument: true
    					});
  • ¶

    We break out early if the first argument isn’t available anywhere.

    	
    					if (!scopeData.value) {
    						scopeData = localScope.read(expr.methodExpr.key, {
    							isArgument: true
    						});
  • ¶

    !steal-remove-start

    						can.dev.warn("can/view/bindings: " + attributeName + " couldn't find method named " + expr.methodExpr.key, {
    							element: el,
    							scope: data.scope
    						});
  • ¶

    !steal-remove-end

    	
    						return null;
    					}
    
    					var args = expr.args(localScope, null)();
    					
    	
    					return scopeData.value.apply(scopeData.parent, args);
    				};
  • ¶

    This code adds support for special event types, like can-enter=”foo”. special.enter (or any special[event]) is a function that returns an object containing an event and a handler. These are to be used for binding. For example, when a user adds a can-enter attribute, we’ll bind on the keyup event, and the handler performs special logic to determine on keyup if the enter key was pressed.

    			if (special[event]) {
    				var specialData = special[event](data, el, handler);
    				handler = specialData.handler;
    				event = specialData.event;
    			}
  • ¶

    Bind the handler defined above to the element we’re currently processing and the event name provided in this attribute name (can-click=”foo”)

    			can.bind.call(onBindElement ? el : can.viewModel(el), event, handler);
  • ¶

    Create a handler that will unbind itself and the event when the attribute is removed from the DOM

    			var attributesHandler = function(ev) {
    				if(ev.attributeName === attributeName && !this.getAttribute(attributeName)) {
    	
    					can.unbind.call(onBindElement ? el : can.viewModel(el), event, handler);
    					can.unbind.call(el, 'attributes', attributesHandler);
    				}
    			};
    			can.bind.call(el, 'attributes', attributesHandler);
    		},
  • ¶

    bindings.behaviors.value

    Behavior for the deprecated can-value

    		value: function(el, data) {
    			var propName = "$value",
    				attrValue = can.trim(removeBrackets(el.getAttribute("can-value"))),
    				getterSetter;
    	
    			if (el.nodeName.toLowerCase() === "input" && ( el.type === "checkbox" || el.type === "radio" ) ) {
    	
    				var property = getComputeFrom.scope(el, data.scope, attrValue, {}, true);
    				if (el.type === "checkbox") {
    	
    					var trueValue = can.attr.has(el, "can-true-value") ? el.getAttribute("can-true-value") : true,
    						falseValue = can.attr.has(el, "can-false-value") ? el.getAttribute("can-false-value") : false;
    	
    					getterSetter = can.compute(function(newValue){
  • ¶

    jshint eqeqeq: false

    						if(arguments.length) {
    							property(newValue ? trueValue : falseValue);
    						}
    						else {
    							return property() == trueValue;
    						}
    					});
    				}
    				else if(el.type === "radio") {
  • ¶

    radio is two-way bound to if the property value equals the element value

    	
    					getterSetter = can.compute(function(newValue){
  • ¶

    jshint eqeqeq: false

    						if(arguments.length) {
    							if( newValue ) {
    								property(el.value);
    							}
    						}
    						else {
    							return property() == el.value;
    						}
    					});
    	
    				}
    				propName = "$checked";
    				attrValue = "getterSetter";
    				data.scope = new can.view.Scope({
    					getterSetter: getterSetter
    				});
    			}
  • ¶

    For contenteditable elements, we instantiate a Content control.

    			else if (isContentEditable(el)) {
    				propName = "$innerHTML";
    			}
    	
    			var dataBinding = makeDataBinding({
    				name: "{("+propName+"})",
    				value: attrValue
    			}, el, {
    				templateType: data.templateType,
    				scope: data.scope,
    				semaphore: {},
    				initializeValues: true,
    				legacyBindings: true,
    				syncChildWithParent: true
    			});
    			
    			can.one.call(el, 'removed', function(){
    				dataBinding.onTeardown();
    			});
    	
    		}
    	};
  • ¶

    Attribute Syntaxes

    The following sets up the bindings functions to be called when called in a template.

  • ¶

    {}="bar" data bindings.

    	can.view.attr(/^\{[^\}]+\}$/, behaviors.data);
  • ¶

    *ref-export shorthand.

    	can.view.attr(/\*[\w\.\-_]+/, behaviors.reference);
  • ¶

    (EVENT) event bindings.

    	can.view.attr(/^\([\$?\w\.\-]+\)$/, behaviors.event);
  • ¶

    !steal-remove-start

    	function syntaxWarning(el, attrData) {
    		can.dev.warn('can/view/bindings/bindings.js: mismatched binding syntax - ' + attrData.attributeName);
    	}
    	can.view.attr(/^\(.+\}$/, syntaxWarning);
    	can.view.attr(/^\{.+\)$/, syntaxWarning);
    	can.view.attr(/^\(\{.+\}\)$/, syntaxWarning);
  • ¶

    !steal-remove-end

  • ¶

    Legacy bindings.

    	can.view.attr(/can-[\w\.]+/, behaviors.event);
    	can.view.attr("can-value", behaviors.value);
  • ¶

    getComputeFrom

    An object of helper functions that make a getter/setter compute on different types of objects.

    	var getComputeFrom = {
  • ¶

    getComputeFrom.scope

    Returns a compute from the scope. This handles expressions like someMethod(.,1).

    		scope: function(el, scope, scopeProp, bindingData, mustBeACompute, stickyCompute){
    			if(!scopeProp) {
    				return can.compute();
    			} else {
    				if(mustBeACompute) {
    					var parentExpression = expression.parse(scopeProp,{baseMethodType: "Call"});
    					return parentExpression.value(scope, new can.view.Options({}));
    				} else {
    					return function(newVal){
    						scope.attr(cleanVMName(scopeProp), newVal);
    					};
    				}
    				
    			}
    			
    		},
  • ¶

    getComputeFrom.viewModel

    Returns a compute that’s two-way bound to the viewModel returned by options.getViewModel().

    		viewModel: function(el, scope, vmName, bindingData, mustBeACompute, stickyCompute) {
    			var setName = cleanVMName(vmName);
    			if(mustBeACompute) {
    				return can.compute(function(newVal){
    					var viewModel = bindingData.getViewModel();
    					if(arguments.length) {
    						viewModel.attr(setName,newVal);
    					} else {
    						return vmName === "." ? viewModel : can.compute.read(viewModel, can.compute.read.reads(vmName), {}).value;
    					}
    				});
    			} else {
    
    				return function(newVal){
    					var childCompute;
    					var viewModel = bindingData.getViewModel();
    
    					if(stickyCompute) {
    						childCompute = viewModel._get(setName, { readCompute: false });
  • ¶

    childCompute is a compute at this point unless it was locally overwritten in the child viewModel.

    						if(!childCompute || !childCompute.isComputed) {
  • ¶

    If it was locally overwritten, make a new compute for the property.

    							childCompute = can.compute();
    							viewModel._set(setName, childCompute, { readCompute: false });
    						}
  • ¶

    Otherwise update the compute’s value.

    						childCompute(newVal);
    					} else {
    						viewModel.attr(setName,newVal);
    					}
    				};
    			}
    		},
  • ¶

    getComputeFrom.attribute

    Returns a compute that is two-way bound to an attribute or property on the element.

    		attribute: function(el, scope, prop, bindingData, mustBeACompute, stickyCompute, event){
    			var hasChildren = el.nodeName.toLowerCase() === "select",
    				isMultiselectValue = prop === "value" && hasChildren && el.multiple,
    				isStringValue,
    				lastSet,
    				scheduledAsyncSet = false,
    				timer,
    				parentEvents,
    				originalValue;
  • ¶

    Determine the event or events we need to listen to when this value changes.

    			if(!event) {
    				if(prop === "innerHTML") {
    					event = ["blur","change"];
    				}
    				else {
    					event = "change";
    				}
    			}
    			if(!can.isArray(event)) {
    				event = [event];
    			}
  • ¶

    Sets the element property or attribute.

    			var set = function(newVal){
  • ¶

    Templates write parent’s out before children. This should probably change. But it means we don’t do a set immediately.

    					if(hasChildren && !scheduledAsyncSet) {
    						clearTimeout(timer);
    						timer = setTimeout(function(){
    							set(newVal);
    						},1);
    					}
    					
    					lastSet = newVal;
    					
    					if(isMultiselectValue) {
    						if (newVal && typeof newVal === 'string') {
    							newVal = newVal.split(";");
    							isStringValue = true;
    						}
  • ¶

    When given something else, try to make it an array and deal with it

    						else if (newVal) {
    							newVal = can.makeArray(newVal);
    						} else {
    							newVal = [];
    						}
  • ¶

    Make an object containing all the options passed in for convenient lookup

    						var isSelected = {};
    						can.each(newVal, function (val) {
    							isSelected[val] = true;
    						});
  • ¶

    Go through each <option/> element, if it has a value property (its a valid option), then set its selected property if it was in the list of vals that were just set.

    						can.each(el.childNodes, function (option) {
    							if (option.value) {
    								option.selected = !! isSelected[option.value];
    							}
    						});
    					} else {
    						if(!bindingData.legacyBindings && hasChildren && ("selectedIndex" in el) && prop === "value" ) {
    							can.attr.setSelectValue(el, newVal);
    						} else {
    							can.attr.setAttrOrProp(el, prop, newVal == null ? "" : newVal);
    						}
    					}
    					
    					return newVal;
    	
    				},
  • ¶

    Returns the value of the element property or attribute.

    				get = function(){
    					if(isMultiselectValue) {
    	
    						var values = [],
    							children = el.childNodes;
    	
    						can.each(children, function (child) {
    							if (child.selected && child.value) {
    								values.push(child.value);
    							}
    						});
    
    						return isStringValue ? values.join(";"): values;
    					} else if(hasChildren && ("selectedIndex" in el) && el.selectedIndex === -1) {
    						return undefined;
    					}
    
    					return can.attr.get(el, prop);
    				};
  • ¶

    If the element has children like <select>, those elements are hydrated (by can.view.target) after the select and only then get their values set. This make sure that when the value is set, it will happen after the children are setup.

    			if(hasChildren) {
  • ¶

    have to set later … probably only with mustache.

    				setTimeout(function(){
    					scheduledAsyncSet = true;
    				},1);
  • ¶

    The following would allow a select’s value to be undefined. el.selectedIndex = -1;

    			}
  • ¶

    If the element is an input element in a form

    			if(el.tagName && el.tagName.toLowerCase() === "input" && el.form){
    				parentEvents = [{
    					el: el.form,
    					eventName: "reset",
    					handler: function(){
    						set(originalValue);
    					}
    				}];
    			}
    
    			var observer;
    
    			originalValue = get();
    			
    			return can.compute(originalValue,{
    				on: function(updater){
    					can.each(event, function(eventName){
    						can.bind.call(el, eventName, updater);
    					});
    					can.each(parentEvents, function(parentEvent){
    						can.bind.call(parentEvent.el, parentEvent.eventName, parentEvent.handler);
    					});
    					if(hasChildren) {
    						var onMutation = function (mutations) {
    							
    							if(stickyCompute) {
    								set(stickyCompute());
    							}
    							
    							updater();
    						};
    						if(can.attr.MutationObserver) {
    							observer = new can.attr.MutationObserver(onMutation);
    							observer.observe(el, {
    								childList: true,
    								subtree: true
    							});
    						} else {
  • ¶

    TODO: Remove in 3.0. Can’t store a function b/c Zepto doesn’t support it.

    							can.data(can.$(el), "canBindingCallback", {onMutation: onMutation});
    						}
    					}
    					
    				},
    				off: function(updater){
    					can.each(event, function(eventName){
    						can.unbind.call(el,eventName, updater);
    					});
    					can.each(parentEvents, function(parentEvent){
    						can.unbind.call(parentEvent.el, parentEvent.eventName, parentEvent.handler);
    					});
    					if(hasChildren) {
    						if(can.attr.MutationObserver) {
    							observer.disconnect();
    						} else {
    							can.data(can.$(el), "canBindingCallback",null);
    						}
    					}
    				},
    				get: get,
    				set: set
    			});
    		}
    	};
  • ¶

    bind

    An object with helpers that perform bindings in a certain direction.
    These use the semaphore to prevent cycles.

    	var bind = {
  • ¶

    bind.childToParent

    Listens to the child and updates the parent when it changes.

    • syncChild - Makes sure the child is equal to the parent after the parent is set.
    		childToParent: function(el, parentCompute, childCompute, bindingsSemaphore, attrName, syncChild){
    			var parentUpdateIsFunction = typeof parentCompute === "function";
  • ¶

    Updates the parent if

    			var updateParent = function(ev, newVal){
    				if (!bindingsSemaphore[attrName]) {
    					if(parentUpdateIsFunction) {
    						parentCompute(newVal);
    						
    						if( syncChild ) {
  • ¶

    If, after setting the parent, it’s value is not the same as the child, update the child with the value of the parent. This is used by can-value.

    							if(parentCompute() !== childCompute()) {
    								bindingsSemaphore[attrName] = (bindingsSemaphore[attrName] || 0 )+1;
    								can.batch.start();
    								childCompute(parentCompute());
    								can.batch.after(function(){
    									--bindingsSemaphore[attrName];
    								});
    								can.batch.stop();
    							}
    						}
    					}
  • ¶

    The parentCompute can sometimes be just an observable if the observable is on a plain JS object. This updates the observable to match whatever the new value is.

    					else if(parentCompute instanceof can.Map) {
    						parentCompute.attr(newVal, true);
    					}
    				}
    			};
    	
    			if(childCompute && childCompute.isComputed) {
    				childCompute.bind("change", updateParent);
    			}
    	
    			return updateParent;
    		},
  • ¶

    parent -> child binding

    		parentToChild: function(el, parentCompute, childUpdate, bindingsSemaphore, attrName){
  • ¶

    setup listening on parent and forwarding to viewModel

    			var updateChild = function(ev, newValue){
  • ¶

    Save the viewModel property name so it is not updated multiple times.

    				bindingsSemaphore[attrName] = (bindingsSemaphore[attrName] || 0 )+1;
    				can.batch.start();
    				childUpdate(newValue);
  • ¶

    only after the batch has finished, reduce the update counter

    				can.batch.after(function(){
    					--bindingsSemaphore[attrName];
    				});
    				can.batch.stop();
    			};
    	
    			if(parentCompute && parentCompute.isComputed) {
    				parentCompute.bind("change", updateChild);
    			}
    	
    			return updateChild;
    		}
    	};
  • ¶

    getBindingInfo

    takes a node object like {name, value} and returns an object with information about that binding. Properties:

    • parent - where is the parentName read from: “scope”, “attribute”, “viewModel”.
    • parentName - what is the parent property that should be read.
    • child - where is the childName read from: “scope”, “attribute”, “viewModel”.
      • childName - what is the child property that should be read.
    • parentToChild - should changes in the parent update the child.
    • childToParent - should changes in the child update the parent.
    • bindingAttributeName - the attribute name that created this binding.
    • initializeValues - should parent and child be initialized to their counterpart. If undefined is return, there is no binding.
    	var getBindingInfo = function(node, attributeViewModelBindings, templateType, tagName){
    		var bindingInfo,
    			attributeName = node.name,
    			attributeValue = node.value || "";
  • ¶

    Does this match the new binding syntax?

    		var matches = attributeName.match(bindingsRegExp);
    		if(!matches) {
    			var ignoreAttribute = ignoreAttributesRegExp.test(attributeName);
    			var vmName = can.camelize(attributeName);
  • ¶

    !steal-remove-start user tried to pass something like id=”{foo}”, so give them a good warning

    			if(ignoreAttribute) {
    				can.dev.warn("can/component: looks like you're trying to pass "+attributeName+" as an attribute into a component, "+
    				"but it is not a supported attribute");
    			}
  • ¶

    !steal-remove-end

  • ¶

    if this is handled by another binding or a attribute like id.

    			if ( ignoreAttribute || viewCallbacks.attr(attributeName) ) {
    				return;
    			}
    			var syntaxRight = attributeValue[0] === "{" && can.last(attributeValue) === "}";
    			var isAttributeToChild = templateType === "legacy" ? attributeViewModelBindings[vmName] : !syntaxRight;
    			var scopeName = syntaxRight ? attributeValue.substr(1, attributeValue.length - 2 ) : attributeValue;
    			if(isAttributeToChild) {
    				return {
    					bindingAttributeName: attributeName,
    					parent: "attribute",
    					parentName: attributeName,
    					child: "viewModel",
    					childName: vmName,
    					parentToChild: true,
    					childToParent: true
    				};
    			} else {
    				return {
    					bindingAttributeName: attributeName,
    					parent: "scope",
    					parentName: scopeName,
    					child: "viewModel",
    					childName: vmName,
    					parentToChild: true,
    					childToParent: true
    				};
    			}
    		}
    		
    		var twoWay = !!matches[1],
    			childToParent = twoWay || !!matches[2],
    			parentToChild = twoWay || !childToParent;
    		
    		var childName = matches[3];
    		var isDOM = childName.charAt(0) === "$";
    		if(isDOM) {
    			bindingInfo = {
    				parent: "scope",
    				child: "attribute",
    				childToParent: childToParent,
    				parentToChild: parentToChild,
    				bindingAttributeName: attributeName,
    				childName: childName.substr(1),
    				parentName: attributeValue,
    				initializeValues: true
    			};
    			if(tagName === "select") {
    				bindingInfo.stickyParentToChild = true;
    			}
    			return bindingInfo;
    		} else {
    			bindingInfo = {
    				parent: "scope",
    				child: "viewModel",
    				childToParent: childToParent,
    				parentToChild: parentToChild,
    				bindingAttributeName: attributeName,
    				childName: can.camelize(childName),
    				parentName: attributeValue,
    				initializeValues: true
    			};
    			if(attributeValue.trim().charAt(0) === "~") {
    				bindingInfo.stickyParentToChild = true;
    			}
    			return bindingInfo;
    		}
    
    	};
  • ¶

    Regular expressions for getBindingInfo

    	var bindingsRegExp = /\{(\()?(\^)?([^\}\)]+)\)?\}/,
    		ignoreAttributesRegExp = /^(data-view-id|class|id|\[[\w\.-]+\]|#[\w\.-])$/i;
  • ¶

    makeDataBinding

    Makes a data binding for an attribute node. Returns an object with information about the binding, including an onTeardown method that undoes the binding.
    If the data binding involves a viewModel, an onCompleteBinding method is returned on the object. This method must be called after the element has a viewModel with the viewModel to complete the binding.

    • node - an attribute node or an object with a name and value property.
    • el - the element this binding belongs on.
    • bindingData - an object with:
      • templateType - the type of template. Ex: “legacy” for mustache.
      • scope - the can.view.Scope,
      • semaphore - an object that keeps track of changes in different properties to prevent cycles,
      • getViewModel - a function that returns the viewModel when called. This function can be passed around (not called) even if the viewModel doesn’t exist yet.
      • attributeViewModelBindings - properties already specified as being a viewModel<->attribute (as opposed to viewModel<->scope) binding.

    Returns:

    • undefined - If this isn’t a data binding.
    • object - An object with information about the binding.
    	var makeDataBinding = function(node, el, bindingData){
  • ¶

    Get information about the binding.

    		var bindingInfo = getBindingInfo(node, bindingData.attributeViewModelBindings, bindingData.templateType, el.nodeName.toLowerCase());
    		if(!bindingInfo) {
    			return;
    		}
  • ¶

    assign some bindingData props to the bindingInfo

    		bindingInfo.alreadyUpdatedChild = bindingData.alreadyUpdatedChild;
    		if( bindingData.initializeValues) {
    			bindingInfo.initializeValues = true;
    		}
  • ¶

    Get computes for the parent and child binding

    		var parentCompute = getComputeFrom[bindingInfo.parent](el, bindingData.scope, bindingInfo.parentName, bindingData, bindingInfo.parentToChild),
    			childCompute = getComputeFrom[bindingInfo.child](el, bindingData.scope, bindingInfo.childName, bindingData, bindingInfo.childToParent, bindingInfo.stickyParentToChild && parentCompute),
  • ¶

    these are the functions bound to one compute that update the other.

    			updateParent,
    			updateChild,
    			childLifecycle;
    		
    		if(bindingData.nodeList) {
    			if(parentCompute && parentCompute.isComputed){
    				parentCompute.computeInstance.setPrimaryDepth(bindingData.nodeList.nesting+1);
    			}
    			if(childCompute && childCompute.isComputed){
    				childCompute.computeInstance.setPrimaryDepth(bindingData.nodeList.nesting+1);
    			}
    		}
  • ¶

    Only bind to the parent if it will update the child.

    		if(bindingInfo.parentToChild){
    			updateChild = bind.parentToChild(el, parentCompute, childCompute, bindingData.semaphore, bindingInfo.bindingAttributeName);
    		}
  • ¶

    This completes the binding. We can’t call it right away because the viewModel might not have been created yet.

    		var completeBinding = function(){
    			if(bindingInfo.childToParent){
  • ¶

    setup listening on parent and forwarding to viewModel

    				updateParent = bind.childToParent(el, parentCompute, childCompute, bindingData.semaphore, bindingInfo.bindingAttributeName,
    					bindingData.syncChildWithParent);
    			}
  • ¶

    the child needs to be bound even if

    			else if(bindingInfo.stickyParentToChild) {
    				childCompute.bind("change", childLifecycle = can.k);
    			}
    			
    			if(bindingInfo.initializeValues) {
    				initializeValues(bindingInfo, childCompute, parentCompute, updateChild, updateParent);
    			}
    			
    			
    		};
  • ¶

    This tears down the binding.

    		var onTeardown = function() {
    			unbindUpdate(parentCompute, updateChild);
    			unbindUpdate(childCompute, updateParent);
    			unbindUpdate(childCompute, childLifecycle);
    		};
  • ¶

    If this binding depends on the viewModel, which might not have been created, return the function to complete the binding as onCompleteBinding.

    		if(bindingInfo.child === "viewModel") {
    			return {
    				value: getValue(parentCompute),
    				onCompleteBinding: completeBinding,
    				bindingInfo: bindingInfo,
    				onTeardown: onTeardown
    			};
    		} else {
    			completeBinding();
    			return {
    				bindingInfo: bindingInfo,
    				onTeardown: onTeardown
    			};
    			
    		}
    	};
  • ¶

    initializeValues

    Updates the parent or child value depending on the direction of the binding or if the child or parent is undefined.

    	var initializeValues = function(bindingInfo, childCompute, parentCompute, updateChild, updateParent){
    		var doUpdateParent = false;
    		if(bindingInfo.parentToChild && !bindingInfo.childToParent) {
    			if(bindingInfo.stickyParentToChild) {
  • ¶

    call updateChild here to set up the compute

    				updateChild({}, getValue(parentCompute));
    			}
    
    		}
    		else if(!bindingInfo.parentToChild && bindingInfo.childToParent) {
    			doUpdateParent = true;
    		}
  • ¶

    Two way Update child or parent depending on who has a value. If both have a value, update the child.

    		else if( getValue(childCompute) === undefined) {
  • ¶

    updateChild

    		} else if(getValue(parentCompute) === undefined) {
    			doUpdateParent = true;
    		}
    		
    		if(doUpdateParent) {
    			updateParent({}, getValue(childCompute) );
    		} else {
    			if(!bindingInfo.alreadyUpdatedChild) {
    				updateChild({}, getValue(parentCompute) );
    			}
    		}
    	};
  • ¶

    For “sticky” select values, we need to know when <option>s are added or removed to a <select>. If we don’t have MutationObserver, we need to setup can.view.live to callback when this happens.

    	if( !can.attr.MutationObserver ) {
    		var updateSelectValue = function(el){
    			var bindingCallback = can.data(can.$(el),"canBindingCallback");
    			if(bindingCallback) {
    				bindingCallback.onMutation(el);
    			}
    		};
    		live.registerChildMutationCallback("select",updateSelectValue);
    		live.registerChildMutationCallback("optgroup",function(el){
    			updateSelectValue(el.parentNode);
    		});
    	}
  • ¶

    isContentEditable

    Determines if an element is contenteditable. An element is contenteditable if it contains the contenteditable attribute set to either an empty string or “true”. By default an element is also contenteditable if its immediate parent has a truthy version of the attribute, unless the element is explicitly set to “false”.

    	var isContentEditable = (function(){
  • ¶

    A contenteditable element has a value of an empty string or “true”

    		var values = {
    			"": true,
    			"true": true,
    			"false": false
    		};
  • ¶

    Tests if an element has the appropriate contenteditable attribute

    		var editable = function(el){
  • ¶

    DocumentFragments do not have a getAttribute

    			if(!el || !el.getAttribute) {
    				return;
    			}
    
    			var attr = el.getAttribute("contenteditable");
    			return values[attr];
    		};
    
    		return function (el){
  • ¶

    First check if the element is explicitly true or false

    			var val = editable(el);
    			if(typeof val === "boolean") {
    				return val;
    			} else {
  • ¶

    Otherwise, check the parent

    				return !!editable(el.parentNode);
    			}
    		};
    	})(),
    		removeBrackets = function(value, open, close){
    			open = open || "{";
    			close = close || "}";
    
    			if(value[0] === open && value[value.length-1] === close) {
    				return value.substr(1, value.length - 2);
    			}
    			return value;
    		},
    		getValue = function(value){
    			return value && value.isComputed ? value() : value;
    		},
    		unbindUpdate = function(compute, updateOther){
    			if(compute && compute.isComputed && typeof updateOther === "function") {
    				compute.unbind("change", updateOther);
    			}
    		},
    		cleanVMName = function(name){
    			return name.replace(/@/g,"");
    		};
  • ¶

    Special Event Types (can-SPECIAL)

    A special object, similar to $.event.special, for adding hooks for special can-SPECIAL types (not native DOM events). Right now, only can-enter is supported, but this object might be exported so that it can be added to easily.

    To implement a can-SPECIAL event type, add a property to the special object, whose value is a function that returns the following:

    // the real event name to bind to
    event: "event-name",
    handler: function (ev) {
        // some logic that figures out if the original handler should be called or not, and if so...
        return original.call(this, ev);
    }
    
    	var special = {
    		enter: function (data, el, original) {
    			return {
    				event: "keyup",
    				handler: function (ev) {
    					if (ev.keyCode === 13) {
    						return original.call(this, ev);
    					}
    				}
    			};
    		}
    	};
    
    
    	can.bindings = {
    		behaviors: behaviors,
    		getBindingInfo: getBindingInfo,
    		special: special
    	};
    	return can.bindings;
    });