Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
<div class="tedi-file-dropzone__file-list" [tediVerticalSpacing]="0.5">
<tedi-card
padding="none"
[className]="fileClasses(file)"
[background]="
file.fileStatus && file.fileStatus == 'invalid'
? 'danger-primary'
Expand All @@ -46,7 +45,7 @@
>
<tedi-card-content class="tedi-file-dropzone__card" padding="xs">
<div class="tedi-file-dropzone__file-name">
{{ file.label ?? file.name }}
{{ file.label || file.name }}

@if (file && validateIndividually() && file.helper; as helper) {
<tedi-tooltip
Expand Down
93 changes: 17 additions & 76 deletions community/components/form/file-dropzone/file-dropzone.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,9 @@ import {
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import {
AbstractControl,
ControlValueAccessor,
NgControl,
ReactiveFormsModule,
ValidationErrors,
} from "@angular/forms";
import {
ClosingButtonComponent,
Expand All @@ -34,11 +32,10 @@ import {
} from "@tedi-design-system/angular/tedi";
import { CardComponent, CardContentComponent } from "../../cards";
import {
DropzoneValidatorFunction,
FeedbackTextProps,
FileDropzone,
FileDropzoneErrors,
FileInputMode,
FormControlErrors,
SizeDisplayStandard,
ValidationState,
} from "./types";
Expand Down Expand Up @@ -145,15 +142,6 @@ export class FileDropzoneComponent implements ControlValueAccessor, OnInit {
* @default false
*/
uploadFolder = input<boolean>(false);
/**
* Validation functions that can be used to validate files.
* Each function should return a string error message if validation fails, or undefined if it passes
* Validators are only added once during component initialization, otherwise use addAsyncValidators on the FormControl.
*/
validators = input<DropzoneValidatorFunction[]>([
validateFileSize,
validateFileType,
]);
/**
* If true, shows the file dropzone as in a erroring state with red border.
* Overrides default validation state.
Expand Down Expand Up @@ -230,7 +218,17 @@ export class FileDropzoneComponent implements ControlValueAccessor, OnInit {
this.addFiles(this.defaultFiles());
this._fileService.mode = this.mode;
const control = this._ngControl.control;
control?.addAsyncValidators(this.runValidators);
control?.addValidators([
validateFileSize(
this.maxSize(),
this.sizeDisplayStandard(),
this._translationService.translate.bind(this._translationService),
),
validateFileType(
this.accept(),
this._translationService.translate.bind(this._translationService),
),
]);

this._ngControl.control?.statusChanges
?.pipe(takeUntilDestroyed(this._destroyRef))
Expand All @@ -242,63 +240,6 @@ export class FileDropzoneComponent implements ControlValueAccessor, OnInit {
this.uploadState.set(this._getNewState());
}

runValidators = async (
control: AbstractControl<FileDropzone[]>,
): Promise<ValidationErrors | null> => {
const controlFiles = control.value;

if (!controlFiles || !controlFiles.length) {
return null;
}

const issues = this.validateFiles(controlFiles);

return issues.length
? {
tediFileDropzone: [...issues],
}
: null;
};

validateFiles(files: FileDropzone[]): FileDropzoneErrors {
const issues: FileDropzoneErrors = [];
files.forEach((file) => {
this.validators().forEach((validator) => {
const error = validator(
this.maxSize(),
this.accept(),
file,
this.sizeDisplayStandard(),
this._translationService.translate.bind(this._translationService),
);

if (error && error.code) {
issues.push(error);
file.fileStatus = "invalid";
file.helper = {
text: error.message,
type: "error",
};
} else if (!issues.find((issue) => issue.fileName === file.name)) {
file.fileStatus = "valid";
file.helper = undefined;
}
});
});
return issues;
}

fileClasses = (file: FileDropzone): string => {
const classList = ["tedi-file-dropzone__file-item"];
if (file.className) {
classList.push(...file.className);
}
if (file.fileStatus != "none") {
classList.push(`tedi-file-dropzone__file-item--${file.fileStatus}`);
}
return classList.join(" ");
};

tooltipClasses = (file: FileDropzone): string => {
const classes = ["tedi-file-dropzone__tooltip"];
classes.push(`tedi-file-dropzone__tooltip--${file.helper?.type || "hint"}`);
Expand Down Expand Up @@ -374,22 +315,22 @@ export class FileDropzoneComponent implements ControlValueAccessor, OnInit {
}

private _currentErrorState(): string | undefined {
const errors = this._ngControl.control?.errors?.["tediFileDropzone"];
const errors: FormControlErrors = this._ngControl.control?.errors;

if (errors && !this.validateIndividually()) {
const dropzoneErrors: FileDropzoneErrors = errors;
const dropzoneErrors = Object.values(errors);

return dropzoneErrors.map((error) => error.message).join(" ");
}
return undefined;
}

private _getNewState(): ValidationState {
const errors: FileDropzoneErrors =
this._ngControl.control?.errors?.["tediFileDropzone"];
const errors: FormControlErrors = this._ngControl.control?.errors;
if (this._ngControl.control?.touched) {
return "none";
}
if (errors?.length) {
if (errors) {
return "invalid";
}
return this.files().length > 0 ? "valid" : "none";
Expand Down
43 changes: 31 additions & 12 deletions community/components/form/file-dropzone/file-dropzone.stories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import {
moduleMetadata,
StoryObj,
} from "@storybook/angular";
import { validateFileSize, validateFileType } from "./utils";
import { FormControl, FormGroup, ReactiveFormsModule } from "@angular/forms";
import {
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from "@angular/forms";
import { ButtonComponent } from "tedi/components";

/**
Expand Down Expand Up @@ -53,7 +57,6 @@ const meta: Meta<FileDropzoneComponent> = {
mode: "append",
name: "file-dropzone",
uploadFolder: false,
validators: [validateFileSize, validateFileType],
hasError: false,
},
argTypes: {
Expand Down Expand Up @@ -112,11 +115,6 @@ const meta: Meta<FileDropzoneComponent> = {
uploadFolder: {
description: ` If true, allows uploading folders instead of just files. This enables the user to select a folder and upload all its contents. Default file browser behaviour will prevent upload of files in this state.`,
},
validators: {
control: false,
description:
"Validation functions that can be used to validate files. Each function should return a string error message if validation fails, or undefined if it passes.",
},
hasError: {
description: `If true, shows the file dropzone as in a erroring state with red border.
Overrides default validation state.`,
Expand All @@ -142,15 +140,17 @@ const Template = (args: StoryArgs) => `
export const Default: Story = {
render: (args) => {
const form = new FormGroup({
files: new FormControl(null),
files: new FormControl(null, {
validators: [Validators.maxLength(1)],
}),
});

const logValue = () => {
console.log(
form.get("files")?.value,
form.controls.files,
form.controls,
form.controls.files.errors
form.controls.files.errors,
);
};
return {
Expand All @@ -164,17 +164,36 @@ export const Default: Story = {
},
};

/** Limits the maximum amount of files to 1 */
export const MaxFileAmount: Story = {
render: (args) => {
const form = new FormGroup({
files: new FormControl(null, {
validators: [Validators.maxLength(1)],
}),
});

return {
template: Template(args),
props: { ...args, form },
};
},
args: {
inputId: "file-dropzone-form-control",
name: "file-form-control",
},
};
/** Replaces any same-name files, when usually it indexes them. */
export const Replace: Story = {
render: (args) => ({ template: Template(args), props: args }),
...Default,
args: {
mode: "replace",
},
};

/** Custom un-translated label text. */
export const WithHint: Story = {
render: (args) => ({ template: Template(args), props: args }),
...Default,
args: {
name: "file",
label: "Custom hint here",
Expand Down
27 changes: 14 additions & 13 deletions community/components/form/file-dropzone/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,22 @@ export class FileDropzone {
}
}

export interface FileDropzoneError {
export type FormControlErrors =
| {
[key: string]: SingleFileDropzoneError;
}
| null
| undefined;

export type DropzoneValidatorError = {
errorKey: string;
value: SingleFileDropzoneError;
};

export type SingleFileDropzoneError = {
fileName: string;
code: string;
message: string;
}

export type FileDropzoneErrors = FileDropzoneError[];
};

export enum FileDropzoneErrorCode {
FILE_TOO_LARGE = "file-too-large",
Expand All @@ -76,12 +85,4 @@ export type FileInputMode = "append" | "replace";

export type ValidationState = "none" | "valid" | "invalid";

export type DropzoneValidatorFunction = (
maxSize: number,
acceptFileTypes: string,
file: FileDropzone,
standard: SizeDisplayStandard,
translate: (key: string, ...args: unknown[]) => string
) => FileDropzoneError | undefined;

export type SizeDisplayStandard = "SI" | "IEC";
Loading