I'm using material-ui in my react project.
I wanna copy a number and paste in multiple text fields.
const [otpArr, setOtpArr] = useState<string[]>(['', '', '', '', '', ''])
let inputRefs = useRef([React.createRef<HTMLDivElement>(), React.createRef<HTMLDivElement>(), React.createRef<HTMLDivElement>(), React.createRef<HTMLDivElement>(), React.createRef<HTMLDivElement>(), React.createRef<HTMLDivElement>()])
{otpArr && otpArr.map((x, index) => (
<NumberTextField
inputRef={inputRefs.current[index]}
variant='outlined'
size='small'
id='otp'
value={x}
key={index}
onChange={(e: any) => {
const temp = [...otpArr]
temp[index] = e.target.value
setOtpArr(temp)
if ((index < otpArr.length - 1) && e.target.value.length === 1) {
inputRefs.current[index + 1]?.current?.focus()
}
}}
inputProps={{ maxLength: 1 }}
style={{
width: '3rem',
direction: 'ltr',
marginTop: '3rem',
}}
/>
))}
any solution?
Here is a working example:
https://codesandbox.io/s/simple-6-digite-input-past-2qqdg
You can read the paste event and get the 6 digit with an event listener:
// listen "paste" event in useEffect
useEffect(() => {
const onPaste = (e) => {
let paste = (e.clipboardData || window.clipboardData).getData("text");
paste = paste.toUpperCase().split("");
if (paste.length === 6) {
setValue(paste);
}
};
document.addEventListener("paste", onPaste);
return () => {
document.removeEventListener("paste", onPaste);
};
}, [value]);
PS: You can add Event on an input element
Related
Currently I have a textarea like this:
<textarea
onChange={handleTextAreaChange}
ref={textAreaRef as MutableRefObject<HTMLTextAreaElement>}
id={id}
value={content}
></textarea>
I am implementing some buttons to add markdown to the textarea to make it easier for the user to update and have this function for bold:
const handleBoldClick = useCallback(() => {
const selectionStart = textAreaRef.current?.selectionStart;
const selectionEnd = textAreaRef.current?.selectionEnd;
if (selectionStart && selectionEnd) {
setContent(
prevContent =>
prevContent.substring(0, selectionStart) +
'**' +
prevContent.substring(selectionStart, selectionEnd) +
'**' +
prevContent.substring(selectionEnd, prevContent.length)
);
} else {
setContent(prevContent => prevContent + '****');
// Want this to run after textarea gets updated
textAreaRef.current?.focus();
textAreaRef.current?.setSelectionRange(
content.length - 3,
content.length - 3
);
}
const changeEvent = new Event('change', { bubbles: true });
// Want to run this after textarea is updated
textAreaRef.current?.dispatchEvent(changeEvent);
}, [content]);
setContent is the setter for content which is passed to the textarea. Is there a way to guarantee the parts I've marked with comments as wanting to only run once the DOM gets updated run when I want them to?
I finagled around with things and went with this approach (gonna post the entire component, which contains some stuff irrelevant to the question):
const MarkdownTextArea = ({
value,
onBlur = () => {},
onChange = () => {},
touched = false,
error,
id,
label
}: MarkdownTextAreaProps) => {
const [content, setContent] = useState(value ?? '');
const [numberOfRows, setNumberOfRows] = useState(5);
const [numberOfCols, setNumberOfCols] = useState(20);
const [isPreview, setIsPreview] = useState(false);
const [changed, setChanged] = useState<'bold' | null>();
const textAreaRef = useRef<HTMLTextAreaElement>();
useEffect(() => {
const setColsAndRows = () => {
const newColumnsNumber = Math.floor(
(textAreaRef.current?.offsetWidth ?? 100) /
(convertRemToPixels(1.2) / 1.85)
);
setNumberOfCols(newColumnsNumber);
setNumberOfRows(calculateNumberOfRows(content, newColumnsNumber));
};
setColsAndRows();
window.addEventListener('resize', setColsAndRows);
return () => {
window.removeEventListener('resize', setColsAndRows);
};
}, [content]);
const handleTextAreaChange: ChangeEventHandler<HTMLTextAreaElement> =
useCallback(
event => {
onChange(event);
setContent(event.target.value);
setNumberOfRows(
calculateNumberOfRows(
event.target.value,
textAreaRef.current?.cols ?? 20
)
);
},
[onChange]
);
const handleBoldClick = useCallback(() => {
const selectionStart = textAreaRef.current?.selectionStart;
const selectionEnd = textAreaRef.current?.selectionEnd;
if (selectionStart && selectionEnd) {
setContent(
prevContent =>
prevContent.substring(0, selectionStart) +
'**' +
prevContent.substring(selectionStart, selectionEnd) +
'**' +
prevContent.substring(selectionEnd, prevContent.length)
);
} else {
setContent(prevContent => prevContent + '****');
}
setChanged('bold');
}, []);
if (changed && textAreaRef.current?.value === content) {
const changeEvent = new Event('change', { bubbles: true });
textAreaRef.current?.dispatchEvent(changeEvent);
if (changed === 'bold' && textAreaRef.current) {
textAreaRef.current.focus();
textAreaRef.current.selectionStart = content.length - 2;
textAreaRef.current.selectionEnd = content.length - 2;
}
setChanged(null);
}
return (
<div className={classes.container} data-testid="markdown-text-area">
<div className={classes['header']}>
<label className={classes.label} htmlFor={id}>
{label}
</label>
<Button
positive
style={{ justifySelf: 'flex-end' }}
onClick={() => setIsPreview(prev => !prev)}
>
{isPreview ? 'Edit' : 'Preview'}
</Button>
</div>
<div className={classes['text-effect-buttons']}>
<button
className={classes['text-effect-button']}
onClick={handleBoldClick}
type="button"
style={{ fontWeight: 'bold' }}
>
B
</button>
</div>
{isPreview ? (
<div className={classes['markdown-container']} id={id}>
<MarkdownParser input={content} />
</div>
) : (
<textarea
onChange={handleTextAreaChange}
className={`${classes['text-input']}${
error && touched ? ` ${classes.error}` : ''
}`}
ref={textAreaRef as MutableRefObject<HTMLTextAreaElement>}
rows={numberOfRows}
cols={numberOfCols}
onBlur={onBlur}
id={id}
value={content}
></textarea>
)}
{error && touched && (
<div className={classes['error-message']}>{error}</div>
)}
</div>
);
};
The part of the following component most relevant to answering the question is the following:
if (changed && textAreaRef.current?.value === content) {
const changeEvent = new Event('change', { bubbles: true });
textAreaRef.current?.dispatchEvent(changeEvent);
if (changed === 'bold' && textAreaRef.current) {
textAreaRef.current.focus();
textAreaRef.current.selectionStart = content.length - 2;
textAreaRef.current.selectionEnd = content.length - 2;
}
setChanged(null);
}
I have built MUI grouped labels, but the moment I try to add the element into the state, the application crashes. Here's the minimal code.
const options = labels.map(option => {
return {
type: JSON.parse(localStorage.getItem("recentTags") as string).includes(option) ? "RECENT" : "ALL ",
labelText: option
};
});
<Autocomplete
multiple
disableClearable
filterSelectedOptions
groupBy={option => option.type}
isOptionEqualToValue={(option, value) => {
const labelText = value.labelText ? value.labelText : value;
return option.labelText.toUpperCase() === labelText.toUpperCase();
}}
value={value}
open={open}
onOpen={() => {
setOpen(true);
}}
onClose={() => {
setOpen(false);
}}
onChange={(_event, newValue) => {
console.log(newValue.map(el => el));
const latestLabel: any = newValue.slice(-1).pop();
const prevLabels = JSON.parse(localStorage.getItem("recentTags") as string);
if (!prevLabels.includes(latestLabel.labelText) && prevLabels.length < 5) {
localStorage.setItem("recentTags", JSON.stringify([...prevLabels, latestLabel.labelText]));
}
const newLabel = newValue.filter((x) => !labels.includes(x))[0];
setValue(newValue);
onSaveEcardLabels!(newValue, ecardItem.id);
if (!!newLabel) {
labels.push(newLabel.labelText);
}
}}
/>
I am storing recently used tags in localstorage, but I am confused in it's logic as well, I am not able to replace any newly recently used tag in local storage. I believe the problem lies in onChange event.
While onChange in newValue I get previous index and current Object, I would like only single array with index i.e. labelText.
I'm having trouble with creating multiple tabs form in React. Example image:
Every new tab mounts a new component. Example code:
const handleAddTab = tabIndex => {
const exampleTab = {
name: `${Date.now()} / 16.02.2022 г.`,
jsx: <Document />,
deletable: true
}
const updatedTabs = state.tabs.map((t, i) => {
if (tabIndex === i) t.subtabs.push(exampleTab)
return t
})
setState(prev => ({
...prev,
tabs: updatedTabs
}))
}
And I'm rendering those components. Example code:
{state.activeSubtabIndex === 0 ?
<Documents />
:
getScreen().subtabs.map((s, i) =>
i === 0 ?
<>
</>
:
<div
style={state.activeSubtabIndex != i ? { display: 'none' } : {}}
>
{s.jsx}
</div>
)
}
I use getScreen() to fetch the current tab and get the subtabs. Example code:
const getScreen = () => {
const typeId = query.get('type_id')
const screen = state.tabs.find(t => t.typeId == typeId)
return screen
}
The way I remove a tab is like so:
const handleCloseTab = (tabIndex, subtabIndex) => {
const updatedTabs = state.tabs.filter((t, i) => {
if (tabIndex === i) {
t.subtabs = t.subtabs.filter((ts, i) => {
return subtabIndex != i
})
}
return t
})
setState(prev => ({
...prev,
tabs: updatedTabs
}))
}
The problem is that every time I delete (for example) the first tab, the second one gets the state from the first one (based on the index it was mapped).
I solved that problem by adding an extra key-value pair (deleted: false) to the exampleTab object
const handleAddTab = tabIndex => {
const exampleTab = {
name: `${Date.now()} / 16.02.2022 г.`,
jsx: <Document />,
deletable: true,
deleted: false <====
}
const updatedTabs = state.tabs.map((t, i) => {
if (tabIndex === i) t.subtabs.push(exampleTab)
return t
})
setState(prev => ({
...prev,
tabs: updatedTabs
}))
}
Whenever I close a tab I'm not unmounting It and removing its data from the array. I'm simply checking if deleted === true and applying style={{ display: 'none' }} to both the tab and component. Example code:
{state.activeSubtabIndex === 0 ?
<Documents />
:
getScreen().subtabs.map((s, i) =>
i === 0 ?
<>
</>
:
<div
style={state.activeSubtabIndex != i || s.deleted ? { display: 'none' } : {}}
key={s.typeId}
>
{s.jsx}
</div>
)}
i have two buttons, but thay are not move slider. maybee anyone have an example?
<button className="carousel__back-button" onClick={() => flicking.current.prev()}> </button>
<button className="carousel__next-button" onClick={() => flicking.current.next()}> </button>
----------initial flicking options----------------------------------------------------------------------
let flicking: React.RefObject<Flicking> =
React.createRef<Flicking>()
let oprionsFlicking = {
className: "flicking",
ref: flicking,
hanger: "0",
anchor: "0",
bound: true,
gap: 5
}
-----------use here-----------------------------------------------------------------------------------------
<StyledDateWrapper>
<Flicking ref={flicking} {...oprionsFlicking} style={{height: '51px', width: '100%'}}>
{ currentMonthDates.length > 0 && Object.entries(currentMonthDates
.reduce((a, i) => {
const normolizeDate = i * 1000;
const dayNumber = new Date(normolizeDate).getDate()
const dayWeek = new Date(normolizeDate).getDay()
return {...a, [dayNumber]: { dayWeek, checked: false }}
}, {}))
.map((el: any) => el[0] == checkedDay ? ([...el, el[1].checked = true]) : el)
.map(([date, day]: any) => {
const monthIdx = new Date(currentMonth.start ? currentMonth.start * 1000 : new Date()).getMonth();
const month = monthesMapper[monthIdx]
return <StyledDateTabWrapper key={date} className="panel">
<DateTab
onClick={() => takeDay(date)}
selected={day.checked}
date={`${date} ${month}`}
day={`${daysInWeek[day.dayWeek - 1] ? daysInWeek[day.dayWeek - 1] : 'Вс'}`}
holiday={daysInWeek[day.dayWeek - 1] ? false : true}
theme={PrimaryThemeConfig}/>
</StyledDateTabWrapper>
}) }
</Flicking>
{ renderArrows() }
</StyledDateWrapper>
p.s. react functional component
I have a screen that represents a schedule which lets the user give each week a name. Once the user finishes editing all the weeks he will presses the checkmark and the app should update the backend.
I am getting a very weird bug where the first time I click the checkmark edit data gets updated (the log says "editData changed" and the ui changes) but when I print the state inside updateTheDB it has not been updated. If I try to enter edit mode again and save without making any new changes the previous changes are updated inside updateTheDB.
I thought this was a copy by reference not but value problem but I am using JSON.parse(JSON.stringify to copy so that can't be it.
The call to setState (setEditData) is inside onSavePressed which is in a modal that opens when the user tries to name a week.
Does anyone know what could have caused this?
EDIT
I want the to cause a render when setEditToServer is called
This is my code:
const Schedule = (props: IPorps) => {
const { week_names_props, navigation } = props;
const days = ['s', 'm', 't', 'w', 't', 's', 'w'];
//bottom Modal
const [active_modal, setActiveModal] = useState<MProps>(null)
//data
const [serverData, setServerData] = useState<IData>({ weekNames: week_names_props, weekEvents: {} })
//edit_mode data
const [editData, setEditData] = useState<IData>(null)
//other
const [isLoading, setIsLoading] = useState<boolean>(false)
const [editable, setEditable] = useState<boolean>(false)
const doTheLoadingThingy = (): void => {
Axios.get(`api/weeks/getWeekNameByCompanyId`, {
params: {
company_id: 1,
},
}).then((response) => {
setServerData({ ...serverData, weekNames: response.data })
//useEffect will hide the loading and the modal
})
.catch(() => { setIsLoading(false); errorToast("get") })
}
const setEditToServer = () => {
console.log("setEditToServer"
setEditData({
weekNames: JSON.parse(JSON.stringify(serverData.weekNames)),
weekEvents: {}
})
}}
useEffect(() => {
setEditToServer()
setIsLoading(false)
}, [serverData])
useEffect(() => {
console.log("editData changed")
}, [editData])
const updateTheDB = () => {
setIsLoading(true)
console.log(editData)
//send to backend
}
useEffect(() => {
navigation.setOptions({
headerLeft: () => (
<View style={{ flexDirection: "row", justifyContent: "space-around", alignItems: "center", paddingHorizontal: 30 }}>
{editable ? (
<>
<Icon
name={"check"}
onPress={() => {
updateTheDB()
setEditable(false)
}}/>
<Icon
name={"cancel"}
onPress={() => {
console.log("cancel")
setEditToServer()
setEditable(false)
}}/>
</>
) : (
<Icon
name={'edit'}
onPress={() => setEditable(true)}/>
)}
</View>
)
})
}, [editable])
return (
<>
{isLoading ?
(<ActivityIndicator size="large" />) :
(
<>
<BottomModal
innerComponent={active_modal ? active_modal.modal : null}
innerComponentProps={active_modal ? active_modal.props : null}
modalStyle={active_modal ? active_modal.style : null}
modalVisible={(active_modal != null)}
onClose={() => {
setActiveModal(null);
}}
/>
<WeekDaysHeader />
{editData ?
(<FlatList
data={editData.weekNames}
keyExtractor={(item) => item.week.toString()}
style={styles.flatListStyle}
// extraData={editData?editData.weekEvents:null}
renderItem={({ item }) => {
return (
<Week
days={days}
events={editData.weekEvents[item.week.toString()]}
dayComponents={ScheduleScreenDay}
week_name={item.week_name ? item.week_name : null}
week_number={Number(item.week)}
onHeaderPressed={editable ?
(week_number, week_title) => {
console.log("Pressed", week_number, week_title)
setActiveModal({
props: {
week_number_props: week_number,
week_title_props: week_title,
onSavePressedProps: (new_name) => {
if (new_name) {
let tmp = JSON.parse(JSON.stringify(editData.weekNames))
const i = tmp.findIndex((item) => item.week.toString() === week_number.toString())
tmp[i].week_name = new_name
setEditData((prev) => ({ weekNames: tmp, weekEvents: prev.weekEvents }))
}
}
}, modal: NameWeekModalComponet,
style: styles.weekNameModalStyle
});
} : undefined}
/>
);
}}
/>) : null}
</>
)
}
</>)
};
export default Schedule;
I thought this was a copy by reference not but value problem but I am using JSON.parse(JSON.stringify) to copy so that can't be it.
Thats exactly the problem:
// Always true
JSON.parse(JSON.stringify(['a'])) !== JSON.parse(JSON.stringify(['a']))
Therefore whenever setEditToServer is called the component will re-render, that's because React makes a shallow comparison with the previous state when deciding for render.
const setEditToServer = () => {
// Always re-render
setEditData({
weekNames: JSON.parse(JSON.stringify(serverData.weekNames)),
weekEvents: {}
})
}}
The problem was in this useEffect:
useEffect(() => {
navigation.setOptions({
headerLeft: () => (
//...
)
})
}, [editable])
The functions inside it used the editData state but the useEffect didn't have it in the deps list so when the onPress was clicked it got the old state of editData. The solution was to add editData to the deps list. Like this:
useEffect(() => {
navigation.setOptions({
headerLeft: () => (
//...
)
})
}, [editable, editData])