Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

ngRepeat should automatically or optionally render a fallback element when the expression yields no results #5919

Closed
matsko opened this issue Jan 21, 2014 · 33 comments

Comments

@matsko
Copy link
Contributor

matsko commented Jan 21, 2014

Say you have a complex expression on ngRepeat like:

<div ng-repeat="item in getItems() | filter:values">...</div>

So how can you detect if there are no items returned after the filter or from the getItems() call without using a scope variable/method? You would have to move everything into a controller.

Despite the repeat expression being complex and a potential bottleneck for an application, extracting the calculation out into a controller and manually watching the data set, filtering it and aggregating it is a lot of extra code after having just created a nice filtering system for your ngRepeat code.

What would be nice is if a fallback DIV was created only when the total (final) results extracted from the ngRepeat expression equal 0. This way you can do something like.

<div ng-repeat="item in getItems() | filter:values">
  <div ng-repeat-empty>
    There are no results found
  </div>
</div>
@IgorMinar
Copy link
Contributor

Can anyone think of a different syntax that we should consider?

I'd like to compare several syntaxes before we settle on the final api.

@scottopherson
Copy link

Nice, I really like this idea and ng-repeat-empty seems pretty elegant. I can't come up with any other syntaxes but I'd also like to see some alternatives. I assume it would work with .ng-enter/.ng-leave as well?

@matsko
Copy link
Contributor Author

matsko commented Jan 22, 2014

@scottopherson yup.

@Micka33
Copy link

Micka33 commented Jan 22, 2014

Like it. That would be nice to be able to set the ng-repeat-empty template using a URL as well.

@scottopherson
Copy link

@Micka33 I suppose <div ng-repeat-empty ng-include="'some-template.html'"></div> would work.

@benjamingr
Copy link
Contributor

👍 this is something I constantly have to deal with with the current workaround and having it baked into Angular seems appropriate given how common the use case is.

@eternalmatt
Copy link

Great idea. Proposing two other syntaxes for perspective

  1. As a sibling element
<div ng-repeat="item in getItems() | filter:values"></div>
<div ng-repeat-empty>There are no results found</div>
  1. @scottopherson's syntax; optionally assigning ng-repeat-empty a value is equivalent to the ng-include foo
<div ng-repeat="item in getItems() | filter:values">
    <div ng-repeat-empty="'some-template.html'"></div>
</div>

Inlining above syntax into ng-repeat should work as well.

<div ng-repeat="item in getItems() | filter:values" 
     ng-repeat-empty="'some-template.html'">
</div>

@matsko
Copy link
Contributor Author

matsko commented Feb 3, 2014

@eternalmatt the sibling element solution can't be done. Anything that requires an element to be positioned close by another element is too strict. Otherwise you would have to pass the same ngRepeat expression into the HTML attribute.

I like the last example, but it forces you to make an external template. The 2nd example is the best of both worlds, but it also requires strict positioning.

@caitp
Copy link
Contributor

caitp commented Feb 3, 2014

Why would the middle option require strict positioning? It would basically be transclude + filter out elements which don't contain the ng-repeat-empty attribute.

But the third example would be the easiest to implement, and maybe that's a good starting point?

@matsko
Copy link
Contributor Author

matsko commented Feb 3, 2014

By strict I meant that the <div ng-repeat-empty> must be inside of the repeater. But that's no different than ng-switch. The last example can be done without breaking anything and is easier to read. Let's do that.

@eternalmatt
Copy link

With the sibling element example, I was thinking this was feasible since
ngRepeatStart and ngRepeatEnd both exist.
On Feb 3, 2014 5:32 PM, "Matias Niemelä" notifications@github.com wrote:

By strict I meant that the
must be inside of the repeater. But that's no different than ng-switch.
The last example can be done without breaking anything and is easier to
read. Let's do that.

Reply to this email directly or view it on GitHubhttps://github.com//issues/5919#issuecomment-34008554
.

@mb-dev
Copy link

mb-dev commented Feb 4, 2014

Syntax similar to ng-switch is also an option:

<div ng-repeat="item in getItems() | filter:values">
    <div ng-repeat-content>
        .. HTML elements go here
    </div>
    <div ng-repeat-empty="'some-template.html'"></div>
</div>

Also can ng-repeat possibly add variable called empty and then it can be used like:

<div ng-if="$empty" ng-include="'some-template.html'"></div>

@rodyhaddad
Copy link
Contributor

@matsko
Keep in mind that ng-repeat repeats the element it's on, not its content.
So given:

<ul>
  <li ng-repeat="item in list">{{ item }}</li>
</ul>

using your strict ng-repeat-empty would result in invalid html:

<ul>
  <li ng-repeat="item in list">
    {{ item }}
    <li ng-repeat-empty>No items</li>
  </li>
</ul>

Same thing with <tr>, <td>, <option>, etc.
So having the ng-repeat-empty inside the current ng-repeat would cause DOM parsing problems.

What @mb-dev proposes would solve this, but it does create new issues to handle (ng-repeat would need to figure out that a -content is in use, or if one uses -empty without -content)

We can always create a new repeater directive

<ul ng-repeat-content="item in list">
  <li>{{ item }}</li>
  <li ng-repeat-empty>No items</li>
</ul>

I'm sure we can figure out a way to have both ng-repeat and ng-repeat-content share the same repeat-logic code, but different configuration for what to repeat and where.

@caitp
Copy link
Contributor

caitp commented Feb 6, 2014

The difficulty with these strategies is, we want the ng-repeat-empty directive to remove itself from the transcluded content, and give ngRepeat the transclude function.

This doesn't really work with the current compiler design, and it would need to be hacked to support that. The simplest way to make this work would be with just an empty-template-url attribute, because this wouldn't depend on any changes to the compiler.

@tdakhla
Copy link

tdakhla commented Feb 7, 2014

I totally ripped off someone's Plunkr example just now, but here's sorta what we've been doing to solve this issue:

http://plnkr.co/edit/Ro3VnZdJbdX7yEKtqPOe?p=preview

It's not the prettiest solution of the bunch, but all that's needed is one global isEmpty function to check the newly created scope variable. And that function most likely exists on scope in one way or another in a large app anyways. No extracting complex calculations into a controller, no extra directives/attributes or any of that.

@longrunningprocess
Copy link

personally, I don't like the idea of the extra directive at the same level as the as the ng-repeat. That feels like coupling to me...

I think the framework provides the ability to solve this well already as demostrated in tdakhla's example so I would prefer not seeing anything new for this use case.

@matsko
Copy link
Contributor Author

matsko commented Feb 7, 2014

Thinking about this more, I don't like placing in an extra item into the repeat element. It can easily be regarded as repeated content, even though it will get cut out of the repeater upon compile.

I like the idea of a local variable that is a snapshot of the current collection.

<div ng-switch="repeated_items.length" ng-if="repeated_items">
  <div ng-switch-when="0">There are no items</div>
  <div ng-switch-when="1">There is currently only one item</div>
  <div ng-switch-default>There are {{ repeated_items.length }} items</div>
</div>
<div ng-repeat="item in getItems()" ng-repeat-alias="repeated_items">
  {{ item }}
</div>

This way ngRepeat doesn't have to be changed much and it won't break anything. The only problem with the example above is that the ngSwitch will only render itself after the 2nd digest.

What do you guys think?

@matsko
Copy link
Contributor Author

matsko commented Feb 7, 2014

If possible, we could also handling the alias as:

<div ng-repeat="item in getItems() alias as repeated_items">...</div>

When a filter or track by is added, it would appear at the end:

<div ng-repeat="item in getItems() | filter:q alias as repeated_items">...</div>
<div ng-repeat="item in getItems() track by item.id alias as repeated_items">...</div>
<div ng-repeat="item in getItems() | filter:q track by item.id alias as repeated_items">...</div>

@dee-kap
Copy link
Contributor

dee-kap commented Feb 26, 2014

This has been suggested earlier on this thread

<div ng-repeat="item in getItems() | filter:values" 
     ng-repeat-empty="'some-template.html'">
</div>

I will add my thoughts.

What if the value of ng-repeat-empty can either be an external template or an element in the same vie. We could pass in the id of an existing element to ng-repeat-element

<div ng-repeat="item in getItems() | filter:values" 
     ng-repeat-empty="'some-template.html'">
</div>

or

<div ng-repeat="item in getItems() | filter:values" 
     ng-repeat-empty="idOfAnElementInTheCurrentView">
</div>

@disintegrator
Copy link

+1

@matsko
Copy link
Contributor Author

matsko commented Mar 12, 2014

We're going with ng-repeat-empty="'some-template.html'" for 1.3. This may be something more versatile in 2.0.

@kuraga
Copy link

kuraga commented May 29, 2014

+1. Any updates?

@esakal
Copy link

esakal commented Jun 6, 2014

I worked on this issue while participating in an hackathon in ng-conf Israel, http://ng-conf.gdg.co.il/

I'm adding a plunker with my suggested solution http://run.plnkr.co/plunks/oDbNNg/

I support setting the empty directive on a parent element of the repeat directive to allow using a different type of element when no items found as shown in sample 2

Sample 1 - Showing fallback for repeat as li element

<ul data-ng-repeat-empty="noItemsFound">
    <li ng-repeat="item in list3 | idValueLowerThen100">{{item.text}}</li>
    <li name="noItemsFound"><span style="color:red">No Items Found </span></li>
</ul>           

Sample 2 - Showing fallback for repeat as sibiling div of the ul (external to ul)

<div data-ng-repeat-empty="noItemsFound">
    <ul>
        <li ng-repeat="item in list2 | idValueLowerThen100">{{item.text}}</li>
    </ul>
    <div name="noItemsFound">
        <div style="color:red">No Items Found </div>
    </div>
</div>

I also pushed my changes to my repository https://github.com/esakal/angular.js/commit/ea638a685227c1ef109b75d07216e0fa813594bf

Any comments, suggestions will be appriciated

@urish
Copy link

urish commented Jun 10, 2014

@esakal thanks for working on this!

I suggest the following syntax, which is both backward-compatible, does not require giving a specific name to each instance of ng-repeat-empty, and I believe does not break the DOM parser in any way:

<ul ng-repeat-container>
    <li ng-repeat="item in list">{{item.text}}</li>
    <li ng-repeat-empty><span class="error">No items found.</span></li>
</ul>

This syntax can also co-exists with the syntax mentioned by @matsko above.

@IgorMinar any comments on the various syntax options suggested so far?

@rodyhaddad
Copy link
Contributor

How about just:

<div ng-repeat="item in getItems() as repeated_items">...</div>

then you can have ngIf to display text for the zero case and you can also use the alias inside of the repeater to display stuff like "1 of 3" when the getItems returns a collection with a length that you don't otherwise know.

@matsko
Copy link
Contributor Author

matsko commented Jul 1, 2014

@rodyhaddad but that aliased value would be registered on the same scope that getItems() lives on.

I really like this solution since it gives the most flexibility.

How does everyone else feel here about this?

@IgorMinar
Copy link
Contributor

+1

matsko added a commit to matsko/angular.js that referenced this issue Jul 2, 2014
…s 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
matsko added a commit to matsko/angular.js that referenced this issue Jul 2, 2014
…s 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
matsko added a commit to matsko/angular.js that referenced this issue Jul 21, 2014
…s 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
Closes angular#8046
@matsko matsko closed this as completed in e0adb9c Jul 22, 2014
@danijar
Copy link

danijar commented Jul 22, 2014

+1

1 similar comment
@precz
Copy link

precz commented Jul 29, 2014

+1

@joshbowdoin
Copy link
Contributor

+1 ! - was hacking my way around this before (often filtering in controller on demand, so that I would have result of filtered list).

@huguangju
Copy link

+1

1 similar comment
@yuyudhan
Copy link

yuyudhan commented Jan 7, 2018

+1

@Narretz
Copy link
Contributor

Narretz commented Jan 8, 2018

There's already a solution for this: you can use the alias expression as <something>, which will publish <something> into the scope, and then you can check if <something> has no length, and display something in that case.
This is in the docs: https://code.angularjs.org/1.6.8/docs/api/ng/directive/ngRepeat
See the example.

@angular angular locked as resolved and limited conversation to collaborators Jan 8, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.