Angular Resource - how to check if a resource instance has any unsaved changes?
Asked Answered
M

3

12

I want to be able to tell whether an $resource instance has been modified by the user - that is, whether its current state is different than what has been initially loaded from the server && has not yet been $saved. How can I achieve that?

Martyry answered 27/10, 2013 at 6:36 Comment(0)
M
14

Assuming you get a resource, and then put it on the current $scope so that it can be edited by a user:

$scope.question = Questions.get({id:"19615328"});

You can then watch it for changes like this:

// some flag, name it anything
$scope.userChange = false;
$scope.$watch('question', function(newValue, oldValue) {
    if(newValue && newValue != oldValue){
        $scope.userChange = true;
        // if you want to you can even do this, this will trigger on every change though
        $scope.question.$save();
    }
}, true);

( Pretty much everything from down here is the result of the extra questions from the chat below )

Then whenever you want to check if it has been changed $scope.userChange can tell you if a change occurred. And when you save the object, reset the $scope.userChange.

You can even do this

$scope.$watch('question', function() {
    $scope.question.$save();
}, true);

Obviously you'd want to add some sort of throttle or "debounce" system so it waits a second or so, once you have this in place any change to the object will cause a save via $scope.$watch.

And in case you want to check for null, for when you have not yet received the actual object.

$scope.$watch('question', function(newValue, oldValue) {
    // dont save if question was removed, or just loaded
    if(newValue != null && oldValue != null){
        $scope.question.$save();
    }
}, true);

You could even wrap the Questions.get call, see this questions for answers on how you can do this on the service & factory level, to do something like this.

Questions.getAndAutosave = function(options){
    var instance = Questions.get(options);
    $scope.$watch(function(){
            return instance;
        },
        function(newValue, oldValue){
            if (newValue === oldValue) return;
            if(newValue != null && oldValue != null){
                instance.$save();
            }
        }, true);
    return instance;
};

Then whenever you call Questions.getAndAutosave, whatever it returns is already being watched, and will be auto-$save'd. The reason we do if (newValue === oldValue) return; is because $watch fires as soon as you call it, and then watches for changes. We don't need to save on the first call.

Mandarin answered 31/10, 2013 at 15:17 Comment(11)
Unfortunately the $watch will also be triggered when $get is executed, because $scope.question changes from null to object... I want something that would only report unsaved changes when user makes the changes, not when the data is being pulled from the server.Martyry
The idea is you watch it after you receive the response. Or; give $watch a second callback, ( this receives the old and new value ), so you can check if the old value was null. ( Or if the new value is null ? :-) )Mandarin
Also, do you think that such functionality could be implemented directly in the service? I know I can augment a $resource with additional functions. Implementing such features in every controller for every resource might be tedious and far from elegantMartyry
I've added a short sample on what you could do, there's another question with many options on how to configure it at the service / factory level.Mandarin
For some reason the if(newValue != null && oldValue != null){ condition in $watch is not sufficient. I have implemented it and right off the bat after the resource is loaded the $scope.userChange flag is set to true, even though the user has not yet made any changes to the object. Any ideas on how to correct that?Martyry
Add some console.log-ging whats the actual value of newValue and oldValue?Mandarin
Here you can see the logged values in the js console: jsfiddle.net/Tt4b5/2Martyry
You seem to be right, I read up on the documentation and $watch always fires the instant you call it, this is apparently the expected behaviour, so you'll have to add a check to see if the values are not the same ( As we don't want to get hit by the first call ) Updated questionMandarin
let us continue this discussion in chatMandarin
I think a combination of object $watch (true as third argument), properly checking newVal and oldVal and setting something like a $changed on the question instance (restrict it there, don't polute $scope) may do the job.Jempty
I found this some months later, got stuck on the $scope.$watch('question'... expression in the answer. It won't work unless third argument is true as mentioned in previous comment (if not, it'll only watch for the reference to the resource to change, not the resource data, and the watch expression will never fire after initial load). See this question: #11136364Buerger
M
1

I've found a solution that both does not treat downloading data from server as user change and is implemented directly in the service itself. It might not be the most efficient solution possible, but provides exactly the functionality I want,

app.factory('testService', ['$resource', '$rootScope', function($resource, $rootScope){
var test = $resource('/api/words/:id', {id: '@id'});

test.orig_get = test.get;

test.get = function(options){
    var instance = test.orig_get(options, function(){
        instance.unsaved = false;
        $rootScope.$watch(function(){
            return instance;
        }, function(newValue, oldValue) {
           if(angular.equals(newValue, oldValue)){
                return;
           }
           var changed_indexes = [];
           for(var i in newValue){
                if(!angular.equals(newValue[i], oldValue[i])){
                    changed_indexes.push(i);
                }
           }
           if(newValue != null && oldValue != null && !(changed_indexes.length==1 && changed_indexes[0]=='unsaved')){
                console.log('detected change. setting unsaved to true');
                instance.unsaved = true;
            }

        }, true);
    });
    return instance;
}

test.prototype.orig_save = test.prototype.$save;

test.prototype.$save = function(options){
    return this.orig_save(options, function(){
        this.unsaved = false;           
    })
}

return test;
}]);
Martyry answered 2/11, 2013 at 10:3 Comment(0)
A
0

You can clone the initial object, then compare when you need to check.

master = null
resource = Resource.get({id:1}, function() {
  master = angular.copy(resource)
})
function isModified() {
  return !angular.equals(resource, master)
}
Agar answered 25/6, 2015 at 13:27 Comment(1)
Although this solves the problem, it is very expensive to an object comparison. If you use a function like this in the view, this expensive comparison algorithm will run on every digest cycle, making the app slow.Bone

© 2022 - 2024 — McMap. All rights reserved.