Show loading animation for slowscript using AngularJS?

前端 未结 8 505
野的像风
野的像风 2020-12-24 12:08

In angularjs 1.2 operations like filtering an ng-repeat with many rows (>2,000 rows) can become quite slow (>1 sec).
I know I can optimize execution times u

相关标签:
8条回答
  • 2020-12-24 12:30

    What you can do is detect the end of the ngRepeat as this post says.

    I'll do something like, in the controller:

    $scope.READY = false;
    

    And in the directive, as the post above says, I'll do something like:

    if (scope.$last) {
        $scope.READY = true;
    }
    

    And you can have a css based loader/spinner with

    <div class="loader" ng-show="!READY">
    </div>
    

    Ofcourse you can also have css based animations which are independent of js execution.

    0 讨论(0)
  • 2020-12-24 12:34

    Use spin.js and the site http://fgnass.github.com/spin.js/ shows the step which is quite easy. the loading animation is in CSS which separated from the UI thread and therefore loaded smoothly.

    0 讨论(0)
  • 2020-12-24 12:36

    The problem is that as long as Javascript is executing, the UI gets no chance to update. Even if you present a spinner before filtering, it will appear "frozen" as long as Angular is busy.

    A way to overcome this is to filter in chunks and, if more data are available, filter again after a small $timeout. The timeout gives the UI thread a chance to run and display changes and animations.

    A fiddle demonstrating the principle is here.

    It does not use Angular's filters (they are synchronous). Instead it filters the data array with the following function:

    function filter() {
        var i=0, filtered = [];
        innerFilter();
    
        function innerFilter() {
            var counter;
            for( counter=0; i < $scope.data.length && counter < 5; counter++, i++ ) {
                /////////////////////////////////////////////////////////
                // REAL FILTER LOGIC; BETTER SPLIT TO ANOTHER FUNCTION //
                if( $scope.data[i].indexOf($scope.filter) >= 0 ) {
                    filtered.push($scope.data[i]);
                }
                /////////////////////////////////////////////////////////
            }
            if( i === $scope.data.length ) {
                $scope.filteredData = filtered;
                $scope.filtering = false;
            }
            else {
                $timeout(innerFilter, 10);
            }
        }
    }
    

    It requires 2 support variables: $scope.filtering is true when the filter is active, giving us the chance to display the spinner and disable the input; $scope.filteredData receives the result of the filter.

    There are 3 hidden parameters:

    • the chunk size (counter < 5) is small on purpose to demonstrate the effect
    • the timeout delay ($timeout(innerFilter, 10)) should be small, but enough to give the UI thread some time to be responsive
    • the actual filter logic, which should probably be a callback in real life scenarios.

    This is only a proof of concept; I would suggest refactoring it (to a directive probably) for real use.

    0 讨论(0)
  • 2020-12-24 12:37

    Promise/deferred can be used in this case, you can call notify to watch the progress of your code, documentation from angular.js: https://code.angularjs.org/1.2.16/docs/api/ng/service/$q Here is a tutorial on heavy JS processing that uses ng-Promise: http://liamkaufman.com/blog/2013/09/09/using-angularjs-promises/, hope it is helpful.

    //app Factory for holding the data model

    app.factory('postFactory', function ($http, $q, $timeout){

    var  factory = {
    
        posts : false,
    
        getPosts : function(){
    
            var deferred = $q.defer();
    
            //avoiding the http.get request each time 
            //we call the getPosts function
    
            if (factory.posts !== false){
                deferred.resolve(factory.posts);
    
            }else{
    
                $http.get('posts.json')
                .success(function(data, status){
                    factory.posts = data
    
                    //to show the loading !
                    $timeout(function(){
                        deferred.resolve(factory.posts)
                    }, 2000);
    
                })
                .error(function(data, status){
                    deferred.error('Cant get the posts !')
                })
    
            };  
    
            return deferred.promise;
        },
    
        getPost : function(id){
    
            //promise
            var deferred = $q.defer();
    
            var post = {};
            var posts = factory.getPosts().then(function(posts){
                post = factory.posts[id];
    
                //send the post if promise kept
                deferred.resolve(post);
    
            }, function(msg){
                deferred.reject(msg);
            })
    
            return deferred.promise;
        },
    
    };
    return factory;
    

    });

    0 讨论(0)
  • 2020-12-24 12:40

    Here are the steps:

    1. First, you should use CSS animations. No JS driven animations and GIFs should be used within heavy processes bec. of the single thread limit. The animation will freeze. CSS animations are separated from the UI thread and they are supported on IE 10+ and all major browsers.
    2. Write a directive and place it outside of your ng-view with fixed positioning.
    3. Bind it to your app controller with some special flag.
    4. Toggle this directive's visibility before and after long/heavy processes. (You can even bind a text message to the directive to display some useful info to the user). -- Interacting with this or anything else directly within a loop of heavy process will take way longer time to finish. That's bad for the user!

    Directive Template:

    <div class="activity-box" ng-show="!!message">
        <img src="img/load.png" width="40" height="40" />
        <span>{{ message }}</span>
    </div>
    

    activity Directive:

    A simple directive with a single attribute message. Note the ng-show directive in the template above. The message is used both to toggle the activity indicator and also to set the info text for the user.

    app.directive('activity', [
        function () {
            return {
                restrict: 'EA',
                templateUrl: '/templates/activity.html',
                replace: true,
                scope: {
                    message: '@'
                },
                link: function (scope, element, attrs) {}
            };
        }
    ]);
    

    SPA HTML:

    <body ng-controller="appController">
        <div ng-view id="content-view">...</div>
        <div activity message="{{ activityMessage }}"></div>
    </body>
    

    Note that the activity directive placed outside of ng-view. It will be available on each section of your single-page-app.

    APP Controller:

    app.controller('appController',
        function ($scope, $timeout) {
            // You would better place these two methods below, inside a 
            // service or factory; so you can inject that service anywhere
            // within the app and toggle the activity indicator on/off where needed
            $scope.showActivity = function (msg) {
                $timeout(function () {
                    $scope.activityMessage = msg;
                });
            };
            $scope.hideActivity = function () {
                $timeout(function () {
                    $scope.activityMessage = '';
                }, 1000); // message will be visible at least 1 sec
            };
            // So here is how we do it:
            // "Before" the process, we set the message and the activity indicator is shown
            $scope.showActivity('Loading items...');
            var i;
            for (i = 0; i < 10000; i += 1) {
                // here goes some heavy process
            }
            // "After" the process completes, we hide the activity indicator.
            $scope.hideActivity();
        });
    

    Of course, you can use this in other places too. e.g. you can call $scope.hideActivity(); when a promise resolves. Or toggling the activity on request and response of the httpInterceptor is a good idea too.

    Example CSS:

    .activity-box {
        display: block;
        position: fixed; /* fixed position so it doesn't scroll */
        z-index: 9999; /* on top of everything else */
        width: 250px;
        margin-left: -125px; /* horizontally centered */
        left: 50%;
        top: 10px; /* displayed on the top of the page, just like Gmail's yellow info-box */
        height: 40px;
        padding: 10px;
        background-color: #f3e9b5;
        border-radius: 4px;
    }
    
    /* styles for the activity text */
    .activity-box span {
        display: block;
        position: relative;
        margin-left: 60px;
        margin-top: 10px;
        font-family: arial;
        font-size: 15px;
    }
    
    /* animating a static image */
    .activity-box img {
        display: block;
        position: absolute;
        width: 40px;
        height: 40px;
        /* Below is the key for the rotating animation */
        -webkit-animation: spin 1s infinite linear;
        -moz-animation: spin 1s infinite linear;
        -o-animation: spin 1s infinite linear;
        animation: spin 1s infinite linear;
    }
    
    /* keyframe animation defined for various browsers */
    @-moz-keyframes spin {
        0% { -moz-transform: rotate(0deg); }
        100% { -moz-transform: rotate(359deg); }
    }
    @-webkit-keyframes spin {
        0% { -webkit-transform: rotate(0deg); }
        100% { -webkit-transform: rotate(359deg); }
    }
    @-o-keyframes spin {
        0% { -o-transform: rotate(0deg); }
        100% { -o-transform: rotate(359deg); }
    }
    @-ms-keyframes spin {
        0% { -ms-transform: rotate(0deg); }
        100% { -ms-transform: rotate(359deg); }
    }
    @keyframes spin {
        0% { transform: rotate(0deg); }
        100% { transform: rotate(359deg); }
    }
    

    Hope this helps.

    0 讨论(0)
  • 2020-12-24 12:44

    Here is an working example :-

    angular
      .module("APP", [])
      .controller("myCtrl", function ($scope, $timeout) {
        var mc = this
        
        mc.loading = true
        mc.listRendered = []
        mc.listByCategory = []
        mc.categories = ["law", "science", "chemistry", "physics"]
        
        
        mc.filterByCategory = function (category) {
          mc.loading = true
          // This timeout will start on the next event loop so 
          // filterByCategory function will exit just triggering 
          // the show of Loading... status
    
          // When the function inside timeout is called, it will
          // filter and set the model values and when finished call 
          // an inbuilt $digest at the end.
          $timeout(function () {
            mc.listByCategory = mc.listFetched.filter(function (ele) {
              return ele.category === category          
            })
            mc.listRendered = mc.listByCategory
            $scope.$emit("loaded")
          }, 50)
        }
        
        // This timeout is replicating the data fetch from a server
        $timeout(function () {
          mc.listFetched = makeList()
          mc.listRendered = mc.listFetched
          mc.loading = false    
        }, 50)
        
        $scope.$on("loaded", function () { mc.loading = false })
      })
      
      function makeList() {
        var list = [
          {name: "book1", category: "law"},
          {name: "book2", category: "science"},
          {name: "book1", category: "chemistry"},
          {name: "book1", category: "physics"}      
        ]
        var bigList = []
        for (var i = 0; i <= 5000; i++) {
          bigList = bigList.concat(list)
        }
        return bigList
      }
    button {
      display: inline-block;      
    }
    <html>
      <head>
        <title>This is an Angular Js Filter Workaround!!</title>
      </head>
       
      <body ng-app="APP">
        
        <div ng-controller="myCtrl as mc">
          <div class = "buttons">
            <label>Select Category:- </label>
            <button ng-repeat="category in mc.categories" ng-click="mc.filterByCategory(category)">{{category}}</button>
          </div>
          
          <h1 ng-if="mc.loading">Loading...</h1>
          
          <ul ng-if="!mc.loading">
            <li ng-repeat="ele in mc.listRendered track by $index">{{ele.name}} - {{ele.category}}</li>
          </ul>
        </div>
        
        <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
        
      </body>
      
    <html>

    0 讨论(0)
提交回复
热议问题