Los componentes AngularJS 1.5+ no son compatibles con Watchers, ¿cuál es la solución?

78

He estado actualizando mis directivas personalizadas a la nueva arquitectura de componentes . He leído que los componentes no son compatibles con los observadores. ¿Es esto correcto? Si es así, ¿cómo detecta cambios en un objeto? Para un ejemplo básico, tengo un componente personalizado myBoxque tiene un juego de componentes secundarios con un enlace en el juego. Si hay un juego de cambio dentro del componente del juego, ¿cómo muestro un mensaje de alerta dentro de myBox? Entiendo que hay un método rxJS, ¿es posible hacer esto puramente en angular? Mi JSFiddle

JavaScript

var app = angular.module('myApp', []);
app.controller('mainCtrl', function($scope) {

   $scope.name = "Tony Danza";

});

app.component("myBox",  {
      bindings: {},
      controller: function($element) {
        var myBox = this;
        myBox.game = 'World Of warcraft';
        //IF myBox.game changes, show alert message 'NAME CHANGE'
      },
      controllerAs: 'myBox',
      templateUrl: "/template",
      transclude: true
})
app.component("game",  {
      bindings: {game:'='},
      controller: function($element) {
        var game = this;


      },
      controllerAs: 'game',
      templateUrl: "/template2"
})

HTML

<div ng-app="myApp" ng-controller="mainCtrl">
  <script type="text/ng-template" id="/template">
    <div style='width:40%;border:2px solid black;background-color:yellow'>
      Your Favourite game is: {{myBox.game}}
      <game game='myBox.game'></game>
    </div>
  </script>

 <script type="text/ng-template" id="/template2">
    <div>
    </br>
        Change Game
      <textarea ng-model='game.game'></textarea>
    </div>
  </script>

  Hi {{name}}
  <my-box>

  </my-box>

</div><!--end app-->
Ka Tech
fuente

Respuestas:

157

Escribir componentes sin observadores

Esta respuesta describe cinco técnicas que se pueden usar para escribir componentes de AngularJS 1.5 sin usar observadores.


Utilice la ng-changedirectiva

¿Qué métodos alternativos están disponibles para observar los cambios de estado de obj sin usar watch en preparación para AngularJs2?

Puede utilizar la ng-changedirectiva para reaccionar a los cambios de entrada.

<textarea ng-model='game.game' 
          ng-change="game.textChange(game.game)">
</textarea>

Y para propagar el evento a un componente principal, el controlador de eventos debe agregarse como un atributo del componente secundario.

<game game='myBox.game' game-change='myBox.gameChange($value)'></game>

JS

app.component("game",  {
      bindings: {game:'=',
                 gameChange: '&'},
      controller: function() {
        var game = this;
        game.textChange = function (value) {
            game.gameChange({$value: value});
        });

      },
      controllerAs: 'game',
      templateUrl: "/template2"
});

Y en el componente principal:

myBox.gameChange = function(newValue) {
    console.log(newValue);
});

Este es el método preferido en el futuro. La estrategia de AngularJS de usar $watchno es escalable porque es una estrategia de sondeo. Cuando el número de $watchoyentes alcanza alrededor de 2000, la interfaz de usuario se vuelve lenta. La estrategia en Angular 2 es hacer que la colocación del marco más reactivo y evitar $watchen $scope.


Utilice el $onChangesgancho de ciclo de vida

Con la versión 1.5.3 , AngularJS agregó el enlace del $onChangesciclo de vida al $compileservicio.

De los Docs:

El controlador puede proporcionar los siguientes métodos que actúan como ganchos del ciclo de vida:

  • $ onChanges (changesObj): se llama siempre que se actualizan los enlaces unidireccionales ( <) o interpolación ( @). El changesObjes un hash cuyas claves son los nombres de las propiedades enlazadas que han cambiado y los valores son un objeto del formulario { currentValue: ..., previousValue: ... }. Utilice este enlace para activar actualizaciones dentro de un componente, como la clonación del valor vinculado para evitar la mutación accidental del valor externo.

- Referencia de API de directiva integral de AngularJS - Ganchos de ciclo de vida

El $onChangesgancho se utiliza para reaccionar a cambios externos en el componente con <enlaces unidireccionales. La ng-changedirectiva se utiliza para propagar cambios desde el ng-modelcontrolador fuera del componente con &enlaces.


Utilice el $doCheckgancho de ciclo de vida

Con la versión 1.5.8 , AngularJS agregó el enlace$doCheck del ciclo de vida al $compileservicio.

De los Docs:

El controlador puede proporcionar los siguientes métodos que actúan como ganchos del ciclo de vida:

  • $doCheck()- Llamado en cada turno del ciclo de digestión. Brinda la oportunidad de detectar cambios y actuar sobre ellos. Cualquier acción que desee realizar en respuesta a los cambios que detecte debe invocarse desde este gancho; implementar esto no tiene ningún efecto sobre cuándo $onChangesse llama. Por ejemplo, este gancho podría ser útil si desea realizar una verificación de igualdad profunda, o para verificar un objeto Date, cuyos cambios no serían detectados por el detector de cambios de Angular y, por lo tanto, no se activarían $onChanges. Este gancho se invoca sin argumentos; si detecta cambios, debe almacenar los valores anteriores para compararlos con los valores actuales.

- Referencia de API de directiva integral de AngularJS - Ganchos de ciclo de vida


Comunicación entre componentes con require

Las directivas pueden requerir que los controladores de otras directivas permitan la comunicación entre ellos. Esto se puede lograr en un componente proporcionando un mapeo de objetos para la propiedad require . Las claves de objeto especifican los nombres de propiedad bajo los cuales los controladores requeridos (valores de objeto) estarán vinculados al controlador del componente requerido.

app.component('myPane', {
  transclude: true,
  require: {
    tabsCtrl: '^myTabs'
  },
  bindings: {
    title: '@'
  },
  controller: function() {
    this.$onInit = function() {
      this.tabsCtrl.addPane(this);
      console.log(this);
    };
  },
  templateUrl: 'my-pane.html'
});

Para obtener más información, consulte la Guía para desarrolladores de AngularJS: comunicación entre componentes


Enviar valores desde un servicio con RxJS

¿Qué pasa en una situación en la que tiene un Servicio que tiene un estado, por ejemplo? ¿Cómo podría impulsar cambios en ese Servicio y otros componentes aleatorios en la página estar al tanto de dicho cambio? Últimamente he estado luchando para abordar este problema

Cree un servicio con extensiones RxJS para Angular .

<script src="//unpkg.com/angular/angular.js"></script>
<script src="//unpkg.com/rx/dist/rx.all.js"></script>
<script src="//unpkg.com/rx-angular/dist/rx.angular.js"></script>
var app = angular.module('myApp', ['rx']);

app.factory("DataService", function(rx) {
  var subject = new rx.Subject(); 
  var data = "Initial";

  return {
      set: function set(d){
        data = d;
        subject.onNext(d);
      },
      get: function get() {
        return data;
      },
      subscribe: function (o) {
         return subject.subscribe(o);
      }
  };
});

Entonces simplemente suscríbase a los cambios.

app.controller('displayCtrl', function(DataService) {
  var $ctrl = this;

  $ctrl.data = DataService.get();
  var subscription = DataService.subscribe(function onNext(d) {
      $ctrl.data = d;
  });

  this.$onDestroy = function() {
      subscription.dispose();
  };
});

Los clientes pueden suscribirse a los cambios con DataService.subscribey los productores pueden impulsar cambios con DataService.set.

La DEMO en PLNKR .

Georgeawg
fuente
Gracias. Estoy buscando reaccionar al cambio de myBox.game en lugar de game.gameChange. Como esto no es una entrada sino una etiqueta, es posible que lo anterior no funcione. Creo que al final tendré que recurrir a rxjs ...
Ka Tech
Agregué información para propagar eventos a los componentes principales.
georgeawg
¿Cómo manejará esto el cambio de valor programático en la myBox.gamevariable?
Pankaj Parkar
Gran respuesta @georgeawg. ¿Qué pasa en una situación en la que tienes un Serviceestado de espera, por ejemplo? ¿Cómo podría impulsar cambios en ese Servicio y otros componentes aleatorios en la página estar al tanto de tal cambio? Últimamente
he
1
Buena respuesta, solo diga que puede mejorar su solución RxJS usando rx.BehaviorSubject () en lugar de rx.Subject (), que hace el trabajo de almacenar el último valor por sí mismo, y se puede inicializar con un valor predeterminado como lo hizo a su servicio
Manuel Ferreiro
8

$watchEl objeto está disponible dentro del $scopeobjeto, por lo que debe agregar $scopela función de fábrica de su controlador y luego colocar el observador sobre la variable.

$scope.$watch(function(){
    return myBox.game;
}, function(newVal){
   alert('Value changed to '+ newVal)
});

Demo aquí

Nota: Sé que se ha convertido directivea component, para eliminar la dependencia de $scopepara que pueda acercarse un paso más a Angular2. Pero parece que no se eliminó en este caso.

Actualizar

Básicamente, el .componentmétodo angular 1.5 solo diferencia dos funcionalidades diferentes. Like component.stands para realizar la adición de comportamientos particulares selector, mientras que as directiveson para añadir comportamientos específicos a DOM. La directiva es solo un método de envoltura en .directiveDDO (objeto de definición de directiva). Solo lo que puede ver es que tenían la link/compilefunción de eliminación mientras usaban el .componentmétodo en el que tenía la capacidad de obtener DOM compilado angular.

Hacer uso $onChanges/ $doCheckgancho del ciclo de vida del ciclo de vida del componente angular de gancho, los estará disponible después angular versión 1.5.3+.

$ onChanges (changesObj) : se llama cuando se actualizan los enlaces. ChangesObj es un hash cuyas claves son los nombres de las propiedades enlazadas.

$ doCheck () : se llama en cada turno del ciclo de resumen cuando cambia el enlace. Brinda la oportunidad de detectar cambios y actuar sobre ellos.

Al usar la misma función dentro del componente, garantizará que su código sea compatible para pasar a Angular 2.

Pankaj Parkar
fuente
Gracias por esto, supongo que me habré quedado con esto ahora. ¿Puede señalarme algún enlace que discuta qué métodos alternativos están disponibles para observar los cambios de estado de obj sin usar el reloj como preparación para AngularJs2?
Ka Tech
1
@KaTech ahora solo puedo decir el uso observablede RxJS, eso sería bueno compatible conAngular2
Pankaj Parkar
4

Para cualquiera que esté interesado en mi solución, termino recurriendo a RXJS Observables, que lo tendrás que usar cuando llegues a Angular 2. Aquí hay un violín que funciona para las comunicaciones entre componentes, me da más control sobre qué mirar.

JS FIDDLE RXJS Observables

class BoxCtrl {
    constructor(msgService) {
    this.msgService = msgService
    this.msg = ''

    this.subscription = msgService.subscribe((obj) => {
      console.log('Subscribed')
      this.msg = obj
    })
    }

  unsubscribe() {
    console.log('Unsubscribed')
    msgService.usubscribe(this.subscription)
  }
}

var app = angular
  .module('app', ['ngMaterial'])
  .controller('MainCtrl', ($scope, msgService) => {
    $scope.name = "Observer App Example";
    $scope.msg = 'Message';
    $scope.broadcast = function() {
      msgService.broadcast($scope.msg);
    }
  })
  .component("box", {
    bindings: {},
    controller: 'BoxCtrl',
    template: `Listener: </br>
    <strong>{{$ctrl.msg}}</strong></br>
    <md-button ng-click='$ctrl.unsubscribe()' class='md-warn'>Unsubscribe A</md-button>`
  })
  .factory('msgService', ['$http', function($http) {
    var subject$ = new Rx.ReplaySubject();
    return {
      subscribe: function(subscription) {
        return subject$.subscribe(subscription);
      },
      usubscribe: function(subscription) {
        subscription.dispose();
      },
      broadcast: function(msg) {
        console.log('success');
        subject$.onNext(msg);
      }
    }
  }])
Ka Tech
fuente
2

Un pequeño aviso sobre el uso de ng-change , como se recomienda con la respuesta aceptada, junto con un componente angular de 1,5.

En caso de que necesite vigilar un componente que ng-modely ng-changeno funcione, puede pasar los parámetros como:

Marcado en el que se utiliza el componente:

<my-component on-change="$ctrl.doSth()"
              field-value="$ctrl.valueToWatch">
</my-component>

Componente js:

angular
  .module('myComponent')
  .component('myComponent', {
    bindings: {
      onChange: '&',
      fieldValue: '='
    }
  });

Marcado de componentes:

<select ng-model="$ctrl.fieldValue"
        ng-change="$ctrl.onChange()">
</select>
Wtower
fuente
0

Disponible en IE11, MutationObserver https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver . Debe inyectar el servicio $ element en el controlador que semi-rompe la separación DOM / controlador, pero creo que esta es una excepción fundamental (es decir, falla) en angularjs. Dado que hide / show es asincrónico, necesitamos una devolución de llamada en el show, que angularjs y angular-bootstrap-tab no proporcionan. También requiere que sepa qué elemento DOM específico desea observar. Utilicé el siguiente código para el controlador angularjs para activar el reflujo del gráfico Highcharts en el programa.

const myObserver = new MutationObserver(function (mutations) {
    const isVisible = $element.is(':visible') // Requires jquery
    if (!_.isEqual(isVisible, $element._prevIsVisible)) { // Lodash
        if (isVisible) {
            $scope.$broadcast('onReflowChart')
        }
        $element._prevIsVisible = isVisible
    }
})
myObserver.observe($element[0], {
    attributes: true,
    attributeFilter: ['class']
})
user982671
fuente
Para facilitar la migración a Angular2 +, evite usar $scopey $rootScope. Considere usar RxJS en su lugar para emisores y suscriptores de eventos.
georgeawg
0

Realmente buena respuesta aceptada, pero debo agregar que también puede usar el poder de los eventos (un poco como en la señal / ranuras Qt si lo desea).

Un evento es transmitido: $rootScope.$broadcast("clickRow", rowId) por cualquier padre (o incluso controlador secundario). Luego, en su controlador puede manejar el evento de esta manera:

$scope.$on("clickRow", function(event, data){
    // do a refresh of the view with data == rowId
});

También puede agregar algunos inicios de sesión como este (tomado de aquí: https://stackoverflow.com/a/34903433/3147071 )

var withLogEvent = true; // set to false to avoid events logs
app.config(function($provide) {
    if (withLogEvent)
    {
      $provide.decorator("$rootScope", function($delegate) {
        var Scope = $delegate.constructor;
        var origBroadcast = Scope.prototype.$broadcast;
        var origEmit = Scope.prototype.$emit;

        Scope.prototype.$broadcast = function() {
          console.log("$broadcast was called on $scope " + this.$id + " with arguments:",
                     arguments);
          return origBroadcast.apply(this, arguments);
        };
        Scope.prototype.$emit = function() {
          console.log("$emit was called on $scope " + this.$id + " with arguments:",
                     arguments);
          return origEmit.apply(this, arguments);
        };
        return $delegate;
      });
    }
});
sebio
fuente
2
Con Angular 2+, el bus de eventos desaparece (los buses de eventos tienen problemas de rendimiento). Para facilitar la migración a Angular2 +, evite usar $scopey $rootScope. Considere usar RxJS en su lugar para emisores de eventos y suscriptores.
georgeawg
0

Voy tarde. Pero puede ayudar a otras personas.

app.component("headerComponent", {
    templateUrl: "templates/header/view.html",
    controller: ["$rootScope", function ($rootScope) {
        let $ctrl = this;
        $rootScope.$watch(() => {
            return $ctrl.val;
        }, function (newVal, oldVal) {
            // do something
        });
    }]
});
tetra maestro
fuente
¿Puedes explicar por qué funciona esto? ¿No es una mala idea abusar del raicescopio?
mix3d