官术网_书友最值得收藏!

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 application
    • scripts: This folder contains AngularJS codebase and business logic:
      • app.coffee: This contains the application module definition and routing
      • controllers: Custom controllers go here:
        • main.coffee: This is the main controller created by default
      • directives: Custom directives go here
      • filters: Custom filters go here
      • services: 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 application
      • main.html: This is the main view created by default
    • index.html: This is the applications' entry point
  • bower_components: This folder contains client-side dependencies
  • node_modules: This contains all project dependencies as node modules
  • test: This contains all the tests for the application:
    • spec: This contains unit tests mirroring structure of the app/scripts folder
    • karma.conf.coffee: This file contains the Karma runner configuration
  • Gruntfile.js: This file contains all project tasks
  • package.json: This file contains project information and dependencies
  • bower.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 the learningYeomanCh3App module
  • The baseURL property is set to the location where the document originated from
  • The sitetitle, sitedesc, sitecopy, and version 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 with authorized set to false and user set to null; 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 named learningYeomanCh3App with dependencies on the ngCookies, ngSanitize, ngResource, and ngRoute 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, and Config modules
  • Inside the controller definition, an App variable is copied from the Config value service
  • The session property is set to the App.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 the ng-controller directive to the AppCtrl controller
  • The header element uses an ng-include directive that specifies what template to load, in this case, the header property on the App.layout object
  • The div element has the view-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 an ng-include directive to load the footer specified on the App.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 display App.sitetitle in a heading element
  • The ng-repeat directive is used to repeat each item in the App.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 referencing App.sitecopy to display the application's copyright message
  • The second span element also contains data binding syntax to reference App.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 the App.feature object
  • Next, inside the div.marketing element, another div element is declared with the ng-repeat directive to loop for each item in the App.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:

  1. It will compile all projects scripts in the app/scripts directory to the .tmp/scripts directory.
  2. Then, invoke the Karma that will launch Chrome and start the runner.
  3. 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, and Posts services
  • The name property is set on the scope with the value of Posts
  • The posts property is set on the scope to the value returned from the Posts.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, where id 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 named Controller: PostsCtrl
  • The beforeEach method handles loading the module named learningYeomanCh3App for testing
  • The PostsCtrl, scope, and location private variables are declared and will be set inside another beforeEach method
  • The beforeEach method runs before each spec defined below; inside the beforeEach method, we use the inject method to load the proper Angular services that will be referenced by the private variables defined above
  • The first it block expects that the scope.name property matches 'Posts'
  • The second it block handles testing the URL location when the add() method is invoked
  • The third it block handles testing the URL location when the view(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 $resource service
  • The factory service returns a $resource instance with a custom update method that changes the request method to PUT
  • 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 named Service: Post
  • The beforeEach method handles loading the module named learningYeomanCh3App for testing
  • The second beforeEach method injects the service for the spec, which is set to the local Post variable
  • The it block expects that the Posts service should fetch a list of posts
  • Inside the function, the httpBackend.expectGET method is used to expect a GET 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 to null because the service should fulfill the promise with the returned data
  • The expect method is used to ensure whether the posts variable is null 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 the posts 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 the learningYeomanCh3App 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 named Filter: markdown
  • The beforeEach method handles loading the module named learningYeomanCh3App 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 the learningYeomanCh3App 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 text Loading...
  • The restrict property is set to EA, which informs Angular how to allow only an element or attribute to use this directive
  • The replace property is set to true, which informs Angular to replace the content with the content defined in the template 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 the loading class added and is hidden using the jQuery fadeOut method
  • Then, the $rootScope.$on method will bind to the $locationChangeStart event dispatched by the router and show the element using the fadeIn method
  • Then, the $rootScope.$on method will bind to the $locationChangeSuccess event dispatched by the router and hide the element using the fadeOut 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 named Directive: loading
  • The beforeEach method handles loading the module named learningYeomanCh3App 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 the Loading... 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 be Loading...

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 the ng-click directive that will invoke the add() method defined on the scope
  • Then the ul element is declared with a class of list-unstyled to modify the default browser styles
  • The inner li element is declared using the ng-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 the ng-include directive that will load the template into the content
  • Then, the section element is used with the ng-bind-html directive set to the value of the post.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 the app/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 the view(id) method on the scope, passing the id 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 the edit(id) method on the scope
主站蜘蛛池模板: 龙门县| 噶尔县| 锦州市| 巩留县| 瑞金市| 桃园县| 巴里| 镇康县| 含山县| 通榆县| 栖霞市| 信丰县| 海林市| 塘沽区| 佛坪县| 洛隆县| 焉耆| 曲靖市| 出国| 芒康县| 益阳市| 舟山市| 肃南| 肇庆市| 侯马市| 江达县| 静安区| 东兰县| 葫芦岛市| 游戏| 靖安县| 武宣县| 米泉市| 肥乡县| 桓台县| 阳朔县| 剑河县| 乌兰县| 宝山区| 武威市| 克山县|