AngularJS: how I abandoned ng-include and bound the states of two controllers

  • Tutorial
In the last article I talked about his first encounter with AngularJS. A year has passed since then, now I have new projects and other, often less trivial tasks. Here I will describe a few nuances that I had to face in the process of working on one of the systems. I hope readers can benefit from my practices.

Search and Anchor


Suppose that we were given the task of developing a client part for our new project. This is the directory where hundreds of thousands of documents will be stored. Since it is quite large, the API provides the ability to load elements per page (with an initial index) and also filter by individual fields in the document.
And so that users do not get lost in the system and can share information with each other, the client must save its state in the address bar.

Well, the task is clear. Getting down.


To store filter values, create the $ query service . He will have two methods:

  • push (query) - receives an object with a set of filters and adds them to the address bar (search field).
  • parse () - Converts search back to a set of filters.

A digression should be made here. Since several templates are used on the page (for example, for pagination), a pound sign (#) is automatically added to the address bar. This is due to the fact that ng-include uses the $ location service , in the presence of which angular begins to assume that we are making a one-page application.

Accordingly, an object of the form

{
  index: 0,
  size: 20
}

will turn into
localhost:1337/catalog#?index=0&size=20

But wait a moment. Users want to not only get the state of the page, but also mark a separate document on it.
The official documentation in this case advises using $ anchorScroll or scrollTo .

Those. now we get the following:
localhost:1337/catalog#?index=0&size=20&scrollTo=5

At that moment, my aesthetic feeling called for a different solution.

The first thought was to abandon ng-include so that the address bar would no longer be subjected to violence by the angular. But then what to do with templates? There was only one way out: write your own directive for working with templates.

With blackjack and patterns


There were no problems with the directive. Angular uses the $ templateCache service to work with templates . You can put a piece of html code into it using text / ng-template or the put () method . Also, by analogy with ng-include , we envisage code execution from the onload atrubit .

Directive Code:

app.directive('template', ['$templateCache', '$compile', function ($templateCache, $compile) {
    return {
        scope: false,
        link: function (scope, element, attrs) {
            var tpl = $compile($templateCache.get(attrs.orgnTemplate))(scope);
            tpl.scope().$eval(attrs.onload || '');
            element.after(tpl);
            element.remove();
        }
    }
}]);


Now we can use the templates as follows:


Having solved the problem with $ location , I rewrote the $ query service a bit so that now it works exclusively with the history API.
By the way, do not try to use them together. This will lead to an endless loop.

So now the address bar is more understandable and pleasant to look at:
localhost:1337/catalog?index=0&size=20#5

And moving around anchors no longer requires additional code.

Ease of communication


Having broken the page into separate templates and controllers, I unexpectedly ran into another problem: the controllers must interact with each other. Even if they are not in a parental relationship. And in some cases (again, pagination), controllers must synchronize their state.

The first option was the interaction between controllers through events. In this case, the controllers send events to each other for each action. Unfortunately, in my case, the number and variety of events per square centimeter of code began to go beyond all reasonable limits. I decided to abandon the optimization and make a separate mechanism for the exchange of information, regardless of the current scope .

So the service $ store appeared . In the first version, he had one method:

  • value (key, value) - saves or retrieves the value by key.

The following code has been added to the controllers:

$scope.$watch(function () {
  return $store.value('foo');
}, function (data) {
  doSomething(data);
}, true);

Now, when I needed to synchronize the state of two or more controllers, I only rewrote the value in the key:

$store.value('stream', data);

Do not forget that all services are singleton, so when you add the service to several controllers simultaneously, we get access to the same object.

Later, when I slightly automated the transfer of data between two templates (for example, the list of elements was now automatically attached to pagination using my $ id ), the alias () method was added to the service :

  • alias (key, values ​​...) - adds or returns a list of synonyms for the specified key.

Thus, I got the opportunity to specify alias in the onload attribute of the template directive. Roughly speaking, if the controller suddenly needs to request a state, this can be done not by the original key, which may not be available, but by a predetermined value.

Instead of an afterword


It turned out that a seemingly trivial task turned into full refactoring. However, at the end of it, at least in my opinion, the code became much easier to read and more predictable in work. I no longer get lost in endless events, eat only healthy foods and go in for sports. I hope this article will help others find peace of mind and learn something new. Good luck and have a nice day!

Also popular now: