在日常生活中,大家应该满常看到有些系统的搜寻输入框是可以在一边打字的同时,一边将搜寻结果呈现在一个下拉选单里,非常地贴心且方便。
当然,这其中其实有很多细节,不过我们今天就专注在前端的表单开发上,来用 Reactive Forms 实作这个搜寻输入框吧!
没错,就算只是个搜寻框,它也是个表单噢!
正好最近六角学院即将举办第三届的前端 & UI 修炼精神时光屋的活动,这次它们与交通部合作,并提供了全国最大的
运输资料流通服务平台 (TDX) 之交通 API 给大家使用,让大家可以透过此活动精进自己的实力,非常推荐给大家。
想当初我第一次写铁人赛时,也是使用了参加六角举办的第一届前端修炼精神时光屋的素材来写,虽然这次没有要参赛,但又跟六角有关系了呢!
总之,藉由这次的机会与交通部提供的 运输资料流通服务平台 (TDX) 之交通 API ,我们来简单地做一个可以查询台北捷运的车站的搜寻输入框吧!
这次因为有 API 可以使用的关系,会精实很多,如果跟不上的朋友,可能要再多熟悉一下 Angular 噢!
简单来说,这个功能会需要一个输入框与一个表格,当使用者在输入框里打字时,表格的内容也会连动呈现出搜寻结果。
由於 Auto-Complete 的搜寻输入框如果要自己做会需要处理不少细节,又不想安装 UI 框架占篇幅,所以我用这个方式来呈现查询结果。
表格的栏位有以下这些:
最後呈现结果:
首先,如果在需求明确的情况下,我个人习惯会先把画面准备好。
HTML 的部份大概会长这样:
<p><input type="text" placeholder="请输入捷运站名称" /></p>
<table>
<caption>
台北捷运之捷运站查询结果
</caption>
<thead>
<tr>
<td>车站代号</td>
<td>车站名称</td>
<td>车站所属县市</td>
<td>车站所属乡镇区</td>
<td>假日是否允许自行车进出站</td>
<td>位置</td>
</tr>
</thead>
<tbody>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td>
<a target="_blank" href=""></a>
</td>
</tr>
</tbody>
</table>
CSS 的部份大家就自行发挥罗!
画面看起来会像这样:
接着我们会需要一个 FormControl
来跟输入框绑定,所以我们在 .ts
里新增一个属性 ─ searchingInputControl
:
export class ReactiveFormsAutoCompleteSearchingComponent implements OnInit {
searchingInputControl = new FormControl();
}
别忘了先到
.module.ts
里引入FormsModule
与ReactiveFormsModule
噢!
然後将 searchingInputControl
与画面输入框绑定:
<p><input type="text" placeholder="请输入捷运站名称" [formControl]="searchingInputControl" /></p>
接着我们使用昨天分享过的 valueChanges
来确认是否已正确绑定:
export class ReactiveFormsAutoCompleteSearchingComponent implements OnInit {
searchingInputControl = new FormControl();
ngOnInit(): void {
this.searchingInputControl.valueChanges.subscribe((value) => {
console.log(value);
});
}
}
结果:
看起来已经有正确的跟搜寻输入框绑定了,那接下来要怎麽做才好呢?
我们的目的是希望使用者在输入捷运站名称的同时,只留下跟使用者的输入有关联的捷运站。
因此,我们会需要一支 Service 来帮我们呼叫交通部所提供的 运输资料流通服务平台 (TDX) 之交通 API ,并把查询结果显示到画面上。
Service 的程序码大概会长这个样子:
@Injectable()
export class ReactiveFormsAutoCompleteSearchingService {
constructor(private httpClient: HttpClient) { }
searchStation(stationName: string): Observable<MetroStationDTO[]> {
let url = 'https://ptx.transportdata.tw/MOTC/v2/Rail/Metro/Station/TRTC?$format=JSON';
if (stationName) {
url += `&$filter=contains(StationName/Zh_tw,'${stationName}')`;
}
return this.httpClient.get<MetroStationDTO[]>(url);
}
}
上述程序码中有以下几个重点:
要呼叫 API 的话,需要先到 .module.ts
里引入 HttpClientModule
,才能在 Service 里使用 HttpClient
来呼叫 API。
MetroStationDTO
是我根据交通部所提供的 运输资料流通服务平台 (TDX) 之交通 API 里定义的资料介面,详细位置需先选择「轨道」再点选「捷运」,如下图所示:
由於 HTTP Method 是 GET
的缘故,所以参数是使用 Query Parameters
的方式带进 URL 之中。
如果使用者没有输入站名时,还带 $filter
参数会收到服务器回传的 Bed Request
错误,因此增加一个判断式 ─ 当传入的 stationName
为 Truthy 值时,才带 $filter
参数。
参数 $filter
的值该怎麽带这件事情其实在文件中没有写,算是这个文件比较美中不足的地方。好在六角学院的院长 ─ 廖洧杰院长前阵子有开直播课教学,而我猜测院长一定有在那堂课讲这件事情,所以去翻了一下该堂直播课的共笔才找到该怎麽带它的值。
Service 准备好之後,接下来就要将 FormControl
的 valueChanges
事件与 API 相结合了。
准备好
见证神蹟了吗?
RxJS 真的是一个很棒的函式库,它让我们可以很好地操作非同步与资料串流,而且还能让我们的程序码非常地简洁、非常地好阅读。
就像我们现在需要把使用者的输入事件与 API 做结合时,用 RxJS 的 Operators 就可以非常完美、漂亮地结合在一起。
就像这样:
export class ReactiveFormsAutoCompleteSearchingComponent implements OnInit {
searchingInputControl = new FormControl();
constructor(private service: ReactiveFormsAutoCompleteSearchingService) { }
ngOnInit(): void {
this.searchingInputControl.valueChanges.pipe(
startWith(''),
debounceTime(500),
switchMap(value => this.service.searchStation(value))
).subscribe((result) => {
console.log(result);
});
}
}
结果:
我相信在这边一定会有非常多朋友看傻眼,这是什麽神操作?!这样就接好了?!
没错!这样就接好了,是不是比你想像中简单非常多呢?
那这串到底做了什麽事呢?
首先,我希望这个画面一开始的时候就会先查询一次,所以我使用 startWith('')
来呼叫查询 API 。
再者,我希望查询的间隔不要太过快速,当使用者「可能」已经打完字的时候才查询,所以我使用 debounceTime(500)
来让查询的时间点会在使用者停止打字 500 毫秒後才呼叫查询 API。
最後,则要将原本是 valueChanges
的 Observable 转换成 呼叫 API 的 Observable 这件事情 ,所以我使用 switchMap(value => this.service.searchStation(value))
。
关於
startWith
,大家可以参考官方文件或是 Mike 的文章。
接着,我们要将得到的资料绑定到画面上,而绑定到画面上的方式大致上有两种:
export class ReactiveFormsAutoCompleteSearchingComponent implements OnInit {
searchingInputControl = new FormControl();
stations: MetroStationDTO[] = [];
constructor(private service: ReactiveFormsAutoCompleteSearchingService) { }
ngOnInit(): void {
this.searchingInputControl.valueChanges.pipe(
startWith(''),
debounceTime(500),
switchMap(value => this.service.searchStation(value))
).subscribe((stations) => {
this.stations = stations;
});
}
}
然後再绑到画面上:
<table>
<caption>
台北捷运之捷运站查询结果
</caption>
<thead>
<tr>
<td>车站代号</td>
<td>车站名称</td>
<td>车站所属县市</td>
<td>车站所属乡镇区</td>
<td>假日是否允许自行车进出站</td>
<td>位置</td>
</tr>
</thead>
<tbody>
<tr *ngFor="let station of stations">
<td>{{ station.StationID }}</td>
<td>{{ station.StationName.Zh_tw }}</td>
<td>{{ station.LocationCity }}</td>
<td>{{ station.LocationTown }}</td>
<td>{{ station.BikeAllowOnHoliday }}</td>
<td>
<a target="_blank" [href]="station.StationPosition">
{{ station.StationPosition }}
</a>
</td>
</tr>
</tbody>
</table>
export class ReactiveFormsAutoCompleteSearchingComponent {
searchingInputControl = new FormControl();
stations$ = this.searchingInputControl.valueChanges.pipe(
startWith(''),
debounceTime(500),
switchMap(value => this.service.searchStation(value))
);
constructor(private service: ReactiveFormsAutoCompleteSearchingService) { }
}
然後透过 AsyncPipe
让 Template 自己订阅:
<table>
<caption>
台北捷运之捷运站查询结果
</caption>
<thead>
<tr>
<td>车站代号</td>
<td>车站名称</td>
<td>车站所属县市</td>
<td>车站所属乡镇区</td>
<td>假日是否允许自行车进出站</td>
<td>位置</td>
</tr>
</thead>
<tbody>
<tr *ngFor="let station of (stations$ | async) || []">
<td>{{ station.StationID }}</td>
<td>{{ station.StationName.Zh_tw }}</td>
<td>{{ station.LocationCity }}</td>
<td>{{ station.LocationTown }}</td>
<td>{{ station.BikeAllowOnHoliday }}</td>
<td>
<a target="_blank" [href]="station.StationPosition">
{{ station.StationPosition }}
</a>
</td>
</tr>
</tbody>
</table>
就结果来说,这两个方法基本上都可以,但我个人非常推荐使用第二种方式。
原因是使用第二种的方式一方面可以避免我们在 Component 被 Destroy 时忘记解除订阅而导致 Memory Leak 的情形,另一方面是 Observable 会比单纯资料好用很多。
甚至有时候我们自己订阅会发生「明明资料就有收到但画面没有更新」的诡异状况。
结果:
虽然目前运作良好,但还有一些小东西还没处理完:
是
或是 否
。latitude, longitude
的格式呈现。以上这三个小东西非常地简单,我想大家应该也都知道该怎麽做,但是既然都已经到了第二十四天了,这边我觉得我们要使用 Pipe
,而不是像之前一样直接写在 Component 里。
这是因为,如果像之前的 getErrorMessage
是写在 Component 里的话,其实当画面渲染时,该函式就会被呼叫,不管该值有没有被改变。
但是使用 Pipe
的话,在该值被改变前,是不会被呼叫第二次的。
再者,使用 Pipe
的话,重用性与可维护性也比较好。
所以我建议大家可以使用 Pipe
来完成最後的小调整。
我个人会建立三个 Pipe
─ BooleanInZhTwPipe
、 GoogleMapLinkPipe
与 LocationStringPipe
。
它们的程序码如下:
@Pipe({
name: 'booleanInZhTw'
})
export class BooleanInZhTwPipe implements PipeTransform {
transform(value: boolean, ...args: unknown[]): string {
return value ? '是' : '否';
}
}
@Pipe({
name: 'googleMapLink'
})
export class GoogleMapLinkPipe implements PipeTransform {
transform({ PositionLat, PositionLon }: StationPosition, ...args: unknown[]): string {
return `https://www.google.com/maps?q=${PositionLat},${PositionLon}&z=7`;
}
}
@Pipe({
name: 'locationString'
})
export class LocationStringPipe implements PipeTransform {
transform({ PositionLat, PositionLon }: StationPosition, ...args: unknown[]): string {
return `${PositionLat}, ${PositionLon}`;
}
}
最终结果:
今天的重点主要是:
startWith
、 debounceTime
与 switchMap
将 valueChanges
与呼叫 API 串联。Pipe
。今天的练习对於一些刚学 Angular 的朋友来说会满精实且资讯量有点大的,大家可以多看几遍,多自己练习、做实验,相信对大家来说会很有帮助。
关於 RxJS ,如果大家想知道更多资讯,我推荐大家去看 Mike 的打通 RxJS 任督二脉系列文,或者是直接买实体书也行。
虽然今天的实作已经完成了,但还有测试的部份,我们明天来撰写它吧!
今天的程序码会放在 Github - Branch: day24 上供大家参考,建议大家在看我的实作之前,先按照需求规格自己做一遍,之後再跟我的对照,看看自己的实作跟我的实作不同的地方在哪里、有什麽好处与坏处,如此反覆咀嚼消化後,我相信你一定可以进步地非常快!
如果有任何的问题或是回馈,还请麻烦留言给我让我知道!
<<: LeetCode 双刀流:62. Unique Paths
前言: 通常会和Broadcast(广播)一起使用,在app status条显示的资料, 可以在Ap...
点击进入React源码调试仓库。 React的更新最终要落实到页面上,所以本文主要讲解DOM节点(H...
在 EP13 - 灾难演练,重建你的 VPC, 我们在重建 VPC 之前, 有带着大家怎麽进行单次备...
Logs - 挖掘系统内部发生的状况 系列文章 (1/4) - Logs 与 Filebeat 的基...
Imperative Programming(命令式/指令式编程) 对程序说明「How to do」...