顯示具有 typescript 標籤的文章。 顯示所有文章
顯示具有 typescript 標籤的文章。 顯示所有文章

2024年7月8日 星期一

Angular 語法、功能、工具、指令等學習紀錄

此篇文使用的各工具版本為:

  • Angular CLI: 18.0.1
  • Node: 20.13.1
  • Package Manager: npm 10.5.2

Angular-CLI:

好用的 Angular 官方工具,可用 node.js 使用

npm install -g @angular/cli

安裝。

以下介紹下一些指令:

檢查 Angular-CLI, Angular 等版本:

ng version

ng new 指令可用來建立初始 Angular 專案檔案結構:
ng new {專案名稱}


ng serve 指令可以開啟預設的 http://localhost:4200 簡易 server 來測試網頁

ng serve


ng generate 指令可建立各種 Angular 元件,例如 Component, Directive 等,例:ng generate component {component 名稱}

ng generate component {component 名稱}
ng generate directive {directive 名稱}
ng generate service {service 名稱}

編譯手包佈署時要用的最終程式,詳細參數可參考 ng build • Angular

ng build
-------------------------------------------------------------------------------------------------------------

Standalone Component:
Angular 14 後推出 Standalone Component (也包括 Standalone Directive 等)
並主推它們,
之後使用 Angular-CLI 建立元件時會預設使用 Standalone。

使用 Standalone Component 的好處是其不須依附於 NgModule 上。

以往我們會使用 NgModule 來管理各個 Component, Directive 等,
但在例如 Component 的 HTML 中如果使用了某另一個 Component 時,
我們會比較難找到是用了哪一個 Component,例如:

AppComponent 的 Component Class:

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {  
}

AppComponent 的 HTML:

<xxx-component></xxx-component>
其中光看 AppComponent 我們無法馬上看出 <xxx-component> 到底是哪個 component,
這代表我們要去找到 AppCompoment 所屬的的 NgModule ,
去看看 app.module.ts 中 declarations 了哪些 Component,
然後一個個去看那些 Component 有哪個的 selector 是 xxx-comonent,
例:
main.ts:
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

app.module.ts:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { OtherComponent1 } from './other1.component';
import { OtherComponent2 } from './other2.component';
// 可能更多 .........

@NgModule({
  declarations: [
    AppComponent,
    OtherComponent1,
    OtherComponent2
	// 可能更多 .........
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

但如果使用 Standalone Component,我們就不須要 NgModule、不需要 app.module.ts,
我們可以直接把 Standalone Component 常成根元件而不是 NgModule。

而使用 Standalone Component 的 Component 必須顯示的 import Standalone Component 進來,
例如假設 <xxx-component> 的 Component 是一個 Standalone Component,
,其 Class 長得像這樣,注意到 @Component 修飾子裡有加上 standalone: true:

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

@Component({
  selector: 'xxx-component',
  standalone: true,
  imports: [],
  templateUrl: './app.xxx-component.html',
  styleUrl: './app.xxx-component.scss'
})
export class AppXxxComponent {
  
}

上例 AppComponent 的 Component Class 就要改寫成:

import { Component } from '@angular/core';
import { XxxComponent } from './xxx-component.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [XxxComponent],
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {  
}

可以看到必須 imports XxxComponent 才能使用 XxxComponent 這個 Standalone Component,
這時我們就可以很清楚地知道 AppComonent 使用了 XxxComponent,
不用再辛苦地去找 XxxComponent 到底在哪裡。

另一個可以注意到的是 AppComponent 也加了 standalone: true,
所以 AppComponent 也是一個 Standalone Component,
我們就可以把 AppComponent 直接當作 Root Component 設定給 main.ts,像這樣:

import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

-------------------------------------------------------------------------------------------------------------
Interpolations (插值、或稱內嵌):
使用雙重大括弧 {{}} 來設定 Interpolations,以顯示 component 裡的 data,
例:
HTML:

{{myData}} 

-------------------------------------------------------------------------------------------------------------

Property Binding, Attribute Binding, Class and Style Binding:
這三種 Binding 都是使用中括弧來設定,且都是單向綁定,
當 HTML DOM 上的值被改變時,不會影響到 Component 中對應物件的值。
例如 <input type="text" [value]="inputValue"/> 的 Value 被使用者在畫面中修改後,
Component 中的 inputValue 物件值並不會被改變。

下面各別介紹:

Property Binding:
綁定 DOM 的 Property,注意跟 Attribute 不同,例如 <td> 的跨行 Property 是 colSpan,而對應的 Attribute 是 colspan。

範例:

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

@Component({
  selector: 'app-xxx',
  standalone: true,
  imports: [],
  template: `
              <a [href]="link">Link</a>
              <img [src]="imgSrc"/>
              <input type="text" [value]="inputValue"/>
              <table>
                  <tr>
                      <td [colSpan]="1+1">xxx</td>
                  </tr>
                  <tr>
                      <td>yyy</td>
                      <td>yyy</td>
                  </tr>
              </table>
            `,
  styleUrl: './xxx.component.scss'
})
export class XxxComponent {
  link = "https://www.cyberlink.com";
  imgSrc = "https://dl-file.cyberlink.com/web/stat/edms/prog/bar/img/Cyberlink.svg";
  inputValue = "xxx";
}

Attribute Binding:
綁定 Attribute,用 [attr.{要設定的 Attribute Name}] 來表示,可以跟上面 Property Binding 做比較,這邊要用 Attribute 的 colspan 而不是 Property 的 colSpan。

範例:

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

@Component({
  selector: 'app-xxx',
  standalone: true,
  imports: [],
  template: `
              <div [attr.aria-label]="ariaLabel">xxx</div>
              <table>
                  <tr>
                      <td [attr.colspan]="1+1">xxx</td>
                  </tr>
                  <tr>
                      <td>yyy</td>
                      <td>yyy</td>
                  </tr>
              </table>
            `,
  styleUrl: './xxx.component.scss'
})
export class XxxComponent {
  ariaLabel = "xxx";
}

Class and Style Binding:
綁定 Class Name 和 Style,可以接受多種輸入,詳細可以參考官方文件 Class and style binding • Angular
例如單個 Class Name 可以接受 Boolean 值做輸入、
多個 Class Name 可以用以空格分隔的 Class List String 或 Map (Key 代表 Class Name,Value 為 Boolean 值,代表要不要設定這個 Class)。

單個 Style 可以接受 String,
多個 Style 可以接受 inline css 的 String
或是 Map (Key 要用給 Javascript 用的 Style Property Name,不可以用給 CSS StyeSheet 用的 Property Name)

範例:

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

@Component({
  selector: 'app-xxx',
  standalone: true,
  imports: [],
  //templateUrl: './xxx.component.html',
  template: `
              <div [class.xxx-class-name]="isAddXxxClass">XXX</div>
              <div [class]="classListStr">XXX</div>
              <div [class]="classList">XXX</div>
              <div [class]="classMap">XXX</div>

              <div [style.background-color]="backgroundColor">xxx</div>
              <div [style.backgroundColor]="backgroundColor">xxx</div>
              <div [style]="styleListStr">xxx</div>
              <div [style]="styleMap">xxx</div>
            `,
  styleUrl: './xxx.component.scss'
})
export class XxxComponent {
  isAddXxxClass = true;
  classListStr = "class-1 class-2";
  classList = ["class-1", "class-2"];
  classMap = {
    "class-1" : true,
    "class-2" : false
  }

  backgroundColor = "#ff00ff";
  styleListStr = "background-color: #ff00ff; font-size: 20px;";
  styleMap = {
    backgroundColor : "#ff00ff",
    fontSize : "20px"
  };
}

-------------------------------------------------------------------------------------------------------------

Event Binding (事件綁定):
使用小括弧 () 來設定 Event Binding,類似於 AngularJS 的 ng-{Event 名} (ng-click, ng-change, etc....),
例:

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

@Component({
  selector: 'app-xxx',
  standalone: true,
  imports: [],
  template: `<input type="text" (input)="onInput($event)"/>
             <button (click)="onClick()">Button</button>`,
  styleUrl: './xxx.component.scss'
})
export class XxxComponent {
  onInput(event: Event) {
    console.log("Inpute Event.");
    console.log((event.currentTarget as HTMLInputElement).value);
  }

  onClick() {
    console.log("Click Event.");
  }
}

-------------------------------------------------------------------------------------------------------------

Two Way Data Binding (雙向綁定):
可參考官方文件 Two-way binding • Angular 和 Directives • Overview - ngModel • Angular

先來看比較原生的實作方式會比較好理解其中的原理。

比如我們現在有兩個 Component,Parent 和 Child,
ParentComponent 裡面使用了 ChildComponent,
ParentComponent 把一個 Property 的值輸入至 ChildComponent 中,
ChildComponent 會在值被改變時主動用 EventEmitter 送出 Event 出來給 ParentComponent,
ParentComponent 收到後就可以做相應的處理,
而 [(xxx)]="yyy"  語法是 [xxx]="yyy" (xxxChange)="yyy = $event;"
的合併簡易寫法。

範例:

Parent Component :

import { Component } from '@angular/core';
import { ChildComponent } from '../child/child.component';

@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [ChildComponent],
  template: `
    <app-child [xxx]="inputValue" (xxxChange)="inputValue = $event;"></app-child>
    <app-child [(xxx)]="inputValue"></app-child>
  `,
  styleUrl: './parent.component.scss'
})
export class ParentComponent {
  inputValue = "";
}

Child Component :

import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-child',
  standalone: true,
  imports: [],
  template: `
    <input type="text" [value]="xxx" (input)="onInput($event)"/>
  `,
  styleUrl: './child.component.scss'
})
export class ChildComponent {
  @Input() xxx!: string;
  @Output() xxxChange = new EventEmitter<string>();

  onInput(event: Event) {
    var value = (<HTMLInputElement>event.currentTarget).value;
    this.xxxChange.emit(value);
  }
}

實作上通常會使用 ngModel 來幫忙,跟上述一樣可以用非簡易跟簡易的兩種作法:

  1. 非簡易作法:比較麻煩的作法,但有時想在賦值前做一些處理時可用到,
    例如先把使用者在<input>中輸入的值改成全大寫後再賦值給 Component 的 Property 值。
    因為有用到 ngModel (其中寫好了 event 向外傳遞的過程等),要 Import FormModule 才能使用,
    先把 Component 的 Property 輸入給 ngModel 這個 Angular 內建的 Directive,
    ngModel 偵測值的改變,當值發生改變時用 EventEmitter 把值用 EventEmitter 向外送出一個 ngModelChange 的 Event 給外層 Component,
    然後外層 Component 接受到 ngModelChange 事件後,可以由 $event 得到改變後的值,
    這時我們就可以執行我們要的操作。

    範例:
    import { Component } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    
    @Component({
      selector: 'app-xxx',
      standalone: true,
      imports: [FormsModule],
      //templateUrl: './xxx.component.html',
      template: `
            <input type="text" [ngModel]="inputValue" (ngModelChange)="inputValue = $event;"/> {{inputValue}}
      `,
      styleUrl: './xxx.component.scss'
    })
    export class XxxComponent {
      inputValue = "xxx";
    }
  2. 簡易作法:比較簡潔的作法,並且也是官方的推薦雙向綁定寫法,
    使用 Angular 提供的 Banana in Box 語法,用 [(ngModel)]="xxx" 來實現,
    是上一個麻煩寫法的簡易寫法,一樣有用到 ngModel,所以要 import FormModule。

    範例:
    import { Component } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    
    @Component({
      selector: 'app-xxx',
      standalone: true,
      imports: [FormsModule],
      //templateUrl: './xxx.component.html',
      template: `
            <input type="text" [(ngModel)]="inputValue"/>
      `,
      styleUrl: './xxx.component.scss'
    })
    export class XxxComponent {
      inputValue = "xxx";
    }
    
    

-------------------------------------------------------------------------------------------------------------
Template Reference Variable (樣板參考變數):
使用 # 來標識 Template Reference Variable,
就可以在 Component Class 中建立一個指向特定對像的變數,根據標識的地方可以指向不同的對像,例如:

  1. 如果在 Component (元件)上聲明變數,該變數就會引用該組件實例。
  2. 如果在標準的 HTML DOM 標記上聲明變數,該變數就會引用該元素。
  3. 如果你在 <ng-template> 元素上聲明變數,該變數就會引用一個 TemplateRef 實例來代表此樣板。
  4. 如果該變數在右側指定了一個名字,比如 #var="ngForm",那麼該變數就會指向標識的元素上具有這個 exportAs 名字的 Directive 或 Component。
    可以參考 [Angular 大師之路] exportAs - 取得 directive 實體的方法 | 全端開發人員天梯
    例如一個 Directive 可以設定 exportAs 來讓 Host DOM 所處的 Component 可以存取 Directive Instnace。
    範例:
    import { Component } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import { Directive, HostBinding } from '@angular/core';
    
    @Directive({
      selector: '[xxx-directive]',
      standalone: true,
      exportAs: "xxxDirective"
    })
    export class MyDirectiveDirective {
      //會為此 Directive 依附的 DOM 加上 background-color style
      @HostBinding("style.backgroundColor") backgroundColor = "#00FF00";
    
      constructor() { }
    
      sayHello() {
        console.log("Hello!");
      }
    }
    
    @Component({
      selector: 'app-xxx',
      standalone: true,
      imports: [FormsModule, MyDirectiveDirective],
      template: `
        <div xxx-directive #abc="xxxDirective" (click)="abc.sayHello()">XYZ</div>
      `,
      styleUrl: './xxx.component.scss'
    })
    export class XxxComponent {
    }

 詳細可參考 Angular - 理解樣板變數

例下面的範例可以在 <button> 被按下時改變 #myTemplateVariable 標識的 <p> 的 innerHTML:

<p #myTemplateVariable>xxxxx ......</p>
<button (click)="myTemplateVariable.innerHTML = 'xxxxx clicked'">click me</button>

下面這個範例 Component 有 Import Form Module,
而其中的 ngForm 這個 Directive 因為有設定 <form> 為 selector 的關係,
所以 <form> 上會存在 ngForm 這個 Directive ,
而因為 ngForm Directive 本身有設定 exportAs: "ngForm" 的關係,
所以使用 #myForm="ngForm" 就可以指向在 <form> 上面的 ngForm Directive,進而取得 valid 等參數或去操作 form。

#firstName 和 #lastName 分別指向其上的 ngModel

** 注意:如果沒寫 "ngForm" ,只寫了 #myForm 的話,myForm 就會變成只指向 <form> 這個 HTML element DOM:

<form #myForm="ngForm">
    <div>First Name : <input type="text" name="firstName" [(ngModel)]="formData.firstName" #firstName="ngModel" required/></div>
    <div style="color: #ff0000;" [hidden]="firstName.valid || firstName.pristine">First name is required!</div>
    <div>Last Name : <input type="text" name="lastName" [(ngModel)]="formData.lastName" #lastName="ngModel" required/></div>
    <div style="color: #ff0000;" [hidden]="lastName.valid || lastName.pristine">Last name is required!</div>
    <div>Is From valid: {{myForm.form.valid}}</div>
</form>
-------------------------------------------------------------------------------------------------------------
Directives:
可用 Angluar CLI 的
ng generate directive {Directive 名稱}

分成
Attribute Directive (可參考 Attribute directives • Angular)
Structural Directive (可參考 Structural directives • Angular)

Directive 跟 Component 不一樣,沒有它自己的 template ,
主要是依附在其他的 Componenet 或者是 DOM 上,
selector 就是 css selector,可以指定 class, attribute 等,
通常是用來對被依附的 DOM 進行各項操作,
例如加新的 class 之類的,
這裡示範下 Attribute Directive 的用法。
(Angular 內建的 *ngIf, *ngFor 也是一種 Directive ,不過是屬於 Structural Directives)。

範例:
import { Component, Directive, ElementRef, HostBinding, HostListener, Input  } from '@angular/core';

@Directive({
  selector: '[xxx-directive]',
  standalone: true,
  exportAs: "xxxDirective"
})
export class XxxDirective {
  //使用 @HostBinding 設定一開始 color 的範例
  //可以跟 @Input 一起用,此例讓 defaultColor 可以接受來自 Host 來的值
  @HostBinding("style.color") @Input() defaultColor = "#FF0000";

  //可以用 ElementRef.nativeElement 來取得宿主 (Host) DOM 的 HtmlElement 物件
  constructor(private elementRef: ElementRef) {//
  }

  //可以使用 @HostListener 來設定宿主 DOM 的 EventBinding
  @HostListener("mouseenter") onMouseenter() {
    this.elementRef.nativeElement.style.backgroundColor = "#00FF00";
  }

  @HostListener("mouseleave") onMouseleave() {
    this.elementRef.nativeElement.style.backgroundColor = "";
  }

  sayHello() {
    console.log("Hello!");
  }
}

@Component({
  selector: 'app-xxx',
  standalone: true,
  imports: [XxxDirective],//
  //templateUrl: './xxx.component.html',
  template: `
    <div xxx-directive defaultColor="#0000FF" #xxxDir="xxxDirective" (click)="xxxDir.sayHello()">XYZ</div>
  `,
  styleUrl: './xxx.component.scss'
})
export class XxxComponent {
}

這裡示範 Structural Directive,例如 *ngIf 和 *ngFor ,
要注意必須先 import CommonModule (或是各別 import NgIf 和 NgFor) 才能正常使用。
Structural Directive 跟 Attribute Directive 不一樣的地方是它主要會改變 DOM 結構,
實作使用 ng-template 配合 Directive 來達成,而 *ngIf, *ngFor 這種寫法是 Angular 的語法糖,
自己也可以自己實作客製的 Structural Directive,詳細可參考 Structural directives • Angular 和 [Angular 大師之路] 自己的樣板語法自己做 (Structural Directives) | 全端開發人員天梯

Component Class:

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-my-component',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './my-component.component.html',
  styleUrl: './my-component.component.scss'
})
export class MyComponentComponent {
  isShow: boolean = true;
  myItemList: {id: number, name: string}[] = [{
    id: 1,
    name: "11"
  },{
    id: 2,
    name: "22"
  },{
    id: 3,
    name: "33"
  }];
}

my-component.component.html:

<div *ngIf="isShow">*ngIf test</div>

<li *ngFor="let myItem of myItemList">
    {{myItem.id}} : {{myItem.name}}
</li>

-------------------------------------------------------------------------------------------------------------
@-Syntax for Control Flow:
Angular 17 推出了 @-Syntax for Control Flow,
可以以更底層、不使用 Directive 的方式做出一樣的功能,
而且不用依附在 DOM 上面,以下示範介紹 @if, @else if, @else, @for, @switch 等:

Component Class:

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

@Component({
  selector: 'app-xxx',
  standalone: true,
  imports: [],
  templateUrl: './xxx.component.html',
  styleUrl: './xxx.component.scss'
})
export class XxxComponent {
  condition1: boolean = true;
  condition2: boolean = false;

  str = "A";
  strA = "A";
  strB = "B";

  myItemList: {id: number, name: string}[] = [{
    id: 1,
    name: "11"
  },{
    id: 2,
    name: "22"
  },{
    id: 3,
    name: "33"
  }];
}

my-component.component.html:

@if (condition1) {
    <p>condition 1</p>
}
@else if (condition2) {
    <p>condition 2</p>
} @else {
    <p>other conditions</p>
}

@switch (str) {
    @case (strA) {
        <p>A</p>
    }
    @case (strB) {
        <p>B</p>
    }
    @default {
        <p>other</p>
    }
}

@for (myItem of myItemList; track myItem.id) {
    <li>{{myItem.id}} : {{myItem.name}}</li>
}

-------------------------------------------------------------------------------------------------------------
@Input:
當一個 Parent Component 使用了另一個 Child Component,
並且 Parent Component 想傳遞某個值進 Child Component,
並且想要在 Child Component 中顯示、操作修改這個值時,
我們就要在 Child Component 中使用 @Input 來告訴 Angular 這個值是從外部傳入的。

例如:

my-component.component.ts (我們的 Parent Component Class):

import { Component } from '@angular/core';
import { SubComponentComponent } from '../sub-component/sub-component.component';

@Component({
  selector: 'app-my-component',
  standalone: true,
  imports: [SubComponentComponent],
  templateUrl: './my-component.component.html',
  styleUrl: './my-component.component.scss'
})
export class MyComponentComponent {
  myItemList: {id: number, name: string}[] = [{
    id: 1,
    name: "11"
  },{
    id: 2,
    name: "22"
  },{
    id: 3,
    name: "33"
  }];

  selectedMyItem = this.myItemList[1];
}
my-component.component.html:
<app-sub-component [item]="selectedMyItem"></app-sub-component>
sub-component.component.ts (我們的 Child Component Class):
import { Component, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-sub-component',
  standalone: true,
  imports: [CommonModule, FormsModule],
  templateUrl: './sub-component.component.html',
  styleUrl: './sub-component.component.scss'
})
export class SubComponentComponent {
  @Input() item?: {id: number, name: string};
}
sub-component.component.html:
<div *ngIf="item">
    Id: <input type="text" [(ngModel)]="item.id"/><br/>
    Name: <input type="text" [(ngModel)]="item.name"/>
</div>

這邊要注意一下,Child Component 的 item 在 Angular 一開始建構各元件時,
可能會是 undefined 的,所以我們要用 *ngIf (或@if) 來設定有 item 值時才顯示 item,
不然 typescript 編譯時會有
NG2: Object is possibly 'undefined'.
錯誤。

-------------------------------------------------------------------------------------------------------------
@Output:
@Input 的用途是讓 Parent Component 向 Child Component 輸入資料,
而 @Output 剛好相反,是用來讓 Child Component 向 Parent Component 送出資料,
在前面 Two-way Binding 的範例有用到,這邊再貼一次,
主要就是用 @Output 修飾 Child Component 的 EventEmitter property,
然後 Child Component 主動地用 EventEmitter 將資料以 Event 的形式向 Parent Component 送,
Parent Component 就可以用 Event Binding 的方式接收資料。

範例:

Parent Component :

import { Component } from '@angular/core';
import { ChildComponent } from '../child/child.component';

@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [ChildComponent],
  template: `
    <app-child [xxx]="inputValue" (xxxChange)="inputValue = $event;"></app-child>
    <app-child [(xxx)]="inputValue"></app-child>
  `,
  styleUrl: './parent.component.scss'
})
export class ParentComponent {
  inputValue = "";
}

Child Component :

import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-child',
  standalone: true,
  imports: [],
  template: `
    <input type="text" [value]="xxx" (input)="onInput($event)"/>
  `,
  styleUrl: './child.component.scss'
})
export class ChildComponent {
  @Input() xxx!: string;
  @Output() xxxChange = new EventEmitter<string>();

  onInput(event: Event) {
    var value = (<HTMLInputElement>event.currentTarget).value;
    this.xxxChange.emit(value);
  }
}
-------------------------------------------------------------------------------------------------------------
Service (服務):
可以用 Angular-CLI 的以下指令建立:
ng generate service {Service 名稱}

Service 會使用 @Injectable 的 Annotation (註解) 來設定,
我們可以把 Service 注入到多個不同的 Component Constructre function 中,
這樣就可以讓多個 Component 共用同個 Service 來共享資訊、
或者把資料的取得移到 Service 裡來讓 Component 可以專注在自己的其他邏輯功能上。

範例:

my-service.service.ts (我們建立的 Service) :
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class MyServiceService {

  constructor() { }

  getDataList(): {id: number, name: string}[] {
    return [{
      id: 1,
      name: "11"
    },{
      id: 2,
      name: "22"
    },{
      id: 3,
      name: "33"
    }];
  }
}
Component Class (使用 Service 取得資料):
import { Component, OnInit } from '@angular/core';
import { MyServiceService } from '../service/my-service.service';

@Component({
  selector: 'app-my-component',
  standalone: true,
  imports: [],
  templateUrl: './my-component.component.html',
  styleUrl: './my-component.component.scss'
})
export class MyComponentComponent implements OnInit {
  myItemList: {id: number, name: string}[] = [];
  selectedMyItem?:{id: number, name: string};

  constructor(private myService: MyServiceService) {
  }

  ngOnInit(): void {
    this.myItemList = this.myService.getDataList();
    this.selectedMyItem = this.myItemList[1];
  }
}
-------------------------------------------------------------------------------------------------------------
Observable (可觀察):
Angular 有很多異步操作 (非同步操作 或稱 Asynchronous) 都利用到了
RxJs 的 Observable ,
其類似於 Javascript 的 Promise、 JQuery 的 Deferred, Promise 或 AngularJS 的 $q, promise,
可以處理 Asynchronous 非同步的動作,例如非同步的 HttpGet 資料取得,
將資料包裹在 Observable 物件中,
然後我們就可以用 Observable 的 subscribe() 來非同步的處理動作。
範例:

my-component.component.ts :
import { Component, OnInit } from '@angular/core';
import { MyServiceService } from '../service/my-service.service';

@Component({
  selector: 'app-my-component',
  standalone: true,
  imports: [],
  templateUrl: './my-component.component.html',
  styleUrl: './my-component.component.scss'
})
export class MyComponentComponent implements OnInit {
  myItemList: {id: number, name: string}[] = [];
  selectedMyItem?:{id: number, name: string};

  constructor(private myService: MyServiceService) {
  }

  ngOnInit(): void {
    this.myService.getDataListAsync().subscribe((dataList: {id: number, name: string}[]) => {
      this.myItemList = dataList;
      this.selectedMyItem = this.myItemList[1];
    });
  }
}

my-service.service.ts:
import { Injectable } from '@angular/core';
import { Observable, delay, of} from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class MyServiceService {

  constructor() { }
  
  getDataListAsync(): Observable<{id: number, name: string}[]> {
    let dataList: {id: number, name: string}[] = [{
      id: 1,
      name: "11"
    },{
      id: 2,
      name: "22"
    },{
      id: 3,
      name: "33"
    }];

    return of(dataList).pipe(delay(5000)); //模擬網路傳輸,延遲 5 秒再送出資料
  }
}

-------------------------------------------------------------------------------------------------------------
Routing (路由):
Routing (路由) 可以指定不同的 url 去載入不同的 Component ,作用類似於 AngularJS 的 <ui-view>, $urlRouterProvider, $stateProvider 那些,下面示範:

main.ts (程式主要進入點):
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));
在 main.ts 中,可以看到 bootstrapApplication() 設定了一個作為 root component 的 AppComponent 和一個 config 設定用的 appConfig,我們來看一下 appConfig 的內容:

app.config.ts :
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
};
app.config.ts 把一個實作了 ApplicationConfig interface export 出來,用到了 provideRouter(routes),其中 routes 就是我們要來設定 Route Rule (路由規則) 的地方,先來看一下 app.routes.ts 的內容。

app.routes.ts (路由規則) :
import { Routes } from '@angular/router';
import { MyComponentComponent } from './my-component/my-component.component';
import { MyComponent2Component } from './my-component-2/my-component-2.component';

export const routes: Routes = [
    {
        path: "my-component",
        component: MyComponentComponent
    }, {
        path: "my-component-2",
        component: MyComponent2Component
    }, {
        path: "",
        redirectTo: "my-component",
        pathMatch: "full"
    }
];
這裡我們設定了預設會轉到的 path 是 /my-component,然後當 path 是 /my-component 時使用 MyComponentComponent 這個 Component,當 path 是 /my-component-2 時使用 MyComponent2Component。

我們先看一下 MyComponentComponent 和 MyComponent2Component 的內容。

my-component.component.ts (就是 MyComponent):
import { Component } from '@angular/core';

@Component({
  selector: 'app-my-component',
  standalone: true,
  imports: [],
  template: `<p>my-component works!</p>`,
  styleUrl: './my-component.component.scss'
})
export class MyComponentComponent {

}
my-component-2.component.ts (就是 MyComponent2Component ) :
import { Component } from '@angular/core';

@Component({
  selector: 'app-my-component-2',
  standalone: true,
  imports: [],
  template: `<p>my-component-2 works!</p>`,
  styleUrl: './my-component-2.component.scss'
})
export class MyComponent2Component {

}
兩者的內容都一樣,只是在 html template 上印出不同句子以示區別而已。

接著最後就是作為 root 的 AppComponent

app.component.ts (就是 AppComponent,為了) :
import { Component } from '@angular/core';
import { RouterLink, RouterOutlet } from '@angular/router';
import {MyComponentComponent} from './my-component/my-component.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, RouterLink, MyComponentComponent],
  //templateUrl: './app.component.html', //也可以把 html 寫在另一個 html 檔裡
  template: `
              <div><a routerLink="/my-component">My Component</a></div>
              <div><a routerLink="/my-component-2">My Component 2</a></div>
              <router-outlet></router-outlet>
            `,
  styleUrl: './app.component.scss'
})
export class AppComponent {
  title = 'my-angular-application';
}

在 AppComponent 中,因為此例使用了 routerLink 和 <router-outlet> ,所以我們要  import RouterLink 和 RouterOutlet 進來。

routerLink 可以作為 DOM 的屬性來設定對應到 Route 規則的 path 到 <a> 上,<a> 會被賦予相應的 href,當 <a> 被按下時,就會改變瀏覽器網址列的網址,
而當網址符合設定的 Route 規則時,
<router-outlet> 就會載入相應的 Component。

-------------------------------------------------------------------------------------------------------------

使用 ActivatedRoute 讀取 url path 中的參數:
使用 ActivatedRoute 可以讀我們方便的讀取 url path 中的參數,
例如如果我們有 Route 規則設定了如下:

app.routes.ts:

import { Routes } from '@angular/router';
import { MyComponentComponent } from './my-component/my-component.component';
import { MyComponent2Component } from './my-component-2/my-component-2.component';

export const routes: Routes = [
    {
        path: "my-component/:id",
        component: MyComponentComponent
    }, {
        path: "my-component-2",
        component: MyComponent2Component
    }, {
        path: "",
        redirectTo: "my-component/1",
        pathMatch: "full"
    }
];

path 中的冒號(:)表示 :id 是一個佔位符,它符合像 /my-component/1, /my-component/2 等這樣的 path。

接著在 MyComponent 中我們可以如下地使用 ActivateRoute.snapshot.paramMap.get("id") 取出 :id 佔位符的值,如 /my-component/1 這個 path 的 id 就會是 1、/my-component/2 就是 2。

my-component.component.ts :

import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-my-component',
  standalone: true,
  imports: [],
  template: `<p>my-component works!</p>`,
  styleUrl: './my-component.component.scss'
})
export class MyComponentComponent {

  constructor(private route: ActivatedRoute) {

  }

  ngOnInit(): void {
    console.log(this.route.snapshot.paramMap.get("id")); // 印出 url path 中 :id 佔位符的實際值
  }
}

-------------------------------------------------------------------------------------------------------------

使用 Location 可以操作一些跟瀏覽器相關的操作,例如回到上一頁 (跟直接連到上一頁的網址不同,會直接影響瀏覽歷史紀錄,就跟瀏覽器的上一頁操作一樣),一樣以 MyComponent 為例,下例的 goBack() 函式如果被呼叫的話就可以回到上一頁:

my-component.component.ts :

import { Component } from '@angular/core';
import { Location } from '@angular/common';

@Component({
  selector: 'app-my-component',
  standalone: true,
  imports: [],
  template: `<p>my-component works!</p>`,
  styleUrl: './my-component.component.scss'
})
export class MyComponentComponent {

  constructor(private location: Location) {

  }

  goBack(): void {
    this.location.back();
  }
}

-------------------------------------------------------------------------------------------------------------

HttpClient:

Angular 提供了 HttpClient 工具來幫助進行 HTTP 呼叫,以 MyComponent 為例:

my-component.component.ts:

import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-my-component',
  standalone: true,
  imports: [],
  template: `<p>my-component works!</p>`,
  styleUrl: './my-component.component.scss'
})
export class MyComponentComponent implements OnInit{

  constructor(private http: HttpClient) {

  }

  ngOnInit(): void {
    this.getData().then((data) => {
      console.log(data);
    });
    this.sendData();
  }

  getData(): Promise<string> {
    //HttpClient.get() 回傳的是 RxJs 的 Observable,要呼叫 Observable 的 subscribe() 才會執行 HTTP GET 呼叫,
    //所以這裡為了之後可以利用 subscribe(data) 的 data ,使用了 Promise 把 http.get() 包起來並把 data 放入 resolve(data) 中,
    //之後可以用 Promise.then((data) => {}) 來得到 data。
    return new Promise<string>((resolve) => {      
      this.http.get("https://xxx.xxx.xxx/xxx", { responseType: "text" }) //沒給 responseType 的話預設是 JSON           
             .subscribe((data: string) => {
                resolve(data);
             });
    });
  }

  sendData(): Promise<void> {
     //HttpClient.post() 回傳的是 RxJs 的 Observable,要呼叫 Observable 的 subscribe() 才會執行 HTTP POST 呼叫
     //所以這裡為了將 subscribe(data) 的 data 回傳,使用了 Promise 把 http.get() 包起來並把 data 回傳。
     //這邊展示如果 resolve() 沒有想要塞值的話,在 typescript 裡可以用 new Promise<void> 來指定 void ,不然 typescript 會報錯。
     return new Promise<void>((resolve) => {
        this.http.post("https://yyy.yyy.yyy/yyy", {data: "tttest"})
                 .subscribe(() => {
                    resolve();
                 });
     });   
  }

}


在 MyComponent 中我們先在 constructor 中注入了 HttpClient 來使用,示範了利用 HttpClient 來進行 HTTP GET 和 HTTP POST 的呼叫,
需要注意到的是,HttpClient.get() 和 HttpClient.post() 回傳的是 RxJs 的 Observable ,屬於 Cold Observable ,不像 Hot Observable 不管有沒有被 subscribe() 都會執行內容, Cold Observable 需要被執行 subscribe() 才會執行內容,詳細可以參考這篇 [RxJS] Cold Observable v.s Hot Observable | 全端開發人員天梯,比較是屬於 RxJS 的東西。

-------------------------------------------------------------------------------------------------------------

Form (表單) 相關

Angular 的 Form 可以用兩種方法來製作,分成 Template-Driven Form (樣板驅動表單) 和 Reactive Form (回應式表單) 兩種,
Template-Driven Form 跟 AngularJs 的方法較為相似,適合簡單的 Form,實作也較簡單易懂。Reactive Form 適合較複雜的 Form,實作一些複雜功能會較有彈性,複用性比較高、也較易於測試。下面分別示範:

Template-Driven Form (樣板驅動表單) :
可參考 Angular - 建立樣板驅動表單
下面直接給範例程式,說明都放在註解中

my-form.component.ts

import { Component } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';

@Component({
  selector: 'app-my-form',
  standalone: true,
  imports: [FormsModule], /* 使用到了 FormsModule */
  templateUrl: './my-form.component.html',
  styleUrl: './my-form.component.scss'
})
export class MyFormComponent {
  formData: { firstName: string, lastName: string } = {
    firstName: "",
    lastName: ""
  };

  submitForm(form: NgForm) {
    console.log("submit!");
    console.log(form.form.valid);
    //因為 NgForm 有把 NgForm.form 上很多屬性複製到 NgForm 本身上,
    //所以直接用 NgForm.valid 也可以存取 NgForm.form.valid
    console.log(form.valid);
    console.log(form.value.firstName);
    console.log(form.value.lastName);
    //NgForm 有些好用的 function,例如重置 Form 狀態的 reset()
    form.reset();
  }
}


my-form.component.html

<p>my-form works!</p>
<!-- 使用 Template Reference Variable, #myForm 來得到 ngForm Directive 的實體對像 -->
 <!-- 在 form 被 submit 時觸發 ngSubmit Event,在這裡可以將 ngForm Directive 實體傳入 -->
<form #myForm="ngForm" 
      (ngSubmit)="submitForm(myForm)">
    <!-- 跟 #myForm 類似,這裡是用 #firstName 取得 ngModel 的實體對像 -->
    <!-- 用 [(ngModel)] 設定雙向繫結 -->
    <div>First Name : <input type="text" name="firstName" 
                                         [(ngModel)]="formData.firstName"
                                         #firstName="ngModel" 
                                         required/></div>
    <!-- 利用得到的名為 firstName 的 ngModel 來存取其 valid, pristine 等值 -->
    <div style="color: #ff0000;" 
        [hidden]="firstName.valid || firstName.pristine">
        First name is required!
    </div>

    <div>Last Name : <input type="text" name="lastName" [(ngModel)]="formData.lastName" #lastName="ngModel" required/></div>
    <div style="color: #ff0000;" [hidden]="lastName.valid || lastName.pristine">Last name is required!</div>

    <div>Is Form valid: {{myForm.valid}}</div>

    <div>
        <!-- 利用取得的名為 myForm 的 ngForm 存取 form 的 valid 等值,也可以用 myForm.valid,因為 form 上的大部份屬性在 ngForm 上都有一部份複本  -->
        <button type="submit" 
                [disabled]="!myForm.form.valid">
                Submit
        </button>
    </div>
</form>

Reactive Form (回應式表單):
可參考 Angular - 回應式表單

my-reactive-form.component.ts

import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormArray, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { first } from 'rxjs';

@Component({
  selector: 'app-my-reactive-form',
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule], //在這裡要使用 ReactiveFormsModule
  templateUrl: './my-reactive-form.component.html',
  styleUrl: './my-reactive-form.component.scss'
})
export class MyReactiveFormComponent implements OnInit {
  myForm!: FormGroup;
  
  
  constructor(private formBuilder: FormBuilder) {

  }
  ngOnInit(): void {
    //設定 FormGroup 和 FormControl,跟 Template-Driven Form 不同,
    //Form 的各元件顯示地被建立在 Javascript code 中,
    //可以很方便的操作和控制。
    //值得注意的是,FormGroup 也是可以塞進 FormGroup, FormArray 中的。
    this.myForm = new FormGroup({
      firstName: new FormControl("", Validators.required),
      lastName: new FormControl("", [Validators.required, Validators.maxLength(10)]),
      address: new FormGroup({
        country: new FormControl("", Validators.required),
        city: new FormControl("", Validators.required)
      }),
      favorites: new FormArray([
        new FormControl("", Validators.required)
      ])
    });

    //也可以用 FormBuilder 幫助用較簡化的方式設定 FormGroup 和 FormControl,
    //作用跟上面的程式完全一樣
    /*
    this.myForm = this.formBuilder.group({
      firstName : ["", Validators.required],
      lastName : ["", [Validators.required, Validators.maxLength(10)]],
      address: this.formBuilder.group({
        country : ["", Validators.required],
        city : ["", Validators.required]
      }),
      favorites: this.formBuilder.array([
        this.formBuilder.control("", Validators.required)
      ])
    });
    */
  }

  //在這邊設定一個 getter 來讓我們在 html 中可以直接用 favoritesFormArray 這個屬性名直接存取 favorites 這個 FormArray
  //像是這樣: <div *ngFor="let favorite of favoritesFromArray.controls; let i = index;"></div>
  //**因為不能寫成這樣: <div *ngFor="let favorite of (myForm.get('favorites') as FromArray)?.controls; let i = index;"></div>
  get favoritesFromArray(): FormArray {
    return this.myForm.get("favorites") as FormArray;
  }

  //實作動態加入 FormControl 到 FormArray 中
  addMoreFavoriteField() {
    //(this.myForm.get("favorites") as FormArray).push(new FormControl("", Validators.required));
    //因為有設定 favoritesFromArray 這個 getter,所以也可以用下面寫法達到一樣效果
    this.favoritesFromArray.push(new FormControl("", Validators.required));
  }

  //實作動態從 FormArray 中移除 FormControl
  removeFavoriteField(index: number) {
    //(this.myForm.get("favorites") as FormArray).removeAt(index);
    //因為有設定 favoritesFromArray 這個 getter,所以也可以用下面寫法達到一樣效果
    this.favoritesFromArray.removeAt(index);
  }

  //測試 FormGroup.setValue()
  setValueTest() {
    //FormGroup.setValue() 可以設定子元件的值,如果結構不對 (例如 address 沒用 Object 格式) 或有其他問題會有 Error
    this.myForm.setValue({
      firstName : "FirstName",
      lastName : "LastName",
      address : "country" //這裡 address 的型別錯了,應該是要物件才對,所以執行會有 Error
    });
  }

  //測試 FormGroup.patchValue()
  patchValueTest() {
    //Formgroup.patchValue() 也可以設定子元件的值,但它會盡可能的去設定,如果結構不對 或有其他問題也不會有 Error
    this.myForm.patchValue({
      firstName : "FirstName",
      lastName : "LastName",
      address : "country" //這裡 address 的型別錯了,應該是要物件才對,但執行不會有 Error,會直接惣略此欄位
    });
  }

  submitForm() {
    console.log(this.myForm.value);
  }
}

my-reactive-form.component.html

<p>my-reactive-form works!</p>
<!-- 設定 formGroup, myForm 就是我們在 Component 中設定的 FormGroup 物件 -->
<!-- 在 form 被 submit 時觸發 -->
<form [formGroup]="myForm"
      (ngSubmit)="submitForm()">
    <!-- 設定 formControlName, firstName 就是我們在 Component 中設定的名為 myForm 的 FormGroup 中的名為 firstName 的 FormControl 物件 -->
    <div>First Name : <input type="text" name="firstName" formControlName="firstName"/></div>
    <!-- 用 FormGorup.get(FromControl 的名字) 取得其下的各 FormControl,再取得各 Form control 的 valid, pristine 等值 -->
    <!-- 也可以用 FormGroup.controls 來取得各 FormControl -->
    <div style="color: #ff0000;" 
        [hidden]="myForm.get('firstName')?.valid || myForm.controls['firstName'].pristine">
        First name is required!
    </div>

    <div>Last Name : <input type="text" name="lastName" formControlName="lastName"/></div>
    <div style="color: #ff0000;" [hidden]="myForm.get('lastName')?.valid || myForm.get('lastName')?.pristine">Last name is required!</div>

    <!-- 用 formGroupName 設定 FormGroup 中的 FormGroup 成員 -->
    <div formGroupName="address">
        <div>Country : <input type="text" name="country" formControlName="country"/></div>
        <div>City : <input type="text" name="city" formControlName="city"/></div>
    </div>

    <div>Address Form Status: {{myForm.get('address')?.status}}</div>

    <div>Is From valid: {{myForm.valid}}</div>
    <div>Form Status: {{myForm.status}}</div>

    <!-- 設定 FormArray,其中的 FormController 可以不用有 Key 名稱 -->
    <div formArrayName="favorites">
        <div *ngFor="let favorite of favoritesFromArray.controls; let i = index;">
            <!-- FormArray 中的各子項元件是用 Array index 去做 key,所以要用 index 做 fromControlName 的設定, -->
            <!-- formControlName 外的中括弧代表等號右邊是 Expression ,而非純 String -->
            Favorite {{i}} : <input type="text"  [formControlName]="i"/>
            <button (click)="addMoreFavoriteField()">Add</button>
            <button (click)="removeFavoriteField(i)">Remove</button>
        </div>
    </div>

    <div><button (click)="setValueTest()">Set value test</button></div>
    <div><button (click)="patchValueTest()">Patch value test</button></div>

    <div>
        <!-- 利用取得的名為 myForm 的 ngForm 存取 form 的 valid 等值,也可以用 myForm.valid,因為 form 上的大部份屬性在 ngForm 上都有一部份複本  -->
        <button type="submit" 
                [disabled]="!myForm.valid">
                Submit
        </button>
    </div>
</form>

2019年7月18日 星期四

Typescript + Webpack (使用 ts-loader ) 練習

在"Typescript + Webpack (or SystemJs) 練習"這篇文章中,使用了多次指令分別去:
1. 用 npm 去 install 要的工具
2. 用 tsc 去使用 typescript 編譯
3. 用 webpack 去使用 webpack 打包程式

這次要來紀錄只使用 npm 指令,
因為 npm 的 package.json 可以設定 script 來方便的執行 command line 的指令,
而 webpack 可以用各種 loader 來處理項任務,其中 ts-loader 可以處理 typescript 的任務,
所以我們可以讓npm 去呼叫 webpack ,webpack 去呼叫 typescript 去完成任務。

這次的檔案結構跟上次差不多,如下所示:

其中,js 資料夾裡的檔案是 webpack 搭配 ts-loader 編擇 src資料夾下檔案所產生出來的,
package-lock.json不用管它,因為是npm 自已產生的。

我們先看看 Greeter.ts 和 test.ts :
Greeter.ts :
export class Greeter{
    name : string;
    constructor(name :string){
        this.name = name;
    }
    greet(){
        console.log("Hello, HIHI, " + this.name);
        alert("Hello, HIHI, " + this.name);
    }
}

test.ts :
import {Greeter} from "./Greeter";
 
let name : string = "Hugo";
let greeter : Greeter = new Greeter(name);
greeter.greet();

可以注意的是,在 test.ts 中 import 的Greeter可以只有檔名而沒有副檔名,因為在webpack.config.js中,有設定resolve {extensions : ['.js', '.ts']} 的設定。
接著我們來看看 npm的package.json、tyscript的tsconfig.json和webpack的webpack.config.js
三個 config 檔的內容:


package.json :
{
  "name": "webpacktypescripttest",
  "scripts": {
    "devEnvBuild": "npm install --save-dev typescript ts-loader webpack webpack-cli",
    "deploy": "webpack"
  },
  "devDependencies": {
    "ts-loader": "^6.0.4",
    "typescript": "^3.5.3",
    "webpack": "^4.36.1",
    "webpack-cli": "^3.3.6"
  }
}

tsconfig.json :
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "sourceMap": true,
    // "outDir": "./dist", //不需要
    "strict": true,
    "esModuleInterop": true    
  }
}

webpack.config.js :
var path = require('path');
module.exports = {
    entry: "./src/test.ts",
    output: {
        path: __dirname + "/js",
        filename: "test.js"
    },
    watch: false,
    devtool : 'source-map',
    mode : "production",
    resolve : {
     extensions : ['.js', '.ts']
    },
    module: {
     rules: [
       { 
        test: /\.ts$/, 
        use: 'ts-loader',
        include: path.resolve(__dirname, 'src')
       }
     ]
 }
}

可以注意到,在 tsconfig.json 中,我們不需要寫 outDir 來指定 typescript 的輸出位置,
其實就算指定了,經過 webpack 用 ts-loader 來執行 typescript 時,
也不會在 outDir 指的地方出現輸出的檔案。

先假設整個環境除了 npm 以外都沒建置完成,即 webpack, typescript, ts-loader.... 等
先用 command line 視窗到 webpackTypescriptTest 資料夾的路徑,
執行指令:
npm run devEnvBuild
就可以執行已經寫在 package.json 中,script 裡,devEnvBuild的指令,等同於執行
npm install --save-dev typescript ts-loader webpack webpack-cli
接著再執行
npm run deploy
就可以發現在 js 資料夾中,檔案被產生出來了,其中已經包括了 typescript的編擇和 webpack 的打包
可以注意到的是,在 webpack.config.js 中 ,
我們用了 module 設定了 ts-loader 來為 ts 檔在打包前進行了 typescript 的編譯。

最後我們在 index.html 中就可以使用 src/test.js 了 :

index.html :

 <head>
  <script src="js/test.js"></script>
 </head>
 <body> 
 </body>
</html>

原始碼下載:
webpackTypescriptTest.7z

參考資料:
  1.  TypeScript | webpack
  2. TYPESCRIPT合成WEBPACK中

2017年5月21日 星期日

Typescript + Webpack (or SystemJs) 練習

今天要來練習使用Typescript編寫Javascript,順便紀錄下如果JS Module的使用方式。

Typescript可以讓我們編寫的 ts 檔編譯成 js 檔,而如果有在 ts 檔裡撰寫Module的語法,
例如export, import等語句,Typescript可以依我們指定的module spec 來將 ts 檔編譯成不同
寫法的 js 檔,而可提供的有例如 amd, system, commonjs 等。

這裡會練習以下項目;

模組化 JS 執行工具:
SystemJs是一個可以執行JS Module的工具,
支援許多module spec,目前有
esm (ECMAScript Module),
cjs (CommonJS),
amd (Asynchronous Module Definition),
global (Global shim module format),
system (System.register or System.registerDynamic compatibility module format),
可以使用在node.js及Web應用中,在Web應用中,
可以在頁面上真接用 <script src="XXX/system.js"></script> 引入,
然後直接寫Javascript code 並指定程式進入點即可。

Webpack是一個功能強大的模組化打包工具,可以將多個資源(JS, CSS, images...)打包成一個檔案,可以解析例如commonjs的module spec,主要以命令列的方式使用,也可搭配其他外掛使用。

在這邊我們會先練習利用Typescript編譯 ts 檔,並且練習兩種不同的 module spec ,
system 和 commonjs, Typescript 對不同的 module spec 編譯出不同的 js 檔,
然後
system 我們會利用 SystemJs 來執行模組化的 js。
commonjs 我們會利用 Webpack 來打包編譯好的 js 檔成一個可執行的模組化 js。

Typescript IDE :
在這邊,可以準備一個有 Typescript 功能的 IDE,例如在這邊我使用 Visual Studio Code ,它有不錯的 Typescript 提供檢查功能及許多外掛,並且本身乾淨可以當一般的編輯器使用,當然也可以根據開發環境使用想應的工具外掛,例如 Netbeans 及 Eclipse (可以參考"Angular2簡易安裝使用 - Eclipse + Typescript plugin + System.js") 也有人提供Typescript外掛,享受使用 Typescript 的好處。
** Note **
Visual Studio Code 如果看不到JS、Typescript的提示文字,可以用"系統管理員"的方式開啟,應該就會出來了。

Typescript 的編譯:
在這邊為了了解最源頭的運作,我選擇使用最單純的方式,用 npm 安裝了 Typescript 後,直接用 tsc 指令配合 tsconfig.json 來執行編譯,許多開發環境的外掛,例如 Visual Studio Code 自己就有編譯Typescript的能力,不過其實最底層也都基本是呼叫 tsc 指令來完成工作。

簡易Server:
這邊使用了 lite-server 來在專案當下目錄建立簡易Server,因為Module JS的寫法會用到Ajax的呼叫,所以需要建立Server,當然要用其他例如webpack-dev-server、Netbeans, Eclipse 配合 Tomcat 之類的也是OK。

=========================================

首先,請先安裝 node.js (自動有 npm),
在全局安裝 Typescript、Webpack
npm install typescript -g
npm install webpack -g

2016年12月11日 星期日

Angular 簡易安裝使用 - Eclipse + Typescript plugin + System.js

Angular 的配置比起它的前一代AngularJs來說,稍微麻煩一點,因為涉及了typescript和module的使用。

在這裡我要介紹在Eclipse的開發環境中,以一個簡單的Angular 範列來說明如何簡單的在Eclipse中設置Angular 的初始設置。Typescript使用了Eclipse的typescript外掛、module使用了System.js。


需準備的步驟如下:

  1. node.js因為等下會使用到npm (Angular CLI),並且Eclipse的Typescript也需要用到node.js的功能,所以我們要先安裝node.js。
    node.js官網
  2. git for windows
    因為Angular CLI會需要用到git,所以需安裝windows版本的git。
    windows版本下載
  3. Angular CLI (選用)
    一個對於建置Angular 專案非常方便的工具,不過這裡為了要學習自己建置,所以只利用了Angular CLI來取得Angular 所需的modules,取得modules不一定要用Angular CLI,所以為選用。
    Angular CLI官方網站
  4. system.js
    這裡我們使用了system.js來使用javascript module,但也可選擇其他的module工具,例如webpack

取得Angular 所需的modules
我們可以利用node.js的npm來安裝Angular 所需的modules (會放到node_modules資料夾中),
但這裡我們也可以利用Angular CLI來簡單取得modules,
安裝好Angular CLI後,可以在命令列視窗中使用
ng new XXX
來在當下資料夾中建立Angular專案,在建立好的專案資料中,可以
找到Angular 所需的modules已經都下載後放在node_modules資料夾中

Eclipse (這裡安裝的是neon版本) + Typescript plugin
Help --> Install New Software...
--> 加入一個連結 : http://eclipse-update.palantir.com/eclipse-typescript/
下載安裝Eclipse的Typescript plugin

建立我們的專案
以下是我們的檔案結構圖

其中紅色框裡的是我們會建立的,藍框則是Typescript plugin幫我們編譯產生的。
在這裡Typescript plugin不是也不需使用如tsconfig.json的設定檔,而是直接在Eclpse裡設定,
對著專案按右鍵,選擇 Properties --> Typescript 可以看到如下畫面,其中
Source folder(s): ts檔放置的位置
Exported folder(s): 在 import module時,預設的路徑位置,例如node_modules之下的import路徑不用再打node_modules,預設會先重這裡找
Output folder: ts編譯成的js檔要放置的位置
接著設定 Compiler,這邊可以位照下面的設定就好,紅色框是比較重要的設定
再來就可以開始製作Angular 的範例了。

/index.jsp:
<%@ page language="java" contentType="text/html; charset=BIG5" pageEncoding="BIG5"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=BIG5">
<title>Insert title here</title>

<script src="node_modules/core-js/client/shim.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="app/js/system.js"></script>
<script>
 System.config({
  defaultJSExtensions: true,
  paths: {
   'app/*': './app/*',
   '@angular/*': './node_modules/@angular/*',
   'rxjs/*': './node_modules/rxjs/*'
  },
  packageConfigPaths: ['./node_modules/@angular/*/package.json']
 });
 SystemJS.import('app/js/index.js');
</script>
</head>
<body>
 <my-app>Loading...</my-app>
</body>
</html>
其中Angular所需的shim.js、Reflect.js、zone.js雖然有被放在node_modules中,但因為它們並沒有寫成modules的型式,所以要直接用<script>來載入。
在這邊我們使用了system.js來載入module,System.config()設置了module的載入設定,以下說明:
defaultJSExtension: 如果為ture,則import module時可以不用寫副檔名,預設為js
paths:規定了module查找路徑,需注意的是在這裡除了使用了@angular之下的module,還有rxjs之下的module。
packageConfigPaths:預設先去尋找指定路徑資料夾內的package.json設定檔,如果找到了main屬性,以main指定的js檔案為要import的module

設定完後,用SytemJS.import()來載入此頁要用的,一開始的js程式。

/ts/index.ts:
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
const platform = platformBrowserDynamic();
platform.bootstrapModule(AppModule);

/ts/app.module.ts:
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent }   from './app.component';
@NgModule({
  imports:      [ BrowserModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ]
})
export class AppModule { }

/ts/app.component.ts
import { Component } from '@angular/core';
@Component({
  selector: 'my-app',
  template: '<h1>My First Angular2!!! App</h1>'
})
export class AppComponent { }


最後啟動Server,瀏覽index.jsp,應該就可以看到頁面一開始有Loading...字樣,然後很快變成My First Angular2!!! App的字的效果了
原始碼下載:

2016年9月17日 星期六

typescript + SystemJS + npm + jQuery (Netbeans, Eclipse)

這篇主要紀錄在Netbeans和Eclipse中使用Typescript,並配合SystemJS的範例,再來我們要利用npm來安裝JQuery並用SystemJS引入JQuery模組,學習建立自己的javascript module。

這篇要實現的需求如下:
  1. 在Netbeans和Eclipse中使用typescript外掛編譯我們的ts檔成js檔。
  2. 使用npm安裝JQuery。
  3. 使用SystemJS引入JQuery模組並使用。
  4. 建立一個自製的JS module, Greeter,配製好相應的package.json和export設定,放到node_modules中。
  5. import自制的Greeter,並建立一個myGreeter去entend(繼承)Greeter擴充功能。
以下是我們完成的目錄結構,需注意到的是,藍色框線標示的js及map檔是ts檔被typescript編譯出來的檔案:



接下來講解步驟: