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>

範例練習編輯主頁

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

項目: 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指令: 迴圈

 屬性繫結

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; }
    // 如有錯誤,則顯示訊息
  }
}

有哪些驗證規則?https://angular.io/api/forms/Validators

表單製作方法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