跳至主要内容

Getting started with Angular 1.5 and ES6: part 1

Getting started

Gulp Angular generator is simple and stupid, but it does not embrace Angular 1.5 completely now. And I would like use Webpack in the new Angular 1.x project.
AngularClass provides a very simple mininal NG6-starter project with Webpack support.

Create project

Clone a copy directly into your local disk.
git clone https://github.com/AngularClass/NG6-starter <your project name>
Enter the project root folder, let's have a glance at the proejct structure.
NG6 Starter
The client holds all source codes of this project.
Under client, there is index.html file which is the entry of this application, and also inlcudes two folders: common and components.
The common folder is the common place to store services, components, driectives etc which can be shared for the whole application scope.
And components folder is the place to save all page oriented components files.
Firstly you have to install all dependencies. Execute the following command in the project root folder.
npm install
Try to run gulp serve to run this application immediately. Anytime you can navigate to http://localhost:3000 to play the running application.
By default, NG6-stater also provides a simple Gulp task(gulp component) to generate components quickly.
Execute gulp component in the root folder to generate some components for further development use.
gulp component posts
gulp component signin
gulp component signup
It will generate three folders(posts, signin, signup) in the components folder under client/app.
Each component specific folder includes serveral files. As an example, let's have a look at posts folder.
  • posts.js is the entry js file of posts component.
  • posts.component.js is the component definition file.
  • posts.controller.js is the controller class for posts component.
  • posts.styl is the component specific style file, it uses Stylus.
  • posts.html is the template file of posts component.
  • posts.spec.js is the testing spec file for posts component.

Reorganize the source codes

Follow this Angular style guide, which describes ES6 and Angular 1.5 component especially.
ES6 module is easy to origanise the source codes. It could autoload index.js in folders and no need to specify index in the path. eg.
import CommonModule from './common/';  
It will search the index.js file in common folder and load it.
I would like change the file name of all entry files to index.js. Finally the project file structure should look like(only show index.js files).
|--common
 --index.js
   |--components
    --index.js
     |--navbar
      --index.js
|--components
 --index.js
   |--posts
    --index.js
In every index.js file, it defines an Angular modlule.
For example, the index.js in common/components/navbar defines an Angular Module named navbar(to avoid naming conflict, I changed module name to app.common.components.navbar).
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import navbarComponent from './navbar.component';

let navbarModule = angular.module('app.common.components.navbar', [
  uiRouter
])

.component('navbar', navbarComponent)

.name;

export default navbarModule;
And in the common/components/index.js file, navbar is imported, and it defines a new Angular Module which depends on this navbar module.
import angular from 'angular';
import Navbar from './navbar/';
//...

let commonComponentsModule = angular.module('app.common.components', [
  Navbar,
    ...
])

.name;

export default commonComponentsModule;
And in the common/index.js file, commonComponentsModule is imported, a new Angular Module is defined.
import angular from 'angular';
import commonComponentsModule from './components/';
//...

let commonModule = angular.module('app.common', [
  commonComponentsModule,
//...
])
.name;

export default commonModule;
Thus the Angular module definition becomes clear, and from top to down, it looks like a tree structure.
App
|--Common
  |--Components
     |--Navbar
By the way, I also want to do some clean work on the app.js.
Extract the content of app.constant(), app.run(), app.config() from app.js into standalone files.
app.constants.js:
const AppConstants = {
  appName: "Angular ES6 Sample",
  jwtKey: "id-token",
  api: 'http://localhost:8080/blog-api-cdi/api'
};

export default AppConstants;
app.run.js:
import * as vis from 'ui-router-visualizer';

function AppRun(Auth, $rootScope, $state, $trace, $uiRouter, $transitions) {
  "ngInject";

 //...

};

export default AppRun;
app.config.js:
function AppConfig($logProvider, toastrConfig, $httpProvider, $stateProvider, $locationProvider, $urlRouterProvider) {
  'ngInject';

  // Enable log
  $logProvider.debugEnabled(true);

  /*
    If you don't want hashbang routing, uncomment this line.
    Our tutorial will be using hashbang routing though :)
  */
  // $locationProvider.html5Mode(true);
  $locationProvider.html5Mode(true).hashPrefix('!');

  $stateProvider
    .state('app', {
      abstract: true,
      component: 'app'
    });

  $urlRouterProvider.otherwise('/');
}

export default AppConfig;
Finally imports these files in app.js, it looks like:
import 'jquery';
import 'tether';
import 'bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'font-awesome/css/font-awesome.min.css';
import angular from 'angular';
import toastr from 'angular-toastr';
import 'angular-toastr/dist/angular-toastr.css';
import 'angular-messages';
import 'angular-animate';
import 'angular-touch';
import uiRouter from 'angular-ui-router';
import Common from './common/';
import Components from './components/';
import AppComponent from './app.component';
import AppRun from './app.run';
import AppConstants from './app.constants';
import AppConfig from './app.config';


const requires = [
  'ngTouch',
  'ngMessages',
  'ngAnimate',
  toastr,
  uiRouter,
  Common,
  Components
];

angular.module('app', requires)
  .component('app', AppComponent)
  .constant('AppConstants', AppConstants)
  .config(AppConfig)
  .run(AppRun);
As you see, it looks more clear now.
You could have noticed I have added some extra resources into this project, such as Bootstrap, FontAwesome etc.

Add extra resources

By default, the NG6-starter repository includes angular(from official AngularJS) and angular-ui-router(from Angular UI team).
Install other Angular NPM packages into this project.
npm install --save angular-messages angular-touch angular-animate
Install angular-toastr which is toastr integration for Angular. We will use it raise notification messsages to client when we perform some actions.
npm install --save angular-toastr
Install Bootstrap and FontAwesome.
npm install --save font-awesome bootstrap@4.0.0-alpha4 jquery tether
We use the latest Bootstrap 4.0 here, currently it is still in active development. So maybe some breaking changes will be included in future.
If you encounter Bootstrap errors like "Bootstrap requires JQuery" etc. when run this project, even you have import them in the app.js file, try to add the following configuration into webpack.config.file to overcome this issue.
plugins:[

    new ProvidePlugin({
      jQuery: 'jquery',
      $: 'jquery',
      jquery: 'jquery',
      "Tether": 'tether',
      "window.Tether": 'tether'
    }),
    ...
Another issue you could see is the css font file loading errors.
Install webpack plugins: css-loader, file-loader and url-loader.
npm install --save-dev css-loader file-loader url-loader
Declare these loaders in webpack.config.file.
module: {
    loaders: [
        ...
        { test: /\.css$/, loader: 'style!css' },
        { test: /\.(png|woff|woff2|eot|ttf|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'url-loader?limit=100000' }
Till now, we have added essential resources into this project.

Component

We have created several components in before steps.
In Angular 1.5, a component can be defined as an object and register it via angular.component().
Check the content of posts.component.js file. It define an object named postsComponent:
import template from './posts.html';
import controller from './posts.controller';
import './posts.styl';

let postsComponent = {
  restrict: 'E',
  bindings: {},
  template,
  controller
};

export default postsComponent;
It is registered in index.js file.
let postsModule = angular.module('posts', [commonSevices, uiRouter])
  .component('posts', postsComponent)
  .name;
Different from the previous version, in Angular 1.5, controllers and templates are part of components.
The controller is still responsive for handling events and serving data bindings for template.
class PostsController {
  constructor() {
    'ngInject';

    this.name = 'posts';
    this.q = "";
    this.posts = [];
  }

  $onInit() {
    console.log("initializing Posts...");
    this.posts = [
       { id: 1, title: 'Getting started with REST', content: 'Content of Getting started with REST', createdAt: '9/22/16 4:15 PM' },
       { id: 2, title: 'Getting started with AngularJS 1.x', content: 'Content of Getting started with AngularJS 1.x', createdAt: '9/22/16 4:15 PM' },
       { id: 3, title: 'Getting started with Angular2', content: 'Content of Getting started with Angular2', createdAt: '9/22/16 4:15 PM' },
    ]
  }

  $onDestroy() {
    console.log("destroying Posts...");
  }

  search() {
    console.log("query posts by keyword" + this.q);
  }
}

export default PostsController;
In concept, Angular 1.5 component is very close to Angular 2, which make upgrading to Angular 2 looks more smooth. An component has several lifecycle hooks, such as $onInit, $onChange, $onDestroy, $postLink etc.
Let's have a look at posts template file: posts.html.
<div class="card">
  <div class="card-block bg-faded">
    <div class="row">
      <div class="col-md-9">
        <form class="form-inline" ng-submit="$ctrl.search()">
          <div class="form-group">
            <input type="text" name="q" class="form-control" ng-model="$ctrl.q" />
          </div>
          <button type="submit" class="btn btn-outline-info">{{'search'}}</button>
        </form>
      </div>
      <div class="col-md-3">
        <span class="pull-md-right">
          <a href="#" class="btn btn-success" ui-sref="app.new-post">new-post</a>
        </span>
      </div>
    </div>
  </div>
</div>

<div class="row">
  <div class="col-md-6" ng-repeat="post in $ctrl.posts ">
    <div class="card card-block">
      <h4 class="card-title">{{post.title}}</h4>
      <h6 class="card-subtitle text-muted">{{post.createdAt}}</h6>
      <p class="card-text">{{post.content}}</p>
      <div class="text-md-right">
        <a href="# " ui-sref="app.edit-post({id: post.id})">edit</a>
        <a href="# " ui-sref="app.view-post({id: post.id})">view</a>
      </div>
    </div>
  </div>
</div>
In posts template file, the controller is alias as $ctrl by default. You can change it by specifying a controllerAs property of component.
let postsComponent = {
  //...
  controllerAs:'myCtrl'
};
In order to run project and preview the result of posts compoent in browser. You have to configure routing of posts component.

Route

In this project, we use angular-ui-router instead of the Angular official router. It is more powerful and provides more features.
For example:
  1. It contains a state machine to manage routings.
  2. It supports nested multi-views.
app component is the root component of this application.
In the app template file: app.html, insert a ui-view directive.
<navbar></navbar>
<div class="page">
  <div class="container">
    <div ui-view></div>
  </div>
</div>
And we defines state of app in app.config.js.
$stateProvider
    .state('app', {
      abstract: true,
      component: 'app'
    });

 $urlRouterProvider.otherwise('/');
And defines posts state in components/posts/index.js.
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import postsComponent from './posts.component';

let postsModule = angular.module('posts', [uiRouter])
  .config(($stateProvider) => {
    "ngInject";
    $stateProvider
      .state('app.posts', {
        url: '/posts',
        component: 'posts'
      });
  })
  .component('posts', postsComponent)
  .name;

export default postsModule;
app component route is declared as abstract, there is an abstract property set to true. posts route name is start with app., which means will be inherited from app. And the posts template view will be rendered as content of the ui-view diretive defined in app template.
Now try to run this project in browser.
Entry root folder, execute the following command.
gulp serve
Try to navigate to http://localhost:3000. You will see the screen like the following.
Posts list
Repeat gulp component command and add more components, such as new-post, edit-post, post-detail, and move the generated files in compoents/posts folder. Do not care about the content of them, we will implement them later.
Add route config in compoents/posts/index.js file.
//...
import postDetailComponent from './post-detail.component';
import newPostComponent from './new-post.component';
import editPostComponent from './edit-post.component';

let postsModule = angular.module('posts', [commonSevices, uiRouter])
  .config(($stateProvider) => {
    "ngInject";
    $stateProvider
      //...
      .state('app.view-post', {
        url: '/post-detail/:id',
        component: 'postDetail'
      })
      .state('app.edit-post', {
        url: '/edit-post/:id',
        component: 'editPost'
      })
      .state('app.new-post', {
        url: '/new-post',
        component: 'newPost'
      });
  })
  //...
  .component('postDetail', postDetailComponent)
  .component('newPost', newPostComponent)
  .component('editPost', editPostComponent)
  .name;

export default postsModule;
We have added route link in posts.html.
For example:
<a href="#" class="btn btn-success" ui-sref="app.new-post">new-post</a>
<a href="# " ui-sref="app.edit-post({id: post.id})">edit</a>
<a href="# " ui-sref="app.view-post({id: post.id})">view</a> 
ui-sref directive accepts a state name and state params.
If the application is running, it should be sync with browser by default. Try to navigate to new-post, edit-post, post-detail pages by click these links.
Try to add posts and new-post link into the navbar component.
Modify the common/compoents/navbar/navbar.html.
<nav class="navbar navbar-fixed-top navbar-light bg-faded" style="background-color: #e3f2fd;">
  <div class="container">
    <a class="navbar-brand" ui-sref="app.home" href="#"><i class="fa fa-home"></i>ANGULAR ES6</a>
    <button class="navbar-toggler hidden-sm-up" type="button" data-toggle="collapse" data-target="#exCollapsingNavbar" aria-controls="exCollapsingNavbar2" aria-expanded="false" aria-label="Toggle navigation">
    &#9776;
  </button>
    <!-- Collect the nav links, forms, and other content for toggling -->
    <div class="collapse navbar-toggleable-xs" id="exCollapsingNavbar">

      <ul class="nav navbar-nav">
        <li class="nav-item" ui-sref-active="active"><a class="nav-link" href="#" ui-sref="app.posts">{{'posts'}}</a></li>
        <li class="nav-item" ui-sref-active="active"><a class="nav-link" href="#" ui-sref="app.new-post">{{'new-post'}}</a></li>
        <li class="nav-item" ui-sref-active="active"><a class="nav-link" href="#" ui-sref="app.about">{{'about'}}</a></li>

      </ul>

      <!-- /.navbar-collapse -->
    </div>
  </div>
  <!-- /.container-fluid -->
</nav>
ui-sref-active will add class active to the element when route is activated.
navbar
Angular UI Router provides some tools to track the route change.
Add the following codes into app.run.js to activate transition track.
  $trace.enable('TRANSITION');
You will the state transition info in browser console when state is changing.
navbar
With help of ui-router-visualizer, you can explore the state tree in a visual graph.
npm install --save ui-router-visualizer
Add the following codes to app.run.js.
import * as vis from 'ui-router-visualizer';

//...
vis.visualizer($uiRouter);
The visual graph will be displayed at the bottom of the page.
navbar

Source codes

Check the sample codes.
发表评论

此博客中的热门博文

JPA 2.1: Attribute Converter

JPA 2.1: Attribute Converter If you are using Hibernate, and want a customized type is supported in your Entity class, you could have to write a custom Hibernate Type. JPA 2.1 brings a new feature named attribute converter, which can help you convert your custom class type to JPA supported type. Create an Entity Reuse thePostentity class as example. @Entity @Table(name="POSTS") public class Post implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name="ID") private Long id; @Column(name="TITLE") private String title; @Column(name="BODY") private String body; @Temporal(javax.persistence.TemporalType.DATE) @Column(name="CREATED") private Date created; @Column(name="TAGS") private List<String> tags=new ArrayList<>(); } Create an attribute converter In this example…

Spring Data Mongo: bridge MongoDB and Spring

Spring Data Mongo: bridge MongoDB and SpringMongoDBis one of the most popular NoSQL products, Spring Data Mongo(Maven archetype id is spring-data-mongodb) tries to provides a simple approach to access MongoDB. Configuration Add the following code fragments in your Spring configuration. <!-- Mongo config --> <mongo:db-factory id="mongoDbFactory" host="localhost" port="27017" dbname="conference-db" /> <bean id="mongoTemplate" class="org.springframework.data.mongodb.core.MongoTemplate"> <constructor-arg ref="mongoDbFactory" /> </bean> <mongo:repositories base-package="com.hantsylabs.example.spring.mongo" /> Firstly, declare aDbFactorywhich is responsible for connecting to the MongoDB server. Then, register theMongoTemplateobject which envelop the data operations on MongoDB. Lastly addmongo:repositorieswith an essentialbase-packageattribute, Spring will discovery your d…

Auditing with Hibernate Envers

Auditing with Hibernate Envers The approaches provided in JPA lifecyle hook and Spring Data auditing only track the creation and last modification info of an Entity, but all the modification history are not tracked. Hibernate Envers fills the blank table. Since Hibernate 3.5, Envers is part of Hibernate core project. Configuration Configure Hibernate Envers in your project is very simple, just need to addhibernate-enversas project dependency. <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-envers</artifactId> </dependency> Done. No need extra Event listeners configuration as the early version. Basic Usage Hibernate Envers provides a simple@Auditedannotation, you can place it on an Entity class or property of an Entity. @Audited private String description; If@Auditedannotation is placed on a property, this property can be tracked.