diff --git a/src/uiSelectController.js b/src/uiSelectController.js index c0a1a94ac..c14e4bea3 100644 --- a/src/uiSelectController.js +++ b/src/uiSelectController.js @@ -5,8 +5,8 @@ * put as much logic in the controller (instead of the link functions) as possible so it can be easily tested. */ uis.controller('uiSelectCtrl', - ['$scope', '$element', '$timeout', '$filter', 'uisRepeatParser', 'uiSelectMinErr', 'uiSelectConfig', - function($scope, $element, $timeout, $filter, RepeatParser, uiSelectMinErr, uiSelectConfig) { + ['$scope', '$element', '$timeout', '$filter', '$q', 'uisRepeatParser', 'uiSelectMinErr', 'uiSelectConfig', + function($scope, $element, $timeout, $filter, $q, RepeatParser, uiSelectMinErr, uiSelectConfig) { var ctrl = this; @@ -284,24 +284,43 @@ uis.controller('uiSelectCtrl', } } - $scope.$broadcast('uis:select', item); + var completeSelection = function() { + $scope.$broadcast('uis:select', item); + + $timeout(function(){ + ctrl.onSelectCallback($scope, callbackContext); + }); + + if (ctrl.closeOnSelect) { + ctrl.close(skipFocusser); + } + if ($event && $event.type === 'click') { + ctrl.clickTriggeredSelect = true; + } + }; var locals = {}; locals[ctrl.parserResult.itemName] = item; - $timeout(function(){ - ctrl.onSelectCallback($scope, { - $item: item, - $model: ctrl.parserResult.modelMapper($scope, locals) - }); - }); + var callbackContext = { + $item: item, + $model: ctrl.parserResult.modelMapper($scope, locals) + }; - if (ctrl.closeOnSelect) { - ctrl.close(skipFocusser); - } - if ($event && $event.type === 'click') { - ctrl.clickTriggeredSelect = true; + var onBeforeSelectResult = ctrl.onBeforeSelectCallback($scope, callbackContext); + + if (angular.isDefined(onBeforeSelectResult)) { + if (!onBeforeSelectResult) { + return; // abort the selection in case of deliberate falsey result + } else if (angular.isFunction(onBeforeSelectResult.then)) { + onBeforeSelectResult.then(completeSelection); + } else { + completeSelection(); + } + } else { + completeSelection(); } + } } }; diff --git a/src/uiSelectDirective.js b/src/uiSelectDirective.js index fc6cc4138..4af6acb7f 100644 --- a/src/uiSelectDirective.js +++ b/src/uiSelectDirective.js @@ -41,6 +41,7 @@ uis.directive('uiSelect', } }(); + $select.onBeforeSelectCallback = $parse(attrs.onBeforeSelect); $select.onSelectCallback = $parse(attrs.onSelect); $select.onRemoveCallback = $parse(attrs.onRemove); diff --git a/test/select.spec.js b/test/select.spec.js index 2ba86fb2a..0ef4d4394 100644 --- a/test/select.spec.js +++ b/test/select.spec.js @@ -959,6 +959,196 @@ describe('ui-select tests', function() { }); + it('should invoke before-select callback before select callback synchronously', function () { + + var order = []; + scope.onBeforeSelectFn = function ($item, $model, $label) { + order.push('onBeforeSelectFn'); + }; + scope.onSelectFn = function ($item, $model, $label) { + order.push('onSelectFn'); + }; + var el = compileTemplate( + ' \ + {{$select.selected.name}} \ + \ +
\ +
\ +
\ +
' + ); + + clickItem(el, 'Samantha'); + $timeout.flush(); + + expect(order[0]).toEqual('onBeforeSelectFn'); + expect(order[1]).toEqual('onSelectFn'); + + }); + + it('should invoke before-select callback before select callback when promised', inject(function ($q) { + + var order = []; + scope.onBeforeSelectFn = function ($item, $model, $label) { + var deferred = $q.defer(); + $timeout(function () { + order.push('onBeforeSelectFn'); + deferred.resolve(order); + }); + return deferred.promise; + }; + scope.onSelectFn = function ($item, $model, $label) { + order.push('onSelectFn'); + }; + var el = compileTemplate( + ' \ + {{$select.selected.name}} \ + \ +
\ +
\ +
\ +
' + ); + + clickItem(el, 'Samantha'); + $timeout.flush(); + + expect(order[0]).toEqual('onBeforeSelectFn'); + expect(order[1]).toEqual('onSelectFn'); + + })); + + it('should complete on-select if before-select callback promise is resolved', inject(function ($q) { + + scope.onBeforeSelectFn = function ($item, $model, $label) { + var deferred = $q.defer(); + $timeout(function () { + deferred.resolve(); + }); + return deferred.promise; + }; + scope.onSelectFn = function ($item, $model, $label) { + scope.$item = $item; + scope.$model = $model; + }; + var el = compileTemplate( + ' \ + {{$select.selected.name}} \ + \ +
\ +
\ +
\ +
' + ); + + expect(scope.$item).toBeFalsy(); + expect(scope.$model).toBeFalsy(); + + clickItem(el, 'Samantha'); + $timeout.flush(); + + expect(scope.selection.selected).toBe('Samantha'); + + expect(scope.$item).toEqual(scope.people[5]); + expect(scope.$model).toEqual('Samantha'); + + })); + + it('should abort selection if before-select callback returns falsy', function () { + + scope.onBeforeSelectFn = function ($item, $model, $label) { + return false; + }; + scope.onSelectFn = function ($item, $model, $label) { + scope.$item = $item; + scope.$model = $model; + }; + var el = compileTemplate( + ' \ + {{$select.selected.name}} \ + \ +
\ +
\ +
\ +
' + ); + + expect(scope.$item).toBeFalsy(); + expect(scope.$model).toBeFalsy(); + + clickItem(el, 'Samantha'); + $timeout.flush(); + + expect(scope.$item).toBeFalsy(); + expect(scope.$model).toBeFalsy(); + + }); + + it('should abort selection if before-select callback rejects promise', inject(function ($q) { + + scope.onBeforeSelectFn = function ($item, $model, $label) { + var deferred = $q.defer(); + $timeout(function () { + deferred.reject(); + }); + return deferred.promise; + }; + scope.onSelectFn = function ($item, $model, $label) { + scope.$item = $item; + scope.$model = $model; + }; + var el = compileTemplate( + ' \ + {{$select.selected.name}} \ + \ +
\ +
\ +
\ +
' + ); + + expect(scope.$item).toBeFalsy(); + expect(scope.$model).toBeFalsy(); + + clickItem(el, 'Samantha'); + $timeout.flush(); + + expect(scope.$item).toBeFalsy(); + expect(scope.$model).toBeFalsy(); + + })); + + it('should keep reference to current selection and incoming selection within before-select callback', function () { + + var currentSelection, incomingSelection; + scope.onBeforeSelectFn = function ($item, $model, $label) { + incomingSelection = $item; + currentSelection = scope.selection.selected; + }; + var el = compileTemplate( + ' \ + {{$select.selected.name}} \ + \ +
\ +
\ +
\ +
' + ); + + clickItem(el, 'Samantha'); + $timeout.flush(); + + expect(currentSelection).toBeFalsy(); + expect(incomingSelection.name).toBe('Samantha'); + + clickItem(el, 'Adam'); + $timeout.flush(); + + expect(currentSelection).toBe('Samantha'); + expect(incomingSelection.name).toBe('Adam'); + + }); + it('should invoke hover callback', function(){ var highlighted;