20

I'm trying to figure out how you handle binding properly when my data is stored in a service.

I can get things working if it put the service into the $scope and then get the templates to bind directly into it but that seems like a really bad idea.

I'd basically like to have it so that my views / controllers are able to easily change the state down in a service and have that reflected everywhere.

It feels like I should be able to do something like the following, but it doesn't work (http://jsfiddle.net/aidankane/AtRVD/1/).

HTML

<div ng-controller="MyCtl">
    <select ng-model="drawing" ng-options="d.file for d in drawings"></select>
</div>
<div ng-controller="MyOtherCtl">
    {{ drawing }}
</div>

JS

var myApp = angular.module('myApp', []);

myApp.factory('myService', function(){
    var me = {
        drawings: [{'file':'a'}, {'file':'b'}]
    };
    // selected drawing
    me.drawing = me.drawings[0];
    return me;
});

function MyCtl($scope, myService){
    // can do:
    // $scope.mys = myService;
    // and then in html ng-model="mys.drawing"
    // but that seems wrong

    $scope.drawings = myService.drawings;
    $scope.drawing = myService.drawing;

    // can I not do this? it doesn't seem to work anyway...
    $scope.$watch('drawing', function(drawing){
        myService.drawing = drawing;
    });
}

function MyOtherCtl($scope, myService){
    $scope.drawing = myService.drawing;
}

MyCtl.$inject = ['$scope', 'myService'];
MyOtherCtl.$inject = ['$scope', 'myService'];
Aidan Kane
  • 3,856
  • 2
  • 25
  • 28
  • I see where you are watching $scope.drawing for change, but your not changing the model, your changing the selected item. Shouldn't you put in a handler for the selected item change, possibly in a directive? – Ben Felda Jan 25 '13 at 18:04
  • How would I do this using directives? I figured the ngSelect directive basically gives me the behaviour that I need. My understanding of the issue is that it's the movement between the controller and the service that's the issue - then again I'm a little confused right now :) – Aidan Kane Jan 25 '13 at 18:08
  • You are right, now that I see your updated fiddle, I have a better idea of what you are trying. You are not confused:) – Ben Felda Jan 25 '13 at 18:16

3 Answers3

41

You can bind to services using $watch and passing a function:

$scope.$watch( function () { return myService.drawing; }, function ( drawing ) {
  // handle it here. e.g.:
  $scope.drawing = drawing;
});

And then use $scope.drawing in your templates and they will automatically update:

<div ng-controller="MyOtherCtl">
  {{ drawing }}
</div>
Josh David Miller
  • 120,525
  • 16
  • 127
  • 95
  • 1
    Ok. That's set me on the right path, I think. So, I've created a new version - http://jsfiddle.net/aidankane/EBr53/ - and I've created watchers in both directions to push/pull between the service. Is that the correct way of doing it? – Aidan Kane Jan 25 '13 at 18:05
  • @AidanKane Yep! Inter-component communication is a common question here and on the mailing list. Services are by far the most common (and usually the more "correct") solution. – Josh David Miller Jan 25 '13 at 18:13
  • I saw pretty early on that they're the right way of holding the data. Just been struggling a little with the co-ordination between service / controller / view. Seems like by using the service I have to put a bit of boilerplate in the controllers to sync things (there's so little in the controllers that I don't mind). Thanks for the prompt responses, appreciated. – Aidan Kane Jan 25 '13 at 18:21
  • Looking at the fiddle by @AidanKane it seems we have to set-up these bindings for each service that has a model that we want two-way binding on. Seems verbose and awkward - there's no more "native" way to do this? – mariachimike Aug 21 '13 at 16:07
  • 1
    @mariachimike There usually is, but it depends a lot on context. For example, you could just do `$scope.drawing = myService.drawing` and avoid the watch entirely, but this is usually frowned upon from a separation of concerns perspective. If you have an example, I'm sure we can find something less generic and less verbose. – Josh David Miller Aug 21 '13 at 16:48
  • @JoshDavidMiller actually my example is pretty generic - just trying to do a two-way binding with a model that is in a service. If I read your answer correctly, a direct binding like that in your last answer is frowned upon because it would tightly couple these two (ideally) independent modules, right? – mariachimike Aug 22 '13 at 16:13
  • 1
    @mariachimike Exactly, but there are always exceptions. – Josh David Miller Aug 22 '13 at 17:15
  • I've created another js fiddle that puts the service in the controller directly to make it easier for people to follow the discussion. I've seen quite a few people doing this in their code and have done it myself sometimes too. As you say, you trade verbosity for tighter coupling http://jsfiddle.net/aidankane/EBr53/ – Aidan Kane Aug 23 '13 at 10:31
2

I think, even more elegant is to work with promises (see $q.deferred()) and to resolve them asynchronously. In the promise function you can then assign the data to $scope's members.

Liglo App
  • 3,719
  • 4
  • 30
  • 54
  • That's fine if you only load your data once, but I don't think data that is changing will bind automatically in the way the OP is looking for. Could be wrong. – John Rix Jul 04 '16 at 00:09
2

There are two ways to do bind the data from a service: 1) By value (will require a watcher as done above to check for variable changes to the service primitive value) 2) By reference (values are directly linked) which is my preferred method of data binding.

I will only explain the second possibility since the accepted answer already shows how a watcher can be implemented. This blog describes what I am going to explain very well.

I created this plunk to illustrate data binding by reference.

Here is the code from the plunk:

HTML

<!DOCTYPE html>
<html ng-app="myApp">

  <head>
    <script data-require="angularjs@1.5.0" data-semver="1.5.0" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.0/angular.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js"></script>
  </head>

  <body ng-controller="myAppCntrl">
    <h1>Hello Plunker!</h1>
    <h3>By value</h3>
    <p>{{byValue}}</p>
    <p>{{objByValue}}</p>
    <h3>By object in service reference</h3>
    <p>{{byRefence.stringPrimitive}}</p>
    <h3>By reference to service singleton</h3>
    <p>{{myservice.stringPrimitive}}</p>
    <p style="color: green">of course, you can reference an object through the service as well</p>
    <p>{{myservice.objectWithPrimitive.stringPrimitive}}</p>

    <button ng-click=update()>Update strings on service</button>
    <br />
    <button ng-click=setDefaults()>Restore Defaults</button>
  </body>

</html>

JAVASCRIPT

var myApp = angular.module("myApp", []);

myApp.controller('myAppCntrl', function($scope, myAppService){
  $scope.myservice = myAppService;
  $scope.byValue = myAppService.stringPrimitive;
  $scope.objByValue = myAppService.objectWithPrimitive.stringPrimitive;
  $scope.byRefence = myAppService.objectWithPrimitive;

  $scope.update = function () {
    myAppService.stringPrimitive = "updated string";
    myAppService.objectWithPrimitive.stringPrimitive = "updated string here too";
  };
  $scope.setDefaults = function () {
    myAppService.stringPrimitive = 'string primitive';
    myAppService.objectWithPrimitive.stringPrimitive = 'string primitive';
  };
});

myApp.service('myAppService', function(){
  this.stringPrimitive = 'string primitive';
  this.objectWithPrimitive = {
    stringPrimitive: 'string primitive'
  };
});

So how does this work?

It is important to understand that this has little to do with how angular works, and a lot to do with how Javascript works. When a variable is set equal to a primitive value (or passed to a function) in javascript (integer, string, etc.) the var is set by value. This means that the the new variable is a copy of the variable you are setting it equal to with a new location in memory. When a variable is set equal to an object (or passed to a function) in javascript the var is set by reference.

What does this mean?

When a $scope var is set by value, and the service variable changes, since the $scope variable is only a copy of the service variable, the service variable no longer has anything to do with the service variable and will not change when the service var does.

When the $scope var is set equal to and object, it is assigned by Reference. This means that when the service object (note that the service is an object since it is instantiated with the new keyword and you can refer to that object with "this" inside the service) or any objects on the service that are referenced are changed, any $scope variable that reference those objects will update as well.

Nick Brady
  • 6,084
  • 1
  • 46
  • 71