refactor(select): reduce duplication and reorder functions

This commit is contained in:
Tobias Bosch
2014-10-07 16:42:39 -07:00
committed by Jeff Cross
parent ab354cf04e
commit aad60953ce
2 changed files with 137 additions and 138 deletions

View File

@@ -77,6 +77,24 @@ var ngOptionsMinErr = minErr('ngOptions');
* used to identify the objects in the array. The `trackexpr` will most likely refer to the
* `value` variable (e.g. `value.propertyName`). With this the selection is preserved
* even when the options are recreated (e.g. reloaded from the server).
* <div class="alert alert-info">
* **Note:** Using `selectAs` together with `trackexpr` is not possible (and will throw).
* TODO: Add some nice reasoning here, add a minErr and a nice error page.
* reasoning:
* - Example: <select ng-options="item.subItem as item.label for item in values track by item.id" ng-model="selected">
* values: [{id: 1, label: 'aLabel', subItem: {name: 'aSubItem'}}, {id: 2, label: 'bLabel', subItem: {name: 'bSubItemß'}}],
* $scope.selected = {name: 'aSubItem'};
* - trackBy is always applied to `value`, with purpose to preserve the selection,
* (to `item` in this case)
* - to calculate whether an item is selected we do the following:
* 1. apply `trackBy` to the values in the array, e.g.
* In the example: [1,2]
* 2. apply `trackBy` to the already selected value in `ngModel`:
* In the example: this is not possible, as `trackBy` refers to `item.id`, but the selected
* value from `ngModel` is `{name: aSubItem}`.
*
* </div>
*
* @example
<example module="selectExample">
@@ -345,7 +363,9 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
// We try to reuse these if possible
// - optionGroupsCache[0] is the options with no option group
// - optionGroupsCache[?][0] is the parent: either the SELECT or OPTGROUP element
optionGroupsCache = [[{element: selectElement, label:''}]];
optionGroupsCache = [[{element: selectElement, label:''}]],
//re-usable object to represent option's locals
locals = {};
if (nullOption) {
// compile the element since there might be bindings in it
@@ -363,146 +383,109 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
// clear contents, we'll add what's needed based on the model
selectElement.empty();
selectElement.on('change', function() {
scope.$apply(function() {
var optionGroup,
collection = valuesFn(scope) || [],
locals = {},
key, value, optionElement, index, groupIndex, length, groupLength, trackIndex;
if (multiple) {
value = [];
for (groupIndex = 0, groupLength = optionGroupsCache.length;
groupIndex < groupLength;
groupIndex++) {
// list of options for that group. (first item has the parent)
optionGroup = optionGroupsCache[groupIndex];
for(index = 1, length = optionGroup.length; index < length; index++) {
if ((optionElement = optionGroup[index].element)[0].selected) {
value.push(getViewValue(optionElement, locals, collection));
}
}
}
} else {
value = getViewValue(selectElement, locals, collection);
}
ctrl.$setViewValue(value);
render();
});
});
selectElement.on('change', selectionChanged);
ctrl.$render = render;
scope.$watchCollection(valuesFn, scheduleRendering);
scope.$watchCollection(function () {
var locals = {},
values = valuesFn(scope);
if (values) {
var toDisplay = new Array(values.length);
for (var i = 0, ii = values.length; i < ii; i++) {
locals[valueName] = values[i];
toDisplay[i] = displayFn(scope, locals);
}
return toDisplay;
}
}, scheduleRendering);
scope.$watchCollection(getLabels, scheduleRendering);
if (multiple) {
scope.$watchCollection(function() { return ctrl.$modelValue; }, scheduleRendering);
}
// ------------------------------------------------------------------ //
function getSelectedSet() {
var selectedSet = false;
function callExpression(exprFn, key, value) {
locals[valueName] = value;
if (keyName) locals[keyName] = key;
return exprFn(scope, locals);
}
function selectionChanged() {
scope.$apply(function() {
var optionGroup,
collection = valuesFn(scope) || [],
key, value, optionElement, index, groupIndex, length, groupLength, trackIndex;
var viewValue;
if (multiple) {
viewValue = [];
forEach(selectElement.val(), function(selectedKey) {
viewValue.push(getViewValue(selectedKey, collection[selectedKey]));
});
} else {
var selectedKey = selectElement.val();
viewValue = getViewValue(selectedKey, collection[selectedKey]);
}
ctrl.$setViewValue(viewValue);
render();
});
}
function getViewValue(key, value) {
if (key === '?') {
return undefined;
} else if (key === '') {
return null;
} else {
var viewValueFn = selectAsFn ? selectAsFn : valueFn;
return callExpression(viewValueFn, key, value);
}
}
function getLabels() {
var values = valuesFn(scope);
var toDisplay;
if (values && isArray(values)) {
toDisplay = new Array(values.length);
for (var i = 0, ii = values.length; i < ii; i++) {
toDisplay[i] = callExpression(displayFn, i, values[i]);
}
return toDisplay;
} else if (values) {
// TODO: Add a test for this case
toDisplay = {};
for (var prop in values) {
if (values.hasOwnProperty(prop)) {
toDisplay[prop] = callExpression(displayFn, prop, values[prop]);
}
}
}
return toDisplay;
}
function createIsSelectedFn(viewValue) {
var selectedSet;
if (multiple) {
var viewValue = ctrl.$viewValue;
if (trackFn && isArray(viewValue) && !selectAs) {
if (!selectAs && trackFn && isArray(viewValue)) {
selectedSet = new HashMap([]);
var locals = {};
for (var trackIndex = 0; trackIndex < viewValue.length; trackIndex++) {
locals[valueName] = viewValue[trackIndex];
selectedSet.put(trackFn(scope, locals), viewValue[trackIndex]);
// tracking by key
selectedSet.put(callExpression(trackFn, null, viewValue[trackIndex]), true);
}
} else {
selectedSet = new HashMap(viewValue);
}
} else if (!selectAsFn && trackFn) {
viewValue = callExpression(trackFn, null, viewValue);
}
return selectedSet;
}
return function isSelected(key, value) {
var compareValueFn;
if (selectAsFn) {
compareValueFn = selectAsFn;
} else if (trackFn) {
compareValueFn = trackFn;
} else {
compareValueFn = valueFn;
}
function getViewValue (el, locals, collection) {
var key = el.val();
var value;
var calculateViewValue;
if (selectAsFn || trackFn) {
calculateViewValue = function () {
var getterFn = selectAsFn || trackFn;
var wrappedCollectionValue = {};
wrappedCollectionValue[valueName] = collection[key];
wrappedCollectionValue[keyName] = key;
for (var i in collection) {
if (collection.hasOwnProperty(i)) {
locals[valueName] = collection[i];
if (keyName) locals[keyName] = i;
if (getterFn(scope, locals) ==
getterFn(scope, wrappedCollectionValue)) {
/*
* trackBy should not be used for final calculation, because it doesn't
* necessarily return the expected value.
*/
return (selectAsFn||valueFn)(scope, locals);
}
}
}
};
} else {
calculateViewValue = function() {
locals[valueName] = collection[key];
if (keyName) locals[keyName] = key;
return valueFn(scope, locals);
};
if (multiple) {
return isDefined(selectedSet.remove(callExpression(compareValueFn, key, value)));
} else {
return viewValue == callExpression(compareValueFn, key, value);
}
}
if (multiple) {
if (keyName) locals[keyName] = key;
calculateViewValue();
return (selectAsFn || valueFn)(scope, locals);
}
if (key == '?') {
value = undefined;
} else if (key === ''){
value = null;
} else {
value = calculateViewValue();
}
return value;
}
function isSelected(viewValue, locals, selectedSet) {
var compareValueFn;
if (selectAsFn) {
compareValueFn = selectAsFn;
} else if (trackFn) {
var withValueName = {};
withValueName[valueName] = viewValue;
compareValueFn = trackFn;
viewValue = trackFn(withValueName);
} else {
compareValueFn = valueFn;
}
var optionValue = compareValueFn(scope, locals);
if (multiple) {
return isDefined(selectedSet.remove(optionValue));
}
return viewValue === optionValue;
}
function scheduleRendering() {
@@ -512,11 +495,10 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
}
}
function render() {
renderScheduled = false;
// Temporary location for the option groups before we render them
// Temporary location for the option groups before we render them
var optionGroups = {'':[]},
optionGroupNames = [''],
optionGroupName,
@@ -527,11 +509,12 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
values = valuesFn(scope) || [],
keys = keyName ? sortedKeys(values) : values,
key,
value,
groupLength, length,
groupIndex, index,
locals = {},
selected,
selectedSet = getSelectedSet(),
isSelected = createIsSelectedFn(viewValue),
anySelected = false,
lastElement,
element,
label;
@@ -542,24 +525,19 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
if (keyName) {
key = keys[index];
if ( key.charAt(0) === '$' ) continue;
locals[keyName] = key;
}
value = values[key];
locals[valueName] = values[key];
optionGroupName = groupByFn(scope, locals) || '';
optionGroupName = callExpression(groupByFn, key, value) || '';
if (!(optionGroup = optionGroups[optionGroupName])) {
optionGroup = optionGroups[optionGroupName] = [];
optionGroupNames.push(optionGroupName);
}
selected = isSelected(
viewValue,
locals,
selectedSet);
selectedSet = selectedSet || selected;
selected = isSelected(key, value);
anySelected = anySelected || selected;
label = displayFn(scope, locals); // what will be seen by the user
label = callExpression(displayFn, key, value); // what will be seen by the user
// doing displayFn(scope, locals) || '' overwrites zero values
label = isDefined(label) ? label : '';
@@ -573,8 +551,8 @@ var selectDirective = ['$compile', '$parse', function($compile, $parse) {
if (!multiple) {
if (nullOption || viewValue === null) {
// insert null option if we have a placeholder, or the model is null
optionGroups[''].unshift({id:'', label:'', selected:!selectedSet});
} else if (!selectedSet) {
optionGroups[''].unshift({id:'', label:'', selected:!anySelected});
} else if (!anySelected) {
// option could not be found, we have to insert the undefined item
optionGroups[''].unshift({id:'?', label:'', selected:true});
}