Comment ignorer la charge initiale lorsque je regarde les changements de modèle dans AngularJS?

184

J'ai une page Web qui sert d'éditeur pour une seule entité, qui se présente comme un graphe profond dans la propriété $ scope.fieldcontainer. Après avoir reçu une réponse de mon API REST (via $ resource), j'ajoute une montre à 'fieldcontainer'. J'utilise cette montre pour détecter si la page / entité est "sale". En ce moment, je fais rebondir le bouton de sauvegarde, mais je veux vraiment rendre le bouton de sauvegarde invisible jusqu'à ce que l'utilisateur salisse le modèle.

Ce que je reçois, c'est un seul déclencheur de la montre, ce qui, je pense, se produit parce que l'affectation .fieldcontainer = ... a lieu immédiatement après la création de ma montre. Je pensais simplement utiliser une propriété "dirtyCount" pour absorber la fausse alarme initiale, mais cela semble très piraté ... et j'ai pensé qu'il devait y avoir un moyen "angular idiomatic" de gérer cela - je ne suis pas le seul à l'aide d'une montre pour détecter un modèle sale.

Voici le code où je règle ma montre:

 $scope.fieldcontainer = Message.get({id: $scope.entityId },
            function(message,headers) {
                $scope.$watch('fieldcontainer',
                    function() {
                        console.log("model is dirty.");
                        if ($scope.visibility.saveButton) {
                            $('#saveMessageButtonRow').effect("bounce", { times:5, direction: 'right' }, 300);
                        }
                    }, true);
            });

Je n'arrête pas de penser qu'il doit y avoir un moyen plus propre de le faire que de protéger mon code "UI salissant" avec un "if (dirtyCount> 0)" ...

Kevin Hoffman
la source
J'ai également besoin de pouvoir recharger $ scope.fieldcontainer en fonction de certains boutons (par exemple, charger une version précédente de l'entité). Pour cela, il me faudrait suspendre la montre, recharger, puis reprendre la montre.
Kevin Hoffman
Envisageriez-vous de changer ma réponse pour être la réponse acceptée? Ceux qui prennent le temps de le lire jusqu'au bout ont tendance à convenir que c'est la bonne solution.
trixtur
@trixtur J'ai le même problème OP, mais dans mon cas, votre réponse ne fonctionne pas, car mon ancienne valeur ne l'est pas undefined. Il a une valeur par défaut qui est nécessaire dans le cas où la mise à jour de mon modèle ne contient pas toutes les informations. Ainsi, certaines valeurs ne changent pas mais doivent se déclencher.
oliholz
@oliholz ​​Donc, au lieu de vérifier par rapport à undefined, retirez le "typeof" et comparez le old_fieldcontainer à votre valeur par défaut.
trixtur le
@KevinHoffman, vous devez marquer la réponse de MW comme la bonne réponse pour les autres personnes qui trouvent cette question. C'est une bien meilleure solution. Merci!
Dev01

Réponses:

118

définir un drapeau juste avant le chargement initial,

var initializing = true

puis quand la première $ watch se déclenche, faites

$scope.$watch('fieldcontainer', function() {
  if (initializing) {
    $timeout(function() { initializing = false; });
  } else {
    // do whatever you were going to do
  }
});

L'indicateur sera supprimé juste à la fin du cycle de résumé en cours, donc le prochain changement ne sera pas bloqué.

réécrit
la source
17
Oui, cela fonctionnera, mais la comparaison de l'ancienne et de la nouvelle approche de valeur suggérée par @MW. est à la fois plus simple et plus idiomatique angulaire. Pouvez-vous mettre à jour la réponse acceptée?
gerryster
2
Toujours définir oldValue pour égaler la newValue sur le premier feu a été ajouté spécifiquement pour éviter d'avoir à faire ce hack de drapeau
Charlie Martin
2
La réponse de MW est en fait incorrecte car la nouvelle valeur de l'ancienne valeur ne gérera pas le cas où l'ancienne valeur n'est pas définie.
trixtur
1
Pour les commentateurs: ce n'est pas un modèle divin, rien ne vous empêche de mettre la variable 'initialisée' dans le cadre d'un décorateur, puis d'en décorer votre montre "normale". En tout cas, cela sort complètement du cadre de cette question.
réécrit le
1
Voir plnkr.co/edit/VrWrHHWwukqnxaEM3J5i pour une comparaison des méthodes proposées, tant pour la configuration des observateurs dans le rappel .get () que pour l'initialisation de la portée.
réécrit le
483

La première fois que l'écouteur est appelé, l'ancienne valeur et la nouvelle valeur seront identiques. Alors fais juste ceci:

$scope.$watch('fieldcontainer', function(newValue, oldValue) {
  if (newValue !== oldValue) {
    // do whatever you were going to do
  }
});

C'est en fait la façon dont les documents Angular recommandent de le gérer :

Une fois qu'un observateur est enregistré avec la portée, l'écouteur fn est appelé de manière asynchrone (via $ evalAsync) pour initialiser l'observateur. Dans de rares cas, cela n'est pas souhaitable car l'écouteur est appelé lorsque le résultat de watchExpression n'a pas changé. Pour détecter ce scénario dans l'écouteur fn, vous pouvez comparer newVal et oldVal. Si ces deux valeurs sont identiques (===) alors l'écouteur a été appelé en raison de l'initialisation

MW.
la source
3
cette façon est plus idiomatique que la réponse de @ réécrit
Jiří Vypědřík
25
Ce n'est pas correct. Le point est d'ignorer le changement de nullà la valeur initiale chargée, de ne pas ignorer le changement lorsqu'il n'y a pas de changement.
réécrit le
1
Eh bien, la fonction d'écoute se déclenchera toujours lorsque la montre est créée. Cela ignore ce premier appel, ce que je pense que le demandeur voulait. S'il veut spécifiquement ignorer le changement lorsque l'ancienne valeur était nulle, il peut bien sûr comparer oldValue à null ... mais je ne pense pas que ce soit ce qu'il voulait faire.
MW.
L'OP pose spécifiquement des questions sur les observateurs sur les ressources (des services REST). Les ressources sont d'abord créées, puis les observateurs sont appliqués, puis les attributs de la réponse sont écrits, et ce n'est qu'alors qu'Angular effectue un cycle. Sauter le premier cycle avec un drapeau «initialisation» fournit un moyen d'appliquer un observateur uniquement sur les ressources initialisées, tout en surveillant les changements de / vers null.
réécrit le
7
C'est faux, oldValuesera nul lors de la première remise des gaz.
Kevin C.
42

Je me rends compte que cette question a été répondue, mais j'ai une suggestion:

$scope.$watch('fieldcontainer', function (new_fieldcontainer, old_fieldcontainer) {
    if (typeof old_fieldcontainer === 'undefined') return;

    // Other code for handling changed object here.
});

L'utilisation de drapeaux fonctionne mais a une petite odeur de code, vous ne pensez pas?

trixtur
la source
1
C'est la voie à suivre. Dans Coffeescript, c'est aussi simple que return unless oldValue?(? Est l'opérateur existentiel dans CS).
Kevin C.
1
Les accessoires sur l'odeur de code de mot! consultez également l'objet de Dieu lol. trop bon.
Matthew Harwood
1
Bien que ce ne soit pas la réponse acceptée, cela a fait fonctionner mon code lors du remplissage d'un objet à partir d'une API et je souhaite surveiller différents états de sauvegarde. Très agréable.
Lucas Reppe Welander
1
Merci - cela m'a aidé
Wand Maker
1
C'est génial, mais dans mon cas, j'avais instancié les données sous forme de tableau vide avant de faire l'appel AJAX à mon API, alors vérifiez la longueur de celui-ci au lieu du type. Mais cette réponse montre au moins la méthode la plus efficace.
Saborknight
4

Lors du chargement initial des valeurs actuelles, l'ancien champ de valeur n'est pas défini. L'exemple ci-dessous vous aide donc à exclure les chargements initiaux.

$scope.$watch('fieldcontainer', 
  function(newValue, oldValue) {
    if (newValue && oldValue && newValue != oldValue) {
      // here what to do
    }
  }), true;
Tahsin Turkoz
la source
Vous voudrez probablement utiliser! == depuis nullet undefinedcorrespondra dans cette situation, ou ( '1' == 1etc)
Jamie Pate
en général, vous avez raison. Mais dans ce cas, newValue et oldValue ne peuvent pas être de telles valeurs de coin en raison des vérifications précédentes. Les deux valeurs ont le même type également parce qu'elles appartiennent au même champ. Je vous remercie.
Tahsin Turkoz
3

Validez simplement l'état du nouveau val:

$scope.$watch('fieldcontainer',function(newVal) {
      if(angular.isDefined(newVal)){
          //Do something
      }
});
Luis Saraza
la source