web-dev-qa-db-fra.com

AngularJS - Crée une directive utilisant ng-model

J'essaie de créer une directive qui créerait un champ d'entrée avec le même modèle-ng que l'élément qui crée la directive.

Voici ce que je suis arrivé jusqu'à présent:

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

Cependant, je ne suis pas convaincu que ce soit la bonne façon de gérer ce scénario et il y a un problème qui dit que mon contrôle n'est pas initialisé avec la valeur du champ cible ng-model.

Voici un Plunker du code ci-dessus: http://plnkr.co/edit/IvrDbJ

Quelle est la bonne façon de gérer cela?

EDIT: après avoir supprimé le ng-model="value" du modèle, cela semble fonctionner correctement. Cependant, je vais garder cette question ouverte parce que je veux vérifier que c’est la bonne façon de procéder.

289
kolrie

EDIT: cette réponse est ancienne et probablement obsolète. Juste un tête-à-tête pour ne pas égarer les gens. Je n'utilise plus Angular, je ne suis donc pas bien placé pour apporter des améliorations.


C'est en fait une bonne logique mais vous pouvez simplifier un peu les choses.

Directif

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

Vous pouvez le voir en action avec ce Plunker .

Voici ce que je vois:

  • Je comprends pourquoi vous voulez utiliser 'ng-model' mais dans votre cas ce n'est pas nécessaire. ng-model consiste à lier _ {existants} _ éléments HTML avec une valeur dans la portée. Puisque vous créez vous-même une directive, vous créez un "nouvel" élément HTML, vous n'avez donc pas besoin de ng-model.

EDIT Comme mentionné par Mark dans son commentaire, il n'y a aucune raison pour que vous {ne puissiez pas utiliser ng-model, juste pour respecter les conventions.

  • En créant explicitement une portée dans votre directive (une portée "isolée"), la portée de la directive ne peut pas accéder à la variable "nom" de la portée parente (c'est pourquoi, je pense, vous avez voulu utiliser le modèle ng).
  • J'ai supprimé ngModel de votre directive et l'ai remplacée par un nom personnalisé que vous pouvez modifier.
  • La chose qui fait que tout fonctionne encore est que le signe '=' dans le champ d'application. Consultez la documentation docs sous l'en-tête 'scope'.

En général, vos directives doivent utiliser la portée isolée (ce que vous avez fait correctement) et utiliser la portée de type '=' si vous souhaitez qu'une valeur de votre directive mappe toujours à une valeur de la portée parent. 

207
Roy Truelove

J'ai pris une combinaison de toutes les réponses, et maintenant, j'ai deux façons de le faire avec l'attribut ng-model:

  • Avec une nouvelle portée qui copie ngModel
  • Avec la même portée qui fait une compilation sur le lien

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>

Je ne suis pas sûr d'aimer la compilation au moment du lien. Cependant, si vous ne remplacez que l'élément par un autre, vous n'avez pas besoin de le faire.

Dans l'ensemble, je préfère le premier. Il suffit de définir la portée sur {ngModel:"="} et de définir ng-model="ngModel" où vous le souhaitez dans votre modèle.

Update : J'ai inséré l'extrait de code et l'ai mis à jour pour Angular v1.2. Il s'avère que la portée isolée est toujours préférable, surtout lorsque vous n'utilisez pas jQuery. Cela revient donc à:

  • Remplacez-vous un seul élément: remplacez-le, laissez la portée, mais notez que le remplacement est obsolète pour la v2.0:

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

    app.directive('myDirectiveWithScope', function() {
      return {
        restrict: 'E',
        scope: {
          ngModel: '=',
        },
        template: '<div class="some"><input ng-model="ngModel"></div>'
      };
    });
    
66
w00t

ce n'est pas si compliqué: dans votre dirctive, utilisez un alias: scope:{alias:'=ngModel'}

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

dans votre html, utilisez comme d'habitude

<dateselect ng-model="birthday"></dateselect>
50
AiShiguang

Vous n'avez besoin de ng-model que pour accéder aux $ viewValue ou $ modelValue du modèle. Voir NgModelController . Et dans ce cas, vous utiliseriez require: '^ngModel'.

Pour le reste, voir Roys répond .

30
asgoth

C’est une réponse un peu tardive, mais j’ai trouvé cet impressionnant post à propos de NgModelController, que je pense être exactement ce que vous cherchiez. 

TL; DR - vous pouvez utiliser require: 'ngModel' puis ajouter NgModelController à votre fonction de liaison:

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

De cette façon, aucun piratage n'est nécessaire - vous utilisez le ng-model intégré à Angular

16
Yaniv Efraim

Je ne définirais pas le ngmodel via un attribut, vous pouvez le spécifier directement dans le modèle:

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

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

2
Mathew Berg

Créer une portée isolée n'est pas souhaitable. Je voudrais éviter d'utiliser l'attribut scope et faire quelque chose comme ça. scope: true vous donne une nouvelle portée enfant mais pas isoler. Utilisez ensuite analyse pour faire pointer une variable de portée locale sur le même objet que l'utilisateur a fourni à l'attribut ngModel.

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

Depuis Angular 1.5, il est possible d'utiliser des composants. Les composants sont la voie à suivre et résolvent ce problème facilement.

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

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

Dans YourController, tout ce que vous devez faire est de:

this.ngModel = "x"; //$scope.$apply("$ctrl.ngModel"); if needed
0
Niels Steenbeek