Change only state of clicked card - reactjs

How can I get the clicked card only to change its string from 'not captured' to 'captured'? Right now, all cards' strings say 'captured' even if I click on only one. I think the problem is that the captured state updates for all the cards and I can't get the captured state to update for the single clicked card. It's an onChange event and a checkbox.
import React, { useState, useEffect } from 'react'
import PokemonCard from '../components/PokemonCard';
const Pokedex = () => {
const [pokemons, setPokemons] = useState([]);
const [captured, setCaptured] = useState(false )
const URL = 'https://pokeapi.co/api/v2/pokemon/?limit=151';
const fetchingPokemons = async () => {
const res = await fetch(URL);
const data = await res.json();
// console.log(data)
setPokemons(data.results)
}
useEffect(() => {
fetchingPokemons()
}, [URL])
const toggleCaptured= (e, id) => {
console.log(id)
if(id && e) {
console.log('oh')
setCaptured(captured => !captured)
}
let capturedPkm = [];
let notCapturedPkm = [];
pokemons.forEach(i => {
if(captured === true) {
capturedPkm.push(pokemons[i])
} else {
notCapturedPkm.push(pokemons[i])
}
})
console.log('captured', capturedPkm, 'not captured', notCapturedPkm)
}
return (
<>
<div style={{display: 'flex', flexWrap: 'wrap', justifyContent: 'space-evenly'}}>
{pokemons ? pokemons.map((pokemon) => {
return (
<>
<div style={{ width: '235px' }} >
<PokemonCard
pokemon={pokemon}
name={pokemon.name}
url={pokemon.url}
key={pokemon.id}
captured={captured}
toggleCaptured={toggleCaptured}
/>
</div>
</>
)
}) : <h1>Loading...</h1>}
</div>
</>
)
}
export default Pokedex
import React, { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import PokemonIcon from './PokemonIcon';
const PokemonCard = (props) => {
const { url, captured, toggleCaptured } = props
const URL = url
const [pokemonCard, setPokemonCard] = useState([])
const fetchingPokemonCard = async () => {
const res = await fetch(URL);
const data = await res.json();
//console.log(data)
setPokemonCard(data)
}
useEffect(() => {
fetchingPokemonCard()
}, [URL])
return (
<>
<div className='pokemon-card' style={{
height: '250px',
maxWidth: '250px',
margin: '1rem',
boxShadow: '5px 5px 5px 4px rgba(0, 0, 0, 0.3)',
cursor: 'pointer',
}} >
<Link
to={{ pathname: `/pokemon/${pokemonCard.id}` }}
state={{ pokemon: pokemonCard, captured }}
style={{ textDecoration: 'none', color: '#000000' }}>
<div
style={{ padding: '20px', display: 'flex', justifyContent: 'center', alignItems: 'center' }} >
<PokemonIcon img={pokemonCard.sprites?.['front_default']} />
</div>
</Link>
<div style={{ textAlign: 'center' }}>
<h1 >{pokemonCard.name}</h1>
<label >
<input
type='checkbox'
defaultChecked= {captured}
onChange={(e) => toggleCaptured(e.target.checked, pokemonCard.id)}
/>
<span style={{ marginLeft: 8, cursor: 'pointer' }}>
{captured === false ? 'Not captured!' : 'Captured!'}
{console.log(captured)}
</span>
</label>
</div>
</div>
<div>
</div>
</>
)
}
export default PokemonCard

Use an object as state:
const [captured, setCaptured] = useState({})
Set the toggle function:
const toggleCaptured = (checked, id) => {
const currentChecked = { ...captured }; // Create a shallow copy
console.log(id)
if (checked) {
currentChecked[id] = true;
} else {
delete currentChecked[id];
}
setCaptured(currentChecked); // Update the state
let capturedPkm = pokemons.filter(({id}) => currentChecked[id]);
let notCapturedPkm = pokemons.filter(({id}) => !currentChecked[id]);
console.log('captured', capturedPkm, 'not captured', notCapturedPkm)
}
The PokemonCard should look like this
<PokemonCard
pokemon={pokemon}
name={pokemon.name}
url={pokemon.url}
key={pokemon.id}
captured={captured[pokemon.id]} // Here
toggleCaptured={toggleCaptured}
/>

Related

Show/Hide multiple elements of each button click with matching indexes in React JS

I have a scenario where I have 2 different components ( buttons and an info container). On each button click I am trying to display each matched info container. I am able to achieve the desired functionality in my buttons, but when I pass state back to my other component I am only able to display the matched index. My desired result is if I clicked a button in my nav and it has an active class all my "info container" should remain visible until the "active" class is toggled/removed.
JS:
...
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
"& > *": {
margin: theme.spacing(1)
}
},
orange: {
color: theme.palette.getContrastText(deepOrange[500]),
backgroundColor: deepOrange[500],
border: "4px solid black"
},
info: {
margin: "10px"
},
wrapper: {
display: "flex"
},
contentWrapper: {
display: "flex",
flexDirection: "column"
},
elWrapper: {
opacity: 0,
"&.active": {
opacity: 1
}
}
}));
const ToggleItem = ({ onChange, id, styles, discription }) => {
const [toggleThisButton, setToggleThisButton] = useState(false);
const handleClick = (index) => {
setToggleThisButton((prev) => !prev);
onChange(index);
};
return (
<>
<Avatar
className={toggleThisButton ? styles.orange : ""}
onClick={() => handleClick(id)}
>
{id}
</Avatar>
{JSON.stringify(toggleThisButton)}
{/* {toggleThisButton && <div className={styles.info}>{discription}</div> } */}
</>
);
};
const ToggleContainer = ({ discription, className }) => {
return <div className={className}> Content {discription}</div>;
};
export default function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [value, setValue] = useState(false);
const handleChange = (newValue) => {
setValue(newValue);
console.log("newValue===", newValue);
};
return (
<>
<div className={classes.wrapper}>
{data.map((d, id) => {
return (
<div key={id}>
<ToggleItem
id={id}
styles={classes}
discription={d}
onChange={handleChange}
/>
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
{data.map((d, id) => {
return (
<ToggleContainer
className={
value === id
? clsx(classes.elWrapper, "active")
: classes.elWrapper
}
key={id}
styles={classes}
discription="Hello"
/>
);
})}
</div>
</>
);
}
Codesanbox:
https://codesandbox.io/s/pedantic-dream-vnbgym?file=/src/App.js:0-2499
Codesandbox : https://codesandbox.io/s/72166087-zu4ev7?file=/src/App.js
You can store the selected tabs in a state. That way you don't need to render 3 (or more) <ToggleContainer>. In <ToggleContainer> pass the selected tabs as props and render the selected tabs content in <ToggleContainer>.
import React, { useState } from "react";
import "./styles.css";
import { makeStyles } from "#material-ui/core/styles";
import Avatar from "#material-ui/core/Avatar";
import { deepOrange } from "#material-ui/core/colors";
import clsx from "clsx";
const useStyles = makeStyles((theme) => ({
root: {
display: "flex",
"& > *": {
margin: theme.spacing(1)
}
},
orange: {
color: theme.palette.getContrastText(deepOrange[500]),
backgroundColor: deepOrange[500],
border: "4px solid black"
},
info: {
margin: "10px"
},
wrapper: {
display: "flex"
},
contentWrapper: {
display: "flex",
flexDirection: "column"
},
elWrapper: {
opacity: 0,
"&.active": {
opacity: 1
}
}
}));
const ToggleItem = ({ onChange, id, styles, discription }) => {
const [toggleThisButton, setToggleThisButton] = useState(false);
const handleClick = (index) => {
onChange(discription, !toggleThisButton);
setToggleThisButton((prev) => !prev);
};
return (
<>
<Avatar
className={toggleThisButton ? styles.orange : ""}
onClick={() => handleClick(id)}
>
{id}
</Avatar>
{JSON.stringify(toggleThisButton)}
{/* {toggleThisButton && <div className={styles.info}>{discription}</div> } */}
</>
);
};
const ToggleContainer = ({ className, selected }) => {
return (
<div className={className}>
{selected.map((item, idx) => (
<div key={idx}>Content {item}</div>
))}
</div>
);
};
export default function App() {
const data = ["first", "second", "third"];
const classes = useStyles();
const [selected, setSelected] = useState([]);
// action : False -> Remove, True -> Add
const handleChange = (val, action) => {
let newVal = [];
if (action) {
// If toggle on, add content in selected state
newVal = [...selected, val];
} else {
// If toggle off, then remove content from selected state
newVal = selected.filter((v) => v !== val);
}
console.log(newVal);
setSelected(newVal);
};
return (
<>
<div className={classes.wrapper}>
{data.map((d, id) => {
return (
<div key={id}>
<ToggleItem
id={id}
styles={classes}
discription={d}
onChange={handleChange}
/>
</div>
);
})}
</div>
<div className={classes.contentWrapper}>
<ToggleContainer styles={classes} selected={selected} />
</div>
</>
);
}

How can I upload Images with Preview in React

I've been trying to upload multiple images WITH preview in NextJS (React). I tried changing the constants to arrays and tried mapping through them but it just doesn't seem to work and I don't know how I could get it to work.
I've made a component out of the upload functionality and here is the code that works for uploading a single image with a Preview.
uploadImage.js
import React, { useEffect, useRef, useState } from "react";
import styled from "styled-components";
function imageUpload() {
const [image, setImage] = useState(null);
const fileInputRef = useRef();
const [preview, setPreview] = useState();
useEffect(() => {
if (image) {
const reader = new FileReader();
reader.onloadend = () => {
setPreview(reader.result);
};
reader.readAsDataURL(image);
} else {
}
}, [image]);
return (
<div className="flex ">
<StyledImg
src={preview}
style={{ objectFit: "cover" }}
onClick={() => setImage(null)}
/>
<StyledButton
onClick={(e) => {
e.preventDefault();
fileInputRef.current.click();
}}
/>
<input
type="file"
style={{ display: "none" }}
accept="image/*"
ref={fileInputRef}
onChange={(e) => {
const file = e.target.files[0];
if (file && file.type.substr(0, 5) === "image") {
setImage(file);
} else {
setImage(null);
}
}}
/>
</div>
);
}
const StyledButton = styled.button`
`;
const StyledImg = styled.img`
width: 100px;
height: 100px;
margin-right: 10px;
`;
export default imageUpload;
Based on these references https://react-dropzone.js.org/#section-previews and https://stackblitz.com/edit/nextjs-buk2rw?file=pages%2Findex.js I replaced my code with the following
ImageUpload.js
import React, { useEffect, useState } from "react";
import { useDropzone } from "react-dropzone";
import styled from "styled-components";
function DragAndDrop() {
const [files, setFiles] = useState([]);
const { getRootProps, getInputProps } = useDropzone({
accept: "image/*",
onDrop: (acceptedFiles) => {
setFiles((files) => [
...files,
...acceptedFiles.map((file) =>
Object.assign(file, {
key: file.name + randomId(), // to allow adding files with same name
preview: URL.createObjectURL(file),
})
),
]);
},
});
const removeFile = (file) => {
setFiles((files) => {
const newFiles = [...files];
newFiles.splice(file, 1);
return newFiles;
});
};
const thumbs = files.map((file, i) => (
<div style={thumb} key={file.key}>
<div style={thumbInner}>
<img src={file.preview} style={img} />
</div>
<button type="button" style={removeButton} onClick={() => removeFile(i)}>
X
</button>
</div>
));
useEffect(
() => () => {
files.forEach((file) => URL.revokeObjectURL(file.preview));
},
[files]
);
return (
<section className="container">
<div {...getRootProps({ className: "dropzone" })}>
<input {...getInputProps()} />
<StyledP className="flex align-center justify-center">
Glisser Images Ici ou Cliquer pour selectionner
</StyledP>
</div>
<aside style={thumbsContainer}>{thumbs}</aside>
</section>
);
}
const StyledP = styled.p`
cursor: pointer;
padding: 30px;
`;
const randomId = () => (Math.random() + 1).toString(36).substring(7);
const thumbsContainer = {
display: "flex",
flexDirection: "row",
flexWrap: "wrap",
marginTop: 16,
};
const thumb = {
display: "inline-flex",
borderRadius: 2,
border: "1px solid #eaeaea",
marginBottom: 8,
marginRight: 8,
width: 100,
height: 100,
padding: 4,
boxSizing: "border-box",
position: "relative",
};
const thumbInner = {
display: "flex",
minWidth: 0,
overflow: "hidden",
};
const img = {
display: "block",
width: "auto",
height: "100%",
};
const removeButton = {
color: "red",
position: "absolute",
right: 4,
};
export default DragAndDrop;

Read more functionality: Too many re-renders. React limits the number of renders to prevent an infinite loop

I am trying to do a Read More functionality, where clicking read more will show the long text for both the header and message. I came across this solution and works, but this only works for only one long text. I want to only have one read more option for both header and message, so I modified it a bit but I am having the above error.
After reading so many similar questions, I still can't seem to fix my code. I understand that this has something to do with the setting of state, but I am at a lost on how to fix this.
Here is a self-contained sample: https://stackblitz.com/edit/react-7qwbcb?file=src/App.js
App.js
import React, { useEffect, useState, useCallback } from 'react';
import Box from '#material-ui/core/Box';
import { notificationsApi } from './fakeAPI';
import ReadMore from './ReadMore';
const Notifications = () => {
const [notifs, setNotifs] = useState(null);
const Lists = useCallback(async () => {
try {
const notif = await notificationsApi.getNotifications();
setNotifs(notif);
} catch (e) {
console.log(e);
}
// return () => {
// notifs;
// };
}, [notifs]);
useEffect(() => {
Lists();
}, [Lists]);
return (
<Box>
{notifs &&
notifs.map((notif) => {
return (
<Box key={notif.id}>
<ReadMore
headerLimit={112}
messageLimit={298}
isRead={notif.isRead}
headerNotif={notif.header}
messageNotif={notif.message}
/>
</Box>
);
})}
</Box>
);
};
export default Notifications;
ReadMore.js
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Box, Typography } from '#material-ui/core';
const ReadMore = (props) => {
const { headerNotif, messageNotif, isRead, headerLimit, messageLimit } =
props;
const [header, setHeader] = useState(null);
const [message, setMessage] = useState(null);
const [limit, setLimit] = useState({
headLimit: headerLimit,
messLimit: messageLimit,
});
const [initialHeaderLimit] = useState(headerLimit);
const [initialMessageLimit] = useState(messageLimit);
const [showOption, setShowOption] = useState(false);
const [viewMoreClicked, setViewMoreClicked] = useState(false);
const getReadMoreContent = () => {
if (headerNotif.length > limit.headLimit) {
setHeader(<span>{headerNotif.substr(0, limit.headLimit)}...</span>);
setShowOption(true);
} else if (headerNotif.length < limit.headLimit) {
setHeader(<span>{headerNotif}</span>);
}
if (messageNotif.length > limit.messLimit) {
setMessage(
<span>
{messageNotif.substr(0, limit.messLimit)}...
{!viewMoreClicked && (
<Box sx={{ pt: 2 }}>
<span
style={{
color: '#008C44',
cursor: 'pointer',
display: 'block',
fontSize: 14,
}}
onClick={() => showLongText()}
>
<u> View More</u>
</span>
</Box>
)}
</span>
);
setShowOption(true);
} else if (messageNotif.length < limit.messLimit) {
setMessage(<span>{messageNotif}</span>);
}
return (
<span>
<Box sx={{ pb: 1 }}>
<Typography
variant="subtitle2"
style={{
fontSize: 16,
color: isRead ? '#333333' : '#008C44',
}}
>
{header}
</Typography>
</Box>
<Box sx={{ pb: 1 }}>
<Typography
variant="body2"
style={{
fontSize: 12,
color: '#666666',
}}
>
{message}
</Typography>
</Box>
<span
style={{
color: '#008C44',
cursor: 'pointer',
display: 'block',
fontSize: 14,
}}
onClick={() => showShortText()}
>
{showOption && viewMoreClicked && <u> View Less</u>}
</span>
</span>
);
};
const showLongText = () => {
setViewMoreClicked(true);
setLimit({
headLimit: headerNotif.length,
messLimit: messageNotif.length,
});
getReadMoreContent();
};
const showShortText = () => {
setViewMoreClicked(false);
setLimit({
headLimit: initialHeaderLimit,
messLimit: initialMessageLimit,
});
getReadMoreContent();
};
return <>{getReadMoreContent()}</>;
};
ReadMore.propTypes = {
headLimit: PropTypes.number,
messLimit: PropTypes.number,
};
export default ReadMore;
I ended up redoing the ReadMore.jsx to limit the use of states and did conditional returns:
import React, { useState } from 'react'
import { Box, Typography } from '#material-ui/core'
const Content = (
header,
message,
isRead,
showLongText,
showShortText,
showViewMore,
showViewLess
) => {
return (
<span>
<Box sx={{ pb: 1 }}>
<Typography
variant="subtitle2"
style={{
fontSize: 16,
color: isRead ? '#333333' : '#008C44',
}}
>
{header}
</Typography>
</Box>
<Box sx={{ pb: 1 }}>
<Typography
variant="body2"
style={{
fontSize: 12,
color: '#666666',
}}
>
{message}
</Typography>
</Box>
{showViewMore && (
<Box sx={{ pt: 2 }}>
<span
style={{
color: '#008C44',
cursor: 'pointer',
display: 'block',
fontSize: 14,
}}
onClick={() => showLongText()}
>
<u> View More</u>
</span>
</Box>
)}
{showViewLess && (
<Box sx={{ pt: 2 }}>
<span
style={{
color: '#008C44',
cursor: 'pointer',
display: 'block',
fontSize: 14,
}}
onClick={() => showShortText()}
>
<u> View Less</u>
</span>
</Box>
)}
</span>
)
}
const ReadMore = (props) => {
const { headerNotif, messageNotif, isRead, headerLimit, messageLimit } =
props
const [limit, setLimit] = useState({
headLimit: headerLimit,
messLimit: messageLimit,
})
const getReadMoreContent = () => {
if (headerNotif.length > limit.headLimit) {
if (messageNotif.length > limit.messLimit) {
return Content(
headerNotif.substr(0, limit.headLimit) + '...',
messageNotif.substr(0, limit.messLimit) + '...',
isRead,
showLongText,
showShortText,
true,
false
)
} else if (messageNotif.length < limit.messLimit) {
return Content(
headerNotif.substr(0, limit.headLimit) + '...',
messageNotif,
isRead,
showLongText,
showShortText,
true,
false
)
}
} else if (headerNotif.length < limit.headLimit) {
if (messageNotif.length > limit.messLimit) {
return Content(
headerNotif,
messageNotif.substr(0, limit.messLimit) + '...',
isRead,
showLongText,
showShortText,
true,
false
)
} else if (messageNotif.length < limit.messLimit) {
return Content(
headerNotif,
messageNotif,
isRead,
showLongText,
showShortText,
false,
false
)
}
}
return Content(
headerNotif,
messageNotif,
isRead,
showLongText,
showShortText,
false,
true
)
}
const showLongText = () => {
setLimit({
headLimit: headerNotif.length,
messLimit: messageNotif.length,
})
getReadMoreContent()
}
const showShortText = () => {
setLimit({
headLimit: headerLimit,
messLimit: messageLimit,
})
getReadMoreContent()
}
return <>{getReadMoreContent()}</>
}
export default ReadMore

How to handle backend data for react-beautiful-dnd

I have been working on a drag and drop planning feature, and I am attempting to pull saved data from my backend. I have been able to successfully log the needed data, however, when I am passing it into the react-beautiful-DnD template I have been using, the data fails to appear in the items array even though it is structured exactly the same as the static starter data in the other column.
const onDragEnd = (result, columns, setColumns) => {
if (!result.destination) return;
const { source, destination } = result;
if (source.droppableId !== destination.droppableId) {
const sourceColumn = columns[source.droppableId];
const destColumn = columns[destination.droppableId];
const sourceItems = [...sourceColumn.items];
const destItems = [...destColumn.items];
const [removed] = sourceItems.splice(source.index, 1);
destItems.splice(destination.index, 0, removed);
setColumns({
...columns,
[source.droppableId]: {
...sourceColumn,
items: sourceItems
},
[destination.droppableId]: {
...destColumn,
items: destItems
}
});
} else {
const column = columns[source.droppableId];
const copiedItems = [...column.items];
const [removed] = copiedItems.splice(source.index, 1);
copiedItems.splice(destination.index, 0, removed);
setColumns({
...columns,
[source.droppableId]: {
...column,
items: copiedItems
}
});
}
};
function DragTables() {
const itemStarter = [
{ id: uuid(), travel: "Flying from NYC to MCO", brand: "American Airlines", category: "Airline", Start: "8/12/21", End: "8/12/21", points: "10000", value: "500" }
];
useEffect (() => {
fetchNewData()
},[])
const [unplannedDataSet, setUnplannedDataSet] = useState([]);
async function fetchNewData() {
// const itineraryId = 2
const response = await fetch('http://localhost:5000/planner/getUnplannedItineraryData', {
method: "POST",
headers: {jwt_token: localStorage.token}
})
const dataSet = await response.json();
setUnplannedDataSet(dataSet)
}
useEffect (() => {
fetchPlannedData()
},[])
const [plannedDataSet, setPlannedDataSet] = useState([]);
async function fetchPlannedData() {
// const itineraryId = 2
const response = await fetch('http://localhost:5000/planner/getPlannedItineraryData', {
method: "POST",
headers: {jwt_token: localStorage.token}
})
const plannedDataSet = await response.json();
setPlannedDataSet(plannedDataSet)
}
const parsedData = [];
unplannedDataSet.forEach(element => {
parsedData.push({
id: element.id,
brand: element.brand
});
});
**const columnsFromBackend = {
//This does not
[uuid()]: {
name: "Itinerary",
items: plannedDataSet
},
//This works
[uuid()]: {
name: "Travel Options",
items: itemStarter
}
};**
const [columns, setColumns] = useState(columnsFromBackend);
//DND component
return (
<div>
<div style={{ display: "flex", justifyContent: "space-around", height: "100%", marginTop: 8}}>
<DragDropContext
onDragEnd={result => onDragEnd(result, columns, setColumns)}
>
{Object.entries(columns).map(([columnId, column], index) => {
return (
<div
style={{
display: "block",
flexDirection: "column",
alignItems: "center",
fontSize: 2
}}
key={columnId}
>
<h4 style={{ display: "flex",
justifyContent: "center",}}>{column.name}</h4>
<div style={{ display: "flex",
justifyContent: "center",
marginTop: 4}}>
<Droppable droppableId={columnId} key={columnId}>
{(provided, snapshot) => {
return (
<div
{...provided.droppableProps}
ref={provided.innerRef}
>
{column.items.map((item, index) => {
return (
<Draggable
key={item.id}
draggableId={item.id}
index={index}
>
{(provided, snapshot) => {
return (
<div className="snapshot"
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<p style={{paddingLeft: 5, paddingTop: 1}}> <div style={{borderBottom: "1px solid white" }}><strong>{item.travel}</strong> </div>
<strong>Brand:</strong> {item.brand} | <strong># of Points:</strong> {item.points} | <strong>Point Value:</strong> ${item.value}
<br></br><strong>Category:</strong> {item.category} | <strong>Start Date:</strong> {item.Start} | <strong>End Date:</strong> {item.End}</p>
<p></p>
</div>
);
}}
</Draggable>
);
})}
{provided.placeholder}
</div>
);
}}
</Droppable>
</div>
</div>
);
})}
</DragDropContext>
</div>
</div>
);
}
export default DragTables;```
The reason why your code is not working is that you put const columnsFromBackend nested in your React DragTables Component. When you do this JavaScript will compile the code each time, producing a new copy of the columnsFromBackend Object, and React will not initialize the useEffect right, causing an infinite loop. Just put columnsFromBackend in the root scope and it will work right, but you will need to sync with the database.
You should not pass an object into useEffect, only a plain-old-data-type, but I think a string is okay but not an Object. I personally use a timer to autosave my state for my Chrome Extension. I have a useState number to switch tabs/views with my nav bar, and in each tab/view the timer updates a different part of my state relevant to the mode. You can also use a boolean useState and each time you change from true to false or false to true it saves.

*Help* Convert class to functional component using hooks

I'm trying to convert EditableTabGroup to a functional component Tags however I can't seem to convert it correctly as I'm trying to remove this. .
EditableTabGroup is working correctly but when I render Tags in Taskform it does not work.
Also, how can I clear state Tags so that onCreate(submit) tags is an empty array?
class EditableTagGroup extends React.Component {
state = {
tags: [],
inputVisible: false,
inputValue: ""
};
handleClose = removedTag => {
const tags = this.state.tags.filter(tag => tag !== removedTag);
console.log(tags);
this.setState({ tags });
};
showInput = () => {
this.setState({ inputVisible: true }, () => this.input.focus());
};
handleInputChange = e => {
this.setState({ inputValue: e.target.value });
};
handleInputConfirm = () => {
const { inputValue } = this.state;
let { tags } = this.state;
if (inputValue && tags.indexOf(inputValue) === -1) {
tags = [...tags, inputValue];
}
console.log(tags);
this.setState({
tags,
inputVisible: false,
inputValue: ""
});
};
saveInputRef = input => (this.input = input);
forMap = tag => {
const tagElem = (
<Tag
closable
onClose={e => {
e.preventDefault();
this.handleClose(tag);
}}
>
{tag}
</Tag>
);
return (
<span key={tag} style={{ display: "inline-block" }}>
{tagElem}
</span>
);
};
render() {
const { tags, inputVisible, inputValue } = this.state;
const tagChild = tags.map(this.forMap);
const { getFieldDecorator } = this.props;
return (
<div>
<div style={{ marginBottom: 16 }}>
<TweenOneGroup
enter={{
scale: 0.8,
opacity: 0,
type: "from",
duration: 100,
onComplete: e => {
e.target.style = "";
}
}}
leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}
appear={false}
>
{tagChild}
</TweenOneGroup>
</div>
{inputVisible && (
<Input
ref={this.saveInputRef}
onChange={this.handleInputChange}
onPressEnter={this.handleInputConfirm}
value={inputValue}
onBlur={this.handleInputConfirm}
type="text"
size="small"
style={{ width: 78 }}
/>
)}
{getFieldDecorator("tags", {
initialValue: this.state.tags
})(
<Input
ref={this.saveInputRef}
type="text"
size="small"
style={{ display: "none" }}
/>
)}
{!inputVisible && (
<Tag
onClick={this.showInput}
style={{ background: "#fff", borderStyle: "dashed" }}
>
<Icon type="plus" /> New Tag
</Tag>
)}
</div>
);
}
}
export default EditableTagGroup;
So, 2 things that I changed in order to make it work:
1) You didn't export Tags from its file. In this example I exported it as a named export, but you should probably just export it as default export (export default Tags).
2) The second problem was in this part of the code:
const handleInputConfirm = () => {
if (inputValue && state.indexOf(inputValue) === -1) {
let state = [...state, inputValue];
}
setState(state);
setInputVisible(false);
setInputValue("");
};
Inside the if condition, where you check current tags and the tag the user wants to add, you define a let "state". There are 2 problems here. The first one is that you're assigning a let inside an if block, which means that its not accessible from outside of the block, hence the line setState(state) is just setting the state to the same state (state refers to the state variable state, not to the new state you defined inside the if block).
The second problem is not really a problem, you just shouldn't assign new variables with names identical to variables in the upper scopes. It's bad practice as you probably understand now.
Read more about let and its scope rules here.
Here is the full working code of Tags:
import React, { useState } from "react";
import { Tag, Input, Icon } from "antd";
import { TweenOneGroup } from "rc-tween-one";
export const Tags = props => {
const [state, setState] = useState([]);
const [inputVisible, setInputVisible] = useState(false);
const [inputValue, setInputValue] = useState("");
const handleClose = removedTag => {
const tags = state.filter(tag => tag !== removedTag);
setState(tags);
};
const showInput = () => {
setInputVisible(true);
};
const handleInputChange = e => {
setInputValue(e.target.value);
};
const handleInputConfirm = () => {
if (inputValue && state.indexOf(inputValue) === -1) {
var newState = [...state, inputValue];
setState(newState);
}
setInputVisible(false);
setInputValue("");
};
const saveInputRef = input => (input = input);
const forMap = tag => {
const tagElem = (
<Tag
closable
onClose={e => {
e.preventDefault();
handleClose(tag);
}}
>
{tag}
</Tag>
);
return (
<span key={tag} style={{ display: "inline-block" }}>
{tagElem}
</span>
);
};
const tagChild = state.map(forMap);
const { getFieldDecorator } = props;
return (
<div>
<div style={{ marginBottom: 16 }}>
<TweenOneGroup
enter={{
scale: 0.8,
opacity: 0,
type: "from",
duration: 100,
onComplete: e => {
e.target.style = "";
}
}}
leave={{ opacity: 0, width: 0, scale: 0, duration: 200 }}
appear={false}
>
{tagChild}
</TweenOneGroup>
</div>
{inputVisible && (
<Input
ref={saveInputRef}
onChange={handleInputChange}
onPressEnter={handleInputConfirm}
value={inputValue}
onBlur={handleInputConfirm}
type="text"
size="small"
style={{ width: 78 }}
/>
)}
{getFieldDecorator("tags", {
initialValue: state.tags
})(
<Input
ref={saveInputRef}
type="text"
size="small"
style={{ display: "none" }}
/>
)}
{!inputVisible && (
<Tag
onClick={showInput}
style={{ background: "#fff", borderStyle: "dashed" }}
>
<Icon type="plus" /> New Tag
</Tag>
)}
</div>
);
};
As for resetting the tags, you can define the state state inside Taskform.js and pass it to Tags as props. That way you can reset the state (setState([])) on Taskform.js.
Taskform.js:
const [tags, setTags] = useState([]);
const handleCreate = () => {
form.validateFields((err, values) => {
if (err) {
return;
}
form.resetFields();
onCreate(values);
setTags([]);
});
};
...
<Tags
getFieldDecorator={getFieldDecorator}
state={tags}
setState={setTags}
/>
Tags.js:
...
const { state, setState } = props;
Of course you should also remove [state, setState] = useState([]) from Tags.js.
Hope it helps!

Resources