3

I ran into a problem (which I think I understand), but I'm not clear about what the solution could be.

In a nutshell, I have a BackendService that wraps some non-Angular object model (in my case, SharePoint, but that is beside the point). I created this BackendService so that it could return Angular-compatible entities (items), so that I could do something like the following:

angular.module("app", [])
.factory("BackendService", function(){
  return new BackendService();
})
.controller("MainCtrl", function($scope, BackendService){
  BackendService.GetItems()
    .then(function(items){
      $scope.Items = items;
      $scope.$apply();
    });

});

So far so good.

Except, I wanted each item to be a self-sufficient and ViewModel-y such that it could be used directly in the View. In other words, I wanted to do the following (notice the button's ng-show and ng-click):

<div ng-controller="MainCtrl">
  <div ng-repeat="item in Items">
    <input ng-model="item.fieldA" type="text"/>
    <input ng-model="item.fieldB" type="text"/>
    <button ng-show="item.IsDirty()" ng-click="item.Save()">Save</button>
  </div>
</div>

The button shows immediately when there is any change (which sets the dirty flag), but when item.Save() - an async function - is called where the dirty flag is unset, the change is not visible in the DOM.

So the problem is: the button doesn't hide when item.IsDirty() === false.

My understanding is that item.Save() being an async function that uses the object model which uses Ajax under the covers (which doesn't use $http since it's not aware of Angular) thus bypasses the digest loop. Because of this, the change in item.IsDirty() is never reflected in the DOM.

Questions:

  1. Is my understanding of the issue correct?
  2. Have I offended an Angular best practice with this approach?
  3. Must I now do something like <button ng-click="SaveItem(item)"> and call $scope.$apply within it?

Edit (in response to an answer by Gordon):

  1. Is it a good idea to pass $scope or use $rootScope deep in the service, or should the service be Angular-agnostic? If not, what would be preferable?

Thanks!

New Dev
  • 48,427
  • 12
  • 87
  • 129

1 Answers1

1

The problem is your scope.items array isn't bound to any data from your service. I'd solve this problem by having an items property on ain object in your service that is data bound to your scope items property.

BackendService.loadData()
 .then(function() {
    $scope.data = BackendService.data;
 });
// data is an object with a property items

Then just as changes happen to the data in your service update the local items array and they will be tracked by the angular data binding.

Ah I finally get it. I was able to get it around it using an emit to trigger an apply: http://plnkr.co/edit/F8rLK2?p=preview

See this Q&A for the deets: angularjs ng-show with promise expression

Community
  • 1
  • 1
Gordon Bockus
  • 860
  • 6
  • 11
  • I don't think this is it. The `items` in the `.then(function(items))` is what is returned from the `BackendService`, and it binds well for all but async changes. But just to be on the safe side, I tried getting the items explicitly from the `BackendService.data()` as you suggested, but still doesn't work (or, rather, works exactly the same) – New Dev Sep 22 '14 at 21:10
  • Right so the deal is you not want to bind to a value that is returned from a function on BackendService, but a property on a model on BackendService. One of the tricky parts of angular is wrapping your head around what can be data binded and see the two way data binding. – Gordon Bockus Sep 22 '14 at 21:34
  • After rereading the question I realized there's more going on here and I misunderstood the problem. The isDirty and Save function on the items are where we are seeing the problem. As far as Save being an async function I'd solve the problem by having your button bind to a scope function that would call Save and then (assuming it returns a promise) have a .then handler the would update the scope items array. It's be something like ng-click="saveItem(index)" where index is the index from your ng-repeat. With that update I believe your isDirty function will start working as desired. – Gordon Bockus Sep 22 '14 at 21:46
  • Yes, that was one approach I reckoned I had to do in my question #3. I would like to avoid that, as it requires replicating all the functionality of the `item` (i.e. Save, Refresh, Validate) in the $scope. – New Dev Sep 22 '14 at 21:53
  • You'll either have to do that or drive your UI off a model attached to the service as mentioned in my answer. That may be the way to go. In your controller just scope.items to an object in the service and load and alter the data in your service only. – Gordon Bockus Sep 22 '14 at 21:57
  • Hmm... I don't know if you mean something different when you say "model attached to the service". If this is the `$scope.data = BackendService.data` approach, then I already tried that and it didn't work for me. – New Dev Sep 22 '14 at 22:11
  • Gordon, I truly appreciate your time trying to help. I think you misunderstand my problem. In your example it works because you don't have an async call in your `item.Save()` - it synchronously changes `this.changed`, which makes `item.isDirty()` change its value. `ng-click` starts the digest loop and, voila, things are working. If you want to simulate my case, try setting `this.changed = true` inside `window.setTimeout`. – New Dev Sep 23 '14 at 05:20
  • Sure. A fun problem. I finally get it. Check out my updated plunk and the linked Q&A. – Gordon Bockus Sep 23 '14 at 13:20
  • Thanks Gordon, I'm upvoting since your approach provides an option. I don't necessarily think that this the the best solution, since it now deeply links the service with an Angular concept of $rootScope. The question was more about best practice than a particular solution. – New Dev Sep 23 '14 at 16:30