Angular黑科技之transclude

@kuitos 2015-08-23 09:41:39发表于 kuitos/kuitos.github.io Angular

Angular黑科技之transclude

原文写于 2015-01-28

tansclude是angular生造的一个单词,不要去google啥意思相信我查不出来的。。。至于tranclude怎么翻译这件事我也是很懊恼,暂且将它译做 "隔离嵌入" 吧。具体含义大家在看过blog后自己体味吧。。。

transclude是angular指令中的一个配置,具体解释看这里

Extract the contents of the element where the directive appears and make it available to the directive. The contents are compiled and provided to the directive as a transclusion function. See the Transclusion section below.
There are two kinds of transclusion depending upon whether you want to transclude just the contents of the directive's element or the entire element:
true - transclude the content (i.e. the child nodes) of the directive's element.
'element' - transclude the whole of the directive's element including any directives on this element that defined at a lower priority than this directive. When used, the template property is ignored.

简言之就是将指令元素内部的节点内容在指令内部变得可用,不受指令隔离作用域影响。
使用场景一般是这样的:
我们需要封装一个组件,该组件有公用的header等区域,同时还有一个可以供使用者自由发挥的div区域。但是作为一个合格的angular开发者我们需要时刻关注自己的指令是否污染上层scope,所有大多情况下我们会使用隔离scope的做法。但是这个时候带来的问题是在隔离scope下我们的自定义div无法顺利的访问到自己作用域内容。这个时候我们需要在自定义指令中提供一个沙箱环境用于装载自定义div,这一块区域不受指令隔离scope影响。通常情况下我们会配合使用ng-transclude实现我们的需求,像下面这样
directive.transclude

.directive("todo", function(){

    return {
        restrict:"E",
        transclude:"true",
        template:"<header>{{header}}</header><div><span>这里是自定义区域</span><ng-transclude></ng-transclude></div>"
        scope:{
            header:"@"
        }
    };
})

调用时这样写
todo

<script>
    $scope.todo = “该干活啦!”;
</script>
<div>
    <todo header="todo list">
        <div>
            <span ng-bind="todo"></span>
        </div>
    </todo>
</div>

页面最后呈现的结果是这样的

<todo>
    <header>todo list</header>
    <div>
        <span>这里是自定义区域</span>
        <ng-transclude>
            <div>
                <span ng-bind="todo">该干活啦!</span>
            </div>
        </ng-transclude>
    </div>
</todo>

todo信息的展示不受指令的隔离作用域影响(隔离作用域中没有todo属性)

如果你以为我这篇仅仅是介绍下ng-transclude就完了。。。怎么可能区区ng-transclude官方文档都能查到demo的东西我会叫他黑科技?!!!没错接下来重点来啦!

ng-transclude这里正常的显示了指令外层作用域的内容是因为我这里只是做简单的读数据,如果我要写数据呢?假如我们有这样一个需求,当我们在transclude的内容里做的操作要影响外层展示,比如这样

<script>

    $scope.todo = “该干活啦!”;
</script>
<div ng-controller>
    <span ng-bind="todo"></span>
    <todo header="todo list">
        <div>
            <button ng-click="todo='休息时间!'"></button>
            <span ng-bind="todo"></span>
        </div>
    </todo>
</div>

<!-- 页面编译完成这样 -->
<div ng-controller>
    <span ng-bind="todo">该干活啦!</span>
    <todo header="todo list">
        <header>todo list</header>
        <span>这里是自定义区域</span>
        <ng-transclude>
            <div>
                <button ng-click="todo='休息时间!'"></button>
                <span ng-bind="todo">该干活啦!</span>
            </div>
        </ng-transclude>
    </todo>
</div>

当我们点击按钮时内层跟完成的todo信息会变成 "休息时间!" 么?
答案是 内层的会变,外层的不会。

原因就在ng-transclude。ng-tranclude是angular自己实现的一个指令,他的作用域继承自自定义指令外层的scope,所以我们在读取外层scope内容时没问题,但当我们尝试改写外层scope的属性时,实际上发生的是在ng-transclude作用域会生成一个相应的属性(比如上文的todo),即 ngTranscludeScope.todo = "休息时间!" ,而ngControllerScope.todo依然为"该干活啦!"。

既然ng-transclude存在这样的问题那么我们改怎么解决不能写操作的问题呢?
好在angular为指令提供了一个transcludeFn的接口,它来自angular框架对指令内嵌内容编译后返回的链接函数,获取这个函数有几个方式

  1. 指令controller注入

    return {
        transclude:true,
        controller:["$transclude",function($transclude){
    
        }]
    };
  2. 指令compile时自动注入

    // 这种方式不推荐,因为transcludeFn通常需要依赖scope
    return {
        transclude:true,
        compile:function(element,attr,transcludeFn){
    
        }
    };
  3. 指令link时自动注入

    return {
        transclude:true,
        link:function(scope,element,attr,controller,transcludeFn){
    
        }
    };

    transcludeFn调用方式如下:
    transcludeFn(scope, cloneLinkFn, futrueElement),看官方声明
    function([scope], cloneLinkingFn, futureParentElement).

    • scope: optional argument to override the scope.
    • cloneLinkingFn: optional argument to create clones of the original transcluded content.
    • futureParentElement:
      • defines the parent to which the cloneLinkingFn will add the cloned elements.
      • default: $element.parent() resp. $element for transclude:'element' resp. transclude:true.
      • only needed for transcludes that are allowed to contain non html elements (e.g. SVG elements) and when thecloneLinkinFn is passed, as those elements need to created and cloned in a special way when they are defined outside their usual containers (e.g. like <svg>).
      • See also the directive.templateNamespace property.

我们这里这样去用

.directive("todo", function(){

    return {
        restrict:"E",
        transclude:"true",
        template:"<header>{{header}}</header><div><span>这里是自定义区域</span><ng-transclude></ng-transclude></div>"
        scope:{
            header:"@"
        },
        link:function(scope,element,attr,controller,transcludeFn){
            transcludeFn(scope.$parent, function(transcludeContent, scope){
                element.find("ng-transclude").replaceWith(transcludeContent);
            });
        }
    };
})

我们通过transcludeFn将自定义指令内的节点的作用域手动指定为当前指令的父级作用域,然后将编译好的模板替换到ng-transclude层。
但是这样写有两个问题:

  1. ng-transclude指令编译过一次模板,我们这里又手动编译了一次,出现重复编译
  2. transcludeFn时我们通过element.find方式找到ng-transclude层,然后手动替换了模板。这种手动操作dom的方式违背了angular的理念

更优雅的做法是这样的

.directive("todo", function(){

    return {
        restrict:"E",
        transclude:"true",
        template:"<header>{{header}}</header><div><span>这里是自定义区域</span><content-transclude></content-transclude></div>"
        scope:{
            header:"@"
        },
        controller:["$transclude",function(transcludeFn){
            this.transcludeFn = transcludeFn;
        }]
    };
})

.directive("contentTransclude",funtion(){
    return {
        restrict:"E",
        require:"^todo",
        link:function(scope,element,attr,todoController){
            todoController.transcludeFn(scope.$parent, function(transcludeContent){
                element.append(transcludeContent);
            });
        }
    };
})

todo指令只提供一个维护transcludeFn的controller,然后新建一个contentTransclude指令调用父级controller的服务将沙箱内容插入到具体位置。这样各区块职责划分更明确,复用性更强,而且整个代码看上去更优雅了。

总结来看,就是transclude提供一个内联function可以让我们给沙箱区域手动指定任意作用域,而不是交由框架自动生成。在我看来这种能控制一切的东西简直就是黑科技!!