文章目录
  1. 1. ng-model
  2. 2. Forms
    1. 2.1. Displaying Error Messages
    2. 2.2. Styling Forms and States
    3. 2.3. Nested Forms with ng-form
    4. 2.4. Other Form Controls
    5. 2.5. Radio Buttons
    6. 2.6. Combo Boxex/Drop-Downs
  3. 3. AngularJS Services
    1. 3.1. Creating a Simple AngularJS Service
    2. 3.2. The defference between Factory, Service, and Provider
  4. 4. Server Communication Using $http
    1. 4.1. $http API
    2. 4.2. $httpProvider
      1. 4.2.1. Interceptors
    3. 4.3. Promise
      1. 4.3.1. $q Service
    4. 4.4. ngResource
    5. 4.5. Best Practices

ng-model

AngularJS provides the ng-model directive for us to deal with inputs and two-way data-binding

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<body ng-controller="MainCtrl as ctrl">
  <input type="text" ng-model="ctrl.username"/>
  You typed 
  <button ng-click="ctrl.change()">Change Values</button>
</body>
<script type="text/javascript">
  angular.module('notesApp', [])
    .controller('MainCtrl', [function() {
    this.username = 'nothing';
    self.change = function() {
      self.username = 'changed';
      self.password = 'password';
    };
  }]);
</script>

Forms

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<form ng-submit="ctrl.submit()">
  <input type="text" ng-model="ctrl.user.username">
  <input type="password" ng-model="ctrl.user.password">
  <input type="submit" value="Submit">
</form>

<form ng-submit="ctrl.submit()" name="myForm">
  <input type="text"
         ng-model="ctrl.user.username"
         required
         ng-minlength="4">
  <input type="password"
         ng-model="ctrl.user.password"
         required>
  <input type="submit"
         value="Submit"
         ng-disabled="myForm.$invalid">
</form>

Form states in AngularJS

  • $invalid, AngularJS sets this state when any of the validations (required, ng-minlength, and others) mark any of the fields within the form as invalid.
  • $valid, The inverse of the previous state, which states that all the validations in the form are currently evaluating to correct.
  • $pristine,All forms in AngularJS start with this state. This allows you to figure out if a user has started typing in and modifying any of the form elements. Possible usage: disabling the reset button if a form is pristine.
  • $dirty, The inverse of $pristine, which states that the user made some changes (he can revert it, but the $dirty bit is set).
  • $error, This field on the form houses all the individual fields and the errors on each form element.

Built-in AngularJS validators

  • required,
  • ng-required, Unlike required, which marks a field as always required, the ng-required directive allows us to conditionally mark an input field as required based on a Boolean condition in the controller.
  • ng-minlengthand ng-maxlength
  • ng-pattern
  • type="email"
  • type="number",Can also have additional attributes for min and max values of the number itself.
  • type="date", This expects the date to be in yyyy-mm-dd format
  • type="url"

Displaying Error Messages

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
<form ng-submit="ctrl.submit()" name="myForm">
<input type="text"
       name="uname"
       ng-model="ctrl.user.username"
       required
       ng-minlength="4">
       
<span ng-show="myForm.uname.$error.required">
  This is a required field
</span>
  
<span ng-show="myForm.uname.$error.minlength">
      Minimum length required is 4
</span>
      
<span ng-show="myForm.uname.$invalid">
  This field is invalid
</span>
  
<input type="password"
       name="pwd"
       ng-model="ctrl.user.password"
       required>
       
<span ng-show="myForm.pwd.$error.required">
  This is a required field
</span>
  
<input type="submit"
       value="Submit"
       ng-disabled="myForm.$invalid">
</form>

Styling Forms and States

AngularJS adds and removes the CSS classes to and from the forms and input elements.

  • $invalid, ng-invalid
  • $valid, ng-valid
  • $pristine, ng-pristine
  • $dirty, ng-dirty
  • required, ng-valid-required or ng-invalid-required
  • min, ng-valid-min or ng-invalid-min
  • max, ng-valid-max or ng-invalid-max
  • minlength, ng-valid-minlength or ng-invalid-minlength
  • maxlength, ng-valid-maxlength or ng-invalid-maxlength
  • pattern, ng-valid-pattern or ng-invalid-pattern
  • url, ng-valid-url or ng-invalid-url
  • email, ng-valid-email or ng-invalid-email
  • date, ng-valid-date or ng-invalid-date
  • number, ng-valid-number or ng-invalid-number
1
2
3
4
5
6
7
8
9
.username.ng-valid {
  background-color: green;
}
.username.ng-dirty.ng-invalid-required {
  background-color: red;
}
.username.ng-dirty.ng-invalid-minlength {
  background-color: lightpink;
}

Nested Forms with ng-form

We sometimes run into cases where we need subsections of our form to be valid as a group, and to check and ascertain its validity.

AngularJS provides an ng-form directive, which acts similar to form but allows nesting, so that we can accomplish the requirement of grouping related form fields

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
<form novalidate name="myForm">
  <input type="text"
         class="username"
         name="uname"
         ng-model="ctrl.user.username"
         required=""
         placeholder="Username"
         ng-minlength="4" />
  <input type="password"
         class="password"
         name="pwd"
         ng-model="ctrl.user.password"
         placeholder="Password"
         required="" />
  <ng-form name="profile">
      <input type="text"
             name="firstName"
             ng-model="ctrl.user.profile.firstName"
             placeholder="First Name"
             required>
      <input type="text"
             name="middleName"
             placeholder="Middle Name"
             ng-model="ctrl.user.profile.middleName">
      <input type="text"
             name="lastName"
             placeholder="Last Name"
             ng-model="ctrl.user.profile.lastName"
             required>
      <input type="date"
             name="dob"
             placeholder="Date Of Birth"
             ng-model="ctrl.user.profile.dob">
  </ng-form>
  <span ng-show="myForm.profile.$invalid">
    Please fill out the profile information
  </span>
  <input type="submit"
         value="Submit"
         ng-disabled="myForm.$invalid"/>
</form>

Other Form Controls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- textarea -->
<textarea ng-model="ctrl.user.address" required></textarea>

<!-- checkbox -->
<input type="checkbox" ng-model="ctrl.user.agree">

<!-- What if we wanted to assign the string
     YES or NO to our model -->
<input type="checkbox"
       ng-model="ctrl.user.agree"
       ng-true-value="YES"
       ng-false-value="NO">
<!--  oneway binding -->
<input type="checkbox"
       ng-checked="sport.selected === 'YES'"></input>

Radio Buttons

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div ng-init="otherGender = 'other'">
  <input type="radio"
         name="gender"
         ng-model="user.gender"
         value="male">Male
  <input type="radio"
         name="gender"
         ng-model="user.gender"
         value="female">Female
  <input type="radio"
         name="gender"
         ng-model="user.gender"
         ng-value="otherGender">
</div>

We assign it as part of the initialization block (ng-init).

Combo Boxex/Drop-Downs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div ng-init="location = 'India'">
  <select ng-model="location">
    <option value="USA">USA</option>
    <option value="India">India</option>
    <option value="Other">None of the above</option>
  </select>
</div>
<select ng-model="ctrl.selectedCountryId"
        ng-options="c.id as c.label for c in ctrl.countries">
</select>
<select ng-model="ctrl.selectedCountry"
        ng-options="c.label for c in ctrl.countries">
</select>

<!-- this.selectedCountryId = 2; -->
<!-- this.selectedCountry = this.countries[1]; -->

AngularJS Services

AngularJS services are functions or objects that can hold behavior or state across our application. Each AngularJS service is instantiated only once, so each part of our application gets access to the same instance of the AngularJS service.

Controllers are stateful, but ephemeral. That is, they can be destroyed and re-created multiple times throughout the course of navigating across a Single Page Application.

When we say “services” in AngularJS, we include factories, services, and providers.

Any service known to AngularJS (internal or our own) can be simply injected into any other service, directive, or controller by stating it as a dependency.

1
2
myModule.controller("MainCtrl", ["$log", function($log) {}]);
myModule.controller("MainCtrl", function($log) {});
  • $window, the $window service in AngularJS is nothing but a wrapper around the global window object.
  • $location, the $location service in AngularJS allows us to interact with the URL in the browser bar, and get and manipulate its value.
    • absUrl, $location.absUrl())
    • url, A getter and setter that gets or sets the URL.
    • path,$location.path(), $location.path('/new')
    • search
      • $location.search(), returns the search parameter as an object
      • $location.search('test'), removes the search parameter from Url
      • $location.search('test', 'abc')
    • $http, it is the core AngularJS service used to make XHR requests to the server from the application.

Creating a Simple AngularJS Service

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
angular.module('notesApp', [])
.controller('MainCtrl', [function() {
  var self = this;
  self.tab = 'first';
  self.open = function(tab) {
    self.tab = tab;
  };
}])

.controller('SubCtrl', ['ItemService', function(ItemService) {
  var self = this;
  self.list = function() {
    return ItemService.list();
  };
  self.add = function() {
    ItemService.add({
      id: self.list().length + 1,
      label: 'Item ' + self.list().length
    });
  };
}])
  
.factory('ItemService', [function() {
  var items = [
    {id: 1, label: 'Item 0'},
    {id: 2, label: 'Item 1'}
  ];
  return {
    list: function() {
      return items;
    },
    add: function(item) {
      items.push(item);
    }
  };
}]);
  • The service will be lazily instantiated. The very first time a controller, service, or directive asks for the service, it will be created.
  • The service definition function will be called once, and the instance stored. Every caller of this service will get this same, singleton instance handed to them.

The defference between Factory, Service, and Provider

You should use module.factory() to define your services if:

  • You follow a functional style of programming
  • You prefer to return functions and objects

When we use a service, AngularJS assumes that the function definition passed in as part of the array of dependencies is actually a JavaScript type/class. So instead of just invoking the function and storing its return value, AngularJS will call new on the function to create an instance of the type/class.

1
2
// class ItemService
angular.module('notesApp', []) .service('ItemService', [ItemService])

When we need to set up some configuration for our service before our application loads, you can use provider function.

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
angular.module('notesApp', [])
.provider('ItemService', function() {
  var haveDefaultItems = true;
  this.disableDefaultItems = function() {
    haveDefaultItems = false;
  };
  // This function gets our dependencies, not the
  // provider above
  this.$get = [function() {
    var optItems = [];
    if (haveDefaultItems) {
      optItems = [
        {id: 1, label: 'Item 0'},
        {id: 2, label: 'Item 1'}
      ];
    }
    return new ItemService(optItems);
  }];
})

.config(['ItemServiceProvider',
  function(ItemServiceProvider) {
  // To see how the provider can change
  // configuration, change the value of
  // shouldHaveDefaults to true and try
  // running the example
  var shouldHaveDefaults = false;
  // Get configuration from server
  // Set shouldHaveDefaults somehow
  //Assume it magically changes for now
  if (!shouldHaveDefaults) {
    ItemServiceProvider.disableDefaultItems();
  }
}])

Note that the provider does not use the same notation as factory and service. It doesn’t take an array as the second argument because providers cannot have dependencies on other services.

The config function executes before the AngularJS app executes. So we can be assured that this executes before our controllers, services, and other functions.

Server Communication Using $http

The AngularJS XHR API follows what is commonly known as the Promise interface.

1
2
3
4
5
6
7
8
9
10
angular.module('notesApp', [])
.controller('MainCtrl', ['$http', function($http) {
  var self = this;
  self.items = [];
  $http.get('/api/note').then(function(response) {
    self.items = response.data;
  }, function(errResponse) {
    console.error('Error while fetching notes');
  });
}]);

Both these handlers get passed in a response object, which has the following keys:

  • headers The headers for the call
  • status The status code for the response
  • config The configuration with which the call was made
  • data The body of the response from the server

$http API

$http provides the following convenience methods to make certain types of requests: GET HEAD POST DELETE PUT JSONP

  • For requests without any post data (think GET), the function takes two arguments: the URL as the first argument, and a configuration object as the second.
  • For requests with post data (POST, PUT), the function takes three arguments: the URL as the first argument, the post data as the second, and a configuration object as the third and final argument.

$http.get(url, config) can be replaced with: $http(config) where the url and the method (GET, in this case) become part of the configuration object itself.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  method: string,
  url: string,
  params: object,
  data: string or object,
  headers: object,
  xsrfHeaderName: string,
  xsrfCookieName: string,
  transformRequest: function transform(data, headersGetter) or an array of functions,
  transformResponse: function transform(data, headersGetter) or an array of functions,
  cache: boolean or Cache object,
  timeout: number,
  withCredentials: boolean
}
  • params: [{key1: 'value1', key2: 'value2'}] would be converted to: ?key1=value1&key2=value2. If we use an object instead of a string or a number for the value, the object will be converted to a JSON string.
  • data: A string or an object that will be sent as the request message data. This basically becomes the POST data for the server.
  • headers: Like passing {'Content-Type': 'text/csv'} would set the Content-Type header to be text/csv.
  • xsrHeaderName: We can set the XSRF header that the server will be setting to prevent XSRF attacks on our website. TODO
  • xsrfCookieName: The name of the cookie that has the xsrf token to be used for the XSRF handshake.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
    // A simple transformRequest that takes the JSON
    // post data and converts it into jQuery like a post data
    // string is as follows:
    
    transformRequest: function(data, headers) {
      var requestStr;
      for (var key in data) {
        if (requestStr) {
          requestStr += '&' + key + '=' + data[key];
        } else {
          requestStr = key + '=' + data[key];
        }
      }
      return requestStr;
    }
    
  • cache: A Boolean or a cache object to use for an application-level caching mechanism. This would be over and above the browser-level caching. If set to true, AngularJS will automatically cache server responses and return them for subsequent requests to the same URL.

$httpProvider

We can use the config section of our module, and use the $httpProvider to configure these defaults.

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
angular.module('notesApp', [])
.controller('LoginCtrl', ['$http', function($http) {
  var self = this;
  self.user = {};
  self.message = 'Please login';
  self.login = function() {
    $http.post('/api/login', self.user).then(
      function(resp) {
        self.message = resp.data.msg;
      });
  };
}])
.config(['$httpProvider', function($httpProvider) {
  // Every POST data becomes jQuery style
  $httpProvider.defaults.transformRequest.push(
    function(data) {
      var requestStr;
      if (data) {
        data = JSON.parse(data);
        for (var key in data) {
          if (requestStr) {
            requestStr += '&' + key + '=' + data[key];
          } else {
            requestStr = key + '=' + data[key];
          }
        }
      }
      return requestStr;
  });
  // Set the content type to be FORM type for all post requests
  // This does not add it for GET requests.
  $httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
}]);

The $httpProvider.defaults.headers object allows us to set default headers for common, get, post, and put requests. Each one ($httpProvider.defaults.headers.post, for example) is an object, where the key is the header name and the value is the value of the header.

The following is the list of keys and values that can have defaults set using $httpPro vider (using $httpProvider.defaults):

  • headers.common
  • headers.get
  • headers.put
  • headers.post
  • transformRequest
  • transformResponse
  • xsrfHeaderName
  • xsrfCookieName

Interceptors

It usually required planning to create a layer through which all requests would be channeled so that we could add global hooks.

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
angular.module('notesApp', [])
.factory('MyLoggingInterceptor', ['$q', function($q) {
  return {
    request: function(config) {
    console.log('Request made with ', config);
    return config;
    // If an error, not allowed, or my custom condition,
    // return $q.reject('Not allowed');
    },
    requestError: function(rejection) {
      console.log('Request error due to ', rejection);
      // Continue to ensure that the next promise chain
      // sees an error
      return $q.reject(rejection);
      // Or handled successfully?
      // return someValue
    },
    response: function(response) {
      console.log('Response from server', response);
      // Return a promise
      return response || $q.when(response);
    },
    responseError: function(rejection) {
      console.log('Error in response ', rejection);
      // Continue to ensure that the next promise chain
      // sees an error
      // Can check auth status code here if need to
      // if (rejection.status === 403) {
      // Show a login dialog
      // return a value to tell controllers it has
      // been handled
      // }
      // Or return a rejection to continue the
      // promise failure chain
      return $q.reject(rejection);
    }
  };
}])
.config(['$httpProvider', function($httpProvider) {
  $httpProvider.interceptors.push('MyLoggingInterceptor');
}]);

The interceptors will be called in the order we add them to the provider, so we can also control the order in which they are called.

Promise

The Promise API was designed to solve this nesting problem and the problem of error handling.

  • Each asynchronous task will return a promise object.
  • Each promise object will have a then function that can take two arguments, a success handler and an error handler.
  • The success or the error handler in the then function will be called only once, after the asynchronous task finishes.
  • The then function will also return a promise, to allow chaining multiple calls.
  • Each handler (success or error) can return a value, which will be passed to the next function in the chain of promises.
  • If a handler returns a promise (makes another asynchronous request), then the next handler (success or error) will be called only after that request is finished.
1
2
3
4
5
6
7
8
9
$http.get('/api/server-config').then(function(configResponse) {
  return $http.get('/api/' + configResponse.data.USER_END_POINT);
}).then(function(userResponse) {
  return $http.get('/api/' + userResponse.data.id + '/items');
}).then(function(itemResponse) {
  // Display items here
}, function(error) {
  // Common error handling
});

If any error happens in any of the functions in the promise chain, AngularJS will find the next closest error handler and trigger it.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var self = this;
self.items = [];
self.newTodo = {};
var fetchTodos = function() {
  return $http.get('/api/note').then(
    function(response) {
      self.items = response.data;
    }, function(errResponse) {
      console.error('Error while fetching notes');
    });
};
fetchTodos();
self.add = function() {
  $http.post('/api/note', self.newTodo)
    .then(fetchTodos)
    .then(function(response) {
      self.newTodo = {};
    });
};

$q Service

If we want to trigger the success handler for the next promise in the chain, we can just return a value from the success or the error handler.

If, on the other hand, we want to trigger the error handler for the next promise in the chain, we can leverage the $q service in AngularJS. Just ask for $q as a dependency in our controller and service, and return $q.reject(data) from the handler.

1
2
3
4
xhrCall()
  .then(S1, E1) //P1
  .then(S2, E2) //P2
  .then(S3, E3) //P3
  • $q.defer() Creates a deferred object when we need to create a promise for our own asynchro‐ nous task. Use deferredObject.resolve and deferredObject.reject to trigger the Promise
  • $q.reject() The return value of this should be returned to ensure that the promise continues to the next error handler instead of the success handler in the promise chain.

ngResource

AngularJS’s optional module, ngResource. ngResource allows us to take an API endpoint and create an AngularJS service around it.

  • GET request to /api/project/ returned an array of projects
  • GET request to /api/project/17 returned the project with ID 17
  • POST request to /api/project/ with a project object as JSON created a new project
  • POST request to /api/project/19 with a project object as JSON updated the project with ID 19
  • DELETE request to /api/project/ deleted all the projects
  • DELETE request to /api/project/23 deleted the project with ID 23
1
2
3
4
angular.module('resourceApp', ['ngResource'])
.factory('ProjectService', ['$resource', function($resource) {
  return $resource('/api/project/:id');
}]);
  • ProjectService.query() to get a list of projects
  • ProjectService.save({id: 15}, projectObj) to update a project with ID 15
  • ProjectService.get({id: 19}) to get an individual project with ID 19

Best Practices

Wrap $http in services

1
2
3
4
5
6
7
8
angular.module('notesApp', [])
.factory('NoteService', ['$http', function($http) {
  return {
    query: function() {
      return $http.get('/api/notes');
    }
  };
}]);

Use interceptors

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
angular.module('notesApp', [])
.factory('AuthInterceptor', ['AuthInfoService', '$q', function(AuthInfoService, $q) {
  return {
    request: function(config) {
      if (AuthInfoService.hasAuthHeader()) {
        config.headers['Authorization'] =
        AuthInfoService.getAuthHeader();
      }
      return config;
    },
    responseError: function(responseRejection) {
      if (responseError.status === 403) {
        // Authorization issue, access forbidden
        AuthInfoService.redirectToLogin();
      }
      return $q.reject(responseRejection);
    }
  };
}])
.config(['$httpProvider', function($httpProvider) {
  $httpProvider.interceptors.push('AuthInterceptor');
}]);
文章目录
  1. 1. ng-model
  2. 2. Forms
    1. 2.1. Displaying Error Messages
    2. 2.2. Styling Forms and States
    3. 2.3. Nested Forms with ng-form
    4. 2.4. Other Form Controls
    5. 2.5. Radio Buttons
    6. 2.6. Combo Boxex/Drop-Downs
  3. 3. AngularJS Services
    1. 3.1. Creating a Simple AngularJS Service
    2. 3.2. The defference between Factory, Service, and Provider
  4. 4. Server Communication Using $http
    1. 4.1. $http API
    2. 4.2. $httpProvider
      1. 4.2.1. Interceptors
    3. 4.3. Promise
      1. 4.3.1. $q Service
    4. 4.4. ngResource
    5. 4.5. Best Practices