在Angular中,如何将JSON对象/数组传递到指令中?

52

目前,我的应用程序有一个控制器,它接收一个JSON文件,然后使用“ng-repeat”迭代其中的数据。这一切都很好,但我还有一个指令需要遍历相同的JSON文件。这会带来问题,因为我不能在一页上两次请求相同的JSON文件(也不想这样做,因为这样效率很低)。如果我更改其中一个JSON文件的文件名,指令和控制器都可以成功请求和遍历JSON数据。

我的问题是:如何将控制器的JSON请求生成的数组传递到指令中?当我已经通过控制器访问了它时,如何传递数组并进行遍历?

控制器

appControllers.controller('dummyCtrl', function ($scope, $http) {
   $http.get('locations/locations.json').success(function(data) {
      $scope.locations = data;
   });
});

HTML

<ul class="list">
   <li ng-repeat="location in locations">
      <a href="#">{{location.id}}. {{location.name}}</a>
   </li>
</ul>
<map></map> //executes a js library

指令(当我使用除locations.json以外的文件名时有效,因为我已经请求过它一次)

.directive('map', function($http) {
   return {
     restrict: 'E',
     replace: true,
     template: '<div></div>',
     link: function(scope, element, attrs) {

$http.get('locations/locations.json').success(function(data) {
   angular.forEach(data.locations, function(location, key){
     //do something
   });
});

2
首先,您应该使用服务而不是控制器来获取位置数据。之后,您可以通过依赖注入在控制器和指令中从服务中获取数据。 - Rodrigo Fonseca
3个回答

91

如果你想遵循所有的“最佳实践”,我建议你做一些事情,其中有些已经在其他答案和评论中提到了。


首先,虽然这不会对你所问的具体问题产生太大影响,但你提到了效率,处理应用程序中的共享数据的最佳方式是将其拆分为一个服务。

个人建议采用AngularJS的promise系统,这将使您的异步服务与原始回调相比更具可组合性。幸运的是,Angular的$http服务已经在内部使用它们。以下是一个服务,它将返回一个承诺来解析JSON文件中的数据;多次调用该服务不会导致第二个HTTP请求。

app.factory('locations', function($http) {
  var promise = null;

  return function() {
    if (promise) {
      // If we've already asked for this data once,
      // return the promise that already exists.
      return promise;
    } else {
      promise = $http.get('locations/locations.json');
      return promise;
    }
  };
});

关于将数据传递给指令,重要的是要记住,指令旨在抽象化通用DOM操作; 你不应该向它们注入应用程序特定的服务。在这种情况下,很容易将locations服务简单地注入到指令中,但这会将指令与该服务耦合起来。

有关代码模块化的简短说明:指令的函数几乎永远不应该负责获取或格式化自己的数据。你可以使用$http服务在指令内部,但这几乎总是错误的做法。编写控制器使用 $http 是正确的方法。指令已经触及一个 DOM 元素,这是一个非常复杂的对象,很难进行测试。将网络 I/O 添加到其中会使您的代码更加难以理解和测试。此外,网络 I/O 锁定了指令获取其数据的方式——也许在其他地方,您希望该指令从套接字接收数据或接收预加载数据。您的指令应该通过 scope.$eval 将数据作为属性传递进去,和/或者拥有一个控制器来处理获取和存储数据。

- 编写AngularJS指令的80/20指南

在这种特定情况下,应该将适当的数据放置在控制器的作用域中,并通过属性与指令共享。

app.controller('SomeController', function($scope, locations) {
  locations().success(function(data) {
    $scope.locations = data;
  });
});
<ul class="list">
   <li ng-repeat="location in locations">
      <a href="#">{{location.id}}. {{location.name}}</a>
   </li>
</ul>
<map locations='locations'></map>
app.directive('map', function() {
  return {
    restrict: 'E',
    replace: true,
    template: '<div></div>',
    scope: {
      // creates a scope variable in your directive
      // called `locations` bound to whatever was passed
      // in via the `locations` attribute in the DOM
      locations: '=locations'
    },
    link: function(scope, element, attrs) {
      scope.$watch('locations', function(locations) {
        angular.forEach(locations, function(location, key) {
          // do something
        });
      });
    }
  };
});

通过这种方式,map指令可以与任何位置数据集一起使用——该指令没有硬编码为使用特定的数据集,并且仅通过将其包含在DOM中链接指令不会触发随机HTTP请求。


没问题。你的“watch”问题听起来有点奇怪;如果你感兴趣,我很乐意帮你看看——你可以在我的SO个人资料中找到我的联系方式。 - Michelle Tilley
如果您的指令不是“SomeController”的“子级”,那该怎么办?那么您该如何传递数据呢?我在页面的不同部分重复使用相同的指令,并且希望可以从任何地方操作数据。 - Kevin Cloet
首先,@BinaryMuse,你的回复非常有帮助。谢谢!我想知道你是否能够举一个更具体的使用案例来详细说明最后一部分。我试图理解为什么有人会使用map指令。只是试图在实际示例中描绘这个特定的指令。(希望这样说得清楚!) - James Barracca
@JamesBarracca 正如我在答案中提到的,指令可以抽象出DOM的创建和操作。你不希望你的控制器或服务去想如何与Leaflet API交互、创建/附加DOM节点等等。你希望它们处理数据,并将原始数据发送给指令,以便根据该数据将其转换为有用的DOM。 - Michelle Tilley
1
我注意到这个线程的第一个评论讨论了使用$watch。由于这个问题现在比较老,Angular 1.5.3通过为新的module.component()提供4个事件监听器来解决了这个问题,它只是一个被限制为元素的指令。欲了解更多信息,请查看-https://blog.thoughtram.io/angularjs/2016/03/29/exploring-angular-1.5-lifecycle-hooks.html - Matt
显示剩余5条评论

11

如你所说,你不需要请求两次文件。将其从控制器传递到指令中即可。假设你在控制器的作用域内使用指令:

.controller('MyController', ['$scope', '$http', function($scope, $http) {
  $http.get('locations/locations.json').success(function(data) {
      $scope.locations = data;
  });
}

然后在您的 HTML 中(调用指令的位置)。
注意:locations是对控制器$scope.locations的引用。

<div my-directive location-data="locations"></div>

最后,在您的指令中

...
scope: {
  locationData: '=locationData'
},
controller: ['$scope', function($scope){
  // And here you can access your data
  $scope.locationData
}]
...

这只是一个指引,方向正确即可,因此它不完整且未经过测试。


使用控制器来重用并从服务器获取数据?你真的认为这是控制器的工作吗? - Rodrigo Fonseca
我将控制器和指令分开使用,通过您的建议,我仍然可以实现我想要的吗? - Omegalen
我也会单独使用控制器而不是指令(是的,有特定情况需要这样做),但当您想在应用程序的其他几个位置使用相同数据时,这没有任何意义。 - Rodrigo Fonseca
@Omegalen 我有点困惑问题出在哪里。如果你要在指令内操作数据,你需要一个指令作用域控制器或链接函数。但是,如果你的意思是将指令应用于原始控制器(例如我的示例中的“MyController”)之外的范围,那么你应该创建一个服务来在整个应用程序中共享数据。如果是这种情况,请告诉我。 - Index
@KGChristensen 后者是正确的。我将指令应用于原始控制器之外。所以在这种情况下,我应该使用一个服务吗? - Omegalen
@Omegalen 是的,你最好使用一个服务来在你的应用程序中共享数据。从那里开始,只需要将你的服务注入到控制器/指令/其他组件中,并请求你的数据即可。只要正确设置了你的服务,它就应该只获取一次数据并将其存储以供后续使用。 - Index

5

您需要的是一个正确的服务:

.factory('DataLayer', ['$http',

    function($http) {

        var factory = {};
        var locations;

        factory.getLocations = function(success) {
            if(locations){
                success(locations);
                return;
            }
            $http.get('locations/locations.json').success(function(data) {
                locations = data;
                success(locations);
            });
        };

        return factory;
    }
]);

locations缓存在作为单例模式的服务中。这是获取数据的正确方式。

在控制器和指令中使用此服务DataLayer即可,示例如下:

appControllers.controller('dummyCtrl', function ($scope, DataLayer) {
    DataLayer.getLocations(function(data){
        $scope.locations = data;
    });
});

.directive('map', function(DataLayer) {
    return {
        restrict: 'E',
        replace: true,
        template: '<div></div>',
        link: function(scope, element, attrs) {

            DataLayer.getLocations(function(data) {
                angular.forEach(data, function(location, key){
                    //do something
                });
            });
        }
    };
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接