Skip to content
This repository was archived by the owner on May 29, 2019. It is now read-only.

Commit 6a97da2

Browse files
feat(typeahead): add typeahead directive
Closes #114
1 parent 3b677ee commit 6a97da2

File tree

6 files changed

+538
-0
lines changed

6 files changed

+538
-0
lines changed

src/typeahead/docs/demo.html

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<div class='container-fluid' ng-controller="TypeaheadCtrl">
2+
<pre>Model: {{selected| json}}</pre>
3+
<input type="text" ng-model="selected" typeahead="state for state in states | filter:$viewValue">
4+
</div>

src/typeahead/docs/demo.js

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/typeahead/docs/readme.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Typeahead is a AngularJS version of [Twitter Bootstrap typeahead plugin](http://twitter.github.com/bootstrap/javascript.html#typeahead)
2+
3+
This directive can be used to quickly create elegant typeheads with any form text input.
4+
5+
It is very well integrated into the AngularJS as:
6+
7+
* it uses the same, flexible syntax as the `select` directive (http://docs.angularjs.org/api/ng.directive:select)
8+
* works with promises and it means that you can retrieve matches using the `$http` service with minimal effort

src/typeahead/test/typeahead.spec.js

+309
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
describe('typeahead tests', function () {
2+
3+
beforeEach(module('ui.bootstrap.typeahead'));
4+
beforeEach(module('template/typeahead/typeahead.html'));
5+
6+
describe('syntax parser', function () {
7+
8+
var typeaheadParser, scope, filterFilter;
9+
beforeEach(inject(function (_$rootScope_, _filterFilter_, _typeaheadParser_) {
10+
typeaheadParser = _typeaheadParser_;
11+
scope = _$rootScope_;
12+
filterFilter = _filterFilter_;
13+
}));
14+
15+
it('should parse the simplest array-based syntax', function () {
16+
scope.states = ['Alabama', 'California', 'Delaware'];
17+
var result = typeaheadParser.parse('state for state in states | filter:$viewValue');
18+
19+
var itemName = result.itemName;
20+
var locals = {$viewValue:'al'};
21+
expect(result.source(scope, locals)).toEqual(['Alabama', 'California']);
22+
23+
locals[itemName] = 'Alabama';
24+
expect(result.viewMapper(scope, locals)).toEqual('Alabama');
25+
expect(result.modelMapper(scope, locals)).toEqual('Alabama');
26+
});
27+
28+
it('should parse the simplest function-based syntax', function () {
29+
scope.getStates = function ($viewValue) {
30+
return filterFilter(['Alabama', 'California', 'Delaware'], $viewValue);
31+
};
32+
var result = typeaheadParser.parse('state for state in getStates($viewValue)');
33+
34+
var itemName = result.itemName;
35+
var locals = {$viewValue:'al'};
36+
expect(result.source(scope, locals)).toEqual(['Alabama', 'California']);
37+
38+
locals[itemName] = 'Alabama';
39+
expect(result.viewMapper(scope, locals)).toEqual('Alabama');
40+
expect(result.modelMapper(scope, locals)).toEqual('Alabama');
41+
});
42+
43+
it('should allow to specify custom model mapping that is used as a label as well', function () {
44+
45+
scope.states = [
46+
{code:'AL', name:'Alabama'},
47+
{code:'CA', name:'California'},
48+
{code:'DE', name:'Delaware'}
49+
];
50+
var result = typeaheadParser.parse("state.name for state in states | filter:$viewValue | orderBy:'name':true");
51+
52+
var itemName = result.itemName;
53+
expect(itemName).toEqual('state');
54+
expect(result.source(scope, {$viewValue:'al'})).toEqual([
55+
{code:'CA', name:'California'},
56+
{code:'AL', name:'Alabama'}
57+
]);
58+
59+
var locals = {$viewValue:'al'};
60+
locals[itemName] = {code:'AL', name:'Alabama'};
61+
expect(result.viewMapper(scope, locals)).toEqual('Alabama');
62+
expect(result.modelMapper(scope, locals)).toEqual('Alabama');
63+
});
64+
65+
it('should allow to specify custom view and model mappers', function () {
66+
67+
scope.states = [
68+
{code:'AL', name:'Alabama'},
69+
{code:'CA', name:'California'},
70+
{code:'DE', name:'Delaware'}
71+
];
72+
var result = typeaheadParser.parse("state.code as state.name + ' ('+state.code+')' for state in states | filter:$viewValue | orderBy:'name':true");
73+
74+
var itemName = result.itemName;
75+
expect(result.source(scope, {$viewValue:'al'})).toEqual([
76+
{code:'CA', name:'California'},
77+
{code:'AL', name:'Alabama'}
78+
]);
79+
80+
var locals = {$viewValue:'al'};
81+
locals[itemName] = {code:'AL', name:'Alabama'};
82+
expect(result.viewMapper(scope, locals)).toEqual('Alabama (AL)');
83+
expect(result.modelMapper(scope, locals)).toEqual('AL');
84+
});
85+
});
86+
87+
describe('typeaheadPopup - result rendering', function () {
88+
89+
var scope, $rootScope, $compile;
90+
beforeEach(inject(function (_$rootScope_, _$compile_) {
91+
$rootScope = _$rootScope_;
92+
scope = $rootScope.$new();
93+
$compile = _$compile_;
94+
}));
95+
96+
it('should render initial results', function () {
97+
98+
scope.matches = ['foo', 'bar', 'baz'];
99+
scope.active = 1;
100+
101+
var el = $compile("<div><typeahead-popup matches='matches' active='active' select='select(activeIdx)'></typeahead-popup></div>")(scope);
102+
$rootScope.$digest();
103+
104+
var liElems = el.find('li');
105+
expect(liElems.length).toEqual(3);
106+
expect(liElems.eq(0)).not.toHaveClass('active');
107+
expect(liElems.eq(1)).toHaveClass('active');
108+
expect(liElems.eq(2)).not.toHaveClass('active');
109+
});
110+
111+
it('should change active item on mouseenter', function () {
112+
113+
scope.matches = ['foo', 'bar', 'baz'];
114+
scope.active = 1;
115+
116+
var el = $compile("<div><typeahead-popup matches='matches' active='active' select='select(activeIdx)'></typeahead-popup></div>")(scope);
117+
$rootScope.$digest();
118+
119+
var liElems = el.find('li');
120+
expect(liElems.eq(1)).toHaveClass('active');
121+
expect(liElems.eq(2)).not.toHaveClass('active');
122+
123+
liElems.eq(2).trigger('mouseenter');
124+
125+
expect(liElems.eq(1)).not.toHaveClass('active');
126+
expect(liElems.eq(2)).toHaveClass('active');
127+
});
128+
129+
it('should select an item on mouse click', function () {
130+
131+
scope.matches = ['foo', 'bar', 'baz'];
132+
scope.active = 1;
133+
$rootScope.select = angular.noop;
134+
spyOn($rootScope, 'select');
135+
136+
var el = $compile("<div><typeahead-popup matches='matches' active='active' select='select(activeIdx)'></typeahead-popup></div>")(scope);
137+
$rootScope.$digest();
138+
139+
var liElems = el.find('li');
140+
liElems.eq(2).find('a').trigger('click');
141+
expect($rootScope.select).toHaveBeenCalledWith(2);
142+
});
143+
});
144+
145+
describe('typeahead', function () {
146+
147+
var $scope, $compile;
148+
var changeInputValueTo;
149+
150+
beforeEach(inject(function (_$rootScope_, _$compile_, $sniffer) {
151+
$scope = _$rootScope_;
152+
$scope.source = ['foo', 'bar', 'baz'];
153+
$compile = _$compile_;
154+
155+
changeInputValueTo = function (element, value) {
156+
var inputEl = findInput(element);
157+
inputEl.val(value);
158+
inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
159+
$scope.$digest();
160+
};
161+
}));
162+
163+
//utility functions
164+
var prepareInputEl = function(inputTpl) {
165+
var el = $compile(angular.element(inputTpl))($scope);
166+
$scope.$digest();
167+
return el;
168+
};
169+
170+
var findInput = function(element) {
171+
return element.find('input');
172+
};
173+
174+
var findDropDown = function(element) {
175+
return element.find('div.dropdown');
176+
};
177+
178+
var findMatches = function(element) {
179+
return findDropDown(element).find('li');
180+
};
181+
182+
var triggerKeyDown = function(element, keyCode) {
183+
var inputEl = findInput(element);
184+
var e = $.Event("keydown");
185+
e.which = keyCode;
186+
inputEl.trigger(e);
187+
};
188+
189+
//custom matchers
190+
beforeEach(function () {
191+
this.addMatchers({
192+
toBeClosed: function() {
193+
var typeaheadEl = findDropDown(this.actual);
194+
this.message = function() {
195+
return "Expected '" + angular.mock.dump(this.actual) + "' to be closed.";
196+
};
197+
return !typeaheadEl.hasClass('open') && findMatches(this.actual).length === 0;
198+
199+
}, toBeOpenWithActive: function(noOfMatches, activeIdx) {
200+
201+
var typeaheadEl = findDropDown(this.actual);
202+
var liEls = findMatches(this.actual);
203+
204+
this.message = function() {
205+
return "Expected '" + angular.mock.dump(this.actual) + "' to be opened.";
206+
};
207+
return typeaheadEl.hasClass('open') && liEls.length === noOfMatches && $(liEls[activeIdx]).hasClass('active');
208+
}
209+
});
210+
});
211+
212+
//coarse grained, "integration" tests
213+
describe('initial state and model changes', function () {
214+
215+
it('should be closed by default', function () {
216+
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source'></div>");
217+
expect(element).toBeClosed();
218+
});
219+
220+
it('should not get open on model change', function () {
221+
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source'></div>");
222+
$scope.$apply(function(){
223+
$scope.result = 'foo';
224+
});
225+
expect(element).toBeClosed();
226+
});
227+
});
228+
229+
describe('basic functionality', function () {
230+
231+
it('should open and close typeahead based on matches', function () {
232+
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
233+
changeInputValueTo(element, 'ba');
234+
expect(element).toBeOpenWithActive(2, 0);
235+
});
236+
237+
it('should not open typeahead if input value smaller than a defined threshold', function () {
238+
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue' typeahead-min-length='2'></div>");
239+
changeInputValueTo(element, 'b');
240+
expect(element).toBeClosed();
241+
});
242+
243+
it('should support custom model selecting function', function () {
244+
$scope.updaterFn = function(selectedItem) {
245+
return 'prefix' + selectedItem;
246+
};
247+
var element = prepareInputEl("<div><input ng-model='result' typeahead='updaterFn(item) as item for item in source | filter:$viewValue'></div>");
248+
changeInputValueTo(element, 'f');
249+
triggerKeyDown(element, 13);
250+
expect($scope.result).toEqual('prefixfoo');
251+
});
252+
253+
it('should support custom label rendering function', function () {
254+
$scope.formatterFn = function(sourceItem) {
255+
return 'prefix' + sourceItem;
256+
};
257+
258+
var element = prepareInputEl("<div><input ng-model='result' typeahead='item as formatterFn(item) for item in source | filter:$viewValue'></div>");
259+
changeInputValueTo(element, 'fo');
260+
var matchHighlight = findMatches(element).find('a').html();
261+
expect(matchHighlight).toEqual('prefix<strong>fo</strong>o');
262+
});
263+
264+
});
265+
266+
describe('selecting a match', function () {
267+
268+
it('should select a match on enter', function () {
269+
270+
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
271+
var inputEl = findInput(element);
272+
273+
changeInputValueTo(element, 'b');
274+
triggerKeyDown(element, 13);
275+
276+
expect($scope.result).toEqual('bar');
277+
expect(inputEl.val()).toEqual('bar');
278+
});
279+
280+
it('should select a match on tab', function () {
281+
282+
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
283+
var inputEl = findInput(element);
284+
285+
changeInputValueTo(element, 'b');
286+
triggerKeyDown(element, 9);
287+
288+
expect($scope.result).toEqual('bar');
289+
expect(inputEl.val()).toEqual('bar');
290+
});
291+
292+
it('should select match on click', function () {
293+
294+
var element = prepareInputEl("<div><input ng-model='result' typeahead='item for item in source | filter:$viewValue'></div>");
295+
var inputEl = findInput(element);
296+
297+
changeInputValueTo(element, 'b');
298+
var match = $(findMatches(element)[1]).find('a')[0];
299+
300+
$(match).click();
301+
$scope.$digest();
302+
303+
expect($scope.result).toEqual('baz');
304+
expect(inputEl.val()).toEqual('baz');
305+
});
306+
});
307+
308+
});
309+
});

0 commit comments

Comments
 (0)