Pro AngularJS
This article will teach you about the individual components of AngularJS (1.3) and how they come together to make an application, i.e. you will known what is an AngularJS application and how to build it. This article is biased towards the community conventions and therefore ignores uncommon/ill/edge use cases (e.g. using HTML comments to reference a directive from the template). This article assumes that you have a good understanding of JavaScript.
All of the examples include references to the relevant documentation. If you are struggling to understand the included examples, refer to the documentation. All of the examples are in JSFiddle.
If this is your first encounter with AngularJS, please first read the official Developer Guide. It explains what type of applications should be developed using AngularJS and what good using AngularJS achieves.
A typical single-page application (SPA) starts with a stateless request to a server. The response is a document that executes the application. The application will load the necessary components (e.g. fragments of HTML and data to represent the current state of the application) in response to user actions. Typically, using AJAJ. The goal is to provide a more fluid user experience akin to a desktop application. The page does not reload at any point in the process.
In contrast, round-trip app logic and data resides on the server side; the browser acts as a rendering engine. The browser makes a series of stateless requests that the server side responds to with dynamically generated HTML. The drawbacks are: a lot of bandwidth waste, the user need to wait until the next HTML document is requested and loaded.
AngularJS is used to develop single-page applications.
Single-page application requires a web-service (backend) to perform create, read, update and delete (CRUD) operations. In modern web development, such service conforms to representational state transfer (REST) architecture.
Two key players are the URL and the request method. The URL identifies the resource, and the HTTP method specifies what operation (CRUD) to perform.
| Method | Intent |
|---|---|
GET | Retrieve object specified by the URL. GET method is nullipotent (the same operation performed 0 or more times does not change the result). |
PUT | Update the object specified by the URL. PUT operation is idempotent (the same operation performed 1 or more times has the same effect on the data). |
POST | Create a new object (can perform double duty: update an object if it exists or create a new one if not) (neither nullipotent or idempotent). |
DELETE | Delete the object specified by the URL (idempotent). |
AngularJS advocates Model-View-Whatever architectural pattern. Angular Controllers setup and add behavior to $scope objects, and Angular's templates and two-binding make the View layer, but Angular has virtually no opinion when it comes to your Model layer. You can do whatever you want.
Regardless of what we are calling it, it is important to separate the presentation logic from business logic and presentation state. To this extent, we are going to be calling it MVC.
| Player | Role |
|---|---|
| Model | Logic that deals with storing and retrieving the data. |
| View | Logic that deals with formatting data to display to the user. |
| Controller | Logic that responds to the user interactions, updates data in the model and provides data to the view. |
Model should:
- Contain the logic for CRUD the domain data (even if that means executing the remote logic via web services).
- Define the data domain.
- Provide a clean API that exposes the model data and operations on it.
Model should not:
- Expose the details of how the model data is obtained or managed (in other words, details of the data storage mechanism or the remote web service should not be exposed to the controllers and views).
- Contain the logic that transforms the model based on user interaction (this is the controller's job).
- Contain logic for displaying data to the user (this is the view's job).
The benefit of isolating model from the controller and view is that you can test your application logic more easily, and that enhancing and maintaining the overall application is simpler and easier.
Controller should:
- Contain the logic required to initialise the scope.
- Contain the logic/behaviours required by the view to present data from the scope.
- Contain the logic/behaviours required to update the scope based on user interaction.
Controller should not:
- Contain logic that manipulates the DOM (that is the job for the view).
- Contain logic that manages the persistence of data (that is the job for the model).
- Manipulate data outside of the scope.
Views should:
- Contain the logic and markup required to present data to the user.
Views should not:
- Contain complex logic (this is better placed in the controller).
- Contain logic that creates, stores or manipulates the domain model.
- Putting the logic in the wrong place (e.g. business logic in the view, rather than a controller; domain logic in the controller, rather than in a model; data store logic in the client model when using a RESTful service).
- Manipulating the DOM directly using jQuery.
- View logic should prepare data only for display and never modify the model.
- Controller should never directly create, update or delete data from the model.
- The client should never directly access the database.
Angular puts itself forth as a MVW framework (TL;DR you need conventions where framework does not enforce them).
These are the most popular community driven conventions (in the order of the number of stars on GitHub):
- https://github.com/johnpapa/angularjs-styleguide
- https://github.com/mgechev/angularjs-style-guide
- https://github.com/toddmotto/angularjs-styleguide
If you are starting with AngularJS, follow either (the most popular) of the existing style guides. Personal advise: if you disagree with a certain aspect of the convention, raise an issue and explain your reason for doing otherwise. If the vast majority of the community disagrees with you, chances are that you are better off to just go with the flow.
Angular provides a number of utility functions for common programming tasks:
- angular.lowercase
- angular.uppercase
- angular.forEach
- angular.extend
- angular.noop
- angular.identity
- angular.isUndefined
- angular.isDefined
- angular.isObject
- angular.isString
- angular.isNumber
- angular.isDate
- angular.isArray
- angular.isFunction
- angular.isElement
- angular.copy
- angular.equals
- angular.bind
- angular.toJson
- angular.fromJson
The list is limiting and depends on the needs of the Angular core development team. For this reason, I prefer the route that the Backbone took. Backbone.js has a hard dependency for Underscore, which is a standalone collection of utility functions. While I don't recommend Angular team to have a hard dependency on an external utility function collection, I advise against using utility functions that are hard-coded into the framework and whose primary function is to support the framework.
If you are starting a large scale application, I advise to not use either of the above functions and familiarize with either of the standalone utility libraries (lodash, Underscore, lazy.js). Whatever you choose, be consistent.
The following functions are specific to AngularJS:
Use angular.bootstrap to manually start up angular application.
The example does not use the ngApp directive to auto-bootstrap an application.
Use angular.reloadWithDebugInfo to reload the current application with debug information turned on.
Debug mode:
- Attaches information about the bindings (
$bindingproperty of the element's data; data is in the jQlite scope and not in theHTMLElement.dataset). - Adds CSS classes ("ng-binding") to data-bound elements (as a result of
ngBind,ngBindHtmlor{{...}}interpolations). - Where the compiler has created a new scope, the scope and either "ng-scope" or "ng-isolated-scope" CSS class are attached to the corresponding element. These scope references can then be accessed via
element.scope()andelement.isolateScope().
Debug information is enabled by default. This function is used when $compileProvider.debugInfoEnabled() has been used to disable debug information.
Disable debug information in production for a significant performance boost. In production, you can call angular.reloadWithDebugInfo() in browser console to enable debug information.
angular.injector creates an injector object ($injector). $injector is used to retrieve object instances as defined by provider, instantiate types, invoke methods, and load modules.
Angular creates a single $injector when it bootstraps an application and uses the single $injector to invoke controller functions, service functions, filter functions, and any other function that might need dependencies as parameters. You can obtain an instance of the injector:
- If you manually bootstrap the application (using
angular.bootstrap()). - Using
angular.element([DOM node]).injector()where [DOM element] is a DOM element where theng-appwas defined (or a child element of this element). - Using DI, e.g.
$injectorparameter in themodule.config().
angular.injector() is for creating a new instance of the $injector.
Creating your own injector is useful in unit tests where you do not want singleton service instances. You must pass a list of the modules the injector will work with (just the core "ng" module in the above code). You have to explicitly list the ng module if you are going to use any services from the core of Angular. Unlike the angular.module method, which assumes you have a dependency on the ng module and will silently add "ng" to your list of dependencies, the injector function makes no assumptions about dependent modules.
angular.element wraps a raw DOM element or HTML string as a jQuery-like element.
Angular is using a built-in subset of jQuery, called "jQuery lite" or "jqLite".
Refer to the official documentation of angular.element to learn about jQuery-lite specific methods and events.
The angular.module is a global place for creating, registering and retrieving Angular modules. All modules (angular core or 3rd party) that should be available to an application must be registered using this mechanism.
When passed two or more arguments, a new module is created. If passed only one argument, an existing module (the name passed as the first argument to module) is retrieved.
A module is a collection of services, directives, controllers, filters, and configuration information. angular.module is used to configure the $injector.
In Angular, templates are written with HTML that contains Angular-specific elements and attributes. Angular combines the template with information from the model and controller to render the dynamic view that a user sees in the browser.
AngularJS extends HTML in the following ways:
- Expressions & Data binding.
- Directives
AngularJS uses double-brace characters ({{ and }}) to denote expression. The content of the expression is evaluated as a subset of JavaScript in the context of the scope. Expressions are evaluated using $interpolate service.
Angular expressions are like JavaScript expressions with the following differences:
- Context
- Forgiving
- You cannot use the following in an Angular expression: conditionals, loops, or exceptions.
- You cannot declare functions in an Angular expression.
- You cannot create regular expressions in an Angular expression.
- You cannot use
,orvoidin an Angular expression. - You can use filters within expressions to format data before displaying it.
In Angular, expressions are evaluated against a scope object, i.e. Angular expressions do not have access to global variables like window, document or location.
In addition to implicit variables and inherited scope variables, every scope inherits these properties from the $rootScope:
| Property | Description |
|---|---|
$id | Unique scope ID (monotonically increasing) useful for debugging. |
$parent | Reference to the parent scope. |
$root | Reference to the root scope. |
Angular expression that evaluates to undefined scope property does not throw an error.
- Directives are functions.
- Directives are referenced in HTML templates using element-names and attributes.
- Directives define a custom behavior and transform the associated DOM element(s).
- Angular has inbuilt directives.
- New directives can be declared using
module.directive.
| Directive | Role |
|---|---|
ng-app | Bootstraps an Angular application. |
ng-bind | Attribute-reference equivalent of the double-brace expression. |
gajus-bar | Custom directive. |
gajus-baz | Intentionally misspelled reference to gajusBar directive. |
The HTML compiler traverses the DOM matching directives against the DOM elements. HTML compiler must know the directive name before it can be matched in DOM. Elements that have names or attributes that do not resolve to an existing directive will be left intact. In the earlier example, ngApp, ngBind and gajusBar are resolved; gajus-baz is misspelled reference to gajusBar. Notice that there is no error; Angular ignores unknown elements and attributes.
- Directive declaration is in camelCase (e.g.
gajusBar). - Directive reference is dash-delimited name (e.g.
gajus-bar). - Inbuilt directives are prefixed with
ng. - Custom directives must be prefixed with a short, unique and descriptive prefix.
Angular has many inbuilt directives for:
- DOM control structures (repeating/hiding/conditionally including DOM fragments).
- Event handling (click, submit, etc.).
- Directives are created using the
directivemethod on an Angularmodule. - Directives are referenced from HTML templates using element-name or attribute.
In the "foo" example, I am:
- Defined "foo" module.
- Defined "bar" directive.
- Instructed directive to:
- replace the referencing element with the template.
- respond to attribute reference.
- use string template
There are a number of Angular directives made open-source by the community available for the re-use:
- Pro AngularJS by Adam Freeman.
- Write Modern Web Apps with the MEAN Stack: Mongo, Express, AngularJS, and Node.js by Jeff Dickey
- D3 on AngularJS by Ari Lerner and Victor Powell