Skip to content

Commit

Permalink
Merge pull request #2149 from canjs/2147-value-bound-to-emptystring
Browse files Browse the repository at this point in the history
changes the behavior of ""  to makes things consistent
  • Loading branch information
daffl committed Jan 4, 2016
2 parents 383a779 + 857a2bd commit e9bbfae
Show file tree
Hide file tree
Showing 4 changed files with 270 additions and 43 deletions.
14 changes: 14 additions & 0 deletions util/attr/attr.js
Expand Up @@ -102,6 +102,20 @@ steal("can/util/can.js", function (can) {
this.set(el, attrName, val);
}
},
setSelectValue: function(el, val) {
// jshint eqeqeq: false
if(val != null) {
var options = el.getElementsByTagName('option');
for(var i = 0; i < options.length; i++) {
if(val == options[i].value) {
options[i].selected = true;
return;
}
}
}

el.selectedIndex = -1;
},
// ## attr.set
// Set the value an attribute on an element.
set: function (el, attrName, val) {
Expand Down
98 changes: 84 additions & 14 deletions view/bindings/bindings.js
Expand Up @@ -10,7 +10,12 @@
// - 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/control", "can/view/scope", "can/view/href", function (can, expression, viewCallbacks) {
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 = {
Expand Down Expand Up @@ -433,7 +438,7 @@ steal("can/util", "can/view/stache/expression.js", "can/view/callbacks", "can/co
var getComputeFrom = {
// ### getComputeFrom.scope
// Returns a compute from the scope. This handles expressions like `someMethod(.,1)`.
scope: function(el, scope, scopeProp, bindingData, mustBeACompute){
scope: function(el, scope, scopeProp, bindingData, mustBeACompute, stickyCompute){
if(!scopeProp) {
return can.compute();
} else {
Expand All @@ -452,7 +457,7 @@ steal("can/util", "can/view/stache/expression.js", "can/view/callbacks", "can/co
// ### getComputeFrom.viewModel
// Returns a compute that's two-way bound to the `viewModel` returned by
// `options.getViewModel()`.
viewModel: function(el, scope, vmName, bindingData, mustBeACompute) {
viewModel: function(el, scope, vmName, bindingData, mustBeACompute, stickyCompute) {
var setName = cleanVMName(vmName);
if(mustBeACompute) {
return can.compute(function(newVal){
Expand All @@ -473,7 +478,7 @@ steal("can/util", "can/view/stache/expression.js", "can/view/callbacks", "can/co
},
// ### 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, event){
attribute: function(el, scope, prop, bindingData, mustBeACompute, stickyCompute, event){
// Determine the event or events we need to listen to
// when this value changes.
if(!event) {
Expand Down Expand Up @@ -533,10 +538,12 @@ steal("can/util", "can/view/stache/expression.js", "can/view/callbacks", "can/co
}
});
} else {
if(!bindingData.legacyBindings && hasChildren && ("selectedIndex" in el)) {
el.selectedIndex = -1;
if(!bindingData.legacyBindings && hasChildren && ("selectedIndex" in el) && prop === "value" ) {
can.attr.setSelectValue(el, newVal);
} else {
can.attr.setAttrOrProp(el, prop, newVal == null ? "" : newVal);
}
can.attr.setAttrOrProp(el, prop, newVal == null ? "" : newVal);

}
return newVal;

Expand All @@ -555,6 +562,8 @@ steal("can/util", "can/view/stache/expression.js", "can/view/callbacks", "can/co
});

return isStringValue ? values.join(";"): values;
} else if(hasChildren && ("selectedIndex" in el) && el.selectedIndex === -1) {
return undefined;
}

return can.attr.get(el, prop);
Expand All @@ -569,18 +578,51 @@ steal("can/util", "can/view/stache/expression.js", "can/view/callbacks", "can/co
setTimeout(function(){
scheduledAsyncSet = true;
},1);
// The following would allow a select's value
// to be undefined.
// el.selectedIndex = -1;
}

var observer;

return can.compute(get(),{
on: function(updater){
can.each(event, function(eventName){
can.bind.call(el,eventName, updater);
});
if(hasChildren) {
var onMutation = function (mutations) {
if(stickyCompute) {
set(stickyCompute());
} else {
if(scheduledAsyncSet) {
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);
});
if(hasChildren) {
if(can.attr.MutationObserver) {
observer.disconnect();
} else {
can.data(can.$(el), "canBindingCallback",null);
}
}
},
get: get,
set: set
Expand Down Expand Up @@ -668,7 +710,7 @@ steal("can/util", "can/view/stache/expression.js", "can/view/callbacks", "can/co
// - `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){
var getBindingInfo = function(node, attributeViewModelBindings, templateType, tagName){
var attributeName = node.name,
attributeValue = node.value || "";

Expand Down Expand Up @@ -723,8 +765,7 @@ steal("can/util", "can/view/stache/expression.js", "can/view/callbacks", "can/co
var childName = matches[3];
var isDOM = childName.charAt(0) === "$";
if(isDOM) {

return {
var bindingInfo = {
parent: "scope",
child: "attribute",
childToParent: childToParent,
Expand All @@ -734,6 +775,10 @@ steal("can/util", "can/view/stache/expression.js", "can/view/callbacks", "can/co
parentName: attributeValue,
initializeValues: true
};
if(tagName === "select" && !childToParent) {
bindingInfo.stickyParentToChild = true;
}
return bindingInfo;
} else {
return {
parent: "scope",
Expand Down Expand Up @@ -776,7 +821,7 @@ steal("can/util", "can/view/stache/expression.js", "can/view/callbacks", "can/co
var makeDataBinding = function(node, el, bindingData){

// Get information about the binding.
var bindingInfo = getBindingInfo(node, bindingData.attributeViewModelBindings, bindingData.templateType);
var bindingInfo = getBindingInfo(node, bindingData.attributeViewModelBindings, bindingData.templateType, el.nodeName.toLowerCase());
if(!bindingInfo) {
return;
}
Expand All @@ -788,11 +833,12 @@ steal("can/util", "can/view/stache/expression.js", "can/view/callbacks", "can/co

// 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),
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;
updateChild,
childLifecycle;

// Only bind to the parent if it will update the child.
if(bindingInfo.parentToChild){
Expand All @@ -807,6 +853,11 @@ steal("can/util", "can/view/stache/expression.js", "can/view/callbacks", "can/co
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);
}
Expand All @@ -817,6 +868,7 @@ steal("can/util", "can/view/stache/expression.js", "can/view/callbacks", "can/co
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`.
Expand Down Expand Up @@ -866,6 +918,24 @@ steal("can/util", "can/view/stache/expression.js", "can/view/callbacks", "can/co
}
};

// 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`
Expand Down

0 comments on commit e9bbfae

Please sign in to comment.