187

I have a web page that serves as the editor for a single entity, which sits as a deep graph in the $scope.fieldcontainer property. After I get a response from my REST API (via $resource), I add a watch to 'fieldcontainer'. I am using this watch to detect if the page/entity is "dirty". Right now I'm making the save button bounce but really I want to make the save button invisible until the user dirties the model.

What I am getting is a single trigger of the watch, which I think is happening because the .fieldcontainer = ... assignment takes place immediately after I create my watch. I was thinking of just using a "dirtyCount" property to absorb the initial false alarm but that feels very hacky ... and I figured there has to be an "Angular idiomatic" way to deal with this - I'm not the only one using a watch to detect a dirty model.

Here's the code where I set my watch:

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

I just keep thinking there's got to be a cleaner way to do this than guarding my "UI dirtying" code with an "if (dirtyCount >0)"...

Kevin Hoffman
  • 5,154
  • 4
  • 31
  • 33
  • I also need to be able to reload $scope.fieldcontainer based on certain buttons (e.g. loading a previous version of the entity). For this, I would need to suspend the watch, reload, then resume the watch. – Kevin Hoffman Jun 05 '13 at 21:14
  • Would you consider changing my answer to be the accepted answer? Those that take the time to read all the way down to it tend to agree that it is the correct solution. – trixtur Jun 02 '15 at 19:51
  • @trixtur I have the same OP problem, but in my case your answer doesn't work, because my old value isn't `undefined`. It has a default value which is necessary in the case of my model update do not came up with all information. So some values dosn't change but have to trigger. – oliholz Jun 04 '15 at 13:41
  • @oliholz So instead of checking against undefined, take out the "typeof" and check the old_fieldcontainer against your default value. – trixtur Jun 04 '15 at 18:33
  • @KevinHoffman you should mark MW's answer at the correct answer for other people finding this question. It's a much better solution. Thanks! – Dev01 Jun 16 '17 at 15:20

5 Answers5

490

The first time the listener is called, the old value and the new value will be identical. So just do this:

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

This is actually the way the Angular docs recommend handling it:

After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization

Ringil
  • 6,277
  • 2
  • 23
  • 37
MW.
  • 12,550
  • 9
  • 36
  • 65
  • 3
    this way is more idiomatic than @rewritten's answer – Jiří Vypědřík Oct 01 '13 at 14:54
  • 26
    This is not correct. The point is ignore the change from `null` to the initial loaded value, not to ignore the change when there is no change. – rewritten Jan 15 '14 at 16:54
  • 1
    Well, the listener function will always trigger when the watch is created. This ignores that first call, which is what I believe the asker wanted. If he specifically wants to ignore the change when the old value was null, he can of course compare oldValue to null... but I don't think that's what he wanted to do. – MW. Jan 16 '14 at 11:41
  • The OP is specifically asking about watchers on resources (from REST services). Resources are first created, then the watchers applied, and then the attributes from the response are written, and only then Angular makes a cycle. Skipping the first cycle with a "initializing" flag provides a way to apply a watcher only on initialized resources, but still watching for changes from/to `null`. – rewritten Feb 02 '14 at 15:14
  • @rewritten: I would advice you to try my suggestion. It will do the exact same thing as the OP's "if (dirtyCount > 0)"-solution. – MW. Feb 03 '14 at 06:30
  • 7
    This is wrong, `oldValue` will be null on the first go-around. – Kevin C. Jun 09 '14 at 19:51
  • I tried this approach, but when I setup more than 1 watch on the same fieldcontainer value only the first one executed had a newValue = (some value), oldValue = undefined. The subsequent calls had newValue = (some value), oldValue = (same value) – Sean Glover Jul 15 '14 at 12:54
  • @SeanGlover: Are you refering to trixtur's answer? – MW. Jul 21 '14 at 06:38
  • @MW I was referring to my own experience with it. – Sean Glover Jul 27 '14 at 13:13
  • I like to do the same. – Sai Dubbaka May 01 '15 at 14:07
  • Kevin C is correct, this doesn't work because `oldValue` is null on load. Trixture's answer works. – J-bob May 26 '15 at 17:57
  • I have tried your suggestion (see http://plnkr.co/edit/VrWrHHWwukqnxaEM3J5i). I'm positive it works but only when added on the .get() callback (which is not always possible). – rewritten Feb 05 '16 at 15:59
119

set a flag just before the initial load,

var initializing = true

and then when the first $watch fires, do

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

The flag will be tear down just at the end of the current digest cycle, so next change won't be blocked.

rewritten
  • 16,280
  • 2
  • 47
  • 50
  • 17
    Yes, that will work but the comparing the old and new value approach suggested by @MW. is both simpler and more Angular idiomatic. Can you update the accepted answer? – gerryster Nov 18 '14 at 19:56
  • 2
    Always setting the oldValue to equal the newValue on the first fire was added specifically to avoid having to do this flag hack – Charlie Martin Feb 24 '15 at 19:27
  • in my case this flag hack was really nice, because there's a logic where everytime when the main paramaters object is changed a call to DB happens. Now I can disable the watch when I want. – Thiago C. S Ventura May 27 '15 at 12:03
  • 2
    MW's answer is actually incorrect as the new value to the old value won't handle the case where the old value is undefined. – trixtur Jun 03 '15 at 22:29
  • This is working for me on my local machine, but then not working on production where load times are longer. Not really sure why. – jordancooperman Jan 05 '16 at 18:55
  • 1
    For commenters: this is not a god model, nothing prevents you to put the 'initialized' variable inside the scope of a decorator, and then decorate your "normal" watch with it. In any case that's completely outside of the scope of this question. – rewritten Feb 05 '16 at 13:36
  • 1
    See http://plnkr.co/edit/VrWrHHWwukqnxaEM3J5i for comparison of the propoosed methods, both setting up watchers in the .get() callback, and on scope initialization. – rewritten Feb 05 '16 at 15:34
  • This is too clumsy... the answer below is much better. – luiscvalmeida Dec 06 '16 at 15:40
43

I realize this question has been answered, however I have a suggestion:

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

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

Using flags works but has a bit of a code smell to it don't you think?

trixtur
  • 708
  • 6
  • 14
  • 1
    This is the way to go. In Coffeescript, it's as simple as `return unless oldValue?` (? is the existential operator in CS). – Kevin C. Jun 09 '14 at 19:52
  • This is a better solution. The function will not execute until the scope attribute is instantiated, so if you are pulling it from an API (as you probably would like to) it will not execute when the value is first populated. I used this for a geocoder. – superluminary Sep 30 '14 at 20:44
  • 1
    Props on the word code smell ! also check out god object lol. too good. – Armeen Moon Jan 13 '15 at 21:37
  • very nice! The answer by MW doesn't work because the value is initially null. Thanks for sharing – J-bob May 26 '15 at 17:55
  • 1
    Although this isn't the accepted answer, this made my code work when populating an object from an API and I want to watch different save states. Very nice. – Lucas Reppe Welander Jun 13 '16 at 12:59
  • 1
    Thanks - this helped for me – Wand Maker Aug 11 '16 at 09:37
  • 1
    This is great, however in my case I had instantiated the data as an empty array before doing the AJAX call to my API, so check the length of that instead of type. But this answer definitely shows the most effective method at least. – Saborknight Mar 31 '17 at 10:14
4

During initial loading of current values old value field is undefined. So the example below helps you for excluding initial loadings.

$scope.$watch('fieldcontainer', 
  function(newValue, oldValue) {
    if (newValue && oldValue && newValue != oldValue) {
      // here what to do
    }
  }), true;
Tahsin Turkoz
  • 4,356
  • 1
  • 27
  • 18
  • You probably want to use !== since `null` and `undefined` will match in this situation, or (`'1' == 1` etc) – Jamie Pate Feb 18 '17 at 00:42
  • in general you are right. But in that case newValue and oldValue can not be such corner values because of the previous checks. Both value has the same type also because belongs to the same field. Thank you. – Tahsin Turkoz Feb 23 '17 at 23:08
3

Just valid the state of the new val:

$scope.$watch('fieldcontainer',function(newVal) {
      if(angular.isDefined(newVal)){
          //Do something
      }
});
David K
  • 3,147
  • 2
  • 13
  • 19
Luis Saraza
  • 346
  • 3
  • 12