首页 > 代码库 > 迈向angularjs2系列(2):angular2组件和指令详解

迈向angularjs2系列(2):angular2组件和指令详解

<%= INIT %>

 

内容

一:angular2 helloworld!

 为了简单快速的运行一个ng2的app,那么通过script引入预先编译好的angular2版本和页面的基本框架。

index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
     <app></app>
     <!--使用app组件-->
     <script src="https://code.angularjs.org/2.0.0-beta.9/angular2-polyfills.min.js">

     </script>
     <script src="https://code.angularjs.org/2.0.0-beta.9/Rx.umd.min.js">

     </script>
     <script src="https://code.angularjs.org/2.0.0-beta.9/angular2-all.umd.min.js">

     </script>
     <script src="./app.js"></script>

</body>
</html>

app.js:

var App=ng.core.Component({
    //定义了名称为App的组件。
    selector:"app",
    //匹配所有的app标签
    template:"<h1>hello  {{target}}</h1>"
})
.Class({
    //Class函数传递了一个对象字面量,只有constuctor方法
    constructor:function(){
        this.target="world";
    }
});
ng.platform.browser.bootstrap(App);
//ng.platform.browser是命名空间

我们看到angularjs2的代码并没有用到typescript,所以它不是必须的,但ng2强烈推荐使用。这里没有用到任何模块加载或者包管理器,那么接下来继续学习。

二: 配置开发环境和angular2的typescript实现

1.通过git克隆项目

step1:安装git

到网站下载exe文件,https://github.com/git-for-windows/git/releases/download/v2.13.2.windows.1/Git-2.13.2-64-bit.exe。安装的过程我除了改了一下安装目录之外。

安装目录:

技术分享

检测git是否安装正确:

首先在开始菜单里找到git cmd(也可以吧快捷方式发送到桌面),然后输出命令git --version。

技术分享

step2:进入自己的项目目录,运行命令 git clone https://github.com/mgechev/switching-to-angular2.git 。

技术分享

技术分享

已经克隆下来。good!

step3:模块安装和启动开发用的Server。

$ npm install ;
$ npm start;

浏览器显示结果为:

技术分享

success

step4:上手试玩(optional)

内容替换,switching-to-angular2/app/ch4/ts/hello-world/app.ts:

import {Component} from ‘@angular/core‘;
import {bootstrap} from ‘@angular/platform-browser-dynamic‘;

@Component({
  selector: ‘app‘,
  templateUrl: ‘./app.html‘
})
class App {
  target:string;
  constructor() {
    this.target = ‘world‘;
  }
}

bootstrap(App);

浏览器显示结果为:

技术分享

2.hello-world深度解析

首先,看一下一共有4个文件。

技术分享

index.html负责hello-world的首页(默认)文件的显示,app.html负责app模板的内容,meta.json是一些元信息,app.ts负责js代码逻辑。

接下来详细看index.html。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title><%= TITLE %></title>
  <!--TITLE变量注入-->
  <meta name="description" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- inject:css -->
  <!-- endinject -->
</head>
<body>

  <app>Loading...</app>
  <!--app组件-->
  <!-- inject:js -->
  <!-- endinject -->
  <%= INIT %>
  <!--INIT是变量注入-->
</body>
</html>

app标签有文本内容,"Loading"一直处于可见状态,直到应用启动好、主组件渲染完毕为止。而 <%= TITLE %> 和 <%= INIT %> 是用来注入变量的。这些变量是在哪里定义的呢?

 ,传授一个全局搜索文本的方法:

step1:文件目录右键,选择find in path

技术分享

step2:搜索"TITLE"。

技术分享

所以,它的定义在switching-to-angular2/tools/tasks的build.index.ts文件中。

build.index.ts:

技术分享
import {join, sep} from ‘path‘;
import {APP_SRC, APP_DEST, DEPENDENCIES, SYSTEM_CONFIG, ENV} from ‘../config‘;
import {transformPath, templateLocals} from ‘../utils‘;

export = function buildIndexDev(gulp, plugins) {
  return function () {
    return gulp.src(join(APP_SRC, ‘**‘, ‘index.html‘))
      // NOTE: There might be a way to pipe in loop.
      .pipe(inject())
      .pipe(plugins.template(
        require(‘merge‘)(templateLocals(), {
          TITLE: ‘Switching to Angular 2‘,
          INIT: `
<script>
  System.config(${JSON.stringify(SYSTEM_CONFIG)});
  System.import("./app")
    .catch(function () {
      console.log("Report this error to https://github.com/mgechev/switching-to-angular2/issues", e);
    });
</script>`
        })
      ))
      .pipe(gulp.dest(APP_DEST));
  };


  function inject() {
    return plugins.inject(gulp.src(getInjectablesDependenciesRef(), { read: false }), {
      transform: transformPath(plugins, ‘dev‘)
    });
  }

  function getInjectablesDependenciesRef() {
    let shims = DEPENDENCIES.filter(dep => dep[‘inject‘] && dep[‘inject‘] === ‘shims‘);
    let libs = DEPENDENCIES.filter(dep => dep[‘inject‘] && dep[‘inject‘] === ‘libs‘);
    let all = DEPENDENCIES.filter(dep => dep[‘inject‘] && dep[‘inject‘] === true);
    return shims.concat(libs).concat(all).map(mapPath);
  }

  function mapPath(dep) {
    let prodPath = join(dep.dest, dep.src.split(sep).pop());
    return (‘prod‘ === ENV ? prodPath : dep.src );
  }
};
build.index.ts

第三,分析一下ch4/ts/hello-world/app.ts。

import {Component} from ‘@angular/core‘;
//导入了component装饰器
import {bootstrap} from ‘@angular/platform-browser-dynamic‘;
//导入了bootstrap函数
@Component({
  //Component装饰了class App
  selector: ‘app‘,
  templateUrl: ‘./app.html‘
  //对象字面量参数和ES5类似,app选择器和视图内容。模板既可以template内联,也可以使用url,和ng1类似。
})
class App {
  target:string;
  constructor() {
    this.target = ‘world‘;
  }
}
bootstrap(App);
//启动APP

四: angular2指令详解

开发app组件现在开始了!

1.基础to-do list应用

(1)进入代码:

进入ch4/ts/ng-for/detailed-syntax目录,一共4个部分。

app.ts:

import {Component} from ‘@angular/core‘;
import {bootstrap} from ‘@angular/platform-browser-dynamic‘;
//导入
@Component({
  //装饰器
  selector: ‘app‘,
  templateUrl: ‘./app.html‘,
})
class App {
  todos:string[];
  name:string;
  constructor() {
    //app类的属性定义,可以直接在app组件的代码里使用
    this.name = "爱莎";
    this.todos = [‘呼风唤雪‘, "保护妹妹"];
  }
}

bootstrap(App);
//启动应用

app.html:

<h1>你好,{{name}}!</h1>
<h3>
  to-do清单:
</h3>
<ul>
  <template ngFor let-todo [ngForOf]="todos">
    <li>{{todo}}</li>
  </template>
</ul>

说明:template标签内部可以放HTML片段,那么屏蔽了浏览器的直接渲染,而接下来由模板引擎来处理。

index.html:

技术分享
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title><%= TITLE %></title>
  <meta name="description" content="">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- inject:css -->
  <!-- endinject -->
</head>
<body>

  <app>Loading...</app>
  <!-- inject:js -->
  <!-- endinject -->
  <%= INIT %>
</body>
</html>
默认页面类似之前DEMO的内容

meta.json:

技术分享
{
  "title": "List of items (detailed syntax)",
  "description": "List of items using explicit template for ng-for",
  "id": 3,
  "presented": true
}
View Code

打开http://localhost:5555/dist/dev/ch4/ts/ng-for/detailed-syntax/地址,显示如下:

技术分享

2. 更加优秀的ngFor指令

 ngFor指令用来遍历集合中的元素,它的行为和ng1中的ng-repeat一样。它有ngForOf属性以及更多的语法特性。

<template ngFor let-todo [ngForOf]="todos">
    <li>{{todo}}</li>
  </template>

(1)优秀一:更简单,提升了指令的语义性。

ng1的指令用法五花八门,需要对各种属性值有深入的理解。而ng2引入了更加简单的约定。

属性有3种用法:

●propertyName="value"

●[propertyName]="expression"

●(eventName)="handlerFunc()"

第一种语法:普通字面量

propertyName接收字符串字面量,angular不会对它进行进一步处理。

第二种语法:方括号语法

提示angular2要当做表达式来处理。属性包围在方括号里面,angular就会尝试执行这个表达式,执行上下文就是模板对应的组件。

第三种语法:小括号语法

当然是事件绑定咯。

可以看到,谁是谁,非常清晰。

(2)模板内部的变量(var-XX)

 <template ngFor let-todo [ngForOf]="todos"> [ngForOf]的值作为表达式执行之后会返回一个集合,通过var-todo语法告诉angular需要创建的新变量叫做todo

(3)使用语法糖

语法糖简单的说就是更方便更简洁的写法。

使用星号,扔掉templatebiaoqian ,指令也直接用到容器标签上。

<ul>
  <li *ngFor="#todo of todos">{{todo}}</li>
</ul>

angular2会自动对上面代码进行“脱糖”处理,就能变成上面比较啰嗦的形式了。

3.自定义angular2指令

 前面小节看了如何在DOM标签上使用指令,指令把DOM标签参数化了,而这一节讲自定义指令。自定义才是高级玩法。

本小节将构建一个简单的tooltip自定义指令。

(1)进入代码:

ch4/ts/tooltip/app.html:

<div saTooltip="Hello world!">
</div>

ch4/ts/tooltip/app.ts:

一共分为导入、Overlay类、指令、常规的APP类定义。

//导入
import {HostListener, Input, Injectable, ElementRef, Inject, Directive, Component} from ‘@angular/core‘;

import {bootstrap} from ‘@angular/platform-browser-dynamic‘;

//Overlay类的定义
class Overlay {
  private el: HTMLElement;
  constructor() {
    var el = document.createElement(‘div‘);
    el.className = ‘tooltip‘;
    this.el = el;
  }
  close() {
    this.el.hidden = true;
  }
  open(el, text) {
    this.el.innerHTML = text;
    this.el.hidden = false;
    var rect = el.nativeElement.getBoundingClientRect();
    this.el.style.left = rect.left + ‘px‘;
    this.el.style.top = rect.top + ‘px‘;
  }
  attach(target) {
    target.appendChild(this.el);
  }
  detach() {
    this.el.parentNode.removeChild(this.el);
  }
}

class OverlayMock {
  constructor() {}
  close() {}
  open(el, text) {}
  attach(target) {}
  detach() {}
}
//指令定义
@Directive({
  selector: ‘[saTooltip]‘
})
export class Tooltip {
  @Input()
  saTooltip:string;

  constructor(private el: ElementRef, private overlay: Overlay) {
    this.overlay.attach(el.nativeElement);
  }
  @HostListener(‘mouseenter‘)
  onm ouseEnter() {
    this.overlay.open(this.el, this.saTooltip);
  }
  @HostListener(‘mouseleave‘)
  onm ouseLeave() {
    this.overlay.close();
  }
}
//APP类定义,并被component装饰器修饰以及最后的启动
@Component({
  selector: ‘app‘,
  templateUrl: ‘./app.html‘,
  providers: [Overlay],
  directives: [Tooltip]
})
  
class App {}

bootstrap(App);

打开http://localhost:5555/dist/dev/ch4/ts/tooltip/地址,那么会有如下结果:

技术分享

(2)导入的类HostListener、Directive、ElementRef

app.ts导入了HostListener, Input, Injectable, ElementRef, Inject, Directive, Component这些类。

●HostListener(eventname)

用于事件处理。指令实例化的时候,angular2会把这个装饰过的方法当成宿主标签上对应eventname的事件处理函数。

@HostListener(‘mouseenter‘)
  //定义监听器
  onm ouseEnter() {
    this.overlay.open(this.el, this.saTooltip);
  }
  //定义监听函数

●Directive

用于定义指令。用来添加额外的元数据。

//指令定义
@Directive({
  selector: ‘[saTooltip]‘
  //大小写敏感哦
})

●ElementRef

在宿主标签里注入其他标签的引用,不仅仅是DOM。比如这里的模板就是angular包装过的div标签,里面有tooltip属性。

<div saTooltip="Hello world!">
</div>

(3)指令输入的input装饰器

接收的参数是需要绑定的属性名。如果不传递参数,,默认绑定到同名属性上。

注意:angular的HTML编译器对大小写敏感。

 我们来看一下input的构造器到底干了什么?

@Input()
    //给指令定义一个saTooltip属性,属性的值是表达式。
  saTooltip:string;

(4)constructor构造器的理解

constructor(private el: ElementRef, private overlay: Overlay) {
    //overloay是定义的overlay类
    //我们来看一下ElementRef究竟是什么
    console.log(el);
    this.overlay.attach(el.nativeElement);
  }

●私有属性el

首先,看一下el,也就是ElementRef是什么。根据打印结果,ElementRef就是app.html模板里包装好的div元素。

技术分享

再来看attach。这行代码负责刚刚的原生div元素添加内容。

attach(target) {
    target.appendChild(this.el);
    //target是原生的div,给它添加子元素,内容是overloay定义的this.el
  }

●私有属性overlay

overlay的类型为Overlay,也就是定义的class类。Overlay类实现了维护tooltip组件外观的逻辑,并可以用angular的DI依赖注入,当然要加上顶层组件Component的定义。

ch4/ts/tooltip/app.ts:

@Component({
  selector: ‘app‘,
  templateUrl: ‘./app.html‘,
  providers: [Overlay],//数组哦,实现Overlay的依赖注入
  directives: [Tooltip]
})

(5)封装指令要在顶层组件声明。

注意:声明的是数组哦。整个组件的内部全部指令都会声明在这里。尽管显式声明有些麻烦,但能带来更好的封装性。而在ng1种所有指令都在全局命名空间中,坏处是容易命名冲突。同时我们知道了组件用了哪些指令,更好理解了。

@Component({
//component装饰器,传递对象字面量作为参数
  selector: ‘app‘,
  templateUrl: ‘./app.html‘,
  providers: [Overlay],
  directives: [Tooltip]//声明指令数组,angular编译器就可以发现tooltip指令了
})

class App {} 

五: 组件详解

todo组件分为导入、接口定义、顶层组件、控制器、启动5个部分。

app.ts:

//导入
import {Component} from ‘@angular/core‘;
import {bootstrap} from ‘@angular/platform-browser-dynamic‘;

//接口定义
interface Todo {
  completed: boolean;
  label: string;
}

//顶层组件
@Component({
  selector: ‘app‘,
  templateUrl: ‘./app.html‘,
  styles: [
    `ul li {
      list-style: none;
    }
    .completed {
      text-decoration: line-through;
    }`
  ]
})
//控制器
class TodoCtrl {
  todos: Todo[] = [{
    label: ‘Buy milk‘,
    completed: false
  }, {
    label: "Save the world",
    completed: false
  }];
  name: string = ‘John‘;
  addTodo(label) {
    this.todos.push({
      label,
      completed: false
    })
  }
  removeTodo(idx) {
    this.todos.splice(idx, 1);
  }
  toggleCompletion(idx) {
    let todo = this.todos[idx];
    todo.completed = !todo.completed;
  }
}
//启动
bootstrap(TodoCtrl);

1.组件样式

我们直接在组件装饰器加入styles属性。

  app/ch4/ts/todo-app/app.ts的样式代码:

@Component({
  selector: ‘app‘,
  templateUrl: ‘./app.html‘,
  styles: [
    `ul li {
      list-style: none;
    }
    .completed {
      text-decoration: line-through;
    }`
   //styles属性,值为数组。
  ]
})

2.组件的控制器

app.ts控制器代码:

//控制器
class TodoCtrl {
  todos: Todo[] = [{
    label: ‘呼风唤雨‘,
    completed: false
  }, {
    label: "保护妹妹",
    completed: false
  }];
  //todos数组。包含两个元素,每个元素的变量指定为Todo。
  name: string = ‘爱莎‘;
  //name属性
  addTodo(label) {
    this.todos.push({
      label,
      completed: false
    })
  }
  //用于添加元素
  removeTodo(idx) {
    this.todos.splice(idx, 1);
  }
  //用于移除元素
  toggleCompletion(idx) {
    let todo = this.todos[idx];
    todo.completed = !todo.completed;
  }
  //切换完成或者未完成
}

模板app.html内容:

<h1>你好 {{name}}!</h1>

<p>
  添加todo:
  <input #newtodo type="text">
  <button (click)="addTodo(newtodo.value); newtodo.value = http://www.mamicode.com/‘‘">Add</button>
</p>

<p>to-do清单:</p>

<ul>
  <!--遍历todos数组-->
  <li *ngFor="let todo of todos; let index = index" [class.completed]="todo.completed">
    <input type="checkbox" [checked]="todo.completed"
      (change)="toggleCompletion(index)">
    {{todo.label}}
  </li>
</ul>

 <li *ngFor="let todo of todos; let index = index" [class.completed]="todo.completed"> 这里使用了index索引。

● (change)="toggleCompletion(index)" 绑定了toggleCompletion函数到change事件上。

● [checked]="todo.completed" 绑定了控制器的todos数据的completed到checked属性上。

   [class.completed]="todo.completed" 意思是如果todo.completed为true,那么添加completed类。angular也允许绑定style和attribute的。

最后启动app的时候,要把控制器传进去。

//启动
bootstrap(TodoCtrl);

打开http://localhost:5555/dist/dev/ch4/ts/todo-app/,结果为

技术分享

3.组件的用户交互

实现toggleCompletion方法。它是控制器里定义的,所以直接使用咯。

toggleCompletion(idx) {
  let todo = this.todos[idx];
  todo.completed = !todo.completed;
}
//切换完成或者未完成的状态

实现addToDo方法,用来添加todo元素。

addTodo(label) {
    this.todos.push({
      label,
      completed: false
    })
  }
  //用于添加元素

通过button按钮添加元素如图:

技术分享

4.app组件的切分

先来看一下指令API的输入输出。可以把指令接受的属性看成输入。把指令触发的事件看成输出。使用第三方库的指令时最主要的关注点就是输入和输出,这些内容构成了指令的API。

如果把todo应用切分成多个独立的组件,组件之间存在交互,那么分析一下这个todo应用咯。

技术分享

最外层的方框代表整个todo应用。内部的第一个方框有一个组件,用来新建todo项目。下面的方框用来存储所有的项目。

根据分析,我们可以定义3个组件:

●TodoApp

●InputBox

●TodoList

5.InputBox组件:关注输入和输出

 

迈向angularjs2系列(2):angular2组件和指令详解