第一个Angular应用 #

一、应用概述 #

我们将创建一个简单的待办事项(Todo)应用,学习Angular的核心概念:

  • 组件创建
  • 数据绑定
  • 事件处理
  • 条件渲染
  • 列表渲染

二、创建项目 #

bash
ng new todo-app --style=css --routing=false --skip-tests
cd todo-app
ng serve

三、创建Todo组件 #

3.1 生成组件 #

bash
ng g c todo

3.2 定义数据模型 #

创建 src/app/todo/todo.model.ts

typescript
export interface Todo {
  id: number;
  title: string;
  completed: boolean;
  createdAt: Date;
}

3.3 组件逻辑 #

编辑 src/app/todo/todo.component.ts

typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { Todo } from './todo.model';

@Component({
  selector: 'app-todo',
  standalone: true,
  imports: [CommonModule, FormsModule],
  templateUrl: './todo.component.html',
  styleUrl: './todo.component.css'
})
export class TodoComponent {
  todos: Todo[] = [];
  newTodoTitle: string = '';
  nextId: number = 1;

  addTodo() {
    if (this.newTodoTitle.trim()) {
      const newTodo: Todo = {
        id: this.nextId++,
        title: this.newTodoTitle.trim(),
        completed: false,
        createdAt: new Date()
      };
      this.todos.push(newTodo);
      this.newTodoTitle = '';
    }
  }

  toggleTodo(todo: Todo) {
    todo.completed = !todo.completed;
  }

  deleteTodo(id: number) {
    this.todos = this.todos.filter(todo => todo.id !== id);
  }

  get completedCount(): number {
    return this.todos.filter(todo => todo.completed).length;
  }

  get totalCount(): number {
    return this.todos.length;
  }
}

3.4 组件模板 #

编辑 src/app/todo/todo.component.html

html
<div class="todo-container">
  <h1>待办事项</h1>
  
  <div class="todo-input">
    <input 
      type="text" 
      [(ngModel)]="newTodoTitle" 
      placeholder="添加新任务..."
      (keyup.enter)="addTodo()"
    />
    <button (click)="addTodo()">添加</button>
  </div>

  <div class="todo-stats">
    <span>总计: {{ totalCount }}</span>
    <span>已完成: {{ completedCount }}</span>
  </div>

  <ul class="todo-list">
    <li *ngFor="let todo of todos" [class.completed]="todo.completed">
      <input 
        type="checkbox" 
        [checked]="todo.completed" 
        (change)="toggleTodo(todo)"
      />
      <span>{{ todo.title }}</span>
      <button (click)="deleteTodo(todo.id)">删除</button>
    </li>
  </ul>

  <div *ngIf="todos.length === 0" class="empty-message">
    暂无待办事项,添加一个吧!
  </div>
</div>

3.5 组件样式 #

编辑 src/app/todo/todo.component.css

css
.todo-container {
  max-width: 500px;
  margin: 50px auto;
  padding: 20px;
  border-radius: 8px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

h1 {
  text-align: center;
  color: #333;
}

.todo-input {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.todo-input input {
  flex: 1;
  padding: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 16px;
}

.todo-input button {
  padding: 10px 20px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.todo-input button:hover {
  background-color: #45a049;
}

.todo-stats {
  display: flex;
  justify-content: space-between;
  margin-bottom: 15px;
  color: #666;
}

.todo-list {
  list-style: none;
  padding: 0;
}

.todo-list li {
  display: flex;
  align-items: center;
  padding: 10px;
  border-bottom: 1px solid #eee;
}

.todo-list li.completed span {
  text-decoration: line-through;
  color: #999;
}

.todo-list li span {
  flex: 1;
  margin: 0 10px;
}

.todo-list li button {
  padding: 5px 10px;
  background-color: #f44336;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.empty-message {
  text-align: center;
  color: #999;
  padding: 20px;
}

四、更新AppComponent #

编辑 src/app/app.component.ts

typescript
import { Component } from '@angular/core';
import { TodoComponent } from './todo/todo.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TodoComponent],
  template: `
    <app-todo></app-todo>
  `,
  styles: []
})
export class AppComponent {}

五、核心概念解析 #

5.1 组件结构 #

text
TodoComponent
├── @Component装饰器    定义组件元数据
├── selector           组件选择器
├── standalone         独立组件标志
├── imports            导入依赖模块
├── templateUrl        模板文件路径
└── styleUrl           样式文件路径

5.2 数据绑定 #

html
<!-- 插值绑定 -->
<span>{{ todo.title }}</span>

<!-- 属性绑定 -->
<input [checked]="todo.completed" />

<!-- 事件绑定 -->
<button (click)="deleteTodo(todo.id)">删除</button>

<!-- 双向绑定 -->
<input [(ngModel)]="newTodoTitle" />

5.3 指令使用 #

html
<!-- *ngFor 列表渲染 -->
<li *ngFor="let todo of todos"></li>

<!-- *ngIf 条件渲染 -->
<div *ngIf="todos.length === 0"></div>

<!-- [class] 类绑定 -->
<li [class.completed]="todo.completed"></li>

六、数据绑定详解 #

6.1 插值表达式 #

html
<p>{{ totalCount }}</p>
<p>{{ 1 + 1 }}</p>
<p>{{ 'Hello' + ' Angular' }}</p>
<p>{{ getCompletedCount() }}</p>

6.2 属性绑定 #

html
<!-- 绑定属性 -->
<img [src]="imageUrl" />
<a [href]="linkUrl">链接</a>
<button [disabled]="isDisabled">按钮</button>

<!-- 绑定样式 -->
<div [style.color]="'red'">红色文字</div>
<div [style.fontSize.px]="16">16px字体</div>

<!-- 绑定类 -->
<div [class.active]="isActive">激活状态</div>
<div [class]="{'active': isActive, 'disabled': isDisabled}">多类绑定</div>

6.3 事件绑定 #

html
<!-- 点击事件 -->
<button (click)="addTodo()">添加</button>

<!-- 键盘事件 -->
<input (keyup.enter)="addTodo()" />
<input (keyup.escape)="clearInput()" />

<!-- 传递事件对象 -->
<input (input)="onInput($event)" />

<!-- 传递参数 -->
<button (click)="deleteTodo(todo.id)">删除</button>

6.4 双向绑定 #

html
<!-- 使用ngModel需要导入FormsModule -->
<input [(ngModel)]="newTodoTitle" />

<!-- 等价于 -->
<input 
  [value]="newTodoTitle" 
  (input)="newTodoTitle = $event.target.value"
/>

七、内置指令 #

7.1 *ngIf #

html
<!-- 基本用法 -->
<div *ngIf="isVisible">显示内容</div>

<!-- else分支 -->
<div *ngIf="isLoading; else loaded">
  加载中...
</div>
<ng-template #loaded>
  加载完成
</ng-template>

7.2 *ngFor #

html
<!-- 基本用法 -->
<li *ngFor="let todo of todos">{{ todo.title }}</li>

<!-- 获取索引 -->
<li *ngFor="let todo of todos; let i = index">
  {{ i + 1 }}. {{ todo.title }}
</li>

<!-- 其他变量 -->
<li *ngFor="let todo of todos; 
            let i = index; 
            let first = first; 
            let last = last;
            let even = even;
            let odd = odd">
  {{ i }} - {{ todo.title }}
</li>

<!-- trackBy优化性能 -->
<li *ngFor="let todo of todos; trackBy: trackByFn">
  {{ todo.title }}
</li>
typescript
trackByFn(index: number, todo: Todo): number {
  return todo.id;
}

7.3 *ngSwitch #

html
<div [ngSwitch]="status">
  <p *ngSwitchCase="'active'">激活状态</p>
  <p *ngSwitchCase="'inactive'">未激活状态</p>
  <p *ngSwitchDefault>未知状态</p>
</div>

八、管道(Pipes) #

8.1 内置管道 #

html
<!-- 日期管道 -->
<p>{{ todo.createdAt | date }}</p>
<p>{{ todo.createdAt | date:'yyyy-MM-dd' }}</p>
<p>{{ todo.createdAt | date:'fullDate' }}</p>

<!-- 大小写管道 -->
<p>{{ title | uppercase }}</p>
<p>{{ title | lowercase }}</p>
<p>{{ title | titlecase }}</p>

<!-- 数字管道 -->
<p>{{ price | number }}</p>
<p>{{ price | number:'1.2-2' }}</p>
<p>{{ price | currency:'CNY' }}</p>

<!-- 百分比管道 -->
<p>{{ progress | percent }}</p>

<!-- JSON管道(调试用) -->
<pre>{{ todo | json }}</pre>

8.2 链式管道 #

html
<p>{{ todo.title | uppercase | slice:0:10 }}</p>

九、应用效果 #

text
┌─────────────────────────────────────┐
│           待办事项                    │
├─────────────────────────────────────┤
│ [添加新任务...          ] [添加]     │
├─────────────────────────────────────┤
│ 总计: 3          已完成: 1           │
├─────────────────────────────────────┤
│ ☐ 学习Angular基础          [删除]   │
│ ☑ 完成第一个应用            [删除]   │
│ ☐ 学习路由                  [删除]   │
└─────────────────────────────────────┘

十、总结 #

本章节我们学习了:

概念 说明
组件 Angular应用的基本构建块
数据绑定 组件与模板之间的数据交互
事件绑定 处理用户交互
指令 扩展HTML功能
管道 数据格式化转换

下一步:Angular项目结构

最后更新:2026-03-26