angularjs - Forms, Inputs, and Services
更新日期:
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-minlength
andng-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 formattype="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-dirtyrequired
, ng-valid-required or ng-invalid-requiredmin
, ng-valid-min or ng-invalid-minmax
, ng-valid-max or ng-invalid-maxminlength
, ng-valid-minlength or ng-invalid-minlengthmaxlength
, ng-valid-maxlength or ng-invalid-maxlengthpattern
, ng-valid-pattern or ng-invalid-patternurl
, ng-valid-url or ng-invalid-urlemail
, ng-valid-email or ng-invalid-emaildate
, ng-valid-date or ng-invalid-datenumber
, 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 callstatus
The status code for the responseconfig
The configuration with which the call was madedata
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 betext/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. UsedeferredObject.resolve
anddeferredObject.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 projectsProjectService.save({id: 15}, projectObj)
to update a project with ID 15ProjectService.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'); }]); |