Skip to main content

[Angular] Angular-first-app

跟著 Angular 官方教程一 the First Angular app 寫的筆記

stackblitz

Ch1. Hello World

第一堂教了些基礎語法和認識檔案結構 ng serve 啟動 angular 專案

src/

src/ 下面有三個基本檔案 index.html is the app's top level HTML template. style.css is the app's top level style sheet. main.ts is where the app start running.

app/

src/app/ 裡面會放入 angular 應用程式的元件,像是 app.component.ts,一開始長得如下,

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

@Component({
selector: "app-root", // 選擇器
standalone: true, // Angular components marked as standalone do not need to be declared in an NgModule
imports: [], // standalone components 才有的屬性,用來匯入其他 standalone components
template: `<h1>Hello world!</h1>`, // HTML
styleUrls: ["./app.component.css"], // CSS
})
export class AppComponent {
title = "homes";
}

配置檔

  1. .angular has files required to build the Angular app.
  2. .e2e has files used to test the app.
  3. .node_modules has the node.js packages that the app uses.
  4. angular.json describes the Angular app to the app building tools.
  5. package.json is used by npm (the node package manager) to run the finished app.
  6. tsconfig.* are the files that describe the app's configuration to the TypeScript compiler.

Ch2. Home component

這一堂建立 HomeComponent,在 termianl 切換到 app/,輸入以下指令

ng generate component Home --standalone --inline-template --skip-tests

app.component.ts 匯入

import { HomeComponent } from './home/home.component'; // 一開始的引入

@Component({
selector: 'app-root',
standalone: true,
imports: [HomeComponent], // 記得引入 standalone 元件
template: `
<main>
<header class="brand-name">
<img class="brand-logo" src="/assets/logo.svg" alt="logo" aria-hidden="true">
</header>
<section class="content">
<app-home></app-home>
</section>
</main>
`,
styleUrls: ['./app.component.css'],
})

並修改一些 template 和 css,形成一個具有搜尋欄的首頁

Ch3. HousingLocation component

建立 HousingLactionComponent

ng generate component HousingLocation --standalone --inline-template --skip-tests

src/app/home/home.component.ts 匯入使用

src/app/home/home.component.ts
import { HousingLocationComponent } from '../housing-location/housing-location.component'; // 匯入新元件

@Component({
selector: 'app-home',
standalone: true,
imports: [CommonModule, HousingLocationComponent], // 加入新元件
template: `
<section>
<form>
<input type="text" placeholder="Filter by city">
<button class="primary" type="button">Search</button>
</form>
</section>
<section class="results">
<app-housing-location></app-housing-location>
</section>
`,
styleUrls: ['./home.component.css'],
})

Ch4. Creating an interface

這一章教我們怎麼建立 interface 來去規範我們的資料,透過語法

ng generate interface housinglocation

會建立出空白的 app/housinglocation.ts,在檔案內寫入我們的資料定義

src/app/housinglocation.ts
export interface HousingLocation {
id: number;
name: string;
city: string;
state: string;
photo: string;
availableUnits: number;
wifi: boolean;
laundry: boolean;
}

另一邊,在 app/home/home.component.ts,匯入 interface,並且在最下方的 export class HomeComponent {} 建立 housingLocation 的實例延伸這個介面。

src/app/home/home.component.ts
export class HomeComponent {
housingLocation: HousingLocation = {
id: 9999,
name: "Test Home",
city: "Test city",
state: "ST",
photo: "assets/example-house.jpg",
availableUnits: 99,
wifi: true,
laundry: false,
};
}

Ch5. Add an input parameter to the component

這一章教到 @Input() 的用法

src/app/housing-location/housing-locations.component.ts
import { Component, Input } from '@angular/core'; // 匯入 Input
import { CommonModule } from '@angular/common';
import { HousingLocation } from '../housinglocation'; // 資料介面

// ...

export class HousingLocationComponent {
@Input() housingLocation!: HousingLocation; // !: 告訴 ts ,housingLocation 不是 null 或 undefined, 而是會透過 angular Input 設計的方式傳入
}

Ch6. Add a property binding to a component’s template

上一章在 housing-location.component.ts 先建立了 @Input() 進入的變數,在他的父層也要透過 property binding (屬性繫結) 的方式,才能將資料傳入

src/app/home/home.component.ts
<app-housing-location [housingLocation]="housingLocation"></app-housing-location>

Ch7. Add an interpolation to a component’s template

接下來,在取得 housingLocation 的資料後,我們可以透過 {{ expression }} 去把資料解釋給 HTML 呈現在畫面上

src/app/housing-location/housing-locations.component.ts
@Component({
selector: 'app-housing-location',
standalone: true,
imports: [CommonModule],
template: `
<section class="listing">
<img class="listing-photo" [src]="housingLocation.photo" alt="Exterior photo of {{housingLocation.name}}">
<h2 class="listing-heading">{{ housingLocation.name }}</h2>
<p class="listing-location">{{ housingLocation.city}}, {{housingLocation.state }}</p>
</section>
`,
styleUrls: ['./housing-location.component.css'],
})

畫面就會呈現如下

Ch8. Use *ngFor to list objects in component

接下來透過 *ngFor 的語法來去呈現物件陣列的資料

HomeComponent
export class HomeComponent {
housingLocationList: HousingLocation[] = [
{
// ...
}
]

修正 template

HomeComponent
<app-housing-location
*ngFor="let housingLocation of housingLocationList"
[housingLocation]="housingLocation">
</app-housing-location>

接下來,就可以看到和 housingLocationList 的陣列長度一樣多的 app-housing-location 元件。

Ch9. Angular services

透過語法 ng generate service housing --skip-tests 建立 housing.service.ts

housing.service.ts ,將 housingLocationList 寫入,並寫入兩個 getter() 去取資料

src/app/housing.service.ts
getAllHousingLocations(): HousingLocation[] {
return this.housingLocationList;
}

getHousingLocationById(id: number): HousingLocation | undefined {
return this.housingLocationList.find(housingLocation => housingLocation.id === id);
}

而在另一邊,HomeComponent 要注入 HousingService

HomeComponent
import { Component, inject } from "@angular/core"; // inject 注入服務
import { HousingService } from "../housing.service";

export class HomeComponent {
housingLocationList: HousingLocation[] = [];
housingService: HousingService = inject(HousingService);

constructor() {
this.housingLocationList = this.housingService.getAllHousingLocations();
} // The constructor is the first function that runs when this component is created.
}

Ch10. Add routes to the application

這一章教我們怎麼建立路徑,這邊先建立一個無關緊要的 DetailsComponent

ng generate component details --standalone --inline-template --skip-tests

接下來都是重頭戲,在 src/app 中建立 routes.ts,匯出 routeConfig,裡面有路徑、元件和標題。

src/app/route.ts
import { Routes } from "@angular/router";
import { HomeComponent } from "./home/home.component";
import { DetailsComponent } from "./details/details.component";

const routeConfig: Routes = [
{
path: "",
component: HomeComponent,
title: "Home page",
},
{
path: "details/:id",
component: DetailsComponent,
title: "Home details",
},
];

export default routeConfig;

src/main.ts 中引入 provideRouterrouteConfig,也要在 bootstrapApplication 的地方,加入 routing 的設置。

import { provideRouter } from '@angular/router';
import routeConfig from './app/routes';

bootstrapApplication(AppComponent,
{
providers: [
provideProtractorTestingSupport(),
provideRouter(routeConfig) // 加入 config
]
}
).catch(err => console.error(err));

接下來,到 src/app/app.component.ts,匯入 RoutingModule,要修改 imports 還有 template,將原本的 <app-home></app-home> 改為 <router-outlet></router-outlet> 以及加入 <a [routerLink]="['/']"> 回到首頁。

src/app/app.component.ts
import { RouterModule } from '@angular/router';

@Component({
selector: 'app-root',
standalone: true,
imports: [
HomeComponent,
RouterModule, // 要加入
],
template: `
<main>
<a [routerLink]="['/']">
<header class="brand-name">
<img class="brand-logo" src="/assets/logo.svg" alt="logo" aria-hidden="true">
</header>
</a>
<section class="content">
<router-outlet></router-outlet>
</section>
</main>
`,
styleUrls: ['./app.component.css'],
})
// ...

接下來依據 routeConfig ,我們可以去 /details/id 的頁面。

Ch11. Integrate details page into application

首先,在主頁會呈現 <app-housing-location> 的內容,我們在他的 template 多增加一個 routerLink 前往特定 /details/:id 的頁面

src/app/housing-location/housing-location.component.ts
import { RouterModule } from '@angular/router';

@Component({
selector: 'app-housing-location',
standalone: true,
imports: [CommonModule, RouterModule],
template: `
<section class="listing">
<img class="listing-photo" [src]="housingLocation.photo" alt="Exterior photo of {{housingLocation.name}}">
<h2 class="listing-heading">{{ housingLocation.name }}</h2>
<p class="listing-location">{{ housingLocation.city}}, {{housingLocation.state }}</p>
<a [routerLink]="['/details', housingLocation.id]">Learn More</a>
</section>
`,
styleUrls: ['./housing-location.component.css'],
})

接著,在 DetailsComponent,我們使用 ActivatedRoute 服務取得 id ,再透過先前建立的 HousingService 去取得該 id 的房屋資料,在 template 之中作呈現。

src/app/details/details.component.ts
import { Component, inject } from "@angular/core";
import { CommonModule } from "@angular/common";
import { ActivatedRoute } from "@angular/router"; // 取得當前路徑的變數
import { HousingService } from "../housing.service";
import { HousingLocation } from "../housinglocation";

@Component({
selector: "app-details",
standalone: true,
imports: [CommonModule],
template: `
<article>
<img
class="listing-photo"
[src]="housingLocation?.photo"
alt="Exterior photo of {{ housingLocation?.name }}"
/>
<section class="listing-description">
<h2 class="listing-heading">{{ housingLocation?.name }}</h2>
<p class="listing-location">
{{ housingLocation?.city }}, {{ housingLocation?.state }}
</p>
</section>
<section class="listing-features">
<h2 class="section-heading">About this housing location</h2>
<ul>
<li>Units available: {{ housingLocation?.availableUnits }}</li>
<li>Does this location have wifi: {{ housingLocation?.wifi }}</li>
<li>
Does this location have laundry: {{ housingLocation?.laundry }}
</li>
</ul>
</section>
</article>
`,
styleUrls: ["./details.component.css"],
})
export class DetailsComponent {
route: ActivatedRoute = inject(ActivatedRoute); // 注入
housingService = inject(HousingService);
housingLocation: HousingLocation | undefined;

constructor() {
const housingLocationId = Number(this.route.snapshot.params["id"]);
this.housingLocation =
this.housingService.getHousingLocationById(housingLocationId);
}
}

?. 使得我們的程式在 housingLocationnull 或是 undefined 的時候不會崩壞。

Ch12. Adding a form to your Angular app

接下來,要在 DetailsComponent ,建立表單,我們先在 housing.service.ts,建立收到表單的函式,收到資料會在 console 呈現。

Submit method in src/app/housing.service.ts
submitApplication(firstName: string, lastName: string, email: string) {
console.log(`Homes application received: firstName: ${firstName}, lastName: ${lastName}, email: ${email}.`);
}

DetailsComponent 中使用表單相關功能和模組,建立 applyFormsubmitApplication 去呼叫提交表單的函式

DetailsComponent
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";

@Component({
selector: "app-details",
standalone: true,
imports: [CommonModule, ReactiveFormsModule], // 模組
// ...
})
export class DetailsComponent {
// ...
applyForm = new FormGroup({
firstName: new FormControl(""),
lastName: new FormControl(""),
email: new FormControl(""),
});

constructor() {
// ...
}

submitApplication() {
this.housingService.submitApplication(
this.applyForm.value.firstName ?? "",
this.applyForm.value.lastName ?? "",
this.applyForm.value.email ?? ""
);
}
}

最後,則是在 template 使用 [formGroup]="applyForm"(submit)=submitApplication(),值得注意的是 formControlName 裡面的值會對應到 this.applyForm.value 的值,所以要寫得和 submitApplication() 一致。

DetailsComponent
template: `
<section class="listing-apply">
<h2 class="section-heading">Apply now to live here</h2>
<form [formGroup]="applyForm" (submit)="submitApplication()">
<label for="first-name">First Name</label>
<input id="first-name" type="text" formControlName="firstName">

<label for="last-name">Last Name</label>
<input id="last-name" type="text" formControlName="lastName">

<label for="email">Email</label>
<input id="email" type="email" formControlName="email">
<button type="submit" class="primary">Apply now</button>
</form>
</section>
`;

Ch13. Add the search feature to your app

來到首頁,要做搜尋功能,依據 city 去做篩選,在 constructor 裡面加入 filteredLocationList,template 的地方改成用 filteredLocationList 去跑迴圈

HomeComponent
template: `
<app-housing-location
*ngFor="let housingLocation of filteredLocationList"
[housingLocation]="housingLocation">
`

// ...

filteredLocationList: HousingLocation[] = [];

constructor() {
this.housingLocationList = this.housingService.getAllHousingLocations();
this.filteredLocationList = this.housingLocationList;
}

接著,我們在 input 的地方加上 #filter 的 template variable (樣板變數),並在下面的 button 加上 (click)="filterResults(filter.value)" 去針對 #filter 拿到的值做篩選。filterResults 函式去處理篩選,改變 filteredLocationList 來讓 HTML 產生畫面的變化。

HomeComponent
template: `
<form>
<input type="text" placeholder="Filter by city" #filter>
<button class="primary" type="button" (click)="filterResults(filter.value)">Search</button>
</form>
`
// ...
filterResults(text: string) {
if (!text) {
this.filteredLocationList = this.housingLocationList;
}

this.filteredLocationList = this.housingLocationList.filter(
(housingLocation) =>
housingLocation?.city.toLowerCase().includes(text.toLocaleLowerCase())
);
}

Ch14. Add HTTP communication to your app

這章要模擬有 server 回傳資料,所以使用 json-server 的套件

npm install -g json-server

然後,在根目錄建立 db.json,裡面的資料是原本的 housingLocationList,然後這個 json-server 也需要透過 terminal 去啟動

json-server --watch db.json

目前畫面大概如下,我們在有 json-server 啟動的情況下,呼叫 http://localhost:3000/locations ,就會回傳我們之前定義的 housingLocationList 的資料。

接著,去 src/app/housing.service.ts 更改我們的取資料函式,getAllHousingLocations()getHousingLocationById()

src/app/housing.service.ts
export class HousingService {
url = "http://localhost:3000/locations"; // fetch url

constructor() {}

async getAllHousingLocations(): Promise<HousingLocation[]> {
const data = await fetch(this.url);
return (await data.json()) ?? [];
}

async getHousingLocationById(
id: number
): Promise<HousingLocation | undefined> {
const data = await fetch(`${this.url}/${id}`);
return (await data.json()) ?? {};
}

submitApplication(firstName: string, lastName: string, email: string) {
console.log(
`Homes application received: firstName: ${firstName}, lastName: ${lastName}, email: ${email}.`
);
}
}

然後,也要在使用那些函式的元件做修正,HomeComponent

HomeComponent
  constructor() {
this.housingService.getAllHousingLocations().then((
housingLocationList: HousingLocation[]) => {
this.housingLocationList = housingLocationList;
this.filteredLocationList = this.housingLocationList;
});
}

DetailsComponent

DetailsComponent
  constructor() {
const housingLocationId = Number(this.route.snapshot.params['id']);

this.housingService
.getHousingLocationById(housingLocationId)
.then((housingLocation) => (this.housingLocation = housingLocation));
}

結論

這個教學總共花了我四小時實作和紀錄,從頭到尾了解基礎的 Angular 使用工具,因為我有些 Angular 開發經驗,所以花的時間會比較少,許多東西在之前做中學有使用過,但這種基礎的東西還是扎穩一點比較好,未來開發也會減少理解程式碼的時間。

參考資料