将 ngModel 绑定到自定义指令 [英] Binding ngModel to a custom directive

查看:48
本文介绍了将 ngModel 绑定到自定义指令的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

所以我已经在这个问题上工作了一个星期,我似乎无法理解整个指令的事情.我已经阅读了很多帖子...

  • So I have been working on this issue for a week now and i cannot seem to get my head around this whole Directive thing. I have read lots of posts ...

    a bunch of videos ...

    And gone through StackOverflow and other forums (links to follow) hoping something will sink in ... I think that the problem that I am running into is that I want to UNDERSTAND why/how these work so that I am not cut/pasting someone else's solution into my code but then having to ask again later when something else crops up because I don't know what my pasted code is doing.

    I am finding however that everyone has a different way to skin this cat and none of them seem to match up with my understanding of HOW this is supposed to work.

    What I am attempting to do is build a form using the Metro UI CSS library. I thought I would start with a simple text-box. yep ... just a simple text box. A Metro UI text-box has some nice built in functionality that I wanted to preserve so I thought that was good place to start.

    I read that in order to leverage Metro UI behaviors with AngularJS I would need to wrap it in a custom directive (Custom data-directives inside an AngularJS ng-repeat). While this example wasn't exactly what I was looking for it seemed to easily explain what I needed to do. Just call the function that applies the behavior in the LINK function of the directive and add the directive attribute to the input element ...

    So I created a directive called 'metroInputTransform" and added it as an attribute to an input element.

    <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>
    

    In the LINK function of the directive I simply called the method that applies the behavior I was looking for. I know that this is a little more verbose than it needs to be but I am trying to learn it so I am stepping through it as best as I can. ... (for full code see 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 };
                }
            };
        });
    

    So this worked, partially. It created the Metro look and feel and associated behavior, but ... ngModel was not binding to the element. So this began a long journey through concepts such as isolate scope, breaking out the various compile, controller, pre-link, post-link functions, at least two different ways of persisting ngModel ... all of which did not work.

    After a variety of reading it was my understanding that the DOM manipulation should happen in the COMPILE function so that any DOM transformations would be available for the compile and then linking stages of the digest process. So I moved the inputTransform() call to the COMPILE function ... (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
                };
            }
        };
    

    No Luck ... same thing ... not binding to ngModel. So I discovered the concept of "isolate scope" ...

    Based on that I tried the following (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
                };
            }
        };
    

    No change ...

    I tried a number of other things but am afraid I may lose you attention soon if I have not already. The closest I got was ONE-WAY binding doing something like below ... and even here you can see that the extraction of the ngModel reference is utterly unacceptable. (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
                    };
                }
            };
        });
    

    I am EXHAUSTED and have absolutely no idea what's left to try. I know that this is a matter of my ignorance and lack of understanding on how/why AngularJS works the way it does. But every article I read leaves me asking as many questions as were answered or takes me down a rabbit hole in which I get more lost than I was when I started. Short of dropping $3000 on live in-person seminars that I cannot afford where I can ask the questions I need answered, I am at a complete dead end with Angular.

    I would be most grateful if anyone could provide guidance, direction ... a good resource ... anything that can help shed some light on this issue in particular, but anything that might help me stop spinning my wheels. In the mean-time I will continue to read and re-read everything I can find and hopefully something will break.

    Thanks

    G

    UPDATE - 10/30/2014

    I am soooo over this issue but want to follow it through. I need and want to learn this. Also I really want to express appreciation for the effort that folks have put into this and while they have presented some solutions, which ultimately may be the best way to go, they have both skirted the issue, which is that I am attempting to use the behaviors provided with the Metro UI CSS library. I would prefer to not have to rewrite them if possible.

    Both solutions provided so far have eliminated the key statement from the solution ... which is the line ...

    $(element).inputTransform()
    

    I don't want to post the entire jQuery widget that comprises the "inputTransform" definition, but I cut the meat of it out and included it here ...

        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;
        };
    

    So, I have applied the directive as an attribute because the Metro code behind it wants to CLONE the text-box (which would not do if it was an element directive) and then REMOVES the original input element. It then creates the new DOM elements and wraps the cloned input element in the newly created DIV container. The catch, I believe is ... that the binding is being broken when the original element is being cloned and removed from the DOM. Makes sense, if the "ng-model" attribute assignment is bound to a reference of the text-box. So the expectation that I originally had was, since the "ng-model" attribute was cloned along with the rest of the element, that in the compile event/function/phase of the directive the reference would be(re)established to the newly created input element. This apparently was not the case. You can see in this updated fiddle that I have made some attempts at reconnecting the ng-model to the new DOM elements with no success.

    Perhaps this is impossible ... it certainly seems that just re-building these things may ultimately be the easier way to go.

    Thanks again Mikko Viitalia and 'azium' ...

    解决方案

    Directives are not the easiest concepts out there and documentation is really not that good and it's scattered around the interwebs.

    I struggled with compile, pre-compile and such when I tried to write my first directives but to date I have never needed those functions. It might be due to my lack of understanding but still...

    Looking at your examples I see there's some basic things that needs clarification. First of all, I'd restrict your directive to Element since it's replacing the control in HTML. I'd use Attribute e.g. to add functionality to existing control.

    There is a (mandatory) naming convention where you use dashed naming in HTML and camel casing inside your JavaScript. So something-cool becomes somethingCool. When you "bind" variables to directive's scope, there's a major difference on how you do it. Using = you bind to variable, using @ to variables evaluated (string) value. So first allows the "two-way binding" but latter of course, not. You can also use & to bind to parent scope's expression/function.

    If you use e.g. plain = then directive's scope expects same name in your HTML. If you wish to use different name, then you add variable name after the =. An example

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

     

    I took liberty to take your first Fiddle of metro-input-transform as starting point and rewrite it in Plunker. I'm trying to explain it here (and hope I understood you right).

    Metro input directive

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

    Directive expects ngModel to bind to and watermark to show when ngModel has no value (text input is empty). Inside link I've introduced clear() function that is used within directive to reset ngModel. When value is reset, watermark is show. I have separated the HTML parts into a separate file, metro-template.html.

    Metro input HTML template

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

    Here we bind ngModel to input and assign placeholder. Button showing [X] is bound to clear() method.

    Now when we have our directive set up, here's the HTML page using it.

    HTML page

    <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>
    

    So in above example usage of metro directive is as follows. This will be replaced with directive's HTML template.

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

    The other input has detect-mouse-over directive applied to it, restricted to Attribute just to show usages/differences between A and E. Mouse detection directive makes input change background-color when mouse is moved over/out of it.

    <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'); 
          });
        }
      };
    });
    

    It also has same ng-model to mirror changes between controls.

    In your example you also had a productService that provided the value to above input controls. I rewrote it as

    Product service

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

    So get() function just gets the hard coded value but it still demonstrates use of services. Controller, named Ctrl is really simplistic. Important part here is that you remember to inject all services and such into your controller. In this case angular's $scope and our own productService.

    Controller

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

     

    Here a screen capture of above solution.

    Changing value in any of the inputs changes value of both. Input below has "mouseover" so it's greyish, mouseout would turn it white again. Pressing [X] clears the value and makes placeholder visible.

    Here's the link to plunker once more http://plnkr.co/edit/GGGxp0

    这篇关于将 ngModel 绑定到自定义指令的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆