I'm struggling to recreate basic JavaScript code with an Angular directive

I created a basic widget/snippet that replaces empty stars with filled stars upon hover. When the mouse moves away, it reverts to the default number of stars, and when clicked, it changes the default value based on the star clicked. While it's a straightforward task, I'm struggling to replicate it using angular js...

I believe I need to utilize directives and transclusion to achieve this. My main challenge is generating a variable number of filled and empty stars based on the default value...

I would greatly appreciate any guidance on this. Here's the code snippet:

HTML section

<div class="ratingList" rating-widget rate='{{ rating }}' increment="increment()">
<span>Hate it</span>
<span class="star"><i class="fa fa-star-o fa-lg"></i></span>
<span class="star"><i class="fa fa-star-o fa-lg"></i></span>
<span class="star"><i class="fa fa-star-o fa-lg"></i></span>
<span class="star"><i class="fa fa-star-o fa-lg"></i></span>
<span class="star"><i class="fa fa-star-o fa-lg"></i></span>
<span>love it</span>

Simple controller

bmApp.controller('MainController', ['$scope', function($scope){
$scope.rating = 3;

$scope.increment = function(){
$scope.rating = $scope.rating + 1;
}

}]);

Problematic directive

bmApp.directive('ratingWidget', function(){
return{
    restrict: 'A',
    replace:true,
    transclude:true,

    template: '<div><button ng-click="increment()">Click</button><div class="rating"></div></div>',

    controller:['$scope', '$element', '$transclude', function($scope, $element, $transclude){

        $transclude(function(clone){
            var stars = clone.filter('.star');
            var filledStar = $('<span class="star"><i class="fa fa-star fa-lg"></i></span>');
            var container = $element.find('.rating');
            angular.forEach(stars, function(val, key){
                var star = $(val);
                if(key<$scope.rate)
                {
                    //console.log(key);
                    container.append(filledStar);
                    //star.replaceWith(filledStar);
                    //angular.element(star.children()[0]).removeClass('fa-star-o').addClass('fa-star')

                }else{
                    //console.log(key);
                    container.append(star);
                }       

            });

        });
    }],
    scope:{
        rate:'@',
        increment:'&'
    }
}

});

I'm facing challenges right from the start, unable to display filled stars based on the default value... The append function is only showing 3 stars...

Answer №1

There are multiple techniques available to manage this type of functionality.

I have made modifications to your sample to demonstrate the utilization of isolate scope and transclusion (for the increment() button).

We have encapsulated the star markup within the ratingWidget directive to enhance modularity and maintain it as a self-contained component.

Thanks to the ng-repeat and ng-class directives, we can avoid direct manipulation of HTML elements, as Angular handles the heavy lifting through data binding.

Here is a link to the Plunker example: http://plnkr.co/edit/abcd1234efg56789?p=preview

(If you review the Plunker history, you will observe my use of jQuery for direct element/class manipulation)

HTML:

<div ng-app="sampleApp">
    <div ng-controller="MainController">
        <div rating-widget rate="rating" max-rating="maxRating">
                <!--
                This transcluded content is linked with the parent scope instead of the
                scope of the `ratingWidget`.

                For instance, the `increment()` function is defined in `MainController`
                not in the `ratingWidget`.
                -->
            <button ng-click="increment()">Click</button>
        </div>
    </div>
</div>

JavaScript:

var sampleApp = angular.module('sampleApp', []);

sampleApp.controller('MainController', ['$scope',
    function($scope) {
        $scope.rating = 3;
        $scope.maxRating = 6;

        $scope.increment = function() {
          if ($scope.rating < $scope.maxRating){
            $scope.rating += 1;
          }
        }
    }]);

sampleApp.directive('ratingWidget', function() {
    return {
        restrict: 'A',
        transclude: true,
        scope: {
            rate: '=',
            maxRating: '='
        },
        link: function(scope, element, attr){
            var classes = {
                empty: 'fa-star-o',
                full: 'fa-star'
            };

            scope.stars = [];
            scope.$watch('maxRating', function(maxRating){
              maxRating = maxRating || 5;
              scope.stars.length = maxRating;
              for (var i = 0, len = scope.stars.length; i < len; i++){
                if (!scope.stars[i]){
                  scope.stars[i] = {
                    cssClass: classes.empty
                  };
                }
              }

              updateRating(scope.rate);
            });

            scope.$watch('rate', function(newRating){
                updateRating(newRating);
            });

            scope.selectRating = function(index){
              // Adjust the $index by adding 1 to align with the ratings starting from one.
              scope.rate = index + 1;
            }

            function updateRating(rating){
                rating = rating || 0;
                for (var i = 0, len = scope.stars.length; i < len; i++){
                    var star = scope.stars[i];
                    if (i < rating){
                      star.cssClass = classes.full;
                    } else {
                      star.cssClass = classes.empty;
                    }
                }
            }
        },
        template:   '<div>' +
                        '<div class="ratingList">' +
                            '<span>Hate it</span>' +
                            '<span class="stars">' +
                              '<span class="star" ng-click="selectRating($index)" ng-repeat="star in stars track by $index"><i class="fa fa-lg" ng-class="star.cssClass"></i></span>' +
                            '</span>' +
                            '<span>love it</span>' +
                        '</div>' +
                        '<div ng-transclude></div' +
                    '</div>'

    }
})

Edit:

@dan-tang

If the button were placed outside the directive but within MainController, everything would function as expected without the need for transclusion.

However, since the button is within the directive and calls a method defined in MainController, transclusion is necessary to bind the content to the parent scope.

Here's a Plunker showcasing this scenario: http://plnkr.co/edit/uvw098xyz12345?p=preview

HTML:

<div ng-controller="MainCtrl">
    <div>I am: {{name}}</div>

    <div widget>
        <!-- 
        Without transclusion, this will display 'widget', but with transclusion, it will show 'controller'.
        Transclusion allows us to control the scope to which these expressions are bound.
        -->
        <div>I am: {{name}}</div>
    </div>
</div>

JavaScript:

testApp.controller('MainCtrl', ['$scope', function($scope){
    $scope.name = 'controller';
}]);

testApp.directive('widget', function(){
    return {
        scope: true,
        transclude: true,
        link: function(scope, element, attr){
            scope.name = 'widget'
        },
        template: '<div>' +
            '<div>I am: {{name}}</div>' +
            '<div ng-transclude></div>' +
        '</div>'
    }
});

I view transclude in Angular as similar to a closure in JavaScript - it grants control over the scope to which variables and expressions are bound.

Here's a basic JavaScript comparison to illustrate the resemblance between the two concepts in the above example:

var name = 'controller';
var printCallback = function(){
    console.log('name=' + name);
}

function Widget(printCallback){
    var name = 'widget';

    this.printName = function(){
        console.log('name=' + name);
        printCallback();
    }
}

var widget = new Widget(printCallback);
widget.printName();
// Output:
// name=widget
// name=controller

Answer №2

The most user-friendly ratings system with customizable levels and seamless installation can be found at this link:

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Encountering yet another frustrating issue with z-index not functioning properly in versions of IE above 7, despite extensive research yielding no solution

I have scoured numerous resources and read countless articles on how to resolve z-index issues in IE 7, 8, and 9. However, none of the suggested solutions seem to work for my particular situation. The issue at hand is that I have interactive content posit ...

Display items from two different collections next to each other by utilizing two ng-repeat directives

I have 2 collections: one for names and one for values. How can I display them using bootstrap columns and angularjs, similar to the image provided in the link below? View the image for reference My attempted code is shown below, however, it does not giv ...

Angular2's $compile directive functions similarly to AngularJS 1's $compile directive

I am currently in the process of migrating my project from Angular 1 to Angular 2 and I am in need of a compile directive. However, I am struggling to rewrite it to achieve the same functionality as before. app.directive("compile", compile); compile.$inje ...

Assign the output of an XQuery query to a variable in JavaScript

Hi, I'm facing an issue that involves using XQuery on XML data stored in a variable. Here is an example of the XML structure: <channel> <available>yes</available> <label>CNN</label> </channel> <channel> <a ...

All Material UI components are aligned in a single row, spanning the entire width of the page

These are the components I am currently working with: Sandbox: https://codesandbox.io/s/6ipdf?file=/demo.js:78-129 <FormControl sx={{ m: 1 }} variant="standard"> <InputLabel htmlFor="demo-customized-textbox">Age& ...

Error: Unable to assign value to the innerHTML property of an undefined element created by JavaScript

When designing my html form, I encountered an issue where I needed to display a message beneath blank fields when users did not fill them out. Initially, I used empty spans in the html code to hold potential error messages, which worked well. However, I de ...

There was an issue stating that valLists is not defined when paginating table rows with AngularJS and AJAX

I found a helpful code snippet on this website for implementing pagination in AngularJS. I'm trying to adapt it to work with data from a MySQL DB table called 'user', but I keep running into an issue where the valLists variable is undefined, ...

What is the best way to delay JavaScript execution until after React has finished rendering?

Perhaps this is not the exact question you were expecting, but it did catch your attention :) The issue at hand revolves around the fact that most of the translations in my text do not update when the global language changes. Specifically, the translation ...

Attaching a buoyant div to the precise location of a different element

I have a unordered list (ul) with individual list items (li) that are displayed within a scrollable container. This means that only 8 list items are visible at a time, but you can scroll through them to see the others. Each list item (li) has an "edit" b ...

What is the best method for storing a model in a database?

Hello, I am currently attempting to save a model to the database. I am simply inputting the value of a title in order to save it, as my id is set to auto increment. However, I have encountered an issue where my attempts have been unsuccessful. Can someone ...

Bug Found in AngularJS v1.3.15: Text Color Rendering Glitch on Page Load with WebKit

It appears that the text colors (which should be blue) are not displaying correctly until a user hovers over the text or resizes the window. I attempted to resolve this issue by adjusting the transition property so that it triggers on hover/active states ...

JavaScript toggle display function not functioning properly when clicked

I've been attempting to create a drop-down list using HTML and JavaScript, but for some inexplicable reason, it's just not functioning as expected despite scouring through countless tutorials on YouTube. Below is the snippet of code that I'm ...

Data within object not recognized by TableCell Material UI element

I am currently facing an issue where the content of an object is not being displayed within the Material UI component TableCell. Interestingly, I have used the same approach with the Title component and it shows the content without any problems. function ...

Tips for populating countryList data in Form.Select component within a React.js application

I have a data file that contains a list of all countries, and I need to display these countries in a select input field, similar to what you see on popular websites when a user logs in and edits their profile information like name, address, and country. H ...

Displaying angular ng-messages only when the field has been touched

Nothing too out of the ordinary here. I need my input to be validated with each keystroke. If the validation fails, I want the error message to display right away, without waiting for the blur event to trigger the $touched function. I assumed this was th ...

a guide on expanding a submenu in a shiny dashboard sidebar without using automated functions

I am facing a challenge in manually expanding a submenu within a sidebar on shiny dashboard. The function updateTabItems does not seem to work with nested menus, only with normal menus. For example, when I click on 'Switch tab', it switches the ...

Using jQuery to dynamically add a value to a comment string

Is there a way to dynamically include tomorrow's start and end times in the message for the setupOrderingNotAvailable function if today's end time has passed? The current message states that online ordering will be available again tomorrow from 1 ...

Having difficulty applying capitalization to the initial word in an input using JavaScript/jQuery

I've read through several other discussions on this forum regarding the same issue. My problem lies in capitalizing the first letter of an input. Here is the link to my code on JSFiddle Despite my efforts, I can't seem to get the substr() funct ...

Headers cannot be set after they have already been sent following the establishment of the content-type

Currently, I'm attempting to retrieve an object from my server's API. This particular API presents the object as a stream. To properly handle the MIME type of the object (which typically ends in .jpg or similar), I want to ensure that the conte ...

When utilizing getServerSideProps, the data is provided without triggering a re-render

Although my approach may not align with SSR, my goal is to render all products when a user visits /products. This works perfectly using a simple products.map(...). I also have a category filter set up where clicking on a checkbox routes to /products?catego ...