0

I know we can achieve conditional binding with ng-repeat through the following:

<tr ng-repeat="viewer in allViewers track by viewer.Id">
   <td>
      <img width="18" ng-src="{{!viewer.StatusByUser || viewer.StatusByUser != 1 ? 
         '/images/icons/hide.svg' : '/images/icons/show-18x11.png'}}" 
         onerror="this.src='/images/icons/hide.svg'" alt="Viewer Status" 
         title="Click to {{!viewer.StatusByUser || viewer.StatusByUser != 1 ? 
         'publish' : 'unpublish'}}" ng-click="manageViewerStatus(viewer.StatusByUser)">
   </td>
</tr>

But as you can see, this is messy and in reality, I have at least 5 to 6 status to keep track of and change the display image icon accordingly.

In JavaScript, we know that there is a way to pass the current element to a function:

<img src="/images/icon.jpg" onclick="changeAttr(this)" />

But in Angular, I tried the following but not working:

In HTML:

<img data-bind="{{renderStatusImg(this, viewer)}}" width="18" alt="Viewer Status"
   title="Click to publish" ng-click="manageViewerStatus(viewer.StatusByUser)">

In controller:

$scope.renderStatusImg = function (img, viewer) {
   $(img).attr("src", !viewer.StatusByUser || viewer.StatusByUser != 1 ? '/images/icons/hide.svg' : '/images/icons/show-18x11.png');
   // More complex logic can be written here in future to cater for more status.
   return viewer.Id;
};

As a result, viewer.Id can be returned and rendered in data-bind attribute, but the src attribute isn't rendered. I have heard of the directive approach but seems like it can only deal with the indexes of the underlying data and not the element itself. Any help will be appreciated, thank you.

Antonio Ooi
  • 1,601
  • 1
  • 18
  • 32

2 Answers2

1

I'd recommend using such a function to get the correct src value, but not write it back to it - that's actually bad practice. Instead, consider a list or an object, where the key is related to the iterator, and the value is the src of the image:

<tr ng-repeat="viewer in allViewers track by viewer.Id">
   <td>
      <img width="18" ng-src="viewerImages[viewer.Id]" alt="Viewer Status" 
         title="Click to {{!viewer.StatusByUser || viewer.StatusByUser != 1 ? 
         'publish' : 'unpublish'}}" ng-click="manageViewerStatus(viewer.StatusByUser)">
   </td>
</tr>
$scope.viewerImages = {};
$scope.allViewers.forEach((v) => {
  $scope.viewerImages[v.Id] = getViewerImage(v);
});

function getViewerImage(viewer) {
   // More complex logic can be written here in future to cater for more status.
   return !viewer.StatusByUser || viewer.StatusByUser != 1 ? '/images/icons/hide.svg' : '/images/icons/show-18x11.png';
};
Ruben Helsloot
  • 12,582
  • 6
  • 26
  • 49
  • Which means for every iteration of the binding, I need to iterate through `allViewers` again just to populate the image src? Can I populate the `viewerImages` first before assigning `allViewers` to the `$scope`? Would that be better? – Antonio Ooi Aug 20 '20 at 12:26
  • What do you mean by "every iteration"? Whenever the data changes? Because that true, but that happened in your example as well, and even more often with the `ng-if`. But, if the data changes for only some, you don't have to calculate all of them, which is a benefit. Of course you can populate `viewerImages` first, it doesn't matter. AngularJS only fires a `$digest` cycle when the code is idle, so if you assign them in the same function, order is not a problem. – Ruben Helsloot Aug 20 '20 at 12:41
  • I mean when every `` is bound, will your code loop through the whole `allViewers` data source again? Which means if I have 20 viewers, will it loop through **20x** just to determine the `src` for each `img`? I was thinking about using the `Map` API to simplify the code and logic, because not only the `src`, many other attributes will need to be changed according to the `StatusByUser`, e.g. `title` and `alt`. – Antonio Ooi Aug 20 '20 at 12:56
  • 1
    No, I meant to do it in the controller, either as you fetch the data or as you initialise the controller. That means do it once. – Ruben Helsloot Aug 20 '20 at 14:06
0

As suggested by @Ruben Helsloot, the question itself may not be a good practice. Apparently, not all technical questions have their own direct good answer, but all goals may have variety of solutions to achieve them for the same result, of which they may include best practices, moderate solutions, or even just a work-around. Therefore, it's good for solution seekers to share also their GOALS as their question alone may not be a recommended good practice.

Now my answer: Using ng-switch for the same goal

After a more extensive research, I found ng-switch is my perfect solution: Cleaner, less code, more extensible and readable, yet more Angular native:

<tr ng-repeat="viewer in allViewers track by viewer.Id" ng-switch on="viewer.StatusByUser">
   <td>
      <img ng-switch-when="0" src="/images/icons/hide.svg" width="18" alt="Draft" title="Click to publish" ng-click="manageViewerStatus(viewer)">
      <img ng-switch-when="1" src="/images/icons/show-18x11.png" width="18" alt="Published" title="Click to unpublish" ng-click="manageViewerStatus(viewer)">
      <img ng-switch-default src="/images/icons/hide.svg" width="18" alt="Old version" title="Click to fix" ng-click="manageViewerStatus(viewer)">
   </td>
   <td>
      <!-- More other columns that bound by viewer.StatusByUser can be benefited 
           from this approach. -->
   </td>
</tr>

Take note of the ng-switch-default, it is one of the main factors that led me to choose ng-switch: It can handle null and undefined object property, great for allowing you to offer fallback and upgrade services to your users. Notice also the title and alt attributes, I can properly define them in every conditional tag for ng-switch -- No complex code and logic required. Note that I don't even have to write a single additional JavaScript to handle this. Thanks to ng-switch.

Antonio Ooi
  • 1,601
  • 1
  • 18
  • 32