Binding ngModel to a custom directive

16,807

Solution 1

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.

imgur

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

Solution 2

Ok I'm not exactly sure what other advantages from the Metro UI you're getting, but here's a simple fiddle that doesn't need your directive at all to capture what you had in your first fiddle that works for me. 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>

The second fiddle you posted, http://jsfiddle.net/gary_stenstrom/xcx2y8uk/64/, is pretty weird to me, because it doesn't seem like you want the second input box to be the same model as your first one. It kind of seems like you want the clicking of the x button to assign the value of the first input to the second. Which makes a lot more sense.

<input ng-model="data.first">
<button ng-click="data.second = data.first; data.first=''">X</button
<input ng-model="data.second">
Share:
16,807

Related videos on Youtube

Gary O. Stenstrom
Author by

Gary O. Stenstrom

Updated on June 30, 2022

Comments

  • Gary O. Stenstrom
    Gary O. Stenstrom almost 2 years

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

  • Gary O. Stenstrom
    Gary O. Stenstrom over 9 years
    This is A solution but it sidesteps the use of the Metro UI library. This would require me to rewrite all the js behaviors provided with Metro UI. Ultimately perhaps that is the answer. I have established that it is the "inputTransform()" call that is breaking the binding ...
  • Gary O. Stenstrom
    Gary O. Stenstrom over 9 years
    Thanks for the effort and explanation. I may just be spitting into the wind here and may need to just change my approach. I do not know that the direction I am taking is worth the effort that may be required to make it work. In lieu of a "pure" solution I will probably change my approach to what you have described here ... I still gotta keep the project moving! Thanks again!
  • Gary O. Stenstrom
    Gary O. Stenstrom over 9 years
    So I marked this as the answer since StackOverflow essentially requires you to check something as an answer in order to stay in good standing. While it doesn't address the EXACT issue it IS the route I have taken in order to address my particular situation. I was spending more time trying to figure out the original problem rather than just taking a different path. Thanks Mikko! You got me moving again and I appreciate it!