<p><a href="https://angularjs.org/">Angular.js</a> is a great JavaScript framework, it makes easy to develop Single Page Applications (SPAs) and removes most of the pain regarding data binding, requests and routing.</p><p><br>Unlike many MVW (Model View Whatever) frameworks and because the way JavaScript works, it does not force us to organize projects in a specific way nor it defines where each piece of code goes in your projects (where do we initialize model variables: controllers? services?).</p><p><br>I've read many blog posts talking about interesting design patterns and project architectures that define how to organize code and make a clean Angular project. So, just for fun, I decided to write a little app trying to follow the patterns which I liked the most. By the end, I had a simple app with some interesting concepts, especially regarding separation of concerns, so I decided to share them hoping to get some feedback. The code is available in my GitHub:</p><p><a href="https://github.com/filipeximenes/django-angular-jogging">https://github.com/filipeximenes/django-angular-jogging</a></p><p>It's a Django app that allows users to enter and edit times for their jogging activities. The API is powered by <a href="http://django-rest-framework.org">Django REST Framework</a>, but for this post I want to focus on the Angular part, so please just ignore the rest of the project.</p><h2 id="the-architecture">The Architecture</h2><p>For the architecture I decided to go with an "app driven" pattern, where each app is [almost] self contained and has its own modules (Controllers, Services, etc). This is how most people recommend organizing the code when working in real world projects. Django developers will find this pattern pretty familiar.</p><figure class="kg-card kg-code-card"><pre><code>/main.module.js
/core /core.module.js /core.ctrl.js /core.service.js
/accounts /accounts.module.js /accounts.ctrl.js /accounts.service.js
/timings /timings.module.js /timings.ctrl.js /timings.service.js
</code></pre><figcaption><a href="https://github.com/filipeximenes/django-angular-jogging/tree/master/core/static/app">Click here to see it in the project</a></figcaption></figure><h2 id="the-main-module">The main module</h2><p>The way it's organized, the main module (<code>main.module.js</code>) is responsible to defining dependencies, setting global configurations and routing. <a href="https://github.com/filipeximenes/django-angular-jogging/blob/master/core/static/app/main.module.js">Take a look here</a>.</p><h2 id="app-modules">App Modules</h2><p>Each app has a module file, responsible to naming it and declaring the internal dependencies.</p><pre><code class="language-javascript">(function (){ var app = angular.module('Jogging.timings', ['Jogging.timings.ctrl', 'Jogging.timings.service']);
})();
</code></pre><p>This also makes it easy to import apps in the main module:</p><pre><code class="language-javascript">var app = angular.module('Jogging', [ ... 'Jogging.core', 'Jogging.accounts', 'Jogging.timings']);
</code></pre><h2 id="restangular">Restangular</h2><p>Before going to the main part of this article, just a brief note:<br>Angular <code>$http</code> service is nice. I mean, its simple enough and does most of what is expected from it. On the other side, Restangular is just great! It allows us to set global configurations like the base url, and has very clean interface to play with web APIs. I really recommend <a href="https://github.com/mgonto/restangular">taking a look at the project</a>.</p><h2 id="controllers-and-services">Controllers and Services</h2><p>Now to the interesting parts! The main rule I tried to follow during the development of this app was:</p><p><code>Keep STATE and LOGIC inside services and make extremely thin controllers</code></p><p>This way, controllers are simply the interface between template and the services.</p><p>Lets take a look on how timings are managed:</p><p><a href="https://github.com/filipeximenes/django-angular-jogging/blob/master/core/static/app/timings/timings.service.js#L4">TimingsFactory</a>:</p><pre><code class="language-javascript">app.factory('TimingsFactory', ['Restangular', 'ConversionFactory', function (Restangular, ConversionFactory){ var timingsResource = Restangular.all('timings'); obj = {}; obj.timings = []; obj.getTimingList = function (filters){ return timingsResource.getList(filters).then(function(response) { obj.timings = response.data; }); }; obj.createTiming = function (data){ data.time = ConversionFactory.fromFormattedToSeconds(createData.formattedTime); return timingsResource.post(data).then(function (response){ obj.timings.push(response.data); }); }; obj.updateTiming = function (index, data){ var timingResource = Restangular.one('timings', obj.timings[index].id); angular.extend(timingResource, data); return timingResource.put().then(function (response){ angular.extend(obj.timings[index], response.data); }); }; obj.deleteTiming = function (index){ var timingResource = Restangular.one('timings', obj.timings[index].id); timingResource.remove().then(function (response){ obj.timings.splice(index, 1); }); }; return obj; } ]);
</code></pre><p>As we can see, the <code>TimingsFactory</code> provides <code>obj.timings</code> variable responsible for the state of the timings list and all functions that perform actions on it (in this case, a simple CRUD).</p><p>The controller will only add the TimingsFactory to the <code>$scope</code>:</p><p><a href="https://github.com/filipeximenes/django-angular-jogging/blob/master/core/static/app/timings/timings.ctrl.js">TimingsCtrl</a>:</p><pre><code class="language-javascript">app.controller('TimingsCtrl', ['$scope', 'TimingsFactory', 'ConversionFactory', 'ReportsFactory', function ($scope, TimingsFactory, ConversionFactory, ReportsFactory){ $scope.timingsFactory = TimingsFactory; ... } ]);
</code></pre><p>And service functions are called directly from the template. For example:</p><p>The <a href="https://github.com/filipeximenes/django-angular-jogging/blob/master/core/templates/base.html#L147">form to create a timing</a>:</p><pre><code class="language-html"><form class="form-inline"> <div class="form-group"> <label for="time">Time: </label> <input data-ng-model="createData.formattedTime" name="time" type="text" class="form-control"> </div> <div class="form-group"> <label for="distance">Distance: </label> <input data-ng-model="createData.distance" name="distance" type="number" class="form-control"> </div> <div class="form-group"> <label for="date">Date: </label> <input data-ng-model="createData.date" name="date" type="date" class="form-control"> </div> <button data-ng-click="timingsFactory.createTiming(createData); createData = {};" class="btn btn-default">add</button>
</form>
</code></pre><p><a href="https://github.com/filipeximenes/django-angular-jogging/blob/master/core/templates/base.html#L178">Filtering timings</a>:</p><pre><code class="language-html"><form class="form-inline"> <div class="form-group"> <label for="startDate">From: </label> <input data-ng-model="filters.start_date" type="date" name="startDate" class="form-control"> </div> <div class="form-group"> <label for="endDate">To: </label> <input data-ng-model="filters.end_date" type="date" name="endDate" class="form-control"> </div> <button data-ng-click="timingsFactory.getTimingList(filters);" class="btn btn-default">filter</button>
</form>
</code></pre><p>If you want to see some more: <a href="https://github.com/filipeximenes/django-angular-jogging/blob/master/core/templates/base.html#L201">the listing and inline performing updates in timings.</a></p><h2 id="separating-concerns">Separating concerns</h2><p>The timings area was easy, all interactions only affected the state of the timings list which were already in the service. So, how to deal with service operations that also affect other <code>$scope</code> data. Lets look at the login process.</p><p>If user correctly enters login info, we expect it to be redirected to the <code>/timings</code> controller, otherwise, provide a visual feedback that there was a problem.</p><p>To do this, the <a href="https://github.com/filipeximenes/django-angular-jogging/blob/master/core/static/app/accounts/accounts.service.js">AccountFactory</a> will receive an object containing envents to be executed after the resquest was made.</p><p>The <a href="https://github.com/filipeximenes/django-angular-jogging/blob/master/core/static/app/accounts/accounts.service.js#L24">performLogin</a> function:</p><pre><code class="language-javascript">this.performLogin = function (data){ Restangular.all('login').post(data) .then(function (response){ _this.setCredentials(response.data.token); if (events.onPerformLoginSuccess){ return events.onPerformLoginSuccess(response); } }, function (response){ if (events.onPerformLoginFailure){ return events.onPerformLoginFailure(response); } });
}; </code></pre><p>and this is how <code>AccountFactory</code> is instantiated:</p><p><a href="https://github.com/filipeximenes/django-angular-jogging/blob/master/core/static/app/accounts/accounts.ctrl.js#L22">LoginController</a></p><pre><code class="language-javascript">app.controller('LoginController', ['$scope', '$location', 'AccountFactory', function ($scope, $location, AccountFactory){ $scope.accountFactory = new AccountFactory({ onPerformLoginSuccess: function (response){ $location.path('/timings'); }, onPerformLoginFailure: function (response){ $scope.loginFailure = true; } }); }
]);
</code></pre><h2 id="that-s-all-for-now">That's all for now</h2><p>So here is what I have to show for now, hope you all liked. Please leave a comment with your opinions so this can be improved!</p>
Join the Tech Forward newsletter
Stay ahead of the curve with our latest trends about web development.
By clicking “Accept all”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage and assist in our marketing efforts. Check our privacy policies.