65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Angular Material 构建文件上传组件。

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2018 年 3 月 22 日

MIT

4分钟阅读

viewsIcon

53200

我们将通过 Angular、Angular CLI 和 Angular Material 构建一个文件上传组件,该组件在 Angular Material 中不存在。

引言

Angular 是一个流行且强大的 JavaScript Web 应用程序开发的前端框架。借助 Angular Material,您可以构建能够适应不同设备的 Web 应用程序。 这开启了 Web 应用程序开发的新时代。 遗憾的是,Angular Material 中不包含文件上传控件,因此让我们自己构建它。 DIY!

准备就绪

在进入实际代码之前,您需要在您的机器上安装 NPM。 请从 Node.js 网站下载。 安装完成后,从您的终端、命令行或 powershell 运行以下命令。 这会将 Angulai CLI 全局安装在您的机器上。

npm install -g "@angular/cli"

Angular CLI 安装完成后,运行以下命令检查版本。

ng --version

结果输出将如下所示

有关 "ng" 的更多帮助,您可以使用 "ng --help"。 我不会花太多时间在它上面。 让我们开始我们的 Angular 项目。 在您的工作区文件夹中运行以下命令。

ng new angular-material-file-upload

它创建一个 Angular 项目文件结构并下载所有模块依赖项。

在您的 IDE 中打开项目文件夹,这里我使用 Visual Studio Code。 它轻量级但健壮且快速。

现在,让我们在 *package.json* 中添加 Angular Material 依赖项。 完成后,在您的项目文件夹中运行 "npm install" 以下载所有模块。

"dependencies": {
      "@angular/animations": "^5.2.0",
      "@angular/common": "^5.2.0",
      "@angular/compiler": "^5.2.0",
      "@angular/core": "^5.2.0",
      "@angular/forms": "^5.2.0",
      "@angular/http": "^5.2.0",
      "@angular/platform-browser": "^5.2.0",
      "@angular/platform-browser-dynamic": "^5.2.0",
      "@angular/router": "^5.2.0",
      "@angular/cdk": "^5.2.4",
      "@angular/material": "^5.2.4",
      "core-js": "^2.4.1",
      "rxjs": "^5.5.6",
      "zone.js": "^0.8.19"
}

深入代码

所有准备工作都已完成,让我们使用命令 "ng generate" 创建我们的文件上传组件。 在 *project* 文件夹下运行以下命令。

ng generate component material-file-upload

它会在 "app" 下生成一个新组件及其文件夹。

在使用 Angular Material 之前,我们需要将必需的模块导入到 "app.module.ts" 中。

import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule, MatIconModule, MatProgressBarModule } from '@angular/material';
...
imports: [
    BrowserModule,
    HttpClientModule,
    BrowserAnimationsModule,
    MatButtonModule,
    MatIconModule,
    MatProgressBarModule
],
...

模块已导入,因此我们将亲自动手编写新组件的代码。 以下是模板代码。

<button mat-button color="warn" (click)="onClick()">
      <mat-icon>file_upload</mat-icon>
      {{text}}
</button>
<br/>
<ul>
      <li *ngFor="let file of files" [@fadeInOut]="file.state">
            <mat-progress-bar [value]="file.progress"></mat-progress-bar>
            <span id="file-label">
            {{file.data.name}} 
            <a title="Retry" (click)="retryFile(file)" *ngIf="file.canRetry">
            <mat-icon>refresh</mat-icon></a>
            <a title="Cancel" (click)="cancelFile(file)" *ngIf="file.canCancel">
            <mat-icon>cancel</mat-icon></a>
            </span>
      </li>
</ul>
<input type="file" id="fileUpload" name="fileUpload" multiple="multiple" 
 accept="{{accept}}" style="display:none;"/>

让我们稍微浏览一下代码。 我们有一个按钮来触发文件选择,而实际的元素 input [type="file"] 将被隐藏。 在这里,我们使用 "mat-button" 指令将按钮呈现为 Material 按钮。 要绑定控件的事件,我们使用 (event_name)=event_handler()。 要绑定控件的属性,我们使用 [property_name]="value"property_name="{{value}}"。 "value" 本身在组件类中声明。 对于迭代,我们使用 *ngFor="let a of array"。 对于流程控制,我们使用 *ngIf="boolean_expression"[@fadeInOut] 用于动画效果。 "fadeInOut" 是我们在组件类中定义的触发器名称。 我们将在下一个代码片段中看到它。

实现实际上很简单。 每当用户选择一个文件时,组件会将其添加到队列(数组)以上传。 如果某些事情中断了上传过程,用户可以重做它。 最后,用户可以在上传完成之前的任何时间取消上传。

这是组件类代码。

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { HttpClient, HttpResponse, HttpRequest, 
         HttpEventType, HttpErrorResponse } from '@angular/common/http';
import { Subscription } from 'rxjs/Subscription';
import { of } from 'rxjs/observable/of';
import { catchError, last, map, tap } from 'rxjs/operators';

@Component({
      selector: 'app-material-file-upload',
      templateUrl: './material-file-upload.component.html',
      styleUrls: ['./material-file-upload.component.css'],
      animations: [
            trigger('fadeInOut', [
                  state('in', style({ opacity: 100 })),
                  transition('* => void', [
                        animate(300, style({ opacity: 0 }))
                  ])
            ])
      ]
})
export class MaterialFileUploadComponent implements OnInit {

      /** Link text */
      @Input() text = 'Upload';
      /** Name used in form which will be sent in HTTP request. */
      @Input() param = 'file';
      /** Target URL for file uploading. */
      @Input() target = 'https://file.io';
      /** File extension that accepted, same as 'accept' of <input type="file" />. 
          By the default, it's set to 'image/*'. */
      @Input() accept = 'image/*';
      /** Allow you to add handler after its completion. Bubble up response text from remote. */
      @Output() complete = new EventEmitter<string>();

      private files: Array<FileUploadModel> = [];

      constructor(private _http: HttpClient) { }

      ngOnInit() {
      }

      onClick() {
            const fileUpload = document.getElementById('fileUpload') as HTMLInputElement;
            fileUpload.onchange = () => {
                  for (let index = 0; index < fileUpload.files.length; index++) {
                        const file = fileUpload.files[index];
                        this.files.push({ data: file, state: 'in', 
                          inProgress: false, progress: 0, canRetry: false, canCancel: true });
                  }
                  this.uploadFiles();
            };
            fileUpload.click();
      }

      cancelFile(file: FileUploadModel) {
            file.sub.unsubscribe();
            this.removeFileFromArray(file);
      }

      retryFile(file: FileUploadModel) {
            this.uploadFile(file);
            file.canRetry = false;
      }

      private uploadFile(file: FileUploadModel) {
            const fd = new FormData();
            fd.append(this.param, file.data);

            const req = new HttpRequest('POST', this.target, fd, {
                  reportProgress: true
            });

            file.inProgress = true;
            file.sub = this._http.request(req).pipe(
                  map(event => {
                        switch (event.type) {
                              case HttpEventType.UploadProgress:
                                    file.progress = Math.round(event.loaded * 100 / event.total);
                                    break;
                              case HttpEventType.Response:
                                    return event;
                        }
                  }),
                  tap(message => { }),
                  last(),
                  catchError((error: HttpErrorResponse) => {
                        file.inProgress = false;
                        file.canRetry = true;
                        return of(`${file.data.name} upload failed.`);
                  })
            ).subscribe(
                  (event: any) => {
                        if (typeof (event) === 'object') {
                              this.removeFileFromArray(file);
                              this.complete.emit(event.body);
                        }
                  }
            );
      }

      private uploadFiles() {
            const fileUpload = document.getElementById('fileUpload') as HTMLInputElement;
            fileUpload.value = '';

            this.files.forEach(file => {
                  this.uploadFile(file);
            });
      }

      private removeFileFromArray(file: FileUploadModel) {
            const index = this.files.indexOf(file);
            if (index > -1) {
                  this.files.splice(index, 1);
            }
      }

}

export class FileUploadModel {
      data: File;
      state: string;
      inProgress: boolean;
      progress: number;
      canRetry: boolean;
      canCancel: boolean;
      sub?: Subscription;
}

让我们仔细看看我们所组成的内容。 在 @Component() 注释中,我们定义了一个动画触发器 "fadeInOut"。 它实现了页面中的列表项在文件上传完成后逐渐消失。 我们为组件提供了四个 @Input() 属性以允许自定义,并提供一个 @Output() 事件发射器以允许在文件上传后进行交互。 这里需要提到的一件事是,我使用 "https://file.io" 作为开发目标。

onClick() 中,每当用户选择新文件时,它会将文件推送到数组 "files" 并触发上传处理程序。 在 uploadFile() 中,我们通过使用 Angular HttpClient 实现了处理文件上传的核心逻辑。 我们用进度报告、故障重试和取消来装饰它。

最后的润色

我们将添加默认的 Material 主题和 Material 图标引用。 将以下代码添加到 *index.html*。

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

将 Angular Material 的默认主题添加到 *styles.css*。

@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
body {
      background: #eeeeee;
      font-family: Roboto, "Helvetica Neue", sans-serif;
}

将组件 CSS 添加到 *material-file-upload.component.css*。

ul,
li {
      margin: 0;
      padding: 0;
      list-style: none;
}

#file-label {
      display: inline-flex;
      vertical-align: middle;
      font-size: 12px;
      line-height: 18px;
}

#file-label mat-icon {
      font-size: 18px;
      text-align: center;
}

#file-label a {
      cursor: pointer;
}

删除生成的内容并将组件添加到 *app.component.html*。 我们将 @Output() 交互事件绑定到 *app.component.ts* 中的 onFileComplete()

<div style="text-align:center">
      <app-material-file-upload (complete)="onFileComplete($event)"></app-material-file-upload>
</div>

以下是 *app.component.ts* 中的 onFileComplete() 实现。

onFileComplete(data: any) {
      console.log(data); // We just print out data bubbled up from event emitter.
}

返回到您的终端、命令行或 powershell,运行以下命令

npm start

打开您的浏览器并导航到 *https://:4200/*。 您应该看到您的文件上传按钮如下所示

上传文件进行测试。

文件上传后,您可以在开发者工具中看到控制台的输出。

翱翔天空

Angular 确实是前端 Web 开发的一个很好的框架选择。 感谢 Google 和所有代码贡献者。 它出色的功能(如路由、模块化、TypeScript 采用、依赖注入等)使 Web 应用程序发展到一个新时代。 希望您会喜欢它并生成您自己的组件和应用程序。

© . All rights reserved.