> https://docs.angularjs.org/api/ng/service/$compile
# 创建自定义angularJS指令
* `@` 用来传递一个字符串值到指令
`@`字符告诉指令这个新的name属性是一个来自外部作用域的字符串值。如果外部作用域中这个name的值被修改了,指令中的这 个值也会自动更新;
`@`在只需要给指令传递字符串值时很方便实用,但在需要把在指令中对值的改变反映到外部作用域时却无能为力。在需要创建在 指令的独立作用域和外部作用域中的双向绑定时
* `=` 用于创建一个双向绑定的对象
`=`告诉指令传入指令本地作用域中的对象需要使用双向绑定的方式。如果外部作用域中的属性值变动,指令本地作用域中的值也 会自动更新;如果指令中修改了这个值,外部作用域中对应的也会同步被修改
* `&`允许传入一个可被指令内部调用的函数
` &` 本地作用域属性允许指令调用方传递一个可被指令内部调用的函数。例如,假设你在写一个指令,终端用户点击指令中的一个按钮并需要在控制器中触发一个事件。你不能把点击事件硬编码在指令的代码内部,这样的话外部的控制器就无法知道指令内部到底发生了什么。在需要时触发一个事件可以很好的解决这个问题(使用`$emit`或`$broadcast`),但是控制器需要知道具体侦听的事件名是什么所以也不是最优的。
&字符从根本上来说相当于: “嘿,给我一个函数我可以在指令中发生某些事件时调用它”;
指令中的模板可以包含一个按钮,当按钮被点击时,action(外部函数的引用)函数将会被调用。
scope属性的值可以为一个bool型,值为false时不使用独立作用域,和不写此属性没区别。
scope中定义的属性名要使用驼峰命名的方式,而在模板中使用的时候要使用连字符语法,假设有一个指令叫datePicker,scope部分定义如下:
~~~
scope: {
isOpen: "=",
currentDate: "=",
onChange: "&"
}
~~~
视图中使用方式如下(假设引号里面的函数和作用域属性是已经在控制器中定义的):
~~~
<div date-picker
is-open="openState"
current-date="currentDate"
on-change="dateChange()"
></div>
~~~
另外,如果`scope`中的一些属性是可选的(如上面例子中,`isOpen`默认为false,指令的使用者可以选择不传递这个属性),在使用这个指令的时候AngularJS就会报错,也就是说`scope`定义的属性在调用指令时都需要被传递(不传递会报错,但不影响程序运行)。解决这个问题的话可以在可选参数后面加一个问号`?`标识这个属性是可选的,修改后的指令`scope`部分如下:
~~~
scope: {
isOpen: "=?", // 注意这里的问号,指定这个参数是可选的
currentDate: "=",
onChange: "&"
}
~~~
## 指令的函数参数
需要注意的是***指令的控制器通过调用$scope.add(name)来尝试调用外部函数并传递一个参数过去。这样可以工作吗?***实际上在外部函数中输出这个参数得到的却是`undefined`,这可能让你抓破脑袋都想不通为什么。那么接下来我们该做什么呢?
### 选择1:使用对象字面量
一种方法是传递一个对象字面量。下面是演示如何把`name`传递到外部函数中的例子:
~~~
angular.module('directivesModule')
.directive('isolatedScopeWithController', function () {
return {
restrict: 'EA',
scope: {
datasource: '=',
add: '&',
},
controller: function ($scope) {
...
$scope.addCustomer = function () {
// 调用外部函数
var name = 'New Customer Added by Directive';
$scope.add({ name: name });
// Add new customer to directive scope
$scope.customers.push({
name: name,
street: counter + ' Main St.'
});
};
},
template: '<button ng-click="addCustomer()">Change Data</button>' +
'<ul><li ng-repeat="cust in customers">{{ cust.name }}</li></ul>'
};
});
~~~
需要注意的是`$scope.add()`方法调用时现在传递了一个对象字面量作为参数。很不幸,这样仍然不能工作!什么原因呢?传递给`$scope.add()`的对象字面量中定义的name属性在分配给指令时同样也需要在外部函数中被定义。非常重要的一点是,在视图中写的参数名必须要与对象字面量中的名字匹配。下面是一个例子:
`<div isolated-scope-with-controller datasource="customers" add="addCustomer(name)"></div>`
可以看到在视图中使用指令时,`addCustomer()`方法添加了个参数`name`。这个name必须要与指令中调用`$scope.add()`时传入的对象字面量中的name相匹配。如此一来指令就能正确工作了。
### 选择2:存储一个函数引用并调用它
上面那种方式的问题在于在使用指令时必须要给函数传递参数而且参数名必须在指令内以对象字面量的形式被定义。如果任何一点不匹配将无法工作。虽然这种方法可以完成需求,但仍然有很多问题。例如如果指令没有完善的使用说明文档就很难知道指令中需要传递的参数名究竟是什么,这时就不得不去翻指令源码查看参数内容了。
另一种可行的方法是在指令上定义一个函数但在函数名后面不加圆括号,如下:
`<div isolated-scope-with-controller-passing-parameter2 datasource="customers" add="addCustomer"></div>`
为了传递参数到外部的`addCustomer`函数你需要在指令中做以下事情。把`$scope.add()(name)`代码放到可被`addCustomer`调用的方法下面:
~~~
angular.module('directivesModule')
.directive('isolatedScopeWithControllerPassingParameter2', function () {
return {
restrict: 'EA',
scope: {
datasource: '=',
add: '&',
},
controller: function ($scope) {
...
$scope.addCustomer = function () {
// 调用外部函数
var name = 'New Customer Added by Directive';
$scope.add()(name);
...
};
},
template: '<button ng-click="addCustomer()">Change Data</button><ul>' +
'<li ng-repeat="cust in customers">{{ cust.name }}</li></ul>'
};
});
~~~
为什么这种方法可以工作?这个需要从&的另一个主要作用说起。&在指令中主要的作用是计算表达式,即在控制器调用以&定义的作用域属性时AngularJS会计算出这个表达式的值并返回。例如在视图中输入`add="x = 42 + 2"`,那么在指令中读取`$scope.add()`时将会返回这个表达式的计算结果(44),任何一个有效的AngularJS的表达式都可以是add属性的值并在读取add属性时被计算。所以当我们在视图中输入不带圆括号的函数`add="customers"`时,指令中`$scope.add()`实际返回的是在控制器中定义的函数`customers()`。所以在指令中调用`$scope.add()(name)`就相当于调用控制器的`customers(name)`。
在指令中输出`$scope.add()`将会得到以下内容(正好验证上面所说):
### `&`背后的运行机制
如果你对&的运行机制感兴趣,当&本地作用域属性被调用(例如上面例子中的a dd本地作用域属性),下面的代码将会执行:
~~~
case '&':
parentGet = $parse(attrs[attrName]);
isolateScope[scopeName] = function(locals) {
return parentGet(scope, locals);
};
break;
~~~
上面的`attrName`变量相当于前面例子中指令本地作用域属性中的add。调用`$parse`返回的`parentGet`函数 如下:
~~~
function (scope, locals) {
var args = [];
var context = contextGetter ? contextGetter(scope, locals) : scope;
for (var i = 0; i < argsFn.length; i++) {
args.push(argsFn[i](scope, locals));
}
var fnPtr = fn(scope, locals, context) || noop;
ensureSafeObject(context, parser.text);
ensureSafeObject(fnPtr, parser.text);
// IE stupidity! (IE doesn't have apply for some native functions)
var v = fnPtr.apply
? fnPtr.apply(context, args)
: fnPtr(args[0], args[1], args[2], args[3], args[4]);
return ensureSafeObject(v, parser.text);
}
~~~
处理代码映射对象字面量属性到外部函数参数并调用函数。
虽然没有必要一定去理解如何使用&本地作用域属性,但是去深入发掘`AngularJS`在背后做了一些什么总是一件有趣的事情。
结尾
从上面可以看到`&`的传参过程还是有点困难的。然而一旦学会了如何使用,整个过程其实并不算太难用。
- 步入JavaScript的世界
- 二进制运算
- JavaScript 的版本是怎么回事?
- JavaScript和DOM的产生与发展
- DOM事件处理
- js的并行加载与顺序执行
- 正则表达式
- 当遇上this时
- Javascript中apply、call、bind
- JavaScript的编译过程与运行机制
- 执行上下文(Execution Context)
- javascript 作用域
- 分组中的函数表达式
- JS之constructor属性
- Javascript 按位取反运算符 (~)
- EvenLoop 事件循环
- 异步编程
- JavaScript的九个思维导图
- JavaScript奇淫技巧
- JavaScript:shim和polyfill
- ===值得关注的库===
- ==文章==
- JavaScript框架
- Angular 1.x
- 启动引导过程
- $scope作用域
- $q与promise
- ngRoute 和 ui-router
- 双向数据绑定
- 规范和性能优化
- 自定义指令
- Angular 事件
- lodash
- Test