Angular Material Flexbox 概念

Flexbox 簡介:

主要應用在RWD,概念在於 flex-container main axis 代表排列的主軸、cross asix 是垂直於 main axis 的主軸,這兩者方向可以改變

Flexbox 重點在於可以自適應螢幕大小而進行改變,並且也提供豐富的屬性可以自行條配。

Angular Material 使用 flexbox css 並且加上 mediaQuery (同 bootstrap responsive ),透過指定

@media (min-with: 980px)

設定可以應用的大小

如果要深入理解,建議參考: 深入解析 CSS Flexboxmedia query 小撇步

可以使用 scss 顯示 layout 的差異:

可以參考 https://tburleson-layouts-demos.firebaseapp.com/#/docs 可以直接看出各項 layout 的變化

angular 前端直接使用 xlxs 將資料直接轉換成 Excel

套件網址: https://github.com/SheetJS/js-xlsx/tree/19620da30be2a7d7b9801938a0b9b1fd3c4c4b00/demos/angular2
使用方式:

import { WorkBook, utils, writeFile, WorkSheet } from 'xlsx';

table = [
  {
    First: 'one',
    Second: 'two',
    Third: 'three',
    Forth: 'four',
    Fifth: 'five'
  },
  {
    First: 'un',
    Second: 'deux',
    Third: 'trois',
    Forth: 'quatre',
    Fifth: 'cinq'
  },
];
onClick() {
  const json = this.replaceHeader(this.table);
  /* generate worksheet */
  // const ws: WorkSheet = utils.json_to_sheet(json);
  const ws: WorkSheet = utils.aoa_to_sheet(this.setupAoa(this.table));
  /* generate workbook and add the worksheet */
  const wb: WorkBook = utils.book_new();
  utils.book_append_sheet(wb, ws, 'Sheet1');
  /* save to file */
  writeFile(wb, 'SheetJS.xlsx');
}
private setupAoa(table) {
  var jsonArray = [];
  jsonArray.push(["This is a Ttile"]);
  jsonArray.push(["第一", "第二", "第三", "第四", "第五"]);
  for(var i = 0; i < this.table.length; i++) {
    jsonArray.push([this.table[i].First, this.table[i].Second, this.table[i].Third, this.table[i].Forth, this.table[i].Fifth]);
  }
  return jsonArray;
}

其中 aoa_to_sheet 可以透過二維陣列對應 Excel 表格的內容(另外也可以使用 json_to_sheet,不過要自行加入表頭設定就比較困難)

最基本的建立 Angular Material 教學

這篇是抄襲官方的 angular Get Start 教學,但簡化步驟,直接切入重點:

換個方式直接說明需要執行的步驟:

  1. 安裝 npm package

npm install --save @angular/material @angular/cdk

npm isntall --save @angular/animations

npm isntall --save hammerjs

  1. 修改以下內容

將所有的 angular material module 放入此處:請注意:很多…:

ng g m shared\material –flat

  1. App.module 加入 materialModule

  1. Style.css 中,加入 theme (否則畫面會很醜):

@import “~@angular/material/prebuilt-themes/indigo-pink.css”;

  1. hammerjs 放入到 main.ts (entry point 不需要每個地方重新呼叫)

import ‘hammerjs’;

  1. index.html 中加入 Material Icon stylesheet

<link rel=”stylesheet” href=”https://fonts.googleapis.com/icon?family=Material+Icons”>

附上所有 angualr module 列表:

CdkTableModule,

MatAutocompleteModule,

MatButtonModule,

MatButtonToggleModule,

MatCardModule,

MatCheckboxModule,

MatChipsModule,

MatStepperModule,

MatDatepickerModule,

MatDialogModule,

MatDividerModule,

MatExpansionModule,

MatGridListModule,

MatIconModule,

MatInputModule,

MatListModule,

MatMenuModule,

MatNativeDateModule,

MatPaginatorModule,

MatProgressBarModule,

MatProgressSpinnerModule,

MatRadioModule,

MatRippleModule,

MatSelectModule,

MatSidenavModule,

MatSliderModule,

MatSlideToggleModule,

MatSnackBarModule,

MatSortModule,

MatTableModule,

MatTabsModule,

MatToolbarModule,

MatTooltipModule

Angular 動態載入 Template:使用 Dynamic Component 方案

主要需求:由於 Template 不是固定的,因此需要可以由後端指定 HTML,前台 angular 動態的載入 tempalte。

 

最主要的參考:Angular 2 dynamic template url with string variable?

這裡的 Component (DynamicTemplateComponent) 中,透過 template 指定 ng-container ,透過 ViewChild 可以動態指定:

<ng-container #dynamicTemplate></ng-container>

@ViewChild(‘dynamicTemplate’, {read: ViewContainerRef}) dynamicTemplate;

 

DynamicTemplateComponent 每次 routing 呼叫後,利用 ViewChild 自動生成對應的 dynamic component 與其 template 的頁面。 angular 提供 Dynamic Component Loader 可以達成,

 

但原始文章有一個主要問題在於無法透過 templateUrl,因為 angular cli 會直接解析內容。

解決方案是透過 template 指定 html 內容:

完整程式如下:

export class DynamicTemplateComponent implements AfterViewInit, OnInit {
  @ViewChild('dynamicTemplate', { read: ViewContainerRef }) dynamicTemplate;

  constructor(private compiler: Compiler, private injector: Injector, private ngModuleRef: NgModuleRef<any>,
    private route: ActivatedRoute, private data: LhcService) { }

  id: number;
  ngOnInit() {
    this.route.params.subscribe(p => {
      this.id = p["id"];
    })
  }

  ngAfterViewInit(): void {
    this.data.getTemplate(this.id).subscribe(data => {
      const tmpComponent = Component({ moduleId: module.id, template: data })
        (class {
          cancel() {
            window.history.back();
          }
        });
 
      const tmpModule = NgModule({ declarations: [tmpComponent] })(class { });
 
      this.compiler.compileModuleAndAllComponentsAsync(tmpModule)
        .then((factories) => {
          const factory = factories.componentFactories[0];
          const cmpRef = factory.create(this.injector, [], null, this.ngModuleRef);
          cmpRef.instance.name = 'dynamic';
          this.dynamicTemplate.insert(cmpRef.hostView);
        })
    });
  }

這裡要額外宣告 module.id,是 angular 告訴 webpack or systemjs 如何 package module 的 metadata,因為我們使用 angular cli (webpack) ,會自動注入 module.id 到 component 中,唯一需要注意的是 typescript 要事先宣告避免無法編譯:

declare var module: {
id: string;
}

如果產生的 component 要加入 處理 *ngFor 等 browser 的內容,要在 NgModule 中,宣告 BrowserModule:

const tmpModule = NgModule({
declarations: [tmpComponent],
imports: [BrowserModule]
})(class { });

其次,在產生的 component 中,很難使用 constructor injection,最好的方式是產生時候,直接將服務對應到 class property 中:

const tmpComponent = Component({ moduleId: module.id, template: data })
  (class {
    private data: LhcService;
    patients: Array<RegFile>;
 
    getPatient() {
      this.data.getAllPatients().subscribe(data => this.patients = data);
    }
    cancel() {
      window.history.back();
    }
  });

…

this.compiler.compileModuleAndAllComponentsAsync(tmpModule)
  .then((factories) => {
    const factory = factories.componentFactories[0];
    const cmpRef = factory.create(this.injector, [], null, this.ngModuleRef);
    cmpRef.instance.name = 'dynamic';
    cmpRef.instance.data = this.data;
    cmpRef.instance.callback = this.interactive;

    this.dynamicTemplate.insert(cmpRef.hostView);
  })

其中 compRef.instance 就是代表 dynamic component 的函數物件,這裡將 parent 的 data (lch.service)對應到 child 中,讓他可以直接使用。

此外,也可以加入 callback function(cmpRef.instance.callback = this.interactive),與 patient controller 互動:

在 parnet 中宣告:

interactive(patient: RegFile): Observable<string> {
console.log("dynamic compoent callback, show reg_no: " + patient.RegNo);
return new Observable((observe) => {
observe.next(patient.Name);
observe.complete();
})
}

重點在於回傳 Observable 讓 child component 可以接收:

callback: Function;
clickPatient(patient: RegFile) {
 this.callback(patient).subscribe(m => console.log("easy way from patient: " + m));
}

此外,class 的宣告也可使用繼承或者介面:

const tmpComponent = Component({ moduleId: module.id, template: data })
(class implements OnInit {
ngOnInit(): void {
console.log("ng onitit");
}

請注意,這裡的 class 不可以移出外部,原因是如果移出去,第一次會成功、但第二次就會造成以下的錯誤:

ERROR Error: Type InnerComponent is part of the declarations of 2 modules: class_1 and class_1! Please consider moving InnerComponent to a higher module that imports class_1 and class_1. You can also create a new NgModule that exports and includes InnerComponent then import that NgModule in class_1 and class_1.

此外,也要注意因為動態生成 component 功能畢竟有限,有些無法執行;例如 ngx-bootstrap 的 datepicker 就因為 module 匯入造成失敗,目前找不到原因,還好可以用 input type=’date’ 方式叫出瀏覽器預設的 date picker:

imports: [BrowserModule, FormsModule, BsDatepickerModule.forRoot()] ERROR TypeError: Cannot read property 'isDisabled' of undefined at Object.eval [as updateRenderer] (BsDatepickerDayDecoratorComponent_Host.ngfactory.js? [sm]:1)

 

Angular CLI 正式環境的編譯

正常在開發中,我們常用:

$ ng build –watch

這時候對應的 _Layout.cshtml 為:

<script src=”~/ClientApp/dist/inline.bundle.js”></script>
<script src=”~/ClientApp/dist/polyfills.bundle.js”></script>
<script src=”~/ClientApp/dist/styles.bundle.js”></script>
<script src=”~/ClientApp/dist/vendor.bundle.js”></script>
<script src=”~/ClientApp/dist/main.bundle.js”></script>
</environment>

主要目的在於提供 Chrome 環境可以 debug,並且可以隨時監測程式碼是否有修改,隨時進行編譯。

但衍生的問題是速度會比較慢,尤其在正式環境是不能接受這樣的速度。可以透過 –prod 參數進行正式環境的編譯,會增加以下幾項工作:

The –prod meta-flag engages the following optimization features.

  • Ahead-of-Time (AOT) Compilation: pre-compiles Angular component templates.
  • Production mode: deploys the production environment which enables production mode.
  • Bundling: concatenates your many application and library files into a few bundles.
  • Minification: removes excess whitespace, comments, and optional tokens.
  • Uglification: rewrites code to use short, cryptic variable and function names.
  • Dead code elimination: removes unreferenced modules and much unused code.

更好的方式可以再進一步縮小檔案大小:

$ ng build –prod –build-optimizer

請注意,編譯出來的內容:

<environment include=”Production”>
<script src=”~/clientapp/dist/inline.318b50c57b4eba3d437b.bundle.js”></script>
<script src=”~/clientapp/dist/polyfills.b488325233b482097d13.bundle.js”></script>
<script src=”~/clientapp/dist/main.21c3b35bee403f2837dc.bundle.js”></script>
</environment>

這裡可以使用 asp.net core environment 區隔 Production & Development,同時 Production 的檔案名稱會加入 UUID,因此會隨時異動,如此就可以避免 client cache 所造成問題。

解決 router-outlet 出現顯示疊加的問題

問題描述如下:如果有 master-detail 畫面,在 master 中控制 detail 的顯示(例如:查詢使用 list-component.html, 新增使用 add-component.html),如下圖:

整個頁面的html 設計如下:

<form #form="ngForm" (submit)="retrieveList()" class="form-horizontal" novalidate>
        ...
        <button type="button" class="btn btn-info" (click)="onQuery()">查詢 </button>
        <button type="button" class="btn btn-success" (click)="onAdd()">新增 </button>
    </div>
</form>
 
<router-outlet></router-outlet>

我們再查詢時候,使用 router.navigator:

onQuery() {
    this.router.navigate(["trace/list", this.careType, this.year, this.month]);
}

這時候設定不同條件按下【查詢】一次以上,查出來的資料會不斷的重疊,這是因為實際上查詢出來的資料是放入到 router-outlet 中,因為沒有清除,所以會不斷的累積。

解決方式是導入另外一個 empty-component 顯是空白資料,如下:

@Component({
    selector: 'trace-empty',
    template: '<p>請按下【查詢】資料</p>',
    styles: []
})
export class TraceEmptyComponent {
}

在 child routing 中,設定此 empty routing:

{
    path: 'trace', component: TraceFileComponent,
    children: [
        { path: '', component: TraceEmptyComponent },
        { path: 'list/:type/:year/:month', component: TraceListComponent },
        { path: 'edit/:type/:id', component: TraceEditComponent }
    ]
},

之後,在 input html 中,

<select type="text" name="Month" class="form-control" [ngModel]="month" (ngModelChange)="month=$event; onClear()"> … </select>

指定 onModelChange event 呼叫 onClear() 將畫面跳到 empty component 中:

onClear() {
    this.router.navigate(["trace"]);
}

使用結果會在點選月份時候,將 router-outlet 轉 empty component,然後按下查詢後,就會到真正顯示頁面,藉由 empty component 清空 router-outlet 來避免顯示疊加的問題。

透過 Angular CLI 產生的前端程式在 IE11 無法正確執行

Angular CLI 產生的前端程式一般而言無法正確在 IE 瀏覽器執行,主要有幾個原因:

  • polyfill.ts 必須要修改內容:

Polyfill 本身的目的就是提供一個中介API層,讓舊的瀏覽器可以支援新的API,Angular CLI 自帶的 polyfill.ts (在 src/ 目錄下),預設會註解掉 IE 的支援,將其移除即可(以下還多增加 es7/array 的支援):

  /** IE9, IE10 and IE11 requires all of the following polyfills. **/
 import 'core-js/es6/symbol';
 import 'core-js/es6/object';
 import 'core-js/es6/function';
 import 'core-js/es6/parse-int';
 import 'core-js/es6/parse-float';
 import 'core-js/es6/number';
 import 'core-js/es6/math';
 import 'core-js/es6/string';
 import 'core-js/es6/date';
 import 'core-js/es6/array';
 import 'core-js/es6/regexp';
 import 'core-js/es6/map';
 import 'core-js/es6/weak-map';
 import 'core-js/es6/set';
  /** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js';  // Run `npm install --save classlist.js`.
  /** IE10 and IE11 requires the following for the Reflect API. */
import 'core-js/es6/reflect';
  /** Evergreen browsers require these. **/
// Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove.
import 'core-js/es7/reflect';
import 'core-js/es7/array';
  • 不可以使用 angular 的 forEach

forEach() loop 是 Angular 所提供一個很好的服務,他支援 Async 方式解析陣列的元件;但可惜 IE 11 不支援,因此,若程式需要支援 IE 11,必須要將 forEach() 寫法轉成標準的 for loop。

舉例如下,正常而言,透過 angular forEach 可以直接瀏覽陣列元素:

selections.forEach(item =>
    this.get(item.ItemType, item.ParentCode, item.NeedEmpty)
        .subscribe(data => item.Details = data)
);

一個很重要的議題在於處理 subscribe 針對 http async 的回傳元件,forEach 可以確保 subscribe 中的 item 的值會是當下的 item,如果一般的 loop:

for (var i = 0; i < selections.length; i++) {
    var item = selections[i];
    this.get(item.ItemType, item.ParentCode, item.NeedEmpty)
.subscribe(data => item.Details = data)

這樣寫法是會錯誤的,因為 item 會永遠是最後一筆!
需要改為 透過 function 來設定 item 的內容:

for (var i = 0; i < selections.length; i++) {
    var item = selections[i];
    this.get(item.ItemType, item.ParentCode, item.NeedEmpty, item);
}

get(key, parentCode, needEmpty, codeFile: CodeFile): void {
    this.http.get<Code[]>(this.baseUrl)
        .subscribe(response => {
            let items: Code[] = <Code[]>response;
            codeFile.Details = items;
            this.selections.push(codeFile);
        });
    }
  • Cache 問題:IE 預設會自帶 Cache, 會造成 Angular httpClient 的讀取都是回傳 Cache 的值,因此造成明明有變更但頁面卻無法顯示。解決方式是加上 HttpHeaders diable Cache:
private noCacheHeader: HttpHeaders = new HttpHeaders().set('Cache-Control', 'no-cache').set('Pragma', 'no-cache');
  public getDailyReport(id: number): Observable<DailyReportItem> {
    let params = new HttpParams().set('id', id.toString());
    return this.http.get<DailyReportItem>("/dailyreport/getreport", { params: params, headers: this.noCacheHeader });
}

這裡必須要加入 Cache-Control + Pragma 兩個設定。

整合 ng-busy 套件,http 呼叫可顯示資料載入中的效果

client 需要在 server 存取資料時候,往往會有些等待的時間。透過指示讓使用者知道正在下載中,可以增加使用者的操作體驗。

ng-busy 就是這樣的套件,主要組合包含 css:設定 [ngBusy] 顯示等待的畫面,必須要在每一個 會存取 http component 中添加。

做法如下:

 

安裝

npm install --save ng-busy

\node_modules\angular2-busy\build\style\busy.css copy wwwroot/css 下,方便後續引用:

<link href="~/css/busy.css" rel="stylesheet" />

在 app.module 中,加入

  import { NgBusyModule } from 'ng-busy';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
  @NgModule({
      imports: [
      	// ...
          BrowserAnimationsModule,
          BusyModule
      ],
      // ...
  })

使用方式:

建議使用 Subscription 方案(也可以用 Promise,不過處理步驟就會有些許不同,主要看 http 如何處理):

  import { Subscription } from 'rxjs';
  
// 在 Component 中,定義 busy 變數,並且將 subscribe 結果放入
  busy: Subscription;
  ngOnInit() {
    this.busy = this.data.getNurses(this.clinicCode).subscribe(data => this.nurses = data);
}

html 中,就可以直接呼叫:

<div [ngBusy]=”{busy: busy, message: ‘資料載入中…’}”></div>

 

如此就會出現下方資料載入中的內容

參考: https://github.com/victos/ng-busy

其中也包含 onBusyStop() & onBusyStart() 兩個 event,可以透過以下方式關聯:

<div [ngBusy]=“…” (busyStop)=“onBusyStop()” (busyStart)=“onBusyStart()”></div>

透過 FileSaver 處理檔案下載方案

在 Angular 中,處理檔案下載不是一件容易的事情,主要方法有以下兩種:

1. 透過 anchor 直接將檔案的下載連結放在網頁上:

<a href="/vpn/downloadFile?filename={{item.patientFile}}" class="btn btn-info btn-sm" type="button"
   ng-disabled="!item.patientFile" icon="fa-file-excel-o" target="_self">
    下載 Excel
</a>

其中 /vpn/download 就是用來下載檔案的方式

2. 直接在 angular client code 中處理

最快的方案(而且不需要花費時間)就是透過 HTML 5 FileSaver 套件,引用方式如下:

  • 下載 npm package,這裡同時下載 typescript 定義檔,提供 Intellisense:

npm install file-saver –save

npm install @types/file-saver –save

透過 –save 會直接 update package.json file

  • 透過 http get 下載檔案:
download() {
    this.http.get("/dailyreport/downloadExcel/" + id, { responseType: 'blob' })
        .subscribe(
            data => this.processFile(data),
            error => super.showMessage(AlertType.Danger, error)
        );
}
 
private processFile(data: any) {
    var blob = new Blob([data], { type: 'application/vnd.ms-excel' });
    FileSaver.saveAs(blob, "file.xlsx");
}

注意這裡使用 blob 型態。

  • 在 Server side 將檔案轉成 filestream:
[HttpGet]
public IActionResult DownloadExcel(string filePath)
{
    var fileContents = System.IO.File.ReadAllBytes(filePath);
    return new FileContentResult(fileContents, "application/vnd.ms-excel");
}

 

 

整合 Asp.net Core & Angular Cli 混合開發模式

Angular Cli 可以說是目前使用 Angular 2 以上的開發主要啟動方案。透過簡單的命令頁可以快速依據設定加入 component 的內容,降低維護基本的 import 項目的複雜度。可以參考:基本的 Angular Cli 說明

但對於使用 aps.net core 開發的人員而言,以下說明如何將 angular cli 整合到專案內,同時可以獲得兩者的便利性。

  • 首先,在專案目錄下執行 ng new(如果不知道 angular cli command 請參閱上面的說明連結),執行完畢後會產生 angular cli 預設的 source code folder:

這裡包含完整的啟動程式架構,可以透過瀏覽 index.html 進行操作。

  • 將產生的檔案移動到 Asp.net Core 的專案下。要複製兩種內容:

angular 執行程式:指定 angular source code 放入到 任意的指定目錄下(例如:ClientApp\ ),這裡的 source code 就是指 src\ 目錄下的所有內容:

angular cli 的組態設定檔案:就是指跟 src\ 目錄平行的檔案,複製到專案的根目錄下(與 Startup.cs 平行的路徑):

  • 修改變更路徑後的 angular cli 相關組態設定

tsconfig.json 用來編譯 typescript to javascript,修改:

1. outDir 改為 asp.net wwwroot: “outDir”./wwwroot/clientapp/out-tsc”

2. include 改為前面所設定的程式目錄(例如: ClientApp\)

修改 angular-cli.json,同樣將 outDir 改為 wwwroot 目錄

完成相關設定後,就可以在專案目錄下執行 angular cli 了。