本系列之前的文章:

这时候Angular程序可以正常访问我们的Knowledge Builder API。

本篇将继续增强Angular程序,为其添加List页面和Detail页面。

  • List页面显示一个集合;
  • Detail页面用来支持:创建、修改和显示;

限于篇幅,这里只描述Knowledge Item的List页面和Detail页面(仅创建部分)。相应的,可以为Question Bank Item实现List页面和Detail页面。

首先,List页面的Html中定义Table控件:

<mat-toolbar-row>
  <span>Knowledge Items</span>
  <span class="example-spacer"></span>
  <section>
    <div class="example-button-row">
      <a mat-stroked-button routerLink="create">CREATE</a>
    </div>
  </section>
</mat-toolbar-row>

<div class="example-container mat-elevation-z8">
  <div class="example-loading-shade" *ngIf="isLoadingResults">
    <mat-progress-spinner *ngIf="isLoadingResults"></mat-progress-spinner>
  </div>

  <div class="example-table-container">

    <table mat-table [dataSource]="data" class="example-table" matSort matSortActive="created" matSortDisableClear
      matSortDirection="desc">
      <!-- ID Column -->
      <ng-container matColumnDef="id">
        <th mat-header-cell *matHeaderCellDef>#</th>
        <td mat-cell *matCellDef="let row"></td>
      </ng-container>

      <!-- Category Column -->
      <ng-container matColumnDef="category">
        <th mat-header-cell *matHeaderCellDef>Category</th>
        <td mat-cell *matCellDef="let row"></td>
      </ng-container>

      <!-- Title Column -->
      <ng-container matColumnDef="title">
        <th mat-header-cell *matHeaderCellDef>Title</th>
        <td mat-cell *matCellDef="let row"></td>
      </ng-container>

      <!-- Created At Column -->
      <ng-container matColumnDef="createdat">
        <th mat-header-cell *matHeaderCellDef>Created At</th>
        <td mat-cell *matCellDef="let row"></td>
      </ng-container>

      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
    </table>
  </div>

  <mat-paginator [length]="resultsLength" [pageSize]="30"></mat-paginator>
</div>

然后,其Component文件中添加AfterViewInit的hook,并去调用service:

@Component({
  selector: 'app-knowledge-items',
  templateUrl: './knowledge-items.component.html',
  styleUrls: ['./knowledge-items.component.scss']
})
export class KnowledgeItemsComponent implements AfterViewInit {
  displayedColumns: string[] = ['id', 'category', 'title', 'createdat'];
  data: any[] = [];

  resultsLength = 0;
  isLoadingResults = true;

  @ViewChild(MatPaginator) paginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;

  constructor(private odataService: ODataService) {}

  ngAfterViewInit() {
    // If the user changes the sort order, reset back to the first page.
    this.sort.sortChange.subscribe(() => this.paginator.pageIndex = 0);

    merge(this.sort.sortChange, this.paginator.page)
      .pipe(
        startWith({}),
        switchMap(() => {
          this.isLoadingResults = true;
          return this.odataService.getKnowledgeItems(
          );
        }),
        map(data => {
          // Flip flag to show that loading has finished.
          this.isLoadingResults = false;
          this.resultsLength = data.total_count;

          return data.items;
        }),
        catchError(() => {
          this.isLoadingResults = false;
          return observableOf([]);
        })
      ).subscribe(data => this.data = data);
  }
}

List页面的SCSS文件:

/* Toolbar */
.example-spacer {
  flex: 1 1 auto;
}

/* Structure */
.example-container {
    position: relative;
    min-height: 200px;
  }
  
  .example-table-container {
    position: relative;
    max-height: 400px;
    overflow: auto;
  }
  
  table {
    width: 100%;
  }
  
  .example-loading-shade {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 56px;
    right: 0;
    background: rgba(0, 0, 0, 0.15);
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  
  .example-rate-limit-reached {
    color: #980000;
    max-width: 360px;
    text-align: center;
  }
  
  /* Column Widths */
  .mat-column-number,
  .mat-column-state {
    max-width: 64px;
  }
  
  .mat-column-created {
    max-width: 124px;
  }

同时,Service中添加两个methods:

  • getKnowledgeItems:读取全部的Knowledge Item,当期这个版本暂不考虑pagination。
  • createKnowledgeItem:创建新的Knowledge Item。

代码如下:

  public getKnowledgeItems(): Observable<any> {
    let headers: HttpHeaders = new HttpHeaders();
    headers = headers.append('Content-Type', 'application/json')
              .append('Accept', 'application/json');

    let params: HttpParams = new HttpParams();
    params = params.append('$top', '100');
    params = params.append('$count', 'true');
    params = params.append('$select', 'ID,Category,Title,CreatedAt,ModifiedAt');
    return this.http.get(`${this.apiUrl}KnowledgeItems`, {
        headers,
        params,
      })
      .pipe(map((response: HttpResponse<any>) => {
        const rjs = response as any;
        return {
          total_count: rjs['@odata.count'],
          items: rjs.value as any[]
        };
      }),
      catchError((error: HttpErrorResponse) => {
        return throwError(error.statusText + '; ' + error.error + '; ' + error.message);
      }));
  }

  public createKnowledgeItem(ki: any): Observable<any> {
    let headers: HttpHeaders = new HttpHeaders();
    headers = headers.append('Content-Type', 'application/json')
              .append('Accept', 'application/json');

    return this.http.post(`${this.apiUrl}KnowledgeItems`, ki, {
        headers
        // params,
      })
      .pipe(map((response: HttpResponse<any>) => {
        const rjs = response as any;
        return {
          total_count: rjs['@odata.count'],
          items: rjs.value as any[]
        };
      }),
      catchError((error: HttpErrorResponse) => {
        return throwError(error.statusText + '; ' + error.error + '; ' + error.message);
      }));
  }

继续增强Detail页面:

<mat-card>
    <mat-card-header>
        <mat-card-title>Knowledge Item</mat-card-title>
        <mat-card-subtitle></mat-card-subtitle>
    </mat-card-header>
    <mat-card-content>
        <form [formGroup]="itemFormGroup">
            <!-- ID -->
            <div class="achih-control-full-width">
            </div>

            <!-- Title -->
            <mat-form-field>
                <input matInput type="text" placeholder="Title" formControlName="titleControl"
                    name="title" #title maxlength="30">
                <mat-hint align="end"> / 30</mat-hint>
            </mat-form-field>

            <!-- Content -->
            <mat-form-field>
              <textarea matInput placeholder="Content" formControlName="contentControl"></textarea>
            </mat-form-field>
        </form>

        <div>
            <button mat-button (click)="onOK()">OK</button>
        </div>
    </mat-card-content>
</mat-card>

Component组件文件,暂只支持Create模式:

@Component({
  selector: 'app-knowledge-item-detail',
  templateUrl: './knowledge-item-detail.component.html',
  styleUrls: ['./knowledge-item-detail.component.scss'],
})
export class KnowledgeItemDetailComponent implements OnInit {

  private _destroyed$: ReplaySubject<boolean>;
  currentMode: string;
  // Step: Generic info
  public itemFormGroup: FormGroup;

  constructor(
    private activateRoute: ActivatedRoute,
    private odataService: ODataService) {
    this.itemFormGroup = new FormGroup({
      titleControl: new FormControl('', Validators.required),
      contentControl: new FormControl('', Validators.required),
    });

    this.currentMode = 'Create';
  }

  ngOnInit(): void {
  }

  onOK(): void {
    // On OK
    if (this.currentMode === 'Create') {
      if (!this.itemFormGroup.valid) {
        if (this.itemFormGroup.hasError) {
          let err = this.itemFormGroup.errors;
          console.log(err);
        }
        return;
      }

      // Create a new knowlege item
      this.odataService.createKnowledgeItem({
        Category: 'Concept',
        Title: this.itemFormGroup.get('titleControl').value,
        Content: this.itemFormGroup.get('contentControl').value
      }).subscribe({
        next: val => {
          // Val
        },
        error: err => {
          // Error
        }
      });
    }
  }
}

现在可以测试Angular程序了。

ListPage

DetailPage

至此,这个Web API 及其相关的Web App已经写完了。

是为之记。
Alva Chien
2020.07.19
更新于2020.12.17