ng中指令间的通信


本文又是一学习的笔记,亦是翻译贴,有兴趣的请移驾原文。
Communication Between Directives in AngularJS

同级指令之间的通信

预备知识

在读本文前,你至少需要对以下几个知识点有些了解:

  • Directive
  • Scope
  • Controller
  • 往控制器中注入service
  • ng-show | ng-hide
  • ng-click
  • ng-model -ng-repeat

Roadmap

  1. 创建第一个指令,分析它的每一行代码
  2. 通过scope对象将指令链到控制器上
  3. 创建第二个指令,把它和前者连一起

创建并分析第一个指令

我们创建的第一个指令要做的事情是:一个搜索的输入框。我们在 app.js 文件夹中定义最初的版本,如下:

// app.js
angular.module('myApp', []).
    directive('mySearchBox', function() {
        return {
            restrict: 'E',
            replace: true,
            template: '<span>My custom search box</span>'
        };
    });

上面的代码应该比较直观,不懂的只好回去恶补一下了,就不像原文那样一行行讲了,有兴趣的可以看原文了。其中有一点就是 template 可以用 templateUrl 替代,并且博主也建议最好是从 CDN 或者一个快速的cache去取的,而不是很慢的server端产生的partial。那好,现在,我们就可以在 index.html 文件中启动angular并用上面自定义的指令了。

<html ng-app="myApp">
  <head>
    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.3/angular.min.js"></script>
    <script src="app.js"></script>
  </head>
  <body> 
      <my-search-box></my-search-box>
  </body>
</html>

上代码唯一要注意的就是在 app.js 中定义的指令是驼峰写法,在html文件中要换成dash连接的写法。当然啦,到目前为止,这个指令啥用没有。

给你的指令属于它自己的的私人空间

所谓私人空间,就是给指令自己的scope。每个指令的DOM实例对象都有自己的scope,而这个scope可能是外面控制器scope的子级scope,比如像下面的例子,两个自定义指令产生了两个scope,都是控制器myCtrl产生的scope的孩子。

<body ng-controller="myCtrl"> 
    <my-search-box></my-search-box>
    <my-search-box></my-search-box>
</body>

Q:现在,如何让指令和外部通信呢?
A:通过指令定义时加入一个 scope 属性。

// app.js
angular.module('myApp', []).
    directive('mySearchBox', function() {
        return {
            restrict: 'E',
            replace: true,
            scope: {
                searchText: '=',
                isSearching: '='
            },
            template: '<span>My custom search box</span>'
        };
    });

上面 scope 属性被置为一个对象,而这个对象里面又有两个属性,这两个属性告诉angular,但凡用到这个指令的,可以在指令标签中使用这两个属性,而且这两个属性与它们被赋值的变量进行了双向绑定,也就是说,用户可以像下面代码一样用上面这个指令:

<my-search-box
    search-text="someScopeVar" 
    is-searching="someOhterScopeVar">
</my-search-box>

上面呢,就是你如何让指令和外部通信的一个方法了,而且非常好用的。上面所看到的两个外部的变量 someScopeVarsomeOtherScopeVar 常常就是在外围的控制器中维护的。当然啦,仅仅只是上面定义的指令,我们还是无法让指令自己拥有的scope为我们所用,还需要对别的地方进行修改,看下面的代码:

angular.module("MyApp", []).
  directive('mySearchBox', function() {
    return {
      restrict: 'E',
      scope: {
        searchText: '=',
        isSearching: '='
      },
      controller: function($scope) {
        $scope.localSearchText = '';
        $scope.clearSearch = function() {
          $scope.searchText = "";
          $scope.localSearchText = "";
        };
        $scope.doSearch = function() {
          $scope.searchText = $scope.localSearchText;
        };
      },
      replace: true,
      template:
      '<form>' +
        '<div>' +
          '<input ng-model="localSearchText" type="text" />' +
        '</div>' +
        '<div>' +
          '<button ng-click="clearSearch()" class="btn btn-small">Clear</button>' +
          '<button ng-click="doSearch()" class="btn btn-small">Search</button>' +
        '</div> ' +
        '<div ng-show="isSearching">' +
          '<img ng-show="isSearching" src="http://loadinggif.com/images/image-selection/3.gif" /> ' +
          'Searching...' +
        '</div>' +
      '</form>'
    };
  })

如果你的模版像上面这样看起来那么多了,我们建议是时候用 templateUrl 了。当然,这里仅是示范使用。那么上面的代码多了什么呢?

  • 指令里面出现了属于指令自己的控制器,这样,我们的自定义指令可以去响应用户的输入和用户交互了,在这里面可以用 $scope 对象引用到之前做了双向绑定的两个变量
  • 模版中可以写angular的指令,并且可以调用指令下的控制器中定义的方法
  • $scope 中多维护了一个变量,就是用户输入通过 ng-model 指令与我们自定义指令中进行了一个双向绑定,这样我们在自定义指令中可以随时捕获用户的输入了
  • 维护一个用户输入是为了让我们等用户真正完成了所有输入并点击搜索时我们才把这个值赋值给 searchText ,这样可以避免在用户每敲一次键盘,我们就得去触发一次search,这是不科学的

Q:到这里了,我们还是没有提到指令自己的scope中的两个属性到底绑定的是外面的什么东西,和什么东西通信,快告诉我们吧,急死我了。
A:骚年,莫着急,这就来,绑定的是外围的控制器,这个搜索框,毕竟还是要通过外围控制器的刺激才能开工的

创建一个外围控制器来持有与指令通信的变量

当然啦,如果仅仅只是想让两个指令之间通信,创建这个控制器并不是严格要求要做的。但是话说回来,在大多数情况下,这是必须的,因为总得有人站出来作为一种“粘合剂”把团队里的其他成员都给整合起来,根据应用的业务逻辑把所有的包括指令呀什么乱七八糟的东西整合到控制器里,有时候是很有用的。
让我们模拟一个city search的业务逻辑那样的需求,注意:业务逻辑和指令中完成的逻辑不同,指令中完成的逻辑一般是视图逻辑,就是那些提供用户与视图/模版交互的逻辑;而实际的业务逻辑就是你这个产品要干什么,有什么功能,大概就是这样了,分清楚了吧。所以,让我们来看看这个例子吧:

<div ng-controller="CitySearchCtrl">
    <h1>Search for Cities</h1>
    <my-search-box search-text="citySearchText" is-searching="isSearchingForCities"></my-search-box>
</div>
// app.js
function CitySearchCtrl($scope, $timeout) {
    $scope.$watch('citySearchText', function(citySearchText) {
        if (citySearchText) {
            $scope.isSearchingForCities = true;
            $timeout(function() {
                // simulate search that always gives the same results
                $scope.isSearchingForCities = false;
                $scope.citySearchResults = ['NY', 'London', 'Paris', 'Beijing'];
            }, 1000);
        } else {
            $scope.isSearchingForCities = false;
            $scope.fruitSearchResults = [];
        }
    });
}

上面的控制器代码没什么好细讲的,就是用 $timeout 返回一个fake的搜索数据,你如果要问,为什么 citySearchText 没有直接写明了 $scope.citySearchText ,那我只能说,骚年,洗洗睡吧,或者往回看看。

创建第二个指令:my-search-results

这个指令和前一个指令有着很相似的 public interface (所谓的公共借口),其实就是暴露给外面可以和外面通信的接口,也就是 scope 属性中定义的那个对象。看如下代码:

directive('mySearchResults', function() {
   return {
     restrict: 'E',
     transclude: true,
     scope: {
       isSearching: '=',
       searchResults: '=',
       searchText: '='
     },
     replace: true,
     template:
       '<div ng-hide="isSearching">' +
         '<h4 ng-show="searchResults">Found  Search Results For "":</h4>' +
         '<ul ng-show="searchResults">' +
           '<li ng-repeat="searchResult in searchResults">' +
             '' +
           '</li>' +
         '</ul>' +
       '</div>'
   };
 });
<div ng-controller="CitySearchCtrl" style="margin: 20px">
    <h1>Search for Cities</h1> 
    <my-search-box search-text="citySearchText" is-searching="isSearchingForCities"></my-search-box>
    <my-search-results is-searching="isSearchingForCities" search-results="citySearchResults" search-text="citySearchText"></my-search-results>
</div>

其实通过上面可以知道,这两个指令之间的耦合度几乎为0,是通过控制器实现了数据传输的,这就是ng中控制器的作用了。

父子级指令间的通信

接下来这部分是受光头egghead视频的启发多加的,它讲述的是父级指令及子级指令之间的通信,依靠的是父级指令中自己的控制器

创建父级指令

假设我们有如下的html和指令:

// html
<div ng-app="superApp">
    <superhero></superhero>
</div>
// main.js
var app = angular.module('superApp', []);
 
app.directive("superhero", function () {
  return {
    restrict: "E",
 
    controller: function ($scope) {
      $scope.abilities = [];
      
      // 以下三个方法为什么用 this ,而不用 $scope 呢?
      this.addStrength = function() {
        $scope.abilities.push("strength");
      };
 
      this.addSpeed = function() {
        $scope.abilities.push("speed");
      };
 
      this.addFlight = function() {
        $scope.abilities.push("flight");
      };
    },
 
    link: function (scope, element) {
      element.addClass("button");
      element.bind("mouseenter", function () {
        console.log(scope.abilities);
      });
    }
  };
});

上面的js代码中唯一一个疑问就是像注释中所说,为什么不把三个方法也注册到 $scope 中去呢?目前还没想到原因。让我们接着看子级指令的创建。

创建子级指令

我们创建了三个属性类型的子级指令,如下:

// main.js
app.directive("strength", function() {
  return {
    require: "superhero",
    link: function (scope, element, attrs, superheroCtrl) {
      superheroCtrl.addStrength();
    }
  };
});

app.directive("speed", function() {
  return {
    require: "superhero",
    link: function (scope, element, attrs, superheroCtrl) {
      superheroCtrl.addSpeed();
    }
  };
});
 
app.directive("flight", function() {
  return {
    require: "superhero",
    link: function (scope, element, attrs, superheroCtrl) {
      superheroCtrl.addFlight();
    }
  };
});

通过子级指令中的 require 属性声明子级需要 superhero 指令作为父级指令,而且默认 restrict 到 A类型指令。那样,在这个指令中的 link function 中的第四个参数,就可以调用父级指令的 controller 中定义的方法了。可以理解为父级指令中 controller 中定义的就是要暴露的API。现在的html看起来像如下这样:

// html
<div ng-app="superApp">
    <superhero flight speed strength>Superman</superhero>
    <superhero speed>The Flash</superhero>
</div>

上面的代码跑起来有个问题,就是子级指令被调用时,父级中的scope被改写了,控制台输出两次都是 speed了

Q:However, with multiple instances of the superhero directive like above, though the two element directives have different sets of directive attributes, each instance is evaluated sequentially. Since the scope is shared between the two element directives, the superhero directive takes on the form of whichever instance came last.
A:正如上面这段英文指出的,哪个子级指令最后被执行,由于scope是共享了,那么 superhero 指令就会采取最后被执行的那个指令形式,在控制台只输出speed了。

解决:在父级指令 superhero 中提供一个 scope 属性,这样,每个这个指令的实例对象,就会拥有一个自己的scope了。done!

最后奉上光头地址,到youtube搜egghead也很多哈。thinkster

左银右煌 /
Published under (CC) BY-NC-SA in categories programming  tagged with AngularJS