AngularJS models are held in the $scope
object. In AngularJS, $scope
is used to gain access to the model related to a particular controller. $rootScope
is a parent scope that can be used to save and access model properties that span multiple controllers. The use of $rootScope
is highly discouraged in most designs, however. There is only one $rootScope
in an application. $scope
is a child scope of $rootScope
.
A properly designed AngularJS application will have little or no use for $rootScope
to store model properties. In this chapter we will focus only on $scope
, used to store the model retrieved from REST services.
The REST services used for this chapter are available at http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog. The services are open to the public and written in JavaScript using Node.js, ExpressJS, and MongoDB. In Chapter 11, you will deploy the same REST services with your AngularJS blog application as a MEAN stack (MongoDB, ExpressJS, AngularJS, and Node.js) application. You will then deploy the MEAN stack to the cloud using a free RedHat OpenShift account.
The following excerpt shows how AngularJS services access the REST services used for this chapter. The REST services return the same JSON that was previously hardcoded in the controllers:
/* chapter7/services.js excerpt */
$resource
(
"http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blog/:id"
...
$resource
(
"http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blogList"
...
The complete modified services.js file is shown here:
/* chapter7/services.js complete file */
'use strict'
;
/* Services */
var
blogServices
=
angular
.
module
(
'blogServices'
,
[
'ngResource'
]);
blogServices
.
factory
(
'BlogPost'
,
[
'$resource'
,
function
(
$resource
)
{
return
$resource
(
"http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blog/:id"
,
{},
{
get
:
{
method
:
'GET'
,
cache
:
false
,
isArray
:
false
},
save
:
{
method
:
'POST'
,
cache
:
false
,
isArray
:
false
},
update
:
{
method
:
'PUT'
,
cache
:
false
,
isArray
:
false
},
delete
:
{
method
:
'DELETE'
,
cache
:
false
,
isArray
:
false
}
});
}]);
blogServices
.
factory
(
'BlogList'
,
[
'$resource'
,
function
(
$resource
)
{
return
$resource
(
"http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blogList"
,
{},
{
get
:
{
method
:
'GET'
,
cache
:
false
,
isArray
:
true
}
});
}]);
Shown next is the controllers.js file. The changes made here greatly simplify the controllers. The services needed for each individual controller are injected and made accessible for that particular controller to use. The blog ID is passed as a path parameter argument to the BlogPost
service. A path parameter is used because we defined /id:
at the end of the BlogPost
service URL in the services.js file. If we removed the /:id from the end of the service URL, AngularJS would pass the value as a query parameter argument instead. The updated file looks like this:
/* chapter7/controllers.js */
'use strict'
;
/* Controllers */
var
blogControllers
=
angular
.
module
(
'blogControllers'
,
[]);
blogControllers
.
controller
(
'BlogCtrl'
,
[
'$scope'
,
'BlogList'
,
function
BlogCtrl
(
$scope
,
BlogList
)
{
$scope
.
blogList
=
[];
BlogList
.
get
({},
function
success
(
response
)
{
console
.
log
(
"Success:"
+
JSON
.
stringify
(
response
));
$scope
.
blogList
=
response
;
},
function
error
(
errorResponse
)
{
console
.
log
(
"Error:"
+
JSON
.
stringify
(
errorResponse
));
}
);
}]);
blogControllers
.
controller
(
'BlogViewCtrl'
,
[
'$scope'
,
'$routeParams'
,
'BlogPost'
,
function
BlogViewCtrl
(
$scope
,
$routeParams
,
BlogPost
)
{
var
blogId
=
$routeParams
.
id
;
$scope
.
blg
=
1
;
BlogPost
.
get
({
id
:
blogId
},
function
success
(
response
)
{
console
.
log
(
"Success:"
+
JSON
.
stringify
(
response
));
$scope
.
blogEntry
=
response
;
},
function
error
(
errorResponse
)
{
console
.
log
(
"Error:"
+
JSON
.
stringify
(
errorResponse
));
}
);
}]);
Once you’ve added the JSON returned from the REST service to the model by assigning it to a scope property, that JSON is made available to the view. All scope properties are accessed from inside the view, as described in previous chapters. There are no changes that need to be made in the view.
If you have used other JavaScript client-side frameworks, by now you should see the simplicity of AngularJS models. With AngularJS, there are no model classes that need to be defined; you don’t need to write model Ajax code or create model objects that have to be bound to the views. All you have to do is assign model properties to the scope. The AngularJS framework handles the rest.
AngularJS models greatly simplify the creation of JavaScript applications. You can cut what potentially could be thousands of lines of model-related code down to only a few lines. By cutting lines of code you also cut valuable development time, and potentially the number of developers needed on a project. The simplicity of the model code also makes applications easier to maintain or enhance, once again cutting costs by cutting development time.
Now we will make the needed changes to enable our blog application to use the public REST services discussed in the previous chapter. First, we must add the services.js file to our project.
Right-click the project and add a new JavaScript file named services.js under the js folder, as shown in Figure 7-1.
Add this code to the newly created file:
/* chapter7/services.js */
'use strict'
;
/* Services */
var
blogServices
=
angular
.
module
(
'blogServices'
,
[
'ngResource'
]);
blogServices
.
factory
(
'BlogPost'
,
[
'$resource'
,
function
(
$resource
)
{
return
$resource
(
"http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blog/:id"
,
{},
{
get
:
{
method
:
'GET'
,
cache
:
false
,
isArray
:
false
},
save
:
{
method
:
'POST'
,
cache
:
false
,
isArray
:
false
},
update
:
{
method
:
'PUT'
,
cache
:
false
,
isArray
:
false
},
delete
:
{
method
:
'DELETE'
,
cache
:
false
,
isArray
:
false
}
});
}]);
blogServices
.
factory
(
'BlogList'
,
[
'$resource'
,
function
(
$resource
)
{
return
$resource
(
"http://nodeblog-micbuttoncloud.rhcloud.com/NodeBlog/blogList"
,
{},
{
get
:
{
method
:
'GET'
,
cache
:
false
,
isArray
:
true
}
});
}]);
Now add the new services.js file to the index.html file’s <head>
section, as shown here, so the file can be loaded by our AngularJS application:
<!-- chapter7/index.html excerpt -->
<script
src=
"js/services.js"
></script>
The complete index.html file is shown here for convenience:
<!-- chapter7/index.html -->
<!DOCTYPE html>
<html
lang=
"en"
ng-app=
"blogApp"
>
<head>
<title>
AngularJS Blog</title>
<meta
name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
>
<meta
http-equiv=
"Content-Type"
content=
"text/html; charset=UTF-8"
>
<link
rel=
"stylesheet"
href=
"lib-css/bootstrap.min.css"
media=
"screen"
/>
<link
rel=
"stylesheet"
href=
"css/styles.css"
media=
"screen"
/>
<script
src=
"js/libs/jquery-1.10.2.min.js"
></script>
<script
src=
"js/libs/bootstrap.min.js"
></script>
<script
src=
"js/libs/angular.min.js"
></script>
<script
src=
"js/libs/angular-route.min.js"
></script>
<script
src=
"js/libs/angular-resource.min.js"
></script>
<script
src=
"js/libs/angular-cookies.min.js"
></script>
<script
src=
"js/app.js"
></script>
<script
src=
"js/controllers.js"
></script>
<script
src=
"js/services.js"
></script>
</head>
<body>
<div
ng-view
></div>
</body>
</html>
The newly created services module must be added to the application before it can be used. We add the new blogServices
module as a dependency of the application at startup time using inline array annotations, as shown here. Now the new services can be injected and used in controllers whenever needed. We can now replace the hardcoded JSON used as mock data in previous chapters:
/* chapter7/app.js */
'use strict'
;
/* App Module */
var
blogApp
=
angular
.
module
(
'blogApp'
,
[
'ngRoute'
,
'blogControllers'
,
'blogServices'
]);
blogApp
.
config
([
'$routeProvider'
,
'$locationProvider'
,
function
(
$routeProvider
,
$locationProvider
)
{
$routeProvider
.
when
(
'/'
,
{
templateUrl
:
'partials/main.html'
,
controller
:
'BlogCtrl'
}).
when
(
'/blogPost/:id'
,
{
templateUrl
:
'partials/blogPost.html'
,
controller
:
'BlogViewCtrl'
});
$locationProvider
.
html5Mode
(
false
).
hashPrefix
(
'!'
);
}]);
Now let’s see how to use the new services in our controllers. Replace the previous code in controllers.js with the code shown next. The code shows how we inject the services into each controller. We populate the scope properties inside the success
callback function, as explained in previous chapters.
As explained earlier, the success
callback function is only called when the REST service call returns successfully. At that point, we can safely populate the scope properties. The scope properties are then bound to the view by the AngularJS framework:
/* chapter7/controllers.js */
'use strict'
;
/* Controllers */
var
blogControllers
=
angular
.
module
(
'blogControllers'
,
[]);
blogControllers
.
controller
(
'BlogCtrl'
,
[
'$scope'
,
'BlogList'
,
function
BlogCtrl
(
$scope
,
BlogList
)
{
BlogList
.
get
({},
function
success
(
response
)
{
console
.
log
(
"Success:"
+
JSON
.
stringify
(
response
));
$scope
.
blogList
=
response
;
},
function
error
(
errorResponse
)
{
console
.
log
(
"Error:"
+
JSON
.
stringify
(
errorResponse
));
});
}]);
blogControllers
.
controller
(
'BlogViewCtrl'
,
[
'$scope'
,
'$routeParams'
,
'BlogPost'
,
function
BlogViewCtrl
(
$scope
,
$routeParams
,
BlogPost
)
{
var
blogId
=
$routeParams
.
id
;
BlogPost
.
get
({
id
:
blogId
},
function
success
(
response
)
{
console
.
log
(
"Success:"
+
JSON
.
stringify
(
response
));
$scope
.
blogEntry
=
response
;
},
function
error
(
errorResponse
)
{
console
.
log
(
"Error:"
+
JSON
.
stringify
(
errorResponse
));
});
}]);
We also made some changes to the controllers.js file to make testing easier. Testing AngularJS controllers can be more complex when REST services are involved. As mentioned previously, we don’t know when REST services will return results, because they are asynchronous calls.
Asynchronous REST service calls will always cause controller unit tests to fail. Unit tests of controllers that depend on REST services will finish execution before the REST services ever return results, so any scope properties used by controller unit tests will be missing when the test script executes if those properties are returned from a REST service call.
There are ways to add a delay and make unit test scripts wait on the REST service results, but they add an unneeded level of complexity to the test scripts. Unit testing, after all, should be a test of a unit of code and not an end-to-end test. Protractor E2E tests are a better way to test REST services.
Look at the code that follows. The BlogList
service is injected into the BlogCtrl
controller. We make an asynchronous call to the get
method of the BlogList
service by passing two callback functions to the call. The success
callback function returns a successful service response object, and the error
callback function returns any errors if the service call fails:
/* chapter7/controllers.js excerpt */
blogControllers
.
controller
(
'BlogCtrl'
,
[
'$scope'
,
'BlogList'
,
function
BlogCtrl
(
$scope
,
BlogList
)
{
$scope
.
blogList
=
[];
BlogList
.
get
({},
function
success
(
response
)
{
console
.
log
(
"Success:"
+
JSON
.
stringify
(
response
));
$scope
.
blogList
=
response
;
},
function
error
(
errorResponse
)
{
console
.
log
(
"Error:"
+
JSON
.
stringify
(
errorResponse
));
}
);
}]);
It may take a second or more for the REST service to return results. Once the REST service does return results, the success
callback function will be called. Unfortunately, the unit test script will have finished execution long before. We remedy this issue by making a change to the controller.
Notice the assignment $scope.blogList = [];
in the preceding code. The assignment has no impact on the functionality of the controller, but it has a major impact on the unit test script associated with the BlogCtrl
controller. The assignment initializes the scope blogList
property with an empty array.
The following code shows how the empty array is used to test the blogCtrl
controller. Notice the line of code checks that the array length is equal to 0:
/* chapter7/controllerSpec.js excerpt */
expect
(
scope
.
blogList
.
length
).
toEqual
(
0
);
We can then rest assured that the controller is working successfully from a “unit of code” perspective. You will see later how to make sure the REST service worked as expected.
In NetBeans, right-click and run your AngularJS blog application. You should see the same data displayed on the screen that was there when the data was hardcoded. If you are using Chrome as your browser, you can turn on “Developer Tools” and click the “Network” menu button to see the REST service calls that are made as you click various links in the application. You can also click the Headers, Preview, Response, and Timing tabs in Developer Tools to see specific information about each service call.
Using Chrome Developer Tools is also a great way to troubleshoot issues with AngularJS REST service calls if you have problems. There’s a great JavaScript debugger that can be used to debug REST service calls and other JavaScript issues.
If you are not familiar with Chrome Developer Tools, see the Google Chrome site for more information. In addition to the Chrome debugger, NetBeans also has a debugger built in for debugging JavaScript applications. For more information on debugging JavaScript in NetBeans, take a look at the NetBeans website.
The best way to test AngularJS services is with Karma. We used Karma as one of our test frameworks in previous chapters. You should have already created the package.json file for the blog project back in Chapter 5. The file is shown again here for reference:
/* chapter7/package.json */
{
"name"
:
"package.json"
,
"devDependencies"
:
{
"karma"
:
"*"
,
"karma-chrome-launcher"
:
"*"
,
"karma-firefox-launcher"
:
"*"
,
"karma-jasmine"
:
"*"
,
"karma-junit-reporter"
:
"*"
,
"karma-coverage"
:
"*"
}
}
We also created the Karma configuration file for the blog project back in Chapter 5, but we need to make a small change to that: we need to add the AngularJS angular-resource.min.js file to the karma.conf.js file to test our services. The angular-resource.min.js file is used by both the BlogList
and BlogPost
services. The modified karma.conf.js file looks like this:
/* chapter7/karma.conf.js */
module
.
exports
=
function
(
config
)
{
config
.
set
({
basePath
:
'../'
,
files
:
[
"public_html/js/libs/angular.min.js"
,
"public_html/js/libs/angular-mocks.js"
,
"public_html/js/libs/angular-route.min.js"
,
"public_html/js/libs/angular-resource.min.js"
,
"public_html/js/*.js"
,
"test/**/*Spec.js"
],
exclude
:
[
],
autoWatch
:
true
,
frameworks
:
[
"jasmine"
],
browsers
:
[
"Chrome"
,
"Firefox"
],
plugins
:
[
"karma-junit-reporter"
,
"karma-chrome-launcher"
,
"karma-firefox-launcher"
,
"karma-jasmine"
]
});
};
Now we need to add new service test specifications for the blog project. Do the following:
/* chapter7/servicesSpec.js */
/* Jasmine specs for controllers */
describe
(
'AngularJS Blog Service Testing'
,
function
()
{
describe
(
'test BlogList'
,
function
()
{
var
$rootScope
;
var
blogList
;
beforeEach
(
module
(
'blogServices'
));
beforeEach
(
inject
(
function
(
$injector
)
{
$rootScope
=
$injector
.
get
(
'$rootScope'
);
blogList
=
$injector
.
get
(
'BlogList'
);
}));
it
(
'should test BlogList service'
,
function
()
{
expect
(
blogList
).
toBeDefined
();
});
});
describe
(
'test BlogPost'
,
function
()
{
var
$rootScope
;
var
blogPost
;
beforeEach
(
module
(
'blogServices'
));
beforeEach
(
inject
(
function
(
$injector
)
{
$rootScope
=
$injector
.
get
(
'$rootScope'
);
blogPost
=
$injector
.
get
(
'BlogPost'
);
}));
it
(
'should test BlogPost service'
,
function
()
{
expect
(
blogPost
).
toBeDefined
();
});
});
});
It is important to point out here that our test specifications for the blog services do not depend on the presence and functionality of the associated REST services that get called by those services. Karma unit tests should test that the AngularJS services can be injected. If the tests are successful, that proves that the services are constructed properly. Our unit testing of services does not, however, prove that the REST services are working.
As I mentioned before, Karma unit tests often run inside some continuous integration (CI) framework. CI systems are often configured to trigger the running of unit tests every time a change is pushed to the source repository. The existence and accessibility of REST services can’t always be guaranteed when you’re unit testing inside a CI.
Unit tests shouldn’t depend on the existence of REST services or other network-related devices. Unit testing should test the individual units of code and not try to do end-to-end testing. We will test the functionality of our REST services when we do E2E testing with Protractor. Any problems related to the calling of REST services will show as failures in Protractor.
The new test specifications will unit test the new services. The controllers will also be tested because we still have the controllerSpec.js file in our system. Our Karma configuration file looks for all test files that end in Spec.js.
Right-click the project and select “Test” from the menu. Karma will start. You should see both Chrome and Firefox browser windows open. The NetBeans test results window should open and display four passed tests for Chrome and four passed tests for Firefox.
If you get any error messages or failed tests, go back over this section and verify that you completed all the configurations and installations. You can also download the Chapter 7 code from the GitHub project site.
We already created a Protractor configuration file for the blog application in Chapter 5. The Protractor configuration file is shown here for reference:
/* chapter7/conf.js Protractor configuration file */
exports
.
config
=
{
seleniumAddress
:
'http://localhost:4444/wd/hub'
,
specs
:
[
'e2e/blog-spec.js'
]
};
Now we need to change the Protractor test specifications created earlier. The new Protractor tests need to interact with the REST services that we use in this chapter.
Copy the code shown here into the blog-spec.js file. Make sure the lines like browser.get("http://localhost:8383/AngularJsBlog/");
match the URL that you use on your system to call the blog application. The URL can be different for different development environments and can depend on how you named your project:
/* chapter7/blog-spec.js Protractor test specification */
describe
(
"Blog Application Test"
,
function
(){
it
(
"should test the main blog page"
,
function
(){
browser
.
get
(
"http://localhost:8383/AngularJsBlogChapter7/"
);
expect
(
browser
.
getTitle
()).
toEqual
(
"AngularJS Blog"
);
//gets the blog list
var
blogList
=
element
.
all
(
by
.
repeater
(
'blogPost in blogList'
));
//tests the size of the blogList
expect
(
blogList
.
count
()).
toEqual
(
1
);
browser
.
get
(
"http://localhost:8383/AngularJsBlogChapter7
/#!/blogPost/5394e59c4f50850000e6b7ea"
);
expect
(
browser
.
getTitle
()).
toEqual
(
"AngularJS Blog"
);
//gets the comment list
var
commentList
=
element
.
all
(
by
.
repeater
(
'comment in blogEntry.comments'
));
//checks the size of the commentList
expect
(
commentList
.
count
()).
toEqual
(
2
);
});
});
Start a new command window and enter the following command to start the test server:
webdriver-manager start
Open a new command window and navigate to the root of the Chapter 5 project. Type the command:
protractor test/conf.js
You should see a browser window open. You should then see the test script navigate through the pages of the blog application. When the Protractor script has finished, the browser window will close.
You should see results like the following in the command window when the Protractor script completes. The number of seconds that it takes the script to finish will vary depending on your particular system:
Finished in 1.52 seconds 1 test, 4 assertions, 0 failures
This concludes our discussion of AngularJS models. We added code to make our blog application work with REST services running in the cloud, and we wrote unit tests to test the new services that we added. We then used Protractor to do end-to-end testing that validated the functionality of our REST services and the AngularJS services associated with those REST services.
We will talk about models again in Chapter 11, when we deploy our application to the cloud as a MEAN stack application. Next, we will add some non-REST services to handle business logic and see the power of AngularJS in action.