I am trying to rebuild a form with several file selectors with primeng but primeng does only offer a FileUpload component which is not the same as a file selector. Same seems to apply for ngx-dropzone.
Example with one file selector:
<div>
<form [formGroup]="model" (ngSubmit)="onSubmit()">
<label for="fileUpload">Csv-File:</label>
<input
id="fileUpload"
type="file"
formControlName="csvFile"
/>
<p>Complete the form to enable button.</p>
<button type="submit" [disabled]="!model.valid">Submit</button>
</form>
</div>
with
import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-input',
templateUrl: './input.component.html',
styleUrls: ['./input.component.scss'],
})
export class InputComponent {
model = new FormGroup({
csvFile: new FormControl('', [Validators.required]),
});
onSubmit() {
console.log(JSON.stringify(this.model.value));
}
}
Example which swagger would create from a spring boot app.
What I have done so far is the following. However, that seems not the the best way to do that.
<div>
<form (ngSubmit)="onSubmit()" [formGroup]="form">
<div class="row" id="csvFileUpload">
<div class="col">
<p-button icon="pi pi-file" label="csv-Datei auswählen">
<input
(input)="onCsvFileSelected($event)"
formControlName="csvFile"
style="opacity: 0; position: absolute; width: 100%; height: 100%"
type="file"
/>
</p-button>
</div>
<div class="col left-spacer">{{ csvFileShadowed.name }}</div>
</div>
<p>Complete the form to enable button.</p>
<p-button [disabled]="!form.valid" type="submit">Submit</p-button>
</form>
</div>
import { Component } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-koko',
templateUrl: './koko.component.html',
styleUrls: ['./koko.component.scss']
})
export class KokoComponent {
csvFileShadowed = {} as File;
form = new FormGroup({
csvFile: new FormControl('', [Validators.required])
});
onSubmit() {
console.log(this.csvFileShadowed);
}
onCsvFileSelected($event: Event) {
if ($event.target && $event.target instanceof HTMLInputElement) {
let inputEvent = $event.target as HTMLInputElement;
if (inputEvent.files?.length == 1) {
this.csvFileShadowed = inputEvent.files[0];
}
}
}
}
.row {
display: flex; /* equal height of the children */
width: fit-content;
}
.col {
flex: 1; /* additionally, equal width */
margin: 0;
padding: 0;
display: flex;
align-items: center;
}
.left-spacer {
margin-left: 0.5em;
max-width: 50%;
}
p-button {
white-space: nowrap;
}
The following solution seems to be working. Here another link for some better understanding: What is ngDefaultControl in Angular?.
If anybody has some improvements please let me know!
parent.component.html
<div>
<form (ngSubmit)="onSubmit()" [formGroup]="form">
<p-dropdown
[options]="banks"
formControlName="bank"
optionLabel="name"
placeholder="Select..."
[autoDisplayFirst]="false"
></p-dropdown>
<app-customer-file-upload formControlName="csvFile" [customChooseLabel]="'csv Datei'" />
<app-customer-file-upload formControlName="dmnFile" [customChooseLabel]="'dmn Datei'" />
<p-button [disabled]="!form.valid" type="submit">Submit</p-button>
</form>
</div>
parent.component.ts
@Component({
...
})
export class KokoComponent implements OnInit {
banks: Bank[] | undefined;
form = {} as FormGroup;
constructor() {}
ngOnInit(): void {
this.banks = [
{ name: 'Comdirect', code: 'COMDIRECT' },
{ name: 'Targobank', code: 'TARGOBANK' },
{ name: 'Ing', code: 'ING' }
];
this.form = new FormGroup({
bank: new FormControl(null, [Validators.required]),
csvFile: new FormControl(null, [Validators.required]),
dmnFile: new FormControl(null, [Validators.required])
});
}
onSubmit() {
let bank = this.form.get('bank')?.value;
let csvFile = this.form.get('csvFile')?.value;
let dmnFile = this.form.get('dmnFile')?.value;
... call the api service to the backend with the parameters ...
}
}
custom-file-upload.component.html
<p-fileUpload
mode="advanced"
[customUpload]="true"
[chooseLabel]="customChooseLabel"
(onSelect)="onSelect($event)"
(onRemove)="onRemove($event)"
[showUploadButton]="false"
[showCancelButton]="false"
[multiple]="false"
></p-fileUpload>
custom-file-upload.component.ts
export const CUSTOM_FILE_UPLOAD_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomFileUploadComponent),
multi: true
};
@Component({
selector: 'app-customer-file-upload',
templateUrl: './custom-file-upload.component.html',
styleUrls: ['./custom-file-upload.component.scss'],
providers: [CUSTOM_FILE_UPLOAD_VALUE_ACCESSOR]
})
export class CustomFileUploadComponent implements ControlValueAccessor {
@Input()
customChooseLabel: string | undefined;
constructor() {}
private onChange: Function | undefined;
writeValue(obj: any): void {
if (this.onChange) {
this.onChange(obj);
}
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {}
onSelect($event: FileSelectEvent) {
const file = $event && $event.files[0];
this.writeValue(file);
}
onRemove($event: FileRemoveEvent) {
this.writeValue(null);
}
}
###### Update: ######
After some more reading I came up with a hopefully better solution for the custom file upload component. Hope that helps others.
<p-fileUpload
#fileUploadComponentSelector
mode="advanced"
[customUpload]="true"
[chooseLabel]="customChooseLabel"
[accept]="acceptMimeType"
(onSelect)="onSelect($event)"
(onRemove)="onRemove()"
[showUploadButton]="false"
[showCancelButton]="false"
[multiple]="false"
[disabled]="disabled"
></p-fileUpload>
import { AfterViewInit, Component, forwardRef, Input, Provider, Renderer2, ViewChild } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { FileSelectEvent, FileUpload } from 'primeng/fileupload';
export const CUSTOM_FILE_UPLOAD_VALUE_ACCESSOR: Provider = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FileUploadComponent),
multi: true
};
@Component({
selector: 'app-file-upload',
templateUrl: './file-upload.component.html',
styleUrls: ['./file-upload.component.scss'],
providers: [CUSTOM_FILE_UPLOAD_VALUE_ACCESSOR]
})
export class FileUploadComponent implements ControlValueAccessor, AfterViewInit {
@ViewChild('fileUploadComponentSelector')
fileUploadElement: FileUpload;
@Input()
customChooseLabel: string | undefined;
@Input()
acceptMimeType: string | undefined;
private _onChange: Function = () => {};
private _onTouched: Function = () => {};
private _value: File | null = null;
private viewInit = false;
private _disabled = false;
constructor(private _renderer: Renderer2) {}
get value(): File | null {
return this._value;
}
set value(v: File | null) {
this.setValue(v, true);
}
get disabled(): boolean {
return this._disabled;
}
ngAfterViewInit() {
this.viewInit = true;
this.setValue(this._value, false);
}
writeValue(value: any): void {
this.setValue(value, false);
}
registerOnChange(fn: any): void {
this._onChange = fn;
}
registerOnTouched(fn: any): void {
this._onTouched = fn;
}
setDisabledState(isDisabled: boolean) {
this._disabled = isDisabled;
}
onSelect($event: FileSelectEvent) {
let fileFromEvent = null;
if ($event && $event.files && $event.files.length > 0) {
fileFromEvent = $event.files[0];
}
this.setValue(fileFromEvent, true);
}
onRemove() {
this.setValue(null, true);
}
setValue(value: any, emitEvent: boolean) {
this._value = value;
if (this.viewInit) {
const newValue: File[] = value == null ? [] : Array.of(value);
this._renderer.setProperty(this.fileUploadElement, 'files', newValue);
}
if (emitEvent && typeof this._onChange === 'function') {
this._onChange(value);
this._onTouched(value);
}
}
}
Additionally, for testing I added ngx-cva-test-suite and added the following test.
import { FileUploadComponent } from './file-upload.component';
import { runValueAccessorTests } from 'ngx-cva-test-suite';
import { KokoPrimengModule } from '../common-primeng/koko-primeng.module';
runValueAccessorTests({
component: FileUploadComponent,
testModuleMetadata: {
imports: [KokoPrimengModule],
declarations: [FileUploadComponent]
},
supportsOnBlur: false,
internalValueChangeSetter: (fixture, value) => {
fixture.componentInstance.setValue(value, true);
},
getComponentValue: fixture => fixture.componentInstance.value,
getValues: () => [new File([], 'test1.txt'), new File([], 'test2.txt'), new File([], 'test3.txt')]
});
For testing with ngx-cva-test-suite it seems some elements need to be public like the method setValue or value itself. Without that test they could be private. Hope I didn't miss anything there.
And again: (improvements) => {pleaseLeaveAComment(improvements)}
Further links: