首页 > 代码库 > 创建自定义指令(二)

创建自定义指令(二)

一、使用嵌入包含

嵌入包含的意思是将一个文档的一部分通过引用插入到另一个文档中。在指令的上下文信息中,当你要创建一个可以包含任意内容的包装器指令时,这将十分有用。

<script type="text/ng-template" id="template">
        <div class="panel panel-default">
            <div class="panel-heading">
                <h4>This is the panel</h4>
            </div>
            <div class="panel-body" ng-transclude>
            </div>
        </div>
</script>
<script type="text/javascript">
        angular.module("exampleApp", [])
            .directive("panel", function () {
                return {
                    link: function (scope, element, attrs) {
                        scope.dataSource = "directive";
                    },
                    restrict: "E",
                    scope: true,
                    template: function () {
                        return angular.element(
                            document.querySelector("#template")).html();
                    },
                    transclude: true
                }
            })
            .controller("defaultCtrl", function ($scope) {
                $scope.dataSource = "controller";
            });
</script>

<body ng-controller="defaultCtrl">
    <panel>
        The data value comes from the: {{dataSource}}
    </panel>
</body>

 

我想要达到的效果:

 <div class="panel panel-default">
            <div class="panel-heading">
                <h4>This is the panel</h4>
            </div>
            <div class="panel-body" ng-transclude>
                 The data value comes from the:controller
            </div>
 </div>

 

之所以使用嵌入包含这样一个术语,因为内容时放在要插入到模板中的panel元素里面的。在使用嵌入包含时有两个特定步骤是必需的。第一步是在创建指令时将transclude定义属性设置为true,如下:transclude:true

第二步是将ng-transclude指令使用到模板中,就放在想插入被包装元素的地方。

设置transclude为true后,会对指令所应用到的元素内容进行包装,但并不是元素本身。如果你想包含进元素,就需要将transclude属性设置为element。

另外需要注意的是,被嵌入包含的内容中的表达式是在控制器作用域中被计算的,而不是指令的作用域。我再控制器的工厂函数中和指令的链接函数中都对dataSource属性定义了值,但是AngularJS的明智之处在于从控制器中取得了该值。我说明智是因为这种方法意味着将被嵌入包含的内容不需要知道它的数据是定义在哪个作用域中,你只需要不把嵌入包含当做一个需要考虑的问题,尽管写表达式就好了,让AngularJS自己去进行计算。

尽管如此,如果在计算嵌入包含表达式时你确实想将指令作用域考虑在内,只需确保将scope属性设置为false,如下

...
restrict:"E",
scope:false,
template:function(){
...

 

这确保了指令在指令作用域上操作,而且任何定义在链接函数中的值将影响嵌入包含的表达式。修改后显示如下:

 <div class="panel panel-default">
            <div class="panel-heading">
                <h4>This is the panel</h4>
            </div>
            <div class="panel-body" ng-transclude>
                 The data value comes from the:directive
            </div>
 </div>

 

 

二、使用编译函数

当指令特别复杂或者需要处理大量数据时,使用编译函数操作DOM并让链接函数执行其他任务,是比较有利的。除了性能以外使用编译函数还有一个好处,就是可以使用嵌入包含来重复生成内容的能力,就像ng-repeat所做的那样。

<body ng-controller="defaultCtrl" class="panel panel-body" >
    <table class="table table-striped">
        <thead><tr><th>Name</th><th>Price</th></tr></thead>
        <tbody>
            <tr simple-repeater source="products" item-name="item">
                <td>{{item.name}}</td><td>{{item.price | currency}}</td>
            </tr>
        </tbody>
    </table>
    <button class="btn btn-default text" ng-click="changeData()">Change</button>
</body>
angular.module("exampleApp", [])
            .controller("defaultCtrl", function ($scope) {
                $scope.products = [{ name: "Apples", price: 1.20 },
                    { name: "Bananas", price: 2.42 }, { name: "Pears", price: 2.02 }];

                $scope.changeData = function () {
                    $scope.products.push({ name: "Cherries", price: 4.02 });
                    for (var i = 0; i < $scope.products.length; i++) {
                        $scope.products[i].price++;
                    }
                }
            })
            .directive("simpleRepeater", function () {
                return {
                    scope: {
                        data: "=source",
                        propName: "@itemName"
                    },
                    transclude: ‘element‘,
                    compile: function (element, attrs, transcludeFn) {
                        return function ($scope, $element, $attr) {
                            $scope.$watch("data.length", function () {
                                var parent = $element.parent();
                                parent.children().remove();
                                for (var i = 0; i < $scope.data.length; i++) {
                                    var childScope = $scope.$new();
                                    childScope[$scope.propName] = $scope.data[i];
                                    transcludeFn(childScope, function (clone) {
                                        parent.append(clone);
                                    });
                                }
                            });
                        }
                    }
                }
            });

 

在HTML中使用source属性指定了数据对象的来源,并使用item-name属性指定了在嵌入包含的模板中可被用于应用当前对象的名称。我的目标是对每个product对象重复生成tr元素,所以我设置了transclude定义对象的element,也就是说元素本身将被包含于嵌入包含中,而不是其内容。我也可以将我的指令应用在tbody元素上并设置transclude属性为true。

这个指令的核心部分是编译函数,是由compile属性指定的。编译函数被传入三个参数:指令所应用到的元素,该元素的属性,以及一个可用于创建嵌入包含元素的拷贝的函数。编译函数会返回一个链接函数(当compile属性被使用时link属性会被忽略)。这可能看起来有点奇怪,但是请记住编译函数的目的是为了修改DOM,所以从编译函数返回一个链接函数是很有帮助的,因为它提供了一个简易的将数据从指令的一部分传递到下一部分的方法。编译函数应当仅仅是操作DOM的,所以并没有为他提供作用域,但是编译函数返回的链接函数可以声明对scope,element,attrs参数的依赖,对应于普通链接函数中的各个参数。

理解编译函数:通过调用$scope.$new方法创建了一个新的作用域。对于嵌入包含内容的每个实例,这允许我将一个不同的对象赋给item属性,使用的是如下方法进行克隆的:

...
transcludeFn(childScope,function(clone){
    parent.append(clone);
})
...

 

对于每个数据对象,调用了传给编译函数的嵌入包含函数。第一个参数是包含item属性的子作用域,item属性设置为当前数据项。第二个参数是一个传入了嵌入包含内容的一组拷贝函数,这份拷贝被使用jqLite添加到父元素下。结果是对于每个数据对象生成了指令所应用到的tr元素的一份拷贝,并且创建了一个新的作用域,在这个作用域中允许嵌入包含内容使用item来引用当前数据对象。

 

三、在指令中使用控制器

 指令能够创建出被其他指令所用的控制器。这允许指令被组合起来创建出更复杂的组件。

<body ng-controller="defaultCtrl">
    <div class="panel panel-default">
        <div class="panel-body">
            <table class="table table-striped" product-table="totalValue" product-data="products" ng-transclude>
                <tr>
                    <th>Name</th>
                    <th>Quantity</th>
                </tr>
                <tr ng-repeat="item in products" product-item></tr>
                <tr>
                    <th>Total:</th>
                    <td>{{totalValue}}</td>
                </tr>
            </table>
        </div>
    </div>
</body>
<script type="text/ng-template" id="productTemplate">
        <td>{{item.name}}</td>
        <td>
            <input ng-model=item.quantity />
        </td>
</script>
angular.module("exampleApp", [])
        .controller("defaultCtrl", function($scope) {
            $scope.products = [{
                name: "Apples",
                price: 1.20,
                quantity: 2
            }, {
                name: "Bananas",
                price: 2.42,
                quantity: 3
            }, {
                name: "Pears",
                price: 2.02,
                quantity: 1
            }];
        })
        .directive("productItem", function() {
            return {
                template: document.querySelector("#productTemplate").outerText,
                require: "^productTable",
                link: function(scope, element, attrs, ctrl) {
                    scope.$watch("item.quantity", function() {
                        ctrl.updateTotal();
                    });
                }
            }
        })
        .directive("productTable", function() {
            return {
                transclude: true,
                scope: {
                    value: "=productTable",
                    data: "=productData"
                },
                controller: function($scope, $element, $attrs) {
                    this.updateTotal = function() {
                        var total = 0;
                        for (var i = 0; i < $scope.data.length; i++) {
                            total += Number($scope.data[i].quantity);
                        }
                        $scope.value = total;
                    }
                }
            }
        });

 

controller用于为指令创建一个控制器,这个函数可以声明对作用域的依赖,对指令所应用到的元素的依赖,和对该元素属性的依赖。require定义对象属性用于声明对控制器的依赖,属性值是指令名和一个可选的前缀。

可用的require属性值的前缀:

None  假定两个指令都应用与同一个元素

^ 在指令所应用到的元素的父元素上查找另一个指令

? 如果找不到指令并不报错——小心使用

 

我指定了名称为productTable,以及前缀^,需要这样指定是因为productTable指令被应用在productItem指令所应用到的元素的父元素上。为了使用控制器中定义的功能,我在链接函数上指定了一个附加参数,如下:

link:function(scope,element,attrs,ctrl){}

控制器参数不能被依赖注入,所以你可以调用任何你想调的东西,我的个人习惯是使用名称ctrl。做了这些修改后,我就可以调用控制器中的函数了,就像它们已经定义在本地指令中一样。

我在调用一个控制器方法,作为一个执行计算的信号,并不需要任何参数,但是你可以从一个控制器传递数据到另一个,只要你记着传给控制器函数的scope参数是定义控制器的那个指令中的作用域就可以了,而不是引用该控制器的那个指令的作用域。

 

添加另一个指令

定义控制器函数的价值来自于对功能进行分离和重用的能力,从而无需构建和测试单个庞大的组件。在上面的例子中,productTable控制器并不知道productItem控制器的设计或实现,也就是说我可以独立地测试他们并任意修改,只要productTable控制器仍然继续提供updateTotal函数即可。

这种方法也允许你能够混合搭配各种指令的功能,从而在一个程序里创建出各种功能的不同组合。

<script type="text/ng-template" id="resetTemplate">
        <td colspan="2">
            <button ng-click="reset()">Reset</button>
        </td>
</script>
<body ng-controller="defaultCtrl">
    <div class="panel panel-default">
        <div class="panel-body">
            <table class="table table-striped" product-table="totalValue" product-data="products" ng-transclude>
                <tr>
                    <th>Name</th>
                    <th>Quantity</th>
                </tr>
                <tr ng-repeat="item in products" product-item></tr>
                <tr>
                    <th>Total:</th>
                    <td>{{totalValue}}</td>
                </tr>
                <tr reset-totals product-data="products" property-name="quantity"></tr>
            </table>
        </div>
    </div>
</body>
.directive("resetTotals", function() {
            return {
                scope: {
                    data: "=productData",
                    propname: "@propertyName"
                },
                template: document.querySelector("#resetTemplate").outerText,
                require: "^productTable",
                link: function(scope, element, attrs, ctrl) {
                    scope.reset = function() {
                        for (var i = 0; i < scope.data.length; i++) {
                            scope.data[i][scope.propname] = 0;
                        }
                        ctrl.updateTotal();
                    }
                }

            }
        });

 

新的指令名为resetTotals,它向表格中添加了一个reset按钮,可以将所有的数量清零,在一个隔离的作用域上提供了数据数组和要清零的属性名称,该指令就可以通过数据绑定查找到要清零的位置。在值被重置后,resetTotals指令调用了productTable指令所提供的updateTotal方法。 

 

创建自定义指令(二)