Why won't this data bind? An odd case in Angularjs


#1

Here is the controller:

.controller('DataCtrl', function($ionicPlatform, $scope, $rootScope) {

	$ionicPlatform.ready(function(){
		$scope.analyticsOn = localStorage.analytics; 
		console.log('analytics are', $scope.analyticsOn);
	
		$scope.counter = 0;
		$scope.change = function(){
			$scope.counter++;
			console.log('analytics are ' + $scope.analyticsOn);
			localStorage.analytics = $scope.analyticsOn; 
		}
	})
});

And here is the html:

<li class="item item-toggle">
   <i class="icon ion-cloud"></i> Data Tracking is {{analyticsOn}} {{counter}}
      <label class="toggle toggle-balanced">
         <input type="checkbox" ng-model="analyticsOn" ng-change="change()">
		  <div class="track">
		     <div class="handle"></div>
		  </div>
       </label>
	</li>

Pressing the toggle gets the console message “analytics are undefined” no matter what.

Yet, and here is the thing, in the app the {{analyticsOn}} updates and flips from true to false and the {{counter}} counts up from an initial value of zero.

So I know that -

  • a. Two way data binding IS working, or the counter wouldn’t work.
  • b. analyticsOn IS changing.

So why don’t the console.logs show it? The plan is to put the value back into the localStorage.analytics for data persistence but this is the first time I’ve used ng-change. If I did this in pure JavaScript it would be fine.

Anyone?


#2

Hmmm, we’re missing where and how localStorage is being used/defined. That is probably where the issue lies. Everything else up to that looks correct.

Can you post your change() function as well as the local storage information?


#3

I’ve posted the change function. It’s right there. $scope.change = function()

The localStorage.analytics is saving a true/false state. The first time you use it it’s undefined, and at another point in the code there is a conditional on it being true. If it’s false or undefined that conditional doesn’t fire, that’s why in the change() function the new value of analyticsOn is stored in localStorage.analytics.

In the jQuery Mobile version of this the conditional allows analytics to be recorded AND updates the DOM. With two way data binding I shouldn’t have to update the DOM - if $scope.analyticsOn is true the toggle should automatically be set to true. That’s partly the point of Angular, no?


#4

You are right, it seems silly but I’ve noticed that in Angular, if the item you are binding is not a property of an object (besides $scope) that it doesn’t bind correctly. I do remember having issues with this, which is why all my forms and any blinded data is wrapped. So instead of doing “$scope.analyticsOn” try “$scope.bindings.analyticsOn” you can change bindings to whatever you want.

This may allow angular to properly bind that data. is localStorage a jQuery module that you’re using? I really would recommend ngStorage, it’s amazing. You just plop it in, inject it into your controller and then when you call things like $storage.analytics = true; it will automatically set it, and then getting it is the same way, but it’s wrapped with angular so it updates with any scope changes unlike some jQuery and other javascript functions because they’re not in the scopes digest.

Edit:

Just another thought that I might add, often time Angular will work seamlessly with things like this, however you have to remember that it’s an entire MVC framework, where as jQuery mobile is not (last time I used it). It just handles the UI and a little bit of the binding, so the scope and how it digests and binds data will work differently. just a thought.

It’s definitely one of those weird bugs that I hate coming across when I’m doing things like this, so I hope some of this information can push it in the right direction .

Here is that link for ngStorage by the way.


#5

localStorage is just vanilla JavaScript localStorage.

The thing that annoys me about Angular is we are told it’s JavaScript but it plainly isn’t. timeOut doesn’t work in Angular, you have to use $timeout.

I reckon I’ll have a look at ngStorage, thanks.


#6

Haha. It IS just javascript, but because it wraps everything in it’s own compile loop some things don’t work.

Using certain “vanilla” javascript functions is kind of like trying to tell someone that you’re going to ride on their bike with them, while they’re already going, and then trying to hop on while they are still riding. I can’t remember who explained it to me like that but it made sense.

Yes, ngStorage is fabulous, let me know if you have any questions on it. It’s pretty straightforward though


#7

I thought it would be like jQuery, where you can just mix and match jQuery and JavaScript and it doesn’t matter where or how you do it.

I’ll give ngStorage a go and get in touch if I have any issues. Thanks.


#8

Question: Am I right in thinking that ngStorage has two way data binding straight into and out of local storage?

Because if that’s true… wow just wow.


#9

You should be able to use localStorage directly in Angular - it seems to work for me.

I’d stick with the localStorage.getItem() / localStorage.setItem() syntax though as I’ve had intermittent problems with the style in your code and the localStorage[“keyname”] style.

The most major difficulty I’ve faced is the bindings vs. the digest cycle but beyond that you are free to use any JavaScript you wish.

You can even do jQuery-like selectors without including jQuery by using jqlite see https://docs.angularjs.org/api/ng/function/angular.element - can be helpful for those of us transitioning to Angular as jqlite is built in to Angular.


#10

Thanks for the tip on jqlite, I’ll look into that too.

If $localStorage does what it looks like it does I’ll be using that from now on. You should look into it - it appears you skip a step as comparted to JavaScript’s handling of localStorage.


#11

You can use it right out of the box! That’s why I love it. So I follow that example that they have in the repo and it’s awesome. So I inject with with $localStorage, and then do $scope.$storage = $localStorage. Then in my settings page I have the ionic toggles with the check boxes and I just bind it to $localStorage.settingName and it just works seamlessly.


#12

Okay, I’m not very good at getting angular scopes but I’ve got it working now.

Yes, it seems that it binds INTO and OUT OF local storage!

Here’s my new code, for comparison.

.controller('DataCtrl', function($ionicPlatform, $scope, $rootScope, $localStorage) {

	$scope.$storage = $localStorage;

	$ionicPlatform.ready(function(){
		if($scope.$storage.analytics==true){
	             //initialise Google Analytics plugin here
		}

		console.log('start analytics are', $scope.$storage.analytics);
	
		$scope.counter = 0;
		$scope.change = function(){
			$scope.counter++;
			console.log('analytics are ' + $scope.$storage.analytics);
		}
	})
});

And the html.

<li class="item item-toggle">
     <i class="icon ion-cloud"></i> Data Tracking is {{$storage.analytics}} {{counter}}
	     <label class="toggle toggle-balanced">
	       <input type="checkbox" ng-model="$storage.analytics" ng-change="change()">
         	       <div class="track">
		            <div class="handle"></div>
	             </div>
	     </label>
</li>

Isn’t that wonderful?


#13

Perfect!

I hope you see how beneficial the angular scope is. If you really want to dive into that, Directives in angular allow you to sort of wrap jQuery and raw javascript plugins/scripts to work. An example of this is Bootstrap the UI framework, some people have written directives that wrap those functions to that it updates and binds things properly. They’re really complex, I don’t generally mess with them, but they can be of use.

I’m so glad it’s working!


#14

The crazy thing about this is that some of that code is redundant anyway - it’s just checkin/logging code. The actual storage and retrieval of the true/false value for initialising the analytics is so, so simple now.

I use Bootstrap already (I’m assuming you mean Twitter Bootstrap) on my site. I’ve seen some great demos showing how two way binding can dynamically alter the UI with angular. It’s so easy to have a theme toggle, and with ngStorage so easy to make it persistent.


#15

Hmmm.

Some further testing shows that although on first use I can set $storage.analytics to true or false subsequent changes are not being stored and retrieved from local storage.

Here is the code:

permissionCallback = function(permission){  
  if(permission===1){
      console.log("analytics allowed");
      analytics.startTrackerWithId('UA-45544004-1'); 
      $scope.$storage.analytics=true;
      navigator.notification.alert('You can turn analytics off in the Data Tracking section at any time.', null, 'Analytics On', 'OK');
  }else{
      console.log("analytics denied");
      $scope.$storage.analytics=false;
      navigator.notification.alert('You can turn analytics on in the Data Tracking section at any time.',null , 'Analytics Off', 'OK');
  }
}

if(typeof $scope.$storage.analytics === 'undefined'){
  navigator.notification.confirm('The app would like your permission to collect data on how you use the app. No personal or user identifiable data will be collected.', permissionCallback, 'Attention', ['Allow','Deny']);
}
else{
  console.log('start analytics are', $scope.$storage.analytics); 
  if(typeof analytics !== 'undefined'){
    console.log("analytics functioning");
    analytics.startTrackerWithId('UA-45544004-1'); 

      $scope.trackClick = function(category, action){
        analytics.trackEvent(category, action); 
        console.log('Tracking category: ' + category + ', Section: ' + action + '.');
      }
  }
}
$scope.counter = 0;
$scope.change = function(){
  $scope.counter++;
  console.log('analytics are ' + $scope.$storage.analytics);
}

And here is the html again.

<li class="item item-toggle">
<i class="icon ion-cloud"></i> Data Tracking is {{$storage.analytics}} {{counter}}
<label class="toggle toggle-balanced">
<input type="checkbox" ng-model="$storage.analytics" ng-change="change()">
	 <div class="track">
	         <div class="handle"></div>
    </div>
  </label>
</li>

It’s either a fault with my logic or, and I think this more likely, a misunderstanding about the scope of the data.

The odd thing is the console log in the change() function (which is purely for tracking these things) is always correct. So using $storage.analytics in the html is the correct way to do it (using $scope.storage.analytics causes all sorts of errors) and it is indeed binding from the html into $scope.storage.analytics.

So why isn’t it saving it to local storage when using the toggle?


#16

I’m not sure if this is any help but may point you in the right direction; where I’ve had problems with angular not binding it’s often a problem with the scope not being updated within the use case you are working with…

http://jimhoskins.com/2012/12/17/angularjs-and-apply.html
https://docs.angularjs.org/api/ng/type/$rootScope.Scope

and sometimes $scope.$apply or $scope.$digest will fix it, but it’s not an elegant solution.