298

I am trying to create a directive that would create an input field with the same ng-model as the element that creates the directive.

Here's what I came up with so far:

HTML

<!doctype html>
<html ng-app="plunker" >
<head>
  <meta charset="utf-8">
  <title>AngularJS Plunker</title>
  <link rel="stylesheet" href="style.css">
  <script>document.write("<base href=\"" + document.location + "\" />");</script>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.js"></script>
  <script src="app.js"></script>
</head>
<body ng-controller="MainCtrl">
  This scope value <input ng-model="name">
  <my-directive ng-model="name"></my-directive>
</body>
</html>

JavaScript

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope) {
  $scope.name = "Felipe";
});

app.directive('myDirective', function($compile) {
  return {
    restrict: 'E',
    scope: {
      ngModel: '='
    },
    template: '<div class="some"><label for="{{id}}">{{label}}</label>' +
      '<input id="{{id}}" ng-model="value"></div>',
    replace: true,
    require: 'ngModel',
    link: function($scope, elem, attr, ctrl) {
      $scope.label = attr.ngModel;
      $scope.id = attr.ngModel;
      console.debug(attr.ngModel);
      console.debug($scope.$parent.$eval(attr.ngModel));
      var textField = $('input', elem).
        attr('ng-model', attr.ngModel).
        val($scope.$parent.$eval(attr.ngModel));

      $compile(textField)($scope.$parent);
    }
  };
});

However, I am not confident this is the right way to handle this scenario, and there is a bug that my control is not getting initialized with the value of the ng-model target field.

Here's a Plunker of the code above: http://plnkr.co/edit/IvrDbJ

What's the correct way of handling this?

EDIT: After removing the ng-model="value" from the template, this seems to be working fine. However, I will keep this question open because I want to double check this is the right way of doing this.

TimPetricola
  • 1,491
  • 2
  • 12
  • 24
kolrie
  • 12,562
  • 14
  • 64
  • 98

8 Answers8

212

EDIT: This answer is old and likely out of date. Just a heads up so it doesn't lead folks astray. I no longer use Angular so I'm not in a good position to make improvements.


It's actually pretty good logic but you can simplify things a bit.

Directive

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope) {
  $scope.model = { name: 'World' };
  $scope.name = "Felipe";
});

app.directive('myDirective', function($compile) {
  return {
    restrict: 'AE', //attribute or element
    scope: {
      myDirectiveVar: '=',
     //bindAttr: '='
    },
    template: '<div class="some">' +
      '<input ng-model="myDirectiveVar"></div>',
    replace: true,
    //require: 'ngModel',
    link: function($scope, elem, attr, ctrl) {
      console.debug($scope);
      //var textField = $('input', elem).attr('ng-model', 'myDirectiveVar');
      // $compile(textField)($scope.$parent);
    }
  };
});

Html with directive

<body ng-controller="MainCtrl">
  This scope value <input ng-model="name">
  <my-directive my-directive-var="name"></my-directive>
</body>

CSS

.some {
  border: 1px solid #cacaca;
  padding: 10px;
}

You can see it in action with this Plunker.

Here's what I see:

  • I understand why you want to use 'ng-model' but in your case it's not necessary. ng-model is to link existing html elements with a value in the scope. Since you're creating a directive yourself you're creating a 'new' html element, so you don't need ng-model.

EDIT As mentioned by Mark in his comment, there's no reason that you can't use ng-model, just to keep with convention.

  • By explicitly creating a scope in your directive (an 'isolated' scope), the directive's scope cannot access the 'name' variable on the parent scope (which is why, I think, you wanted to use ng-model).
  • I removed ngModel from your directive and replaced it with a custom name that you can change to whatever.
  • The thing that makes it all still work is that '=' sign in the scope. Checkout the docs docs under the 'scope' header.

In general, your directives should use the isolated scope (which you did correctly) and use the '=' type scope if you want a value in your directive to always map to a value in the parent scope.

Roy Truelove
  • 22,016
  • 18
  • 111
  • 153
  • 18
    +1, but I'm not sure I agree with the statement "ng-model is to link existing HTML elements with a value in the scope." The two `contenteditable` directive examples in the Angular docs -- [forms page](http://docs.angularjs.org/guide/forms), [NgModelController page](http://docs.angularjs.org/api/ng.directive:ngModel.NgModelController) -- both use ng-model. And the ngModelController page says that this controller is "meant to be extended by other directives." – Mark Rajcok Feb 16 '13 at 03:59
  • 35
    I am not sure why this answer is rated so highly because it does not accomplish what the original question asked - which is to use ngModel. Yes, one can avoid using ngModel by putting state in the parent controller but this comes at the expense of having two controllers tightly bound and not being able to use / reuse them independently. It's like using a global variable instead of setting up a listener between two components - it may technically be simpler but it's not a good solution in most cases. – Pat Niemeyer Aug 16 '14 at 18:21
  • I'd add that if he wanted to rely on the parent controller he should inject it with 'require: ^parent' anyway - so that he can make the dependency explicit and optional if desired. – Pat Niemeyer Aug 16 '14 at 18:21
  • @PatNiemeyer maybe I am overlooking something but when I compare the 2 plunkers I see no difference in controller usage so I do not see how the second version (without ng-model) creates highly coupled controllers? – Jeroen Dec 11 '14 at 08:23
  • 3
    @Jeroen The way I see it the main benefit is the consistency with other places where the model is passed in as `hg-model` (and not the issue of coupling, IMO). This way the data context always uses ng-model whether it is a `` or a custom directive, thus simplifying cognitive overhead for the HTML writer. I.e. it saves the HTML writer having to find out what the the name for `my-directive-var` is for each directive, especially since there's no autocomplete to help you. – zai chang Apr 05 '15 at 06:32
  • In case you're confused by `$scope` & `scope` in `link` function - https://thinkster.io/a-better-way-to-learn-angularjs/scope-vs-scope – Rohit May 28 '15 at 17:47
  • 2
    umm...ok...but now this no longer works with `ng-model-options` or any of the other ng model things, does it? – George Mauer Dec 05 '16 at 14:27
69

I took a combo of all answers, and now have two ways of doing this with the ng-model attribute:

  • With a new scope which copies ngModel
  • With the same scope which does a compile on link

var app = angular.module('model', []);

app.controller('MainCtrl', function($scope) {
  $scope.name = "Felipe";
  $scope.label = "The Label";
});

app.directive('myDirectiveWithScope', function() {
  return {
    restrict: 'E',
    scope: {
      ngModel: '=',
    },
    // Notice how label isn't copied
    template: '<div class="some"><label>{{label}}: <input ng-model="ngModel"></label></div>',
    replace: true
  };
});
app.directive('myDirectiveWithChildScope', function($compile) {
  return {
    restrict: 'E',
    scope: true,
    // Notice how label is visible in the scope
    template: '<div class="some"><label>{{label}}: <input></label></div>',
    replace: true,
    link: function ($scope, element) {
      // element will be the div which gets the ng-model on the original directive
      var model = element.attr('ng-model');
      $('input',element).attr('ng-model', model);
      return $compile(element)($scope);
    }
  };
});
app.directive('myDirectiveWithoutScope', function($compile) {
  return {
    restrict: 'E',
    template: '<div class="some"><label>{{$parent.label}}: <input></label></div>',
    replace: true,
    link: function ($scope, element) {
      // element will be the div which gets the ng-model on the original directive
      var model = element.attr('ng-model');
      return $compile($('input',element).attr('ng-model', model))($scope);
    }
  };
});
app.directive('myReplacedDirectiveIsolate', function($compile) {
  return {
    restrict: 'E',
    scope: {},
    template: '<input class="some">',
    replace: true
  };
});
app.directive('myReplacedDirectiveChild', function($compile) {
  return {
    restrict: 'E',
    scope: true,
    template: '<input class="some">',
    replace: true
  };
});
app.directive('myReplacedDirective', function($compile) {
  return {
    restrict: 'E',
    template: '<input class="some">',
    replace: true
  };
});
.some {
  border: 1px solid #cacaca;
  padding: 10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0/angular.min.js"></script>
<div ng-app="model" ng-controller="MainCtrl">
  This scope value <input ng-model="name">, label: "{{label}}"
  <ul>
    <li>With new isolate scope (label from parent):
      <my-directive-with-scope ng-model="name"></my-directive-with-scope>
    </li>
    <li>With new child scope:
      <my-directive-with-child-scope ng-model="name"></my-directive-with-child-scope>
    </li>
    <li>Same scope:
      <my-directive-without-scope ng-model="name"></my-directive-without-scope>
    </li>
    <li>Replaced element, isolate scope:
      <my-replaced-directive-isolate ng-model="name"></my-replaced-directive-isolate>
    </li>
    <li>Replaced element, child scope:
      <my-replaced-directive-child ng-model="name"></my-replaced-directive-child>
    </li>
    <li>Replaced element, same scope:
      <my-replaced-directive ng-model="name"></my-replaced-directive>
    </li>
  </ul>
  <p>Try typing in the child scope ones, they copy the value into the child scope which breaks the link with the parent scope.
  <p>Also notice how removing jQuery makes it so only the new-isolate-scope version works.
  <p>Finally, note that the replace+isolate scope only works in AngularJS >=1.2.0
</div>

I'm not sure I like the compiling at link time. However, if you're just replacing the element with another you don't need to do that.

All in all I prefer the first one. Simply set scope to {ngModel:"="} and set ng-model="ngModel" where you want it in your template.

Update: I inlined the code snippet and updated it for Angular v1.2. Turns out that isolate scope is still best, especially when not using jQuery. So it boils down to:

  • Are you replacing a single element: Just replace it, leave the scope alone, but note that replace is deprecated for v2.0:

    app.directive('myReplacedDirective', function($compile) {
      return {
        restrict: 'E',
        template: '<input class="some">',
        replace: true
      };
    });
    
  • Otherwise use this:

    app.directive('myDirectiveWithScope', function() {
      return {
        restrict: 'E',
        scope: {
          ngModel: '=',
        },
        template: '<div class="some"><input ng-model="ngModel"></div>'
      };
    });
    
w00t
  • 17,944
  • 8
  • 54
  • 62
  • 1
    I updated the plunker with all three scope possibilities and for child elements of the template or the root element of the template. – w00t Jun 04 '13 at 11:06
  • 1
    This is great, but how do you essentially make this optional? I'm creating a textbox directive for a UI library, and I want the model to be optional, meaning the textbox still will work if the ngModel isn't set. – Nick Radford Sep 18 '13 at 16:58
  • 1
    @NickRadford Simply check if ngModel is defined on the $scope and if not, don't use it? – w00t Sep 19 '13 at 21:10
  • 1
    Will there be any problems or additional overhead with reusing `ng-model` in an isolated scope? – Jeff Ling Apr 17 '14 at 06:40
  • 2
    @jeffling not sure but I don't think so. Copying ngModel is pretty light weight and isolated scope limits exposure. – w00t Apr 17 '14 at 06:47
  • Does this work without `template`? I tried all these options, none of them seems to work. My directive is a wrapper to a jquery library, so I don't have the html yet. This library allows me to insert an input html to be placed inside the component, but using this doesn' seem to work. – Alisson Reinaldo Silva Jan 09 '17 at 13:12
  • @Alisson did try using `link`? – w00t Jan 10 '17 at 13:37
  • 1
    @w00t yes. Actually I found another way to resolve my specific situation, which didn't require using a model inside my directive. Thanks for the reply! – Alisson Reinaldo Silva Jan 10 '17 at 16:16
  • Just a FYI, this is not the right way to support the ngModel if you care about how the Parsers and Formatters run in the pipeline, basically doing it like this will make it impossible to put a Parser in between the Directive and the model by adding another custom directive that only controls that... See: https://plnkr.co/edit/0oFTarMU7HEFYnNC5DFf?p=preview – Jens May 20 '19 at 10:27
54

it' s not so complicated: in your dirctive, use an alias: scope:{alias:'=ngModel'}

.directive('dateselect', function () {
return {
    restrict: 'E',
    transclude: true,
    scope:{
        bindModel:'=ngModel'
    },
    template:'<input ng-model="bindModel"/>'
}

in your html, use as normal

<dateselect ng-model="birthday"></dateselect>
AiShiguang
  • 1,031
  • 1
  • 9
  • 13
31

You only need ng-model when you need to access the model's $viewValue or $modelValue. See NgModelController. And in that case, you would use require: '^ngModel'.

For the rest, see Roys answer.

Community
  • 1
  • 1
asgoth
  • 35,552
  • 12
  • 89
  • 98
  • 2
    ng-model is also useful even if you don't need $viewValue or $modelValue. It is useful even if you only want the data-binding features of ng-model, like @kolrie's example. – Mark Rajcok Feb 09 '13 at 20:02
  • 1
    And the `^` should be there only if the ng-model is applied in a parent element – georgiosd Sep 04 '13 at 22:14
19

This is a little late answer, but I found this awesome post about NgModelController, which I think is exactly what you were looking for.

TL;DR - you can use require: 'ngModel' and then add NgModelController to your linking function:

link: function(scope, iElement, iAttrs, ngModelCtrl) {
  //TODO
}

This way, no hacks needed - you are using Angular's built-in ng-model

doublesharp
  • 26,888
  • 6
  • 52
  • 73
Yaniv Efraim
  • 6,633
  • 7
  • 53
  • 96
2

I wouldn't set the ngmodel via an attribute, you can specify it right in the template:

template: '<div class="some"><label>{{label}}</label><input data-ng-model="ngModel"></div>',

plunker: http://plnkr.co/edit/9vtmnw?p=preview

Mathew Berg
  • 28,625
  • 11
  • 69
  • 90
1

Creating an isolate scope is undesirable. I would avoid using the scope attribute and do something like this. scope:true gives you a new child scope but not isolate. Then use parse to point a local scope variable to the same object the user has supplied to the ngModel attribute.

app.directive('myDir', ['$parse', function ($parse) {
    return {
        restrict: 'EA',
        scope: true,
        link: function (scope, elem, attrs) {
            if(!attrs.ngModel) {return;}
            var model = $parse(attrs.ngModel);
            scope.model = model(scope);
        }
    };
}]);
btm1
  • 3,866
  • 2
  • 23
  • 26
0

Since Angular 1.5 it's possible to use Components. Components are the-way-to-go and solves this problem easy.

<myComponent data-ng-model="$ctrl.result"></myComponent>

app.component("myComponent", {
    templateUrl: "yourTemplate.html",
    controller: YourController,
    bindings: {
        ngModel: "="
    }
});

Inside YourController all you need to do is:

this.ngModel = "x"; //$scope.$apply("$ctrl.ngModel"); if needed
Niels Steenbeek
  • 4,692
  • 2
  • 41
  • 50
  • What I found is that it works if you do indeed use "=" rather than "<" which is otherwise best practice using Components. I'm not sure what the "inside YourController" part of this answer means, the point of this is not to set ngModel inside the component? – Marc Stober Feb 06 '17 at 20:02
  • 1
    @MarcStober With the "inside YourController" I only wanted to show that the ngModel is available as getter and setter. In this example the $ctrl.result will become "x". – Niels Steenbeek Feb 07 '17 at 08:06
  • Ok. I think the other part that's important is you can also, in your controller template, do `input ng-model="$ctrl.ngModel"` and it will sync with with $ctrl.result also. – Marc Stober Feb 07 '17 at 16:09