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:
The list of event look like:
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?
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