0

TLDR: How to use FormArray or FormGroup with multiple file-input fields?

Hello everyone,

I want to build an event manager with Angular. Each event has a title, description, image and a flyer. The image is a png or jpg file-input and the flyer is a pdf one.

I need a toolbar performing the following actions:

toolbar

  • add should append a new event to the list withouth image or pdf
  • delete should remove all selected event
  • save should update to the database the event and load the two file-inputs files.

The list of event look like:

event

But I struggle to implement the form.

How can I use multiple file-input into a dynamic form with angular?

This is what I tried to do (event = promotion):

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)
    }
  }

The issue with this solution is to get a unique file for each event. I want an unique file-input for each event and if I use FormGroup, I'm unable to have n file-input associated to each event. Should I use FormArrays of FormGroup and use image and pdf into each group?

1 Answer 1

2

This is how I achieved it.

Instead of using formArray. I added a custom control for each objects. Here's my 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>

And my ts

  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();
        })
      })
  }

When I want to upload them, I do a deep copy of my events array and I put the file into the image and pdf attributes.

  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());
  }

Then I send them with my http service ensuring to pass the file with the FormData way and giving the good http headers ! (you must control that your api accept the 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);
  }

Finally, my backend api take care of the database storage:

First the route

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);

Then the controller

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))
}

This is it! I used several day to solve this puzzle and I'm proud to show you a working solution. Dont hesitate to ask question. I would be happy to help

Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.