I'm building a quiz/survey builder, kind of CMS interface which will allow users to add as many questions as they want by clicking the type of question they want.
To start with, my state is set up in the App component as follow:
state = {
components: API.components,
comps: []
}
The admin screen has a selected number of buttons which will activate a question onClick. The question comes from the API.components.
For example, we have:
- Welcome Message
- Thank You Message
- Yes or No
/* 1. Generate a list of buttons with onClick props.addQuestion passing a fixed id via 'i' parameter */
const QuestionTypes = props => {
return (
<div data-name='typequestion'>
{props.details.map((el, i) => <div key={i}><button className="sm" onClick={() => props.addQuestion(i)}>{el.label}</button></div>)}
</div>
)
}
See Menu Questions screenshot: https://www.awesomescreenshot.com/image/3982311/8964261115690780c3f67d390ce08665
onClick, each of these buttons will trigger the 'addQuestion' method, which will pass a fixed ID (key) to the 'selectComponent' function to add the selected component to the comps[] array:
/* onClick, a method 'addQuestion' is called */
/* This method will setState and call a function selectComponent passing a key (id) in order to select the correct component */
addQuestion = key => {
this.setState({
comps: [...this.state.comps, this.selectComponent(key)]
});
}
The selectComponent function has a switch to pass the correct component:
selectComponent = (key) => {
switch(key) {
case 0:
return <WelcomeMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
case 1:
return <ThankYouMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
case 2:
return <YesNo details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
default:
return 'Please select a component from the left side menu'
}
}
This will add an element to comps[] array:
[
0: {element here ..}
1: {element here ..} etc.
]
Here an example of the code for the component:
const ThankYouMessage = props => (
<section className="ui-component-view" data-name="thankyou">
<img src={ThankYouImage} alt="x" />
<h3>Thanks for completing our form!</h3>
<div>
<DeleteButton deleteQuestion={props.deleteQuestion} />
</div>
</section>
);
See Selected Welcome Message Component screenshot: https://www.awesomescreenshot.com/image/3982315/f59e1bf79a31194aa3ee3ad2467658a0
PROBLEM:
As you can see, each component will have a delete button.
While each component is added to the array without issues, I can't find a way to delete ONLY the selected component when I click the delete button.
I've tried to use .filter(), splice() but I don't have the right index for the newly created or updated array list.
I want to use the React way to do it, not jQuery or Javascript-ish.
Example of Delete Button. Please note that the props.index is passing the original clicked button id (key), which will not match the newly comps[] array index:
const DeleteButton = props => (
<span className="deleteButton" onClick={() => props.deleteQuestion(props.index)}>×<small>Delete</small></span>
);
export default DeleteButton;
Here the Delete method:
deleteQuestion = e => {
const comps = [...this.state.comps]
// 2. here I need to add something that will DELETE ONLY the clicked button index for that component
// 3. Update state
this.setState({ comps });
}
Please see the full code for the App component:
class App extends React.Component {
state = {
components: API.components,
comps: [],
multichoice: {}
}
selectComponent = (key) => {
switch(key) {
case 0:
return <WelcomeMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
case 1:
return <ThankYouMessage details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
case 2:
return <YesNo details={this.state.components[key]} deleteQuestion={this.deleteQuestion} comps={this.state.comps} index={fuck} />
break;
default:
return 'Please select a component from the left side menu'
}
}
addQuestion = key => {
this.setState({
comps: [...this.state.comps, this.selectComponent(key)]
});
}
deleteQuestion = e => {
const comps = [...this.state.comps]
// 2. here I need to add something that will DELETE ONLY the component related to the delete button
// 3. Update state
this.setState({ comps });
}
render() {
return (
<Container>
<Row>
<Col>
<h1>Survey Builder </h1>
</Col>
</Row>
<Row>
<Col lg={3} className="border-right">
<QuestionTypes addQuestion={this.addQuestion} details={this.state.components} />
</Col>
<Col lg={9}>
<Row>
<Col lg={12}>
<QuestionEdit comps={this.state.comps} details={this.state.components} />
</Col>
</Row>
</Col>
</Row>
</Container>
)
}
}
export default App;
You should not keep the components inside the state (cause that breaks the components lifecycle and it is hard to compare them). Instead, just keep the keys:
addQuestion = key => {
this.setState({
comps: [...this.state.comps, key]
});
}
Then inside render(), map the keys to the components:
{this.state.comps.map((key, index) => <SomeComponent remove={() => this.removeQuestion(index)} />)}
Now removeQuestion can simply be:
removeQuestion(index) {
this.setState(({ comps }) => ({ comps: comps.filter((_, i) => i !== index) }));
}
EDITED:
No components should be held on the state, just objects representing the questions.
The comps state should be immutable, that means that each time a question is added or deleted you should create a new Array out of the old one, currently on the state.
Since you're not using any advanced state managers (like Redux, etc.), and nor should you at this point, I would suggest having a data atribute on each question with the question ID on it. Once clicked you can fetch the question ID from the target the click event is carrying and use it to figure out where is the question item reside on the comps state. Once you have it, create a new comps state, by constructing a new Array which does not have that question you've just deleted.
I would also like to recommend not using a switch/case here, since it defies the open/close principle. I think you will find a dictionary approach, where you map the type of the question to the corresponding component, much more scaleable and maintainable.
Hope this helps :)
Related
I have a speed dial's icon that, when clicked, should add an accordion item to the accordion list.
Its onClick attribute calls a handleClick function that currently adds an object to formik.values.
The object is added to the values map as expected but the user needs to refresh the screen in order to see the new accordion item.
I guess I'm not properly handling the change of state in the form.
I know formik exposes the handleChange method but I'm not sure if and how it should be used in this scenario.
function AddSpeedDial({formik}) {
const dialActions = [
{
icon: <DinnerDiningIcon />,
name: 'New Dish',
handleClick: () => {
const dishCardsLength = Object.keys(formik.values.dishCards).length;
formik.values.dishCards[dishCardsLength+1] = {
dishName: ''
}
}
}
];
return (
<Box>
<SpeedDial>
{dialActions.map((action) => (
<SpeedDialAction
key={action.name}
icon={action.icon}
tooltipTitle={action.name}
onClick={action.handleClick}
/>
))}
</SpeedDial>
</Box>
);
}
I have this FormControl element with Select that accepts an array of options that is being used for MenuItem options and also a value as props and this component looks like this:
const TaxonomySelector = (props) => {
const { isDisabled, taxonomies, selectedTaxonomy, handleTaxonomyChange } = props;
return (
<Grid item xs={12}>
{console.log(selectedTaxonomy)}
{console.log(taxonomies)}
<FormControl disabled={isDisabled} fullWidth>
<InputLabel>Таксономия</InputLabel>
<Select
value={selectedTaxonomy || ''}
onChange={handleTaxonomyChange}>
{Object.values(taxonomies).map((taxonomy) => (
<MenuItem key={taxonomy.id} name={taxonomy.name} value={taxonomy}>
{taxonomy.name} от {moment(taxonomy.date).format('YYYY-MM-DD')}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
);
};
The values that I pass as props are correctly displaying as filled out in the console at all stages the component is being rendered. And also in the case when this component is used for selection through using handleTaxonomyChange function everything is working correctly with user being able to select a particular option out of the array provided to the MenuItem. However, the problem occurs in case when the parent component of this component is being open for View Only or with already pre-defined values. In this case I get the following:
It seems like there's something is being passed to the Select component (even I checked through React Component in DevTools and value was showed correctly) but for some reason it is not being displayed.
The parent component contains the following code related to this part:
const INITIAL_SELECTED_TAXONOMY = null;
const [selectedTaxonomy, setSelectedTaxonomy] = useState(INITIAL_SELECTED_TAXONOMY);
const handleTaxonomyChange = (e) => setSelectedTaxonomy(e.target.value);
useEffect(() => {
getTaxonomies();
}, []);
useEffect(() => {
if (viewTypeOnlyView) {
handleStageChange(1);
handleDialogTitleChange('Конструктор КС. Режим просмотра');
}
if (viewTypeEdit) {
handleDialogTitleChange('Конструктор КС. Режим редактирования');
}
if (viewTypeCopy) {
handleDialogTitleChange('Конструктор КС. Дублирование КС');
}
if (defaultData) {
if (defaultData.name) setName(defaultData.name);
if (defaultData.taxonomy) setSelectedTaxonomy(defaultData.taxonomy);
// if (defaultData.entryPoints) setSelectedEntryPoints(defaultData.entryPoints);
if (defaultData.entryPoints) {
getEntryPointDescsFn('4.1', defaultData.entryPoints);
}
if (defaultData.message) setMessage(defaultData.message);
}
}, [viewType]);
ViewType is a prop that is being passed to this component and calling those methods in order to fill up the state with predefined values.
And the TaxonomySelector component inside the return statement:
<TaxonomySelector
taxonomies={taxonomies}
isDisabled={currentStage === 1}
selectedTaxonomy={selectedTaxonomy}
handleTaxonomyChange={handleTaxonomyChange} />
At first I thought that the issue could be related to how the component is being rendered and maybe it renders before that data pre-fill useEffect hook is being triggered. However, it seems that other elements, like the ones with string values name and message are being correctly filled out with no issues. Seems like that the issue is specifically related to Select elements. Could you let me know what could it possibly be?
Looks like disabled prop in FormControl is true.
For debug set disabled prop false
It is console logging the right array out all the time, but the point here is that it should be outputting that in the 'TodoList.tsx'. Not sure how to get that fixed in this case. Anyone who could help me with this. To see the bigger picture, please click on this link:
Link to codesandbox todo
I want the returned value from App.js currentFilter function pass it to TodoListItem.js, so it will update the map function constantly when user clicks on filter buttons.
// TodoFilter
import React from 'react';
interface TodoListFilter {
currentFilter: CurrentFilter;
}
export const TodoFilter: React.FC<TodoListFilter> = ({ currentFilter }) => {
return (
<ul>
Filter
<li onClick={() => currentFilter('All')}>All</li>
<li onClick={() => currentFilter('Complete')}>Completed</li>
<li onClick={() => currentFilter('Incomplete')}>Incompleted</li>
</ul>
)
}
// App.js
const currentFilter: CurrentFilter = filterTodo => {
let activeFilter = filterTodo;
switch (activeFilter) {
case 'All':
return todos;
case 'Complete':
return todos.filter(t => t.complete);
case 'Incomplete':
return todos.filter(t => !t.complete);
default:
console.log('Default');
}
}
return (
<React.Fragment>
<TodoList
todos={todos}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
editTodo={editTodo}
saveEditedTodo={saveEditedTodo}
getEditText={getEditText}
/>
<TodoFilter currentFilter={currentFilter}/>
<AddTodoForm addTodo={addTodo}/>
</React.Fragment>
)
// TodoListItem
import React from 'react';
import { TodoListItem } from "./TodoListItems";
interface TodoListProps {
todos: Array<Todo>;
toggleTodo: ToggleTodo;
deleteTodo: DeleteTodo;
editTodo: EditTodo;
getEditText: GetEditText;
saveEditedTodo: SaveEditedTodo;
currentFilter: CurrentFilter;
}
export const TodoList: React.FC<TodoListProps> = ({ todos, toggleTodo, deleteTodo, editTodo, getEditText, saveEditedTodo, currentFilter }) => {
return (
<ul>
{todos.map((todo, i) => {
return <TodoListItem key={i}
todo={todo}
toggleTodo={toggleTodo}
deleteTodo={deleteTodo}
editTodo={editTodo}
saveEditedTodo={saveEditedTodo}
getEditText={getEditText}
/>
})}
</ul>
)
}
//Folder structure
src
-App.tsx
-AddTodoForm.tsx
-TodoFilter.tsx
-TodoList.tsx
The reason why the list not updating is that currentFilter passed as a prop to TodoList component is not used there at all.
Please consider two ways of solving it:
Pass a full list + filter object and apply filter inside TodoList
Apply filter object on the list at App component level and pass filtered list to TodoList component.
Personally I would go with the second approach but it's up to you :)
You need to create two arrays.One is original and second is filtered like this in your example.
const [todos, setTodos] = useState(initialTodos);
const [filtered, setFiltered] = useState(initialTodos);
Now you need to send filtered array in list component.Any updation or deletion you have to make on your todos array.And in currentFilter,you have to filter from original array that is todos and set it to filtered array in like this:
useEffect(() => {
setFiltered(todos);
}, [todos]);
Link of forked sandbox : link
Let me know if this helps you.
I have mapped list of data from JSON. When I clicked on of the item it should open a crawl with additional details from the same JSON file. I am able to map everything one I clicked bit I was not able to toggle. How do I do toggling.
This is my render method
render() {
return (
<div>
<h1>API</h1>
<div>
{this.state.apis.map(api => (
<div
key={api.id}
id={api.id}
onClick={this.handleCrawl}>
{api.title}
</div>
))}
</div>
<div>
{this.state.apis.map(api => (
<div
key={api.id}
id={api.id}>
{this.state.showCrawl[api.id] && (
<SwaggerUI url={api.opening_crawl}/>
)}
</div>
))}
</div>
</div>
);
}
This is the method for toggling. When I clicked an item the SwaggerUI component shows up and If I clicked the same link it hides.
The problem is if I clicked the 2nd link 1st link still shows. I need other view to be closed.
handleCrawl = e => {
const { id } = e.target;
this.setState(current => ({
showCrawl: { ...current.showCrawl, [id]: !current.showCrawl[id] }
}));
};
just don't spread the previous state's props.
try this:
handleCrawl = e => {
const { id } = e.target;
this.setState(current => ({
showCrawl: { [id]: !current.showCrawl[id] }
}));
};
Because in your code:
initial state:
{showCrawl: {}}
Say first time you click the first one(id: 1), your state become:
{showCrawl: {1: true}}
then u click the second one(id: 2)
{showCrawl: {1: true, 2: true}}
That's not your expected. Right?
So just don't spread the property, it should be going well.
In general, you can show or hide an element in a react component like this:
{this.state.showComponent ? (<Component/>) : (null)}
as an alternative, you can control the hiding/showing of the element in the component itself, with a show prop:
<Component show={this.state.showComponent} />
-- edit
I think I misunderstood your problem. Your problem is that you only want SwaggerUI to show for one thing at a time, but it's showing for multiple.
This is because of the way you designed your function,
handleCrawl = e => {
const { id } = e.target;
this.setState(current => ({
showCrawl: { ...current.showCrawl, [id]: !current.showCrawl[id] }
}));
};
You're only ever ADDING ids to showCrawl, not changing the ids that you toggled previously. You'll have to fix that function
I'd like to know what's the best pattern to use in the following use case:
I have a list of items in my ItemList.js
const itemList = items.map((i) => <Item key={i}></Item>);
return (
<div>{itemList}</div>
)
Each of this Items has an 'EDIT' button which should open a dialog in order to edit the item.
Where should I put the Dialog code?
In my ItemList.js => making my Item.js call the props methods to open the dialog (how do let the Dialog know which Item was clicked? Maybe with Redux save the id of the item inside the STORE and fetch it from there?)
In my Item.js => in this way each item would have its own Dialog
p.s. the number of items is limited, assume it's a value between 5 and 15.
You got a plenty of options to choose from:
Using React 16 portals
This option let you render your <Dialog> anywhere you want in DOM, but still as a child in ReactDOM, thus maintaining possibility to control and easily pass props from your <EditableItem> component.
Place <Dialog> anywhere and listen for special app state property, if you use Redux for example you can create it, place actions to change it in <EditableItem> and connect.
Use react context to send actions directly to Dialog, placed on top or wherever.
Personally, i'd choose first option.
You can have your <Dialog/> as separate component inside application's components tree and let it to be displayed in a case if your application's state contains some property that will mean "we need to edit item with such id". Then into your <Item/> you can just have onClick handler that will update this property with own id, it will lead to state update and hence <Dialog/> will be shown.
UPDATED to better answer the question and more completely tackle the problem. Also, followed the suggestion by Pavlo Zhukov in the comment below: instead of using a function that returns functions, use an inline function.
I think the short answer is: The dialog code should be put alongside the list. At least, this is what makes sense to me. It doesn't sound good to put one dialog inside each item.
If you want to have a single Dialog component, you can do something like:
import React, { useState } from "react";
import "./styles.css";
const items = [
{ _id: "1", text: "first item" },
{ _id: "2", text: "second item" },
{ _id: "3", text: "third item" },
{ _id: "4", text: "fourth item" }
];
const Item = ({ data, onEdit, key }) => {
return (
<div key={key}>
{" "}
{data._id}. {data.text}{" "}
<button type="button" onClick={onEdit}>
edit
</button>
</div>
);
};
const Dialog = ({ open, item, onClose }) => {
return (
<div>
<div> Dialog state: {open ? "opened" : "closed"} </div>
<div> Dialog item: {JSON.stringify(item)} </div>
{open && (
<button type="button" onClick={onClose}>
Close dialog
</button>
)}
</div>
);
};
export default function App() {
const [isDialogOpen, setDialogOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState(null);
const openEditDialog = (item) => {
setSelectedItem(item);
setDialogOpen(true);
};
const closeEditDialog = () => {
setDialogOpen(false);
setSelectedItem(null);
};
const itemList = items.map((i) => (
<Item key={i._id} onEdit={() => openEditDialog(i)} data={i} />
));
return (
<>
{itemList}
<br />
<br />
<Dialog
open={isDialogOpen}
item={selectedItem}
onClose={closeEditDialog}
/>
</>
);
}
(or check it directly on this CodeSandbox)