quick rundown of code before I ask question (Im using Material UI in react)
this is a container that should just hold chat messages
const ChatContainer = ({ chatMessages }) => {
const classes = useStyles();
return (
<Paper className={classes.chatContainer}>
{chatMessages.map((msg) => (
<ChatMessage key={msg.id} author={msg.author} content={msg.content} />
))}
</Paper>
);
};
export default ChatContainer;
this is a component to send things in this case a chat message
const SendInput = ({ label, onSubmit }) => {
const [inputValue, setInputValue] = useState("");
const classes = useStyles();
const handleChange = (e) => setInputValue(e.target.value);
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(inputValue);
setInputValue("");
};
return (
<form onSubmit={(e) => handleSubmit(e)}>
<TextField
label={label}
placeholder={label}
variant="outlined"
value={inputValue}
onChange={handleChange}
fullWidth
InputProps={{
endAdornment: (
<>
<Divider orientation="vertical" className={classes.divider} />
<IconButton type="submit">
<SendIcon color="primary" />
</IconButton>
</>
),
}}
/>
</form>
);
};
export default SendInput;
this is how im rendering them together
<Box>
<ChatContainer chatMessages={chatMsgs} />
<SendInput label="Send message" onSubmit={handleSubmit} />
</Box
here is what the screen looks like https://gyazo.com/d96744d356ceef81aa06345f0f0c2304
what I want is the ChatContainer to fill up the whitespace and push the input to the bottom of the screen. any help would be appreciated thx
There are multiple ways to achieve this. This question has many of them. I use flexbox approach which is given that answer.
Make the height of root item (Box) 100vh to fill all screen, make it flex, and set its direction as column to show child items vertically.
const useStyles = makeStyles({
root: {
display: "flex",
flexDirection: "column",
height: "100vh"
}
});
export default function App() {
const classes = useStyles();
const handleSubmit = console.log;
return (
<Box className={classes.root}>
<ChatContainer chatMessages={chatMsgs} />
<SendInput label="Send message" onSubmit={handleSubmit} />
</Box>
);
}
Make marginTop property of last item auto to push it bottom.
const useStyles = makeStyles({
root: {
marginTop: "auto"
}
});
const SendInput = ({ label, onSubmit }) => {
const classes = useStyles();
// removed for simplicity
return (
<form onSubmit={(e) => handleSubmit(e)} className={classes.root}>
{/* removed for simplicity */}
</form>
);
};
View at codesandbox.
Related
I have a react native modal with a search bar and a flatlist that shows results. The search result in the flatlist has to be tapped twice for the click to register. I need to figure out how to make it work on first click. Here is the code
const Item = ({ item, onPress, value }) => (
<TouchableOpacity style={styles.modalItemStyle} onPress={onPress}>
<View style={styles.modalIconStyle}>
{item.id === value && <Icon name="sheep" size={20} color="#68c25a" />}
</View>
<Text style={styles.modalItemTextStyle}>{item.title}</Text>
</TouchableOpacity>
);
const MyDropdown = ({
data,
label,
field,
onSelect,
value,
searchable = true,
}) => {
const [modalOpen, setModalOpen] = useState(false);
const [selectedValue, setSelectedValue] = useState(value);
const [query, setQuery] = useState("");
const [modalData, setModalData] = useState(data);
useEffect(() => {
if (query.length > 0) {
const filteredData = data.filter((item) =>
item.title.toLowerCase().includes(query.toLowerCase())
);
setModalData(filteredData);
} else {
setModalData(data);
}
}, [query]);
const inputRef = useRef(null);
const searchRef = useRef(null);
const renderItem = ({ item }) => {
return (
<Item
item={item}
value={selectedValue.id}
onPress={() => {
inputRef.current.blur();
Keyboard.dismiss();
setQuery("");
setSelectedValue(item);
setModalOpen(false);
}}
/>
);
};
return (
<View style={styles.selectContainer}>
<TextInput
ref={inputRef}
//react native paper text input with value and label
label={label}
value={selectedValue.title}
style={styles.sheepTextInput}
mode="outlined"
onChangeText={(text) => onSelect(text)}
showSoftInputOnFocus={false}
onFocus={() => {
setModalOpen(true);
inputRef.current.blur();
}}
></TextInput>
<Modal height="auto" isVisible={modalOpen}>
<View style={styles.modal}>
{searchable && (
<View>
<TextInput
ref={searchRef}
mode="outlined"
outlineColor="#68c25a"
activeOutlineColor="#68c25a"
style={styles.modalSearch}
value={query}
onChangeText={(q) => setQuery(q)}
placeholder="Search"
//add clear button
right={
<TextInput.Icon
name="close"
color="#68c25a"
onPress={() => {
setQuery("");
}}
/>
}
></TextInput>
</View>
)}
<FlatList
keyboardShouldPersistTaps="always"
data={modalData}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
</View>
</Modal>
</View>
);
};
I tried adding keyboardShouldPersistTaps with different options to flatlist, I also tried to blur through refs (searchref), but none of those approaches worked. What am I doing wrong?
keyboardShouldPersistTaps should be "handled" if you want it to work on first tap.
<FlatList
keyboardShouldPersistTaps="handled"
data={modalData}
renderItem={renderItem}
keyExtractor={(item) => item.id}
/>
I managed to fix this by setting the keyboardshouldpersisttaps property to handled on every scrollview, including the flatlist in question and the component that renders all of those inputs in my form. I also had to wrap the flatlist with the search input in a scrollview. Of course I now have a virtualized lists should never be nested warning, but at least the taps now work.
If I have variable as such:
const [APIKey, setAPIKey] = useState([])
Can I group these into 1 variable and pass it along as a prop?
const passAPI = {APIKey, setAPIKey}
Here is how I'm attempting to pass it:
const Home = () => {
const [APIKey, setAPIKey] = useState([])
const [Symbols, setSymbols] = useState([])
const [Endpoint, setEndpoint] = useState([])
const [Base, setBase] = useState([])
const passAPI = {APIKey, setAPIKey}
const passSymbols = {Symbols, setSymbols}
const passEndpoint = {Endpoint, setEndpoint}
const passBase = {Base, setBase}
return (
<Grid container spacing='8' className='HomeWrapper'>
<Grid item sx={{ display: { xs: 'none', sm: 'flex' }, width: '50%' }} className='x1'>
<Paper elevation='6' className='HomePaper' sx={{ width: '100%'}}>
<TopLeft passAPI={passAPI} passSymbols={passSymbols} passEndpoint={passEndpoint} passBase={passBase}/>
</Paper>
</Grid>
...
And here is an example of how I was trying to use it:
const TopRight = ({passAPI, passSymbols, passEndpoint, passCurrency}) => {
return (
<Box className='TopRightWrapper' sx={{ display: 'flex', flexDirection: 'column' }} pl={2} pt={1} pb={1}>
<FormControl onSubmit={passAPI}>
<Box>
<InputLabel htmlFor="my-input">API Key</InputLabel>
<Input id="my-input" aria-describedby="my-helper-text" />
<button type='submit'>click</button>
</Box>
</FormControl>
I would want to use the setAPIKey function that is found in the passAPI variable. How do I go about this?
As I can see passAPI in your case is an object. So to get setState from passAPI you should use "passAPI.setAPIKey" in onSubmit.
So it should be like that:
<FormControl onSubmit={passAPI.setAPIKey}>
<Box>
<InputLabel htmlFor="my-input">API Key</InputLabel>
<Input id="my-input" aria-describedby="my-helper-text" />
<button type='submit'>click</button>
</Box>
</FormControl>
Also FormControl API does not have onSubmit event, you should use "form" tag instead.
Through form onSumbit event argument you would be able to access "target" property which would have form elements inside, and then you will access value of that input element. Something like that
<form
onSubmit={(e) => {
e.preventDefault();
console.log("e", e.target[0].value);
passAPI.setAPIKey(e.target[0].value)
}}
>
Let me know if it helps.
Been struggling to try and solve this one,
I have a <form> that is wrapped around a Controller using a render. The render is a list of <Chip>'s. I want the list of chips to act as a field, each user toggled chip would act as the data inputted to the form, based on the label value of a chip. The expected data can be either one string or an array of strings based on how many <Chips> are toggled.
I've tried to replicate examples that use <Checkbox>, but I wasn't able to re-create the component.
I have a state of total chips toggled, which re-produces the data I would like to submit via my button. Is there a way to pass this state to my form data?
I'm currently only able to submit empty data through my form because my chip values and form data are not linked up correctly.
To solve this I need to add onChange to my onClick parameter for <Chip>, I've tried doing this:
onClick={(value, e) => { handleToggle(value); onChange(value) }}
But that does not work?
Here is my Form
<form noValidate onSubmit = { handleSubmit(onSubmit) }>
<Controller
render={({ onChange ,...props }) => (
<Paper component="ul" className={classes.root}>
{stocklist.map((value, index) =>
{
return (
<li key={index}>
<Chip
name="stock_list"
variant="outlined"
label={value}
key={index}
color={checked.includes(value) ? 'secondary' : 'primary'}
onClick={handleToggle(value)}
className={classes.chip}
ref={register}
/>
</li>
);
})}
</Paper>
)}
name="stock_list"
control={control}
defaultValue={[]}
onChange={([, data]) => data}
/>
{checked && checked.length > 0 &&
<Fab
color="primary"
aria-label="delete"
type="submit"
>
<DeleteIcon />
</Fab>
}
</form>
Here is how I toggle the chips, and create a state checked which holds the values for every toggled chip
const { register, control, handleSubmit } = useForm();
const [checked, setChecked] = React.useState([]);
const handleToggle = (value) => () => {
const currentIndex = checked.indexOf(value);
const newChecked = [...checked];
if (currentIndex === -1) {
newChecked.push(value);
} else {
newChecked.splice(currentIndex, 1);
}
setChecked(newChecked);
};
Here is my onSubmit function
const onSubmit = (data, e) =>
{
console.log(data);
axiosInstance
.patch('url', {
stock_list: data.stock_list,
})
.then((res) =>
{
console.log(res);
console.log(res.data);
});
};
Check it out https://codesandbox.io/s/cocky-roentgen-j0pcg Please toggle one of the chips, and then press the button that appears. If you then check the console, you will notice an empty form array being submitted.
I guess this will work for you
import React, { useState } from "react";
import { Chip, Paper, Fab, Grid } from "#material-ui/core/";
import { makeStyles } from "#material-ui/core/styles";
import { Controller, useForm } from "react-hook-form";
import DeleteIcon from "#material-ui/icons/Delete";
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
justifyContent: "center",
flexWrap: "wrap",
listStyle: "none",
padding: theme.spacing(0.5),
margin: 0,
"#media (max-width: 600px)": {
overflowY: "auto",
height: 200
}
},
chip: {
margin: theme.spacing(0.5)
}
}));
export default function app() {
const { register, control, handleSubmit } = useForm();
const classes = useStyles();
const [checked, setChecked] = React.useState([]);
const stocklist = ["AAPL", "AMD", "TSLA"];
const onSubmit = (data, e) => {
console.log("data.stock_list");
console.log(data.stock_list);
console.log("data.stock_list");
};
const handleToggle = (value) => {// <== I changed this part
const currentIndex = checked.indexOf(value);
const newChecked = [...checked];
if (currentIndex === -1) {
newChecked.push(value);
} else {
newChecked.splice(currentIndex, 1);
}
setChecked(newChecked);
return newChecked;// <== I changed this part
};
return (
<>
{(!stocklist || stocklist.length === 0) && (
<p style={{ textAlign: "center" }}>Your Bucket is empty...</p>
)}
{stocklist && stocklist.length > 0 && (
<Grid>
<form noValidate onSubmit={handleSubmit(onSubmit)}>
<Controller
name="stock_list"// <== I changed this part
render={({ onChange, ...props }) => (
<Paper component="ul" className={classes.root}>
{stocklist.map((value, index) => {
return (
<li key={index}>
<Chip
// name="stock_list"// <== I changed this part
variant="outlined"
label={value}
key={index}
color={
checked.includes(value) ? "secondary" : "primary"
}
onClick={(e) => {
onChange(handleToggle(value));// <== I changed this part
}}
className={classes.chip}
ref={register}
/>
</li>
);
})}
</Paper>
)}
control={control}
defaultValue={{}}
onChange={([, data]) => data}
/>
{checked && checked.length > 0 && (
<Fab color="primary" aria-label="delete" type="submit">
<DeleteIcon />
</Fab>
)}
</form>
</Grid>
)}
</>
);
}
I have a parent component that initializes the state using hooks. I pass in the state and setState of the hook into the child, but whenever I update the state in multiple children they update the state that is not the most updated one.
To reproduce problem: when you make a link and write in your info and click submit, it successfully appends to the parent state. If you add another one after that, it also successfully appends to the parent state. But when you go back and press submit on the first link, it destroys the second link for some reason. Please try it out on my codesandbox.
Basically what I want is a button that makes a new form. In each form you can select a social media type like fb, instagram, tiktok, and also input a textfield. These data is stored in the state, and in the end when you click apply changes, I want it to get stored in my database which is firestore. Could you help me fix this? Here is a code sandbox on it.
https://codesandbox.io/s/blissful-fog-oz10p
and here is my code:
Admin.js
import React, { useState } from 'react';
import Button from '#material-ui/core/Button';
import AddNewLink from './AddNewLink';
const Admin = () => {
const [links, setLinks] = useState({});
const [newLink, setNewLink] = useState([]);
const updateLinks = (socialMedia, url) => {
setLinks({
...links,
[socialMedia]: url
})
}
const linkData = {
links,
updateLinks,
}
const applyChanges = () => {
console.log(links);
// firebase.addLinksToUser(links);
}
return (
<>
{newLink ? newLink.map(child => child) : null}
<div className="container-sm">
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
onClick={() => {
setNewLink([ ...newLink, <AddNewLink key={Math.random()} linkData={linkData} /> ])}
}
>
Add new social media
</Button>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
style={{marginTop: '50px'}}
onClick={() => applyChanges()}
>
Apply Changes
</Button>
<h3>{JSON.stringify(links, null, 4)}</h3>
</div>
</>
);
}
export default Admin;
AddNewLink.js
const AddNewLink = props => {
const [socialMedia, setSocialMedia] = useState('');
const [url, setUrl] = useState('');
const { updateLinks } = props.linkData;
const handleSubmit = () => {
updateLinks(socialMedia, url)
}
return (
<>
<FormControl style={{marginTop: '30px', marginLeft: '35px', width: '90%'}}>
<InputLabel>Select Social Media</InputLabel>
<Select
value={socialMedia}
onChange={e => {setSocialMedia(e.target.value)}}
>
<MenuItem value={'facebook'}>Facebook</MenuItem>
<MenuItem value={'instagram'}>Instagram</MenuItem>
<MenuItem value={'tiktok'}>TikTok</MenuItem>
</Select>
</FormControl>
<form noValidate autoComplete="off" style={{marginBottom: '30px', marginLeft: '35px'}}>
<TextField id="standard-basic" label="Enter link" style={{width: '95%'}} onChange={e => {setUrl(e.target.value)}}/>
</form>
<div className="container-sm">
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
style={{marginBottom: '30px'}}
onClick={() => handleSubmit()}
>
Submit
</Button>
</div>
</>
)
}
export default AddNewLink;
All I see is that links in AddNewLink would be a stale closure but in your question you never use it. Here is your code "working" since you didn't describe what it is supposed to do it always "works"
const { useState } = React;
const AddNewLink = (props) => {
const [socialMedia, setSocialMedia] = useState('');
const [url, setUrl] = useState('');
const { updateLinks, links } = props.linkData;
console.log('links is a stale closure:', links);
const handleSubmit = () => {
updateLinks(socialMedia, url);
};
return (
<div>
<select
value={socialMedia}
onChange={(e) => {
setSocialMedia(e.target.value);
}}
>
<option value="">select item</option>
<option value={'facebook'}>Facebook</option>
<option value={'instagram'}>Instagram</option>
<option value={'tiktok'}>TikTok</option>
</select>
<input
type="text"
id="standard-basic"
label="Enter link"
style={{ width: '95%' }}
onChange={(e) => {
setUrl(e.target.value);
}}
/>
<button
type="submit"
variant="contained"
color="primary"
style={{ marginBottom: '30px' }}
onClick={() => handleSubmit()}
>
Submit
</button>
</div>
);
};
const Admin = () => {
const [links, setLinks] = useState({});
const [newLink, setNewLink] = useState([]);
const updateLinks = (socialMedia, url) =>
setLinks({
...links,
[socialMedia]: url,
});
const linkData = {
links,
updateLinks,
};
const applyChanges = () => {
console.log(links);
// firebase.addLinksToUser(links);
};
return (
<React.Fragment>
{newLink ? newLink.map((child) => child) : null}
<div className="container-sm">
<button
type="submit"
variant="contained"
color="primary"
onClick={() => {
setNewLink([
...newLink,
<AddNewLink
key={Math.random()}
linkData={linkData}
/>,
]);
}}
>
Add new social media
</button>
<button
type="submit"
variant="contained"
color="primary"
style={{ marginTop: '50px' }}
onClick={() => applyChanges()}
>
Apply Changes
</button>
<h3>{JSON.stringify(links, null, 4)}</h3>
</div>
</React.Fragment>
);
};
ReactDOM.render(<Admin />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
It is not a good idea to put jsx in local state, save the data in state instead and pass that to the component every render.
Before diving to the main problem, my use case is I am trying to handle the scroll to a desired section. I will have navigations on the left and list of form sections relative to those navigation on the right. The Navigation and Form Section are the child component. Here is how I have structured my code
Parent.js
const scrollToRef = ref => window.scrollTo(0, ref.current.offsetTop);
const Profile = () => {
const socialRef = React.useRef(null);
const smsRef = React.useRef(null);
const handleScroll = ref => {
console.log("scrollRef", ref);
scrollToRef(ref);
};
return (
<>
<Wrapper>
<Grid>
<Row>
<Col xs={12} md={3} sm={12}>
<Navigation
socialRef={socialRef}
smsRef={smsRef}
handleScroll={handleScroll}
/>
</Col>
<Col xs={12} md={9} sm={12}>
<Form
socialRef={socialRef}
smsRef={smsRef}
/>
</Col>
</Row>
</Grid>
</Wrapper>
</>
);
};
Navigation.js(child component)
I tried using forwardRef but seems like it only accepts one argument as ref though I have multiple refs.
const Navigation = React.forwardRef(({ handleScroll }, ref) => {
// it only accepts on ref argument
const items = [
{ id: 1, name: "Social connections", pointer: "social-connections", to: ref }, // socialRef
{ id: 2, name: "SMS preference", pointer: "sms", to: ref }, // smsRef
];
return (
<>
<Box>
<UL>
{items.map(item => {
return (
<LI
key={item.id}
active={item.active}
onClick={() => handleScroll(item.to)}
>
{item.name}
</LI>
);
})}
</UL>
</Box>
</>
);
});
export default Navigation;
Form.js
I do not have idea on passing multiple refs when using forwardRef so for form section I have passed the refs as simple props passing.
const Form = ({ socialRef, smsRef }) => {
return (
<>
<Formik initialValues={initialValues()}>
{({ handleSubmit }) => {
return (
<form onSubmit={handleSubmit}>
<Social socialRef={socialRef} />
<SMS smsRef={smsRef} />
</form>
);
}}
</Formik>
</>
);
};
Social.js
const Social = ({ socialRef }) => {
return (
<>
<Row ref={socialRef}>
<Col xs={12} md={3}>
<Label>Social connections</Label>
</Col>
<Col xs={12} md={6}></Col>
</Row>
</>
);
};
Can anyone help me at passing multiple refs so when clicked on the particular navigation item, it should scroll me to its respective component(section).
I have added an example below. I have not tested this. This is just the idea.
import React, { createContext, useState, useContext, useRef, useEffect } from 'react'
export const RefContext = createContext({});
export const RefContextProvider = ({ children }) => {
const [refs, setRefs] = useState({});
return <RefContext.Provider value={{ refs, setRefs }}>
{children}
</RefContext.Provider>;
};
const Profile = ({ children }) => {
// ---------------- Here you can access refs set in the Navigation
const { refs } = useContext(RefContext);
console.log(refs.socialRef, refs.smsRef);
return <>
{children}
</>;
};
const Navigation = () => {
const socialRef = useRef(null);
const smsRef = useRef(null);
const { setRefs } = useContext(RefContext);
// --------------- Here you add the refs to context
useEffect(() => {
if (socialRef && smsRef) {
setRefs({ socialRef, smsRef });
}
}, [socialRef, smsRef, setRefs]);
return <>
<div ref={socialRef}></div>
<div ref={smsRef}></div>
</>
};
export const Example = () => {
return (
<RefContextProvider>
<Profile>
<Navigation />
</Profile>
</RefContextProvider>
);
};