How do I retain a file's name when converting to Base64? - reactjs

I'm using "Input" to get files in a react app. I'm able to get a blob using readAsDataURL() but it's stripping out the file name and is replacing with "Data". When I log the blob I'm getting "data:image/jpeg;base64,/9j/4QAYRXhpZgAAS..." - "data" being the name that displays.
const handleGetFiles = (e) => {
const reader = new FileReader();
reader.readAsDataURL(e);
reader.onload = () => {
fileCollection((fileCollection) => [...fileCollection, {id: index, data: reader.result}]);
}
}
<Input
accept="image/*,video/*"
id="contained-button-file"
multiple type="file"
onChange={e => handleGetFiles(e)}
/>
Is there a way to use URL.createObjectURL(file) instead? I can't get that to work either.

The filename that you're looking for doesn't come from the FileReader API, but rather from the input element's files property.
You can expose the file in 2 ways from React:
Through a ref:
const Input = () => {
const inputEl = React.useRef();
const handleGetFiles = () => {
alert(inputEl.current.files[0].name)
};
return (
<input
...
onChange={handleGetFiles}
ref={inputEl}
/>
);
}
Through the event target
const Input = () => {
const handleGetFiles = (e) => {
alert(e.target.files[0].name)
};
return (
<input
...
onChange={handleGetFiles}
/>
);
}

Related

For multi-page forms where parent has to call validate() on child, how to manage state?

To provide some context, I am working on building a multi-page form where each page is its own component that should re-render itself in response to (valid/invalid) user input and prevent the form to move to the next page for invalid inputs on current page. Since there is no two-way binding in React, I leveraged a shared context provider between parent-child and used a custom hook to access validation logic. In order to perform the validation checks from the parent I attach a reference to a function which I call inside the child by sending it via props. Each time the parent has to move to next page it simply calls the function that is pointed to by this reference.
Shared context:
class Role {
id: string = '';
description: string = '';
tenure: string = '';
}
class Person {
name: string = '';
age: string = '';
roles: Role[] = [];
}
export const SharedContextProvider: React.FC = ({children}) => {
const [person, setPerson] = useState<Person>(new Person('', ''));
const validateName = () => {
let isValid = person.name !== '';
return {isValid: () => isValid, error: () => isValid ? '' : 'Invalid Name'};
};
...; // other validation methods here skipped
const isValidSection = (errorSetters: Map<FIELDS, (error: string) => void>, validatorHelper: Map<FIELDS, Helper>) => {
let isValidSection = true;
errorSetters.forEach((setError, field) => {
let validation = validatorHelper.get(field);
if (!validation) {
throw new Error('validation not defined!');
}
let isValidField = validation.isValid(); // check if field is valid
isValidSection = isValidSection && isValidField;
let error = validation?.error() || '';
console.log('Validating', field, 'isValid:', isValidField, 'error:', error);
let doSth = errorSetters.get(field)!!;
doSth(error); // debugging
});
return isValidSection;
}
const personalDetailsValidator = (errorSetters: Map<FIELDS, (error: string) => void>) => {
const validatorHelper = new Map<FIELDS, Helper>(
[
[FIELDS.NAME, validateName()],
[FIELDS.AGE, validateAge()],
]
);
return {isValidSection: () => isValidSection(errorSetters, validatorHelper)};
};
const professionalDetailsValidator = (role: Role, errorSetters: Map<FIELDS, (error: string) => void>) => {
const validatorHelper = new Map<FIELDS, Helper>(
[
[FIELDS.DESCRIPTION, validateRoleDescription(role)],
[FIELDS.TENURE, validateTenure(role)],
]
);
return {isValidSection: () => isValidSection(errorSetters, validatorHelper)};
};
const data: SharedContextValue = {
person,
personalDetailsValidator,
professionalDetailsValidator,
setPerson
};
return <SharedContext.Provider value={data}>{children}</SharedContext.Provider>
}
const useSharedContext = () => {
const context = useContext(SharedContext);
if (context === undefined) {
throw new Error('Cannot be undefined');
}
return context;
}
Parent
const App = () => {
const [
activeStepIndex, // controls page on the form
setActiveStepIndex
] = React.useState(0);
let isCurrentSectionValid: () => boolean; // reference to function
const validationHandler = (validateSection: () => boolean) => {
isCurrentSectionValid = validateSection;
}
return (
<Wizard
i18nStrings={{...}}
onNavigate={({detail}) => {
let isValidSection = isCurrentSectionValid();
console.log('[isValidSection]', isValidSection);
if (isValidSection) {
setActiveStepIndex(detail.requestedStepIndex)
}
}
}
activeStepIndex={activeStepIndex}
steps={[
{
content: <PersonalDetails validationHandler={validationHandler}/>
},
{
title: "Completed",
content: <Final validationHandler={validationHandler}/>,
isOptional: true
},
]}
/>
);
}
Child1
const PersonalDetails: React.FC<PersonalProps> = ({validationHandler}) => {
const {person, personalDetailsValidator, setPerson} = useSharedContext();
const [name, setName] = useState(person.name || '');
const [age, setAge] = useState(person.age || '');
const [nameErrorText, setNameErrorText] = useState<string>('');
const [ageErrorText, setAgeErrorText] = useState<string>('');
useEffect(() => {
setPerson(new Person(name, age));
}, [name, age]);
const validator = personalDetailsValidator(new Map<FIELDS, (error: string) => void>([
[FIELDS.NAME, setNameErrorText],
[FIELDS.AGE, setAgeErrorText],
]));
// store validation state for each child
const rolesValidators: Map<string, () => boolean> = new Map<string, () => boolean>();
const roleValidationHandler = ((validateSection: () => boolean, id: string) => {
rolesValidators.set(id, validateSection);
});
validationHandler(() => {
let isValidSection = validator.isValidSection();
let areValidRoles = Array.from(rolesValidators.values()).map(item => item())
.reduce((i, j) => i && j, true);
console.log('isValidSection', isValidSection, 'areValidRoles', areValidRoles);
return isValidSection && areValidRoles;
});
return (
<Container
header={
<Header variant="h2">
Personal Info
</Header>
}
>
<SpaceBetween direction="vertical" size="l">
<FormField label="Name" errorText={nameErrorText}>
<Input value={name} placeholder={'Enter Name'} onChange={({detail}) => {
setName(detail.value);
}}/>
</FormField>
<FormField label="Age" errorText={ageErrorText}>
<Input value={age} placeholder={'Enter Age'}
onChange={({detail}) => {
setAge(detail.value);
}}
/>
</FormField>
{person.roles.map((role, index) =>
<ProfessionalDetails key={role.id} validationHandler={roleValidationHandler} role={role}/>)}
</SpaceBetween>
</Container>
);
}
Child2:
const ProfessionalDetails: React.FC<ProfessionalProps> = ({validationHandler, role}) => {
const [description, setDescription] = useState(role.description || '');
const [tenure, setTenure] = useState(role.tenure || '');
const [descriptionErrorText, setDescriptionErrorText] = useState<string>();
const [tenureErrorText, setTenureErrorText] = useState<string>();
const {professionalDetailsValidator} = useSharedContext();
const validator = professionalDetailsValidator(role, new Map<FIELDS, (error: string) => void>([
[FIELDS.DESCRIPTION, setDescriptionErrorText],
[FIELDS.TENURE, setTenureErrorText],
]));
validationHandler(() => {
return validator.isValidSection();
}, role.id);
useEffect(() => {
role.description = description;
role.tenure = tenure;
console.log('Updating role...', JSON.stringify(role));
}, [role]);
return (
<Container
header={
<Header variant="h2">
Professional Info
</Header>
}
>
<SpaceBetween direction="vertical" size="l">
<FormField label="Description" errorText={descriptionErrorText}>
<Input value={description} placeholder={'Enter Occupation'}
onChange={event => setDescription(event.detail.value)}/>
</FormField>
<FormField label="Tenure" errorText={tenureErrorText}>
<Input value={tenure} placeholder={'Enter Hobby'}
onChange={event => setTenure(event.detail.value)}/>
</FormField>
</SpaceBetween>
</Container>
);
}
Child1,Child2 is validated as expected in isolation. But for list of child2 components if previous states capture an error (for empty input) then the state is not reset when the error is rectified and the errors are retained preventing moving to the next page.
I had two questions:
Is this a sustainable approach to validate forms with deep nested components?
What are good patterns for parent-child communication in these situations?
Note: I cannot use Redux due to my company constraints. It has to be done via React Context/Hooks.

Run tesseract.js OCR onFileUpload and extract text

I think the title is self-explanatory. What am I doing wrong below? What I want to achieve is getting the text out of a photo, right after the user selects a photo.
The error I get is:
createWorker.js:173 Uncaught Error: RuntimeError: null function or function signature mismatch
What am I doing wrong?
const { createWorker } = require("tesseract.js");
const [file,setFile] = useState();
const worker = createWorker({
logger: (m) => console.log(m),
});
const doOCR = async (image) => {
await worker.load();
await worker.loadLanguage("eng");
await worker.initialize("eng");
const {
data: { text },
} = await worker.recognize(image);
// } = await worker.recognize('https://tesseract.projectnaptha.com/img/eng_bw.png');
console.log(text);
setOcr(text);
};
const [ocr, setOcr] = useState("Recognizing...");
useEffect(() => {
file ? doOCR(file) : console.log('no file selected yet!');
}, [file]);
const getFile = (e) => {
console.log("Upload event:", e);
if (e) {
if (Array.isArray(e)) setFile(e[0]);
setFile(e)
}
}
....
<p>{ocr}</p> /* this only displays "Recognizing..." */
<Form.Item
name="uploadedPhoto"
label="Upload your photo scan"
getValueFromEvent={getFile}
// rules={[{ required: true }]}>
<Input type="file"
// onChange={onImageUpload}
/>
</Form.Item>
Solved it by doing it like this instead of the above (I applied the function to the onChange of the Input itself, not the Form.Item element)
const handleFileSelected = (e) => {
const files = Array.from(e.target.files);
setFile(files[0]);
};
<Input type="file" onChange={handleFileSelected} />

Antd uploader accepting all files despite giving the accept prop

I am using antd drag and drop component https://ant.design/components/upload/#components-upload-demo-drag. In the example they have given if I add the prop accept it only accepts the restricted formats and do not add other files in the fileList. However when I use this component in my application, it adds all kinds of files. Why is this behavior occuring and how to avoid it?
const Uploader = () => {
const [files, setFiles] = useState([])
const onChangeHandler = (res) => {
setFiles(res.fileList)
};
console.log(files)
return (
<Upload.Dragger
accept=".pdf,.doc,.docx"
onChange={onChangeHandler}
showUploadList={false}
multiple
fileList={files}
>
Upload
</Upload.Dragger>
);
};
If I drag a png image for instance it does not get added in the fileList but if I manually select any file (which is in not in accept prop) it adds in state which I do not want. Any help?
You can combine with the prop beforeUpload (example in the documentation)
Example:
const Uploader = () => {
const types = ["application/pdf","application/vnd.openxmlformats-officedocument.wordprocessingm","application/msword"];
const [files, setFiles] = useState([])
const onChangeHandler = (res) => {
console.log(res);
let addFiles = true;
for(let i = 0; i < res.fileList.length; i++) {
if (!types.includes(res.fileList[i].type)) {
addFiles = false;
}
}
if( addFiles ) {
setFiles(res.fileList);
}
console.log(files);
};
return (
<Upload.Dragger
accept=".pdf,.doc,.docx"
beforeUpload={(file) => {
if (!types.includes(file.type)) {
message.error(`${file.name} is not a pdf, doc or docx file`);
return false;
} else {
return true
}
}}
onChange={onChangeHandler}
showUploadList={false}
multiple
fileList={files}
>
Upload
</Upload.Dragger>
);
};

How to type a input ref for file upload

I'm trying to type this ref for input, but I don't how. Im using it for file upload. Any idea on how I can type this?
const ProfileLayout: React.FC = ({ children }) => {
let inputUpdateAvatarPhoto = useRef();
.
.
.
.
const handleImageChange = async (e: any) => {
const formData = new FormData();
formData.append('avatar', inputUpdateAvatarPhoto.files[0]);
.
.
.
.
<input
id="avatar"
accept="image/*"
type="file"
ref={input => (inputUpdateAvatarPhoto = input)}
onInput={e => {
handleImageChange(e);
}}
/>
Fixing Your Ref
Your ref has a number of issues:
You need to declare the type of your ref by setting the generic on useRef like useRef<HTMLInputElement>.
React expects the initial value for a DOM element ref to be null instead of undefined.
You are mixing up ref object syntax with callback ref syntax. With a ref object you just pass the whole object to the DOM element like ref={inputUpdateAvatarPhoto}
In order to access the current value of the ref object, you need to look at the .current property
This code should work, but the next code is better
const ProfileLayoutV1: React.FC = ({ children }) => {
const inputUpdateAvatarPhoto = useRef<HTMLInputElement>(null);
const handleImageChange = async (e: React.FormEvent<HTMLInputElement>) => {
const files = inputUpdateAvatarPhoto.current?.files;
// make sure that it's not null or undefined
if (files) {
const formData = new FormData();
formData.append("avatar", files[0]);
}
// need to set something
};
return (
<input
id="avatar"
accept="image/*"
type="file"
ref={inputUpdateAvatarPhoto}
onInput={(e) => {
handleImageChange(e);
}}
/>
);
};
You Don't Need a Ref
See how your handleImageChange function gets an event e, but doesn't use it? inputUpdateAvatarPhoto.current is the same as e.currentTarget!
We actually don't even need a handler on the input though, because you can get a FormData object for the whole form! Check out this example in the MDN docs: Sending files using a FormData object.
We want to set the name property on the input since that's what's used to determine its key in the FormData object.
const ProfileLayoutV2: React.FC = ({ children }) => {
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
// prevent the page from redirecting ot reloading
e.preventDefault();
// get the `FormData` for the whole form
const formData = new FormData(e.currentTarget);
// logs a `File` object
console.log(formData.get("avatar"));
};
return (
<form name="profile_form" onSubmit={handleSubmit}>
<input
id="avatar"
name="avatar"
accept="image/*"
type="file"
/>
<button type="submit">Submit</button>
</form>
);
};
Note that if you want to use the File directly, you'll need to check that it's defined.
const avatar = formData.get("avatar"); // type is `string | File | null`
if ( avatar instanceof File ) {
console.log("we have a file", avatar); // type is now just `File`
}

React Testing Library - Simulating uploading file with FileReader

I want to test a React component where certificate file is uploaded and the result of file can be shown in a text area.
I am still learning writing unit test and here facing difficulty to mock FileReader, readAsText and onloadend.
I have read few examples but they do not work as expected. Any guidance would be helpful
Thanks
Cert Component
export default function Cert() {
const fileInputRef = useRef();
function handleEvent(obj, event) {
var event = new Event(event, { target: obj, bubbles: true });
return obj ? obj.dispatchEvent(event) : false;
}
let handleChangeFile = (file) => {
let fileData = new FileReader();
fileData.readAsText(file);
fileData.onloadend = () => {
let el = document.getElementById('cert');
el.value = event.target.result;
handleEvent(el, 'input');
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
fileData.onerror = function () {
console.log(fileData.error);
};
};
return (
<CheckboxWithOptions name="int" label='app'>
<Field
component={ExpandingTextareaField}
name="certificate"
label='Upload Certificate *'
isRequired
validate={required}
id="cert"
/>
<div>
<Button
label='Upload file'
type="button"
appearance="brand"
size="large"
onClick={() => {
fileInputRef.current.click();
}}
data-typeId="test"
/>
<input
style={{
opacity: 0,
position: 'fixed',
top: 0,
left: 0,
width: 0,
}}
type="file"
accept=".cert"
ref={fileInputRef}
onChange={(e) => handleChangeFile(e.target.files[0])}
data-testid="testinput"
/>
</div>
}
Cert.test.js
it('upload file to show in textarea ', async () => {
const file = new File(['dummy'], 'test.cert', { type: 'cert' })
render(
<div>
<label htmlFor="file-uploader">Upload file:</label>
<input id="file-uploader" type="file" onChange={(e) => handleChangeFile('e')} />
</div>,
)
const { getByLabelText } = render(<TestComponent />);
await waitForElementToBeRemoved(() => screen.getByText(/loading/i));
expect(getByLabelText("CheckBox").value).toBe("false");
fireEvent.click(screen.getByLabelText("CheckBox"))
const input = screen.getByLabelText(/Upload file/i)
fireEvent.click(input)
userEvent.upload(input, file)
await waitFor(() => expect(handleChangeFile).toBeCalledTimes(1)); // working fine till here
jest.spyOn(global, "FileReader")
.mockImplementation(function () {
readAsText = jest.fn();
});
fireEvent.change(input, {
target: {
files: [file]
}
});
expect(FileReader).toHaveBeenCalled(); // not working
// test the FileReader and textarea
})
Had the same issue with checking some side effect that should be invoked on FileReader.onload, so I just ended up setting a short pause after triggering the event (I'm using enzyme):
const pauseFor = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
...
wrapper.find('.upload-box').simulate('drop', someMockedDropEvent);
// set pause was the only way to make reader.onload to fire
await pauseFor(100);
expect(something).toEqual(something)

Resources