Skip to content
This repository has been archived by the owner on May 5, 2018. It is now read-only.

Commit

Permalink
Merge pull request #443 from antmt/select2_directive_bug_fix
Browse files Browse the repository at this point in the history
Select2 Refactor
  • Loading branch information
ProLoser committed Feb 23, 2013
2 parents eea2cb1 + 8d64b8f commit 992477a
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 11 deletions.
26 changes: 17 additions & 9 deletions modules/directives/select2/select2.js
Expand Up @@ -45,21 +45,23 @@ angular.module('ui.directives').directive('uiSelect2', ['ui.config', '$timeout',
// Watch the model for programmatic changes // Watch the model for programmatic changes
controller.$render = function () { controller.$render = function () {
if (isSelect) { if (isSelect) {
elm.select2('val', controller.$modelValue); elm.select2('val', controller.$viewValue);
} else { } else {
if (isMultiple) { if (isMultiple) {
if (!controller.$modelValue) { if (!controller.$viewValue) {
elm.select2('data', []); elm.select2('data', []);
} else if (angular.isArray(controller.$modelValue)) { } else if (angular.isArray(controller.$viewValue)) {
elm.select2('data', controller.$modelValue); elm.select2('data', controller.$viewValue);
} else { } else {
elm.select2('val', controller.$modelValue); elm.select2('val', controller.$viewValue);
} }
} else { } else {
if (angular.isObject(controller.$modelValue)) { if (angular.isObject(controller.$viewValue)) {
elm.select2('data', controller.$modelValue); elm.select2('data', controller.$viewValue);
} else if (!controller.$viewValue) {
elm.select2('data', null);
} else { } else {
elm.select2('val', controller.$modelValue); elm.select2('val', controller.$viewValue);
} }
} }
} }
Expand Down Expand Up @@ -109,11 +111,17 @@ angular.module('ui.directives').directive('uiSelect2', ['ui.config', '$timeout',
} }


// Set initial value since Angular doesn't // Set initial value since Angular doesn't
elm.val(scope.$eval(attrs.ngModel)); //elm.val(scope.$eval(attrs.ngModel));


// Initialize the plugin late so that the injected DOM does not disrupt the template compiler // Initialize the plugin late so that the injected DOM does not disrupt the template compiler
$timeout(function () { $timeout(function () {
elm.select2(opts); elm.select2(opts);

// Set initial value - I'm not sure about this but it seems to need to be there
elm.val(controller.$viewValue);
// important!
controller.$render();

// Not sure if I should just check for !isSelect OR if I should check for 'tags' key // Not sure if I should just check for !isSelect OR if I should check for 'tags' key
if (!opts.initSelection && !isSelect) if (!opts.initSelection && !isSelect)
controller.$setViewValue(elm.select2('data')); controller.$setViewValue(elm.select2('data'));
Expand Down
156 changes: 154 additions & 2 deletions modules/directives/select2/test/select2Spec.js
@@ -1,3 +1,22 @@
// a helper directive for injecting formatters and parsers
angular.module('ui.directives').directive('injectTransformers', [ function () {
return {
restrict: 'A',
require: 'ngModel',
priority: -1,
link: function (scope, element, attr, ngModel) {
var local = scope.$eval(attr.injectTransformers);

if (!angular.isObject(local) || !angular.isFunction(local.fromModel) || !angular.isFunction(local.fromElement)) {
throw "The injectTransformers directive must be bound to an object with two functions (`fromModel` and `fromElement`)";
}

ngModel.$parsers.push(local.fromElement);
ngModel.$formatters.push(local.fromModel);
}
};
}]);

/*global describe, beforeEach, module, inject, it, spyOn, expect, $ */ /*global describe, beforeEach, module, inject, it, spyOn, expect, $ */
describe('uiSelect2', function () { describe('uiSelect2', function () {
'use strict'; 'use strict';
Expand All @@ -16,6 +35,54 @@ describe('uiSelect2', function () {
query.callback(data); query.callback(data);
} }
}; };

scope.transformers = {
fromModel: function (modelValue) {
if (!modelValue) {
return modelValue;
}

if (angular.isArray(modelValue)) {
return modelValue.map(function (val) {
val.text += " - I've been formatted";
return val;
});
}

if (angular.isObject(modelValue)) {
modelValue.text += " - I've been formatted";
return modelValue;
}

return modelValue + " - I've been formatted";
},
fromElement: function (elementValue) {
var suffix = " - I've been formatted";

if (!elementValue) {
return elementValue;
}

if (angular.isArray(elementValue)) {
return elementValue.map(function (val) {
val.text += val.text.slice(0, val.text.indexOf(" - I've been formatted"));
return val;
});
}

if (angular.isObject(elementValue)) {

elementValue.text = elementValue.text.slice(0, elementValue.text.indexOf(suffix));
return elementValue;
}

if (elementValue) {
return elementValue.slice(0, elementValue.indexOf(suffix));
}

return undefined;
}
};
})); }));


/** /**
Expand Down Expand Up @@ -94,7 +161,7 @@ describe('uiSelect2', function () {
compile('<input ui-select2/>'); compile('<input ui-select2/>');
}).toThrow(); }).toThrow();
}); });
it('should creae proper DOM structure', function () { it('should create proper DOM structure', function () {
var element = compile('<input ui-select2="options" ng-model="foo"/>'); var element = compile('<input ui-select2="options" ng-model="foo"/>');
expect(element.siblings().is('div.select2-container')).toBe(true); expect(element.siblings().is('div.select2-container')).toBe(true);
}); });
Expand Down Expand Up @@ -135,10 +202,95 @@ describe('uiSelect2', function () {
}); });
}); });
}); });
describe('consumers of ngModel should correctly use $viewValue', function() {
it('should use any formatters if present (select - single select)', function(){
scope.foo = 'First';
var element = compile('<select ui-select2 ng-model="foo" inject-transformers="transformers"><option>First - I\'ve been formatted</option><option>Second - I\'ve been formatted</option></select>');
expect(element.select2('val')).toBe('First - I\'ve been formatted');
scope.$apply('foo = "Second"');
expect(element.select2('val')).toBe('Second - I\'ve been formatted');
});

// isMultiple && falsey
it('should use any formatters if present (input multi select - falsey value)', function() {
// need special function to hit this case
// old code checked modelValue... can't just pass undefined to model value because view value will be the same
scope.transformers.fromModel = function(modelValue) {
if (modelValue === "magic") {
return undefined;
}

return modelValue;
};

var element = compile('<input ng-model="foo" multiple ui-select2="options" inject-transformers="transformers">');
spyOn($.fn, 'select2');
scope.$apply('foo="magic"');
expect(element.select2).toHaveBeenCalledWith('data', []);
});
// isMultiple && isArray
it('should use any formatters if present (input multi select)', function() {
var element = compile('<input ng-model="foo" multiple ui-select2="options" inject-transformers="transformers">');
spyOn($.fn, 'select2');
scope.$apply('foo=[{ id: 1, text: "first" },{ id: 2, text: "second" }]');
expect(element.select2).toHaveBeenCalledWith('data', [{ id: 1, text: "first - I've been formatted" },{ id: 2, text: "second - I've been formatted" }]);
});
// isMultiple...
it('should use any formatters if present (input multi select - non array)', function() {
var element = compile('<input ng-model="foo" multiple ui-select2="options" inject-transformers="transformers">');
spyOn($.fn, 'select2');
scope.$apply('foo={ id: 1, text: "first" }');
expect(element.select2).toHaveBeenCalledWith('val', { id: 1, text: "first - I've been formatted" });
});

// !isMultiple
it('should use any formatters if present (input - single select - object)', function() {
var element = compile('<input ng-model="foo" ui-select2="options" inject-transformers="transformers">');
spyOn($.fn, 'select2');
scope.$apply('foo={ id: 1, text: "first" }');
expect(element.select2).toHaveBeenCalledWith('data', { id: 1, text: "first - I've been formatted" });
});
it('should use any formatters if present (input - single select - non object)', function() {
var element = compile('<input ng-model="foo" ui-select2="options" inject-transformers="transformers">');
spyOn($.fn, 'select2');
scope.$apply('foo="first"');
expect(element.select2).toHaveBeenCalledWith('val', "first - I've been formatted");
});

it('should not set the default value using scope.$eval', function() {
// testing directive instantiation - change order of test
spyOn($.fn, 'select2');
spyOn($.fn, 'val');
scope.$apply('foo=[{ id: 1, text: "first" },{ id: 2, text: "second" }]');

var element = compile('<input ng-model="foo" multiple ui-select2="options" inject-transformers="transformers">');
expect(element.val).not.toHaveBeenCalledWith([{ id: 1, text: "first" },{ id: 2, text: "second" }]);
});
it('should expect a default value to be set with a call to the render method', function() {
// this should monitor the events after init, when the timeout callback executes
var opts = angular.copy(scope.options);
opts.multiple = true;

scope.$apply('foo=[{ id: 1, text: "first" },{ id: 2, text: "second" }]');

spyOn($.fn, 'select2');
var element = compile('<input ng-model="foo" multiple ui-select2="options" inject-transformers="transformers">');

// select 2 init
expect(element.select2).toHaveBeenCalledWith(opts);

// callback setting
expect(element.select2).toHaveBeenCalledWith('data', [{ id: 1, text: "first - I've been formatted" },{ id: 2, text: "second - I've been formatted" }]);

// retieve data
expect(element.select2).toHaveBeenCalledWith('data');
});

});
it('should set the model when the user selects an item', function(){ it('should set the model when the user selects an item', function(){
var element = compile('<input ng-model="foo" multiple ui-select2="options">'); var element = compile('<input ng-model="foo" multiple ui-select2="options">');
// TODO: programmactically select an option // TODO: programmactically select an option
// expect(scope.foo).toBe(/* selected val */); // expect(scope.foo).toBe(/* selected val */) ;
}); });
}); });
}); });

0 comments on commit 992477a

Please sign in to comment.