- Learning Yeoman
- Jonathan Spratley
- 5265字
- 2021-09-03 10:02:33
Anatomy of an Angular project
Generally in a single page application (SPA), you create modules that contain a set of functionality, such as a view to display data, a model to store data, and a controller to manage the relationship between the two. Angular incorporates the basic principles of the MVC pattern into how it builds client-side web applications.
The major Angular concepts are as follows:
- Templates: A template is used to write plain HTML with the use of directives and JavaScript expressions
- Directives: A directive is a reusable component that extends HTML with the custom attributes and elements
- Models: A model is the data that is displayed to the user and manipulated by the user
- Scopes: A scope is the context in which the model is stored and made available to controllers, directives, and expressions
- Expressions: An expression allows access to variables and functions defined on the scope
- Filters: A filter formats data from an expression for visual display to the user
- Views: A view is the visual representation of a model displayed to the user, also known as the Document Object Model (DOM)
- Controllers: A controller is the business logic that manages the view
- Injector: The injector is the dependency injection container that handles all dependencies
- Modules: A module is what configures the injector by specifying what dependencies the module needs
- Services: A service is a piece of reusable business logic that is independent of views
- Compiler: The compiler handles parsing templates and instantiating directives and expressions
- Data binding: Data binding handles keeping model data in sync with the view
Why Angular?
AngularJS is an open source JavaScript framework known as the Superheroic JavaScript MVC Framework, which is actively maintained by the folks over at Google. Angular attempts to minimize the effort in creating web applications by teaching the browser's new tricks. This enables the developers to use declarative markup (known as directives or expressions) to handle attaching the custom logic behind DOM elements.
Angular includes many built-in features that allow easy implementation of the following:
- Two-way data binding in views using double mustaches
{{ }}
- DOM control for repeating, showing, or hiding DOM fragments
- Form submission and validation handling
- Reusable HTML components with self-contained logic
- Access to RESTful and JSONP API services
The major benefit of Angular is the ability to create individual modules that handle specific responsibilities, which come in the form of directives, filters, or services. This enables developers to leverage the functionality of the custom modules by passing in the name of the module in the dependencies.
Creating a new Angular project
Now it is time to build a web application that uses some of Angular's features. The application that we will be creating will be based on the scaffold files created by the Angular generator; we will add functionality that enables CRUD operations on a database.
Installing the generator-angular
To install the Yeoman Angular generator, execute the following command:
$ npm install -g generator-angular
Note
For Karma testing, the generator-karma needs to be installed.
Scaffolding the application
To scaffold a new AngularJS application, create a new folder named learning-yeoman-ch3
and then open a terminal in that location. Then, execute the following command:
$ yo angular --coffee
This command will invoke the AngularJS generator to scaffold an AngularJS application, and the output should look similar to the following screenshot:

Understanding the directory structure
Take a minute to become familiar with the directory structure of an Angular application created by the Yeoman generator:
app
: This folder contains all of the front-end code, HTML, JS, CSS, images, and dependencies:images
: This folder contains images for the applicationscripts
: This folder contains AngularJS codebase and business logic:app.coffee
: This contains the application module definition and routingcontrollers
: Custom controllers go here:main.coffee
: This is the main controller created by default
directives
: Custom directives go herefilters
: Custom filters go hereservices
: Reusable application services go here
styles
: This contains all CSS/LESS/SASS files:main.css
: This is the main style sheet created by default
views
: This contains the HTML templates used in the applicationmain.html
: This is the main view created by default
index.html
: This is the applications' entry point
bower_components
: This folder contains client-side dependenciesnode_modules
: This contains all project dependencies as node modulestest
: This contains all the tests for the application:spec
: This contains unit tests mirroring structure of theapp
/scripts
folderkarma.conf.coffee
: This file contains the Karma runner configuration
Gruntfile.js
: This file contains all project taskspackage.json
: This file contains project information and dependenciesbower.json
: This file contains frontend dependency settings
Note
The directories (directives
, filters
, and services
) get created when the subgenerator is invoked.
Configuring the application
Let's go ahead and create a configuration file that will allow us to store the application wide properties; we will use the Angular value services to reference the configuration object.
Open up a terminal and execute the following command:
$ yo angular:value Config
This command will create a configuration service located in the app
/scripts
/services
directory. This service will store global properties for the application.
Note
For more information on Angular services, visit http://goo.gl/Q3f6AZ.
Now, let's add some settings to the file that we will use throughout the application. Open the app/scripts/services/config.coffee
file and replace with the following code:
'use strict' angular.module('learningYeomanCh3App').value('Config', Config = baseurl: document.location.origin sitetitle: 'learning yeoman' sitedesc: 'The tutorial for Chapter 3' sitecopy: '2014 Copyright' version: '1.0.0' email: 'jonniespratley@gmail.com' debug: true feature: title: 'Chapter 3' body: 'A starting point for a modern angular.js application.' image: 'http://goo.gl/YHBZjc' features: [ title: 'yo' body: 'yo scaffolds out a new application.' image: 'http://goo.gl/g6LO99' , title: 'Bower' body: 'Bower is used for dependency management.' image: 'http://goo.gl/GpxBAx' , title: 'Grunt' body: 'Grunt is used to build, preview and test your project.' image: 'http://goo.gl/9M00hx' ] session: authorized: false user: null layout: header: 'views/_header.html' content: 'views/_content.html' footer: 'views/_footer.html' menu: [ title: 'Home', href: '/' , title: 'About', href: '/about' , title: 'Posts', href: '/posts' ] )
The preceding code does the following:
- It creates a new
Config
value service on thelearningYeomanCh3App
module - The
baseURL
property is set to the location where the document originated from - The
sitetitle
,sitedesc
,sitecopy
, andversion
attributes are set to default values that will be displayed throughout the application - The
feature
property is an object that contains some defaults for displaying a feature on the main page - The
features
property is an array of feature objects that will display on the main page as well - The
session
property is defined withauthorized
set tofalse
anduser
set tonull
; this value gets set to the current authenticated user - The
layout
property is an object that defines the paths of view templates, which will be used for the corresponding keys - The
menu
property is an array that contains the different pages of the application
Note
Usually, a generic configuration file is created at the top level of the scripts
folder for easier access.
Creating the application definition
During the initial scaffold of the application, an app.coffee
file is created by Yeoman located in the app/scripts
directory. The scripts/app.coffee
file is the definition of the application, the first argument is the name of the module, and the second argument is an array of dependencies, which come in the form of angular modules and will be injected into the application upon page load.
The app.coffee
file is the main entry point of the application and does the following:
- Initializes the application module with dependencies
- Configures the applications router
Any module dependencies that are declared inside the dependencies array are the Angular modules that were selected during the initial scaffold. Consider the following code:
'use strict' angular.module('learningYeomanCh3App', [ 'ngCookies', 'ngResource', 'ngSanitize', 'ngRoute' ]) .config ($routeProvider) -> $routeProvider .when '/', templateUrl: 'views/main.html' controller: 'MainCtrl' .otherwise redirectTo: '/'
The preceding code does the following:
- It defines an
angular
module namedlearningYeomanCh3App
with dependencies on thengCookies
,ngSanitize
,ngResource
, andngRoute
modules - The
.config
function on the module configures the applications' routes by passing route options to the$routeProvider
service
Note
Bower downloaded and installed these modules during the initial scaffold.
Creating the application controller
Generally, when creating an Angular application, you should define a top-level controller that uses the $rootScope
service to configure some global application wide properties or methods. To create a new controller, use the following command:
$ yo angular:controller app
This command will create a new AppCtrl
controller located in the app/scripts/controllers
directory.
Open the app/scripts/controllers/app.coffee
file and replace with the following code:
'use strict' angular.module('learningYeomanCh3App') .controller('AppCtrl', ($rootScope, $cookieStore, Config) -> $rootScope.name = 'AppCtrl' App = angular.copy(Config) App.session = $cookieStore.get('App.session') window.App = $rootScope.App = App)
The preceding code does the following:
- It creates a new
AppCtrl
controller with dependencies on the$rootScope
,$cookieStore
, andConfig
modules - Inside the controller definition, an
App
variable is copied from theConfig
value service - The
session
property is set to theApp.session
cookie, if available
Creating the application views
The Angular generator will create the applications' index.html
view, which acts as the container for the entire application. The index view is used as the shell for the other views of the application; the router handles mapping URLs to views, which then get injected to the element that declares the ng-view
directive.
Modifying the application's index.html
Let's modify the default view that was created by the generator. Open the app/index.html
file, and add the content right below the following HTML comment:
<!-- Add your site or application content here -->
The structure of the application will consist of an article element that contains a header, section, and footer. Replace with the following content:
<article id="app" ng-controller="AppCtrl" class="container"> <header id="header" ng-include="App.layout.header"></header> <section id="content" class="view-animate-container"> <div class="view-animate" ng-view=""></div> </section> <footer id="footer" ng-include="App.layout.footer"></footer> </article>
In the preceding code:
- The
article
element declares theng-controller
directive to theAppCtrl
controller - The
header
element uses anng-include
directive that specifies what template to load, in this case, the header property on theApp.layout
object - The
div
element has theview-animate-container
class that will allow the use of CSS transitions - The
ng-view
attribute directive will inject the current routes view template into the content - The
footer
element uses anng-include
directive to load the footer specified on theApp.layout.footer
property
Note
Use ng-include
to load partials, which allows you to easily swap out templates.
Creating Angular partials
Use the yo angular:view
command to create view partials that will be included in the application's main layout. So far, we need to create three partials that the index view (app
/index.html
) will be consuming from the App.layout
property on the $rootScope
service that defines the location of the templates.
Note
Names of view partials typically begin with an underscore (_).
Creating the application's header
The header partial will contain the site title and navigation of the application. Open a terminal and execute the following command:
$ yo angular:view _header
This command creates a new view template file in the app/views
directory.
Open the app/views/_header.html
file and add the following contents:
<div class="header"> <ul class="nav nav-pills pull-right"> <li ng-repeat="item in App.menu" ng-class="{'active': App.location.path() === item.href}"> <a ng-href = "#{{item.href}}"> {{item.title}} </a> </li> </ul> <h3 class="text-muted"> {{ App.sitetitle }} </h3> </div>
The preceding code does the following:
- It uses the
{{ }}
data binding syntax to displayApp.sitetitle
in a heading element - The
ng-repeat
directive is used to repeat each item in theApp.menu
array defined on$rootScope
Creating the application's footer
The footer partial will contain the copyright message and current version of the application. Open the terminal and execute the following command:
$ yo angular:view _footer
This command creates a view template file in the app/views
directory.
Open the app/views/_footer.html
file and add the following markup:
<div class="app-footer container clearfix"> <span class="app-sitecopy pull-left"> {{ App.sitecopy }} </span> <span class="app-version pull-right"> {{ App.version }} </span> </div>
The preceding code does the following:
- It uses a
div
element to wrap two span elements - The first
span
element contains data binding syntax referencingApp.sitecopy
to display the application's copyright message - The second
span
element also contains data binding syntax to referenceApp.version
to display the application's version
Customizing the main view
The Angular generator creates the main view during the initial scaffold. Open the app/views/main.html
file and replace with the following markup:
<div class="jumbotron"> <h1>{{ App.feature.title }}</h1> <img ng-src="{{ App.feature.image }}"/> <p class="lead"> {{ App.feature.body }} </p> </div> <div class="marketing"> <ul class="media-list"> <li class="media feature" ng-repeat="item in App.features"> <a class="pull-left" href="#"> <img alt="{{ item.title }}" src="http://placehold.it/80x80" ng-src="{{ item.image }}" class="media-object"/> </a> <div class="media-body"> <h4 class="media-heading">{{item.title}}</h4> <p>{{ item.body }}</p> </div> </li> </ul> </div>
The preceding code does the following:
- At the top of the view, we use the
{{ }}
data binding syntax to display the title and body properties declared on theApp.feature
object - Next, inside the
div.marketing
element, anotherdiv
element is declared with theng-repeat
directive to loop for each item in theApp.features
property - Then, using the
{{ }}
data binding syntax wrapped around the title and body properties from the item being repeated, we output the values
Previewing the application
To preview the application, execute the following command:
$ grunt serve
Your browser should open displaying something similar to the following screenshot:

Tip
Download the AngularJS Batarang (http://goo.gl/0b2GhK) developer tool extension for Google Chrome for debugging.
Testing an Angular application
Testing an Angular application is very easy when using Yeoman because all the configuration files are created during the initial project scaffold and all the subgenerators create a test spec that can easily be customized with specific functionality.
Angular unit tests
When scaffolding new files using the Yeoman Angular generator, the subgenerators create skeleton specs that are located in the test/spec
directory. Yeoman makes it extremely easy to start testing your scripts.
Configuring the Karma Runner
Since the application is using various client-side dependencies, you will need to add the library locations to the test/karma.conf.coffee
file:
//Line #15 files: [ //Bower Dependencies 'bower_components/jquery/jquery.js', 'bower_components/bootstrap/dist/js/bootstrap.js', ... ],
Running unit tests
To run the tests for the application, execute the following command:
$ grunt test
The preceding command will do the following:
- It will compile all projects scripts in the
app/scripts
directory to the.tmp/scripts
directory. - Then, invoke the Karma that will launch Chrome and start the runner.
- It will show the output of the tests in the console as they go.
Note
You can invoke the Karma runner directly by executing: $ karma start test/karma.conf.coffee
.
The results from the test task should look similar to the following screenshot:

End-to-end tests with Protractor
Being able to test the functionality of the code in the application is always a good thing, but being able to run actual browser tests that mimic the actions of a real user is priceless. The folks over at Angular have created a new end-to-end testing framework that goes by the name of Protractor, which is built on top of the Selenium WebDriverJS.
The npm installs Protractor. To install the project, execute the following command:
$ npm install –g protractor
This command will install the Protractor library globally, which then can be invoked from any directory. After installing it, you will need to update Selenium; Protractor includes webdriver-manager
, which allows you to update and start the standalone server.
To download and update the Selenium Server, execute the following command:
$ webdriver-manager update
The command above will download the latest driver from Selenium and place it in the correct directory. To start the server, use the following command:
$ webdriver-manager start
This command will kick off the Selenium Server process that is run in the background, allowing you to run your Protractor tests. You should see the following screenshot if it's working correctly:

Configuring Protractor
To configure Protractor to run tests against your application, you will need to create a configuration file that contains settings for the location of test specs, the driver URL, and so on. First, create a configuration file named e2e.conf.js
in the projects root directory.
Open the e2e.conf.js
file and add the following content:
exports.config = { seleniumAddress: 'http://localhost:4444/wd/hub', capabilities: { 'browserName': 'chrome' }, specs: ['test/e2e/*.js'], jasmineNodeOpts: { showColors: true } };
In the preceding code:
- The
exports.config
object holds properties that are passed to Protractor - The
seleniumAddress
property specifies the location of the WebDriver server that is running - The
capabilities
property contains the browser capabilities to enable; here we are specifying to use the Chrome browser; other options can include PhantomJS - The
specs
property specifies the location of the test specs to run - The
jasmineNodeOpts
property specifies what options to pass to the Jasmine node module
Creating Protractor e2e spec
Now, let's create a simple example spec that will load the application and check if the site title matches what we are expecting. Create an empty file named app.js
in the test/e2e
directory.
Open the test/e2e/app.js
file and add the following content:
describe('Chapter3 e2e:', function() { beforeEach(function() { browser.get('http://localhost:9000'); }); it('should have site title', function() { var siteTitle; siteTitle = element(protractor.By.binding('App.sitetitle')); expect(siteTitle.getText()).toEqual('Learning Yeoman'); }); });
To run the tests, just open the terminal and execute the following command:
$ protractor e2e.conf.js
This command will start the Protractor runner and execute the specs specified in the configuration file.
Angular controllers
The controllers in an Angular app are the most important things in the application. They define the scope for the view, which acts as a proxy by receiving user actions or input and performing the proper operations with the assistance of Angular services and/or custom services that third party modules expose.
Creating controllers
Creating Angular controllers is very easy using the angular:controller generator; to create a new controller, open the terminal and execute the following command:
$ yo angular:controller posts
The preceding command creates a new PostsCtrl
controller located in the app/scripts/controllers
directory.
Using controllers
Generally, in Angular, you use controllers to handle view-specific interactions and handle setting up the model data for the templates to display.
To demonstrate this, open the app/scripts/controllers/posts.coffee
file and add the following code:
'use strict' angular.module('learningYeomanCh3App').controller 'PostsCtrl', ($scope, $location, Posts) -> $scope.name = 'Posts' $scope.posts = Posts.query() $scope.add = ()-> $location.path('/posts/new') $scope.view = (id)-> $location.path('/posts/view/' + id)
The preceding code does the following:
- It defines a controller on the
learningYeomanCh3App
module and dependencies on the$scope
,$location
, andPosts
services - The
name
property is set on the scope with the value ofPosts
- The
posts
property is set on the scope to the value returned from thePosts.query()
method - The
add
function on the scope will handle sending the browser to the/posts/new
URL - The
view
function on the scope will handle sending the browser to the/posts/view/:id
URL, whereid
is dynamic and comes from the view
Note
For more information on controllers, visit http://goo.gl/8Yk1vg.
Testing controllers
The angular:controller generator creates a controller spec file when the angular:controller generator is invoked. The controller specs are located in the test/specs/controllers
directory.
To demonstrate testing the controller created now, open the test/specs/controllers/posts.coffee
spec and add the following code:
'use strict' describe 'Controller: PostsCtrl', () -> beforeEach module 'learningYeomanCh3App' PostsCtrl = {} scope = {} location = {} beforeEach inject ($controller, $rootScope, $location) -> scope = $rootScope.$new() location = $location PostsCtrl = $controller 'PostsCtrl', { $scope: scope $location: location } it 'should have name equal to "Posts" on the scope', () -> expect(scope.name).toBe('Posts') it 'should change the location to /posts/new', () -> location.path('/posts') scope.add() expect(location.path()).toEqual('/posts/new') it 'should change the location to /posts/view/:id', () -> location.path('/posts') scope.view(1) expect(location.path()).toEqual('/posts/view/1')
In the preceding code:
- At the top, the
describe
method is used to contain the inner code into a spec namedController: PostsCtrl
- The
beforeEach
method handles loading the module namedlearningYeomanCh3App
for testing - The
PostsCtrl
,scope
, andlocation
private variables are declared and will be set inside anotherbeforeEach
method - The
beforeEach
method runs before each spec defined below; inside thebeforeEach
method, we use theinject
method to load the proper Angular services that will be referenced by the private variables defined above - The first
it
block expects that thescope.name
property matches'Posts'
- The second
it
block handles testing the URL location when theadd()
method is invoked - The third
it
block handles testing the URL location when theview(id)
method is invoked
Angular services
In Angular, services are singleton objects or functions that contain methods common with web applications. Angular has a number of built-in services that simplify various tasks within a web application. Angular also supports custom services that allow a developer to create reusable modules with ease.
Creating services
The angular:service
subgenerator handles creating a new Angular service. Let's create a simple $resource
service that will be used to connect to a RESTful backend API service; open the terminal and execute the following command:
$ yo angular:service Posts
The preceding command does the following:
- It will create a new service located in the
app/scripts/services
directory - This service will use the Angular
$resource
service
Using services
A service is a reusable piece of business logic independent of views. Let's add the logic to handle connecting to a RESTful backend API to access; open the app/scripts/services/posts.coffee
file and add the following content:
'use strict' angular.module('learningYeomanCh3App').factory 'Post', ($resource) -> return $resource('/api/posts/:id', { id: '@_id' }, { 'query': method: 'GET', isArray: true 'update': method : 'PUT' })
The preceding code does the following:
- Defines a new factory service named
Post
on the main module with a dependency on the$resourc
e service - The factory service returns a
$resource
instance with a customupdate
method that changes the request method toPUT
- This factory can be used to access RESTful API services
Testing services
The angular:service subgenerator will create a service test spec located in the test/spec/services
directory when invoked. Open the test/spec/services/post.coffee
file and add the following code:
'use strict' describe 'Service: Post', () -> beforeEach module 'learningYeomanCh3App' Post = {} httpBackend = null mockData = [{_id: 1}, {_id:2}, {_id:3}] beforeEach inject (_$httpBackend_, _Post_) -> Post = _Post_ httpBackend = _$httpBackend_ it 'should fetch list of posts', () -> httpBackend.expectGET('/api/posts').respond(mockData) posts = null promise = Post.query().$promise promise.then((data)-> posts = data ) expect(posts).toBeNull() httpBackend.flush() expect(posts.length).toEqual(3)
In the preceding code:
- At the top, the
describe
method is used to contain the inner code into a spec namedService: Post
- The
beforeEach
method handles loading the module namedlearningYeomanCh3App
for testing - The second
beforeEach
method injects the service for the spec, which is set to the localPost
variable - The
it
block expects that thePosts
service should fetch a list of posts - Inside the function, the
httpBackend.expectGET
method is used to expect aGET
request to the passed argument that is the URL of the request and respond to the request with the mock data defined in the spec - The local
posts
variable is set tonull
because the service should fulfill the promise with the returned data - The
expect
method is used to ensure whether theposts
variable isnull
before the request is made - The
httpBackend.flush()
method is used to process any pending requests - The last
expect
method is used to check whether the length of theposts
variable matches the expected length, which is 3
Note
For more information on testing, visit http://goo.gl/BDo0jZ.
Angular filters
A filter formats the data for display to the user. In Angular, filters are used in expressions, which can be used in templates, controllers, or services. Angular includes several filters for formatting various types of data.
Note
For more information on filters, visit http://goo.gl/Mvhe0S.
Creating filters
The angular:filter subgenerator handles creating a new Angular filter. Let's create a markdown
filter that will convert markdown to HTML; open the terminal and execute the following command:
$ yo angular:filter markdown
The preceding command will create a new filter located in the app/scripts/filters
directory. This directive will use a third-party library for conversion, so execute the following command:
$ bower install markdown --save
This will download and install the markdown library in the app/bower_components
directory.
To install this library into the app/index.html
file, execute the following command:
$ grunt wiredep
The preceding command will attempt to wire all components listed in the project's bower.json
file to the app/index.html
page by adding a script
tag referencing the main .js
file defined in the components metadata.
Note
Remember to add this library to the Karma configuring file before running unit tests.
Using filters
The filter definition is simply a function that returns a string; the first argument of the function is the input value to be filtered and any arguments passed after are parameters passed to the filter function. Open the app/scripts/filters/markdown.coffee
file and add the following content:
'use strict' angular.module('learningYeomanCh3App').filter 'markdown', () -> (input, truncate) -> input = input.substring(0, truncate) if input and truncate return (markdown.toHTML(input)) if input
In the preceding code:
- It creates a new filter named
markdown
on thelearningYeomanCh3App
module - The filter takes two arguments in the
return
function; the first argument will be converted to markdown and the second argument will be the length of the string - Inside the function, the returned string is set to the first argument; if there is a second argument passed, then the length of the returned string is the number passed as the second argument
- The filter returns the parsed input value that is passed and returned from the
markdown.toHTML
method, if there is an input value
Testing filters
The angular:filter subgenerator will create a filter test spec located in the test
/spec
/filters
directory when invoked. It is up to the developer to add the testing logic. Let's write a test for the created filter now. Open the test/spec/filters/markdown.coffee
file and add the following code:
'use strict' describe 'Filter: markdown', () -> markdown = {} beforeEach module 'learningYeomanCh3App' beforeEach inject ($filter) -> markdown = $filter 'markdown' it 'should return the input converted to HTML', () -> input = '#Heading 1' output = '<h1>Heading 1</h1>' expect(markdown(input)).toBe(output) it 'should return input to HTML, truncated length', () -> input = 'This text is **bold**, and this will be truncated.' output = '<p>This text is <strong>bold</strong>, and</p>' expect(markdown(input, 26)).toBe(output)
The preceding code does the following:
- At the top, the
describe
method is used to contain the inner code into a spec namedFilter: markdown
- The
beforeEach
method handles loading the module namedlearningYeomanCh3App
for testing - The second
beforeEach
method injects the filter for the spec, which is set to the local markdown variable - The first
it
block tests if the passed input is converted to the expected output - The second
it
block tests if the passed input is converted to HTML and limited to the length passed in the second argument of the filter function
Note
For more information, visit http://goo.gl/M3fhsT.
Angular directives
A directive in Angular is a reusable declarative HTML component with custom attributes and/or elements with self-contained logic that instructs Angular's HTML compiler to attach the specified behavior to that DOM element or transform the DOM element and its children.
Note
For more information on directives, visit http://goo.gl/4bGGBh.
Creating directives
The angular:directive
subgenerator handles creating a new Angular directive. Let's create a loading directive by opening the terminal and executing the following command:
$ yo angular:directive loading
The preceding command creates a new directive located in the app
/scripts
/directives
folder. This directive will handle showing or hiding the element when the route is changing.
Using directives
A directive contains reusable functionality that can be used in view templates. Let's add the logic to the loading directive; open app/scripts/directives/loading.coffee
and add the following code:
'use strict' angular.module('learningYeomanCh3App').directive('loading', ($rootScope) -> template: '<p>Loading...</p>' restrict: 'EA' replace: true link: (scope, element, attrs) -> element.addClass('loading').fadeOut('fast') $rootScope.$on( '$routeChangeStart', -> element.fadeIn('fast') ) $rootScope.$on('$routeChangeSuccess', -> element.fadeOut() ) )
The preceding code does the following:
- It defines a new
loading
directive on thelearningYeomanCh3App
module, with dependencies on the$rootScope
service - The
directive
definition consists of an object with properties that define how the directive is used - The
template
property is set to an inline string that will simply display the textLoading...
- The
restrict
property is set toEA
, which informs Angular how to allow only an element or attribute to use this directive - The
replace
property is set totrue
, which informs Angular to replace the content with the content defined in thetemplate
property - The
link
property is set to a function that is invoked by Angular to wire the directive - Inside the
link
function, the element gets theloading
class added and is hidden using thejQuery fadeOut
method - Then, the
$rootScope.$on
method will bind to the$locationChangeStart
event dispatched by the router and show the element using thefadeIn
method - Then, the
$rootScope.$on
method will bind to the$locationChangeSuccess
event dispatched by the router and hide the element using thefadeOut
method
Testing directives
The angular:directive
subgenerator will create a directive test spec located in the test
/spec
/directives
directory when invoked. Open the test
/spec
/directives
/loading.coffee
file and add the following code:
'use strict' describe 'Directive: loading', () -> beforeEach module 'learningYeomanCh3App' scope = {} beforeEach inject ($controller, $rootScope, $location) -> scope = $rootScope.$new() scope.location = $location it 'should replace element with Loading...', inject ($compile) -> element = angular.element '<loading></loading>' element = $compile(element) scope expect(element.text()).toBe 'Loading...'
In the preceding code:
- At the top, the
describe
method is used to contain the inner code into a spec namedDirective: loading
- The
beforeEach
method handles loading the module namedlearningYeomanCh3App
for testing - The second
beforeEach
method injects the proper dependencies for the spec, which are set to local variables - The
it
block defines that the directive should replace the element with theLoading...
text - The
$compile
method is used to compile the directive into HTML against the scope - The
expect
method is used to check the text value of the element, which should beLoading...
Note
For more information on testing, visit http://goo.gl/m5UaDH.
Angular views
Creating views is extremely easy with Yeoman using the angular:view subgenerator. Let's create the CRUD views for the application.
Creating the Angular views
To create a new view, open the terminal and execute the following command:
$ yo angular:view [name]
Note
To create a view, controller, and route, use yo angular:route
.
Creating the posts list
Let's create a view that will list all the posts defined on the scope. Open the terminal and execute the following command:
$ yo angular:view posts
The preceding command creates a new posts.html
template located in the app
/views
directory. This view will contain angular specific directives along with custom filters to display a list of posts.
Open the app/views/posts.html
file and add the following code:
<div id="posts"> <button class="btn btn-default pull-right" ng-click="add()">Add New</button> <ol class="breadcrumb"> <li><a href="#">Home</a></li> <li class="active">Posts</li> </ol> <ul class="posts list-unstyled"> <li ng-repeat="post in posts | filter:tag"> <div class="post" data-id="{{post._id}}"> <header ng-include="'views/post-header.html'"></header> <section ng-bind-html="post.body | markdown:200"></section> </div> </li> </ul> </div>
The preceding code does the following:
- It wraps the entire content in a
div
element with an ID of posts, and a simple breadcrumb list displaying the current location - A button with the label
Add New
is declared using theng-click
directive that will invoke theadd()
method defined on the scope - Then the
ul
element is declared with a class oflist-unstyled
to modify the default browser styles - The inner
li
element is declared using theng-repeat
directive that will loop each item in the posts defined on the scope - The
filter:tag
expression is used to inform Angular to only repeat items that match the defined filter - The
header
element is used with theng-include
directive that will load the template into the content - Then, the
section
element is used with theng-bind-html
directive set to the value of thepost.body
property - The
markdown:200
expression will invoke the custom markdown filter to transform markdown to HTML, limited to 250 characters
Creating the post-header view
Now, we need to create a post-header view that is present in the posts.html
view; open the terminal and execute the following command:
$ yo angular:view post-header
The preceding command does the following:
- Creates a new
post-header.html
template located in theapp/views
directory - This view will contain elements that will be used in the list and detail views
Open the app/views/post-header.html
file and add the following code:
<div class="header"> <a href="" ng-click="view(post._id)"> <h1 class="media-heading">{{post.title}}</h1> </a> <span> Posted on {{post.created | date:'mediumDate'}} </span> | <span ng-if="post.tags">Tags: <span class="label label-default tag"ng-repeat="t in post.tags">{{t}}</span> | </span> <a href="" ng-click="edit(post._id)">class="btn btn-xs btn-default edit"> <i class="glyphicon glyphicon-pencil"></i> EDIT </a> </div>
The preceding code does the following:
- The
div
element is declared with a class of header that will contain the title, created date, and tags of the post being repeated - The
ng-click
directive that is used will invoke theview(id)
method on the scope, passing theid
of the post being repeated - The
ng-repeat
directive is used to loop each tag in the tags array - The
ng-click
directive that is used will invoke theedit(id)
method on the scope
- Expert C++
- Hyper-V 2016 Best Practices
- Learning Cython Programming
- Spring 5.0 By Example
- Python Deep Learning
- 3D少兒游戲編程(原書第2版)
- Getting Started with Python Data Analysis
- Angular開發入門與實戰
- HTML5開發精要與實例詳解
- Android嵌入式系統程序開發:基于Cortex-A8(第2版)
- Solutions Architect's Handbook
- App Inventor 2 Essentials
- 零基礎看圖學ScratchJr:少兒趣味編程(全彩大字版)
- Ext JS 4 Plugin and Extension Development
- Learning WordPress REST API