Material UI: File Upload using ButtonBase - reactjs

In Material UI Documentation, they showed how to create an "Upload" button by hiding the input file and then adding the Button component inside the label. (see: https://material-ui.com/demos/buttons/)
Now, I want a different button, so I'm working with ButtonBase but it's not working - file select pop-up doesn't show. I'm not sure if I'm missing anything, maybe I'm missing some parameter to pass it?
<input
accept="image/*"
className="d-none"
id="image-upload"
type="file"
/>
<label htmlFor="image-upload"
className="d-block" >
<Button component="span">
Upload
</Button> {/* working */}
<ButtonBase>
test
</ButtonBase> {/* not working*/}
</label>
ButtonBase API: https://material-ui.com/api/button-base/

First, what version are you running? Material-UI is a very fast project so you need to make sure you're checking the documentation for whatever version you are at.
I favor using explicit events (ref in this case) and this works for me under 3.1.0
<input
ref={'file-upload'}
type='file'
/>
<ButtonBase
onClick={e => {
this.refs['file-upload'].click()
}}
>
<div style={{
color: 'red',
backgroundColor: 'yellow',
border: '1px solid green',
}}
>
Upload!
</div>
</ButtonBase>
<hr />
<Button
type='file'
onClick={e => {
this.refs['file-upload'].click()
}}
>
File Upload Material
</Button>
I use something similar to this in one of my projects and I just hide the <input type='file' /> element.

the same can be implement with hooks
export default function myForm(props) {
const inputEl = React.useRef(null);
const onButtonClick = () => {
console.log("inside")
// `current` points to the mounted file input element
inputEl.current.click();
};
return (
<React.Fragment>
Upload Photos
<br/>
<input
accept="image/*"
className={classes.input}
id="outlined-button-file"
multiple
type="file"
ref={inputEl}
/>
<label htmlFor="outlined-button-file">
<ButtonBases
onClick={()=>onButtonClick()}
/>
</label>
</React.Fragment>
)}
Don't forget to call onClick inside ButtonBasses component.
export default function ButtonBases(props) {
const classes = useStyles();
return (
<div className={classes.root}>
<ButtonBase
...
onClick={props.onClick}
>
....
</ButtonBase>
))}
</div>
);
}

Related

Using useRef to assign focus to an element that conditionally exists

I am having some trouble trying to implement this functionality which creates an input field component upon clicking a button, and then assigning focus to the input field as well. I am getting an error saying that inputRef.current is undefined, and am not sure how to proceed.
export default function Post(props) {
const inputRef = React.useRef();
const [commentsToggle, setCommentsToggle] = React.useState(false);
function commentClickHandler() {
setCommentsToggle((prev) => !prev);
inputRef.current.focus();
}
return (
<div className="post">
<div className="postAuthor">
<Avatar
{...stringAvatar(`${props.post.author.username}`)}
alt={`${props.post.author.username}'s Avatar`}
src="./placeholder.jpg"
variant="rounded"
style={avatarStyle}
>
{props.post.author.username[0].toUpperCase()}
</Avatar>
<b>{props.post.author.username}</b> posted:
</div>
<p className="postContent">{props.post.content}</p>
<p className="postDate">{props.post.formatted_date}</p>
<span className="postButtonContainer">
<IconButton className="starsButton" onClick={starClickHandler}>
{props.post.stars.includes(userInfo.userID) ? (
<StarIcon />
) : (
<StarBorderIcon />
)}
{props.post.stars.length}
</IconButton>
<Tooltip title="Add a comment." placement="right">
<IconButton className="commentsButton" onClick={commentClickHandler}>
{commentsToggle ? <ChatBubbleIcon /> : <ChatBubbleOutlineIcon />}
{props.post.comments.length}
</IconButton>
</Tooltip>
</span>
<hr></hr>
<div>
<CommentList comments={props.post.comments}></CommentList>
{commentsToggle ? (
<NewCommentInput
ref={inputRef}
targetPostURL={props.post.url}
getUserData={props.getUserData}
setCommentsToggle={setCommentsToggle}
></NewCommentInput>
) : null}
</div>
</div>
);
}
The above is the code from my parent component, which will conditionally render the child component (the input) that I want to receive focus. Below is this input component:
const NewCommentInput = React.forwardRef((props, ref) => {
return (
<form className="commentInput" onSubmit={commentSubmitHandler}>
<TextField
ref={ref}
multiline
fullWidth
size="small"
name="comment"
placeholder="comment on this post..."
value={commentState.content}
onChange={commentChangeHandler}
></TextField>
<Button variant="outlined" onClick={commentSubmitHandler}>
Comment
</Button>
</form>
);
});
How can I solve inputRef.current being undefined when I click the button that create the component that has the ref?

Dropdown option cannot be selected if I put the component inside my label

Why doesn't my Dropdown component work when I put it inside my <label> tag? The dropdown is displaying the options, but if you click on one, the selection is not working/displayed.
Any suggestions?
export default function App() {
return (
<div>
<div className="mt-10 mb-3 h-6 text-md uppercase font-bold>
People
</div>
<button type="button" onClick={addInvitee}>
+Add menu
</button>
<form onSubmit={handleSubmit}>
{invited.map(({ age, email, id, location, name }, index) => (
<div key={id}>
<div className="grid grid-cols-3 gap-5">
<label className="mr-3 h-6 text-md font-bold">
Names:
<input
type="text"
value={name}
placeholder="Names"
name="name"
onChange={updateInvitee(id)}
/>
</label>
//other inputs with the exact same pattern
<label>
Choice:
<Dropdown className="w-3/5" options={CHOICE} isMulti={false} />
</label>
...//other inputs
</form>
</div>
);
}
Dropdown.jsx
import React, { useState } from "react";
import Select from "react-select";
import Tag from "./Tag";
export default function Dropdown({
className,
style,
options,
styleSelect,
defaultValue,
isMulti = false
}) {
const [selected, setSelected] = useState(defaultValue);
const styles = {
select: {
width: "100%",
maxWidth: 200
}
};
return (
<div style={style}>
{selected && isMulti === false ? (
<Tag
selected={selected}
setSelected={setSelected}
styleSelect={styleSelect}
/>
) : (
<Select
className={className}
style={styles.select}
value={selected}
onChange={setSelected}
options={options}
isMulti={isMulti}
/>
)}
</div>
);
}
Here is my CodeSandbox
Firstly here's something important to note:
Based on this image, a console snippet from sandbox, it shows that the selection happens, but it gets cleared instantly.
What's the cause for this? Hmm.. let's take a look.
Consider this code snippet:
<label>
Press the text
<br /><br />
<button onClick="console.log('button-clicked')">Button</button>
</label>
Here, the <button> is placed inside a <label>. Do you notice, that when you click on press the text, the button's onClick gets triggered? ... But why?
Well, by default, a <label> is a focusable html element, and when focused, it triggers any form control element placed inside it.
So how does this relate to your code?
You have this line of code inside your <Tag> component onClick={() => setSelected(null)} and that's where the problem is. When you pick a selection, the selected gets updated and the component re-renders and displays your <Tag> component... but the event still bubbles up tree again until it reaches the <label> element. remember, at this point, it's no longer the <Select> component shown, but the <Tag> component. And guess what? the <label> gets focused and triggers the <button> inside your <Tag> component which clears (setSelected(null)) the selected state property. Once the selected is cleared, the <Dropdown> component re-renders and the <Select> component is displayed again.
This goes on and on and on as you try to select, then the process repeats.
So, from your code... Just remove this here onClick={() => setSelected(null)} and you'll see it will work. So you just need to work around it on how to clear the selected, but I have suggested a solution below.
The Solution
In your <Dropdown> component, we should try and prevent the event from bubbling. So all you need to do is add the following onClick={e=>e.preventDefault()} in your <div>
<div style={style} onClick={(e) => e.preventDefault()}>
{selected && isMulti === false ? (
<Tag
selected={selected}
setSelected={setSelected}
styleSelect={styleSelect}
/>
) : (
<Select
className={className}
style={styles.select}
value={selected}
onChange={setSelected}
options={options}
isMulti={isMulti}
/>
)}
</div>
Here's the original sandbox with the solution applied.

get selected file object with react-hook-form

I have a component which uploads a file. When an image is selected it previews it in another component.
So, when an image is selected, I need to access it. I tried using getValues() and watch() from react-hook-form but it is returning me path of the file in string form not the file object.
<div className="photo">
<Avatar
src={
// it gives path of the file as string
getValues('photo')
? URL.createObjectURL(getValues('photo')[0])
: "https://icon-library.com/images/no-image-icon/no-image-icon-0.jpg"
}
alt="photo preview"
sx={{ width: "200px", height: "200px" }}
/>
<label htmlFor="contained-button-file">
<Controller
name="photo"
control={control}
render={({ field }) => (
<Input
{...field}
accept="image/*"
id="contained-button-file"
type="file"
error={Boolean(errors["photo"])}
helperText={errors["photo"]?.message}
/>
)}
/>
<Button variant="contained" component="span">
Upload
</Button>
</label>
</div>
Somehow, watch() and getValues() reading value of input field here. While they should be returning e.target.files instead.
It can be checked here and here, It works the same way. It returns the files.
Why it does not return files but returning value here?
Using work around for now. Using setValue() and calling it on change of the input file.
<div className="photo">
<Avatar
src={
watch('photo')
? URL.createObjectURL(watch('photo')[0])
: "https://icon-library.com/images/no-image-icon/no-image-icon-0.jpg"
}
alt="photo preview"
sx={{ width: "200px", height: "200px" }}
/>
<label htmlFor="contained-button-file">
<Input
accept="image/*"
id="contained-button-file"
type="file"
error={Boolean(errors["photo"])}
onChange={(e) => setValue("photo", e.target.files)}
multiple
helperText={errors["photo"]?.message}
/>
<Button variant="contained" component="span">
Upload
</Button>
</label>
</div>

Passing function to create new component to child using react hooks

I have a component that consists of an array of SVG arrows, like this
const App = () => {
const [arrows, createArrow]=useState([<Arrow x="7" y="100"/>,
<Arrow x="50" y="100"/>])
return (
<div>
<button onClick={()=>createArrow([...arrows, <Arrow x="75" y="100"/>])}>Add Force</button>
<ArrowForm newArrow = {createArrow} arrows = {arrows}/>
<svg
style={{
border: '1px solid green',
height: '200px',
width: '100%',
}}
>
<Grid/>
{arrows.map((child) => child)}
</svg>
</div>
)
}
I'd like to set the position of the arrow using a form component, which I've created like this:
export default function ArrowForm({newArrow, arrows}) {
const[position, setPosition] = useState({x:1, y:0})
return(
<div>
<label>x:
<input type="number" value = {position.x} onChange={e=>setPosition(e.target.value)}/>
</label>
<label>y:
<input type="number" value = {position.y} onChange={e=>setPosition(e.target.value)}/>
</label>
<input type="submit" value="Submit" onSubmit= ()=> {newArrow([...arrows, <Arrow x="75" y="100"/>])} />
</div>
)
}
I think I need to pass the arrows state and createArrow function from the App form so that I can create arrows when I press the submit button in the form, but I'm getting a Maximum Update Depth exceeded error.
The bug is in your ArrowForm
<input type="submit"
value="Submit"
onSubmit= {newArrow([...arrows, <Arrow x="75" y="100"/>])} />
it means as soon as your component render call onSubmit instead you have to do this:
<input
type="submit"
value="Submit"
onSubmit={() => newArrow([...arrows, <div x="75" y="100" />])}
/>
Here is the demo. See console. As I don't know what is your arrow component do.
#Shubham Verma answer is correct, but you and he/she missed that input doesn't have onSubmit property, it is form that does.
So you should use onClick.
Here is the sandbox - https://codesandbox.io/s/restless-surf-xb1h1

how to use useRef for the attribute htmlFor?

I want to customize the input tag for file upload.
This is my code. Here for the attribute htmlFor, I am giving id of the input tag.Then it is working. But instead I want to use useRef ref. How can I do that ? If I follow the below method, it will be problematic if I render this component more than once. right ?
const App = () => {
const inputRef = useRef(null);
const [file, setFile] = useState(null);
return (
<>
<input
ref={inputRef}
accept=".pdf"
style={{ display: "none" }}
id="raised-button-file"
multiple
type="file"
onChange={e => {
setFile(e.target.files[0]);
}}
/>
<label htmlFor="raised-button-file">
<button component="span">
<span>file</span>
</button>
</label>
</>
);
};
Another way of using <label> tag is by wrapping your element as a child without specifying an id for it.
<label>
<input
accept=".pdf"
style={{ display: "none" }}
multiple
type="file"
onChange={e => {
setFile(e.target.files[0]);
}}
/>
<span>File</span>
</label>
If you prefer to open your file input dialog with your ref, you can do like this.
const handleOpenFileInput = () => {
inputRef.current.click();
};
<label onClick={handleOpenFileInput}>
<button>file</button>
</label>
<input
ref={inputRef}
accept=".pdf"
style={{ display: "none" }}
multiple
type="file"
onChange={e => {
setFile(e.target.files[0]);
}}
/>
If you use userRef it won't solve your problem. The problem is in the label and the htmlFor attribute. It constantly gets the inputs whose ids match the htmlFor attribute, and since your render the component multiple times, it always gets the first match.
I would simply pass the id of each component as a property, so that each time the label matches the right input. I would change the code to look more like this:
const Form = ({id}) => {
const onChangeInput = e => {
const [file] = e.target.files
}
return (
<form>
<div>
<input type="file" id={id} name="file" className="my-input" accept="application/pdf" style={{display:"none"}} onChange={onChangeInput} multiple/>
<label htmlFor={id}>Upload</label>
</div>
</form>
)
}
function App() {
return (
<div className="App">
<Form id="form1"/>
<Form id="form2"/>
<Form id="form3"/>
</div>
);
}
To make sure that each document has been uploaded correctly, I passed a className attribute to the inputs so I can get all of them. Running this code, I find all files that I have uploaded
Array.from(document.querySelectorAll('.my-input')).map(v => v.files[0].name)

Resources