AngularJS - Create a directive that uses ng-model
Asked Answered
H

8

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.

Hazelton answered 2/1, 2013 at 0:44 Comment(1)
What if you remove scope and set it to scope: false? How to bind to ng-model in that case?Clyte
U
213

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.

Uis answered 2/1, 2013 at 3:18 Comment(7)
+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, NgModelController page -- both use ng-model. And the ngModelController page says that this controller is "meant to be extended by other directives."Ectogenous
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.Triacid
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.Triacid
@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?Leprose
@Leprose 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 <input> 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.Sphagnum
In case you're confused by $scope & scope in link function - thinkster.io/a-better-way-to-learn-angularjs/scope-vs-scopeIl
umm...ok...but now this no longer works with ng-model-options or any of the other ng model things, does it?Orchid
D
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>'
      };
    });
    
Dedans answered 3/6, 2013 at 15:0 Comment(9)
I updated the plunker with all three scope possibilities and for child elements of the template or the root element of the template.Dedans
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.Leman
@NickRadford Simply check if ngModel is defined on the $scope and if not, don't use it?Dedans
Will there be any problems or additional overhead with reusing ng-model in an isolated scope?Lan
@jeffling not sure but I don't think so. Copying ngModel is pretty light weight and isolated scope limits exposure.Dedans
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.Pipkin
@Alisson did try using link?Dedans
@Dedans 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!Pipkin
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: plnkr.co/edit/0oFTarMU7HEFYnNC5DFf?p=previewRafflesia
C
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>
Chainplate answered 25/3, 2014 at 5:40 Comment(1)
This is so much easier when dealing with libraries like Kendo UI. Thanks!Wunder
W
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.

Weltschmerz answered 2/1, 2013 at 6:41 Comment(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.Ectogenous
And the ^ should be there only if the ng-model is applied in a parent elementEratosthenes
E
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

Et answered 28/7, 2015 at 15:40 Comment(0)
A
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

Airdrie answered 2/1, 2013 at 3:10 Comment(0)
A
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);
        }
    };
}]);
Automata answered 18/6, 2017 at 1:0 Comment(0)
F
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
Falchion answered 16/8, 2016 at 10:7 Comment(3)
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?Harbourage
@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".Falchion
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.Harbourage

© 2022 - 2024 — McMap. All rights reserved.