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
Related
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. :)
I have a list of users and I want to display in another component on the same page the user data in input fields for every user that I click on.
When no user is selected, I want the component to just render some text and a button to add a user. When the button is clicked the component renders the form with empty input fields so that we can add a new user.
I tried the following, but it's just showing the data for the first one I click on. It's not updating.
The main page:
const index = (props) => {
const [selectedUser, setSelectedUser] = useState(null);
const [state, setState] = useState("Index");
const onChange = (item) => {
setState("Edit");
setSelectedUser(item);
};
const onClick = (e, item) => {
if (e.type === "click" && e.clientX !== 0 && e.clientY !== 0) {
onChange(item);
} else {
console.log('prevented "onClick" on keypress');
}
};
const renderComponent = () => {
switch (state) {
case "Index":
return (
<>
<div className="btn" onClick={(e) => setState("Edit")}>
+ New Staff
</div>
<img src="/storage/illustrations/collaboration.svg" />
</>
);
case "Edit":
return (
<div>
<StaffForm profile={selectedUser} />
</div>
);
}
};
return (
<>
<div>
<div>
<h1>Staff</h1>
</div>
<div>
<div>
{profiles.map((item, index) => {
return (
<div key={index} onClick={(e) => onClick(e, item)}>
<input
type={"radio"}
name={"staff"}
checked={state === item}
onChange={(e) => onChange(item)}
/>
<span>{item.user.name}</span>
</div>
);
})}
</div>
<div>{renderComponent()}</div>
</div>
</div>
</>
);
};
The Staff Form Component:
const StaffForm = ({ profile }) => {
const { data, setData, post, processing, errors, reset } = useForm({
email: profile ? profile.user.email : "",
name: profile ? profile.user.name : "",
phone_number: profile ? profile.user.phone_number : "",
avatar: profile ? profile.user.avatar : "",
});
const [file, setFile] = useState(data.avatar);
const handleImageUpload = (e) => {
setFile(URL.createObjectURL(e.target.files[0]));
setData("avatar", e.target.files[0]);
};
const onHandleChange = (event) => {
setData(
event.target.name,
event.target.type === "checkbox"
? event.target.checked
: event.target.value
);
};
return (
<div>
<ImageUpload
name={data.name}
file={file}
handleImageUpload={handleImageUpload}
/>
<TextInput
type="text"
name="name"
value={data.name}
autoComplete="name"
isFocused={true}
onChange={onHandleChange}
placeholder={t("Name")}
required
/>
<TextInput
type="text"
name="phone_number"
value={data.phone_number}
autoComplete="phone_number"
placeholder={t("Phone Number")}
onChange={onHandleChange}
required
/>
<TextInput
type="email"
name="email"
value={data.email}
autoComplete="email"
onChange={onHandleChange}
placeholder={t("Email")}
required
/>
</div>
);
};
First of all something you should avoid is the renderComponent() call.Check here the first mistake mentioned in this video. This will most likely fix your problem but even if it doesn't the video explains why it should not be used.
Something else that caught my eye(possibly unrelated to your question but good to know) is the onChange function. When two pieces of state change together it is a potential source of problems, check out this article on when to use the useReducer hook.
Also be careful with naming React Components, they need to be capital case, this question contains appropriate answers explaining it.
(To only solve your problem stick to no.1 of this list, there are some improvements i'd do here overall for code quality and beauty, msg me for more details)
Im Having a Table which has multiple records and Filter component with the Search Bar. What im trying to do is Based on the value selected by the user from all the filters i have pass those arrays to parent and form an object,
Im having 3 components here,
1)Parent : Data
export default function Data(props) {
const [domain, setDomain] = useState([]);
const [fileType, setFileType] = useState([]);
const [entity, setEntity] = useState(["Patents"]);
const [year, setYear] = useState({});
//This is the search bar state
const [keywords, setKeywords] = useState([]);
//based on the filter values im calling the API to get the records for table based on the value selected by the user from my filer
useEffect(() => {
const fetchResults = async (projectid) => {
const url = props.apiURL.rpaapiurl + "/search";
console.log("fetchData called-->" + url);
const resultsObj = {
projectId: projectid,
filter: {
domain: domain,
fileType: fileType,
entity: entity,
},
};
const response = await fetch(url, {
method: "POST",
body: JSON.stringify(resultsObj),
headers: {
"Content-Type": "application/json",
},
});
const data = await response.json();
console.log("All data-->", data);
setResults(data);
};
fetchResults(5);
}, [domain, fileType, entity]);
const handleFileType = (fileTypeArray) => {
setFileType(fileTypeArray);
};
return (
<Item1>
<Dropdown onChangeFileType={(FileTypeFilteredArray) => handleFileType(FileTypeFilteredArray)} ></Dropdown>
</Item1>
<Item2>
<Table
Data={dataresults}
Attributes={resultTable}
entitytypeHandler={props.entitytypeHandler}
></Table>
</Item2>
)
From the data parent component im passing the hadler which will return updated array from the child and im setting it to state.
2)Child : Dropdown
export default function Dropdown(props) {
return (
<FilterItem>
<Input
type="search"
placeholder="Search in title, description, keywords"
></Input>
<Filter1></Filter1>
<Filetr2></Filetr2>
<ContentFormat
onChangeFileType={props.onChangeFileType}
></ContentFormat>
<Filter4></Filter4>
<Filter5></Filter5>
<TextWrap>
<P text="End year" fontSize="14px" color="#454545"></P>
<KeywordImg src={droparrow} />
</TextWrap>
</FilterItem>
)}
Nothing special here since we can not skip a component passing the same thing to nested child,
Nested Child : ContentFormat
export default function ContentFormat(props) {
const [isDisplay, setIsDisplay] = useState("false");
const array = ["HTML", "PDF"];
const toggle = () => {
setIsDisplay(!isDisplay);
};
let fileTypeArray = [];
const handleSelection = (event) => {
const value = event.target.value;
console.log("value-->", +value);
if (event.target.checked == true) {
fileTypeArray.push(value);
console.log("if fileTypeArray-->", fileTypeArray);
} else if (fileTypeArray.length > 0) {
fileTypeArray = fileTypeArray.filter((element) => {
console.log("element-->", +element);
if (event.target.value !== element) return element;
});
console.log("else fileTypeArray-->", fileTypeArray);
}
console.log("function fileTypeArray-->", fileTypeArray);
};
const applyClickHandler = () => {
console.log("Applied fileTypeArray-->", fileTypeArray);
props.onChangeFileType(fileTypeArray);
};
return (
<div>
<DropContent>
<DropButton onClick={toggle}>
{" "}
<P text="By Content Format" fontSize="14px" color="#454545"></P>
<KeywordImg src={droparrow} />
</DropButton>
<ContextWrapper style={{ display: isDisplay ? "none" : "block" }}>
<P
text="Filter by Extension types"
fontSize="18px"
color="#ACACAC"
textAlign="center"
padding="22px 32px 14px"
></P>
<DropScroll className="sl-style-3">
{array.map((item, index) => {
return (
<ContextItem key={index}>
<DropList
onHandleSelection={handleSelection}
text={item}
value={item}
></DropList>
</ContextItem>
);
})}
</DropScroll>
<ApplyButton onClick={applyClickHandler}>
<P text="Apply" fontSize="16px" color="#fff" textAlign="center"></P>
</ApplyButton>
</ContextWrapper>
</DropContent>
</div>
);
}
4)DropList
export default function DropList(props) {
const changeHandler = (e) => {
console.log(e);
props.onHandleSelection(e);
};
return (
<div>
<div className="">
<TickBox
type="checkbox"
id={props.id}
name={props.name}
value={props.value}
onChange={(e) => {
changeHandler(e);
}}
/>
{props.text}
</div>
</div>
);
}
I'm getting the updated array on click of apply button in the parent but if user un-selects any check box the it deleting the complete array
In data i have to form the object base on the state array passed by all the filters, i tried for the one filter as above but its not working can any one suggest better way to do it,
Because here handling one filter is default and i have to do it for total 5 filters
So any suggestion or one common component for all the filters
Im not sure whether i should be asking these kinda questions or not since I'm very at posting the right questios but pardon me if its wrong question or the way of asking is wrong,
Any help would be appricited.
I'm developing weather app in react. It's learning project. I'm getting error.
There are 3 components I use.
App.js:
<WeatherApiAddressProvider>
<WeatherCity />
</WeatherApiAddressProvider>
WeatherApiAddressProvider(Context API):
const setApiAddress = (city) => {
const address = `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=7b7503a2aa9ca872738c7213619a72f5`;
return address;
};
export const WeatherApiAddressProvider = ({ children }) => {
return (
<WeatherContext.Provider value={setApiAddress}>
{children}
</WeatherContext.Provider>
);
};
WeatherCity.js:
const setApiAddress = useContext(WeatherApiAddressContext);
const [city, setCity] = useState("");
const handleChange = (e) => {
setCity(e.target.value);
};
return (
<div>
<div className="container">
<div className="row">
<div className="col">
<select
className="form-select mb-3"
aria-label="Default select example"
value={city}
onChange={handleChange}
>
<option className="placeholder" value="">
Please select a city
</option>
<option value="adana">Adana</option>
...
</select>
</div>
<div className="col">
<div className="p-5">
<WeatherDetail setApiAddress={setApiAddress} city={city} />
</div>
</div>
</div>
</div>
</div>
);
WeatherDetail.js:
const [weatherInfo, setWeatherInfo] = useState({});
let imgCode = weatherInfo.weather[0].icon;
useEffect(() => {
axios.get(city ? setApiAddress(city) : "").then((response) => {
setWeatherInfo(response.data);
});
}, [city, setApiAddress]);
return (
<div>
{city !== "" && (
<div className="card">
<img
className="card-img-top"
src={`https://openweathermap.org/img/wn/${imgCode}.png`}
/>
<div className="card-body">
<h5 className="card-title">{weatherInfo.name}</h5>
<p className="card-text">
Some quick example text to build on the card title and make up the
bulk of the card's content.
</p>
</div>
</div>
)}
{}
</div>
);
As you know, WeatherApiAddressProvider throw setApiAddres function, WeatherCity throw city variable. WeatherDetail receive these and get data from weatherapi using axios.
The error I get:
TypeError: Cannot read property '0' of undefined(let imgCode = weatherInfo.weather[0].icon)
Solutions I tried:
I logged received data(weatherInfo or response.data) and I saw that this data is object. Then I defined initialState(weatherInfo) as object but it didn't solve the problem.
I created a object like received data in initialState. But it didn't solve problem.
I want to show weather representative image and weather details in WeatherDetail.js. How can I do that?
Change the expression as follows in your WeatherDetails.js
let imgCode = weatherInfo && weatherInfo.weather.length >0 && weatherInfo.weather[0].icon
weatherInfo.weather is undefined at this point:
const [weatherInfo, setWeatherInfo] = useState({});
let imgCode = weatherInfo.weather[0].icon;
I believe you want to set the value of imgCode after you call setWeatherInfo.
Right here
const [weatherInfo, setWeatherInfo] = useState({});
let imgCode = weatherInfo.weather[0].icon;
You are trying to get first element of an array that doesn't exist on empty object weatherInfo you've just created. The simplest solution for this would be to use optional chaining and change weatherInfo.weather[0].icon to weatherInfo?.weather[0]?.icon however it's not ideal because you would have broken link to an image.
You could create another state with
const [imgCode, setImgCode] = useState('');
and set it as you do for setWeatherInfo.
I would also do an optional rendering based on this variable so you won't show broken image.
{ imgCode && <img
className="card-img-top"
src={`https://openweathermap.org/img/wn/${imgCode}.png`}
/>
}
This was causing the error:
useEffect(() => {
axios.get(city ? setApiAddress(city) : "")
.then((response) => {
setWeatherInfo(response.data);
})
}, [city, setApiAddress]);
I refactored code like this:
useEffect(() => {
axios.get(city ? setApiAddress(city) : "")
.then((response) => {
setWeatherInfo(response.data.weather[0]);
})
}, [city, setApiAddress]);
I will use State as needed.
This is a bit of an open ended question, as I'm sure the way I'm going about this is incorrect. But I'm curious why React isn't re-rendering as I would expect. I suspect it has to do with the behavior of the useState hook paired with a functional component.
The code is in this CodeSandbox link, and code noted below:
function App() {
var foosList = ["foo1", "foo2", "foo3"];
const [selectedFoo, setSelectedFoo] = useState(-1);
const isSelected = i => i === selectedFoo;
return (
<div className="App">
<FoosWrapper>
<TitleSpan>Foos</TitleSpan>
<ListGroup>
{foosList.map((fooItem, i) => (
<ListGroupItem
key={fooItem}
active={isSelected(i)}
onClick={() => setSelectedFoo(i)}
>
{fooItem}
</ListGroupItem>
))}
</ListGroup>
</FoosWrapper>
<BarsWrapper>
<TitleSpan>Bars</TitleSpan>
<Bars foo={foosList[selectedFoo]} />
</BarsWrapper>
</div>
);
}
const Bars = props => {
const [pendingBar, setPendingBar] = useState("");
const [newBars, setNewBars] = useState([]);
const keyPress = e => {
if (e.key === "Enter") {
save(pendingBar);
}
};
const save = bar => {
newBars.push(bar);
setNewBars([...newBars]);
};
return (
<div>
<ListGroup>
<ListGroupItem key={props.foo}>{props.foo}</ListGroupItem>
</ListGroup>
<ListGroup>
{newBars.map(newBar => (
<ListGroupItem key={newBar}>{newBar}</ListGroupItem>
))}
</ListGroup>
<InputGroup>
<Input
placeholder="Add a bar"
onChange={e => setPendingBar(e.target.value)}
onKeyPress={keyPress}
/>
</InputGroup>
</div>
);
};
Broadly, there are two logical widgets: Foos and Bars. Foos on the left, Bars, on the right. I'd like to have a user select a 'foo', and a distinct list of bars associated with said 'foo' is displayed on the right. A user may add new bars to each respective 'foo'. Can think of foo having a parent relationship to bar.
The Bars component maintains a list of bars added by the user. My expectation is the Bars component would re-render the internal newBars collection when a new foo is selected. However, that state hangs around and is displayed regardless of what 'foo' is selected on the lefthand side.
That seems weird, but perhaps I'm not thinking of React functional components and hooks in the right way. Would love to understand why this behavior exists, and additional would love to hear proposed approaches that make more sense.
Any insight is greatly appreciated!
If you want the hierarchy to be reflected, then initialize the state of your bars so that it synchronizes with the state of your foos. Right now, your Bars is a single component maintaining its own state independently of App. Here's how I would approach this particular relationship.
function App() {
const foos = useMemo(() => ["foo1", "foo2", "foo3"], []);
const [bars, setBars] = useState(foos.map(() => []));
const [selectedIndex, setSelectedIndex] = useState(0);
const setBar = useCallback(
bar => {
setBars(bars => Object.assign(
[...bars],
{ [selectedIndex]: bar }
));
},
[setBars, selectedIndex]
);
const isSelected = useCallback(
index => index === selectedIndex,
[selectedIndex]
);
const foosList = useMemo(
() => foos.map((foo, index) => (
<ListGroupItem
key={foo}
active={isSelected(index)}
onClick={() => setSelectedIndex(index)}
>
{foo}
</ListGroupItem>
)),
[foos, isSelected, setSelectedIndex]
);
return (
<div className="App">
<FoosWrapper>
<TitleSpan>Foos</TitleSpan>
<ListGroup>{foosList}</ListGroup>
</FoosWrapper>
<BarsWrapper>
<TitleSpan>Bars</TitleSpan>
<Bars
foo={foos[selectedIndex]}
bars={bars[selectedIndex]}
setBars={setBar}
/>
</BarsWrapper>
</div>
);
}
function Bars({ foo, bars, setBars }) {
const [pendingBar, setPendingBar] = useState("");
const barsList = useMemo(
() => bars.map(bar => (
<ListGroupItem key={bar}>{bar}</ListGroupItem>
)),
[bars]
);
const save = useCallback(
bar => { setBars([...bars, bar]); },
[setBars, bars]
);
const change = useCallback(
event => { setPendingBar(event.target.value); },
[setPendingBar]
);
const keyPress = useCallback(
event => { if (event.key === "Enter") save(pendingBar); },
[pendingBar, save]
);
return (
<div>
<ListGroup>
<ListGroupItem key={foo}>{foo}</ListGroupItem>
</ListGroup>
<ListGroup>{barsList}</ListGroup>
<InputGroup>
<Input
placeholder="Add a bar"
onChange={change}
onKeyPress={keyPress}
/>
</InputGroup>
</div>
);
}
I may have gone a bit overboard with the memoization hooks, but that should give you at least an idea of what and how various values can be memoized.
Keep in mind that the second argument is the array of dependencies which determines whether the memoized value is recomputed or retrieved from cache. The dependencies of hooks are checked by reference, much like a PureComponent.
I also opted to initialize selectedIndex to 0 to avoid addressing the issue of how to handle the render function for Bars when no foo is selected. I'll leave that as an exercise to you.
If you want to show the bars for the selected foo, you need to structure your "bars" data accordingly. The simplest way to do this is to keep the "bars" data in the format of an object instead of an array. You can write the code as below.
const Bars = props => {
const [pendingBar, setPendingBar] = useState("");
const [newBars, setNewBars] = useState({});
const { foo } = props;
const keyPress = e => {
if (e.key === "Enter") {
save(pendingBar);
}
};
const save = bar => {
if (!foo) {
console.log("Foo is not selected");
return;
}
const bars = newBars[foo] || [];
bars.push(bar);
setNewBars({
...newBars,
[foo]: [...bars]
});
};
return (
<div>
<ListGroup>
<ListGroupItem key={props.foo}>{props.foo}</ListGroupItem>
</ListGroup>
<ListGroup>
{newBars[foo] &&
newBars[foo].map(newBar => (
<ListGroupItem key={newBar}>{newBar}</ListGroupItem>
))}
</ListGroup>
<InputGroup>
<Input
placeholder="Add a bar"
onChange={e => setPendingBar(e.target.value)}
onKeyPress={keyPress}
/>
</InputGroup>
</div>
);
};