0

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.

swagger-generated-two-file-selectors

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;
}
  • What would be the reason that primeng does not offer a file selector component?
  • What would be the best way to use several file selectors in a (reactive) form?
Mfried
  • 57
  • 2
  • 9
  • What is wrong with PrimeNg's upload component? How does it not work in your situation? – Meqwz Jul 08 '23 at 17:19
  • Similar to https://stackoverflow.com/questions/73295132/how-do-i-bind-a-primeng-file-upload-component-to-my-angular-form-control I would like to bind the p-uploadFile to a form without a manual method. But I get `NG01203: No value accessor for form control name: 'abcFile'...`. I use it the same way for the other controls in the form and it works for them. Additionally, once the file was selected maybe the user would realize that the wrong file was selected and would like to select again which doesn't seem to work. Would there be anywhere a best practice guide for such a use case? – Mfried Jul 10 '23 at 06:05
  • Here are two examples (not for primeng): https://medium.com/netanelbasal/how-to-implement-file-uploading-in-angular-reactive-forms-89a3fffa1a03 and https://www.positronx.io/angular-file-upload-with-progress-bar-tutorial/ (using bootstrap) – Mfried Jul 10 '23 at 06:42
  • Did you see this part of the Natanel Basel's post: "Creating a Custom Form Control At this point, we have a problem. Angular doesn’t come with a built-in value accessor for file input, so we’ll get the following error: `Error: No value accessor for form control with name: ‘image’` What Angular means is that it doesn’t know how to connect our component to the form API. Let’s inform Angular on how to do that by creating a custom value accessor" You need to create a custom value accessor for PrimeNg's fileupload to work the same as the other inputs. – Meqwz Jul 10 '23 at 13:33
  • In fact I didn't see that. Thanks a lot. But what about the other point: once select a file with fileUpload you cannot re-select it (in case the wrong file was selected). – Mfried Jul 11 '23 at 06:34
  • You should probably ask that in a separate question, showing how exactly you're using the component. It's possible to clear the file selection e.g. in the advanced mode: https://primeng.org/fileupload#advanced – skink Jul 11 '23 at 07:10
  • Yes, please ask a new question about how to use the PrimeNg file upload component how you want to use it. It's possible to do. – Meqwz Jul 11 '23 at 12:55

1 Answers1

1

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:

Mfried
  • 57
  • 2
  • 9