AngularJS OneTime Binding with ng-repeat doesn't let me refresh scope - javascript

I have a list that I am using ng-repeat and one-time binding because there is no need for me to update once I have the data.
I was hoping that I could use one-time binding and force a digest, but I get the common error that a $digest is already being processed or whatever it is.
My code looks like the following
$scope.$on(IonicEvents.beforeEnter, function() {
Locations.getAllLocations()
.then(function(allLocations) {
$scope.model.locations = allLocations;
//I have tried $scope.$digest()..
//also $scope.$apply()...
}, function(error) {
$log.error('Error retrieving locations');
})
});
My ng-repeat in my template looks like the following:
<ion-item class="item" ng-repeat="location in ::model.locations">
<div class="row">
<div class="col">
<p>{{::location.name}}</p>
</div>
</div>
</ion-item>
Any ideas, or am I completely off-track?

I think First you may check if(!$scope.$$phase) then apply your proccess
$scope.$on(IonicEvents.beforeEnter, function() {
if(!$scope.$$phase) {
Locations.getAllLocations()
.then(function(allLocations) {
$scope.model.locations = allLocations;
//I have tried $scope.$digest()..
//also $scope.$apply()...
}, function(error) {
$log.error('Error retrieving locations');
})
}
});

Yes I think you are off-track.
My idea for your particular situation would be using a clone object (with no reference to the main object) once they are retrieved from Locations.getAllLocations() using
$scope.clonedLocationObj = angular.copy($scope.model.locations);
and render this object to your view so that it won't effect your main object.
As far as one way binding is concerned, Angular doesn't let you change the one time binded model again because when we declare a value such as {{ ::foo }} inside the DOM, once this foo value becomes defined, Angular will render it, unbind it from the watchers and thus reduce the volume of bindings inside the $digest loop. Simple!
angular one time binding syntax article
Hope it helps.

Related

Unable to call $scope function in ng-repeat

I have this function showPopupSelectTopic(subject) to call in my ng-repeat html code. But it does not work at all.
<div style="width:100%;" ng-controller="manageStudyCtrl">
<div class="div-subject" ng-repeat="subject in dataSubject" ng-click="showPopupSelectTopic(subject)">
<div class="round-button-subject-2">
<div class="subject-name-2 subject-eng" style="color:{{subject.subject_code.colour_code}}">
{{subject.subject_code.short_name}}
<div>{{subject.avg_overall_coverage | number : 0}}%</div>
</div>
<circular-progress
value = "subject.avg_overall_coverage"
max="100"
orientation="1"
radius="36"
stroke="8"
base-color="#b9b9b9"
progress-color="{{subject.subject_code.colour_code}}"
iterations="100"
animation="easeInOutCubic"
></circular-progress>
</div>
</div>
</div>
I want to call my showPopupSelectTopic(subject)in my controller so that I can make popup and manipulate the data.
I have done outside from ng-repeatand its working perfectly. However if I used in ng-repeat then it would not execute as expected. How to solve this issue?
My controller:
angular.module('manageStudy', [])
.controller('manageStudyCtrl', function($scope,){
$scope.showPopupSelectTopic = function(subject) {
alert(subject.chapter_id);
};
});
That's not possible due to every ng-repeat creating its own child scope. That being said, ever functio invocation will lead the child to copy some variables into its own scope. You'd have to use their parentscope or refere to the origin $scope of your controller.
ng-click="$parent.showPopupSelectTopic(subject)"
This should solve the problem. However, it's kinda dirty. A better solution would be to return your parents scope and use it in every child scope just like that. So declare a function inside of your controller (e.g. $scope.getScope) and let it simply return its $scope. Afterwards you'll be able to access it properly.
$scope.getScope = function() {
return $scope;
}
ng-click = "getScope().showPopupSelectTopic(subject)"
ng-repeat works only with an iterate able object, like array or collection. Before you open the div where you intent to repeat, the iterate able object must be in ready in the scope. Try using ngInit instead of ngClick to initialize the array before attempting to ngRepeat

How do I pass data through an ES6 "class" function in angular

I'm trying to create a controller in angular as an ES6 class. This works as expected ( able to print the string returned through the message function). See below.
<div ng-controller="ProfilingCtrl as profileData">
{{profileData.message()}}
</div>
But what I want to do now i to be able to use a function class in ng-repeat, like below.
<div ng-controller="ProfilingCtrl as profileData">
<div ng-reapeat="x in profileData.getData()">
{{x[0]}}
</div>
</div>
getData() is a function returning an array of strings. Is this even possible? If not, how would one go about doing such an operation?
Big chance your issue is coming from ng-reapeat -> typo should be ng-repeat. Still helping you with your code:
It would be useful if you shared your controller code. But I'll make something up.
First, unless you are trying to display the first character of each string in your data array, remove the [0], as x represents the element resulting from iteration.
<div ng-controller="ProfilingCtrl as profileData">
<div ng-repeat="x in profileData.getData()">
{{x}}
</div>
</div>
Further recommendation: bind your data to a variable, instead of a function. ES6 style (though I would still stick with functions for controllers)
class MyController {
constructor(myDataService) {
this.myDataService = myDataService;
this.myData = []; // This is where the data will be
}
handleError(error) {
// do something error
}
getData() {
this.myDataService.getData() // this is some method that returns a promise
.then(response => this.myData = response) // response from http in a promise
.catch(error => this.handleError(error));
}
$onInit() { // angular will take care of starting this off
this.getData();
}
}
Note the $onInit is called by Angular when the component is ready. Read about lifecycle hooks: https://toddmotto.com/angular-1-5-lifecycle-hooks
The lifecycle hook is outside of scope of your question, but still nice to get into.
Note that the comment made earlier about "bind it to $scope" is not recommended. ControllerAs as you are doing is better, could later be put elsewhere though.

ngRepeat track by: How to add event hook on model change?

I have a simple ngRepeat like the following:
<some-element ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id">
<!-- stuff -->
</some-element>
arrayOfRecords is updated from a server and may contain new data.
ngRepeat's track by feature can figure out when a new element is added to the array and automatically updates the DOM without changing the existing elements. I would like to hook into that code and execute a callback function when there's new data coming in or old data is removed. Is it possible to easily do this via Angular?
From what I understand, there's a $$watchers which triggers callbacks whenever there's changes to certain variables, but I don't know how to go about hacking that. Is this the right direction?
NOTE: I know I can manually save the arrayOfRecords and compare it with the new values when I fetch them to see what changed. However, since Angular already offers a track by feature which has this logic, it would be nice if I can have Angular automatically trigger an event callback when an element is added or removed from the array. It doesn't make sense to duplicate this logic which already exists in Angular.
Probably you could create a directive and add it along with ng-repeat, so the directive when created(when item is added by ng-repeat) will emit an event and similarly when the item is destroyed it will emit another event.
A simple implementation here:
.directive('tracker', function(){
return{
restrict:'A',
link:function(scope, el, attr){
scope.$emit('ITEM_ADDED', scope.$eval(attr.tracker))
scope.$on('$destroy', function(){
scope.$emit('ITEM_REMOVED', scope.$eval(attr.tracker))
});
}
}
});
and use it as:
<some-element
ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id"
tracker="item">
and listen for these events at the parent controller for example.
Demo
Or using function binding but in a different way, without using isolate scope for that.
.directive('tracker', function() {
return {
restrict: 'A',
link: function(scope, el, attr) {
var setter = scope.$eval(attr.tracker);
if(!angular.isFunction(setter)) return;
setter({status:'ADDED', item:scope.$eval(attr.trackerItem)});
scope.$on('$destroy', function() {
setter({status:'REMOVED', item:scope.$eval(attr.trackerItem)});
})
}
}
});
Demo
The one above was specific to your question since there is no other built in way, Note that if you were to really find out the items added/removed, you could as well do it in your controller by diffing the 2 lists. You could try use lodash api like _.unique or even simple loop comparisons to find the results.
function findDif(oldList,newList){
return {added:_.uniq(newList, oldList), removed:_.uniq(oldList, newList)};
}
Demo
You can change it to:
<div ng-model="arrayOfRecords">
<some-element ng-repeat="singleRecord in arrayOfRecords track by singleRecord.id">
<!-- stuff -->
</some-element>
</div>
The model will change as soon as arrayOfRecords will change.

How to have an angularJS directive on a page called via ajax?

I have the following html (which can be accessed directly or called via ajax):
<section id="content" ng-controller="setTreeDataCtrl" get-subthemes>
<dl ng-repeat="subtheme in allSubthemes">
<dt>{{subtheme.Title}}</dt>
</dl>
Then I'm using the following directive:
myApp.directive('getSubthemes', function() {
return function($scope, element, attrs) {
$scope.allSubthemes = [];
angular.forEach($scope.data.Themes, function(value, key) {
angular.forEach(value.SubThemes, function(value2, key2) {
$scope.allSubthemes.push({
'ThemeTitle': value.Title,
'ThemeUrlSlug': value.UrlSlug,
'Title': value2.Title,
'UrlSlug': value2.UrlSlug
});
});
});
}
});
$scope.allSubthemes seems ok, but the dl's don't get rendered.
I can see for a second everything rendered properly and then it get's back to {{subtheme.Title}}, almost like it's being "unrendered"... any ideas of what I'm doing wrong?
Demo jsFiddle: http://jsfiddle.net/HMp3a/
rGil fixed the jsFiddle. It was missing a ng-app="pddc" declaration on an element so Angular did not know where to begin its magic.
I'd like to mention another way to render to the data in question. I suggest using an ng-repeat within an ng-repeat. See my forked & updated fiddle here. You can actually refer to the parent theme within the ng-repeat of the subtheme, so you don't have to copy values from the parent theme into each subtheme (which effectively eliminates the need for the directive in this example).
Another reason to use a nested ng-repeat is because of async issues that could come up when pulling data from a web service asynchronously. What could happen is when the directive executes, it may not have any data to loop through and populate because the data hasn't arrived yet.
If you use two ng-repeats, Angular will watch the $scope.data and re-run the ng-repeats when the data arrives. I've added a 500 ms delay to setting the data in my example to simulate web service latency and you'll see that even with the "latency", the data eventually renders.
There are two other ways around the async issue:
Use scope.$watch() in your directive, to watch for the data manually, or
Use the "resolve" functionality from Angular's routing feature to make sure the data is retrieved prior to controller execution.
While these alternative methods work, I think both are more complicated then just using two ng-repeats.

How to clear/remove observable bindings in Knockout.js?

I'm building functionality onto a webpage which the user can perform multiple times. Through the user's action, an object/model is created and applied to HTML using ko.applyBindings().
The data-bound HTML is created through jQuery templates.
So far so good.
When I repeat this step by creating a second object/model and call ko.applyBindings() I encounter two problems:
The markup shows the previous object/model as well as the new object/model.
A javascript error occurs relating to one of the properties in the object/model, although it's still rendered in the markup.
To get around this problem, after the first pass I call jQuery's .empty() to remove the templated HTML which contains all the data-bind attributes, so that it's no longer in the DOM. When the user starts the process for the second pass the data-bound HTML is re-added to the DOM.
But like I said, when the HTML is re-added to the DOM and re-bound to the new object/model, it still includes data from the the first object/model, and I still get the JS error which doesn't occur during the first pass.
The conclusion appears to be that Knockout is holding on to these bound properties, even though the markup is removed from the DOM.
So what I'm looking for is a means of removing these bound properties from Knockout; telling knockout that there is no longer an observable model. Is there a way to do this?
EDIT
The basic process is that the user uploads a file; the server then responds with a JSON object, the data-bound HTML is added to the DOM, then the JSON object model is bound to this HTML using
mn.AccountCreationModel = new AccountViewModel(jsonData.Account);
ko.applyBindings(mn.AccountCreationModel);
Once the user has made some selections on the model, the same object is posted back to the server, the data-bound HTML is removed from then DOM, and I then have the following JS
mn.AccountCreationModel = null;
When the user wishes to do this once more, all these steps are repeated.
I'm afraid the code is too 'involved' to do a jsFiddle demo.
Have you tried calling knockout's clean node method on your DOM element to dispose of the in memory bound objects?
var element = $('#elementId')[0];
ko.cleanNode(element);
Then applying the knockout bindings again on just that element with your new view models would update your view binding.
For a project I'm working on, I wrote a simple ko.unapplyBindings function that accepts a jQuery node and the remove boolean. It first unbinds all jQuery events as ko.cleanNode method doesn't take care of that. I've tested for memory leaks, and it appears to work just fine.
ko.unapplyBindings = function ($node, remove) {
// unbind events
$node.find("*").each(function () {
$(this).unbind();
});
// Remove KO subscriptions and references
if (remove) {
ko.removeNode($node[0]);
} else {
ko.cleanNode($node[0]);
}
};
You could try using the with binding that knockout offers:
http://knockoutjs.com/documentation/with-binding.html
The idea is to use apply bindings once, and whenever your data changes, just update your model.
Lets say you have a top level view model storeViewModel, your cart represented by cartViewModel,
and a list of items in that cart - say cartItemsViewModel.
You would bind the top level model - the storeViewModel to the whole page. Then, you could separate the parts of your page that are responsible for cart or cart items.
Lets assume that the cartItemsViewModel has the following structure:
var actualCartItemsModel = { CartItems: [
{ ItemName: "FirstItem", Price: 12 },
{ ItemName: "SecondItem", Price: 10 }
] }
The cartItemsViewModel can be empty at the beginning.
The steps would look like this:
Define bindings in html. Separate the cartItemsViewModel binding.
<div data-bind="with: cartItemsViewModel">
<div data-bind="foreach: CartItems">
<span data-bind="text: ItemName"></span>
<span data-bind="text: Price"></span>
</div>
</div>
The store model comes from your server (or is created in any other way).
var storeViewModel = ko.mapping.fromJS(modelFromServer)
Define empty models on your top level view model. Then a structure of that model can be updated with
actual data.
storeViewModel.cartItemsViewModel = ko.observable();
storeViewModel.cartViewModel = ko.observable();
Bind the top level view model.
ko.applyBindings(storeViewModel);
When the cartItemsViewModel object is available then assign it to the previously defined placeholder.
storeViewModel.cartItemsViewModel(actualCartItemsModel);
If you would like to clear the cart items:
storeViewModel.cartItemsViewModel(null);
Knockout will take care of html - i.e. it will appear when model is not empty and the contents of div (the one with the "with binding") will disappear.
I have to call ko.applyBinding each time search button click, and filtered data is return from server, and in this case following work for me without using ko.cleanNode.
I experienced, if we replace foreach with template then it should work fine in case of collections/observableArray.
You may find this scenario useful.
<ul data-bind="template: { name: 'template', foreach: Events }"></ul>
<script id="template" type="text/html">
<li><span data-bind="text: Name"></span></li>
</script>
Instead of using KO's internal functions and dealing with JQuery's blanket event handler removal, a much better idea is using with or template bindings. When you do this, ko re-creates that part of DOM and so it automatically gets cleaned. This is also recommended way, see here: https://stackoverflow.com/a/15069509/207661.
I think it might be better to keep the binding the entire time, and simply update the data associated with it. I ran into this issue, and found that just calling using the .resetAll() method on the array in which I was keeping my data was the most effective way to do this.
Basically you can start with some global var which contains data to be rendered via the ViewModel:
var myLiveData = ko.observableArray();
It took me a while to realize I couldn't just make myLiveData a normal array -- the ko.oberservableArray part was important.
Then you can go ahead and do whatever you want to myLiveData. For instance, make a $.getJSON call:
$.getJSON("http://foo.bar/data.json?callback=?", function(data) {
myLiveData.removeAll();
/* parse the JSON data however you want, get it into myLiveData, as below */
myLiveData.push(data[0].foo);
myLiveData.push(data[4].bar);
});
Once you've done this, you can go ahead and apply bindings using your ViewModel as usual:
function MyViewModel() {
var self = this;
self.myData = myLiveData;
};
ko.applyBindings(new MyViewModel());
Then in the HTML just use myData as you normally would.
This way, you can just muck with myLiveData from whichever function. For instance, if you want to update every few seconds, just wrap that $.getJSON line in a function and call setInterval on it. You'll never need to remove the binding as long as you remember to keep the myLiveData.removeAll(); line in.
Unless your data is really huge, user's won't even be able to notice the time in between resetting the array and then adding the most-current data back in.
I had a memory leak problem recently and ko.cleanNode(element); wouldn't do it for me -ko.removeNode(element); did. Javascript + Knockout.js memory leak - How to make sure object is being destroyed?
Have you thought about this:
try {
ko.applyBindings(PersonListViewModel);
}
catch (err) {
console.log(err.message);
}
I came up with this because in Knockout, i found this code
var alreadyBound = ko.utils.domData.get(node, boundElementDomDataKey);
if (!sourceBindings) {
if (alreadyBound) {
throw Error("You cannot apply bindings multiple times to the same element.");
}
ko.utils.domData.set(node, boundElementDomDataKey, true);
}
So to me its not really an issue that its already bound, its that the error was not caught and dealt with...
I have found that if the view model contains many div bindings the best way to clear the ko.applyBindings(new someModelView); is to use: ko.cleanNode($("body")[0]); This allows you to call a new ko.applyBindings(new someModelView2); dynamically without the worry of the previous view model still being binded.
<div id="books">
<ul data-bind="foreach: booksImReading">
<li data-bind="text: name"></li>
</ul>
</div>
var bookModel = {
booksImReading: [
{ name: "Effective Akka" },
{ name: "Node.js the Right Way" }]
};
ko.applyBindings(bookModel, el);
var bookModel2 = {
booksImReading: [
{ name: "SQL Performance Explained" },
{ name: "Code Connected" }]
};
ko.cleanNode(books);
ko.applyBindings(bookModel2, books);

Categories

Resources