321

I am wondering if there is a way (similar to Gmail) for AngularJS to delay showing a new route until after each model and its data has been fetched using its respective services.

For example, if there were a ProjectsController that listed all Projects and project_index.html which was the template that showed these Projects, Project.query() would be fetched completely before showing the new page.

Until then, the old page would still continue to show (for example, if I were browsing another page and then decided to see this Project index).

4

13 回答 13

375

$routeProvider resolve property allows delaying of route change until data is loaded.

First define a route with resolve attribute like this.

angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']).
  config(['$routeProvider', function($routeProvider) {
    $routeProvider.
      when('/phones', {
        templateUrl: 'partials/phone-list.html', 
        controller: PhoneListCtrl, 
        resolve: PhoneListCtrl.resolve}).
      when('/phones/:phoneId', {
        templateUrl: 'partials/phone-detail.html', 
        controller: PhoneDetailCtrl, 
        resolve: PhoneDetailCtrl.resolve}).
      otherwise({redirectTo: '/phones'});
}]);

notice that the resolve property is defined on route.

function PhoneListCtrl($scope, phones) {
  $scope.phones = phones;
  $scope.orderProp = 'age';
}

PhoneListCtrl.resolve = {
  phones: function(Phone, $q) {
    // see: https://groups.google.com/forum/?fromgroups=#!topic/angular/DGf7yyD4Oc4
    var deferred = $q.defer();
    Phone.query(function(successData) {
            deferred.resolve(successData); 
    }, function(errorData) {
            deferred.reject(); // you could optionally pass error data here
    });
    return deferred.promise;
  },
  delay: function($q, $defer) {
    var delay = $q.defer();
    $defer(delay.resolve, 1000);
    return delay.promise;
  }
}

Notice that the controller definition contains a resolve object which declares things which should be available to the controller constructor. Here the phones is injected into the controller and it is defined in the resolve property.

The resolve.phones function is responsible for returning a promise. All of the promises are collected and the route change is delayed until after all of the promises are resolved.

Working demo: http://mhevery.github.com/angular-phonecat/app/#/phones Source: https://github.com/mhevery/angular-phonecat/commit/ba33d3ec2d01b70eb5d3d531619bf90153496831

于 2012-08-15T15:12:59.057 回答
51

这是一个适用于 Angular 1.0.2 的最小工作示例

模板:

<script type="text/ng-template" id="/editor-tpl.html">
    Editor Template {{datasets}}
</script>

<div ng-view>

</div>

JavaScript:

function MyCtrl($scope, datasets) {    
    $scope.datasets = datasets;
}

MyCtrl.resolve = {
    datasets : function($q, $http) {
        var deferred = $q.defer();

        $http({method: 'GET', url: '/someUrl'})
            .success(function(data) {
                deferred.resolve(data)
            })
            .error(function(data){
                //actually you'd want deffered.reject(data) here
                //but to show what would happen on success..
                deferred.resolve("error value");
            });

        return deferred.promise;
    }
};

var myApp = angular.module('myApp', [], function($routeProvider) {
    $routeProvider.when('/', {
        templateUrl: '/editor-tpl.html',
        controller: MyCtrl,
        resolve: MyCtrl.resolve
    });
});​
​

http://jsfiddle.net/dTJ9N/3/

精简版:

由于 $http() 已经返回了一个 promise(又名 deferred),我们实际上不需要创建我们自己的。所以我们可以简化MyCtrl。决心:

MyCtrl.resolve = {
    datasets : function($http) {
        return $http({
            method: 'GET', 
            url: 'http://fiddle.jshell.net/'
        });
    }
};

$http() 的结果包含datastatusheadersconfig对象,所以我们需要将 MyCtrl 的 body 更改为:

$scope.datasets = datasets.data;

http://jsfiddle.net/dTJ9N/5/

于 2012-09-10T23:43:56.440 回答
32

我看到有人问如何使用 angular.controller 方法和缩小友好的依赖注入来做到这一点。由于我刚刚开始这项工作,我觉得有必要回来提供帮助。这是我的解决方案(取自原始问题和米斯科的回答):

angular.module('phonecat', ['phonecatFilters', 'phonecatServices', 'phonecatDirectives']).
  config(['$routeProvider', function($routeProvider) {
    $routeProvider.
      when('/phones', {
        templateUrl: 'partials/phone-list.html', 
        controller: PhoneListCtrl, 
        resolve: { 
            phones: ["Phone", "$q", function(Phone, $q) {
                var deferred = $q.defer();
                Phone.query(function(successData) {
                  deferred.resolve(successData); 
                }, function(errorData) {
                  deferred.reject(); // you could optionally pass error data here
                });
                return deferred.promise;
             ]
            },
            delay: ["$q","$defer", function($q, $defer) {
               var delay = $q.defer();
               $defer(delay.resolve, 1000);
               return delay.promise;
              }
            ]
        },

        }).
      when('/phones/:phoneId', {
        templateUrl: 'partials/phone-detail.html', 
        controller: PhoneDetailCtrl, 
        resolve: PhoneDetailCtrl.resolve}).
      otherwise({redirectTo: '/phones'});
}]);

angular.controller("PhoneListCtrl", [ "$scope", "phones", ($scope, phones) {
  $scope.phones = phones;
  $scope.orderProp = 'age';
}]);

由于此代码源自问题/最流行的答案,因此未经测试,但如果您已经了解如何制作缩小友好的角度代码,它应该会向您发送正确的方向。我自己的代码不需要的一个部分是将“电话”注入到“电话”的解析函数中,我也根本没有使用任何“延迟”对象。

我还推荐这个 youtube 视频http://www.youtube.com/watch?v=P6KITGRQujQ&list=UUKW92i7iQFuNILqQOUOCrFw&index=4&feature=plcp,这对我帮助很大

如果你感兴趣,我决定也粘贴我自己的代码(用咖啡脚本编写),这样你就可以看到我是如何让它工作的。

仅供参考,我提前使用了一个通用控制器来帮助我在几个模型上进行 CRUD:

appModule.config ['$routeProvider', ($routeProvider) ->
  genericControllers = ["boards","teachers","classrooms","students"]
  for controllerName in genericControllers
    $routeProvider
      .when "/#{controllerName}/",
        action: 'confirmLogin'
        controller: 'GenericController'
        controllerName: controllerName
        templateUrl: "/static/templates/#{controllerName}.html"
        resolve:
          items : ["$q", "$route", "$http", ($q, $route, $http) ->
             deferred = $q.defer()
             controllerName = $route.current.controllerName
             $http(
               method: "GET"
               url: "/api/#{controllerName}/"
             )
             .success (response) ->
               deferred.resolve(response.payload)
             .error (response) ->
               deferred.reject(response.message)

             return deferred.promise
          ]

  $routeProvider
    .otherwise
      redirectTo: '/'
      action: 'checkStatus'
]

appModule.controller "GenericController", ["$scope", "$route", "$http", "$cookies", "items", ($scope, $route, $http, $cookies, items) ->

  $scope.items = items
      #etc ....
    ]
于 2013-03-14T19:11:38.267 回答
18

此提交是 1.1.5 及更高版本的一部分,它公开$promise$resource. 包含此提交的 ngResource 版本允许解析如下资源:

$routeProvider

resolve: {
    data: function(Resource) {
        return Resource.get().$promise;
    }
}

控制器

app.controller('ResourceCtrl', ['$scope', 'data', function($scope, data) {

    $scope.data = data;

}]);
于 2013-05-26T12:06:31.400 回答
16

这个片段是依赖注入友好的(我什至将它结合使用ngminuglify),它是一个更优雅的基于域驱动的解决方案。

下面的示例注册了一个电话 资源和一个常量 phoneRoutes,其中包含该(电话)域的所有路由信息。在提供的答案中我不喜欢的是解析逻辑的位置——模块不应该知道任何事情,或者对资源参数提供给控制器的方式感到困扰。这样逻辑就保持在同一个域中。

注意:如果您使用的是ngmin(如果您不使用:您应该),您只需要使用 DI 数组约定编写解析函数。

angular.module('myApp').factory('Phone',function ($resource) {
  return $resource('/api/phone/:id', {id: '@id'});
}).constant('phoneRoutes', {
    '/phone': {
      templateUrl: 'app/phone/index.tmpl.html',
      controller: 'PhoneIndexController'
    },
    '/phone/create': {
      templateUrl: 'app/phone/edit.tmpl.html',
      controller: 'PhoneEditController',
      resolve: {
        phone: ['$route', 'Phone', function ($route, Phone) {
          return new Phone();
        }]
      }
    },
    '/phone/edit/:id': {
      templateUrl: 'app/phone/edit.tmpl.html',
      controller: 'PhoneEditController',
      resolve: {
        form: ['$route', 'Phone', function ($route, Phone) {
          return Phone.get({ id: $route.current.params.id }).$promise;
        }]
      }
    }
  });

下一部分是在模块处于配置状态时注入路由数据并将其应用于$routeProvider

angular.module('myApp').config(function ($routeProvider, 
                                         phoneRoutes, 
                                         /* ... otherRoutes ... */) {

  $routeProvider.when('/', { templateUrl: 'app/main/index.tmpl.html' });

  // Loop through all paths provided by the injected route data.

  angular.forEach(phoneRoutes, function(routeData, path) {
    $routeProvider.when(path, routeData);
  });

  $routeProvider.otherwise({ redirectTo: '/' });

});

使用此设置测试路由配置也非常简单:

describe('phoneRoutes', function() {

  it('should match route configuration', function() {

    module('myApp');

    // Mock the Phone resource
    function PhoneMock() {}
    PhoneMock.get = function() { return {}; };

    module(function($provide) {
      $provide.value('Phone', FormMock);
    });

    inject(function($route, $location, $rootScope, phoneRoutes) {
      angular.forEach(phoneRoutes, function (routeData, path) {

        $location.path(path);
        $rootScope.$digest();

        expect($route.current.templateUrl).toBe(routeData.templateUrl);
        expect($route.current.controller).toBe(routeData.controller);
      });
    });
  });
});

你可以在我最新的(即将到来的)实验中看到它的全部荣耀。虽然这种方法对我来说很好,但我真的很想知道为什么 $injector在检测到任何承诺对象的注入时没有延迟任何东西的构造;它会让事情变得更容易。

编辑:使用 Angular v1.2(rc2)

于 2013-10-06T21:01:13.670 回答
11

延迟显示路线肯定会导致异步纠缠......为什么不简单地跟踪主实体的加载状态并在视图中使用它。例如,在您的控制器中,您可能会在 ngResource 上同时使用成功和错误回调:

$scope.httpStatus = 0; // in progress
$scope.projects = $resource.query('/projects', function() {
    $scope.httpStatus = 200;
  }, function(response) {
    $scope.httpStatus = response.status;
  });

然后在视图中你可以做任何事情:

<div ng-show="httpStatus == 0">
    Loading
</div>
<div ng-show="httpStatus == 200">
    Real stuff
    <div ng-repeat="project in projects">
         ...
    </div>
</div>
<div ng-show="httpStatus >= 400">
    Error, not found, etc. Could distinguish 4xx not found from 
    5xx server error even.
</div>
于 2012-09-03T17:59:53.767 回答
8

我在上面的 Misko 代码中工作,这就是我所做的。这是一个更新的解决方案,因为$defer已更改为$timeout. 然而,替换$timeout将等待超时时间(在 Misko 的代码中,1 秒),然后返回数据,希望它及时解决。通过这种方式,它会尽快返回。

function PhoneListCtrl($scope, phones) {
  $scope.phones = phones;
  $scope.orderProp = 'age';
}

PhoneListCtrl.resolve = {

  phones: function($q, Phone) {
    var deferred = $q.defer();

    Phone.query(function(phones) {
        deferred.resolve(phones);
    });

    return deferred.promise;
  }
}
于 2012-09-07T05:04:46.757 回答
7

使用 AngularJS 1.1.5

使用AngularJS 1.1.5语法更新 Justen 回答中的“电话”功能。

原来的:

phones: function($q, Phone) {
    var deferred = $q.defer();

    Phone.query(function(phones) {
        deferred.resolve(phones);
    });

    return deferred.promise;
}

更新:

phones: function(Phone) {
    return Phone.query().$promise;
}

多亏了 Angular 团队和贡献者,时间缩短了。:)

这也是马克西米利安霍夫曼的答案。显然,该提交进入了 1.1.5。

于 2013-07-28T07:03:39.200 回答
5

您可以使用$routeProvider 解析属性来延迟路由更改,直到加载数据。

angular.module('app', ['ngRoute']).
  config(['$routeProvider', function($routeProvider, EntitiesCtrlResolve, EntityCtrlResolve) {
    $routeProvider.
      when('/entities', {
        templateUrl: 'entities.html', 
        controller: 'EntitiesCtrl', 
        resolve: EntitiesCtrlResolve
      }).
      when('/entity/:entityId', {
        templateUrl: 'entity.html', 
        controller: 'EntityCtrl', 
        resolve: EntityCtrlResolve
      }).
      otherwise({redirectTo: '/entities'});
}]);

请注意,该resolve属性是在路由上定义的。

EntitiesCtrlResolve并且EntityCtrlResolve是在与控制器相同的文件中定义的常量对象。EntitiesCtrlEntityCtrl

// EntitiesCtrl.js

angular.module('app').constant('EntitiesCtrlResolve', {
  Entities: function(EntitiesService) {
    return EntitiesService.getAll();
  }
});

angular.module('app').controller('EntitiesCtrl', function(Entities) {
  $scope.entities = Entities;

  // some code..
});

// EntityCtrl.js

angular.module('app').constant('EntityCtrlResolve', {
  Entity: function($route, EntitiesService) {
    return EntitiesService.getById($route.current.params.projectId);
  }
});

angular.module('app').controller('EntityCtrl', function(Entity) {
  $scope.entity = Entity;

  // some code..
});
于 2014-11-14T12:04:21.357 回答
3

我喜欢 darkporter 的想法,因为对于一个刚接触 AngularJS 的开发团队来说,它很容易理解并立即工作。

我创建了这个适配,它使用 2 个 div,一个用于加载栏,另一个用于加载数据后显示的实际内容。错误处理将在其他地方进行。

向 $scope 添加一个“就绪”标志:

$http({method: 'GET', url: '...'}).
    success(function(data, status, headers, config) {
        $scope.dataForView = data;      
        $scope.ready = true;  // <-- set true after loaded
    })
});

在 html 视图中:

<div ng-show="!ready">

    <!-- Show loading graphic, e.g. Twitter Boostrap progress bar -->
    <div class="progress progress-striped active">
        <div class="bar" style="width: 100%;"></div>
    </div>

</div>

<div ng-show="ready">

    <!-- Real content goes here and will appear after loading -->

</div>

另请参阅:Boostrap 进度条文档

于 2013-02-27T18:51:03.957 回答
1

我喜欢上面的答案并从他们那里学到了很多东西,但是上面的大多数答案都缺少一些东西。

我被困在一个类似的场景中,我正在使用从服务器的第一个请求中获取的一些数据来解析 url。我面临的问题是,如果承诺是rejected.

我正在使用一个自定义提供程序,该提供程序用于返回一个在配置阶段Promiseresolveof解决的。$routeProvider

我想在这里强调的是when它的概念是这样的。

它在 url 栏中看到 url,然后when在被调用的控制器中看到相应的块,并且视图到目前为止被引用得很好。

可以说我有以下配置阶段代码。

App.when('/', {
   templateUrl: '/assets/campaigns/index.html',
   controller: 'CampaignListCtr',
   resolve : {
      Auth : function(){
         return AuthServiceProvider.auth('campaign');
      }
   }
})
// Default route
.otherwise({
   redirectTo: '/segments'
});

在浏览器的根 url 上,第一个运行块被调用,否则otherwise被调用。

让我们想象一下我在地址栏中点击 rootUrlAuthServicePrivider.auth()函数被调用的场景。

假设返回的 Promise 处于拒绝状态 ,然后呢???

根本没有渲染任何东西。

Otherwise块将不会被执行,因为它对于任何未在配置块中定义且 angularJs 配置阶段未知的 url。

当这个promise没有解决时,我们将不得不处理被触发的事件。失败$routeChangeErorr时被解雇$rootScope

它可以被捕获,如下面的代码所示。

$rootScope.$on('$routeChangeError', function(event, current, previous, rejection){
    // Use params in redirection logic.
    // event is the routeChangeEvent
    // current is the current url
    // previous is the previous url
    $location.path($rootScope.rootPath);
});

IMO 将事件跟踪代码放在应用程序的运行块中通常是一个好主意。此代码在应用程序的配置阶段之后运行。

App.run(['$routeParams', '$rootScope', '$location', function($routeParams, $rootScope, $location){
   $rootScope.rootPath = "my custom path";
   // Event to listen to all the routeChangeErrors raised
   // by the resolve in config part of application
   $rootScope.$on('$routeChangeError', function(event, current, previous, rejection){
       // I am redirecting to rootPath I have set above.
       $location.path($rootScope.rootPath);
   });
}]);

这样我们就可以在配置阶段处理 Promise 失败。

于 2015-01-12T10:39:34.717 回答
0

我有一个复杂的多级滑动面板界面,禁用屏幕层。在禁用屏幕层上创建指令,该指令将创建单击事件以执行状态,例如

$state.go('account.stream.social.view');

正在产生闪烁的效果。history.back() 而不是它工作正常,但在我的情况下它并不总是回到历史。所以我发现如果我只是在我的禁用屏幕上创建属性 href 而不是 state.go ,就像一个魅力。

<a class="disable-screen" back></a>

指令“返回”

app.directive('back', [ '$rootScope', function($rootScope) {

    return {
        restrict : 'A',
        link : function(scope, element, attrs) {
            element.attr('href', $rootScope.previousState.replace(/\./gi, '/'));
        }
    };

} ]);

app.js 我只是保存以前的状态

app.run(function($rootScope, $state) {      

    $rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams) {         

        $rootScope.previousState = fromState.name;
        $rootScope.currentState = toState.name;


    });
});
于 2016-01-08T01:51:23.603 回答
-2

一种可能的解决方案可能是将 ng-cloak 指令与我们使用模型的元素一起使用,例如

<div ng-cloak="">
  Value in  myModel is: {{myModel}}
</div>

我认为这需要最少的努力。

于 2014-10-03T18:08:34.100 回答