3

I have 2 dates - start and end. I want to validate (and re-assure) start < end. I get notified they change using $watch, and then I go and change them to make sure the condition is met, but then it triggers the $watch again and then I get max iterations reached of $digest and things go south.

So my question is - how do I change variables that are watched inside the $watch without causing infinite cycle. (Aside from having more checks and changing them only if they actually needs to be changed)

Here is my code:

var TIME_DAY = 1000 * 60 * 60 * 24;

$scope.$watch('timeFilter.startTime', function () {
    validateStartEndTime();
},true);

$scope.$watch('timeFilter.endTime', function () {
    validateStartEndTime();
},true);

function validateStartEndTime() {
    if ($scope.timeFilter.startTime != null && $scope.timeFilter.endTime != null) {
        // Remove all seconds
        if ($scope.timeFilter.startTime.getSeconds() > 0)
            $scope.timeFilter.startTime.setSeconds(0);
        if ($scope.timeFilter.endTime.getSeconds() > 0)
            $scope.timeFilter.endTime.setSeconds(0);
        if ($scope.timeRange.minTime.getSeconds() > 0)
            $scope.timeRange.minTime.setSeconds(0);
        if ($scope.timeRange.maxTime.getSeconds() > 0)
            $scope.timeRange.maxTime.setSeconds(0);

        // When start time isn't before end time
        if ($scope.timeFilter.startTime >= $scope.timeFilter.endTime) {
            // We want to increase end time by 1 minute, if possible - do it. Else - reduce start time by 1 minute
            if ($scope.timeFilter.endTime < $scope.timeRange.maxTime) {
                $scope.timeFilter.endTime.setMinutes($scope.timeFilter.startTime.getMinutes() + 1);
            } else {
                $scope.timeFilter.startTime.setMinutes($scope.timeFilter.startTime.getMinutes() - 1);
            }
        }

        // Start time before min time
        if ($scope.timeFilter.startTime < $scope.timeRange.minTime) {
            $scope.timeFilter.startTime = $scope.timeRange.minTime;
            if ($scope.timeFilter.endTime <= $scope.timeFilter.startTime) {
                $scope.timeFilter.endTime = $scope.timeFilter.startTime;
                $scope.timeFilter.endTime.setMinutes($scope.timeFilter.endTime.getMinutes() + 1);
            }
        }

        // End time after max time
        if ($scope.timeFilter.endTime > $scope.timeRange.maxTime) {
            $scope.timeFilter.endTime = $scope.timeRange.maxTime;
            if ($scope.timeFilter.startTime >= $scope.timeFilter.endTime) {
                $scope.timeFilter.startTime = $scope.timeFilter.endTime;
                $scope.timeFilter.startTime.setMinutes($scope.timeFilter.startTime.getMinutes() - 1);
            }
        }
    }
}
AlexD
  • 4,062
  • 5
  • 38
  • 65
  • simply set a flag variable. set the flag once your condition is true and only allow any action if flag is not set yet. – oshell Sep 03 '15 at 07:41
  • I think you will be better served by using angular wrapped DOM events for your date validation, like `ng-blur`, `ng-change`... – tomaoq Sep 03 '15 at 07:43
  • I would go with a boolean variable, e.g. manualChange. This should be set to false in the beginning of validateStartEndTime. Then in each watch, if manualChange is true then call validateStartEndTime, otherwise set manualChange to true and return. – dchar Sep 03 '15 at 07:47
  • How would I go about making a flag? The context of the validation first finishes, then the digest is called, therefore changing to true on $watch and to false at the end of the validation function would prove useless. Am I missing something? – AlexD Sep 03 '15 at 09:05

2 Answers2

1

Honestly I would prefer to find a way to avoid to change a variable that is watched constantly. But, if you really want, you could deregister your $watch function when you have to change your variables being watched and then re-register again the $watch function.

To deregister a $watch function you just need to get the reference and invoke when you need.

var myWatch = $scope.$watch('timeFilter.startTime', function () {
   validateStartEndTime();
},true);
myWatch(); // It will deregister it.

Bear in mind that sometimes when you first watch a variable, it will trigger automatically the function. My suggestion is to always make the check if the old variable is really changed.

var myWatch = $scope.$watch('timeFilter.startTime', function (newValue, oldValue) {
   if(newValue === oldValue){
      return;
   }
   validateStartEndTime();
},true);

I hope it works for you. Unfortunately I cannot test this code right now.

Daniele
  • 161
  • 5
  • The check of oldvalue === newvalue is great. How would I go about deregistering? I call myWatch from inside the $watch? And then when do I re-register it? – AlexD Sep 03 '15 at 09:09
  • You re register after you change the variable that wasn't watched for a moment. – Daniele Sep 03 '15 at 13:12
0

I combined the suggestions and advice from everyone who replied to something like this:

$scope.$watch('timeFilter.startTime', function(newValue, oldValue) {
    if (newValue === oldValue) return;
    validateStartEndTime();
}, true);

$scope.$watch('timeFilter.endTime', function(newValue, oldValue) {
    if (newValue === oldValue) return;
    validateStartEndTime();
}, true);

function validateStartEndTime(startChanged) {
    if ($scope.timeFilter.startTime == null || $scope.timeFilter.endTime == null) return;
    var tempStart = new Date($scope.timeFilter.startTime), tempEnd = new Date($scope.timeFilter.endTime);
    if ($scope.timeRange.minTime && $scope.timeRange.maxTime) {
        // Logic start

        // Logic ends

        if ($scope.timeFilter.startTime !== tempStart)
            $scope.timeFilter.startTime = tempStart;
        if ($scope.timeFilter.endTime !== tempEnd)
            $scope.timeFilter.endTime = tempEnd;
    }
}

This way I make sure I do as little changes as possible. I'm open to suggestions if anyone has any

AlexD
  • 4,062
  • 5
  • 38
  • 65