0

The question: Is there a way to get the desired model change without injecting $scope into an Angular "controller as" method in the following set up?

The html:

<div data-ng-controller="Buildings as vm">
  <select data-ng-model="vm.selected.building"
          data-ng-options="building.name for building in vm.buildings"
          data-ng-change="vm.changeBuilding()">
  </select>
  <div data-ng-repeat="building in vm.buildings">
    <div data-ng-if="vm.selected.building.name === building.name">
      <select data-ng-model="vm.selected.room"
              data-ng-options="room.name for room in building.rooms"
              data-ng-change="vm.changeRoom()">
        <option value=""></option>
      </select>
    </div>
  </div>
</div>

The controller:

angular.module('App').controller('Buildings', ['$scope', 'BuildingService', 'Geolocation',
    function ($scope, BuildingService, Geolocation) {
        var vm = this;
        BuildingService.getAll().then(function (buildings) {
            vm.buildings = buildings;
            vm.selected = { // defaults
                building: buildings[0],
                room: null
            };
        });
        if (navigator.geolocation) {
            navigator.geolocation.getCurrentPosition(function (position) {
                vm.selected.building = Geolocation.closest(position, vm.buildings);
                vm.selected.room = null;
                $scope.$apply(); // <--- Why is that needed?
            });
        }
        vm.changeBuilding = function () {
            vm.selected.room = null;
            console.log(vm.selected);
        };
        vm.changeRoom = function () {
            console.log(vm.selected);
        };
    }
]);

The explanation: I fetch an array of buildings from a service, and then set the first one as a default into the scope. Then I calculate (using latitudes, and longitudes of the buildings) which of the buildings is the closest one to my current location, and change the model object accordingly.

This all works. The default building flashes in the first html select control, which then changes into the nearest building.

However, I find it awkward having to inject $scope into the controller, when I'm using the "Controller as" syntax, because usually that is not needed. You see, if I take out the $scope.$apply(); line, the view does not change. It still shows the default building, even though the vm.selected.building actually contains the closest one. (And when I try to select a room from the second html select, the first one updates to the value actually in the vm.selected.building model object.

So, why is this? Is there a way to get the desired functionality without the injection?

EDIT: So the real question is: Why do I have to call $scope.$apply() here? Why does the view not change without it?

finn
  • 33
  • 6
  • You'll need $scope injected if you want things to show in the view. Also just as a side note, there is no need to use the array notation for the dependency injection. – alexrogers Sep 27 '14 at 08:45
  • The things show in the view without the explicitly injected scope when using the "controller as" syntax. (http://www.johnpapa.net/angularjss-controller-as-and-the-vm-variable/) If I've understood correctly one of the points of using "controller as" is that the explicit $scope injection is not needed. – finn Sep 27 '14 at 09:09
  • As for the side note, the documentation says that the js minifies/uglifiers break the code if explicit $inject, or the array notation is not used. (http://code.angularjs.org/1.2.25/docs/guide/di) That's why I've been using it. – finn Sep 27 '14 at 09:13
  • If you're minifying your code though, i'd hope you're using a task runner like grunt or gulp and in which case you can easily use ng-annotate https://github.com/olov/ng-annotate - explained here https://docs.angularjs.org/guide/services – alexrogers Sep 27 '14 at 09:18
  • Why do you not want to use the $scope injection? I think that is the convention if you need to expose stuff for the view. – alexrogers Sep 27 '14 at 09:23
  • Yes, I'm using grunt. Thanks for the mg-annotate tip. I'll look into that. Getting rid of the array notation would definitely simplify the code. – finn Sep 27 '14 at 09:27

1 Answers1

1

Here in this section you update vm.selected.building in a callback

        navigator.geolocation.getCurrentPosition(function (position) {
            /* See how this is a callback passed to getCurrentPosition that will be executed later */
            vm.selected.building = Geolocation.closest(position, vm.buildings);
            vm.selected.room = null;
            $scope.$apply(); // <--- Why is that needed?
        });

During apply() angular synchronizes you bindings. After most actions apply is called automatically e.g. after your constructor code or after most callbacks you provide to angular code. However if you have a third party library which does not call $apply automatically for you, you need to do so yourself. So you either have to inject scope so that you can apply here, OR you need to make your navigator (which seems to be a global variable) an angular service and make this one call apply for you.

yankee
  • 38,872
  • 15
  • 103
  • 162
  • Ok, thanks. I think I understand. I'll have to try making that a service. The navigator variable comes from the browser. I do not create it anywhere in the code. http://www.w3schools.com/html/html5_geolocation.asp – finn Sep 27 '14 at 09:39