kalai kalai - 1 year ago 43
AngularJS Question

How to show star-rating dynamically based on response?

I need to display star rating dynamically based on response.

I am able to display values from 1 to 5 but if rating is 0 then no empty stars are displaying.

If rating = 0.4 also it's showing 1 star filled.

My controller:

(function() {
'use strict';


angular
var app = angular
.module('app')

app.directive('starRating', function () {

return {
restrict: 'A',
template: '<ul class="rating">' +
'<li ng-repeat="star in stars" ng-class="star" ng-click="toggle($index)">' +
'\u2605' +
'</li>' +
'</ul>',
scope: {
ratingValue: '=',
max: '='
},
link: function (scope, elem, attrs) {

var updateStars = function () {

scope.stars = [];
for (var i = 0; i < scope.max; i++) {
if(i == 0) {
scope.stars = [];
scope.stars.push({
empty: i = 0
});
} else {
scope.stars.push({
filled: i < scope.ratingValue
});
}
}

};
scope.$watch('ratingValue', function (oldVal, newVal) {
if (newVal) {
updateStars();
}
});
}
}
});


app.controller('Controller', Controller);

Controller.$inject = ['UserService', '$location', '$rootScope', '$scope', 'fileUpload', 'FlashService', '$cookieStore', '$timeout', '$window'];

function Controller(UserService, $location, $rootScope, $scope, fileUpload, FlashService, $cookieStore, $timeout, $window) {

$scope.selectTestSubject = function() {

$scope.rating = 0;

console.log(levels.length);

for(var k=0; k<levels.length; k++) {
var respRating = levels[k].rating;
// var respRating = 1.5;

console.log(respRating);

$scope.ratings = [{
current: respRating,
max: 5
}];

if(respRating == 0) {

$scope.defaultRating = true;
} else {

$scope.defaultRating = false;
}
}
}
}
}) ();


My HTML page:

<div><span ng-repeat="rating in ratings">

<div star-rating rating-value="rating.current" max="rating.max"></div>
</span>
</div>

Answer Source

One problem with your solution is your $watch expression. Where you have the following:

scope.$watch('ratingValue', function (oldVal, newVal) {
    if (newVal) {
        updateStars();
    }
});

oldVal and newVal are actually the wrong way around, the $watch function first takes in the new value followed by the old value. Secondly, the condition if (newVal) doesn't work for 0, because 0 is a falsey value.

Instead, you should have:

scope.$watch('ratingValue', function(value, previousValue) {
    // Only update the view when the value has changed.
    if (value !== previousValue) {
        updateStars();
    }
});

Your updateStars function also always reinitialises the scope.stars variable and appends onto it. Doing this can have some unwanted side effects and results in the view not reflecting the model value. It's best to initialise the array, then append the item if it doesn't yet exist or update the existing value. So you'll have something like this:

// Initialise the stars array.
scope.stars = [];

var updateStars = function() {

    for (var i = 0; i < scope.max; i++) {
        var filled = i < Math.round(scope.ratingValue);
        // Check if the item in the stars array exists and 
        // append it, otherwise update it.
        if (scope.stars[i] === undefined) {
            scope.stars.push({
                filled: filled
            });
        } else {
            scope.stars[i].filled = filled;
        }
    }

};

Since the $watch expression only updates the stars when the value has changed, you'll now need to trigger the update the first time your link function fires. So this is simply:

// Trigger an update immediately.
updateStars();

Your template also does not correctly utilise the filled property on the star, it should instead contain the appropriate ng-class like so:

<ul class="rating">
    <li class="star" 
        ng-repeat="star in stars" 
        ng-class="{ filled: star.filled }"
        ng-click="toggle($index)">
      \u2605
    </li>
</ul>

With a simple style,

.star {
  cursor: pointer;
  color: black;
}

.star.filled {
  color: yellow;
}

You can also improve this rating system by listening to mouseenter and mouseleave effects, so that the stars appear yellow when the user is selecting a new value. This is pretty common functionality. You can achieve this by making a few modifications.

To begin with, the template should be updated to listen for these events:

<ul class="rating">
    <li class="star" 
        ng-repeat="star in stars" 
        ng-class="{ filled: star.filled }"
        ng-mouseenter="onMouseEnter($event, $index + 1)"
        ng-mouseleave="onMouseLeave($event)"
        ng-click="toggle($index)">
      \u2605
    </li>
</ul>

Next, we want to make a small adjustment to the updateStars function to take in a rating parameter:

var updateStars = function(rating /* instead of blank */ ) {

    for (var i = 0; i < scope.max; i++) {
        var filled = i < Math.round(rating); // instead of scope.ratingValue
        // Check if the item in the stars array exists and 
        // append it, otherwise update it.
        if (scope.stars[i] === undefined) {
            scope.stars.push({
                filled: filled
            });
        } else {
            scope.stars[i].filled = filled;
        }
    }

};

// Trigger an update immediately.
updateStars(scope.ratingValue /* instead of blank */ );

scope.$watch('ratingValue', function(value, previousValue) {
    // Only update the view when the value changed.
    if (value !== previousValue) {
        updateStars(scope.ratingValue /* instead of blank */ );
    }
});

Now we can add in our event callbacks from the view,

// Triggered when the cursor enters a star rating (li element).
scope.onMouseEnter = function (event, rating) {
    updateStars(rating);
};

// Triggered when the cursor leaves a star rating.
scope.onMouseLeave = function (event) {
    updateStars(scope.ratingValue);
};

And that's it! Full demo here.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download