Permalink
Browse files

feat(select): refactor select directive (cf. #85)

  • Loading branch information...
1 parent b4f5efe commit 205cd71a7811e318335d27092bd771f171a21073 @mgcrea committed Apr 11, 2013
Showing with 328 additions and 287 deletions.
  1. +22 −22 src/directives/select.js
  2. +80 −98 test/unit/directives/selectSpec.js
  3. +16 −12 vendor/bootstrap-select.css
  4. +210 −155 vendor/bootstrap-select.js
View
@@ -1,38 +1,38 @@
angular.module('$strap.directives')
-.directive('bsSelect', function() {
+.directive('bsSelect', ['$timeout', function($timeout) {
'use strict';
+ var NG_OPTIONS_REGEXP = /^\s*(.*?)(?:\s+as\s+(.*?))?(?:\s+group\s+by\s+(.*))?\s+for\s+(?:([\$\w][\$\w\d]*)|(?:\(\s*([\$\w][\$\w\d]*)\s*,\s*([\$\w][\$\w\d]*)\s*\)))\s+in\s+(.*)$/;
+
return {
+ restrict: 'A',
+ require: '?ngModel',
+ link: function postLink(scope, element, attrs, controller) {
- scope: true,
+ var options = scope.$eval(attrs.bsSelect) || {};
+ // console.warn('postLink', options, arguments);
- link: function (scope, element, attrs) {
+ $timeout(function() {
+ element.selectpicker(options);
+ element.next().removeClass('ng-scope');
+ });
- var previousClasses = element.attr('class');
+ // If we have a controller (i.e. ngModelController) then wire it up
+ if(controller) {
- var syncClasses = function () {
- var currentClasses = element.attr('class');
- element.next().removeClass(previousClasses).addClass(currentClasses).removeClass('ng-scope');
- previousClasses = element.attr('class');
- };
+ // Watch for changes to the model value
+ scope.$watch(attrs.ngModel, function(newValue, oldValue) {
+ if (newValue !== oldValue) {
+ element.selectpicker('refresh');
+ }
+ });
- // Watch for changes to the length of our select element
- scope.$watch(function () {
- return element[0].length;
- }, function () {
- element.selectpicker('render');
- });
-
- // Watch for changes to the model value
- scope.$watch(attrs.ngModel, function () {
- element.selectpicker('render');
- syncClasses();
- });
+ }
}
};
-});
+}]);
@@ -1,120 +1,102 @@
'use strict';
+// global describe, it
-describe('select directive', function () {
-
- var fixture;
- var element;
- var currentScope;
- var bootstrapSelect;
- var menu;
+describe('select', function () {
+ var scope, $sandbox, $compile, $timeout, $httpBackend, $templateCache;
beforeEach(module('$strap.directives'));
- beforeEach(function () {
- var body = angular.element('body');
- fixture = angular.element('<div></div>');
- fixture.appendTo(body);
- });
-
- afterEach(function () {
- fixture.remove();
- });
-
- beforeEach(inject(function ($compile, $rootScope) {
-
- element = angular.element('<select bs-select ng-model="model.item" ng-options="i.id as i.name for i in items"></select>');
- element.appendTo(fixture);
-
- currentScope = $rootScope;
-
- $rootScope.model = {
- item: 2
- };
-
- $rootScope.items = [
- {
- id: 1,
- name: 'item 1'
- },
- {
- id: 2,
- name: 'item 2'
- }
- ];
-
- $compile(element)($rootScope, false);
- $rootScope.$digest();
-
- bootstrapSelect = element.siblings('.bootstrap-select');
- menu = bootstrapSelect.find('ul[role=menu]');
+ beforeEach(inject(function ($injector, $rootScope, _$compile_, _$timeout_, _$httpBackend_, _$templateCache_) {
+ scope = $rootScope;
+ $compile = _$compile_;
+ $timeout = _$timeout_;
+ $httpBackend = _$httpBackend_;
+ $templateCache = _$templateCache_;
+ $sandbox = $('<div id="sandbox"></div>').appendTo('body');
}));
- it('initialises bootstrap select on the element', function () {
- expect(bootstrapSelect.length).toBe(1);
- });
-
- it('adds every item to the bootstrap select menu', function () {
- expect(menu.children().length).toBe(2);
+ afterEach(function() {
+ $sandbox.remove();
+ scope.$destroy();
});
- it('updates the bootstrap select menu when items are changed', function () {
-
- currentScope.items.push({
- id: 3,
- name: 'item 3'
+ var templates = {
+ 'default': {
+ scope: {items: [{id: '1', name: 'foo'}, {id: '2', name: 'bar'}, {id: '3', name: 'baz'}], selectedItem: '2'},
+ element: '<select ng-model="selectedItem" ng-options="value.id as value.name for (key, value) in items" bs-select></select>'
+ }
+ };
+
+ function compileDirective(template) {
+ template = template ? templates[template] : templates['default'];
+ angular.extend(scope, template.scope);
+ var $element = $(template.element).appendTo($sandbox);
+ $element = $compile($element)(scope);
+ scope.$digest(); // evaluate $evalAsync queue used by $q
+ $timeout.flush();
+ return $element;
+ }
+
+ describe('default template', function() {
+
+ var elm, select, menu;
+ beforeEach(function() {
+ elm = compileDirective();
+ select = elm.next('.bootstrap-select');
+ menu = select.find('ul[role=menu]');
});
- currentScope.$apply();
-
- expect(menu.children().length).toBe(3);
- });
-
- it('selects the correct item by default', function () {
- expect(menu.find('.selected').text()).toBe('item 2');
- });
-
- it('updates the scope when a new item is selected', function () {
- menu.find('li a').first().click();
-
- expect(currentScope.model.item).toBe(1);
- });
-
- it('updates bootstrap select when the model changes', function () {
-
- currentScope.model.item = 1;
- currentScope.$apply();
-
- expect(menu.find('.selected').text()).toBe('item 1');
- });
-
- it('does not add ng-scope class to bootstrap select element', function () {
- expect(bootstrapSelect.hasClass('ng-scope')).toBe(false);
- });
-
- it('adds new classes from original element when the model changes', function () {
-
- element.addClass('dummy');
+ it('initialises bootstrap select on the element', function () {
+ expect(select.length).toBe(1);
+ });
- currentScope.model.item = 1;
- currentScope.$apply();
+ it('adds every item to the bootstrap select menu', function () {
+ expect(menu.children().length).toBe(scope.items.length);
+ });
- expect(bootstrapSelect.hasClass('dummy')).toBe(true);
- });
+ it('updates the bootstrap select menu when items are changed', function () {
+ scope.items.push({id: '4', name: 'qux'});
+ scope.$digest();
+ expect(menu.children().length).toBe(scope.items.length);
+ });
- it('syncs classes removed from original element when the model changes', function () {
+ it('selects the correct item by default', function () {
+ expect(menu.find('.selected').text()).toBe('bar');
+ });
- element.addClass('dummy');
+ it('updates the scope when a new item is selected', function () {
+ menu.find('li a').first().click();
+ expect(scope.selectedItem).toBe('1');
+ });
- currentScope.model.item = 1;
- currentScope.$apply();
+ it('updates bootstrap select when the model changes', function () {
+ scope.selectedItem = '3';
+ scope.$digest();
+ expect(menu.find('.selected').text()).toBe('baz');
+ });
- element.removeClass('dummy');
+ it('does not add ng-scope class to bootstrap select element', function () {
+ expect(select.hasClass('ng-scope')).toBe(false);
+ });
- currentScope.model.item = 2;
- currentScope.$apply();
+ // it('adds new classes from original element when the model changes', function () {
+ // elm.addClass('dummy');
+ // scope.model.item = 1;
+ // scope.$digest();
+ // expect(select.hasClass('dummy')).toBe(true);
+ // });
+
+ // it('syncs classes removed from original element when the model changes', function () {
+ // element.addClass('dummy');
+ // scope.model.item = 1;
+ // scope.$digest();
+ // element.removeClass('dummy');
+ // scope.model.item = 2;
+ // scope.$digest();
+ // expect(select.hasClass('dummy')).toBe(false);
+ // });
- expect(bootstrapSelect.hasClass('dummy')).toBe(false);
});
-});
+});
@@ -8,12 +8,16 @@
}
.bootstrap-select.btn-group, .bootstrap-select.btn-group[class*="span"] {
- float:none;
+ float:none;
display: inline-block;
margin-bottom: 10px;
margin-left:0;
}
+.bootstrap-select.btn-group.pull-right, .bootstrap-select.btn-group[class*="span"].pull-right, .row-fluid .bootstrap-select.btn-group[class*="span"].pull-right {
+ float:right;
+}
+
.input-append .bootstrap-select.btn-group {
margin-left: -1px;
}
@@ -22,7 +26,7 @@
margin-right: -1px;
}
-.bootstrap-select {
+.bootstrap-select:not([class*="span"]) {
width: 220px;
}
@@ -37,9 +41,9 @@
}
.bootstrap-select.btn-group .btn .filter-option {
- overflow:hidden;
+ overflow:hidden;
position:absolute;
- left:12px;
+ left:12px;
right:25px;
text-align:left;
}
@@ -65,8 +69,8 @@
}
.bootstrap-select.btn-group .dropdown-menu dt {
- display:block;
- padding:3px 20px;
+ display:block;
+ padding:3px 20px;
cursor:default;
}
@@ -91,7 +95,7 @@ bootstrap-select.btn-group .dropdown-menu li > a {
}
.bootstrap-select.btn-group .dropdown-menu li:not(.disabled) > a:hover small {
- color: #64b1d8;
+ color: #64b1d8;
color:rgba(255,255,255,0.4);
}
@@ -100,13 +104,13 @@ bootstrap-select.btn-group .dropdown-menu li > a {
}
-.bootstrap-select.btn-group.show-tick .dropdown-menu li.selected a i.check-mark {
+.bootstrap-select.btn-group.show-tick .dropdown-menu li.selected a i.check-mark {
display:inline-block;
position: absolute;
right: 20px;
}
-.bootstrap-select.btn-group .dropdown-menu li a i.check-mark {
+.bootstrap-select.btn-group .dropdown-menu li a i.check-mark {
display: none;
}
@@ -119,7 +123,7 @@ bootstrap-select.btn-group .dropdown-menu li > a {
}
.bootstrap-select.btn-group .dropdown-menu li:not(.disabled) > a:hover small, .bootstrap-select.btn-group .dropdown-menu li:not(.disabled) > a:focus small {
- color: #64b1d8;
+ color: #64b1d8;
color:rgba(255,255,255,0.4);
}
@@ -130,8 +134,8 @@ bootstrap-select.btn-group .dropdown-menu li > a {
.bootstrap-select.btn-group.show-menu-arrow .dropdown-menu {
overflow-y:visible !important;
-}
-
+}
+
.bootstrap-select.btn-group.show-menu-arrow .dropdown-menu::after {
position: absolute;
top: -6px;
Oops, something went wrong.

0 comments on commit 205cd71

Please sign in to comment.