AngularJS RDBMS data handling best practices - angularjs

With SPA app growth, some things become unclear with Angular JS.
Suppose, we have a RDBMS data structure with lots of referencing entities, like Country (id), State (id, countryId), City (id, stateId), Street (id, cityId).
And we need to render most of it within a single view, just like a street index grouped by parent recursively. I know it's not so practical example, especially considering that our real application entities have mutual data that we need to bind to.
How would "Best Practice" look like?
Now, we're using lots of ng-repeat's like this:
<ul>
<li ng-repeat="country in countryList">
{{ country.name }}
<ul ng-if="(stateList | filter:{countryId === country.id}).length">
<li ng-repeat="state in filterStatesByCountryId(country.id)">
{{ state.name }}
<ul ng-if="(cityList | filter:{stateId === state.id}).length">
<li ng-repeat="city in cityList | filter:{stateId === state.id}">{{ city.name }}</li>
</ul>
</li>
</ul>
</li>
</li>
filterStatesByCountryId(country.id) is the same as stateList | filter:{countryId === country.id}.
Our app must have bindings in place, so that if street.name property gets updated in the StreetService, the view reflects this change. Therefore $scope.streetList = StreetService.all is not an option right out of the box and instead of $scope.streetList we would have $scope.street = StreetService and then use it as $scope.street.all | filter:{cityId: city.id}.
Must say this looks scary.
What would be the most optimal both performance and service architecture wise way of rendering and displaying this kind of lists of data?
One of idea would be to store whole merged hierarchy of entities into one. So that country with all states, cities and streets becomes one object. Then country entity would have states {Array.<StateEntity>} property, state {StateEntity} would have cities {Array.<CityEnity>} prop, etc. But then if a single street.name would get updated, we would need to traverse whole country data object to find what street we need to update, do the update and then re-render the view, or just have a $scope.watch(expr, fn, true) which would be an overkill for that complex hierarchy.
Thanks.
P.S. We cannot switch to NoSQL for storing data.

Angular is not really built to deal with relational data structures I can see that what you're trying to do here mimicks C# MVC templating for loops.
The thing is let's say you have:
- 100 countries in your list => 100 watchers
- 20-ish states per country => 2000-ish watchers
- 30-ish cities per country => 60 000 watchers
IF you would show everything on one screen, now that is not what you're doing of course, but keep that in mind.
As an initial performance improvement I would add a track by to each ng-repeat statement.
<li ng-repeat="state in filterStatesByCountryId(country.id) track by country.id">
From the angular documentation it states:
If you are working with objects that have an identifier property, you can track by the identifier instead of the whole object. Should you reload your data later, ngRepeat will not have to rebuild the DOM elements for items it has already rendered, even if the JavaScript objects in the collection have been substituted for new ones
and also
Note: track by must always be the last expression
Now, ng-repeat can be a bit sluggish in performance, I would not 2-way databind the view properties:
{{ city.name }} => {{ ::city.name }}
This could give you a significant speed increase in general, because angular won't bind watchers to those fields.
To solve your base problem however, I would model it as a singular JSON dataset. However you don't want to go through the hassle of mutating your dataset to work with Angular. At least I wouldn't, so what I would go for just a simple one-way databinding in this case. I don't know how often a street name is going to change on the fly.
It would look something like this:
<li ng-repeat="country in ::countryList track by country.id">
Also maybe try using controllerAs syntax, helps for cleaner code.
However if you really bump into speed issues with ng-repeat it's always best to write your own version of it tailored to your use-case.

Related

Binding behavior of ng-repeat track by

Lets assume I am binding one big nested object to the $scope of the view shown in the code. Now, the value of an "e" object is updated. This would cause angular the check all bindings and update the DOM. If I used "track by" instead, in each ng-repeat directive, would that mean that only the binding for the "e" object would react and the dom for the "e" object be updated?
<div ng-repeat="a in b">
<div ng-repeat="c in a">
<div ng-repeat="d in c">
<div ng-repeat="e in d">
{{e.value}}<br>
</div>
</div>
</div>
</div>
The bindings will be checked no matter what, and updated only if different, per the digest cycle. As for re-building the DOM elements, Angular uses unique identifiers to determine whether each item in an ng-repeat already has a matching DOM element, or if it needs to render a new one.
By default, Angular creates and manages these unique identifiers under the hood, using the $id of each object (or $$hashKey).
track by was added later, as a way to tell Angular to use a unique identifier of your choice, rather than managing it under the hood.
This is useful when updating the data removes/changes the $id or $$hashKey, triggering unnecessary re-builds of each DOM element, even when the data didn't change at all.
Consider this example:
You have an ngRepeat which displays data records:
<li ng-repeat="item in data">{{item.value}}</li>
You use a service DataService to update your data, which has a fetch() method which retrieves data from an SQL database, and returns the records.
Updating the data in your $scope involves calling that service, and re-assigning your data variable to the result:
$scope.data = DataService.fetch();
That means, even if only one item was different, all the $id or $$hashKey properties are gone or different, and Angular will assume all items are new. It will re-build all the DOM elements from scratch.
However, since your data is from an SQL database, you already have a unique identifier (primary key), the id column. You could then change your ngRepeat to be:
<li ng-repeat="item in data track by item.id">{{item.value}}</li>
Now, instead of looking for $$hashKey, which gets lost every time you re-assign the data, Angular will use the property you told it to (item.id). Since that property does persist across re-assigning the variable, the list is once again optimized, because Angular will only re-build DOM elements for new items.

quick-ng-repeat does not update view

I use quick-ng-repeat Quick-ng-repeat for my list to iterate because it is a huge list and I would improve performance.
Now I recognized that if I change the model, the view is not updated with this code:
<span quick-ng-repeat="item in collection track by $index" quick-repeat-list="items">
{{ item }}
</span>
With this code
<span ng-repeat="item in collection track by $index">
{{ item }}
</span>
every works fine.
Does anyone have any idee why this happens?
Thanks a lot!
quick-ng-repeat does not implement deep watch just like ng-repeat, it implement one way binding approach. So if your model changes frequently, don't use it.
In Angular 1.2 a new addition was made to the syntax of ngRepeat: the amazingly awesome track by clause. It allows you to specify your own key for ngRepeat to identify objects by, instead of just generating unique IDs.
This means that you can change the above to be ng-repeat="task in tasks track by task.id" and since the ID would be the same in both your original tasks and the updated ones from the server – ngRepeat will know not to recreate the DOM elements and reuse them
The Quick ng-repeat is doing one way binding ,it something similar to what we do in angularjs
<div ng-repeat="item in ::items">{{item.name}}</div>
This does not create unnecessary watches.
I think Quick-ng-repeat use Single binding same as in angular 1.4 like (::).Single binding mean it will be create a watcher for your repeat that why it will not effect when object is change.
<div ng-repeat="obj in ::objList">{{obj.name}}</div>

Is there a way to stop angularjs from watching a variable for changes?

I have a variable I need to put in a template, but it's only created and never updated or removed. It is, however, referenced in multiple areas of the template. It's used in ng-repeat, so it's an object. Not sure if that matters.
But I want to reference the variable once and stop angularjs from watching it. Is this possible?
Are you familiar with bind once?
Angular internally creates a $watch for each ng-* directive in order to keep the data up to date, so in this example just for displaying few info it creates 6 + 1 (ngRepeatWatch) watchers per person, even if the person is supposed to remain the same once shown. Iterate this amount for each person and you can have an idea about how easy is to reach 2000 watchers. Now if you need it because those data could change while you show the page or are bound to some models, it's ok. But most of the time they are static data that don't change once rendered. This is where bindonce can really help you.
https://github.com/Pasvaz/bindonce
<ul>
<li bindonce ng-repeat="person in Persons">
<a bo-href="'#/people/' + person.id"><img bo-src="person.imageUrl"></a>
<a bo-href="'#/people/' + person.id" bo-text="person.name"></a>
<p bo-class="{'cycled':person.generated}" bo-html="person.description"></p>
</li>
</ul>

ng-repeat with track by over multiple properties

I have a problem with angular ng-repeat directive.
Currently I work on some project where from the API I get a list of items (some times it could be 1k items) and this list should be refreshed every 5 seconds (it is monitoring related project).
When the list length is a little bigger the website while re-rendering DOM could "slow". It comes out that angular regenerate the whole DOM (but 95% of item are the same ! )
One of the possible approach is to set "track by" expression for example to item.id. But here comes another problem, I also want regenerate items when for example descriptions was changed by other user. Since track by is expression to item.id changes in item.description didn't update item in DOM.
There is way to track by over multiple properties? Maybe some function?
Or maybe do comparison by "hand" ?
Any ideas, code samples I would appreciate :)
UPDATE
what I discover when I set track by to item.id angular didn't re-crete html for items, just update value in already created element and it seems to be "faster" then removing and creating.
Previously I though a little bit different.
FIX
For those who are looking for better performance over >1k items in ng-repeat USE track by item.id it will boost your performance ;)
You do not need to create a function to handle track by multi properties.
You can do:
<div ng-repeat="item in lines track by item.id+item.description">
As the comment suggested you could try something like this:
<select ng-model="item" ng-options="item.id as item.description for item in items track by itemTracker(item)">
In your controller:
$scope.itemTracker= function(item) {
return item.id + '-' + item.description;
}
This might help with the number of DOM elements being re-rendered when the list changes.
Based my knowledge, the angularjs model is bind to the ui view, so the model will rerender via $apply or $digest once the value changed. so in your case, u wan bind the model value to ui view but also do not want to re-render the view if the value has not change,that is impossbile. this is what i know.
however, u can just manipulate the dom element. for example
store the data to a variable
var x = [{id:"id1",value:"v1"},{id:"id2",value:"v2"}]
in html, manual append or using directive to append, then assign the id to the element,
<div id="id1">v1</div>
check and compare the value, based ur needs.
once found, then angular.element("#yourid").text()
this will solve your browser resources consume problems.

Creating master detail in angular using the model

Given an object below -
function PersonCtrl(){
$scope.persons = [{name: "Mike", age:21,
occupation:{qualification: "engineer", company:"Intel"}}];
}
and the DOM below -
<ul>
<li ng-repeat="person in persons">
Name : {{person.name}}
<div ng-model="person.occupation">
Qualification : {{person.occupation.qualification}}
</div>
</li>
</ul>
I have a list of persons whose names have to be displayed in the list. Now I will initially load the data without any details, in this case qualification of the person.
When someone clicks on the person's name, I will retrieve the persons details. I would like to just change the model, ie add the qualification details to that person's model, and angular to then create the DOM.
One way to control this is use the ng-show, and set its expression, so that it only shows the qualification div, if and when the qualification object has values. However this will also lead to the details div being created for every person, and thus bad for performance.
Is there a way that the dom is created / destroyed by angular when an expression evaluates to true or false ?
If we want to physically remove / add parts of the DOM conditionally the family of ng-switch directives (ng-switch, ng-switch-when, ng-switch-default) will come handy.
If the detail data is small, and there's no huge cost to getting it, or rules about whether the current user is allowed to see it, then I'd say render it and just hide it initially. Keeping it collapsed just lets the user not think about that detail unless they want it for certain records.
If it's big, or expensive to retrieve/calculate, or there are rules prohibiting some users from seeing certain details, that's different. In that case, I'd render only the "button" to access it, and load the details via ajax when requested.

Resources