将 ngModel 绑定到自定义指令

Posted

技术标签:

【中文标题】将 ngModel 绑定到自定义指令【英文标题】:Binding ngModel to a custom directive 【发布时间】:2014-12-19 16:53:24 【问题描述】:

所以我已经研究这个问题一周了,我似乎无法理解整个指令的内容。我已经阅读了很多帖子...

Demystifying Directives Directives Compile, Pre and Post Linking

一堆视频...

Creating Reusable Directives in AngularJS Writing Directives

并浏览了 *** 和其他论坛(链接跟随),希望有一些东西会沉入其中......我认为我遇到的问题是我想了解这些为什么/如何工作,这样我就不会被削减/将其他人的解决方案粘贴到我的代码中,但稍后又出现其他问题时不得不再次询问,因为我不知道我粘贴的代码在做什么。

然而,我发现每个人都有不同的方法给这只猫剥皮,而且似乎没有一个符合我对这应该如何工作的理解。

我正在尝试使用Metro UI CSS 库构建一个表单。我想我会从一个简单的文本框开始。是的...只是一个简单文本框。 Metro UI 文本框有一些不错的内置功能,我想保留这些功能,所以我认为这是一个很好的起点。

我读到,为了利用 AngularJS 的 Metro UI 行为,我需要将其包装在自定义指令 (Custom data-directives inside an AngularJS ng-repeat) 中。虽然这个例子并不是我想要的完全,但它似乎很容易解释我需要做什么。只需在指令的 LINK 函数中调用应用行为的函数,并将指令属性添加到输入元素...

所以我创建了一个名为“metroInputTransform”的指令并将其作为属性添加到输入元素。

<div data-ng-controller="pageOneFormCtrl as page">
    <input  type="text" id="txProductName" 
            data-ng-model="page.data.productName"
            data-metro-input-transform=""
            placeholder="product name" />
</div>

在指令的 LINK 函数中,我简单地调用了应用我正在寻找的行为的方法。我知道这比它需要的要冗长一些,但我正在努力学习它,所以我正在尽我所能地逐步完成它。 ...(完整代码见this fiddle)

var metroDirectives = angular.module('metroDirectives', []);
    metroDirectives.directive('metroInputTransform', function ($compile) 

        function postLink($scope, element, attrs, controller) 

            $(element).inputTransform();
        ;

        return 
            priority: 100,
            compile: function (element, attrs) 

                return  postLink ;
            
        ;
    );

所以这部分奏效了。它创建了 Metro 外观和相关行为,但是...... ngModel 没有绑定到元素。所以这开始了一段漫长的旅程,通过隔离范围等概念,分解各种编译、控制器、预链接、后链接功能,至少两种不同的持久化 ngModel 的方式……所有这些都不起作用。

经过各种阅读后,我的理解是 DOM 操作应该发生在 COMPILE 函数中,以便任何 DOM 转换都可用于摘要过程的编译和链接阶段。所以我把 inputTransform() 调用移到了 COMPILE 函数... (fiddle)

    return 
        priority: 100,
        terminal: true,  // if I didn't put this everything would execute twice
        compile: function (element, attrs)   

            $(element).inputTransform();

            return 
                pre: preLink,
                post: postLink
            ;
        
    ;

没有运气......同样的事情......没有绑定到 ngModel。于是我发现了“隔离作用域”的概念……

Understanding Isolate Scope - 视频 Using Isolate Scopes in Directives - 视频 Using ngModel with Isolate Scope

基于此,我尝试了以下 (fiddle)...

    return 
        priority: 100,
        scope: 
            ngModel : '='
        ,
        terminal: true,  // if I didn't put this everything would execute twice
        compile: function (element, attrs)   

            $(element).inputTransform();

            return 
                pre: preLink,
                post: postLink
            ;
        
    ;

没有变化……

我尝试了许多其他方法,但如果我还没有尝试过,恐怕我很快就会失去你的注意力。我得到的最接近的是 ONE-WAY 绑定,执行如下操作......即使在这里您也可以看到提取 ngModel 引用是完全不可接受的。 (fiddle)

var metroDirectives = angular.module('metroDirectives', []);
    metroDirectives.directive('metroInputTransform', function () 

        function postLink($scope, element, attrs, controller) 
            //
            // Successfully perfomes ONE-WAY binding (I need two-way) but is clearly VERY 
            // hard-coded. I suppose I could write a pasrsing function that would do this
            // for whatever they assign to the ngModel ... but ther emust be a btter way
                $(element).on("change", '[data-metro-input-transform]', function(e) 
                    $scope.$apply(function()
                        $scope['page']['data']['productName'] = e.currentTarget.value;
                    );
                );
        ;

        return 
            priority: 100,
            terminal: true,  // if I didn't put this here the compile would execute twice
            compile: function (element, attrs)   

                $(element).inputTransform();

                return 
                    pre: function ($scope, element, attrs, controller, transcludeFn)  ,
                    post: postLink
                ;
            
        ;
    );

我筋疲力尽,完全不知道还有什么可以尝试的。我知道这是因为我对 AngularJS 如何/为什么以它的方式工作的无知和缺乏理解。但是我读到的每一篇文章都让我提出与已回答的问题一样多的问题,或者把我带入一个兔子洞,在这个洞里我比刚开始时更迷失。没有在现场现场研讨会上投入 3000 美元,我负担不起,在那里我可以提出我需要回答的问题,我在 Angular 上处于完全的死胡同。

如果有人能提供指导、方向......一个很好的资源......任何可以帮助特别阐明这个问题的东西,但任何可以帮助我停止旋转的东西,我将不胜感激。与此同时,我将继续阅读和重新阅读我能找到的所有内容,希望会有所突破。

谢谢

G

更新 - 2014 年 10 月 30 日

我对这个问题很感兴趣,但想坚持下去。我需要并且想要学习这个。此外,我真的想对人们为此付出的努力表示感谢,虽然他们提出了一些解决方案,这最终可能是最好的方法,但他们都回避了这个问题,即我正在尝试使用Metro UI CSS 库提供的行为。如果可能的话,我宁愿不必重写它们。

到目前为止提供的两种解决方案都消除了解决方案中的关键语句......这是行......

$(element).inputTransform()

我不想发布包含“inputTransform”定义的整个 jQuery 小部件,但我将其删减并包含在此处...

    function createInputVal(element, name, buttonName) 

        var wrapper = $("<div/>").addClass("input-control").addClass(name);
        var button = $("<button/>").addClass(buttonName);
        var clone = element.clone(true); // clone the original element
        var parent = element.parent();

        $(clone).appendTo(wrapper);
        $(button).appendTo(wrapper);
        $(wrapper).insertBefore(element);
        $(element).remove(); // delete the original element

        return wrapper;
    ;

因此,我将该指令作为属性应用,因为它背后的 Metro 代码想要克隆文本框(如果它是元素指令则不会这样做),然后删除原始输入元素。然后它创建新的 DOM 元素并将克隆的输入元素包装在新创建的 DIV 容器中。我相信,问题是......当原始元素被克隆并从 DOM 中删除时,绑定被破坏了。有意义,如果“ng-model”属性分配绑定到文本框的 reference。所以我最初的期望是,由于“ng-model”属性与元素的其余部分一起被克隆,在指令的编译事件/函数/阶段中,引用将(重新)建立到新的创建的输入元素。显然情况并非如此。您可以在这个更新的fiddle 中看到,我已经尝试将 ng-model 重新连接到新的 DOM 元素,但没有成功。

也许这是不可能的……看来重建这些东西最终可能是更容易的方法。

再次感谢 Mikko Viitalia 和 'azium' ...

【问题讨论】:

【参考方案1】:

指令并不是最简单的概念,文档也不是那么好,而且分散在互联网上。

当我尝试编写我的第一个指令时,我遇到了compilepre-compile 等问题,但迄今为止我从未需要这些功能。这可能是由于我缺乏理解,但仍然......

看看你的例子,我发现有一些基本的事情需要澄清。首先,我会将您的指令限制为Element,因为它正在替换 html 中的控件。我会使用Attribute 例如为现有控件添加功能。

有一个(强制性)命名约定,您在 HTML 中使用虚线命名,在 javascript 中使用驼峰式命名。所以something-cool 变成了somethingCool。当您将变量“绑定”到指令的范围时,您的操作方式会有很大的不同。使用= 绑定到变量,使用@ 绑定到变量评估(字符串)值。所以首先允许“双向绑定”,但后者当然不允许。您还可以使用&amp; 绑定到父作用域的表达式/函数。

如果您使用例如普通的= 然后指令的范围在您的 HTML 中需要相同的名称。如果您想使用不同的名称,则在= 之后添加变量名称。一个例子

ngModel : '='        // <div ng-model="data"></div>
otherVar: '@someVar' // <div some-var="data></div> or <some-var="data"></some-var>

 

我冒昧地将your first Fiddle 的metro-input-transform 作为起点and rewrite it in Plunker。我想在这里解释一下(希望我理解正确)。

地铁输入指令

directives.directive('metroInput', function () 
  return 
    restrict: 'E',
    scope: 
      ngModel: '=',
      placeholder: '@watermark'
    ,
    link: function (scope) 
      scope.clear = function () 
        scope.ngModel = null; 
      ;
    ,
    templateUrl: 'metro-template.html'
  ;
);

指令期望 ngModel 绑定到并且 watermark 显示 ngModel 没有值(文本输入为空)。在link 内部,我介绍了clear() 函数,该函数在指令中用于重置ngModel。重置值时,显示watermark。我已将 HTML 部分分成单独的文件 metro-template.html。

Metro 输入 HTML 模板

<input type="text" ng-model="ngModel" placeholder=" placeholder ">
<button type="button" class="btn-clear" ng-click="clear()">x</button>

这里我们将ngModel 绑定到输入和分配placeholder。显示 [X] 的按钮绑定到 clear() 方法。

现在,当我们设置好指令后,这是使用它的 HTML 页面。

HTML 页面

<body>
  <div ng-controller="Ctrl">
    <section>
      The 'Product name' textbox in the 'Directive' 
      fieldset and the textbox in the 'Controls'<br>
      fieldset should all be in sync. 
    </section>

    <br>

    <fieldset>
      <legend>Directive</legend>
      <label for="productName">Product name</label>
      <br>
      <metro-input name="productName" 
                   ng-model="data.productName"
                   watermark="product name">
      </metro-input>
    </fieldset>

    <br>

    <fieldset>
      <legend>Control</legend>
      <input detect-mouse-over
             type="text" 
             ng-model="data.productName">
    </fieldset>
  </div>
</body>

所以在上面的例子中,metro 指令的用法如下。这将被指令的 HTML 模板替换。

<metro-input name="productName" 
             ng-model="data.productName" 
             watermark="product name">
</metro-input>

另一个输入应用了detect-mouse-over 指令,仅限于Attribute 只是为了显示AE 之间的用法/差异。鼠标检测指令使输入在鼠标移过/移出时更改背景颜色。

<input detect-mouse-over
       type="text" 
       ng-model="data.productName">

.

directives.directive('detectMouseOver', function ()  
  return 
    link: function (scope, element, attrs) 
      element.bind('mouseenter', function () 
        element.css('background-color', '#eeeeee');
      );
      element.bind('mouseleave', function () 
        element.css('background-color', 'white'); 
      );
    
  ;
);

它也有相同的ng-model 来反映控件之间的变化。

在您的示例中,您还有一个productService,它为上述输入控件提供了值。我把它改写为

产品服务

app.service('productService', function () 
  return 
    get: function () 
      return  productName: 'initial value from service' ;
    
  ;
);

所以get() 函数只是获取硬编码的值,但它仍然演示了服务的使用。控制器,命名为Ctrl 非常简单。这里重要的部分是您记得将所有服务等注入您的控制器。在这种情况下,角度的$scope 和我们自己的productService

控制器

app.controller('Ctrl', function ($scope, productService) 
  $scope.data = productService.get();
);

 

这里是上述解决方案的屏幕截图。

更改任何输入中的值都会更改两者的值。下面的输入有“mouseover”,所以它是灰色的,mouseout 会再次变成白色。按 [X] 清除值并使占位符可见。

这里是 plunker 的链接http://plnkr.co/edit/GGGxp0

【讨论】:

感谢您的努力和解释。我可能只是在这里随风而去,可能需要改变我的方法。我不知道我所采取的方向是否值得为此付出努力。代替“纯”解决方案,我可能会将我的方法更改为您在此处描述的内容......我仍然必须让项目继续进行!再次感谢! 所以我将此标记为答案,因为 *** 本质上要求您检查某些内容作为答案,以保持良好的信誉。虽然它没有解决确切的问题,但它是我为解决我的特殊情况而采取的路线。我花了更多时间试图找出最初的问题,而不是仅仅采取不同的方式。谢谢米科!你让我再次感动,我很感激!【参考方案2】:

好的,我不确定您从 Metro UI 中获得了哪些其他优势,但这是一个简单的小提琴,根本不需要您的指令来捕捉您在第一个小提琴中拥有的对我有用的东西。 http://jsfiddle.net/f0sph1vp/7/

<input placeholder="page.placeholder"
       ng-model="page.data.productName"  
       ng-focus="page.data.productName=''">
<button ng-click="page.data.productName=''">x</button>

您发布的第二个小提琴http://jsfiddle.net/gary_stenstrom/xcx2y8uk/64/ 对我来说很奇怪,因为您似乎不希望第二个输入框与您的第一个输入框相同。似乎您希望单击 x 按钮将第一个输入的值分配给第二个。这更有意义。

<input ng-model="data.first">
<button ng-click="data.second = data.first; data.first=''">X</button
<input ng-model="data.second">

【讨论】:

这是一个解决方案,但它回避了 Metro UI 库的使用。这需要我重写 Metro UI 提供的所有 js 行为。最终也许这就是答案。我已经确定是“inputTransform()”调用破坏了绑定...

以上是关于将 ngModel 绑定到自定义指令的主要内容,如果未能解决你的问题,请参考以下文章

将数据数组从控制器传递到自定义指令?

在 Angular2 ngModel 值未在自定义指令的 onBlur 事件上更新

angularJS contenteditable 指令双向绑定

AngularJS指令进阶 -- ngModelController详解

Angular2-无法给元素的属性做双向绑定,除非这个属性是指令或者组件

14自定义指令