多个 file-input 和 Angular 动态表单

Multiple file-input with Angular Dynamic Form

TLDR:如何将 FormArray 或 FormGroup 与多个 file-input 字段一起使用?

大家好,

我想用 Angular 构建一个事件管理器。每个活动都有标题、描述、图像和传单。图片是 png 或 jpg file-input,传单是 pdf 格式。

我需要一个执行以下操作的工具栏:

事件列表如下:

但我很难实施表格。

如何将多个 file-input 与 angular 一起使用到动态表单中?

这是我尝试做的(事件 = 促销):

HTML:

  <form [formGroup]="promotionForm" *ngFor="let promotion of promotions; let i = index;">
    <div class="card" *ngIf="promotion.displayed" (click)="promotion.selected = !promotion.selected;">
      <div>
        <h2>Title</h2>
        <input matInput [(ngModel)]="promotion.title" [ngModelOptions]="{standalone: true}"/>
      </div>
      <div>
        <h2>Subtitle</h2>
        <input matInput [(ngModel)]="promotion.subtitle" [ngModelOptions]="{standalone: true}"/>
      </div>
      <div>
        <h2>Language</h2>
        <mat-select [(value)]="promotion.language"></mat-select>
      </div>
      <div class="description">
        <h2>Description</h2>
        <textarea matInput cdkAutosizeMinRows="5" [(ngModel)]="promotion.description"></textarea>
      </div>
      <div class="img">
        <div class="container">
          <h2>Image</h2>
          <mat-form-field>
            <ngx-mat-file-input formControlName="image" [multiple]="false" accept="image/webp, image/jpeg, image/png">
              <mat-icon ngxMatFileInputIcon>folder</mat-icon>
            </ngx-mat-file-input>
          </mat-form-field>
        </div>
        <img src="{{promotion.image}}">
      </div>
      <div class="pdf">
        <h2>PDF</h2>
        <mat-form-field>
          <ngx-mat-file-input formControlName="pdf" [multiple]="false" accept="application/pdf">
            <mat-icon ngxMatFileInputIcon>folder</mat-icon>
          </ngx-mat-file-input>
        </mat-form-field>
      </div>
      <mat-checkbox class="checkbox" [checked]="promotion.selected"></mat-checkbox>
    </div>
  </form>

TS:

promotions: Promotion[] = [];
  maxSize: number = 2; //Mo
  promotionForm: FormGroup;
  images: any[];
  pdfs: any[];

  constructor(private promotionService: PromotionService, private fb: FormBuilder) { }

  ngOnInit(): void {
    this.promotionForm = this.fb.group({
      image: [MaxSizeValidator(this.maxSize * 1024 * 1024)],
      pdf: [MaxSizeValidator(this.maxSize * 1024 * 1024)]
    })
    this.getAllPromotions();
  }

  getAllPromotions() {
    this.promotionService.getAllPromotions()
      .subscribe(promotions => {
        this.promotions = promotions.reverse();
        this.promotions.map(p => {
          p.selected = false;
          p.displayed = true;
        })
      })
  }

  deletePromotions() {
    let toDelete$ = this.promotions.filter(p => p.selected).map(p => { return this.promotionService.deletePromotion(p._id) });
    forkJoin(toDelete$).subscribe(() => this.getAllPromotions());
  }

  updatePromotions() {
    let toUpdate$ = this.promotions.filter(p => p.selected).map(p => { return this.promotionService.updatePromotion(p) });
    forkJoin(toUpdate$).subscribe(() => this.getAllPromotions());
  }

  selectAll() {
    if (this.promotions.filter(p => p.displayed).every(p => p.selected)) {
      this.promotions.map(p => p.selected = false)
    } else {
      this.promotions.filter(p => p.displayed).map(p => p.selected = true)
    }
  }

此解决方案的问题是为每个事件获取一个唯一的文件。我希望每个事件都有一个唯一的 file-input,如果我使用 FormGroup,我无法将 n file-input 关联到每个事件。我应该使用 FormGroup 的 FormArrays 并在每个组中使用图像和 pdf 吗?

我就是这样实现的。

而不是使用 formArray。我为每个 objects 添加了一个自定义控件。这是我的 html

<ng-container [formGroup]="promotion.filesForm">
        <div class="img">
          <div class="container">
            <h2>Image</h2>
            <mat-form-field>
              <ngx-mat-file-input formControlName="image" [multiple]="false" accept="image/webp, image/jpeg, image/png" (change)="promotion.image=''">
                <mat-icon ngxMatFileInputIcon>folder</mat-icon>
              </ngx-mat-file-input>
            </mat-form-field>
          </div>
          <img src="{{promotion.image}}">
        </div>
        <div class="pdf">
          <h2>PDF</h2>
          <mat-form-field>
            <ngx-mat-file-input formControlName="pdf" [multiple]="false" accept="application/pdf">
              <mat-icon ngxMatFileInputIcon>folder</mat-icon>
            </ngx-mat-file-input>
          </mat-form-field>
          <a href="{{promotion.pdf}}">Flyer</a>
        </div>
      </ng-container>

还有我的老师

  addPromotionFileForm() {
    const fileForm = new FormGroup({
      image: new FormControl('', [MaxSizeValidator(this.maxSize * 1024 * 1024)]),
      pdf: new FormControl('', [MaxSizeValidator(this.maxSize * 1024 * 1024)])
    });
    return fileForm as FormGroup;
  }

  getAllPromotions() {
    this.promotionService.getAllPromotions()
      .subscribe(promotions => {
        this.promotions = promotions.reverse();
        this.promotions.map(p => {
          p.selected = false;
          p.displayed = true;
          p.filesForm = this.addPromotionFileForm();
        })
      })
  }

当我想上传它们时,我对我的事件数组进行了深度复制,并将文件放入图像和 pdf 属性中。

  updatePromotions() {
    let payload = cloneDeep(this.promotions);
    payload.map(p => {
      p.image = p.filesForm.value.image;
      p.pdf = p.filesForm.value.pdf;
      delete p.filesForm;
    });
    let toUpdate$ = payload.filter(p => p.selected).map(p => { return this.promotionService.updatePromotion(p) });
    forkJoin(toUpdate$).subscribe(() => this.getAllPromotions());
  }

然后我用我的 http 服务发送它们,确保以 FormData 方式传递文件并提供良好的 http headers! (您必须控制您的 api 接受 header enctype

  updatePromotion(promotion: any) {
    const formData = new FormData();
    Object.keys(promotion).forEach(key => formData.append(key, promotion[key]));
    const httpOptions = {
      headers: new HttpHeaders({
        'enctype': 'multipart/form-data',
        'Authorization': this.auth
      })
    };
    return this.http.put<any>(this.apiUrl, formData, httpOptions);
  }

最后,我的后端 api 负责数据库存储:

首先是路线

const auth = require('../middleware/auth');
const multer = require('../middleware/multer');
const promotionController = require('../controller/promotion-controller');

const promotionUpload = [{
    name: 'image',
    maxCount: 1
  },
  {
    name: 'pdf',
    maxCount: 1
  }
];
router.put('/', auth, multer.fields(promotionUpload), promotionController.updatePromotion);

然后控制器

exports.updatePromotion = (req, res) => {
  let payload = {
    ...req.body
  };

  // If req contains files
  if (req.files) {
    if (req.files['pdf']) {
      payload.pdf = req.protocol + "://" + req.get('host') + "/" + req.files['pdf'][0].path;
    }
    if (req.files['image']) {
      payload.image = req.protocol + "://" + req.get('host') + "/" + req.files['image'][0].path;
    }
  }

  // Update database
  Promotion.findByIdAndUpdate(payload._id, payload)
    .then(() => res.status(200).json("Success"))
    .catch((error) => res.status(500).json("Failure: " + error))
}

就是它了!我用了几天时间来解决这个难题,我很自豪地向您展示了一个可行的解决方案。不要犹豫,问问题。我很乐意提供帮助