Home > OS >  is there a way to replace an element (that is inside a .map()) with a custom component, when state c
is there a way to replace an element (that is inside a .map()) with a custom component, when state c

Time:09-06

I'm working on a Todo App, I'd like to replace a particular div element (when a button is clicked) with a form component.

There’s this particular component that renders all my todos. on each todo item it renders, there’s a description and two buttons. one of the buttons is for deleting the particular todo item, the other is for updating it. I don’t have an issue deleting a particular todo. where i have an issue is updating a particular one. When I click the update button, it’s supposed to replace that particular todo item with a custom form component. then I can click update on that form and it’ll update, in theory. Tthe actual problem now is that when I click the update button on a particular todo item, it replaces all the todo items w the custom form component.

const Todos: React.FC<Props> = props => {
const ctx = useContext(TodoContext);

const updateForm = <TodoUpdateForm />;

const editTodo = (value: Todo) => {
ctx?.setClicked(true);

const updatedTodo = ctx?.todos.filter(todo => todo.id === value.id);
console.log(updatedTodo);
};

const completeTodo = (todo: string) => ctx?.completeTodo(todo);
const removeTodo = (todo: string) => ctx?.removeTodo(todo);

return (
<div>
  <AnimatePresence>
    {ctx?.todos.map((todo, index) => {
      return (
        <motion.div
          key={index}
          whileInView={{ x: [-100, 0], opacity: [0, 1] }}
          exit={{ x: [0, 20], opacity: [1, 0] }}
          transition={{
            duration: 1,
            ease: 'easeInOut',
            delayChildren: 0.5,
          }}
          className={`${
            todo.isComplete ? 'todo-row complete' : 'todo-row'
          } app__flex`}
        >
          {ctx.isClicked ? (
            updateForm
          ) : (
            <div key={todo.id} className="todos-item">
              <div
                key={todo.id}
                onClick={() => completeTodo(todo.id)}
                className="app__todo-list-item"
              >
                {todo.text}
              </div>

              <motion.div
                whileInView={{ opacity: [0, 1] }}
                transition={{
                  duration: 2,
                  ease: 'easeInOut',
                  delayChildren: 0.5,
                }}
                className="icons app__flex"
              >
                <RiCloseCircleLine
                  className="delete-icon"
                  onClick={() => removeTodo(todo.id)}
                />
                <TiEdit
                  className="edit-icon"
                  onClick={() => editTodo(todo)}
                />
              </motion.div>
            </div>
          )}
        </motion.div>
      );
    })}
  </AnimatePresence>
</div>
);
};

export default Todos;

the Context Provider

export const TodoContext = React.createContext<TodoContextObj | null>({
todos: [],
edit: { id: '', value: '' },
isClicked: false,
addTodo: (todo: Todo) => {},
removeTodo: (id: string) => {},
completeTodo: (id: string) => {},
updateTodo: (id: string, todo: Todo) => {},
deleteTodos: (key: string) => {},
setClicked: (state: boolean) => {},
});

type Props = { children: React.ReactNode };

const initialValue: InitialState = { id: null, value: '' };

const ContextProvider: React.FC<Props> = props => {
const [todos, setTodos] = useState<Array<Todo>>([]);
const [edit, setEdit] = useState(initialValue);
const [isClicked, setIsClicked] = useState(false);

useEffect(() => {
  const data = localStorage.getItem('todos');

  if (data) setTodos(JSON.parse(data));

  return () => {};
}, []);

const setClicked = (state: boolean) => setIsClicked(state);

const addTodo = (todo: Todo) => {
  // Guard Clause
  if (!todo.text || /^\s*$/.test(todo.text)) return;

  const newTodos = [...todos, todo];

  setTodos(newTodos);
  setIsClicked(false);

localStorage.setItem('todos', JSON.stringify(newTodos));
};

const updateTodo = (id: string, value: Todo) => {
// Guard Clause
if (!value.text || /^\s*$/.test(value.text)) return;

if (edit.id) setEdit({ id: value.id, value: value.text });

localStorage.removeItem('todos');

setTodos(prev => prev.map(item => (item.id === id ? value : item)));
setIsClicked(false);

localStorage.setItem('todos', JSON.stringify(todos));
};

const removeTodo = (id: string) => {
const updatedTodo = [...todos].filter(todo => todo.id !== id);

localStorage.removeItem('todos');
setTodos(updatedTodo);
localStorage.setItem('todos', JSON.stringify(updatedTodo));
};

const completeTodo = (id: string) => {
let updateTodos = todos.map(todo => {
  if (todo.id === id) todo.isComplete = !todo.isComplete;
  return todo;
});

setTodos(updateTodos);
};

const deleteTodos = (key: string) => {
localStorage.removeItem(key);
setTodos([]);
};

const contextValue: TodoContextObj = {
todos,
isClicked,
addTodo,
removeTodo,
completeTodo,
updateTodo,
deleteTodos,
setClicked,
};

return (
<TodoContext.Provider value={contextValue}>
  {props.children}
</TodoContext.Provider>
);
};

export default ContextProvider;

CodePudding user response:

You get this pattern a lot. The simplest solution is just to make a component that represents a single Todo. You can then give that component the editing state.

CodePudding user response:

You are setting a simple bool flag to say if you are in edit mode. This is not enough info for the component to know which todo is in edit mode. You need to change setIsClicked (which i recommend you rename to setEditTodoId or something) to accept todo.id then set this string all the way down. You can type it so null is also allowed.

Then, in each loop, you will check if the current todo.id matches the stored id. If it does, you'd render the form.

  • Related