Jimothey
Jimothey

Reputation: 2434

ng-if not updating when $scope var changes in AngularJS

In my view I have this:

<div class="tab-loading-container" ng-if="mapStatus.loading =='true'">
    <div class="tab-loading">Loading map</div>
</div>

In my controller I have:

$scope.mapStatus = {};

and then various functions that update the scope var used by the ng-if, when certain criteria are met, such as being offline etc (for example:

function enableMap () {
    $scope.mapStatus.loading = false;
}

My issue is that although the scope var is getting changed correctly (confirmed with good 'ol console.log and angular chrome extension) the ng-if in the view never updates / gets added / removed to show / remove the div.

I've tried using $apply (though my understanding of it isn't great), for example:

function enableMap () {
    $scope.$apply(function() {
        $scope.mapStatus.loading = false;
    });
}

but that throws errors such as Error: [$rootScope:inprog] $apply already in progress

Feel like I'm missing something obvious :(


More code as requested:

angular.module('app.case.controller', [])
.controller('CaseController', function($rootScope, $scope, $state, $stateParams, $filter, casesFactory, $ionicActionSheet, $ionicModal, $ionicTabsDelegate, $ionicLoading, ConnectivityMonitor) {

/// Other non related code

// location map - refactor into a factory
    $scope.mapStatus = {};

    function initMap () {
        var pos = { 'lat':  52.6136149, 'lng': -1.1936672 };

        var latLng = new google.maps.LatLng(pos.lat, pos.lng);

        var mapOptions = {
            center: latLng,
            zoom: 15,
            mapTypeId: google.maps.MapTypeId.ROADMAP,
            fullscreenControl: false,
            mapTypeControl: false
        };

        $scope.map = new google.maps.Map(document.getElementById('map'), mapOptions);

        google.maps.event.trigger(map, 'resize');

        //Wait until the map is loaded
        google.maps.event.addListenerOnce($scope.map, 'idle', function () {

            enableMap();

            console.log('map loaded');

            var marker = new google.maps.Marker({
                map: $scope.map,
                animation: google.maps.Animation.DROP,
                position: latLng
            });

            google.maps.event.trigger(map, 'resize'); 

            $scope.map.setCenter(latLng);

        });
    }

    function loadGoogleMaps () {    
        $scope.mapStatus.loading = true;

        // This function will be called once the SDK has been loaded
        window.mapInit = function(){
            initMap();
        };

        // Create a script element to insert into the page
        var script = document.createElement('script');
        script.type = 'text/javascript';
        script.id = 'googleMaps';
        script.src = 'https://maps.googleapis.com/maps/api/js?key=XXX&callback=mapInit';

        document.body.appendChild(script);
    }

    function enableMap () {
        $scope.mapStatus.loading = false;
        $scope.mapStatus.offline = false;
    }

    function disableMap () {
        $scope.mapStatus.offline = true;
    }

    function checkLoaded () {
        if (typeof google == 'undefined"' || typeof google.maps == 'undefined') {
            loadGoogleMaps();
        } else {
            enableMap();
        }       
    }

    function addConnectivityListeners () {

        if (ionic.Platform.isWebView()) {

            // Check if the map is already loaded when the user comes online, if not, load it
            $rootScope.$on('$cordovaNetwork:online', function(event, networkState) {
                checkLoaded();
            });

            // Disable the map when the user goes offline
            $rootScope.$on('$cordovaNetwork:offline', function(event, networkState) {
                disableMap();
            });

        } else {

            //Same as above but for when we are not running on a device
            window.addEventListener("online", function(e) {
                checkLoaded();
            }, false);    

            window.addEventListener("offline", function(e) {
                disableMap();
            }, false);  
        }
    }

    function showMap () {

        console.log('showMap() called');

        if (typeof google == 'undefined' || typeof google.maps == 'undefined') {

            console.warn("Google Maps SDK needs to be loaded");

            disableMap();

            if (ConnectivityMonitor.isOnline()){
                loadGoogleMaps();
            }
        }
        else {
            if (ConnectivityMonitor.isOnline()){
                initMap();
                enableMap();
            } else {
                disableMap();
            }
        }

        addConnectivityListeners();
    }

    $scope.initMap = function () {
        showMap();
    };

To confirm the scope vars are being changed here's a screenshot from the AngularJS chrome extension:

enter image description here

Upvotes: 3

Views: 12304

Answers (4)

Lindauson
Lindauson

Reputation: 3421

If you are using John Papa's Controller As pattern with vm make sure you are using the name of the controller in the template and not vm:

controller: 'featureController',
controllerAs: 'featureCtrl'

Use vm.value in the Controller:

const vm = this;

vm.value = true;

Use featureController.value in the template:

<span>Here is the value: {{featureController.value}}</span>

Upvotes: 0

Caner
Caner

Reputation: 59168

(I post this answer in case someone comes across this problem using angular v2+)

This problem could happen if the expression ngIf directive is referring to gets updated by a (asynchronous) call coming from outside angular zone. This won't cause change detection in angular, so your view won't be updated until next change detection happens. You can learn more about zones and change detection from these links: zones change detection

I came across this issue when I was integrating google sign in to my component. signInChangeListener is called by the 3rd party google library whenever the user signs in or out:

export class GoogleLoginService {
   signedIn: boolean = false;
   ...
   signInChangeListener(isSignedIn) {
       this.signedIn = isSignedIn;
   }
}

In my HTML I had:

<div *ngIf="signedIn"> ... </div>

I had to change my code so that the change is detected by angular:

import { Injectable, NgZone } from '@angular/core';

export class GoogleLoginService {
   signedIn: boolean = false;

   constructor(private ngZone: NgZone) { }
   ...
   signInChangeListener(isSignedIn) {
      // Run the value change inside angular 
      this.ngZone.run(() => {
         this.signedIn = isSignedIn;
      });
   }
}

Upvotes: 0

Mike Feltman
Mike Feltman

Reputation: 5176

I'm not sure if this will solve your issue or not, but before you get too far into this, I'd recommend getting rid of the references to $scope and use controllerAs. I've seen numerous instances where references to properties/methods added directly to $scope fail for no particular reason. Specificially I'd recommend that you:

  • remove where you have injected $scope,
  • add var ctrl=this to your constructor code in your controller,
  • change all of the references to $scope in your controller to ctrl,
  • add controllerAs vm to where you load your controller,
  • change all of the references to controller properties in your view to be prefixed with vm.

You may get lucky and this might just solve your problem, but either way, this is still a better way to work with controllers. (If you are on Angular 1.5+ I'd really recommend you refactor this as a component before you get too far.)

Upvotes: 1

George
George

Reputation: 6739

I've changed it so it's only a boolean value and not comparing it to a string. Are you ever setting $scope.mapStatus.loading to true anyway? Otherwise, it will never show as it'll always be false.

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

function MyCtrl($scope) {
  $scope.mapStatus = {};

  $scope.hideMe = function() {
    $scope.mapStatus.loading = false;
  }

  $scope.showMe = function() {
    $scope.mapStatus.loading = true;
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="myApp">
  <div ng-controller="MyCtrl">
    <div class="tab-loading-container" ng-if="mapStatus.loading">
      <div class="tab-loading">Loading map</div>
    </div>
    <button ng-click="hideMe()">Hide Me</button>
    <button ng-click="showMe()">Show Me</button>
  </div>
</div>

Upvotes: 0

Related Questions