I have an array of categories, each category opens to a list of options. I am creating a button that once you click it should close the current category and open the next. The category and current index are passed as props from surveyForm, I can't figure out why I can't increment to the next index and how I would tell it to open the next category.
When I console log the c_index, I get the right index, when I console log the category I get [object, object]
Right now my openNext button only toggles the current category closed.... please help!
const SPQuestion = ({ category, c_index }) => {
const [isOpen, setIsOpen] = useState(false);
const [index, setIndex] = useState(0);
const diversitySurvey = useSelector(state => state.appData.diversitySurvey)
const categories = diversitySurvey.categories
const toggle = () => setIsOpen(!isOpen);
const openNext = (c_index) => {
if( c_index === categories.length - 1){
toggle()
} else {
toggle()
setIndex(index + 1) // I also tried c_index++ and categories[c_index]++
setIsOpen(true)
}
}
return (
<div key={category.title} className="sp-question active">
<Button onClick={toggle} className={`${category.hasFollowUps ? 'parent-category' : ''}`} ><legend>{category.title}</legend></Button>
<div border border-primary>
<Collapse isOpen={isOpen}>
<Field model={`appData.diversitySurvey.categories[${c_index}].selectedValue`} >
<div>
<span className="sp-instructions">
{category.select === 'one' && 'Select One'}
{category.select === 'many' && 'Select all that apply'}
</span>
<Row>
<Col className="py-2 rounded foo" sm="12">
<Row sm="2" xl="3">
{category.options.map((option, o_index) => {
return (
<SPOption
c_index={c_index}
o_index={o_index}
option={option}
select={category.select}
/>
)
}
)}
</Row>
<div id="fixme" className="d-flex flex-row-reverse">
<Button active className="float-right" style={{backgroundColor: '#70d9b8'}} size="sm" onClick={() => openNext(c_index)} >Next</Button>
</div>
</Col>
</Row>
</div>
</Field>
</Collapse>
</div>
</div>
)
}
export const SurveyForm = ({ handleSubmit, survey }) => {
const diversitySurvey = useSelector(State => State.appData.diversitySurvey)
const errorMessage = useSelector(state => state.errorMessage)
const dispatch = useDispatch()
return (
<Form model="appData" onSubmit={handleSubmit} >
<Row>
<Col md="12" className="title"><h1>{diversitySurvey.title}</h1></Col>
</Row>
<Row>
<Col md="12" className="questions">
{diversitySurvey.categories.map((category, c_index) => {
return (
<SPQuestion category={category} c_index={c_index} modelPrefix="appData.diversitySurvey" />
)
})}
<div className="sp-button-container">
<Button className="sp-submit" onClick={() => dispatch(submitSurvey(diversitySurvey))}>Submit</Button>
</div>
{errorMessage &&
<Alert className="sp-fetch-error" color="danger">
<p className="text-danger">{errorMessage}</p>
</Alert>
}
</Col>
</Row>
</Form>
)
}
I think you should add a new state,selected index state, in the parent component. Then add a selected index props and a function props to set value in it in SPQuestion component and use the new props to compare whether the collapse is open or not.
const SPQuestion = ({ category, c_index ,s_index,setSelectedIndex}) => {
const toggle = () => {
if( c_index == s_index){
setSelectedIndex(-1);
}else{
setSelectedIndex(c_index)
}
}
const openNext = () => {
if( c_index < categories.length){
setSelectedIndex(c_index + 1);
}
}
<Collapse isOpen={c_index === s_index}>
}
Then in parent component:
const [index, setIndex] = useState(-1);
<SPQuestion category={category} c_index={c_index} s_index={index} setSelectedIndex={setIndex} modelPrefix="appData.diversitySurvey" />
I haven't tested it yet so if you can provide sandbox, please do.
Related
When editing a todo it will automictically clear the value, I would like it to contain its original value so you can edit upon it rather than typing everything all over again.
Im assuming usestate is setting the editingText into an empty string in which case in will always output a empty value?
Also I would like to incorporate a cancel button in which cancels eiditing and returns back to its current value.
const App = () => {
const [todos, setTodos] = React.useState([]);
const [todo, setTodo] = React.useState("");
const [todoEditing, setTodoEditing] = React.useState(null);
const [editingText, setEditingText] = React.useState("");
function handleSubmit(e) {
e.preventDefault();
const newTodo = {
id: new Date().getTime(),
text: todo,
completed: false,
};
setTodos([...todos].concat(newTodo));
setTodo("");
}
function deleteTodo(id) {
let updatedTodos = [...todos].filter((todo) => todo.id !== id);
setTodos(updatedTodos);
}
function toggleComplete(id) {
let updatedTodos = [...todos].map((todo) => {
if (todo.id === id) {
todo.completed = !todo.completed;
}
return todo;
});
setTodos(updatedTodos);
}
function submitEdits(id) {
const updatedTodos = [...todos].map((todo) => {
if (todo.id === id) {
todo.text = editingText;
}
return todo;
});
setTodos(updatedTodos);
setTodoEditing(null);
}
return (
<div id="todo-list">
<h1>Todo List</h1>
<form onSubmit={handleSubmit}>
<input
type="text"
onChange={(e) => setTodo(e.target.value)}
value={todo}
/>
<button type="submit">Add Todo</button>
</form>
{todos.map((todo) => (
<div key={todo.id} className="todo">
<div className="todo-text">
{todo.id === todoEditing ? (
<input
type="text"
onChange={(e) => setEditingText(e.target.value)}
/>
) : (
<div>{todo.text}</div>
)}
</div>
<div className="todo-actions">
{todo.id === todoEditing ? (
<button onClick={() => submitEdits(todo.id)}>Submit Edits</button>
) : (
<button onClick={() => setTodoEditing(todo.id)}>Edit</button>
)}
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</div>
</div>
))}
</div>
);
};
export default App;
Use defaultValue to set the initial value of the input
<div className="todo-text">
{todo.id === todoEditing ? (
<input
defaultValue={todo.text}
type="text"
onChange={(e) => setEditingText(e.target.value)}
/>
) : (
<div>{todo.text}</div>
)}
</div>
Adding a cancel button is just setting your edit id to null
<>
<button onClick={() => submitEdits(todo.id)}>
Submit Edits
</button>
<button onClick={() => setTodoEditing(null)}>Cancel</button>
</>
Stackblitz: https://stackblitz.com/edit/react-ts-rarpqn?file=App.tsx
Use value prop
{todo.id === todoEditing ? (
<input
value={todo.text}
type="text"
onChange={(e) => setEditingText(e.target.value)}
/>
) : (
<div>{todo.text}</div>
)}
The radio group value gets updated but its not showing selected. Over here, the value is shown in console of radio group selected but the radio group is not highlighted or shown selected.
const Quiz = () => {
const { questions, quiz, options } = useSelector((state) => state.quiz);
const [currentQuestion, setCurrentQuestion] = useState(0);
console.log(currentQuestion[number] + "1q");
const dispatch = useDispatch();
const history = useHistory();
const location = useLocation();
const classes = useStyles();
// this is to get the questions from the history coming from redux store.
useEffect(() => {
if (!questions) {
dispatch(fetchQuestions(history));
}
}, []);
const handleRadioChange = (number, event) => {
let currentSelection = questions.find(question => question.number === number);
console.log(currentSelection + "radio selected");
currentSelection.value = event.target.value;
console.log(currentSelection.value + "calculate score");
// Set the new question count based on the current one
setCurrentQuestion((current) => {
return Math.min(
current + 1,
questions.length - 1
);
});
};
const previousQuestion = (current_question) => {
let new_current_questions = Math.max(current_question - 1, 0);
setCurrentQuestion(new_current_questions);
};
function handleSubmit(event) {
event.preventDefault();
const valid = questions.some((q) => !q.value);
console.log(valid + "questionsalpha");
if (!valid) {
dispatch(postQuiz({ responses: questions, id: quiz.id }, history));
}
setCurrentQuestion(0);
}
return (
!questions?.length ? <CircularProgress /> : (
<Grid className={classes.container} container alignItems="stretch" spacing={1}>
<form onSubmit={handleSubmit}>
{/* Only show the question if it's index is less than or equal to the current question */}
<button type="submit" onClick={previousQuestion}>{current_question+1 ? 'Previous' : null}</button>
{questions.map((question, index) => (index <= currentQuestion ? (
<FormControl component="fieldset" key={question.number} className={classes.formControl} data-hidden={question.number !== current_question[question.number]}>
<FormLabel component="legend">{question.question}</FormLabel>
<RadioGroup aria-label="quiz" name="quiz" value={question.value || ''} onChange={(e) => handleRadioChange(question.number, e)}>
{options.map((option) =>
<FormControlLabel key={option.score} value={option.score} control={<Radio />} label={option.label} />
)}
</RadioGroup>
</FormControl>
) : null))}
<Button type="submit" variant="outlined" color="primary" className={classes.button}>
Submit
</Button>
</form>
</Grid>
)
);
};
export default Quiz;
Though the value of Radio group selected is coming in console.log, but the radio group is not shown selected. How to make it appear which radio button is selected. Please help. Thanks. I want to show the selected radio button also.
Try something like this:
<FormControlLabel key={option.score} value={option.score} control={<Radio />} label={option.label} checked={question.value===option.score} />
I'm trying to add and remove a count every time the div is clicked and either added or removed from the list, So far I have been able to add the increment count when the div is clicked but it still adds the count even when the name has already been clicked and added to the list. I have placed the incrementCount() in the incorrect place and also I have not been able to work out where to add the decrementCount() to. This should be easy for most people. and many thanks in advance if you could help out or point me in the right direction 😀 the Link to sandbox is here ➡️ https://codesandbox.io/s/optimistic-hamilton-len0q?file=/src/Home.js:244-258
import { useEffect, useState } from "react";
export const List = (props) => {
const [selectedNames, setSelectedNames] = useState([]);
const [names, setNames] = useState([]);
const [count, setCount] = useState(0);
const decrementCount = () => {
if (count > 0) setCount(count - 1);
};
const incrementCount = () => {
setCount(count + 1);
};
useEffect(() => {
props.title === "" && setNames(props.items);
}, [setNames, props]);
return (
<div className="">
<div className="">
⬇ Click on the names below to add them to the list
{names &&
names.map((item, index) => (
<div
key={`${props}-${index}`}
onClick={() => {
incrementCount();
!selectedNames.includes(item) &&
setSelectedNames((oldValue) => [...oldValue, item]);
}}
>
<div className="list-name">{item.name}</div>
</div>
))}
</div>
<div className="count-box">
{count}
<span>selected</span>
</div>
<div
className="unselect-all-box"
onClick={() => {
setSelectedNames([]);
setCount(0);
}}
>
Unselect all
</div>
{selectedNames &&
selectedNames.map((format) => (
<div key={format.id}>
<div className="">{format.name}</div>
<div
className="remove-selected"
onClick={() => {
setSelectedNames(
selectedNames.filter((f) => f.name !== format.name)
);
}}
>
(Press HERE to remove name)
</div>
</div>
))}
</div>
);
};
export default List;
export const App = (props) => {
const formats = [
{
id: "0001",
name: "(1) Sam Smitty",
},
{
id: "0002",
name: "(2) Hong Mong",
},
];
return (
<div>
<List title="" items={formats} />
</div>
);
};
export default App;```
I think you are over-complicating things a bit. From what I can tell, the count state is just "derived state" from the selectedNames array length. There's really no need to increment/decrement a selected names count when you can just count the length of the selectedNames array.
Remove the increment/decrement handlers and use the selectedNames array length. By deriving the selected count there's no need to count anything manually.
<div className="count-box">
{selectedNames.length}{" "}
<span>selected</span>
</div>
export const List = (props) => {
const [selectedNames, setSelectedNames] = useState([]);
const [names, setNames] = useState([]);
useEffect(() => {
props.title === "" && setNames(props.items);
}, [setNames, props]);
return (
<div className="">
<div className="">
⬇ Click on the names below to add them to the list
{names?.map((item, index) => (
<div
key={`${props}-${index}`}
onClick={() => {
!selectedNames.includes(item) &&
setSelectedNames((oldValue) => [...oldValue, item]);
}}
>
<div className="list-name">{item.name}</div>
</div>
))}
</div>
<div className="count-box">
{selectedNames.length} <span>selected</span>
</div>
<div
className="unselect-all-box"
onClick={() => {
setSelectedNames([]);
}}
>
Unselect all
</div>
{selectedNames?.map((format) => (
<div key={format.id}>
<div className="">{format.name}</div>
<div
className="remove-selected"
onClick={() => {
setSelectedNames(
selectedNames.filter((f) => f.name !== format.name)
);
}}
>
(Press HERE to remove name)
</div>
</div>
))}
</div>
);
};
Increment the count only if the item is not in the array
onClick={() => {
if(!selectedNames.includes(item)){
incrementCount();
setSelectedNames((oldValue) => [...oldValue, item]);
}
}}
I have an array of data that will be used to create accordions, I'd like to make it so that only one of them can be expanded at once (i.e, if the user expands accordion #1 and then #2, #1 will un-expand)
I have this code:
const MyAccordion = props => {
const [expanded, setExpanded] = React.useState()
const handleChange = panel => (_, isExpanded) => {setExpanded(isExpanded ? panel : false)}
const classes = styles //?
let accordionInfo = createAccordionInfo(props.propthing);
return (
<Accordion
key={accordionInfo.uid}
onChange={handleChange(accordionInfo.uid)}
expanded={expanded === accordionInfo.uid}
TransitionProps={{unmountOnExit: true}}
className={classes.accordion}
>
<AccordionSummary expandIcon={<ExpandMoreIcon />} aria-controls={`${accordionInfo.uid}-content`} id={`${accordionInfo.uid}-header`}>
<Typography>Accordion Summary</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>Accordion Details</Typography>
</AccordionDetails>
</Accordion>
)
}
const MyAccordions = props => {
const [expanded, setExpanded] = React.useState()
const handleChange = panel => (_, isExpanded) => {setExpanded(isExpanded ? panel : false)}
return (
<div className={styles.root}>
{accordions.map(accordion => (
<MyAccordion onChange={handleChange} propthing={accordion} />
))}
</div>
)
}
I'm quite new to React so I suspect I've made a mistake with the states. Any help / tips would be appreciated! Thank you
It looks like you tried putting state and handlers in both the parent MyAccordions and the children MyAccordion components. If you want only one accordion open at-a-time then I suggest placing the state in the parent component so it can manage what is open/expanded. Use the children accordion ids as the basis of determining which should expand.
Parent
const MyAccordions = props => {
const [expanded, setExpanded] = React.useState(null);
const handleChange = id => (_, isExpanded) => {
// if expanded, set id to open/expand, close it otherwise
setExpanded(isExpanded ? id: null);
};
return (
<div className={styles.root}>
{accordions.map(accordion => {
const info = createAccordionInfo(accordion);
return (
<MyAccordion
key={info.uid} // <-- set React key here!!
onChange={handleChange(info.uid)}
expanded={expanded === info.uid}
/>
)
})}
</div>
);
};
Child
const MyAccordion =({ expanded, onChange }) => {
const classes = styles //?
return (
<Accordion
onChange={onChange}
expanded={expanded}
TransitionProps={{unmountOnExit: true}}
className={classes.accordion}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`${accordionInfo.uid}-content`}
id={`${accordionInfo.uid}-header`}
>
<Typography>Accordion Summary</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography>Accordion Details</Typography>
</AccordionDetails>
</Accordion>
);
};
I have a small webapp and I need to display a dropdown menu when you hover over the word "collective" but the menu should be still there untill I click on a word in the menu or hover out of the menu but it instantly disappears when I move my mouse out from the word "collective" and I don't even get to move my mouse pointer to any of the drop down menu item. I want my drop down menu to be like the one in https://www.geeksforgeeks.org/
my sandbox so far
https://codesandbox.io/s/funny-river-c76hu
For the app to work, you would have to type in the input box "collective", click analyse, then a progressbar will show, click on the blue line in the progressbar, an underline would show under the word "collective" then you should hover over "collective" word and a drop down menu should be displayed.
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import { Content, Dropdown, Label, Progress, Button, Box } from "rbx";
import "rbx/index.css";
function App() {
const [serverResponse, setServerResponse] = useState(null);
const [text, setText] = useState([]);
const [loading, setLoading] = useState(false);
const [modifiedText, setModifiedText] = useState(null);
const [selectedSentiment, setSentiment] = useState(null);
const [dropdownContent, setDropdownContent] = useState([]);
const [isCorrected, setIsCorrected] = useState(false);
const [displayDrop, setDisplayDrop] = useState(false);
useEffect(() => {
if (serverResponse && selectedSentiment) {
const newText = Object.entries(serverResponse[selectedSentiment]).map(
([word, recommendations]) => {
const parts = text[0].split(word);
const newText = [];
parts.forEach((part, index) => {
newText.push(part);
if (index !== parts.length - 1) {
newText.push(
<u
className="dropbtn"
data-replaces={word}
onMouseOver={() => {
setDropdownContent(recommendations);
setDisplayDrop(true);
}}
onMouseOut={() => setDisplayDrop(false)}
>
{word}
</u>
);
}
});
return newText;
}
);
setModifiedText(newText.flat());
}
}, [serverResponse, text, selectedSentiment]);
const handleAnalysis = () => {
setLoading(true);
setTimeout(() => {
setLoading(false);
setServerResponse({ joy: { collective: ["inner", "constant"] } });
}, 1500);
};
const handleTextChange = event => {
setText([event.target.innerText]);
};
const replaceText = wordToReplaceWith => {
const replacedWord = Object.entries(serverResponse[selectedSentiment]).find(
([word, recommendations]) => recommendations.includes(wordToReplaceWith)
)[0];
setText([
text[0].replace(new RegExp(replacedWord, "g"), wordToReplaceWith)
]);
setModifiedText(null);
setServerResponse(null);
setIsCorrected(true);
setDropdownContent([]);
};
const hasResponse = serverResponse !== null;
return (
<Box>
<Content>
<div
onInput={handleTextChange}
contentEditable={!hasResponse}
style={{ border: "1px solid red" }}
>
{hasResponse && modifiedText
? modifiedText.map((text, index) => <span key={index}>{text}</span>)
: isCorrected
? text[0]
: ""}
</div>
<br />
{displayDrop ? (
<div
id="myDropdown"
class="dropdown-content"
onClick={() => setDisplayDrop(false)}
>
{dropdownContent.map((content, index) => (
<>
<strong onClick={() => replaceText(content)} key={index}>
{content}
</strong>{" "}
</>
))}
</div>
) : null}
<br />
<Button
color="primary"
onClick={handleAnalysis}
disabled={loading || text.length === 0}
>
analyze
</Button>
<hr />
{hasResponse && (
<Label>
Joy{" "}
<Progress
value={Math.random() * 100}
color="info"
onClick={() => setSentiment("joy")}
/>
</Label>
)}
</Content>
</Box>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);