Cómo implementar un ng-change para una directiva personalizada


Tengo una directiva con una plantilla como

<div>
    <div ng-repeat="item in items" ng-click="updateModel(item)">
<div>

Mi directiva se declara como:

return {
    templateUrl: '...',
    restrict: 'E',
    require: '^ngModel',
    scope: {
        items: '=',
        ngModel: '=',
        ngChange: '&'
    },
    link: function postLink(scope, element, attrs) 
    {
        scope.updateModel = function(item)
        {
             scope.ngModel = item;
             scope.ngChange();
        }
    }
}

Me gustaría que ng-change se llamara cuando se hace clic en un elemento y el valor de foo ya se ha cambiado.

Es decir, si mi directiva se implementa como:

<my-directive items=items ng-model="foo" ng-change="bar(foo)"></my-directive>

Esperaría llamar a bar cuando el valor de foo se haya actualizado.

Con el código dado anteriormente, ngChange se llama con éxito, pero se llama con el valor antiguo de foo en lugar de el nuevo valor actualizado.

Una forma de resolver el problema es llamar a ngChange dentro de un tiempo de espera para ejecutarlo en algún momento en el futuro, cuando el valor de foo ya se ha cambiado. Pero esta solución me hace perder el control sobre el orden en el que se supone que las cosas deben ejecutarse y supongo que debería haber una solución más elegante.

También podría usar un vigilante sobre foo en el ámbito padre, pero esta solución realmente no da un método ngChange para ser implantado y se les ha dicho que los observadores son grandes consumidores de memoria.

¿Hay alguna manera de hacer que ngChange se ejecute sincrónicamente sin un tiempo de espera o un observador?

Ejemplo: http://plnkr.co/edit/8H6QDO8OYiOyOx8efhyJ?p=preview

Author: simeg, 2014-07-15

5 answers

Si necesita ngModel puede llamar a $setViewValue en el ngModelController, que evalúa implícitamente ng-change. El cuarto parámetro de la función de enlace debe ser el ngModelCtrl. El siguiente código hará que ng-change funcione para su directiva.

link : function(scope, element, attrs, ngModelCtrl){
    scope.updateModel = function(item) {
        ngModelCtrl.$setViewValue(item);
    }
}

Para que su solución funcione, elimine ngChange y ngModel del ámbito aislado de myDirective.

Aquí hay un plunk: http://plnkr.co/edit/UefUzOo88MwOMkpgeX07?p=preview

 54
Author: Samuli Ulmanen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-11-02 14:23:30

Tl; dr

En mi experiencia, solo necesitas heredar del ngModelCtrl. la expresión ng-change se evaluará automáticamente cuando utilice el método ngModelCtrl.$setViewValue

angular.module("myApp").directive("myDirective", function(){
  return {
    require:"^ngModel", // this is important, 
    scope:{
      ... // put the variables you need here but DO NOT have a variable named ngModel or ngChange 
    }, 
    link: function(scope, elt, attrs, ctrl){ // ctrl here is the ngModelCtrl
      scope.setValue = function(value){
        ctrl.$setViewValue(value); // this line will automatically eval your ng-change
      };
    }
  };
});

Más precisamente

ng-change se evalúa durante el ngModelCtrl.$commitViewValue() SI la referencia de objeto de tu ngModel ha cambiado. el método $commitViewValue() es llamado automáticamente por $setViewValue(value, trigger) si no utiliza el gatillo argumento o no precisando ningún ngModelOptions .

He especificado que el ng-chagese activaría automáticamente si la referencia del $viewValue cambia. Cuando su ngModel es un string o un int, no tiene que preocuparse por ello. Si su ngModel es un objeto y que acaba de cambiar algunas de sus propiedades, entonces $setViewValue no eval ngChange.

Si tomamos el ejemplo de código desde el inicio del post

scope.setValue = function(value){
    ctrl.$setViewValue(value); // this line will automatically evalyour ng-change
};
scope.updateValue = function(prop1Value){
    var vv = ctrl.$viewValue;
    vv.prop1 = prop1Value;
    ctrl.$setViewValue(vv); // this line won't eval the ng-change expression
};
 14
Author: lucienBertin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-11-30 14:41:52

Después de algunas investigaciones, parece que el mejor enfoque es usar $timeout(callback, 0).

Inicia automáticamente un ciclo $digest justo después de ejecutar la devolución de llamada.

Así que, en mi caso, la solución era usar

$timeout(scope.ngChange, 0);

De esta manera, no importa cuál sea la firma de su devolución de llamada, se ejecutará tal como la definió en el ámbito padre.

Aquí está el plunkr con tales cambios: http://plnkr.co/edit/9MGptJpSQslk8g8tD2bZ?p=preview

 9
Author: htellez,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-10-31 23:57:07

Las respuestas de Samuli Ulmanen y lucienBertin lo clavan, aunque un poco de lectura adicional en la documentación de AngularJS proporciona más consejos sobre cómo manejar esto (ver https://docs.angularjs.org/api/ng/type/ngModel.NgModelController).

Específicamente en los casos en los que está pasando objetos a set setViewValue(myObj). AngularJS Documentatation afirma:

Cuando se usa con entradas estándar, el valor de la vista siempre será una cadena (que en algunos casos se analiza en otro tipo, como un objeto Date para input [date].) Sin embargo, los controles personalizados también pueden pasar objetos a este método. En este caso, debemos hacer una copia del objeto antes de pasarlo a set setViewValue. Esto se debe a que ngModel no realiza una vigilancia profunda de los objetos, solo busca un cambio de identidad. Si solo cambia la propiedad del objeto, ngModel no se dará cuenta de que el objeto ha cambiado y no invocará las canalizaciones par parsers y valid validators. Por esta razón, no debe cambiar las propiedades de la copia una vez que se haya pasado a set setViewValue. De lo contrario, puede hacer que el valor del modelo en el ámbito cambie incorrectamente.

Para mi caso específico, mi modelo es un objeto moment date, por lo que primero debo clonar el objeto antes de llamar a setViewValue. Tengo suerte aquí, ya que moment proporciona un método de clonación simple: var b = moment(a);

link : function(scope, elements, attrs, ctrl) {
    scope.updateModel = function (value) {
        if (ctrl.$viewValue == value) {
            var copyOfObject = moment(value);
            ctrl.$setViewValue(copyOfObject);
        }
        else
        {
            ctrl.$setViewValue(value);
        }
    };
}
 0
Author: Arkiliknam,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-04 14:27:34

El problema fundamental aquí es que el modelo subyacente no se actualiza hasta que el ciclo de resumen que ocurre después de scope.updateModel ha terminado de ejecutarse. Si la función ngChange requiere detalles de la actualización que se está realizando, esos detalles se pueden poner explícitamente a disposición de ngChange, en lugar de confiar en que la actualización del modelo se haya aplicado previamente.

Esto se puede hacer proporcionando un mapa de nombres de variables locales a valores al llamar a ngChange. En este escenario, puede mapear el nuevo valor del modelo a un nombre que puede ser referenciado en la expresión ng-change.

Por ejemplo:

scope.updateModel = function(item)
{
    scope.ngModel = item;
    scope.ngChange({newValue: item});
}

En el HTML:

<my-directive ng-model="foo" items=items ng-change="bar(newValue)"></my-directive>

Véase: http://plnkr.co/edit/4CQBEV1S2wFFwKWbWec3?p=preview

 -2
Author: chrisg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-16 00:18:33