aboutsummaryrefslogtreecommitdiffstats
path: root/docs/src/reader.js
blob: a021c05ff5548d54e992d6a5a5b6a74203041fdd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/**
 * All reading related code here. This is so that we can separate the async code from sync code
 * for testability
 */

exports.collect = collect;

var ngdoc = require('./ngdoc.js'),
    Q = require('qq'),
    qfs = require('q-fs'),
    PATH = require('path');

var NEW_LINE = /\n\r?/;

function collect() {
  var allDocs = [];

  //collect docs in JS Files
  var path = 'src';
  var promiseA = Q.when(qfs.listTree(path), function(files) {
    var done;
    //read all files in parallel.
    files.forEach(function(file) {
      var work;
      if(/\.js$/.test(file)) {
        work = Q.when(qfs.read(file, 'b'), function(content) {
          processJsFile(content, file).forEach (function(doc) {
            allDocs.push(doc);
          });
        });
      }
      done = Q.when(done, function() {
        return work;
      });
    });
    return done;
  });

   //collect all ng Docs in Content Folder
   var path2 = 'docs/content';
   var promiseB = Q.when(qfs.listTree(path2), function(files){
     var done2;
     files.forEach(function(file) {
       var work2;
       if (file.match(/\.ngdoc$/)) {
         work2 = Q.when(qfs.read(file, 'b'), function(content){
            var section = '@section ' + file.split(PATH.sep)[2] + '\n';
            allDocs.push(new ngdoc.Doc(section + content.toString(),file, 1).parse());
          });
       }
       done2 = Q.when(done2, function() {
         return work2;
       });
     });
     return done2;
   });

  return Q.join(promiseA, promiseB, function() {
    return allDocs;
  });
}

function processJsFile(content, file) {
  var docs = [];
  var lines = content.toString().split(NEW_LINE);
  var text;
  var startingLine ;
  var match;
  var inDoc = false;

  lines.forEach(function(line, lineNumber){
    lineNumber++;
    // is the comment starting?
    if (!inDoc && (match = line.match(/^\s*\/\*\*\s*(.*)$/))) {
      line = match[1];
      inDoc = true;
      text = [];
      startingLine = lineNumber;
    }
    // are we done?
    if (inDoc && line.match(/\*\//)) {
      text = text.join('\n');
      text = text.replace(/^\n/, '');
      if (text.match(/@ngdoc/)){
        //console.log(file, startingLine)
        docs.push(new ngdoc.Doc('@section api\n' + text, file, startingLine).parse());
      }
      doc = null;
      inDoc = false;
    }
    // is the comment add text
    if (inDoc){
      text.push(line.replace(/^\s*\*\s?/, ''));
    }
  });
  return docs;
}
class="p">('phones/phones.json').success(function(data) { $scope.phones = data; }); $scope.orderProp = 'age'; } //PhoneListCtrl.$inject = ['$scope', '$http']; </pre> `$http` makes an HTTP GET request to our web server, asking for `phone/phones.json` (the url is relative to our `index.html` file). The server responds by providing the data in the json file. (The response might just as well have been dynamically generated by a backend server. To the browser and our app they both look the same. For the sake of simplicity we used a json file in this tutorial.) The `$http` service returns a {@link api/ng.$q promise object} with a `success` method. We call this method to handle the asynchronous response and assign the phone data to the scope controlled by this controller, as a model called `phones`. Notice that angular detected the json response and parsed it for us! To use a service in angular, you simply declare the names of the dependencies you need as arguments to the controller's constructor function, as follows: function PhoneListCtrl($scope, $http) {...} Angular's dependency injector provides services to your controller when the controller is being constructed. The dependency injector also takes care of creating any transitive dependencies the service may have (services often depend upon other services). Note that the names of arguments are significant, because the injector uses these to look up the dependencies. <img class="diagram" src="img/tutorial/xhr_service_final.png"> ### '$' Prefix Naming Convention You can create your own services, and in fact we will do exactly that in step 11. As a naming convention, angular's built-in services, Scope methods and a few other angular APIs have a '$' prefix in front of the name. Don't use a '$' prefix when naming your services and models, in order to avoid any possible naming collisions. ### A Note on Minification Since angular infers the controller's dependencies from the names of arguments to the controller's constructor function, if you were to {@link http://en.wikipedia.org/wiki/Minification_(programming) minify} the JavaScript code for `PhoneListCtrl` controller, all of its function arguments would be minified as well, and the dependency injector would not be able to identify services correctly. To overcome issues caused by minification, just assign an array with service identifier strings into the `$inject` property of the controller function, just like the last line in the snippet (commented out) suggests: PhoneListCtrl.$inject = ['$scope', '$http']; There is also one more way to specify this dependency list and avoid minification issues using the bracket notation which wraps the function to be injected into an array of strings (representing the dependency names) followed by the function to be injected: var PhoneListCtrl = ['$scope', '$http', function($scope, $http) { /* constructor body */ }]; Both of these methods work with any function that can be injected by Angular, so it's up to your project's style guide to decide which one you use. ## Test __`test/unit/controllersSpec.js`:__ Because we started using dependency injection and our controller has dependencies, constructing the controller in our tests is a bit more complicated. We could use the `new` operator and provide the constructor with some kind of fake `$http` implementation. However, the recommended (and easier) way is to create a controller in the test environment in the same way that angular does it in the production code behind the scenes, as follows: <pre> describe('PhoneCat controllers', function() { describe('PhoneListCtrl', function(){ var scope, ctrl, $httpBackend; beforeEach(inject(function(_$httpBackend_, $rootScope, $controller) { $httpBackend = _$httpBackend_; $httpBackend.expectGET('phones/phones.json'). respond([{name: 'Nexus S'}, {name: 'Motorola DROID'}]); scope = $rootScope.$new(); ctrl = $controller(PhoneListCtrl, {$scope: scope}); })); </pre> Note: Because we loaded Jasmine and `angular-mocks.js` in our test environment, we got two helper methods {@link api/angular.mock.module module} and {@link api/angular.mock.inject inject} that we'll use to access and configure the injector. We created the controller in the test environment, as follows: * We used the `inject` helper method to inject instances of {@link api/ng.$rootScope $rootScope}, {@link api/ng.$controller $controller} and {@link api/ng.$httpBackend $httpBackend} services into the Jasmine's `beforeEach` function. These instances come from an injector which is recreated from scratch for every single test. This guarantees that each test starts from a well known starting point and each test is isolated from the work done in other tests. * We created a new scope for our controller by calling `$rootScope.$new()` * We called the injected `$controller` function passing the `PhoneListCtrl` function and the created scope as parameters. Because our code now uses the `$http` service to fetch the phone list data in our controller, before we create the `PhoneListCtrl` child scope, we need to tell the testing harness to expect an incoming request from the controller. To do this we: * Request `$httpBackend` service to be injected into our `beforeEach` function. This is a mock version of the service that in a production environment facilitates all XHR and JSONP requests. The mock version of this service allows you to write tests without having to deal with native APIs and the global state associated with them both of which make testing a nightmare. * Use the `$httpBackend.expectGET` method to train the `$httpBackend` service to expect an incoming HTTP request and tell it what to respond with. Note that the responses are not returned until we call the `$httpBackend.flush` method. Now, we will make assertions to verify that the `phones` model doesn't exist on `scope` before the response is received: <pre> it('should create "phones" model with 2 phones fetched from xhr', function() { expect(scope.phones).toBeUndefined(); $httpBackend.flush(); expect(scope.phones).toEqual([{name: 'Nexus S'}, {name: 'Motorola DROID'}]); }); </pre> * We flush the request queue in the browser by calling `$httpBackend.flush()`. This causes the promise returned by the `$http` service to be resolved with the trained response. * We make the assertions, verifying that the phone model now exists on the scope. Finally, we verify that the default value of `orderProp` is set correctly: <pre> it('should set the default value of orderProp model', function() { expect(scope.orderProp).toBe('age'); }); }); }); </pre> You should now see the following output in the Testacular tab: Chrome 22.0: Executed 2 of 2 SUCCESS (0.028 secs / 0.007 secs) # Experiments * At the bottom of `index.html`, add a `{{phones | json}}` binding to see the list of phones displayed in json format. * In the `PhoneListCtrl` controller, pre-process the http response by limiting the number of phones to the first 5 in the list. Use the following code in the $http callback: $scope.phones = data.splice(0, 5); # Summary Now that you have learned how easy it is to use angular services (thanks to Angular's dependency injection), go to {@link step_06 step 6}, where you will add some thumbnail images of phones and some links. <ul doc-tutorial-nav="5"></ul>