Skip to content

Angular

cli

bash
pnpm add -g @angular/cli

ng new <project-name> --package-manager pnpm

# ng g c <component-name>
ng generate component <component-name>
# ng g s <service-name> --type=service
ng generate service <service-name> --type=service

属性 [props], 事件 (event)

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

@Component({
  selector: "app-root",
  template: `
    <h1>Hello, Angular!</h1>
    <label>
      label
      <input type="text" (input)="handleInputText($event)" />
    </label>
    <p>{{ inputText() }}</p>

    <form (submit)="handleSubmit($event)">
      <label>
        username
        <input type="text" name="username" />
      </label>

      <label>
        password
        <input type="password" name="password" />
      </label>

      <button type="submit">submit</button>
    </form>

    <form (submit)="handleFullName($event)">
      <label>
        firstName
        <input
          type="text"
          [value]="firstName()"
          (input)="handleFirstName($event)"
        />
      </label>

      <label>
        lastName
        <input
          type="text"
          [value]="lastName()"
          (input)="handleLastName($event)"
        />
      </label>

      <button type="submit">fullName</button>
    </form>
  `,
})
export class App {
  inputText = signal("");
  firstName = signal("");
  lastName = signal("");

  handleInputText(e: Event) {
    this.inputText.set((e.target as HTMLInputElement).value);
  }

  handleSubmit(e: Event) {
    e.preventDefault();
    const target = e.target as HTMLFormElement;
    const formData = new FormData(target);
    const username = formData.get("username") as string;
    const password = formData.get("password") as string;
    if (!username || !password) {
      alert("username or password is empty");
    } else {
      alert(`${username} ${password}`);
    }
    target.reset();
  }

  handleFirstName(e: Event) {
    this.firstName.set((e.target as HTMLInputElement).value);
  }

  handleLastName(e: Event) {
    this.lastName.set((e.target as HTMLInputElement).value);
  }

  handleFullName(e: Event) {
    e.preventDefault();
    if (!this.firstName() || !this.lastName()) {
      alert("firstName or lastName is empty");
    } else {
      alert(`${this.firstName()} ${this.lastName()}`);
    }
    this.firstName.set("");
    this.lastName.set("");
  }
}

双向绑定 [(ngModel)]

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

@Component({
  imports: [FormsModule], 
  selector: "app-root",
  template: `
    <form (submit)="handleFullName($event)">
      <label>
        firstName
        <input type="text" name="firstName" [(ngModel)]="firstName" />
      </label>

      <label>
        lastName
        <input type="text" name="lastName" [(ngModel)]="lastName" />
      </label>
      <button type="submit">fullName</button>
    </form>
  `,
})
export class App {
  firstName = signal("");
  lastName = signal("");

  handleFullName(e: Event) {
    e.preventDefault();
    if (!this.firstName() || !this.lastName()) {
      alert("firstName or lastName is empty");
    } else {
      alert(`${this.firstName()} ${this.lastName()}`);
    }
    this.firstName.set("");
    this.lastName.set("");
  }
}

Signal

ts
import {
  Component,
  computed,
  Signal,
  signal,
  WritableSignal,
} from "@angular/core";
import { FormsModule } from "@angular/forms";

@Component({
  imports: [FormsModule],
  selector: "app-root",
  template: `
    <form (submit)="handleFullName($event)">
      <label>
        firstName
        <input
          type="text"
          name="firstName"
          [(ngModel)]="firstName"
          [class]="getInputClassName(firstName())"
        />
      </label>

      <label>
        lastName
        <input
          type="text"
          name="lastName"
          [(ngModel)]="lastName"
          [class]="getInputClassName(lastName())"
        />
      </label>

      <button type="submit">fullName {{ fullName() }}</button>
    </form>
  `,
  styles: `
    input.error {
      border: 0.1rem solid red;
      border-radius: 0.3rem;
    }
  `,
})
export class App {
  firstName: WritableSignal<string> = signal<string>("");
  lastName: WritableSignal<string> = signal<string>("");
  fullName: Signal<string> = computed(
    () => `${this.firstName()} ${this.lastName()}`,
  ); 

  getInputClassName(name: string) {
    const len = name.trim().length;
    return len >= 4 && len <= 16 ? "" : "error";
  }

  handleFullName(e: Event) {
    e.preventDefault();
    if (!this.firstName() || !this.lastName()) {
      alert("firstName or lastName is empty");
    } else {
      alert(`${this.firstName()} ${this.lastName()}`);
    }
    this.firstName.set("");
    this.lastName.update((_oldVal) => "");
  }
}

@if, @else if, @else

  • @if, @else if, @else
  • @for, @empty
  • @switch, @case, @default
ts
import { Component, signal } from "@angular/core";
import { FormsModule } from "@angular/forms";

interface ITodoItem {
  id: number;
  title: string;
  done: boolean;
}

@Component({
  imports: [FormsModule],
  selector: "app-root",
  templateUrl: "./app.html",
  styleUrl: "./app.css",
  // styleUrls: ["./app.css"],
})
export class App {
  inputText = signal<string>("");
  todoList = signal<ITodoItem[]>([]);
  currentId = signal<number>(0);
  handleClickAdd() {
    this.todoList.update((list) => {
      return [
        ...list,
        { id: this.currentId(), title: this.inputText(), done: false },
      ];
    });
    this.currentId.update((id) => id + 1);
    this.inputText.set("");
  }
  handleClickDelete(id: number) {
    this.todoList.update((list) => list.filter((item) => item.id !== id));
  }
  handleClickDone(id: number) {
    this.todoList.update((list) =>
      list.map((item) => {
        if (item.id === id) {
          item.done = !item.done;
        }
        return item;
      }),
    );
  }
}
html
<ul>
  @if (todoList().length) { @for (todo of todoList(); track todo.id) {
  <li [class]="todo.done ? 'done-text' : ''">
    {{ todo.id }}. {{ todo.title }}
    <button type="button" (click)="handleClickDone(todo.id)">
      @if (todo.done) { Undo } @else { Done }
    </button>
    <button type="button" (click)="handleClickDelete(todo.id)">Delete</button>
  </li>
  }} @else { Todo list is empty }
</ul>

<ul>
  @for (todo of todoList(); track todo.id) {
  <li [class]="todo.done ? 'done-text' : ''">
    {{ todo.id }}. {{ todo.title }}
    <button type="button" (click)="handleClickDone(todo.id)">
      @if (todo.done) { Undo } @else { Done }
    </button>
    <button type="button" (click)="handleClickDelete(todo.id)">Delete</button>
  </li>
  } @empty { Todo list is empty }
</ul>

<label>
  title
  <input type="text" [(ngModel)]="inputText" />
</label>

<button type="button" (click)="handleClickAdd()">Add</button>
css
.done-text {
  text-decoration: line-through;
}

组件, input/output

  • input: 类似 Vue3 defineProps
  • output: 类似 Vue3 defineEmits
bash
# ng g c bg-green
ng generate component bg-green
ts
import {
  Component,
  input,
  InputSignal,
  output,
  OutputEmitterRef,
} from "@angular/core";

@Component({
  selector: "app-bg-green",
  template: `
    <label>
      Is background green?
      <input
        type="checkbox"
        [checked]="bgGreen()"
        (change)="handleBgGreenChange($event)"
      />
    </label>
  `,
})
export class BgGreen {
  // input: 类似 Vue3 defineProps
  // bgGreen = input<boolean>(false /** initialValue */, {
  //   alias: 'bg-green', // opts
  // }); //! readonly
  bgGreen: InputSignal<boolean> = input.required<boolean>({
    alias: "bg-green", // opts
  }); //! readonly

  // output: 类似 Vue3 defineEmits
  onBgGreenToggle: OutputEmitterRef<{
    bgGreen: boolean;
  }> = output<{ bgGreen: boolean }>({
    alias: "on-bg-green-change", // opts
  });

  handleBgGreenChange(e: Event) {
    const newValue = (e.target as HTMLInputElement).checked;
    this.onBgGreenToggle.emit({
      bgGreen: newValue,
    });
  }
}
ts
import { Component, signal } from '@angular/core';
import { BgGreen } from './bg-green/bg-green';

@Component({
  imports: [BgGreen],
  selector: 'app-root',
  template: `
    <app-bg-green
      [bg-green]="bgGreen()"
      (on-bg-green-change)="handleBgGreenChange($event)"
    />
  `,
  styles: `
    .bg-green {
      background: hsl(117, 50%, 50%, 50%);
    }
  `,
})
export class App {
  bgGreen = signal<boolean>(false);

  handleBgGreenChange(value: { bgGreen: boolean }) {
    const { bgGreen: newBgGreen } = value;
    this.bgGreen.set(newBgGreen);
  }
}

双向绑定 model

ts
import { Component, model, ModelSignal } from "@angular/core";
import { FormsModule } from '@angular/forms';

@Component({
  selector: "app-bg-green",
  template: `
    <label>
      <ng-content />
      <!-- <input
              type="checkbox"
              [checked]="bgGreen()"
              (change)="handleBgGreenChange($event)" /> -->
      <input type="checkbox" [(ngModel)]="bgGreen" />
    </label>
  `,
  imports: [FormsModule],
})
export class BgGreen {
  // bgGreen: InputSignal<boolean> = input.required<boolean>({
  //   alias: "bg-green", // opts
  // }); //! readonly

  // bgGreen = model<boolean>(false /** initialValue */, {
  //   alias: 'bg-green',  // opts
  // });
  bgGreen: ModelSignal<boolean> = model.required<boolean>({ 
    alias: "bg-green", // opts
  }); 

  handleBgGreenChange(e: Event) {
    const newValue = (e.target as HTMLInputElement).checked;
    this.bgGreen.set(newValue);
    // this.bgGreen.update((oldValue) => !oldValue);
  }
}
ts
import { Component, computed, signal } from "@angular/core";
import { BgGreen } from "./bg-green/bg-green";

@Component({
  imports: [BgGreen],
  selector: "app-root",
  template: `
    <app-bg-green [(bg-green)]="parentBgGreen">
      <span>Is background green?</span>
    </app-bg-green>
  `,
  styles: `
    .bg-green {
      background: hsl(117, 50%, 50%, 50%);
    }
  `,
})
export class App {
  parentBgGreen = signal<boolean>(false);
}

插槽 <ng-content />

类似 Vue3 的 slot, React 的 children

生命周期

Vue3 (Composition API)Angular
setup()constructor(), ngOnInit()
onBeforeMount()ngOnInit()
onMounted()ngAfterViewInit()
onBeforeUpdate()
onUpdated()ngAfterViewChecked()
onBeforeUnmount()ngOnDestroy()
onUnmounted()ngOnDestroy()
onErrorCaptured()--
watch(), watchEffect()ngOnChanges()
ts
import { Component, OnInit, signal } from "@angular/core";

const getMessage = (): Promise<{ message: string }> => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (Math.random() > 0.3) {
        resolve({ message: new Date().toLocaleString() });
      } else {
        reject("Failed to get message");
      }
    }, 3000);
  });
};

@Component({
  selector: "app-root",
  template: `
    <h1>App</h1>
    @if (isLoading()) {
      <p>Loading...</p>
    } @else {
      @if (currentMessage().length) {
        <p>{{ currentMessage() }}</p>
      } @else if (errorMessage().length) {
        <p>{{ errorMessage() }}</p>
      } @else {
        <p>No message</p>
      }
    }
    <button type="button" (click)="handleGetMessage()" [disabled]="isLoading()">
      Click to get message
    </button>
  `,
})
export class App implements OnInit {
  ngOnInit() {
    this.handleGetMessage();
  }
  currentMessage = signal<string>("");
  errorMessage = signal<string>("");
  isLoading = signal(false);

  async handleGetMessage() {
    try {
      this.isLoading.set(true);
      const res = await getMessage();
      this.currentMessage.set(res.message);
      this.errorMessage.set("");
    } catch (err) {
      this.currentMessage.set("");
      this.errorMessage.set(String(err));
    } finally {
      this.isLoading.set(false);
    }
  }
}

HTTPClient

ts
import { createServer, IncomingMessage, ServerResponse } from "http";

function middlewareCORS(
  handler: (req: IncomingMessage, res: ServerResponse) => void,
): (req: IncomingMessage, res: ServerResponse) => void {
  return (req, res) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader(
      "Access-Control-Allow-Methods",
      "GET,POST,PUT,DELETE,OPTIONS",
    );
    res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization");

    if (req.method === "OPTIONS") {
      res.writeHead(200);
      res.end();
      return;
    }
    handler(req, res);
  };
}

const server = createServer((req: IncomingMessage, res: ServerResponse) => {
  if (req.url === "/api/v1/message") {
    const handler = middlewareCORS((_req, res) => {
      res.setHeader("Content-Type", "application/json");
      res.writeHead(200);
      res.end(JSON.stringify({ message: new Date().toLocaleString() }));
    });
    handler(req, res);
  } else {
    res.writeHead(404);
    res.end("Not Found");
  }
});

server.listen(3000, () => {
  console.log("http://localhost:3000");
});
ts
import {
  ApplicationConfig,
  provideBrowserGlobalErrorListeners,
} from "@angular/core";
import { provideRouter, Routes } from "@angular/router";
import { provideHttpClient, withFetch } from "@angular/common/http"; 

const routes: Routes = [];
export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    provideRouter(routes),
    provideHttpClient(withFetch()), 
  ],
};
ts
import { Component, inject, OnInit, signal } from "@angular/core";
import { HttpClient } from "@angular/common/http";

interface IResponse {
  message: string;
}

@Component({
  selector: "app-root",
  template: `
    <h1>App</h1>
    @if (isLoading()) {
      <p>Loading...</p>
    } @else {
      @if (currentMessage().length) {
        <p>{{ currentMessage() }}</p>
      } @else if (errorMessage().length) {
        <p>{{ errorMessage() }}</p>
      } @else {
        <p>No message</p>
      }
    }
    <button type="button" (click)="handleGetMessage()" [disabled]="isLoading()">
      Click to get message
    </button>
  `,
})
export class App implements OnInit {
  http = inject(HttpClient); 
  ngOnInit() {
    this.handleGetMessage();
  }
  currentMessage = signal<string>("");
  errorMessage = signal<string>("");
  isLoading = signal(false);

  async handleGetMessage() {
    this.http
      .get<IResponse>("http://localhost:3000/api/v1/message", {
        timeout: 3000,
      })
      .subscribe({
        next: (res) => {
          this.currentMessage.set(res.message);
          this.errorMessage.set("");
        },
        error: (err) => {
          this.currentMessage.set("");
          this.errorMessage.set(JSON.stringify(err));
        },
      });
  }
}

Service

bash
# ng g s service --type=service
ng generate service message --type=service
ts
import { inject, signal, Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { map, catchError, of, finalize } from "rxjs";

interface IResponse {
  message: string;
}

@Injectable({
  providedIn: "root",
})
export class MessageService {
  private http = inject(HttpClient);

  currentMessage = signal<string>("");
  errorMessage = signal<string>("");
  isLoading = signal(false);

  async getMessage() {
    this.isLoading.set(true);
    this.http
      .get<IResponse>("http://localhost:3000/api/v1/message", {
        timeout: 3000,
      })
      // .subscribe((res) => {
      //   this.currentMessage.set(res.message);
      //   this.errorMessage.set('');
      // });

      // .subscribe({
      //   next: (res) => {
      //     this.currentMessage.set(res.message);
      //     this.errorMessage.set('');
      //   },
      //   error: (err) => {
      //     this.currentMessage.set('');
      //     this.errorMessage.set(JSON.stringify(err));
      //   },
      // });

      .pipe(
        map((res) => res.message),
        catchError((err) => {
          this.errorMessage.set(JSON.stringify(err));
          return of("" /** newMessage */);
        }),
        finalize(() => this.isLoading.set(false)),
      )
      .subscribe((newMessage) => {
        if (newMessage) {
          this.currentMessage.set(newMessage);
          this.errorMessage.set("");
        }
      });
  }
}
ts
import { Component, inject, OnInit } from "@angular/core";
import { MessageService } from "./message.service";

@Component({
  selector: "app-root",
  template: `
    <h1>App</h1>
    @if (isLoading()) {
      <p>Loading...</p>
    } @else {
      @if (currentMessage().length) {
        <p>{{ currentMessage() }}</p>
      } @else if (errorMessage().length) {
        <p>{{ errorMessage() }}</p>
      } @else {
        <p>No message</p>
      }
    }
    <button type="button" (click)="handleGetMessage()" [disabled]="isLoading()">
      Click to get message
    </button>
  `,
})
export class App implements OnInit {
  messageService = inject(MessageService);
  currentMessage = this.messageService.currentMessage;
  errorMessage = this.messageService.errorMessage;
  isLoading = this.messageService.isLoading;

  handleGetMessage() {
    this.messageService.getMessage();
  }

  ngOnInit() {
    this.messageService.getMessage();
  }
}