Skip to content

Commit

Permalink
Merge pull request #895 from vrk-kpa/feature/file-input-pseudofile-su…
Browse files Browse the repository at this point in the history
…pport

[Feature] FileInput controlled state without file data
  • Loading branch information
LJKaski authored Feb 4, 2025
2 parents 92e027d + 20b42d1 commit 56cf0fc
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 58 deletions.
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ commands:
checkout_and_cache:
steps:
- checkout
- run:
- run:
name: Node version
command: node --version
- restore_cache: # special step to restore the dependency cache
Expand Down
117 changes: 115 additions & 2 deletions src/core/Form/FileInput/FileInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ Examples:
- [Small screen](./#/Components/FileInput?id=small-screen)
- [Non-controlled validation](./#/Components/FileInput?id=non-controlled-validation)
- [Controlled state](./#/Components/FileInput?id=controlled-state)
- [Controlled items metadata](./#/Components/FileInput?id=controlled-items-metadata)
- [Controlled items status](./#/Components/FileInput?id=controlled-items-status)
- [Controlled items with custom data handling](./#/FileInput?id=controlled-items-with-custom-data-handling)
- [Accessing the input with ref](./#/Components/FileInput?id=accessing-the-input-with-ref)
- [Full width](./#/Components/FileInput?id=full-width)
- [Hidden label](./#/Components/FileInput?id=hidden-label)
Expand Down Expand Up @@ -182,6 +183,8 @@ interface ControlledFileItem {
buttonIcon?: ReactElement;
// Override default remove button behavior
buttonOnClick?: (file) => void;
// File metadata for when you want to save the file outside the component state
metadata: Metadata;
}
```

Expand Down Expand Up @@ -237,7 +240,7 @@ const validateFiles = (newFileList) => {
</div>;
```

### Controlled items metadata
### Controlled items status

Below is a static example of the different, more granularly controlled items that can be provided as controlled value.

Expand Down Expand Up @@ -332,6 +335,116 @@ const mockedItems = [
</div>;
```

### Controlled items with custom data handling

If you want to handle the file data without saving it to the component state, you can opt to provide only the necessary metadata to show the file in the component/list. You can then handle the file data as you want when saving the form.

Provide at least `fileName`, `fileType` and `fileSize` as the metadata of the controlled value object. File previews can also be handled either by providing a `fileURL` or a `filePreviewOnClick` in the `ControlledFileItem` object.

The interface for the metadata is as follows:

```jsx static
export interface Metadata {
/**
* The size of the file in bytes.
*/
fileSize: number;
/**
* The name of the file.
*/
fileName: string;
/**
* The type of the file
*/
fileType: string;
/**
* id of the file
*/
id?: string;
}
```

```jsx
import { FileInput } from 'suomifi-ui-components';
import React, { useState } from 'react';

const [statusText, setStatusText] = useState('');
const [status, setStatus] = useState('default');
const [controlledValue, setControlledValue] = useState([]);

const validateFiles = (newFileList) => {
if (newFileList.length === 0) return;
const filesArray = Array.from(newFileList);
let invalidFileFound = false;
let errorText = 'File size must be less than 1 megabytes ';

filesArray.forEach((file) => {
if (file.size > 1000000) {
errorText += `(${file.name}) `;
invalidFileFound = true;
}
});

if (invalidFileFound) {
setStatus('error');
setStatusText(errorText);
} else {
setStatus('default');
setStatusText('');
filesArray.length > 0
? customSaveFunction(filesArray)
: setControlledValue([]);
}
};

const customSaveFunction = (files) => {
const pseudoFiles = [];
files.forEach((file) => {
// Create a metadata object based on file
const fileItemData = {
metadata: {
id: `${Math.random().toString(36).substring(2, 15)}`,
fileName: file.name,
fileSize: file.size,
fileType: file.type
},
buttonOnClick: () => {
// Filter out the item based on id
setControlledValue((prevValue) =>
prevValue.filter(
(item) => item.metadata.id !== fileItemData.metadata.id
)
);
},
filePreviewOnClick: () =>
// Fetch the file from wherever you store it
console.log(`Fetching file ${file.name} from backend`)
};
pseudoFiles.push(fileItemData);
// Save actual file data however you want
});
setControlledValue((prevValue) => [...prevValue, ...pseudoFiles]);
};

<div style={{ width: 600 }}>
<FileInput
labelText="Resume"
inputButtonText="Choose file"
dragAreaText="Choose file or drag and drop here"
removeFileText="Remove"
addedFileAriaText="Added file: "
hintText="Use the pdf file format. Maximum file size is 1 MB"
status={status}
statusText={statusText}
onChange={validateFiles}
value={controlledValue}
filePreview
multiFile
multiFileListHeadingText="Added files"
/>
</div>;
```

### Accessing the input with ref

The component's ref points to the underlying `<input type="file">` element and can thus be used to access the component's value as well as setting focus to it.
Expand Down
119 changes: 89 additions & 30 deletions src/core/Form/FileInput/FileInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,11 @@ export interface ControlledFileItem {
/**
* The actual file object.
*/
file: File;
file?: File;
/**
* Additional metadata for the file.
*/
metadata?: Metadata;
/**
* Status of the element. Affects styling.
*/
Expand All @@ -174,7 +178,38 @@ export interface ControlledFileItem {
/**
* Override default remove button behavior.
*/
buttonOnClick?: (file: File) => void;
buttonOnClick?: () => void;
/**
* Callback for when file preview link is clicked
*/
filePreviewOnClick?: () => void;
/**
* URL to the file. Used in the file preview link. Secondary to `filePreviewOnClick`
*/
fileURL?: string;
}

export interface Metadata {
/**
* The size of the file in bytes.
*/
fileSize: number;
/**
* The name of the file.
*/
fileName: string;
/**
* The type of the file
*/
fileType: string;
/**
* URL to the file
*/
fileURL?: string;
/**
* id of the file
*/
id?: string;
}

type InternalFileInputProps = FileInputProps & GlobalMarginProps;
Expand Down Expand Up @@ -237,7 +272,18 @@ const BaseFileInput = (props: InternalFileInputProps) => {
) => {
const newFileList = new DataTransfer();
controlledValueObjects.forEach((fileItem) => {
newFileList.items.add(fileItem.file);
if (fileItem.file) {
newFileList.items.add(fileItem.file);
} else if (fileItem.metadata) {
// Create a new mock file from metadata
const { fileName, fileType } = fileItem.metadata;
const blob = new Blob([], { type: fileType });
const file = new File([blob], fileName, {
type: fileType,
lastModified: -1,
});
newFileList.items.add(file);
}
});
return newFileList.files;
};
Expand Down Expand Up @@ -300,7 +346,12 @@ const BaseFileInput = (props: InternalFileInputProps) => {
setFilesToStateAndInput(newFileList.files);
}
if (propOnChange) {
propOnChange(newFileList.files || new FileList());
const filteredFiles = Array.from(newFileList.files).filter(
(file) => file.lastModified !== -1,
);
const filteredFileList = new DataTransfer();
filteredFiles.forEach((file) => filteredFileList.items.add(file));
propOnChange(filteredFileList.files || new FileList());
}
};

Expand Down Expand Up @@ -428,38 +479,46 @@ const BaseFileInput = (props: InternalFileInputProps) => {
const handleOnChange = (event: ChangeEvent<HTMLInputElement>) => {
const newFileList = new DataTransfer();
const filesFromEvent = event.target.files;
if (!controlledValue) {
if (multiFile) {
const previousAndNewFiles = Array.from(files || []).concat(
Array.from(filesFromEvent || []),

if (filesFromEvent && filesFromEvent.length > 0) {
if (!controlledValue) {
if (multiFile) {
const previousAndNewFiles = Array.from(files || []).concat(
Array.from(filesFromEvent || []),
);
previousAndNewFiles.forEach((file) => {
newFileList.items.add(file);
});
} else {
const filesFromEventArr = Array.from(filesFromEvent || []);
filesFromEventArr.forEach((file) => {
newFileList.items.add(file);
});
}
setFilesToStateAndInput(newFileList.files);
} else if (inputRef.current) {
const controlledValueAsArray = Array.from(
buildFileListFromControlledValueObjects(controlledValue) || [],
);
previousAndNewFiles.forEach((file) => {
const controlledFileList = new DataTransfer();
controlledValueAsArray.forEach((file) => {
controlledFileList.items.add(file);
newFileList.items.add(file);
});
} else {
inputRef.current.files = controlledFileList.files;
const filesFromEventArr = Array.from(filesFromEvent || []);
filesFromEventArr.forEach((file) => {
newFileList.items.add(file);
});
}
setFilesToStateAndInput(newFileList.files);
} else if (inputRef.current) {
const controlledValueAsArray = Array.from(
buildFileListFromControlledValueObjects(controlledValue) || [],
);
const controlledFileList = new DataTransfer();
controlledValueAsArray.forEach((file) => {
controlledFileList.items.add(file);
newFileList.items.add(file);
});
inputRef.current.files = controlledFileList.files;
const filesFromEventArr = Array.from(filesFromEvent || []);
filesFromEventArr.forEach((file) => {
newFileList.items.add(file);
});
}
if (propOnChange) {
propOnChange(newFileList.files);
if (propOnChange) {
const filteredFiles = Array.from(newFileList.files).filter(
(file) => file.lastModified !== -1,
);
const filteredFileList = new DataTransfer();
filteredFiles.forEach((file) => filteredFileList.items.add(file));
propOnChange(filteredFileList.files);
}
}
};

Expand Down Expand Up @@ -549,7 +608,7 @@ const BaseFileInput = (props: InternalFileInputProps) => {
removeFileText={removeFileText}
removeFile={removeFile}
smallScreen={smallScreen}
metaData={controlledValue && controlledValue[0]}
fileItemDetails={controlledValue && controlledValue[0]}
/>
</HtmlDiv>
)}
Expand Down Expand Up @@ -584,7 +643,7 @@ const BaseFileInput = (props: InternalFileInputProps) => {
removeFileText={removeFileText}
removeFile={removeFile}
smallScreen={smallScreen}
metaData={controlledValue && controlledValue[index]}
fileItemDetails={controlledValue && controlledValue[index]}
/>
))}
</HtmlDiv>
Expand Down
Loading

0 comments on commit 56cf0fc

Please sign in to comment.