Angular中FormArray文件存入FormData显示null问题求助
Angular FormArray 文件字段转JSON后为null的解决方案
问题概述
在Angular项目中使用FormArray管理行业经历表单,选择文件后能在FormArray中看到文件数据,也能成功存入fileArray,但将FormArray数据转为JSON并添加到FormData时,文件字段显示为null。
问题根源
JSON.stringify()无法序列化File对象,会将其转换为空对象{}或null,因此直接将包含File的FormArray值转为JSON字符串时,文件信息会完全丢失。同时FormData无法识别嵌套在JSON字符串中的文件,必须单独追加文件对象。
解决方法
拆分表单数据,将普通文本字段与文件字段分开处理:
- 提取每个表单组中的普通字段,组成数组后转为JSON字符串存入FormData
- 单独将每个文件对象追加到FormData,通过索引区分不同条目的文件
修改后的组件代码
import { Component, ElementRef, OnInit, ViewChildren, QueryList } from '@angular/core'; import { FormBuilder, FormGroup, FormArray, Validators } from '@angular/forms'; import { UtilityService } from 'path-to-your-utility-service'; import { NzNotificationService } from 'ng-zorro-antd/notification'; @Component({ selector: 'app-apply-assessor-pool', templateUrl: './apply-assessor-pool.component.html', styleUrls: ['./apply-assessor-pool.component.scss'] }) export class ApplyAssessorPoolComponent implements OnInit { assessorEntryForm: FormGroup; fileArray: any[] = []; allowedPhotoExtensions: string[] = ['pdf', 'jpg', 'png']; maxFormatPhotoFileSize: number = 5 * 1024 * 1024; @ViewChildren('industryInputVariable8') industryInputVariables: QueryList<ElementRef>; constructor( private fb: FormBuilder, private utilityService: UtilityService, private notification: NzNotificationService ) {} ngOnInit() { this.assessorEntryForm = this.fb.group({ industryExpFormArray: this.fb.array([]), }); } get industryExpFormArray(): FormArray { return this.assessorEntryForm.get('industryExpFormArray') as FormArray; } addIndustryExperience(): void { const industryExpFormArray = this.industryExpFormArray; if (industryExpFormArray.length >= 4) { this.notification.error('Failed', 'You can add maximum 4 industry experience !'); return; } industryExpFormArray.push(this.fb.group({ organization: ["", Validators.required], designation: ["", Validators.required], occupationName: ["", Validators.required], responsibility: ["", Validators.required], yearOfService: ["", Validators.required], industryProofFile: ["", Validators.required], })); } onSelectIndustryFile(event: any, parentIndex: number) { const message = this.utilityService.validateInputFile( event.target.files, this.allowedPhotoExtensions, this.maxFormatPhotoFileSize ); if (message !== '') { this.notification.error('error', message); this.industryInputVariables.toArray()[parentIndex].nativeElement.value = ''; return; } const file = event.target.files[0]; const target: DataTransfer = event.target as DataTransfer; if (target.files.length !== 1) { throw new Error('Cannot use multiple files'); } this.industryExpFormArray.at(parentIndex)?.get('industryProofFile')?.setValue(file); } submitForm(): void { Object.values(this.assessorEntryForm.controls).forEach(control => { control.markAsDirty(); control.updateValueAndValidity(); }); if (!this.assessorEntryForm.valid) return; const formData = new FormData(); const industryExperienceData: any[] = []; this.industryExpFormArray.controls.forEach((item: FormGroup, index: number) => { const { industryProofFile, ...restFields } = item.value; industryExperienceData.push(restFields); if (industryProofFile) { formData.append(`industryProofFiles[${index}]`, industryProofFile); } }); formData.append('assessorIndustryList', JSON.stringify(industryExperienceData)); } }
模板修改(修复文件输入引用问题)
<form [formGroup]="assessorEntryForm"> <b>Relevant Industry Experiences (start with the latest experience)</b> <div class="float-right"> <button class="btn btn-primary" (click)="addIndustryExperience()" type="button">Add Industry Experience (max 4)</button> </div> <nz-table *ngIf="industryExpFormArray.controls.length>0" #basicTable [nzData]="industryExpFormArray.controls" [nzFrontPagination]="false"> <thead> <tr> <th>Sl.</th> <th>Name of Organization</th> <th>Designation</th> <th>Occupation/Field</th> <th>Responsibilities</th> <th>Years of Service/ Experience</th> <th>Proof Document</th> <th>Action</th> </tr> </thead> <tbody> <ng-container formArrayName="industryExpFormArray"> <tr *ngFor="let item of industryExpFormArray?.controls; let parentIndex = index;"> <ng-container [formGroupName]="parentIndex"> <td>{{parentIndex+1}}</td> <td> <nz-form-item> <nz-form-control nzHasFeedback [nzErrorTip]="organizationErrorTpl"> <input formControlName="organization" nz-input placeholder="Name of Organization" /> <ng-template #organizationErrorTpl let-control> <ng-container *ngIf="control.hasError('required')"> Please enter organization name! </ng-container> </ng-template> </nz-form-control> </nz-form-item> </td> <td> <nz-form-item> <nz-form-control nzHasFeedback [nzErrorTip]="designationErrorTpl"> <input formControlName="designation" nz-input placeholder="Designation" /> <ng-template #designationErrorTpl let-control> <ng-container *ngIf="control.hasError('required')"> Please enter designation! </ng-container> </ng-template> </nz-form-control> </nz-form-item> </td> <td> <nz-form-item> <nz-form-control nzHasFeedback [nzErrorTip]="occupationErrorTpl"> <input formControlName="occupationName" nz-input placeholder="Occupation/Field" /> <ng-template #occupationErrorTpl let-control> <ng-container *ngIf="control.hasError('required')"> Please enter occupation/field! </ng-container> </ng-template> </nz-form-control> </nz-form-item> </td> <td> <nz-form-item> <nz-form-control nzHasFeedback [nzErrorTip]="responsibilityErrorTpl"> <textarea formControlName="responsibility" nz-input placeholder="Responsibilities"></textarea> <ng-template #responsibilityErrorTpl let-control> <ng-container *ngIf="control.hasError('required')"> Please enter responsibilities! </ng-container> </ng-template> </nz-form-control> </nz-form-item> </td> <td> <nz-form-item> <nz-form-control nzHasFeedback [nzErrorTip]="yearErrorTpl"> <input formControlName="yearOfService" nz-input placeholder="Years of Service" /> <ng-template #yearErrorTpl let-control> <ng-container *ngIf="control.hasError('required')"> Please enter years of service! </ng-container> </ng-template> </nz-form-control> </nz-form-item> </td> <td> <nz-form-item> <nz-form-control nzHasFeedback nzValidatingTip="Validating..." [nzErrorTip]="photoFileErrorTpl"> <input type="file" nz-input #industryInputVariable8 (change)="onSelectIndustryFile($event,parentIndex)" /> <ng-template #photoFileErrorTpl let-control> <ng-container *ngIf="control.hasError('required')"> Please select proof document! </ng-container> </ng-template> </nz-form-control> </nz-form-item> </td> <td> <button nz-button nzType="danger" (click)="industryExpFormArray.removeAt(parentIndex)">Delete</button> </td> </ng-container> </tr> </ng-container> </tbody> </nz-table> </form>
补充说明
- 后端接收方式:后端需要从FormData中分别获取
assessorIndustryList(JSON字符串,解析后得到普通表单数据)和industryProofFiles(文件数组,按索引对应每个行业经历)。 - 表单验证:确保所有字段都正确绑定
formControlName,保证表单验证生效。 - 文件清空逻辑:使用
@ViewChildren获取所有文件输入控件,清空时对应索引的输入,避免只清空最后一个输入的问题。
内容的提问来源于stack exchange,提问作者Md.Arif Shakil Nobin
相关产品推荐
相关产品推荐

