AngularJS is a framework made of components. Different components are different tools for different problems. If you are familiar with MVC (model-view-controller) frameworks of any type, the distinction between these components will likely make a lot of sense to you. If you are new to MVC, consider it a way to separate an application into presentation, logic and data layers. In addition, MVC provides loose coupling between these layers that allow layers to be swappable. For example, you may have written an entire app in AngularJS, but you want it displayed differently for mobile--just provide a new view; there is no need to write an entirely new application.
The core components are as follows:
A fundamental concept to understand in AngularJS is the scope. A scope houses the application model. Not only individual models, but functions as well. Every controller has its own scope. Some directives have their own scope. It's important to know that you can take a scope and visually map it onto a chunk of markup (if you use the AngularJS Batarang, a debugging tool, you can inspect individual HTML nodes and see what is contained within their scope). That's right; it mimics the DOM hierarchy of your view. Have a <p> somewhere with an ngController on it? That <p> now has a scope, and whatever you put in the scope, the <p> can touch. Remove <p> and the scope no longer exists. Some directives create their own scopes, like ngRepeat, and you can look at your markup and know where that new scope lives.
AngularJS is actually closer to an MVVM (Model-View-ViewModel) framework, if you are familiar with that term. Think of the scope object as the ViewModel. But Igor Minar, one of the AngularJS authors, has declared AngularJS to be a MVW (Model-View-Whatever) framework; it's a just a framework that separates logic from the presentation. His reasoning is that the lines between these framework types are often blurred by frameworks, and it's not really worth arguing about as long as you're productive in your framework of choice:
It's a JavaScript-ish bit of code that AngularJS understands and processes. Anything that looks like JavaScript within your view is not JavaScript; it's an Angular expression. As such, not all JavaScript is a valid expression! Expressions are evaulated against scopes; if you have an expression foo = 'bar';
the string foo
in the current scope will be assigned the value of "bar". Expressions do not deal with undefined
and null
values as JavaScript does; given something like ng-show="foo.bar";
if foo is undefined, you will not receive an error. The operators ===
and !==
are not necessary; stop worrying about it. You can not use any conditionals, loops, or exceptions within AngularJS expressions. Finally, you can use filters with the |
(pipe) operator. AngularJS expression language is in development; look for more features in the future.
Of utmost importance to understand is that there is a root scope from which all scopes inherit, and all scopes inherit prototypically from their parent scope by default. If you set something on the root scope of the application (you can get at this via the $rootScope
service), it will be available to all non-isolate scopes (an isolate scope is used in certain directives and does not inherit from its parent, on purpose, in order to not pollute or otherwise overstep its bounds) within the application, no matter where they are. If you need a debug()
function (or something; consider the $log
service for debugging) available everywhere in the view, put it on the root scope. If you don't need it everywhere, please do not abuse the root scope.
Scopes are simply JavaScript objects, but they have special functions. Here are overviews of some of the more useful ones:
$apply
: Executes an AngularJS expression from outside of the framework, and typically used in directives. If you have a 3rd library that needs to set a model, you must wrap that assignment in a call to $apply
or AngularJS will fail to recognize the change happened, and all sorts of nothing will occur. Watches will not fire, promises will not get resolved, etc., etc.$watch
: Watches/observes an AngularJS expression; if that expression changes, the function you pass to $watch
will fire. The function will receive "new" and "old" parameters. There is an optional 3rd argument to a $watch
; since, by default, $watch
evaluates by reference, certain expressions may not trigger a watch function like you think they would. For example, say you had a watch on the expression foo
. If foo
is reassigned to a new object, the watch will fire. However, if there is an object member bar
and it changes, the watch will not fire. The inner object changed, yes, but there was no change in reference. If you want to evaulate by value (a much more expensive operation; use wisely!), supply true as the third parameter to the $watch
call. Note that when you define a $watch
for the first time, its listener function will be called asynchronously, so it's often necessary to do a comparison between the two parameters as the first line of your listener function if that value is set asynchronously.$eval
: Evaluates an expression against the scope and returns its value. Useful in directives, if you want to evaluate a tag's attribute as an AngularJS expression, and use its value later.$on/$broadcast/$emit
: You can fire and respond to events up or down the scope inheritance chain using these functions. Use like you would custom jQuery event handlers, for example.Here are some examples of Scope functions:
// since "click" is actually jQuery doing the work, AngularJS doesn't know about it.
// thus, if we want any watchers to fire when we update our scope object, we must
// use scope.$apply().
someModule.directive('someDirective', function() {
return function(scope, elm) {
elm.click(function() {
scope.$apply(function() {
scope.foo = 'bar';
});
}
}
});
// this watch will ONLY fire if the object on the scope is reassiged or loses its reference
$scope.$watch('some_object', function(newval, oldval) {
if (newval !== oldval) {
$scope.someFunction(newval);
}
});
// this watch will fire if the object on the scope is reassiged or loses its reference OR
// if an object member changes. more expensive
$scope.$watch('some_object', function(newval, oldval) {
if (newval !== oldval) {
$scope.someFunction(newval);
}
}, true);
// usage: <div some-directive="bar"></div>
someModule.directive('someDirective', function() {
return function(scope, elm, attrs) {
// some_object is now the value of bar within the current scope
var some_object = scope.$eval(attrs.someDirective);
}
});
// consider this to be in controller Foo; when $broadcast is called all subcontrollers
// will be listening and execute their callback functions. it will also listen on all
// "anotherEvent" events coming from subcontroller Bar, and any other subcontrollers.
$scope.$broadcast('someEvent', obj1);
$scope.$on('anotherEvent', function(evt, data) {
// do something with obj2
});
// consider this to be in subcontroller Bar
$scope.$on('someEvent', function(evt, data) {
// do something with the event object and/or "obj1"
});
$scope.$emit('anotherEvent', obj2);
// note that if you want sibling controllers to respond to a $broadcast, you will
// need to do this in a parent controller. if one is not present, it may make
// sense to use $rootScope:
$rootScope.$broadcast('globalEvent', super_important_stuff);
If you see this JavaScript error, it means that AngularJS is already attempting to evaluate expressions. The most common solution is to remove your call to $apply
because it may not be necessary. Sometimes this doesn't do the trick; however, because if you remove your $apply
, your watches don't always fire. Typically I've seen this happen when the same function gets executed from an AngularJS context, synchronously, and outside of an AngularJS context, asynchronously (as with a setTimeout or something). In this case you must ensure your function itself has no $apply
in it, but rather each function call is wrapped in an $apply
if it is appropriate.
The following examples in this book will contain unit tests written in Jasmine (http://pivotal.github.com/jasmine/). Jasmine is a BDD (Behavior-Driven Development)-style testing framework written in JavaScript for testing JavaScript. If you are unfamiliar with BDD, it's OK--you don't have to know anything about it to start using Jasmine. The syntax will seem a little strange, but makes sense pretty quickly.
While TDD (test-driven-development) is simply a practice--in a nutshell, write your tests first--BDD is more than this. It is a different way of approaching software development. The main difference between the two is that BDD tests can, in their essence, be written by testers and even end-users. As you are determining your business logic in a specification, you can write down "x should happen when y happens" and convert this directly into a test with a BDD-style testing framework. I haven't read any books about BDD, so I cannot recommend one, but you are encouraged to find out more if you are interested. Remember, you do not need to "subscribe" to BDD to use Jasmine for testing; it works great as a general-purpose unit testing framework.
We'll also assume that the angular-mocks.js file is included when doing these tests, as it provides some convience methods for Jasmine to work with AngularJS.
AngularJS provides functional (or E2E) tests within its online tutorial. While these tests have a lot of value, our application is small and we can get by with manual testing. Once your application grows, it makes more sense to automate functional tests, but I don't find it necessary for our purposes.
We have models, views, controllers, services/factories/providers, filters, and directives. All of these components will make up a module. A module may be an entire application, a part of an application, or a suite of reusable components. In a two-page application, for example, you may have a module that provides common functionality between the two pages, and a module for each page itself. You would include the "common" module when defining each page's module. If you had other applications, you may want to break out common functionality to yet another module which all applications can use.
$http
service is really a provider, and you can configure its defaults during initialization if you wish. A provider returns nothing, but defines this.$get();
a function to return a factory-like object. When you define provider Foo
, you're really definining FooProvider;
the thing returned from this.$get
is really Foo
.
DOM Manipulation outside of a directive
A service, factory, or provider can be used for DOM manipulation, but if it is, it should solely serve that function as to make unit testing straightforward and avoid spaghetti. A great place to put functionality that creates a modal pop-up is in a service. Creating a new "view" could even be construed as business logic. It turns out that using a service in this manner may feel more natural than trying to shoehorn the same functionality into a directive. For example, I have a case in which I provide a showWarning()
function (via a factory) to display a warning, the contents of which sometimes fetched over HTTP, to the user, within a modal (note that if we didn't need to show this in a modal, it would be better to simply set the warning text in a model and use ngShow
to display it).
keyDown
event? Overwrite that input directive! We'll have an example of this later. Directives essentially extend HTML and can be used to create a domain-specific language for your application or applications.There has been error in communication with Booktype server. Not sure right now where is the problem.
You should refresh this page.