Angular - Refactoring


In this video we are going to refactor the generated Angular Seed layout to one which is a little more in-line with the way I tend to shape Angular projects. I am not saying this is the defacto way to structure an Angular project by any means, but this layout tends to suit me.

If you want to follow Angular best practice / recommended styles, then be sure to follow the official Angular style guide.

It's worth pointing out at this stage that you can see the final layout of this project on the GitHub repository for this section of the course.

The main changes here will be to rename the template and controller to better match their purpose, and extract the route configuration to a separate file. Then it is important to make Angular aware of all these changes.

As we flesh out the various sections of CRUD - specifially Create, and Update - we will add new sub-directories for the controller and templates, but all routes will be stored in one single file.

In larger projects it would be more common to use some build process - often Gulp - to handle a large chunk of this for you. For example, as a project grows to multiple 10s of controllers, views, services, etc, it is not ideal to have to manually add or update lines in your index.html for each of these files.

Instead, it is more common for a build tool such as Gulp to collect, concatenate, lint, annotate, babelify, minify, and any other of potentially tens of processes to remove this burden from you.

Again, however, considering the small scale of this project, this would be overkill. It would also potentially obscure the single thing we are trying to cover and understand - connectivity between Angular JS and Symfony.

Project Re-organisation

I mentioned in the previous video that I immediately removed the 'components' directory from the Angular Seed installation.

My first step is to create a new directory - /app/blogPost - which will contain all the code related to Creating, Updating, Listing, and Deleting our blog posts.

I now want to rename my module from myApp.view1 to myApp.blogPost, and remove references to myApp.view2:

// /app/app.js

// * snip *

angular.module('myApp', [
  'ngRoute',
  'myApp.blogPost'
]).

// * snip *

This means I now need to update any other references to myApp.view1 that remain in my project.

But there's a slight twist here that may catch you out.

In the generated code from Angular Seed, the following line exists in view1/view1.js:

// /app/view1/view1.js

angular.module('myApp.view1', ['ngRoute'])

At this stage I am going to rename and move /app/view1/view1.js to blogPost/list/listController.js.

What we want to do is extract the module creation to our /app/blogPost/index.js file, and only retrieve the existing module in our controller. The syntax for this is deceptive:

// /app/blogPost/list/listController.js

angular.module('myApp.blogPost')

And extract everything except the controller function to a new file:

// /app/blogPost/index.js

'use strict';

angular.module('myApp.blogPost', ['ngRoute'])

    .config(['$routeProvider', function($routeProvider) {

        $routeProvider
            .when('/list', {
                templateUrl: 'blogPost/list/list.html',
                controller: 'listController'
            })
        ;
    }]);

Note that I haven't just extracted the content here, I have also updated the route name to /list, and updated the templateUrl and controller parameters also.

This will help us keep similar things together - routes for example, all in one file - and our controller code in seperate files.

The most important point to note here is that this new blogPost/index.js file contains the module creation method - identified by the second parameter - an array of other modules that this module depends on.

If the module definition doesn't have the array then Angular tries to find an existing module with the given name. My most common mistake when starting with Angular was to effectively declare a new module every time I used it - in other words, always including the array as the second parameter. Angular didn't appreciate the errors of my ways.

If still unsure on this then I'd advise reading this page of the Angular Docs, specifically the section on 'Creation versus Retrieval'.

With the module creation process, and routing configuration extracted to the /app/blogPost/index.js file, we can now look at the other changes.

Different Directories For Different Functions

You may disagree with this layout, and that's fine. This works for me, on this scale of project. Angular directory structure is not set in stone, so again consider the best practices, but also go with what works for you.

At this stage I have:

  • renamed the view1 directory to blogPost
  • renamed the view1/view1.js file to blogPost/list/listController.js

Now I want to rename the view1/view1.html file to blogPost/list/list.html.

The /app/view1 directory can now be deleted.

However, as files have been moved and renamed, we need to keep Angular in sync with all our file path changes. Again, this is something that a better build process could take care of for you. There are plenty of more in-depth tutorials on how to do this out there in Internet-land, a quick Google for "angular gulp build process" or similar will yield plenty of possibilities.

We now need to update the index.html file to make sure the script paths are pointing at the new files:

<!-- /app/index.html -->

  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="app.js"></script>
  <script src="blogPost/index.js"></script>
  <script src="blogPost/list/listController.js"></script>

At this stage we have the rough shell / outline of how the final project will look. We will need to create another couple of subdirectories under /app/blogPost to contain our create and update code, but there should be no further major changes to the project structure.

Extracting API Connectivity

The last change to make is a relatively easy one at this stage. If we had left this till a little later then we'd end up changing more files. This is to extract the API call(s) to a separate file.

I'm going to create a file called /app/blogPost/Api.js which is going to contain functions that wrap our calls to the API. This sounds a little pointless, but stick with me.

The first step is to extract the $http.get(...) code from the /app/blogPost/list/listController.js file, and move it into a getAll function inside our new Api.js file:

// /app/blogPost/Api.js

'use strict';

angular.module('myApp.blogPost')

    .factory('Api', [
        '$http',
        function ($http) {

            var ROOT_URL = 'http://api.symfony-3.dev/app_dev.php/posts';

            function getAll() {
                return $http.get(ROOT_URL);
            }

            return {
                getAll: getAll
            }

        }
    ]);

We will come back to this file throughout the Angular videos to add more functions - get, post, put, etc.

Notice that we don't actually do very much here, just wrap the API call implementation and return a promise that will resolve to contain the outcome - whether that be a good response, or an error. Whatever calls the methods can then determine what action to take.

The factory function is available via Angular's module. It expects us to return an object literal:

            return {
                getAll: getAll
            }

When I first started using JavaScript beyond JQuery I found syntax like this to be confusing.

All this means is we will return an object - return {}

On that object we will have a key called getAll.

getAll will contain a function which has not yet been executed. Notice, getAll: getAll - which is not the same as getAll: getAll(). Without the brackets we are passing the function itself, not the output of that function. It's subtle, but it's also very important, and a mindset shift from PHP.

One other thing worth noting - and something you will likely start to see more of in the future - is that in ES6, we could shorten this syntax to:

            return {
                getAll
            }

In ES6, when the key and value are the same, we don't need to write out both the key and value. Cool, but potentially confusing. We aren't using ES6 in the project, so I can't do this here.

Be sure to update index.html with the new Api.js file (this gets tedious, hence gulp / build processes):

<!-- /app/index.html -->

  <script src="bower_components/angular/angular.js"></script>
  <script src="bower_components/angular-route/angular-route.js"></script>
  <script src="app.js"></script>
  <script src="blogPost/index.js"></script>
  <script src="blogPost/Api.js"></script>
  <script src="blogPost/list/listController.js"></script>

We now need to update the listController to use Api instead of $http:

// /app/blogPost/list/listController.js

'use strict';

angular.module('myApp.blogPost')

.controller('listController', ['$scope', 'Api', function($scope, Api) {

  $scope.blogPosts = [];

  Api.getAll()
    .then(function (result) {
      console.log('result', result);
      $scope.blogPosts = result.data;
    }, function (error) {
      console.log('error', error);
    });

}]);

And that's us pretty much refactored and setup for the rest of the videos in the Angular section of this series.

Code For This Video

Get the code for this video.

Episodes