AngularJS: crea una directiva que usa ng-model

294

Estoy tratando de crear una directiva que cree un campo de entrada con el mismo modelo ng que el elemento que crea la directiva.

Esto es lo que se me ocurrió hasta ahora:

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

Sin embargo, no estoy seguro de que esta sea la forma correcta de manejar este escenario, y existe un error de que mi control no se está inicializando con el valor del campo objetivo del modelo ng.

Aquí hay un Plunker del código anterior: http://plnkr.co/edit/IvrDbJ

¿Cuál es la forma correcta de manejar esto?

EDITAR : después de eliminar la ng-model="value"plantilla, parece que funciona bien. Sin embargo, mantendré esta pregunta abierta porque quiero verificar que esta sea la forma correcta de hacerlo.

kolrie
fuente
1
¿Qué pasa si lo quita scopey lo configura scope: false? ¿Cómo unirse ng-modelen ese caso?
Saeed Neamati

Respuestas:

210

EDITAR : Esta respuesta es antigua y probablemente desactualizada. Solo un aviso para que no desvíe a la gente. Ya no uso Angular, así que no estoy en una buena posición para hacer mejoras.


En realidad, es una lógica bastante buena, pero puedes simplificar un poco las cosas.

Directiva

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 con directiva

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

Puedes verlo en acción con este Plunker .

Esto es lo que veo:

  • Entiendo por qué quieres usar 'ng-model' pero en tu caso no es necesario. ng-model es vincular elementos html existentes con un valor en el ámbito. Como usted mismo está creando una directiva, está creando un 'nuevo' elemento html, por lo que no necesita ng-model.

EDITAR Como mencionó Mark en su comentario, no hay razón para que no pueda usar ng-model, solo para mantener la convención.

  • Al crear explícitamente un ámbito en su directiva (un ámbito 'aislado'), el ámbito de la directiva no puede acceder a la variable 'nombre' en el ámbito primario (que es por eso, creo, que quería usar ng-model).
  • Eliminé ngModel de su directiva y lo reemplacé con un nombre personalizado que puede cambiar a lo que sea.
  • Lo que hace que todo funcione es que el signo '=' en el ámbito. Pedido de los docs docs bajo el encabezado 'alcance'.

En general, sus directivas deben usar el alcance aislado (lo cual hizo correctamente) y usar el alcance de tipo '=' si desea que un valor en su directiva siempre se asigne a un valor en el alcance primario.

Roy Truelove
fuente
18
+1, pero no estoy seguro de estar de acuerdo con la afirmación "ng-model es vincular elementos HTML existentes con un valor en el ámbito". Los dos contenteditableejemplos de directivas en la documentación de Angular - página de formularios , página NgModelController - ambos usan ng-model. Y la página ngModelController dice que este controlador está "destinado a ser extendido por otras directivas".
Mark Rajcok
33
No estoy seguro de por qué esta respuesta tiene una calificación tan alta porque no cumple con la pregunta original, que es usar ngModel. Sí, se puede evitar el uso de ngModel poniendo el estado en el controlador principal, pero esto se produce a expensas de tener dos controladores estrechamente unidos y no poder usarlos / reutilizarlos de forma independiente. Es como usar una variable global en lugar de configurar un oyente entre dos componentes: técnicamente puede ser más simple pero no es una buena solución en la mayoría de los casos.
Pat Niemeyer
Agregaría que si quisiera confiar en el controlador principal, debería inyectarlo con 'require: ^ parent' de todos modos, para que pueda hacer que la dependencia sea explícita y opcional si lo desea.
Pat Niemeyer
3
@Jeroen Según veo, el principal beneficio es la coherencia con otros lugares donde se pasa el modelo como hg-model(y no el problema del acoplamiento, IMO). De esta forma, el contexto de datos siempre usa ng-model, ya sea una <input>directiva personalizada o una, lo que simplifica la sobrecarga cognitiva para el escritor de HTML. Es decir, ahorra que el escritor HTML tenga que averiguar cuál es el nombre my-directive-varde cada directiva, especialmente porque no hay autocompletado para ayudarlo.
zai chang
2
umm ... ok ... pero ahora esto ya no funciona con ng-model-optionsninguna de las otras cosas del modelo, ¿verdad?
George Mauer
68

Tomé un combo de todas las respuestas, y ahora tengo dos formas de hacerlo con el atributo ng-model:

  • Con un nuevo alcance que copia ngModel
  • Con el mismo alcance que hace una compilación en el enlace

No estoy seguro de que me guste la compilación en el momento del enlace. Sin embargo, si solo está reemplazando el elemento con otro, no necesita hacerlo.

En general, prefiero el primero. Simplemente establezca el alcance {ngModel:"="}y establezca ng-model="ngModel"dónde lo quiere en su plantilla.

Actualización : introduje el fragmento de código y lo actualicé para Angular v1.2. Resulta que el alcance de aislamiento es aún mejor, especialmente cuando no se usa jQuery. Entonces se reduce a:

  • ¿Está reemplazando un solo elemento? Simplemente reemplácelo, deje el alcance solo, pero tenga en cuenta que el reemplazo está en desuso para v2.0:

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

    app.directive('myDirectiveWithScope', function() {
      return {
        restrict: 'E',
        scope: {
          ngModel: '=',
        },
        template: '<div class="some"><input ng-model="ngModel"></div>'
      };
    });
w00t
fuente
1
Actualicé el plunker con las tres posibilidades de alcance y para los elementos secundarios de la plantilla o el elemento raíz de la plantilla.
w00t
1
Esto es genial, pero ¿cómo haces que esto sea opcional? Estoy creando una directiva de cuadro de texto para una biblioteca de interfaz de usuario, y quiero que el modelo sea opcional, lo que significa que el cuadro de texto seguirá funcionando si el ngModel no está configurado.
Nick Radford
1
@NickRadford Simplemente verifique si ngModel está definido en el $ scope y si no, ¿no lo usa?
w00t
1
¿Habrá algún problema o sobrecarga adicional con la reutilización ng-modelen un ámbito aislado?
Jeff Ling
2
@jeffling no estoy seguro, pero no lo creo. Copiar ngModel es bastante ligero y el alcance aislado limita la exposición.
w00t
52

no es tan complicado: en tu directorio, usa un alias: scope:{alias:'=ngModel'}

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

en tu html, úsalo normalmente

<dateselect ng-model="birthday"></dateselect>
AiShiguang
fuente
1
Esto es mucho más fácil cuando se trata de bibliotecas como Kendo UI. ¡Gracias!
bytebender
30

Solo necesita ng-model cuando necesita acceder a $ viewValue o $ modelValue del modelo. Ver NgModelController . Y en ese caso, lo usarías require: '^ngModel'.

Para el resto, vea la respuesta de Roys .

asgoth
fuente
2
ng-model también es útil incluso si no necesita $ viewValue o $ modelValue. Es útil incluso si solo desea las características de enlace de datos de ng-model, como el ejemplo de @ kolrie.
Mark Rajcok
1
Y ^debería estar allí solo si el modelo ng se aplica en un elemento padre
georgiosd
18

Esta es una respuesta un poco tardía, pero encontré esta increíble publicación sobre NgModelController, que creo que es exactamente lo que estabas buscando.

TL; DR : puede usar require: 'ngModel'y luego agregar NgModelControllera su función de enlace:

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

De esta manera, no se necesitan hacks: estás utilizando el Angular incorporado ng-model

Yaniv Efraim
fuente
2

No establecería el ngmodel a través de un atributo, puede especificarlo directamente en la plantilla:

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

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

Mathew Berg
fuente
0

Desde Angular 1.5 es posible usar componentes. Los componentes son el camino a seguir y resuelve este problema fácilmente.

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

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

Dentro de YourController, todo lo que necesita hacer es:

this.ngModel = "x"; //$scope.$apply("$ctrl.ngModel"); if needed
Niels Steenbeek
fuente
Lo que encontré es que funciona si de hecho usas "=" en lugar de "<", que de otra manera es la mejor práctica usando Componentes. No estoy seguro de lo que significa la parte "dentro de YourController" de esta respuesta, ¿el objetivo de esto no es establecer ngModel dentro del componente?
Marc Stober
1
@MarcStober Con el "dentro de YourController" solo quería mostrar que ngModel está disponible como getter y setter. En este ejemplo, $ ctrl.result se convertirá en "x".
Niels Steenbeek
Okay. Creo que la otra parte que es importante es que también puede, en la plantilla de su controlador, hacer input ng-model="$ctrl.ngModel"y se sincronizará con $ ctrl.result también.
Marc Stober
0

Crear un alcance aislado no es deseable. Evitaría usar el atributo de alcance y hacer algo como esto. alcance: verdadero le da un nuevo alcance secundario pero no aislado. Luego use parse para apuntar una variable de alcance local al mismo objeto que el usuario ha suministrado al atributo 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);
        }
    };
}]);
btm1
fuente