Ionic Tutorial
Chatbot with DialogFlow (part 1)
Outline
- VS Code擴充功能 與 Ionic Framework
- Side menu範例
- 地圖範例
- 表單製作與輸入驗證
VS Code 擴充功能(1/2)
輸入ionic
輸入"ionic v4"
VS Code 擴充功能(2/2)
選取
分別點擊JavaScript與TypeScript
安裝JavaScript/ TypeScript擴充功能
Side Menu App
Sidemenu版型(1/4)
❶ 建立Ionic 專案
ionic start SidemenuExample sidemenu --type=angular
cd SidemenuExample
ionic serve
2個頁面
選單頁面
選單定義
Sidemenu版型(2/4)
<ion-app>
<ion-router-outlet></ion-router-outlet>
</ion-app>
app.component.html
<ion-app>
<ion-split-pane>
<ion-menu>
<ion-header>
<ion-toolbar>
<ion-title>Menu</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
<ion-menu-toggle auto-hide="false" *ngFor="let p of appPages">
<ion-item [routerDirection]="'root'" [routerLink]="[p.url]">
<ion-icon slot="start" [name]="p.icon"></ion-icon>
<ion-label>
{{p.title}}
</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
</ion-content>
</ion-menu>
<ion-router-outlet main></ion-router-outlet>
</ion-split-pane>
</ion-app>
沒有sidemenu
ion-split-pane: multiple views
選單
頁面
有sidemenu
Sidemenu版型(3/4)
app.component.ts
import { Component } from '@angular/core';
// ...[略]
export class AppComponent {
public appPages = [
{ title: 'Home', url: '/home', icon: 'home' },
{ title: 'List', url: '/list', icon: 'list' }
];
// ...[略]
}
2個選單項目
項目名稱
圖示名稱
超連結
// ...[略]
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', loadChildren: './home/home.module#HomePageModule' },
{ path: 'list', loadChildren: './list/list.module#ListPageModule' }
];
// ...[略]
app-routing.module.ts
路徑設定
圖示名稱參考: https://ionicons.com/
❷ 設定選單項目(app.component.ts)
Sidemenu版型(4/4)
app.component.ts
// ...[略]
export class AppComponent {
public appPages = [
{ title: 'Home', url: '/home', icon: 'home' },
{ title: 'List', url: '/list', icon: 'list' }
];
// ...[略]
}
<!-- ...[略] -->
<ion-list>
<ion-menu-toggle auto-hide="false" *ngFor="let p of appPages">
<ion-item [routerDirection]="'root'" [routerLink]="[p.url]">
<ion-icon slot="start" [name]="p.icon"></ion-icon>
<ion-label>
{{p.title}}
</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
<!-- ...[略] -->
app.component.html
選單陣列
選單項目
API參考文件: ion-item (v4 BETA)
API參考文件: ion-icon (v4 BETA)
目的路徑
圖示
Sidemenu範例練習
範例練習建立專案
ionic start SMenuExample sidemenu --type=angular
cd SMenuExample
ionic g page chat
ionic g page about
❶ 新建sidemenu專案、建立chat, about頁面
❷ 刪除list頁面
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full'},
{ path: 'home', loadChildren: './home/home.module#HomePageModule'},
// 刪除 path: 'list' 該行
{ path: 'chat', loadChildren: './chat/chat.module#ChatPageModule' },
{ path: 'about', loadChildren: './about/about.module#AboutPageModule' }
];
app-routing.module.ts
public appPages = [
{ title: '首頁', url: '/home', icon: 'home'},
{ title: '聊天', url: '/chat', icon: 'chatboxes'},
{ title: '關於我們', url: '/about', icon: 'people'},
];
app.component.ts
Ⓐ 刪除list資料夾
Ⓑ 刪除'/list', 加入'/chat', '/about'
Ⓒ 刪除path: 'list'那一行
範例練習HomePage內容: 準備工作
Ⓒ 主頁(HomePage)

Ⓐ 加入選單(各個頁面)
Ⓑ 建立資料模型
新增資料夾 _models
新增檔案 place.ts
卡片版型: ion-card
Ⓓ 關於我們(AboutPage)
清單: ion-list
項目: ion-item
Ⓔ 聊天(ChatPage)
ion-footer
表單輸入
[(ngModel)]
ion-grid
ion-toggle
ion-button
範例練習加入選單
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-menu-button></ion-menu-button>
</ion-buttons>
<ion-title>
{{ pageTitle }}
</ion-title>
</ion-toolbar>
</ion-header>
<!-- 略 -->
chat.page.html
❶ ion-buttons: 按鈕群組
❷ ion-menu-button: 選單按鈕(漢堡按鈕)
about.page.html
home.page.html
Ⓐ 加入選單
範例練習定義資料模型
export interface Place {
title: string; // 景點名稱
photoURL?: string; // 景點圖片
intro?: string; // 景點介紹
}
_model/place.ts
❶ 定義資料模型介面
陣列用法➩ 變數名稱: 型別[]
供頁面共用(相同資料規格)
?代表optional(可有可無)
Ⓑ 資料模型
import { Place } from '../_model/place';
// 也可定義陣列(const代表常數)
export const PLACES: Place[] = [
{title: '幾米主題南港站', photoURL: 'g_me.jpg', intro: '受到各方...'},
{title: '140高地公園', photoURL: 'high_land.jpg', intro: '140高...'},
{title: '通化公園', photoURL: 'th_park.jpg', intro: '本公園位於 ...'},
{title: '世貿公園', photoURL: 'wtc_park.jpg', intro: '公園面積約...'},
];
export class HomePage {
poi: Place = {title:'名稱', photoURL: 'pic.jpg'};
college: Place = {title:'學校名稱', photoURL: 'pic.jpg', '一所學校'};
places: Place[] = PLACES;
}
一般用法➩ 變數名稱: 型別
使用頁面.page.ts
❷ import模型介面
範例練習編輯主頁
❶ import模型介面
❸ 定義屬性places,值為上列常數PLACES
Ⓒ 編輯主頁
import { Component } from '@angular/core';
import { Place } from '../_model/place';
export const PLACES: Place[] = [
{title: '捷運北門站(台北鐵道局)', photoURL: 'north_gate.jpg', intro: '捷運北門站為捷運松山線,位於塔城街,為一地下4層車站'},
{title: '幾米主題南港站', photoURL: 'g_me.jpg', intro: '受到各方廣大喜愛的幾米主題裝置藝術'},
{title: '140高地公園', photoURL: 'high_land.jpg', intro: '140高地公園位於台北市文山區萬美里境內'},
{title: '通化公園', photoURL: 'th_park.jpg', intro: '本公園位於文昌街與通化街口,於民國76年建立'},
{title: '世貿公園', photoURL: 'wtc_park.jpg', intro: '公園面積約1.2公頃,位於南港區經貿二路106巷'},
];
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
places: Place[] = PLACES;
constructor() {}
}
home.page.ts
❷ 準備頁面資料(定義常數PLACES)
範例練習編輯主頁
Ⓒ 編輯主頁
卡片版型: ion-card
<ion-card>
<ion-img src="/assets/myImg.png"></ion-img>
<ion-card-header>
<ion-card-title>Hello World</ion-card-title>
</ion-card-header>
<ion-card-content>
<p>The content for this card</p>
</ion-card-content>
</ion-card>
ion-card V.4: https://beta.ionicframework.com/docs/components/#card
範例練習編輯主頁
places陣列 (3個欄位)
❸ 內文
Ⓒ 編輯主頁
<!-- ion-header 略 -->
<ion-content padding>
<ion-card *ngFor="let place of places">
<ion-img src="/assets/images/{{ place.photoURL }}"></ion-img>
<ion-card-header>
<ion-card-title>{{ place.title }}</ion-card-title>
</ion-card-header>
<ion-card-content>
<p>{{ place.intro }}</p>
</ion-card-content>
</ion-card>
</ion-content>
home.page.html
❷ 標題
export const PLACES: Place[] = [
{title: '捷運北門站', photoURL: 'north_gate.jpg', intro: '捷運北門站...'},
//...略
export class HomePage {
places: Place[] = PLACES;
//... 略
}
home.page.ts
*ngFor: 迴圈指令
❶ 圖片
範例練習編輯關於我們
Ⓓ關於我們
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-about',
templateUrl: './about.page.html',
styleUrls: ['./about.page.scss'],
})
export class AboutPage implements OnInit {
persons = [
{name: '趙大春', cv: '程式設計高手', photoURL: 'https://randomuser.me/api/portraits/thumb/men/18.jpg'},
{name: '周曉秋', cv: '視覺設計,美編', photoURL: 'https://randomuser.me/api/portraits/thumb/women/31.jpg'},
{name: '王武', cv: '網路行銷', photoURL: 'https://randomuser.me/api/portraits/thumb/men/8.jpg'},
{name: '張文慈', cv: '財務', photoURL: 'https://randomuser.me/api/portraits/thumb/women/25.jpg'},
];
constructor() { }
ngOnInit() {
}
}
about.page.ts
定義persons屬性陣列(3個欄位)
範例練習編輯關於我們
Ⓓ關於我們
<ion-list>
<ion-item>
<ion-avatar slot="start">
<img src="/docs/assets/img/avatar-finn.png"></img>
</ion-avatar>
<ion-label>
<h3>I'm a big deal</h3>
<p>Listen, I've had a pretty messed up day...</p>
</ion-label>
</ion-item>
</ion-list>
ion-list V.4: https://beta.ionicframework.com/docs/components/#list
清單: ion-list
項目: ion-item
範例練習編輯關於我們
Ⓓ關於我們
places陣列 (3個欄位)
❸ 內文
<!-- ion-header 略 -->
<ion-content padding>
<ion-list>
<ion-item *ngFor="let p of persons">
<ion-avatar slot="start">
<img src="{{ p.photoURL }}" />
</ion-avatar>
<ion-label>
<h3>{{ p.name }}</h3>
<p>{{ p.cv }}</p>
</ion-label>
</ion-item>
</ion-list>
</ion-content>
about.page.html
❷ 小標題
export class AboutPage implements OnInit {
persons = [
{name: '趙大春', cv: '程式設計高手', photoURL: 'https://rand.../18.jpg'},
//...略
about.page.ts
*ngFor: 迴圈指令
❶ 頭像
範例練習聊天表單
Ⓔ 聊天表單
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-chat',
templateUrl: './chat.page.html',
styleUrls: ['./chat.page.scss'],
})
export class ChatPage implements OnInit {
message: string;
messages = [];
align_right = true;
constructor() { }
ngOnInit() {}
onSubmit() {
if (!this.message) { return; }
const msg = { msg: this.message, align: 'button-left'};
if (this.align_right) {
msg.align = 'button-right';
}
this.messages.push(msg);
}
}
chat.page.ts
❶ 輸入值屬性
❷ 聊天內容屬性
❸ 開關切換屬性
❶ 輸入欄位
❷ 聊天內容
❸ 切換開關
❺ 訊息送出處理器(輸入欄位)
[(ngModel)] = "message"
❹ 雙向繫結(資料同步)
範例練習聊天表單
Ⓔ 聊天表單
<ion-header><!--略--></ion-header>
<ion-content padding>
<ion-list>
<ion-item *ngFor="let msg of messages" >
<ion-button></ion-button>
</ion-item>
</ion-list>
</ion-content>
<ion-footer>
<form #myForm="ngForm">
<ion-item>
<ion-label position="floating">輸入訊息</ion-label>
<ion-input [(ngModel)]="message" ....... required>
</ion-input>
</ion-item>
</form>
</ion-footer>
ion-footer
表單輸入
[(ngModel)]
ion-grid
ion-toggle
ion-button
範例練習聊天表單
事件處理器
❸ 必要欄位
<!-- ion-header 略 -->
<!-- ion-content 略 -->
<ion-footer>
<form #myForm="ngForm">
<ion-item>
<ion-label position="floating">輸入訊息</ion-label>
<ion-input [(ngModel)]="message" name="message"
(keyup.enter)="onSubmit(); myForm.reset()"
type="text" required>
</ion-input>
</ion-item>
</form>
</ion-footer>
chat.page.html
❷ keyup.enter: Enter鍵按下
message: string;
messages = [];
align_right = true;
onSubmit() {
if (!this.message) { return; }
// ...略
this.messages.push(msg);
}
chat.page.ts
#myForm: 定義區域變數myForm
❶ ngModel設定輸入儲存的屬性
Ⓔ 聊天表單
✪ 表單設計方式:ngForm + ngModel +ngSubmit
name必要欄位
reset(): 清除表單內容
範例練習聊天表單
Ⓔ 聊天表單
<ion-grid fixed>
<ion-row>
<ion-col size="8">
<form #myForm="ngForm">
<!-- 略 -->
</form>
</ion-col>
<ion-col size="4">
<ion-item>
<ion-label color="dark">靠右</ion-label>
<ion-toggle [(ngModel)]="align_right"></ion-toggle>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
ion-grid: 可由很多ion-row組成
ion-toggle
ion-row: 由多個ion-col組成
每一個ion-row: 12等分, 此ion-col佔8等分
範例練習聊天表單
屬性
切換靠右設定
<ion-grid fixed>
<ion-row>
<ion-col size="8">
<form #myForm="ngForm"> ... 略 ...</form>
</ion-col>
<ion-col size="4">
<ion-item>
<ion-label color="dark">靠右</ion-label>
<ion-toggle [(ngModel)]="align_right"></ion-toggle>
</ion-item>
</ion-col>
</ion-row>
</ion-grid>
chat.page.html
align_right = true;
onSubmit() {
if (!this.message) { return; }
const msg = { msg: this.message, align: 'button-left'};
if (this.align_right) {
msg.align = 'button-right';
}
this.messages.push(msg);
}
chat.page.ts
Ⓔ 聊天表單
✪ ion-grid排版型 與 表單元素ion-toggle(開關)
繫結屬性
css設定: button-left為預設值
範例練習聊天表單
form {
ion-item {
--ion-item-background: #3880ff;
--ion-item-color: #ffffff;
}
}
.button-left {
margin-right: auto;
}
.button-right {
margin-left: auto;
}
chat.page.scss
Ⓔ 聊天表單
地圖範例
Open Street Map's App
OpenStreetMap
Google Map
地圖 OS map vs. Google map
啟用Google Maps Platform取得金鑰
建立Google Cloud專案
提供帳單資訊 (信用卡)
收費機制
使用 @ionic-native/google-maps
cordova-plugin-googlemaps
不需金鑰
Free to charge
使用 leaflet
@asymmetrik/ngx-leaflet
@types/leaflet
使用外掛
前置準備
使用OS Map專案準備工作(1/4)
❶ 建立Ionic 專案
ionic start OsMapExample blank --type=angular
cd OsMapExample
ionic serve
❷加入leaflet相關套件
npm install leaflet
npm install @asymmetrik/ngx-leaflet
npm install @types/leaflet
使用OS Map專案準備工作(2/4)
❸ 如果地圖放在app.component,則在app.module.ts加入import模組設定
// ...
import { LeafletModule } from '@asymmetrik/ngx-leaflet';
@NgModule({
// ...
imports: [
// ...,
LeafletModule.forRoot()
],
// ...
})
export class AppModule { }
注意: 所有使用地圖頁面的 .module.ts檔,也要分別import
app.module.ts
使用OS Map專案準備工作(3/4)
❹ 在angular.json加入leaflet.css設定
"styles": [{
"input": "src/theme/variables.scss"
},
{
"input": "src/global.scss"
},
"./node_modules/leaflet/dist/leaflet.css"
],
找到"styles"段落
加入leaflet.css
使用OS Map專案準備工作(4/4)
❺ 在angular.json加入讀取圖檔的路徑
"assets": [{
"glob": "**/*",
"input": "./node_modules/leaflet/dist/images",
"output": "./assets"
},
"src/assets",
"src/favicon.ico",
{
"glob": "**/*.svg",
"input": "node_modules/@ionic/angular/dist/ionic/svg",
"output": "./svg"
}
],
找到"assets"段落
修改成leaflet路徑
加入路徑與圖檔
使用OS Map頁面與服務(1/16)
ionic g service _service/data
ionic g page detail
Ⓒ 主頁(HomePage)
細節頁面
Ⓑ 建立資料服務
Ⓓ 新增細節頁面
Ⓐ 建立資料模型
新增資料夾 _models
新增檔案 place.ts
使用OS Map頁面與服務(2/16)
export class Place {
id: string; // id
title: string; // 景點名稱
address?: string; // 地址
location: {
lat: number; // 緯度
lng: number; // 經度
};
intro?: string; // 景點介紹
constructor(id, title, loc, addr?, intro?) {
this.id = id;
this.title = title;
if (addr) { this.address = addr; }
this.location = loc;
if (intro) { this.intro = intro; }
}
}
_model/place.ts(未完)
❶ 定義資料模型
❷ 地圖需要經緯度座標
供頁面、services共用(相同資料規格)
?代表optional(可有可無)
Ⓐ 資料模型
使用OS Map頁面與服務(3/16)
export const DUMMY_PLACES: Place[] = [
new Place('10001', '捷運北門站(台北鐵道局)', {lat: 25.049556, lng: 121.510181},
'臺北市 大同區塔城街10號',
'捷運北門站為捷運松山線,位於塔城街,為一地下4層車站,長約171公尺、寬約32公尺,開挖深度約32公尺,設有3個出入口、2座通風井及2座無障礙電梯。'),
new Place('10002', '幾米主題南港站', {lat: 25.051801, lng: 121.606045},
'臺北市 南港區忠孝東路7段380號',
'受到各方廣大喜愛的幾米主題裝置藝術,就藏身在捷運板南線的南港站之中。南港捷運站大膽引進享譽國際的幾米繪畫,完整表現出捷運站的現代性,既寫實又富美感。'),
new Place('10003', '140高地公園', {lat: 25.004011, lng: 121.568527},
'臺北市 文山區萬寧街125號',
'140高地公園位於台北市文山區萬美里境內,萬寧街北側,介於萬寧街與萬美街2段間的丘陵地,最高海拔為138公尺的枹子腳山。'),
new Place('10004', '通化公園', {lat: 25.032323, lng: 121.560089},
'臺北市 信義區文昌街與通化街口',
'本公園位於文昌街與通化街口,於民國76年建立,原命名為「文通公園」,面積1,180平方公尺,後改「文通公園」為「通化公園」。'),
new Place('10005', '世貿公園', {lat: 25.058038, lng: 121.615468},
'臺北市 南港區經貿二路106巷',
'為提供市民更多的停車空間,停管處新建南港區世貿公園地下停車場,並於民國100年簡易綠美化,公園面積約1.2公頃,位於南港區經貿二路106巷。'),
];
_model/place.ts(續)
❸ 準備圖資
暫時使用(實際應從資料庫來)
Ⓐ 資料模型
景點資料來源:臺北市臺北旅遊網-景點資料(中文)
使用OS Map頁面與服務(4/16)
// ...
import { Place, DUMMY_PLACES } from '../_models/place';
export class DataService {
places: Place[] = DUMMY_PLACES;
// ...
// 回傳單一景點
getPlace(id: string): Place {
return this.places.find(p => p.id === id );
}
}
_services/data.service.ts
❷ 引入資料模型/資料內容
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor() { }
}
❶ service基本框架: Injectable
❸ 設定屬性值
❹ 提供method
修改 _services/data.service.ts
Ⓑ 資料服務
使用OS Map頁面與服務(5/16)
places: Place[] = DUMMY_PLACES;
屬性名稱
Method名稱
陣列.find(): 搜尋第一個符合條件的元素
getPlace(id: string): Place {
return this.places.find(p => p.id === id );
}
型別 (陣列型別)
常數初值
import { Place, DUMMY_PLACES } from '../_models/place';
回傳型別
引數(名稱: 型別)
陣列.find((陣列元素引數) => {函數主體});
引入外部型別/常數
Ⓑ 資料服務
使用OS Map頁面與服務(6/16)
import { Injectable } from '@angular/core';
import { Place, DUMMY_PLACES } from '../_models/place';
@Injectable({
providedIn: 'root'
})
export class DataService {
places: Place[] = DUMMY_PLACES;
constructor() { }
// 回傳所有景點
getPlaces(): Place[] {
return this.places;
}
// 回傳單一景點
getPlace(id: string): Place {
return this.places.find(p => p.id === id );
}
}
_services/data.service.ts
❺ 提供2個服務
Ⓑ 資料服務
使用OS Map頁面與服務(7/16)
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
const routes: Routes = [
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', loadChildren: './home/home.module#HomePageModule' },
{ path: 'detail/:id', loadChildren: './detail/detail.module#DetailPageModule' },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
❶ 修改app-routing.module.ts
為細節頁面路徑加入引數
'detail' 改為 'detail/:id'
id為「引數名稱」
app-routing.module.ts
Ⓒ編輯主頁
使用OS Map頁面與服務(8/16)
import { DataService } from './../_service/data.service';
import { Component } from '@angular/core';
import { Place } from '../_models/place';
@Component({
selector: 'app-home',
templateUrl: 'home.page.html',
styleUrls: ['home.page.scss'],
})
export class HomePage {
places: Place[];
constructor(private ds: DataService) {
this.places = ds.getPlaces();
}
}
編輯app.page.ts
使用服務讀取景點資料
❷ 引入服務、型別
❸ 景點屬性陣列
app.page.ts
Ⓒ編輯主頁
❹ 透過服務讀取陣列
❷ 需在constuctor加入服務引數
使用OS Map頁面與服務(9/16)
<ion-header>
<ion-toolbar>
<ion-title>
Open Street Map
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<ion-list>
<ion-item *ngFor="let place of places" [href]="'/detail/' + place.id ">
<ion-label>{{ place.title }}</ion-label>
</ion-item>
</ion-list>
</ion-content>
編輯app.page.html
app.page.html
Ⓒ編輯主頁
❻ [href]屬性繫結: 設定網址
❺ *ngFor指令: 迴圈
ion-toolbar: 參考Ionic 4頁面結構
ion-list: 參考Ionic 4 List 元件
❼ 屬性繫結
app-routing.module.ts
定義頁面路徑'detail/:id'
例: '/detail/10001'
使用OS Map頁面與服務(10/16)
import { LeafletModule } from '@asymmetrik/ngx-leaflet';
// ...
@NgModule({
imports: [
//...,
LeafletModule
],
//...
})
export class DetailPageModule {}
detail.module.ts
❶ 引入地圖模組LeafletModule
用到地圖的頁面module.ts都要加
Ⓓ細節頁面
編輯detail.module.ts
使用OS Map頁面與服務(11/16)
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { DataService } from '../_service/data.service';
import { Place } from '../_models/place';
import * as L from 'leaflet';
// ...[略]
export class DetailPage implements OnInit {
place: Place; // 景點
map: any; // 地圖
options: {}; // 地圖選項
constructor(private route: ActivatedRoute,private ds: DataService) {
const id = this.route.snapshot.paramMap.get('id');
this.place = this.ds.getPlace(id);
}
ngOnInit() {
this.showMap();
}
private showMap() {
// 地圖內容:詳見後述
}
onMapReady(map) {
// 刷新地圖: 詳見後述
}
}
detail.page.ts(未完)
❶ 擷取路徑參數: ActivatedRoute
❶ ActivatedRoute
Ⓓ細節頁面
❷ 引入資料服務、型別
❸ 引入地圖服務
leaflet功能以L為名稱
❹ 取得引數值
❺ 透過引數取得景點
❷ DataService
使用OS Map頁面與服務(12/16)

Ionic 4 頁面生命週期

僅執行一次
僅執行一次
使用OS Map頁面與服務(13/16)

OS Map圖層
使用OS Map頁面與服務(14/16)
import * as L from 'leaflet';
// ...[略]
private showMap() {
const streetMap = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
});
const center = L.latLng([this.place.location.lat, this.place.location.lng]);
const marker = L.marker(center, {
icon: L.icon({
iconSize: [25, 41],
iconAnchor: [13, 41],
iconUrl: 'assets/marker-icon.png',
shadowUrl: 'assets/marker-shadow.png'
})
});
this.options = {
layers: [ streetMap, marker ],
zoom: 12,
center: center
};
}
onMapReady(map) {
this.map = map;
setTimeout(() => { this.map.invalidateSize(); }, 100);
}
// ...[略]
detail.page.ts
❹ 地圖設定值: 兩個圖層
Ⓓ細節頁面
❸ 圖標
❷ 地圖中心點(來自景點座標值)
zoom level, center所在座標
❶ 街道圖層
❺ 呼叫invalidateSize()刷新地圖
網址為固定格式
使用OS Map頁面與服務(15/16)
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>{{ place.title }}</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<ion-card>
<ion-card-header>
<ion-card-subtitle>{{ place.address }}</ion-card-subtitle>
<ion-card-title>{{ place.title }}</ion-card-title>
</ion-card-header>
<ion-card-content>
<p>{{ place.intro }}</p>
<div class="osmap" leaflet [leafletOptions]="options"
(leafletMapReady)="onMapReady($event)" style="height: 300px"></div>
</ion-card-content>
</ion-card>
</ion-content>
detail.page.ts
Ⓓ細節頁面
❸ 加入地圖
❷ ion-card
❶ 返回按鈕
ion-card (參考連結)
使用OS Map頁面與服務(16/16)
<div class="osmap" leaflet
[leafletOptions]="options"
(leafletMapReady)="onMapReady($event)"
style="height: 300px">
</div>
Ⓓ細節頁面
必要指令
指令屬性繫結
事件繫結: 地圖準備好, 啟動重繪
CSS語法: 設定地圖高度
地圖通常需要以CSS設定高度或其他設定
否則可能無法正確顯示
OS Map路徑規劃
- 安裝leaflet routing machine外掛
- 路徑規劃: 自動呼叫ORSM服務完成之
Leaflet Routing Machine
npm i leaflet-routing-machine
npm i --save-dev @types/leaflet-routing-machine
安裝外掛與TypeScript Typing
在angular.json加入leaflet-routing-machine css設定
"styles": [{
"input": "src/theme/variables.scss"
},
{
"input": "src/global.scss"
},
"./node_modules/leaflet/dist/leaflet.css",
"./node_modules/leaflet-routing-machine/dist/leaflet-routing-machine.css"
],
找到"styles"段落
加入leaflet-routing-machine.css
angular.json存檔後,記得重啟ionic serve!!
Leaflet Routing Machine
import * as L from 'leaflet'; // 原本已import的leaflet
import 'leaflet-routing-machine'; // 再import leaflet-routing-machine
ts檔import 'leaflet-routing-machine
ts檔使用方式
"styles": [{
"input": "src/theme/variables.scss"
},
{
"input": "src/global.scss"
},
"./node_modules/leaflet/dist/leaflet.css",
"./node_modules/leaflet-routing-machine/dist/leaflet-routing-machine.css"
],
找到"styles"段落
加入leaflet-routing-machine.css
angular.json存檔後,記得重啟ionic serve!!
表單製作與輸入驗證
表單製作方法1: ngModel
- HTML <FORM></FORM>搭配
- 方法1: ngModel (雙向繫結)
- 方法2: Angular Reactive Forms
- 表單送出: ngSubmit (事件繫結)
表單製作ngModel
<form (ngSubmit)="todo()" #myForm="ngForm">
<!-- 第一個欄位 -->
<!-- 第二個欄位 -->
...
<!-- 送出按鈕 -->
<ion-button type="submit">送出</ion-button>
</form>
<ion-item>
<ion-label>帳號</ion-label> <!--欄位標籤 -->
<ion-input type="text" [(ngModel)]="field1" name="name"></ion-input><!--雙向繫結-->
</ion-item>
export class HomePage {
field1 = ""; // 屬性
...
todo() {
// 表單送出後的動作
}
}
HTML端
TS端
type, name都是必要屬性!
myForm為名稱
import { ReactiveFormsModule } from '@angular/forms';
...
@NgModule({
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
IonicModule,
RouterModule.forChild(routes)
],
...
表單製作方法2: Angular Reactive Forms
❶ 前置動作: 在模組檔 import ReactiveFormsModule
例如: HomePage要做表單, 則加在home.module.ts (並非加在app.module.ts)
表單製作方法2: Angular Reactive Forms
<form [formGroup]="myForm" (ngSubmit)="todo()">
<!-- 第一個欄位 -->
<!-- 第二個欄位 -->
...
<!-- 送出按鈕 -->
<ion-button type="submit">送出</ion-button>
</form>
<ion-item>
<ion-label>帳號</ion-label> <!--欄位標籤 -->
<ion-input type="text" formControlName="field1"></ion-input>
</ion-item>
HTML端
TS端
不再需要name屬性!
import { FormBuilder, FormGroup } from '@angular/forms';
export class HomePage implements OnInit {
myForm: any;
constructor(private builder: FormBuilder) {}
ngOnInit(){
this.myForm = this.builder.group({
field1: ['', []] });
}
todo() { // 表單送出後的動作 }
}
變得較為複雜
❷ 加入formGroup
❸ 加入ngSumbut與按鈕
❹ 以formControlName製作欄位
❺ import
❻ 建立變數
❼ 建立表單、欄位
表單製作方法2: 輸入驗證
輸入驗證Angular Reactive Forms: HTML端
<form [formGroup]="loginForm" (ngSubmit)="todo()"> <!-- 事件處理method -->
<!-- 第一個欄位 -->
<!-- 驗證訊息 -->
<!-- 第二個欄位 -->
<!-- 驗證訊息 -->
<ion-button type="submit">送出</ion-button>
</form>
<ion-item>
<ion-label>電子郵件</ion-label>
<ion-input required type="email" formControlName="email">
</ion-input>
</ion-item>
<ion-item *ngIf="formErrors.email">
<ion-text color="danger">
<p>{{ formErrors.email }}</p>
</ion-text>
</ion-item>
欄位
驗證錯誤訊息
表單製作方法2: 輸入驗證
輸入驗證Angular Reactive Forms: TS端
export class LoginPage implements OnInit {
loginForm: any;
private buildForm() {
this.loginForm = this.builder.group({
email: ['', [Validators.required, Validators.email]],
});
this.loginForm.valueChanges.subscribe(data => {
this.onValueChanged(data)
});
this.onValueChanged(); // 清空錯誤訊息
}
// 自訂錯誤訊息統整函式
private onValueChanged(data?: any) {
if (!this.loginForm) { return; }
// 如有錯誤,則顯示訊息
}
}
表單製作方法2: 輸入驗證
export class LoginPage implements OnInit {
formErrors = {
'email': '',
};
validatorMessages = {
'email': {
'required': '必填欄位',
'email': '請照電子郵件格式填入'
}
};
private onValueChanged(data?: any) {
if (!this.loginForm) { return; } // 表單尚未建立
const form = this.loginForm;
for (const field in this.formErrors) { // 迴圈:所有欄位
this.formErrors[field] = ''; // 清除訊息
const control = form.get(field); // 取得欄位變數
// 欄位存在 欄位已編輯過 not 欄位格式正確
if (control && control.dirty && !control.valid) {
const messages = this.validatorMessages[field]; // 取得欄位所有訊息
// 驗證錯誤的條件
for (const key in control.errors) { // 迴圈: 欄位所有驗證條件
this.formErrors[field] += messages[key] + ' ';
}
}
}
}
}
this.loginForm = this.builder.group({
email: ['', [Validators.required, Validators.email]],
});
範例專案
ionic start FormExample blank --type=angular
cd FormExample
ionic g page login


顯示驗證訊息
登入錯誤訊息
輸入表單
import { ReactiveFormsModule } from '@angular/forms';
❶ 設定模組連結: import ReactiveFormsModule
例如: loginPage要做表單, 則加在login.module.ts
...
@NgModule({
imports: [
CommonModule,
FormsModule,
ReactiveFormsModule,
IonicModule,
RouterModule.forChild(routes)
],
...
login.module.ts
輸入表單
❷ 編輯表單頁ts檔
FormBuilder, FormGroup: 製作表單
Validators: 提供驗證規則
import { Component, OnInit } from '@angular/core';
import { FormBuilder, Validators} from '@angular/forms';
import { Router } from '@angular/router';
import { ToastController } from '@ionic/angular';
@Component({
selector: 'app-login',
templateUrl: './login.page.html',
styleUrls: ['./login.page.scss'],
})
export class LoginPage implements OnInit {
loginForm: any;
formErrors = {
'email': '',
'password': ''
};
validatorMessages = {
'email': {
'required': '必填欄位',
'email': '請照電子郵件格式填入'
},
'password': {
'required': '必填欄位',
'pattern': '至少須包含一字母一數字',
'minlength': '長度至少為6',
'maxlength': '長度最多為15'
},
};
constructor(
private builder: FormBuilder,
private router: Router,
private tc: ToastController,
) { }
ngOnInit() {
console.log('ng on init');
this.buildForm();
}
async presentToast() {
const toast = await this.tc.create({
message: '帳密錯誤',
position: 'top',
duration: 2000,
});
toast.present();
}
signIn() {
const form = this.loginForm.value;
const data = {
email: form.email,
password: form.password,
};
if (data.email === 'abc@user.com' && data.password === '123456') {
this.router.navigate(['home']);
} else {
this.presentToast();
}
}
private buildForm() {
this.loginForm = this.builder.group({
email: ['', [Validators.required, Validators.email]],
password: ['', [
Validators.required,
Validators.minLength(6),
Validators.maxLength(15)
]]
});
this.loginForm.valueChanges.subscribe(data => this.onValueChanged(data));
// reset messages
this.onValueChanged();
}
private onValueChanged(data?: any) {
if (!this.loginForm) { return; }
const form = this.loginForm;
for (const field in this.formErrors) {
this.formErrors[field] = '';
const control = form.get(field);
if (control && control.dirty && !control.valid) {
const messages = this.validatorMessages[field];
// tslint:disable-next-line:forin
for (const key in control.errors) {
this.formErrors[field] += messages[key] + ' ';
}
}
}
}
}
login.page.ts
輸入表單
❸ 編輯表單頁HTML檔
<form>標籤: 屬性繫結formGroup, 事件繫結ngSubmit
每個欄位: 以formControlName 設定欄位名稱(定義於ts檔)
<ion-header>
<ion-toolbar color="primary">
<ion-title text-center>登入</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<form [formGroup]="loginForm" #myForm="ngForm" (ngSubmit)="signIn(); myForm.reset()">
<ion-item>
<ion-label position="stacked">電子郵件</ion-label>
<ion-input required type="email" placeholder="電子郵件" formControlName="email">
</ion-input>
</ion-item>
<ion-item *ngIf="formErrors.email">
<ion-text color="danger">
<p>{{ formErrors.email }}</p>
</ion-text>
</ion-item>
<ion-item>
<ion-label position="stacked">密碼</ion-label>
<ion-input required type="password" placeholder="6~15個英數字組合" formControlName="password"></ion-input>
</ion-item>
<ion-item *ngIf="formErrors.password">
<ion-text color="danger">
<p>{{ formErrors.password }}</p>
</ion-text>
</ion-item>
<ion-grid>
<ion-row>
<ion-col text-center>
<ion-button type="submit" fill="outline" color="danger" [disabled]="!loginForm.valid">
登入
</ion-button>
</ion-col>
</ion-row>
</ion-grid>
</form>
</ion-content>
login.page.html
輸入表單
❹ ngSubmit事件繫結的method
...
// ngSubmit 連結之事件處理器
signIn() {
const form = this.loginForm.value;
const data = {
email: form.email,
password: form.password,
};
if (data.email === 'abc@user.com' && data.password === '123456') {
this.router.navigate(['home']);
} else {
this.presentToast();
}
}
// 跳出訊息函式
async presentToast() {
const toast = await this.tc.create({
message: '帳密錯誤',
position: 'top',
duration: 2000,
});
toast.present();
}
login.page.ts
輸入表單
❺ 加上輸入驗證訊息與判斷邏輯 (TS檔)
private onValueChanged(data?: any) {
if (!this.loginForm) { return; }
const form = this.loginForm;
for (const field in this.formErrors) {
this.formErrors[field] = '';
const control = form.get(field);
if (control && control.dirty && !control.valid) {
const messages = this.validatorMessages[field];
// tslint:disable-next-line:forin
for (const key in control.errors) {
this.formErrors[field] += messages[key] + ' ';
}
}
}
}
validatorMessages = {
'email': {
'required': '必填欄位',
'email': '請照電子郵件格式填入'
},
'password': {
'required': '必填欄位',
'pattern': '至少須包含一字母一數字',
'minlength': '長度至少為6',
'maxlength': '長度最多為15'
},
};
login.page.ts
輸入表單
6. 修改首頁為LoginPage
...
const routes: Routes = [
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: 'home', loadChildren: './home/home.module#HomePageModule' },
{ path: 'login', loadChildren: './login/login.module#LoginPageModule' },
];
少了sendMessage()
app-routing.module.ts
Chatbot with DialogFlow
ChatBotApp api-ai-javascript的問題
],
"include": [
"../node_modules/api-ai-javascript/*.ts",
"../node_modules/api-ai-javascript/ts/**/*.ts",
"./main.ts",
"./polyfills.ts",
"**/*.ts"
],
Error: ...index.ts is missing from the TypeScript compilation
Angular改版後發生的問題 (angular 6.1.x)
修改 src/tsconfig.app.json, 加入:
rxjs 改為6版後, 程式用法改變,參考:
ChatBotApp rxjs改版產生的問題
Ionic Tutorial
By Leuo-Hong Wang
Ionic Tutorial
Chatbot with DialogFlow (part 1)
- 1,414