Skip to content

Commit 6b99f79

Browse files
committed
feat(ngRepeat): provide support for aliasing filtered repeater results as a scope member
ngRepeat can now alias the snapshot of the list of items evaluated after all filters have been applied as a property on the scope. Prior to this fix, when a filter is applied on a repeater, there is no way to trigger an event when the repeater renders zero results. Closes angular#5919
1 parent f07af61 commit 6b99f79

File tree

2 files changed

+85
-3
lines changed

2 files changed

+85
-3
lines changed

src/ng/directive/ngRepeat.js

+18-3
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@
112112
* For example: `item in items | filter:searchText track by item.id` is a pattern that might be used to apply a filter
113113
* to items in conjunction with a tracking expression.
114114
*
115+
* * `variable in expression as alias_expression` – You can also provide an optional alias expression which will then store the
116+
* intermediate results of the repeater after the filters have been applied. Typically this is used to render a special message
117+
* when a filter is active on the repeater, but the filtered result set is empty.
118+
*
119+
* For example: `item in items | filter:x as results` will store the fragment of the repeated items as `results`, but only after
120+
* the items have been processed through the filter.
121+
*
115122
* @example
116123
* This example initializes the scope to a list of names and
117124
* then uses `ngRepeat` to display every person:
@@ -132,9 +139,12 @@
132139
I have {{friends.length}} friends. They are:
133140
<input type="search" ng-model="q" placeholder="filter friends..." />
134141
<ul class="example-animate-container">
135-
<li class="animate-repeat" ng-repeat="friend in friends | filter:q">
142+
<li class="animate-repeat" ng-repeat="friend in friends | filter:q as results">
136143
[{{$index + 1}}] {{friend.name}} who is {{friend.age}} years old.
137144
</li>
145+
<li class="animate-repeat" ng-if="results.length == 0">
146+
<strong>No results found...</strong>
147+
</li>
138148
</ul>
139149
</div>
140150
</file>
@@ -208,8 +218,8 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
208218
$$tlb: true,
209219
link: function($scope, $element, $attr, ctrl, $transclude){
210220
var expression = $attr.ngRepeat;
211-
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?\s*$/),
212-
trackByExp, trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn,
221+
var match = expression.match(/^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?(?:\s+as\s+([\s\S]+?))?\s*$/),
222+
trackByExp, trackByExpGetter, aliasAs, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn,
213223
lhs, rhs, valueIdentifier, keyIdentifier,
214224
hashFnLocals = {$id: hashKey};
215225

@@ -221,6 +231,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
221231
lhs = match[1];
222232
rhs = match[2];
223233
trackByExp = match[3];
234+
aliasAs = match[4];
224235

225236
if (trackByExp) {
226237
trackByExpGetter = $parse(trackByExp);
@@ -272,6 +283,10 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) {
272283
nextBlockOrder = [],
273284
elementsToRemove;
274285

286+
if (aliasAs) {
287+
$scope[aliasAs] = collection;
288+
}
289+
275290
var updateScope = function(scope, index) {
276291
scope[valueIdentifier] = value;
277292
if (keyIdentifier) scope[keyIdentifier] = key;

test/ng/directive/ngRepeatSpec.js

+67
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,58 @@ describe('ngRepeat', function() {
374374
});
375375
});
376376

377+
describe('alias as', function() {
378+
it('should assigned the filtered to the target scope property if an alias is provided', function() {
379+
element = $compile(
380+
'<div ng-repeat="item in items | filter:x track by $index as results">{{item.name}}/</div>')(scope);
381+
382+
scope.items = [
383+
{ name : 'red' },
384+
{ name : 'blue' },
385+
{ name : 'green' },
386+
{ name : 'black' },
387+
{ name : 'orange' },
388+
{ name : 'blonde' }
389+
];
390+
391+
expect(scope.results).toBeUndefined();
392+
scope.$digest();
393+
394+
scope.x = 'bl';
395+
scope.$digest();
396+
397+
expect(scope.results).toEqual([
398+
{ name : 'blue' },
399+
{ name : 'black' },
400+
{ name : 'blonde' }
401+
]);
402+
403+
scope.items = [];
404+
scope.$digest();
405+
406+
expect(scope.results).toEqual([]);
407+
});
408+
409+
it('should render a message when the repeat list is empty', function() {
410+
element = $compile(
411+
'<div>' +
412+
' <div ng-repeat="item in items | filter:x as results">{{item}}</div>' +
413+
' <div ng-if="results.length == 0">' +
414+
' No results found...' +
415+
' </div>' +
416+
'</div>')(scope);
417+
418+
scope.items = [1,2,3,4,5,6];
419+
scope.$digest();
420+
expect(trim(element.text())).toEqual('123456');
421+
422+
scope.x = '0';
423+
scope.$digest();
424+
425+
expect(trim(element.text())).toEqual('No results found...');
426+
});
427+
});
428+
377429

378430
it('should allow expressions over multiple lines', function() {
379431
element = $compile(
@@ -1196,6 +1248,21 @@ describe('ngRepeat and transcludes', function() {
11961248
});
11971249

11981250

1251+
it('should set the state before linking', function() {
1252+
module(function($compileProvider) {
1253+
$compileProvider.directive('assertA', valueFn(function(scope) {
1254+
// This linking function asserts that a is set.
1255+
// If we only test this by asserting binding, it will work even if the value is set later.
1256+
expect(scope.a).toBeDefined();
1257+
}));
1258+
});
1259+
inject(function($compile, $rootScope) {
1260+
var element = $compile('<div><span ng-repeat="a in [1]"><span assert-a></span></span></div>')($rootScope);
1261+
$rootScope.$digest();
1262+
dealoc(element);
1263+
});
1264+
});
1265+
11991266
it('should set the state before linking', function() {
12001267
module(function($compileProvider) {
12011268
$compileProvider.directive('assertA', valueFn(function(scope) {

0 commit comments

Comments
 (0)