Inversing slider value without affecting drag functionality MUI Slider React - javascript

I have a MUI slider where the value of the slider needs to be inverted based on a boolean variable. My current code inverts it correctly but it also inverts the drag functionality, meaning if I tried to drag my slider towards 0, it would move towards 100 instead. I can't think of a workaround to correct this. Would appreciate if anyone has any suggestions I could try.
const handleSelectionChange = (e: any) => {
if (typeof e.target.value === 'string') {
onChange(e.target.value)
} else {
onChange(Number(e.target.value))
}
}
<Slider
aria-label='DiceRoll'
defaultValue={50}
valueLabelDisplay='auto'
step={1}
min={0}
max={100}
value={overUnder ? 100 - targetNumber : targetNumber} //overUnder is boolean and targetNumber ranges from 0 to 100.
marks={marks}
onChange={handleSelectionChange}
/>

Using the valueLabelFormat function, you can simply format the number display instead of the number value itself, preventing the problem.
Use this instead:
valueLabelFormat={(v) => (overUnder ? 100 - v : v)}
Set the value back to normal:
value={targetNumber}
const { StrictMode, useState } = React;
const { createRoot } = ReactDOM;
const App = () => {
const [overUnder, setOverUnder] = useState(false);
const [targetNumber, setTargetNumber] = useState(42.0);
const handleSelectionChange = (e) => {
if (typeof e.target.value === "string") {
setTargetNumber(e.target.value);
} else {
setTargetNumber(Number(e.target.value));
}
};
const onFlip = () => {
setOverUnder((s) => !s);
};
return (
<div>
<div>
<button onClick={onFlip}>Flip</button>
</div>
<div>Target Number: {targetNumber}</div>
<div>Over Under: {`${overUnder}`}</div>
<MaterialUI.Slider
aria-label="DiceRoll"
defaultValue={50}
valueLabelDisplay="on"
step={1}
min={0}
max={100}
value={targetNumber}
valueLabelFormat={(v) => (overUnder ? 100 - v : v)}
marks={false}
inverted={true}
onChange={handleSelectionChange}
sx={{ pt: "10rem" }}
/>
</div>
);
}
const rootElement = document.getElementById("root");
const root = createRoot(rootElement);
root.render(
<StrictMode>
<App />
</StrictMode>
);
<div id="root"></div>
<script crossorigin src="https://unpkg.com/react#18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/#mui/material#latest/umd/material-ui.production.min.js"></script>
Also a sandbox for reference.

So after a few hours I've found the solution, it's actually a pretty simple solution, just an additional 3 lines of codes. We just have to introduce another state which only purpose is to display the correct slider Value.
const handleSelectionChange = (e: any) => {
if(overUnder){ //defaultValue of overUnder is true
if (typeof e.target.value === 'string') {
onChange(e.target.value)
} else {
onChange(Number(e.target.value))
}
} else {
if (typeof e.target.value === 'string') {
onChange(100 - e.target.value)
setFlippedTargetNumber(e.target.value)
} else {
onChange(100 - Number(e.target.value))
setFlippedTargetNumber(Number(e.target.value))
}
}
}
const [ flippedTargetNumber, setFlippedTargetNumber] = useState<number>(50)
<Slider
aria-label='DiceRoll'
defaultValue={50}
valueLabelDisplay='auto'
step={1}
min={0}
max={100}
value={overUnder ? targetNumber : flippedTargetNumber} //overUnder is boolean and targetNumber ranges from 0 to 100.
marks={marks}
onChange={handleSelectionChange}
/>
basically what this does is, when overUnder = true, and when it's true, the slider would just work normally. when it is flipped and overUnder = false, onChange will update targetNumber to inverse [which is needed for other components]. and setFlippedTargetNumber will be used as slider Value instead of targetNumber.
Hope this helps anyone who is facing a similar problem. :)

Related

.push() is improperly pushing twice on Next.js

The .push() in insertToCart() function in Cart component is pushing twice, no idea why. Since all the logs run once, I think it should just work, the appendToCart() works perfectly. I'm returning the previous value on the functions on purpose, to not trigger a re-render of the whole app. Any help would be much appreciated.
Edit : saveItem() is being called twice.
export default function Cart({ children }) {
const [cart, setCart] = useState([]);
useEffect(() => {
setCart(JSON.parse(localStorage.getItem('frentesRosarinos')) || []);
}, []);
function insertToCart(product, amount) {
console.log('insert');
setCart((pc) => {
pc.push({ ...product, amount });
localStorage.setItem('frentesRosarinos', JSON.stringify(pc));
return pc;
});
}
function appendToCart(id, amount) {
console.log('append');
setCart((pc) => {
const savedItem = pc.find((c) => c.id === id);
savedItem.amount = amount;
localStorage.setItem('frentesRosarinos', JSON.stringify(pc));
return pc;
});
}
return (
<CartContext.Provider
value={{ cart, insertToCart, appendToCart }}
>
{children}
</CartContext.Provider>
);
}
export default function Product({ product, isCart }) {
const { cart, appendToCart, insertToCart } =
useContext(CartContext);
const [addItemClosed, setAddItemClosed] = useState(true);
const [amount, setAmount] = useState(
product.amount || cart.find(({ id }) => id === product.id)?.amount || 0,
);
const amountToAdd = useRef(0);
function saveItem(value) {
setAmount((pa) => {
const newAmount = parseInt(value) + pa;
pa === 0 ? insertToCart(product, newAmount) : appendToCart(product.id, newAmount);
return newAmount;
});
amountToAdd.current.value = 0;
}
return (
<div className={styles.container}>
<div className={styles.addItemsContainer}>
<p
onClick={() => setAddItemClosed((ps) => !ps)}
className={`${styles.addItemsButton} ${addItemClosed ? styles.addBorders : ''}`}
>
Agregar más items
</p>
<div
className={`${styles.quantity} ${
addItemClosed ? styles.addBorders : styles.firstChildHide
}`}
>
<span>Ingrese una cantidad</span>
<input ref={amountToAdd} type='number' />
</div>
<p
className={`${styles.addOne} ${
addItemClosed ? styles.addBorders : styles.secondChildHide
}`}
onClick={() => saveItem(amountToAdd.current.value)}
>
Agregar
</p>
</div>
<div style={{ margin: '10px 0' }}>
<PressButton action={() => saveItem(1)} size='medium' lightMode>
{isCart ? 'Agregar otro' : 'Añadir al carrito'}
</PressButton>
</div>
</div>
);
}
I'm returning the previous value on the functions on purpose, to not
trigger a re-render of the whole app. Any help would be much
appreciated
You should not do this, official react docs say you should update state in immutable way, and you should follow this advice.
Also here:
pc.push({ ...product, amount });
again you are doing mutation which is not OK.
PS. Also in general you should be careful when calling functions inside functional set state in case they perform side effects, because in strict mode the setter function will be invoked twice. Calling a set state within another set state (as you call setCart inside setAmount) could be considered a side effect.
You shouldn't be calling inside a state setter a function that will call this same setter. Change saveItem to this:
function saveItem(value) {
amount === 0 ? insertToCart(product, newAmount) : appendToCart(product.id, newAmount);
setAmount((pa) => {
const newAmount = parseInt(value) + pa;
return newAmount;
});
amountToAdd.current.value = 0;
}
Change insertToCart, so you don't mutate the array (this line pc.push({ ...product, amount }); is a state mutation). Use destructing instead, like so:
function insertToCart(product, amount) {
console.log('insert');
setCart((pc) => {
const newCart = [...pc, { ...product, amount }]
localStorage.setItem('frentesRosarinos', JSON.stringify(newCart));
return newCart;
});
}

Combine react-easy-crop with react-uploady

I am trying to combine react-easy-crop.js with react-uploady.js but do not succeed. There is an example in which react-uploady is combined with react-image-crop which I am trying to adapt using react-easy-cropper. After selecting a picture to be shown in the cropper and then pressing 'UPLOAD CROPPED' I run into an error:
TypeError
Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The provided value is not of type '(CSSImageValue or HTMLCanvasElement or HTMLImageElement or HTMLVideoElement or ImageBitmap or OffscreenCanvas or SVGImageElement or VideoFrame)'.
and do not know how to proceed.
The codesandbox showing this behavior is here.
How can I avoid this error? How should react-easy-crop be implemented with react-uploady?
I'm not sure what the issue is with the original sandbox or with the adaptation to react-easy-crop but I was able to easily adapt it to the desired library (despite not liking its UI very much, but to each his own, I guess)
In any case, here's a working sandbox with react-easy-crop: https://codesandbox.io/s/react-uploady-crop-and-upload-with-react-easy-crop-5g7vw
Including here the preview item that I updated:
const ItemPreviewWithCrop = withRequestPreSendUpdate((props) => {
const {
id,
url,
isFallback,
type,
updateRequest,
requestData,
previewMethods
} = props;
const [uploadState, setUploadState] = useState(UPLOAD_STATES.NONE);
const [croppedImg, setCroppedImg] = useState(null);
//data for react-easy-crop
const [crop, setCrop] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
const onCropComplete = useCallback((croppedArea, croppedAreaPixels) =>
setCroppedAreaPixels(croppedAreaPixels),
[]);
const isFinished = uploadState === UPLOAD_STATES.FINISHED;
useItemProgressListener(() => setUploadState(UPLOAD_STATES.UPLOADING), id);
useItemFinalizeListener(() => setUploadState(UPLOAD_STATES.FINISHED), id);
const onUploadCrop = useCallback(async () => {
if (updateRequest && croppedAreaPixels) {
const [croppedBlob, croppedUri] = await getCroppedImg(
url,
croppedAreaPixels
);
requestData.items[0].file = croppedBlob;
updateRequest({ items: requestData.items });
setCroppedImg(croppedUri);
}
}, [url, requestData, updateRequest, croppedAreaPixels]);
const onUploadCancel = useCallback(() => {
updateRequest(false);
if (previewMethods.current?.clear) {
previewMethods.current.clear();
}
}, [updateRequest, previewMethods]);
return isFallback || type !== PREVIEW_TYPES.IMAGE ? (
<PreviewImage src={url} alt="fallback img" />
) : (
<>
{requestData && uploadState === UPLOAD_STATES.NONE ? (
<div className="crop-view">
<div className="crop-container">
<Cropper
image={url}
crop={crop}
zoom={zoom}
aspect={4 / 3}
onCropChange={setCrop}
onCropComplete={onCropComplete}
onZoomChange={setZoom}
/>
</div>
<div className="controls">
<input
type="range"
value={zoom}
min={1}
max={3}
step={0.1}
aria-labelledby="Zoom"
onChange={(e) => {
setZoom(e.target.value);
}}
className="zoom-range"
/>
</div>
</div>
) : (
<PreviewImage src={croppedImg || url} alt="img to upload" />
)}
<PreviewButtons
finished={isFinished}
crop={crop}
updateRequest={updateRequest}
onUploadCancel={onUploadCancel}
onUploadCrop={onUploadCrop}
/>
<p>{isFinished ? "FINISHED" : ""}</p>
</>
);
});
The change was pretty seamless, just a matter of using the different cropper, and wrapping it with a bit different CSS due to the way it looks&behaves. I took the image cropping code from their example over at: https://codesandbox.io/s/q8q1mnr01w?file=/src/cropImage.js

how to set focus to next input on enter key press in react js with refs

Iam using multiple inputs inside maps i want to set focus to next input when i click enter in react Hooks.
With the help of refs
Iam using material ui text field for getting input
I tried in react class component wihtout ref it works with error but in hooks it not works
class compomnent code:
constructor(props) {
this.state = {}
}
inputRefs = [];
_handleKeyPress = e => {
const {currentTarget} = e;
let inputindex = this.inputRefs.indexOf(currentTarget)
if (inputindex < this.inputRefs.length - 1) {
this.inputRefs[inputindex + 1].focus()
}
else {
this.inputRefs[0].focus()
}
};
Inside render in added this within map function
this.state.data.map((data) => return (
<TextField
inputProps = {{onKeyPress:(e) => this.function1(e, data)}}
onChange={this.changevaluefunction}
inputRef={ref => this.inputRefs.push(ref)}
onFocus={this.handleFocus} ref={`input${id}`} /> ))
I have implemented the solution in a different way with the functional component. I have taken the 4 fields and seated its ref with the createRef hook.
I can see from your solution, you wanted to move focus to the next input element whenever you press Enter key on the current element.
I am passing the next target element argument in the onKeyUp handler along with the actual event and then detecting whether the Enter key is pressed or not. If Enter key is pressed and the targetElem is present then I am moving focus to the passed targetElem. By this way you have better control over the inputs.
You can see my solution here
https://codesandbox.io/s/friendly-leftpad-2nx91?file=/src/App.js
import React, { useRef } from "react";
import TextField from "#material-ui/core/TextField";
import "./styles.css";
const inputs = [
{
id: "fName",
label: "First Name"
},
{
id: "lName",
label: "Last Name"
},
{
id: "gender",
label: "Gender"
},
{
id: "address",
label: "Address"
}
];
export default function App() {
const myRefs = useRef([]);
const handleKeyUp = (e, targetElem) => {
if (e.key === "Enter" && targetElem) {
targetElem.focus();
}
};
return (
<div>
{inputs.map((ipt, i) => (
<TextField
onKeyUp={(e) =>
handleKeyUp(e, myRefs.current[i === inputs.length - 1 ? 0 : i + 1])
}
inputRef={(el) => (myRefs.current[i] = el)}
id={ipt.id}
fullWidth
style={{ marginBottom: 20 }}
label={ipt.label}
variant="outlined"
key={ipt.id}
/>
))}
</div>
);
}
You can convert this.inputRefs into a React ref so it persists through renders, and other than this you pretty much remove all references to any this object.
Example Component:
const LENGTH = 10;
const clamp = (min, max, val) => Math.max(min, Math.min(val, max));
export default function App() {
const [data] = useState([...Array(LENGTH).keys()]);
const inputRefs = useRef([]); // <-- ref to hold input refs
const handleKeyPress = index => () => { // <-- enclose in scope
const nextIndex = clamp(0, data.length - 1, index + 1); // <-- get next index
inputRefs.current[nextIndex].focus(); // <-- get ref and focus
};
return (
<div className="App">
{data.map((data, index) => (
<div key={index}>
<TextField
inputProps={{ onKeyPress: handleKeyPress(index) }} // <-- pass index
inputRef={(ref) => (inputRefs.current[index] = ref)} // <-- save input ref
/>
</div>
))}
</div>
);
}
If you are mapping the input field and want to focus on click, you can directly give the id attribute to the input and pass the array id.
After that, you can pass id inside a function as a parameter, and get it by document.getElementById(id).focus().

How to remove text from previous element if condition is not satisfied - reactjs

I'm making small app in react js which basically would display SELECTED if item is selected.
Here is my code:
import React, { useState } from 'react';
function SelectedFiles(props) {
const [selectedFile, setSelectedFile] = useState(0);
const selectSelectedFileOnChange = id => {
setSelectedFile(id);
props.onSetSelectedFile(id);
};
return (
<MainContainer>
<RadioButton
key={props.id}
value={props.id}
name="Acfile"
onChange={e => {
selectSelectedFileOnChange(props.id);
}}
disabled={false}
></RadioButton>
<span>{props.file.name}</span>
<span>{props.file.size}</span>
<span>{props.file.isPrimary === true ? 'SELECTED' : null}</span>
</MainContainer>
);
}
export default SelectedFiles;
This component is part of parent component and purpose of this component is just to display an items:
<AddF className="modal-body">
{docs && docs.length > 0
? docs.map(file => (
<SelectedFiles
key={file.id}
id={file.id}
file={file}
onSetSelectedFile={handleSetPrimaryFile}
/>
))
: null}
</AddF>
const handleSetPrimaryFile = id => {
props.onSetPrimaryFile(id);
};
As its possible to see guys I dont know how to remove text from NOT SELECTED element..
Thanks guys ! Cheers
Could you please try below and see if it works?
const [files, setFiles] = useState({docs[0].id: true});
<AddF className="modal-body">
{docs && docs.length > 0
? docs.map(file => (
<SelectedFiles
key={file.id}
id={file.id}
file={file}
isPrimary={!!files[file.id]}
onSetSelectedFile={handleSetPrimaryFile}
/>
))
: null}
</AddF>
const handleSetPrimaryFile = id => {
setFiles({[id]: true});
props.onSetPrimaryFile(id);
};
Change SelectedFiles.js as
<span>{props.isPrimary === true ? 'SELECTED' : null}</span>
Hope it helps.
This snippet {props.file.isPrimary === true ? 'SELECTED' : null} is determining when SELECTED should appear. But I don't see where props.file would ever change.
I also see you using both the useState hook and some sort of prop function passed in to handle selection.
The solution is to have some sort of unique identifier for the files (perhaps that's file.id), then check this value on the selectedFile to determine if SELECTED should appear, e.g., props.file.id === selectedFile.

React Material UI - Make Slider update state

I am building a multi-step form (survey) with React.js and Material-UI components library.
At the step with a slider, the component doesn't update the state. I was trying to set value with setValue from React.useState() and other various methods. Didn't work out. Does anybody know what the problem is? I'm stuck.
Here is the link to Codesandox: project's code
Code of the Slider component:
import React from 'react';
import { Slider, Input } from '#material-ui/core/';
export default function DiscreteSlider({ values, handleChange }) {
const [value, setValue] = React.useState(values.areaSpace);
const handleSliderChange = (event, newValue) => {
setValue(newValue);
};
const handleInputChange = event => {
setValue(event.target.value === '' ? '' : Number(event.target.value));
};
const handleBlur = () => {
if (value < 0) {
setValue(0);
} else if (value > 100) {
setValue(100);
}
};
return (
<div onChange={handleChange('areaSpace')} style={{marginTop: '20px', marginBottom: '20px'}}>
<Input
value={value}
margin="dense"
onChange={handleInputChange}
onBlur={handleBlur}
inputProps={{
step: 1,
min: 0,
max: 800,
type: 'number',
'aria-labelledby': 'input-slider',
}}
/>
<Slider
style={{marginTop: '20px'}}
value={typeof value === 'number' ? value : 0}
onChange={handleSliderChange}
aria-labelledby="input-slider"
step={1}
min={0}
max={800}
onChangeCommitted={handleChange}
/>
</div>
);
}
On your Slider you have the following:
onChangeCommitted={handleChange}
The handleChange above is being passed from MainForm.js which defines it as:
// Handle inputs change
handleChange = input => event => {
this.setState({ [input]: event.target.value });
}
When that function gets called, all it is going to do is return another function. You need to call handleChange("areaSpace") in order to get a function that will then try to set the "areaSpace" state when it is called. Another problem is that the change function is getting the value from event.target.value, but for the Slider the value is passed as a second parameter.
The following code addresses both of these issues:
onChangeCommitted={(event, value) =>
handleChange("areaSpace")({ target: { value } })
}
There are more elegant ways of dealing with this, but the above fixes the problem without changing any other layers. It does still leave another problem which is that if you change the input instead of the Slider, the areaSpace state won't be updated, but I'll leave that as a problem for you to work through.

Categories