Hello World - 组件与双向绑定

知识库
知识库文档
/tech-stacks/angular/examples/Hello World - 组件与双向绑定.md

文档

Angular Hello World — 组件与双向绑定

目标

创建 Angular 应用,展示组件、双向绑定、服务注入、路由和 HTTP 请求。

完整代码

项目创建

ng new angular-demo --routing --style=scss
cd angular-demo
ng generate component counter
ng generate component todo
ng generate service todo
ng serve

根组件 src/app/app.component.ts

import { Component } from "@angular/core";

@Component({
  selector: "app-root",
  template: `
    <main class="app">
      <h1>🅰️ Angular Demo</h1>
      <p class="subtitle">企业级前端框架的 Hello World</p>

      <nav>
        <a routerLink="/counter">计数器</a>
        <a routerLink="/todo">待办事项</a>
      </nav>

      <router-outlet></router-outlet>
    </main>
  `,
  styles: [`
    .app {
      max-width: 560px; margin: 40px auto; padding: 0 16px;
      font-family: -apple-system, 'Microsoft YaHei', sans-serif;
    }
    h1 { text-align: center; }
    .subtitle { text-align: center; color: #666; }
    nav { display: flex; gap: 16px; justify-content: center; margin: 20px 0; }
    nav a {
      padding: 8px 20px; border-radius: 8px;
      text-decoration: none; background: #f1f5f9; color: #333;
      transition: 0.2s;
    }
    nav a:hover, nav a.active { background: #3b82f6; color: white; }
  `],
})
export class AppComponent {
  title = "angular-demo";
}

路由 src/app/app.routes.ts

import { Routes } from "@angular/router";
import { CounterComponent } from "./counter/counter.component";
import { TodoComponent } from "./todo/todo.component";

export const routes: Routes = [
  { path: "", redirectTo: "/counter", pathMatch: "full" },
  { path: "counter", component: CounterComponent },
  { path: "todo", component: TodoComponent },
];

计数器组件 src/app/counter/counter.component.ts

import { Component, signal } from "@angular/core";

@Component({
  selector: "app-counter",
  template: `
    <div class="card">
      <h2>🔢 计数器</h2>
      <p class="hint">(使用 Angular Signals API)</p>

      <div class="count">{{ count() }}</div>

      <div class="buttons">
        <button class="btn-inc" (click)="increment()">+ 1</button>
        <button class="btn-dec" (click)="decrement()">- 1</button>
        <button class="btn-reset" (click)="reset()">重置</button>
      </div>

      @if (count() >= 10) {
        <div class="achievement">🎉 达到 {{ count() }} 次!</div>
      }
    </div>
  `,
  styles: [`
    .card { text-align: center; padding: 24px; }
    .hint { color: #888; font-size: 0.85rem; }
    .count { font-size: 3rem; font-weight: 700; margin: 12px 0; }
    .buttons button {
      padding: 10px 22px; border: none; border-radius: 8px;
      font-size: 1rem; cursor: pointer; margin: 4px; color: white;
    }
    .btn-inc { background: #10b981; }
    .btn-dec { background: #ef4444; }
    .btn-reset { background: #64748b; }
    .achievement {
      margin-top: 16px; padding: 12px;
      background: #fef3c7; border-radius: 8px;
    }
  `],
})
export class CounterComponent {
  count = signal(0);

  increment() {
    this.count.update((c) => c + 1);
  }
  decrement() {
    this.count.update((c) => c - 1);
  }
  reset() {
    this.count.set(0);
  }
}

Todo 服务 src/app/todo.service.ts

import { Injectable } from "@angular/core";

export interface TodoItem {
  id: number;
  text: string;
  done: boolean;
}

@Injectable({ providedIn: "root" })
export class TodoService {
  private todos: TodoItem[] = [
    { id: 1, text: "学 Angular", done: true },
    { id: 2, text: "写项目", done: false },
  ];

  getAll(): TodoItem[] {
    return this.todos;
  }

  add(text: string): void {
    this.todos.push({
      id: Date.now(),
      text,
      done: false,
    });
  }

  toggle(id: number): void {
    const todo = this.todos.find((t) => t.id === id);
    if (todo) todo.done = !todo.done;
  }

  remove(id: number): void {
    this.todos = this.todos.filter((t) => t.id !== id);
  }
}

Todo 组件 src/app/todo/todo.component.ts

import { Component, signal } from "@angular/core";
import { FormsModule } from "@angular/forms";
import { TodoService, TodoItem } from "../todo.service";

@Component({
  selector: "app-todo",
  standalone: true,
  imports: [FormsModule],
  template: `
    <div class="card">
      <h2>📋 待办事项</h2>

      <div class="input-row">
        <input
          [(ngModel)]="newTodo"
          (keydown.enter)="addTodo()"
          placeholder="添加新任务..."
        />
        <button class="btn-primary" (click)="addTodo()">添加</button>
      </div>

      <ul>
        @for (todo of todos; track todo.id) {
          <li class="todo-item">
            <span [class.done]="todo.done" (click)="toggleTodo(todo.id)">
              {{ todo.text }}
            </span>
            <div>
              <button
                class="btn-sm"
                [style.background]="todo.done ? '#f59e0b' : '#10b981'"
                (click)="toggleTodo(todo.id)"
              >
                {{ todo.done ? "↩" : "✓" }}
              </button>
              <button class="btn-sm btn-del" (click)="removeTodo(todo.id)">
                ✕
              </button>
            </div>
          </li>
        }
      </ul>

      <p class="status">
        {{ doneCount() }} / {{ todos.length }} 项完成
      </p>
    </div>
  `,
  styles: [`
    .card { padding: 24px; }
    .input-row { display: flex; gap: 8px; margin-bottom: 16px; }
    .input-row input {
      flex: 1; padding: 10px; border: 2px solid #e2e8f0;
      border-radius: 8px; font-size: 1rem;
    }
    .btn-primary {
      padding: 10px 20px; border: none; border-radius: 8px;
      background: #3b82f6; color: white; cursor: pointer;
    }
    .todo-item {
      display: flex; justify-content: space-between; align-items: center;
      padding: 10px 0; border-bottom: 1px solid #f1f5f9;
    }
    .done { text-decoration: line-through; color: #94a3b8; }
    .btn-sm { border: none; border-radius: 6px; padding: 4px 10px;
      color: white; cursor: pointer; margin-left: 4px; }
    .btn-del { background: #ef4444; }
    .status { margin-top: 12px; color: #888; font-size: 0.9rem; }
  `],
})
export class TodoComponent {
  todos: TodoItem[] = [];
  newTodo = "";

  constructor(private todoService: TodoService) {
    this.todos = this.todoService.getAll();
  }

  doneCount() {
    return this.todos.filter((t) => t.done).length;
  }

  addTodo() {
    if (this.newTodo.trim()) {
      this.todoService.add(this.newTodo.trim());
      this.newTodo = "";
    }
  }

  toggleTodo(id: number) {
    this.todoService.toggle(id);
  }

  removeTodo(id: number) {
    this.todoService.remove(id);
    this.todos = this.todoService.getAll();
  }
}

运行步骤

ng serve
# 访问 http://localhost:4200

预期输出

  • 导航栏切换计数器 / 待办事项两个页面
  • 计数器使用 Signal 响应式更新
  • 待办事项支持添加、完成、删除
  • 服务注入(Dependency Injection)自动管理 TodoService 单例

信息

路径
/tech-stacks/angular/examples/Hello World - 组件与双向绑定.md
更新时间
2026/5/31