ChatGPT解决这个技术问题 Extra ChatGPT

AngularJS - Create a directive that uses ng-model

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.

What if you remove scope and set it to scope: false? How to bind to ng-model in that case?

R
Roy Truelove

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.


+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."
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.
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.
@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 <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.
umm...ok...but now this no longer works with ng-model-options or any of the other ng model things, does it?
w
w00t

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

', replace: true }; }); app.directive('myDirectiveWithChildScope', function($compile) { return { restrict: 'E', scope: true, // Notice how label is visible in the scope template: '
', 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: '
', 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: '', replace: true }; }); app.directive('myReplacedDirectiveChild', function($compile) { return { restrict: 'E', scope: true, template: '', replace: true }; }); app.directive('myReplacedDirective', function($compile) { return { restrict: 'E', template: '', replace: true }; }); .some { border: 1px solid #cacaca; padding: 10px; }
This scope value , label: "{{label}}"
  • With new isolate scope (label from parent):
  • With new child scope:
  • Same scope:
  • Replaced element, isolate scope:
  • Replaced element, child scope:
  • Replaced element, same scope:

Try typing in the child scope ones, they copy the value into the child scope which breaks the link with the parent scope.

Also notice how removing jQuery makes it so only the new-isolate-scope version works.

Finally, note that the replace+isolate scope only works in AngularJS >=1.2.0

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: '', replace: true }; });

Otherwise use this: app.directive('myDirectiveWithScope', function() { return { restrict: 'E', scope: { ngModel: '=', }, template: '

' }; });


I updated the plunker with all three scope possibilities and for child elements of the template or the root element of the template.
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.
@NickRadford Simply check if ngModel is defined on the $scope and if not, don't use it?
Will there be any problems or additional overhead with reusing ng-model in an isolated scope?
@jeffling not sure but I don't think so. Copying ngModel is pretty light weight and isolated scope limits exposure.
A
AiShiguang

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>

This is so much easier when dealing with libraries like Kendo UI. Thanks!
C
Community

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.


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.
And the ^ should be there only if the ng-model is applied in a parent element
d
doublesharp

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


M
Mathew Berg

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


N
Niels Steenbeek

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

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?
@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".
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.
b
btm1

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