AngularJS Tutorial

Recently I gave a quick demo/intro on AngularJS to people who have not done AngularJS before, so if you are interested and would love to learn AngularJS, read on :)

The demo project is here, and it's based on yeoman's generator-gulp-angular scaffold app.

TL;DR

  • This post is meant for an AngularJS beginner who only just started working on AngularJS but is looking for different ways to abstract the existing codebase.
  • There are five routes in the demo app, which show the abstraction progress: a scaffold app to a production ready app
  • Each route is almost the same as the other, but they are meant to demonstrate the flexibility of writing an app, as well as to draw the comparison between different approaches.

Setup

  > git clone https://github.com/Neil-Ni/angularDemo.git
  > cd angularDemo
  angularDemo > npm install
  angularDemo > bower install
  angularDemo > gulp serve
  ..
  ..
  [BS] [BrowserSync SPA] Running...
  [BS] Access URLs:
  --------------------------------------
    Local: http://localhost:3000/
    External: http://192.168.0.19:3000/
  --------------------------------------
    UI: http://localhost:3001
    UI External: http://192.168.0.19:3001
  --------------------------------------
  [BS] Serving files from: .tmp/serve
  [BS] Serving files from: src
  ..

There are five routes, and each route maps to one directory. For example, you can find the third route's controller and html under angularDemo/src/app/main3/. And the route number here represents the level of abstractions going on in the app. If you go from http://localhost:3000/#/ to http://localhost:3000/#/2 you will notice they are exactly the same, but the second route is implemented slightly differently to introduce different concepts.

Often, many tutorials go through only one way to write an application, and a reader must go through multiple posts to learn different ways to write code. This demo is setup in a way that allows us to see different approaches to accomplish the same goal, and also understand why the approach can be different or better from another. Below I will refer to each of the approaches by their controller names (Ex: mainController, main2controller, main3controller, etc) and highlight the purposes of each approach with bullet points. The best way to go through this tutorial is to have the application open in your editor on the side while going through the points mentioned in the post.

Again, feel free to comment on the post and let me know if there's something I haven't gone through enough. Thanks!

mainController
  • module

    1. From the very first line of main/main.controller.js, we have angular.module('angularDemo'), which appears in all Angular files in the app. What it does is reference the module name of your application, and in this case the application is called angularDemo.
    2. By convention, the first declaration of your module appears in src/app/index.js, which is normally where we put our app's configuration, which also includes the configuration for other modules we inject into this app.
    3. For example, in src/app/index.js, we configure $stateProvider which is an Angular provider available after we injected ui.router module. And inside .config, we use $stateProvider to specify state, which maps to route, template and controller.
  • controller/scope

    1. Inside main/main.controller.js, controller is defined right after angular.module('angularDemo') with .controller('MainCtrl') syntax.
    2. MainCtrl is defined with the function function ($scope) { ... }. After this definition, the app can reference MainCtrl from other places (like how it is used in src/app/index.js)
    3. Inside MainCtrl a $scope variable is being passed into the function. It's really similar to this or self in other object oriented languages, but more conveniently, it allows data binding between controller and html template.
  • templating
    1. Inside main/main.html, the {{}} syntax allows Angular to bind with the variables defined in controller.
    2. From src/app/index.js we associate MainCtrl with main/main.html, which means right now the variables inside MainCtrl are bound to main/main.html. Ex. awesomeThings in html is bound to $scope.awesomeThings in controller now.
    3. ng-repeat="awesomeThing in awesomeThings | orderBy:'rank'" is one way to template html in Angular. What it does is loop through the array in the scope of this html template, and order by a particular rank property
main2Controller
  • ng-include, ng-controller and scope

    1. html can be abstracted out to another file, and can be referenced using ng-include
    2. Ex. We can shorten the previous ng-repeat block by replacing it with <div ng-include="'app/main2/list/list.html'" ng-controller="Main2listCtrl"></div>, and moved the actual ng-repeat code into list.html. The other benefit for doing this is that we can define another controller to bind with this html template.
  • scope variables

    1. Notice that $scope.awesomeThings is not actually defined under Main2listCtrl but list.html is still doing ng-repeat over $scope.awesomeThings. The reason is that Main2listCtrl's view is included under Main2Ctrl's view, which means Main2listCtrl naturally inherits scope variables from Main2Ctrl
    2. Although the $scope.awesomeThings can be accessed from list.html, it is more prone to errors because we've introduced a dependency of Main2listCtrl to Main2Ctrl. And this means any future refactoring (such as renaming awesomeThings) in Main2Ctrl can cause the ng-include template to break easily.
main3Controller
  • directives
    1. Directives are another way to abstract scope
    2. Directives are a better way to rewrite our changes in main2Controller. A directive allows us to do something similar to ng-include and ng-controller. Additionally, it explicitly passes your variables from another scope.
    3. Ex. <main3-list-directive awesome-things="awesomeThings"></main3-list-directive>
    4. The great thing about writing it this way is that it is treating the template more like an isolated scope with its defined controller and variables. For example, one benefit is to be really explicit about what data this directive requires. If we refactored $scope.awesomeThings in Main3Ctrl to $scope.newList, this is all we need to do <main3-list-directive awesome-things="newList"></main3-list-directive>. The old ng-include way would require us to manually replace the variable mentioned in list.html.
    5. Directive is like a function that takes certain variables and magically produces the correct html based on the variables we provide. And just like other languages, a function like this should be unit testable and therefore reusable.
main4Controller
  • components: hierarchical and non-hierarchical

    1. A hierarchical component would be the list directive in main3Controller. This list is context specific and only makes sense under certain views and controllers. For example, it only produces the list correctly when provided with the data with awesomeThings format. Therefore, it makes sense to create a list directory directly under main, because it will help the next developer find the code easily and also suggests that this is not really a reusable component.
    2. In src/app/components, we also have directives that are not restricted by the view logic and can be reused at multiple places in the app. For example, a navigation bar is needed for all views, so it makes sense to abstract it into a component and reference it from all the views instead of rewriting it everytime.
    3. Another non-hierarchical component is rn-dropdown-sort, which is a directive that helps sorting a column. For example, <rn-dropdown-sort title="Sort By Title" property="title"></rn-dropdown-sort> is a button that has a label Sort By Title on it and when clicked, it should sort the data by the title property.
  • events - $emit, $on

    1. One good way to communicate between the actions and data change in the app is through events. For example, clicking on rn-dropdown-sort, we want to sort the title property ascendingly/descendingly in the list, but the component itself is agnostic about the data and the controller should be the one doing the data manipulation.
    2. To deal with this situation, we would do $scope.$emit from rn-dropdown-sort on click, and pass a sorting function. A parent controller would listen with $scope.$on and use the sorting function to sort the $scope.awesomeThings in the controller accordingly.
    3. Any controller that includes this component can listen to the emit event from rn-dropdown-sort, which means we could do $scope.$on either inside main4ListDirective's controller or Main4Ctrl. This can be cofusing sometimes, but we really only need to sort the array within the scope where the data is showing, which means we should really listen to the event inside main4ListDirective's controller.
  • service

    1. Services are another way to abstract controller
    2. Inside src/app/components/rn-dropdown-sort/rn-dropdown-sort.js, there is a service defined with .factory('RnDropdownSortOptions'). All the logic inside this service was once written inside rnDropdownSort's controller, but right now it's been abstracted out to another service and referenced by injecting back to the controller.
    3. Services help to simplify the controller logic and unit testings as well. Right now each service's responsibility is simple and defined, which means they can be tested easily. For example, all RnDropdownSortOptions is doing is providing the ascending/descending sorting functions, and we can already start writing a spec to see whether or not these sorting functions are sorting the data as we expected.
main5Controller
  • promises
    1. Promises are a neat solution to handle callbacks.
    2. Ex. In Main5Ctrl, we have injected a service called AwesomeThings. AwesomeThings service is acting like a model that fetches data from backend api, so if we call getAll(), we normally would wait for the backend to respond with data. And promises provide a simple syntax .then to cope with this and return the result right after we received them. We'd use it like this: AwesomeThings.getAll().then(function (result) { $scope.awesomeThings = result; ...
    3. Also take a look at AwesomeThings service to see how to manually returns a promise from a function.
    4. Promises also come with catch and finally. The syntax looks like this:
AwesomeThings  
  .getAll()
  .then(function (result) {
    $scope.awesomeThings = result;
  }).catch(function (error) {
      //Do something with error
  }).finally(function() {
    //Do something finally
  }) 
  • $watch
    1. $watch listens to variable change. For example, $scope.$watch('awesomeThings', function () {...}) would watch $scope.awesomeThings changes from null to an actual array, or from an actual array to a larger array.
    2. It gets really convenient when the scope.variable is a promise result. For example, in main5ListDirective, we pass awesomeThings from Main5Ctrl, but awesomeThings were waiting on the promise result when it first got passed into the directive, which means awesomeThings is initially undefined in main5ListDirective. What we need to do is add $scope.$watch('awesomeThings', ...) to wait till awesomeThings changes its value, and then update the data inside main5ListDirective to be a defined array.
comments powered by Disqus