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) {
This module provides CanJS’s default data and event bindings. It’s broken up into several parts:
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) {
var behaviors = {
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]();
}
};
},
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;
}
}
}
});
},
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);
},
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);
},
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();
});
}
};
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);
An object of helper functions that make a getter/setter compute on different types of objects.
var getComputeFrom = {
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);
};
}
}
},
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);
}
};
}
},
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 value
s 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
});
}
};
An object with helpers that perform bindings in a certain direction.
These use the semaphore to prevent cycles.
var bind = {
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;
}
};
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;
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
};
}
};
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);
});
}
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,"");
};
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;
});