Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

React, form input stops working after opening and closing custom drop down

I have created a form with a custom drop down component, and I’ve spent the day trying to find out what is causing this.

I’ve tried using useRef and use defaultValue SO answers.

The ‘Menu Name’ text input can be changed multiple times until I click my <DropDownComponent/>.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

After I selected a few days, and close the popup, my input becomes unusable.

Availability Component

After opening and closing the popup, I lose the ability to interact with the text input

Why is this happening? I feel like <input onChange=()=>{}/> is somehow being lost on the DOM possibly?

MenuForm.tsx

import { ChangeEvent, useEffect,  useState } from "react";
import { .......... } from '../../../styling/styled-components';
import DropDownComponent from "../../UI/DropDownComponent/DropDownComponent";
import useImgUpload from "../../../hooks/useImgUpload";
import Icon from "../../../assets/icons/Icons";

const MenuForm = () => {
  const {imgUrl, uploadFn} = useImgUpload();
  const INPUT_LIST = ["Mon", "Tue", "Wed", "Thur", "Fri", "Sat", "Sun"];
  const [nameInput, setNameInput] = useState<string>('');
  const [availableDays, setAvailableDays ] = useState<string[]>([]);

  useEffect(() => {
   console.log(availableDays);
  },[availableDays])

  const anyTimeCheckBox = (event: ChangeEvent<HTMLInputElement>) => {
    console.log(event.target.checked);
    console.log(event.target.id);
  }

  const showUploadedImg = (event: ChangeEvent<HTMLInputElement>) => {
    console.log(event);
    uploadFn(event);
  }

  const submitHandler = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    console.log(`${nameInput}, ${availableDays}`);
  }

  const selectedDayHandler = (arr:string[]) => {
    setAvailableDays( arr );
    console.log('Days Set');
  }

  const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
      setNameInput((currState:string) => {return currState = e.target.value});
  }

  console.log('Menu Form rendered');

  return(
    <MenuFormStyle onSubmit={submitHandler}>

      <MenuImgWrapper>
        <ImgInput
          type='file'
          alt='Add Image'
          accept="image/png, image/jpeg"  
          onChange={showUploadedImg}
        />
        <ImgDisplay>
          <LoadedImgCont>
            {
              imgUrl 
              ?
              <LoadedImg src={imgUrl} alt='uploaded-img'></LoadedImg>
              : 
              <>
                <NoImg src={Icon.AddImage} alt='icon-placeholder'></NoImg>
                <NoImgTxt>Add Image</NoImgTxt>
              </>
            }
          </LoadedImgCont>
        </ImgDisplay>
        
      </MenuImgWrapper>

      <MenuNameInput
        type='text' 
        placeholder= 'Menu name'
        name='menuName'
        onChange={handleInputChange}
        defaultValue={nameInput}
      />

      {/* Day + Time selections */}
      <DropDownComponent list={INPUT_LIST} setSelectedDays={selectedDayHandler}/>
      
      <TimeStartInput
        type='time'
        placeholder='Start'
      />

      <TimeEndInput 
        type='time'
        placeholder="End"
      />

      <TimeCheckBoxWpr>
        <TimeCheckBox
          type='checkbox'
          name='24/7'
          id='isAlwaysAvailable'
          onChange={anyTimeCheckBox}/>
        <label htmlFor='24/7'>Anytime</label>
      </TimeCheckBoxWpr>

      {/*Extra Settings */}
      <FieldSetMenu>
        <legend>Menu Settings:</legend>
        <div>
          <input
            type='checkbox'
            name='apples'
            id='apples'
            />
            <label htmlFor='apples'>Hide menu</label>
        </div>
      </FieldSetMenu>

      <MenuSaveButton>Save</MenuSaveButton>

    </MenuFormStyle>
  );
}

export default MenuForm;

DropDownComponent

import React, { useEffect, useMemo, useRef, useState } from "react";
import Icon from "../../../assets/icons/Icons";

import {
  ArrowDownIcon,
  CloseIcon,
  CustomListItem,
  DaySelectInput,
  DaySelectPopUp,
  DisplaySelectedDays,
  SelectedText,
  UList,
} from "../../../styling/styled-components";

interface IProps {
  list: string[];
  setSelectedDays: (arr: string[]) => void;
  multiSelect?: boolean;
}

interface ListProps {
  list: DropDownProps[];
  updateSelection: (e: React.MouseEvent<HTMLElement>) => void;
  closeDropDown: () => void;
}

interface DropDownProps {
  value: string;
  selected: boolean;
}

const DropDownComponent = (props: IProps) => {
  const [displayDropDown, setDisplayDropDown] = useState(false);
  const [selectedItems, setSelectedItems] = useState<string[]>([]);
  const [customListItems, setCustomListItems] = useState<DropDownProps[]>();
  const { list, setSelectedDays } = props;

  useMemo(() => {
    let buildListArray: DropDownProps[] = list.reduce(
      (previous, current) => [...previous, { value: current, selected: false }],
      [{ value: "Reset", selected: false }]
    );
    setCustomListItems((currState) => {
      return (currState = buildListArray);
    });
    console.log("DropDownComponent");
  }, []);

  useEffect(() => {
    setSelectedDays(selectedItems);
  },[selectedItems, setSelectedDays]);

  const captureSelected = (event: any) => {
    //updates state of custom drop down menu

    let dropDownItem = customListItems![event.target.value];
    dropDownItem.selected = !dropDownItem.selected;
    let updatedSelectState = dropDownItem.selected;

    if (updatedSelectState && dropDownItem.value === "Reset") {
      console.log('Reset Triggered');
      setSelectedItems([]);
      customListItems?.forEach((item) => {
        item.selected = false;
      });
      return;
    } else if (updatedSelectState && !existsInList()) {
      console.log('Added to SelectedItems');
      setSelectedItems((currState) => {
        return [...currState, dropDownItem.value];
      });
    } else if (!updatedSelectState && existsInList()) {
      console.log('Removed from selected items');
      let updatedArray = selectedItems.filter(
        (item) => item !== dropDownItem.value
      );
      setSelectedItems(updatedArray);
    }

    function existsInList(): boolean {
      return selectedItems.includes(dropDownItem.value);
    }
  };

  const displaySelection = (items: string[]): string => {
    return items.join(", ");
  };

  const toggleDropDown = () => {
    console.log(selectedItems);
    setDisplayDropDown(currState => !currState);
    console.log('toggle');
  };

  return (
    <DaySelectInput>
      <DisplaySelectedDays>
        <SelectedText>
          { 
            !selectedItems.length 
            ? 
            "Availability" 
            : 
            displaySelection(selectedItems) 
          }
        </SelectedText>

        {
          displayDropDown 
          ? 
          <CloseIcon src={Icon.Close} alt='dummy-icon'/>
          :
          <ArrowDownIcon 
            src={ Icon.ArrowDown } 
            alt='icon-down' 
            onClick={toggleDropDown}
            />
        }
        
        </DisplaySelectedDays>

        {
          displayDropDown 
          && 
          <DropDownList
              list={customListItems!}
              updateSelection={captureSelected}
              closeDropDown={toggleDropDown}
          />
        }
    </DaySelectInput>
  );
};

const DropDownList = (props: ListProps) => {
  const elRef = useRef<HTMLDivElement | null>(null);
  const { list, closeDropDown } = props;

  useEffect(() => {
    console.log('ClickOutside Effect')
    document.addEventListener("mousedown", handleClickOutside);
    return () => {
      // Unbind the event listener on clean up
      document.removeEventListener("mousedown", handleClickOutside);
    };

    function handleClickOutside(event: any) {
      event.preventDefault();
      if (elRef.current && !elRef.current?.contains(event.target)) {
        closeDropDown();
      }
    }
  }, [elRef, closeDropDown]);

  return (
    <DaySelectPopUp ref={elRef}>
      <UList>
        {list?.map((listItem: DropDownProps, index:number) => {
          return (
            <CustomListItem
              key={`li-${listItem.value}`}
              value={index}
              onClick={props.updateSelection}
              userSelected={listItem.selected}
            >
              {listItem.value}
            </CustomListItem>
          );
        })}
      </UList>
    </DaySelectPopUp>
  );
};
export default DropDownComponent;

styled components file incase you want to try for yourself

import styled from 'styled-components/macro';

const primaryMain = '#003332';
const primaryLight = '#305d5b';
const primaryDark = '#000e0a';

const baseBackground = '#EBE9E9';

const secondaryMain = '#003332';
const secondaryLight = '#305d5b';
const secondaryDark = '#000e0a';

const textLight = 'white';
const textDark = 'black';

//Texts
const StandardText = styled.p` 
  color: ${textLight};
`;

//UI
const BaseCard = styled.div` 
  display: inline-flex;
  justify-content: center;
  align-items: center;
  background: ${baseBackground};
  padding: 5px; 
  margin: 5px;
`;

const GreenCard = styled(BaseCard)`
  height: 50px;
  margin: 5px 0;
  border-radius: 5px;
  background: ${primaryMain};
`

//NavigationComponent
const NavBackDrop = styled.div`
  width: 100%;
  background: ${primaryDark};
  padding: 10px 0px 5px 0px;
`;
const NavWrapper = styled.div`
  width: 100%;
  background: ${primaryMain};
  padding: 5px 0px;

  display: inline-flex;
  align-items: center;
`;

const NavLogoWrapper = styled.div` 
  width: 50px;
  height: 40px;
  background: ${primaryLight};
  margin-left: 10px;
`;

const NavLinkWrapper = styled.div` 
  width: 100%;
  display: inline-flex;
  justify-content: flex-start;
  align-items: center;

  & > a {
    min-width: 100px;
    height: fit-content;
    text-align: center;
    margin: 0px 10px;
    color: white;
    text-decoration: none;
    cursor: pointer;
  }

  & > a.active {
    background: ${primaryLight};
    padding: 0px 10px;
    border-radius: 5px;
    max-width: 100px;

    & > p {
      color: white;
      font-weight: 700;
      transform: scale(1.05);
      margin: 10px 0px;
    }
  }

`;

const ProfileImgWrapper = styled.div` 
  width: 40px;
  height: 40px;
  background: ${primaryLight};
  border-radius: 50%;
  float: right;
  margin-right: 10px;
`;

const DashBoardGrid = styled.div` 
  min-height: 80vh;
  height: fit-content;
  display: grid;
  grid-template-columns: 30vw 30vw 35vw;
  grid-gap: 10px;
  padding: 10px;
  justify-content: center;

  & > div {
    border-radius: 5px;
    box-shadow: 1px 1px 2px 1px #00000038;
  }
`;

const DashSalesGridWrapper = styled(BaseCard)` 
  grid-column: 1 / span 2;
`;

const DashMenuWrapper = styled(BaseCard)` 
  grid-column: 3 ;
  grid-row: 1 / span 2;
  display: grid;
  grid-template-rows: 50px;
  grid-template-columns: 100%;
`;

const DashKpiWrapper = styled(BaseCard)`
  grid-column: 1 / span 2 ;
  grid-row: 2 ;
`;

const MenuWrapper = styled.div` 
  width: 100vw;
  display: inline-flex;
  justify-content: space-around;
`

const Grid = styled(BaseCard)` 
  width: 45vw;
  display: grid;
  grid-template-columns: 100%;
  grid-auto-rows: minmax(50px, auto);
  align-items: flex-start;
  border-radius: 3px;
  height: fit-content;
  box-shadow: 1px 1px 2px 1px #00000054;
`;

const CreateNewButton = styled.button`
  height: 50px;
  border: 3px solid ${primaryLight};
  border-radius: 5px;
  background: white;
  cursor: pointer;
  display: inline-flex;
  justify-content: center;
  align-items: center;

  &:hover{
    transform: scale(1.01);
    box-shadow: 1px 1px 2px 1px #00000054;
    font-size: 0.9rem;

    &>img{
      transform: scale(1.2);
    }
  }
`;

const IconImg = styled.img`
  width: 30px;
  height: 30px;
  margin-right: 5px;
`;

const CustomListItem = styled.li<{userSelected:boolean}>`
  color: ${ props =>  props.userSelected ? 'white': 'none'};
  background: ${ props =>  props.userSelected ? primaryLight : 'none'};
  cursor: pointer;
  min-width: 28%;
  margin: 2px;
  border-radius: 5px;
  border: 3px solid ${primaryLight};
  font-weight: 500;
  text-align: center;

  &:first-of-type {
    width: 100%;
  }

  &:hover{
    background: white;
    color: black;
  }
`;

const DaySelectInput = styled.div` 
  grid-column: 3/ span 2;
  grid-row: 2;
  position: relative;
`;

const DaySelectPopUp = styled.div` 
  width: 100%;
  position: absolute;
  top: 100%;
  background: #d6d9d9;
  border: 2px solid ${primaryLight};
  padding: 5px;
  margin-top: 3px;
`;

const UList = styled.ul` 
    display: flex;
    flex-flow: wrap;
    list-style-type: none;
    padding: 0px;
    justify-content: flex-start;
`;

const MenuFormStyle = styled.form` 
    display: grid;
    justify-content: center;
    align-items: flex-end;
    grid-template-columns: 27% 27% 20% 20%;
    grid-gap: 5px;

    color: #444;
    border: 3px solid #305d5b;
    border-radius: 10px;
    padding: 10px;
    margin-top: 2px;
  `;

  const MenuImgWrapper = styled.div` 
    grid-column: 1/span 2;
    grid-row: 1/ span 2;
    height: 200px;
    width: 200px;
    position: relative;
  `;

  const ImgInput = styled.input` 
    width: 100%;
    height: 100%;
    opacity: 0;
    z-index: 1;
    position: absolute;
  `;

  const ImgDisplay = styled.div`
    width: 100%;
    height: 100%;
    position: absolute;
    top: 0;
    z-index: 0;
    border: 3px solid #003332;
    background: #d6d9d9;
  `;

  const MenuNameInput = styled.input` 
    grid-column: 3/span 2;
    grid-row: 1;
    min-height: 30px;
    border-radius: 5px;
    border: 3px solid ${primaryLight};
  `;

  const TimeStartInput = styled.input` 
    grid-column: 3;
    grid-row: 3;
  `;
  const TimeEndInput = styled.input` 
    grid-column: 4;
    grid-row: 3;
`;

  const TimeCheckBox = styled.input` 
    border-radius: 50%;
  `;

  const TimeCheckBoxWpr = styled.div` 
    grid-column: 4;
    grid-row: 4;
    grid-column: 2;
    grid-row: 3;
    margin-left: auto;
    width: fit-content;
  `;

  const FieldSetMenu = styled.fieldset` 
    grid-row: 5/span 2;
    grid-column: 1/span 4;
  `;

  const MenuSaveButton = styled.button` 
    grid-row: 9;
    grid-column: 4;
  `;

  const DisplaySelectedDays = styled.div` 
    width: 100%;
    border: 2px solid ${primaryLight};
    padding: 5px;
  `;

  const SelectedText = styled.span` 
    margin: 0px;
  `;

  const LoadedImg = styled.img` 
    width: 80%;
    height:auto;
  `;

  const NoImg = styled(LoadedImg)`
    width: 40px;
    margin-right: 5px;
  `;

  const NoImgTxt = styled.p` 
    font-size: 20px;
  `;

  const LoadedImgCont = styled.div`
    width: 100%;
    height: 100%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  `;

  const ArrowDownIcon = styled.img` 
    cursor: hover;
    float: right;
    width: fit-content;
  `;

  const CloseIcon = styled(ArrowDownIcon)``;



export {
  ArrowDownIcon,
  BaseCard,
  CloseIcon,
  CreateNewButton,
  CustomListItem,
  DashBoardGrid,
  DashKpiWrapper,
  DashMenuWrapper,
  DashSalesGridWrapper,
  DaySelectInput,
  DaySelectPopUp,
  DisplaySelectedDays,
  FieldSetMenu,
  GreenCard,
  Grid,
  IconImg,
  ImgInput,
  ImgDisplay,
  LoadedImg,
  LoadedImgCont,
  MenuFormStyle,
  MenuImgWrapper,
  MenuNameInput,
  MenuSaveButton,
  MenuWrapper,
  NavBackDrop,
  NavWrapper,
  NavLogoWrapper,
  NavLinkWrapper,
  NoImg,
  NoImgTxt,
  ProfileImgWrapper,
  SelectedText,
  StandardText,
  TimeCheckBoxWpr,
  TimeEndInput,
  TimeStartInput,
  TimeCheckBox,
  UList,
}

>Solution :

In DropDownList you register some click outside listeners, but this happens in a useMemo when it should be a useEffect. Therefore the click handler is never removed when the popup closes which probably makes everything unelectable as preventDefault is called on every click.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading