FE-SPRINT-REACT-CUSTOM-COMPONENT

leekoby·2023년 2월 22일
0

CodeStates

목록 보기
1/6
post-thumbnail

📌들어가기에 앞서

해당 포스트는 CodeStates 과정중의 내용을 복습하며 정리한 내용입니다.


📖 Modal


🔑 Modal Component


Open Modal 버튼을 클릭하면 Opened!로 바뀌면서 모달창이 화면에 나타나야 한다. Modal UI 컴포넌트는 기존의 브라우저 페이지 위에 새로운 윈도우 창이 아닌, 레이어를 까는 것을 말한다. 모달창을 구현하면서 CSS의 positionstopPropagation에 대해 익힐 수 있었다. 부모 컴포넌트에 이벤트 핸들러가 걸려있을 때 자식컴포넌트에도 같은 핸들러가 작동이되는데, 이때 자식 컴포넌트에서는 작동을 안하게 해주려면 그 해당 이벤트 핸들러에 stopPropagation()를 활용해주면 된다.


📚 레퍼런스


버블링과 캡처링 | 모던 JavaScript 튜토리얼

이벤트 버블링, 이벤트 캡처 그리고 이벤트 위임까지 | 캡틴판교 블로그

Event.stopPropagation() | MDN


🔎 소스코드


import { useState } from 'react';
import styled from 'styled-components';

export const ModalContainer = styled.div`
  // TODO : Modal을 구현하는데 전체적으로 필요한 CSS를 구현합니다.
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  position : relative;
`;

export const ModalBackdrop = styled.div`
  // TODO : Modal이 떴을 때의 배경을 깔아주는 CSS를 구현합니다.
  position: fixed;
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  top:0;
  left:0;
  right:0;
  bottom:0;
  margin:0;
  background-color: rgba(0,0,0,0.4);
  z-index:1;
`;

export const ModalBtn = styled.button`
  background-color: var(--coz-purple-600);
  text-decoration: none;
  border: none;
  padding: 20px;
  color: white;
  border-radius: 30px;
  cursor: grab;
`;

export const ModalView = styled.div.attrs((props) => ({
  // attrs 메소드를 이용해서 아래와 같이 div 엘리먼트에 속성을 추가할 수 있습니다.
  role: 'dialog',
}))`
  // TODO : Modal창 CSS를 구현합니다.
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  border-radius: 1rem;
  background-color: #fff;
  width: 500px;
  height: 200px;
  border: 3px solid lightblue;
  >.close-btn{
    position: absolute;
    top:2px;
    right:7px;
    cursor: pointer;
  }
 
`;
export const Modal = () => {
  const [isOpen, setIsOpen] = useState(false);

  const openModalHandler = (e) => {
    // TODO : isOpen의 상태를 변경하는 메소드를 구현합니다.
    setIsOpen(!isOpen)
  };

  return (
    <>
      <ModalContainer>
        <ModalBtn onClick={openModalHandler}
        // TODO : 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
        >
          {isOpen ? 'Opened' : 'Open Modal'}
          {/* open, close condition */}

          {/* TODO : 조건부 렌더링을 활용해서 Modal이 
          열린 상태(isOpen이 true인 상태)일 때는 
          ModalBtn의 내부 텍스트가 'Opened!' 로 Modal이 
          닫힌 상태(isOpen이 false인 상태)일 때는 
          ModalBtn 의 내부 텍스트가 'Open Modal'이 되도록 구현해야 합니다. */}
        </ModalBtn>

        {isOpen === false ? null :
          <ModalBackdrop onClick={openModalHandler}>
            <ModalView onClick={e => e.stopPropagation()}>
              <div className='close-btn' onClick={openModalHandler}>&times;</div>
              <div>Hello</div>
              <div>World</div>
            </ModalView>
          </ModalBackdrop>
        }
        {/* TODO : 조건부 렌더링을 활용해서 Modal이 
        열린 상태(isOpen이 true인 상태)일 때만 모달창과 배경이 뜰 수 있게 구현해야 합니다. */}
      </ModalContainer>
    </>
  );
};

📖 Toggle


🔑 Toggle Component


두 가지 상태만 가지고 있는 스위치이다. 이 부분에서 transition과 CSS 선택자에 대해 많이 알아보게 되었다.
linear-gradient를 사용하면 배경이 한쪽에서 부터 점점 채워지는 느낌으로 구현할 수 있다고 해서 보다 자연스러운 모션을 위해 추가 했다. isOn 상태를 활용해서 className을 변경해주는 방식으로 CSS를 적용해줘서 토글 스위치가 움직이는 것을 구현할 수 있었다.


📚 레퍼런스


CSS 트랜지션 사용하기 | MDN

2.14 CSS3 Transition | poiemaweb

Transition | TCPSCHOOL

CSS 선택자 | MDN


🔎 소스코드


import { useState } from 'react';
import styled from 'styled-components';

const ToggleContainer = styled.div`
  position: relative;
  margin-top: 8rem;
  left: 47%;
  cursor: pointer;

  > .toggle-container {
    width: 50px;
    height: 24px;
    border-radius: 30px;
    background-color: #8b8b8b;
    // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.
    background-position: right;
      background: linear-gradient(to left, #8b8b8b 50%, blue 50%) right;
      background-size: 200%;
      transition: 1s;
    &.toggle--checked{
      background-position: left;
      background: linear-gradient(to right, blue 50%, #8b8b8b 50%) left;
      background-size: 200%;
      transition: 1s;
    }
  }

  > .toggle-circle {
    position: absolute;
    top: 1px;
    left: 1px;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background-color: #ffffff;
    // TODO : .toggle--checked 클래스가 활성화 되었을 경우의 CSS를 구현합니다.

    transition: 0.5s;
    &.toggle--checked {
    left: 27px;
    transition: 0.5s;
    }


  }
`;

const Desc = styled.div`
  // TODO : 설명 부분의 CSS를 구현합니다.
  display: flex;
  justify-content: center;
  margin-top: 0.5rem;
`;

export const Toggle = () => {
  const [isOn, setIsOn] = useState(false);

  const toggleHandler = () => {
    // TODO : isOn의 상태를 변경하는 메소드를 구현합니다.
    setIsOn(!isOn)
  };

  return (
    <>
      <ToggleContainer
        // TODO : 클릭하면 토글이 켜진 상태(isOn)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
        onClick={toggleHandler}>
        {/* TODO : 아래에 div 엘리먼트 2개가 있습니다. 각각의 클래스를 'toggle-container', 'toggle-circle' 로 지정하세요. */}
        {/* TIP : Toggle Switch가 ON인 상태일 경우에만 toggle--checked 클래스를 div 엘리먼트 2개에 모두 추가합니다. 조건부 스타일링을 활용하세요. */}
        <div className={`toggle-container ${isOn ? 'toggle--checked' : null}`} />
        <div />
        <div className={`toggle-circle ${isOn ? 'toggle--checked' : null}`} />
        <div />
      </ToggleContainer>
      {/* TODO : Desc 컴포넌트를 활용해야 합니다. */}
      {/* TIP:  Toggle Switch가 ON인 상태일 경우에 Desc 컴포넌트 내부의 텍스트를 'Toggle Switch ON'으로, 그렇지 않은 경우 'Toggle Switch OFF'가 됩니다. 조건부 렌더링을 활용하세요. */}
      {isOn ?
        <Desc><div className='switch--On'>Toggle Switch On</div></Desc> :
        <Desc><div className='switch--Off'>Toggle Switch Off</div></Desc>
      }
    </>
  );
};

📖 Tab


🔑 Tab Component


Tab UI 컴포넌트는 동일한 메뉴 라인에서 뷰를 전환할 때 사용한다. 이 부분에서는 map함수의 두번째 인자로 index를 넣어서 핸들러 함수에 전달해주는 것이 핵심이다.
li 엘리먼트를 map 함수로 menu의 갯수만큼 만들어준다. 이때 인덱스를 두번째 인자로 넣어서 onClick 핸들러 함수에 index를 전달해준다. 그리고 인덱스를 전달받은 함수를 통해서 currentTab 상태를 해당 인덱스로 바꿔주면서 클릭된 Tab의 className을 바꿔줌으로 인해서 클릭 된 메뉴의 CSS 속성만 바꿔 선택되었음을 시각화 시켜줄 수 있게된다.


📚 레퍼런스


🔎 소스코드


import { useState } from 'react';
import styled from 'styled-components';

// TODO: Styled-Component 라이브러리를 활용해 TabMenu 와 Desc 컴포넌트의 CSS를 구현합니다.

const TabMenu = styled.ul`
  background-color: #dcdcdc;
  color: rgba(73, 73, 73, 0.5);
  font-weight: bold;
  display: flex;
  flex-direction: row;
  justify-items: center;
  align-items: center;
  list-style: none;
  margin-bottom: 7rem;
  height: 40px;

  .submenu {
    ${'' /* 기본 Tabmenu 에 대한 CSS를 구현합니다. */}

    display: flex;
    justify-content: center;
    flex-grow: 1;
    cursor: pointer;
  }

  .focused {
    ${'' /* 선택된 Tabmenu 에만 적용되는 CSS를 구현합니다.  */}
    background-color: blue;
    color: white;
    height: 100%;
    display: flex;
    align-items: center;
    transition: 0.5s;

  }

  & div.desc {
    text-align: center;
  }
`;

const Desc = styled.div`
  text-align: center;
`;

export const Tab = () => {
  // TIP: Tab Menu 중 현재 어떤 Tab이 선택되어 있는지 확인하기 위한
  // currentTab 상태와 currentTab을 갱신하는 함수가 존재해야 하고, 초기값은 0 입니다.

  const menuArr = [
    { name: 'Tab1', content: 'Tab menu ONE' },
    { name: 'Tab2', content: 'Tab menu TWO' },
    { name: 'Tab3', content: 'Tab menu THREE' },
  ];
  const [currentTab, setCurrentTab] = useState(0)

  const selectMenuHandler = (index) => {
    // TIP: parameter로 현재 선택한 인덱스 값을 전달해야 하며, 이벤트 객체(event)는 쓰지 않습니다
    // TODO : 해당 함수가 실행되면 현재 선택된 Tab Menu 가 갱신되도록 함수를 완성하세요.
    console.log(index)
    setCurrentTab(index);


  };

  return (
    <>
      <div>
        <TabMenu>
          {/*TODO: 아래 하드코딩된 내용 대신에, map을 이용한 반복으로 코드를 수정합니다.*/}
          {/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며, 
                  나머지 2개의 tab은 'submenu' 가 됩니다.*/}

          {menuArr.map((item, key) => {
            return <li key={key}
              className={`${key === currentTab ? 'submenu focused' : 'submenu'}`}
              onClick={() => selectMenuHandler(key)}>{item.name}</li>
          })}

        </TabMenu>
        <Desc>
          {/*TODO: 아래 하드코딩된 내용 대신에, 현재 선택된 메뉴 따른 content를 표시하세요*/}
          <p>{menuArr[currentTab].content}</p>
        </Desc>
      </div>
    </>
  );
};

📖 Tag


🔑 Tag Component


Tag 컴포넌트는 레이블 지정을 통해 구분되는 키워드 집합을 만들 때 자주 사용된다. 당장 이 블로그를 작성하는 곳에서도 Tag를 선택할 수 있었다.

input창에 값을 입력하고 Enter키를 누르면 입력이 되어야 하고, 빈값이나 이미 있는 값을 입력하고 Enter를 치면 입력이 되지 않게 구현하여야 한다. 그리고 x버튼을 누르면 삭제도 가능하도록 구현해야 한다. 이 부분에서는 어떻게 하면 입력이 되고 어떨때는 입력이 안되고, 삭제를 구현하는 로직을 짜는 것이 핵심이었다.
input창에 입력된 값을 trim 해주고있는데, 공백만 입력한 경우에도 입력이 되지않게 해주기 위함이다.


📚 레퍼런스


🔎 소스코드


import { useState } from 'react';
import styled from 'styled-components';

// TODO: Styled-Component 라이브러리를 활용해 여러분만의 tag 를 자유롭게 꾸며 보세요!

export const TagsInput = styled.div`
  margin: 8rem auto;
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
  min-height: 48px;
  width: 480px;
  padding: 0 8px;
  border: 1px solid rgb(214, 216, 218);
  border-radius: 6px;

  > ul {
    display: flex;
    flex-wrap: wrap;
    padding: 0;
    margin: 8px 0 0 0;

    > .tag {
      width: auto;
      height: 32px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      padding: 0 8px;
      font-size: 14px;
      list-style: none;
      border-radius: 6px;
      margin: 0 8px 8px 0;
      background: #4000c7;
        > .tag-close-icon {
        display: block;
        width: 16px;
        height: 16px;
        line-height: 16px;
        text-align: center;
        font-size: 14px;
        margin-left: 8px;
        color: #4000c7;
        border-radius: 50%;
        background: #fff;
        cursor: pointer;
      }
    }
  }

  > input {
    flex: 1;
    border: none;
    height: 46px;
    font-size: 14px;
    padding: 4px 0 0 0;
    :focus {
      outline: transparent;
    }
  }

  &:focus-within {
    border: 1px solid #4000c7;
  }
`;

export const Tag = () => {
  const initialTags = ['CodeStates', 'kimcoding'];

  const [tags, setTags] = useState(initialTags);


  const removeTags = (indexToRemove) => {
    // TODO : 태그를 삭제하는 메소드를 완성하세요.
    setTags(tags.filter(tag => {
      return tag !== tags[indexToRemove]
    }))
  };

  const addTags = (e) => {
    // TODO : tags 배열에 새로운 태그를 추가하는 메소드를 완성하세요.
    // 이 메소드는 태그 추가 외에도 아래 3 가지 기능을 수행할 수 있어야 합니다.
    const value = e.target.value.trim();
    if (e.key === 'Enter' && !tags.includes(value) && value) {
      setTags([...tags, value])
      e.target.value = ''


    } else if (e.key === 'Enter' && !value) {
      e.target.value = ''

    }
    // - 이미 입력되어 있는 태그인지 검사하여 이미 있는 태그라면 추가하지 말기
    // - 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 말기
    // - 태그가 추가되면 input 창 비우기


  };

  return (
    <>
      <TagsInput>
        <ul id="tags">
          {tags.map((tag, index) => (
            <li key={index} className="tag">
              <span className="tag-title">{tag}</span>
              <span className="tag-close-icon"
                onClick={() => removeTags(index)}>&times;
                {/* TODO :  tag-close-icon이 tag-title 오른쪽에 x 로 표시되도록 하고,
                            삭제 아이콘을 click 했을 때 removeTags 메소드가 실행되어야 합니다. */}
              </span>
            </li>
          ))}
        </ul>
        <input
          className="tag-input"
          type="text"
          onKeyUp={(e) => addTags(e)}
          // { /* 키보드의 Enter 키에 의해 addTags 메소드가 실행되어야 합니다. */ }
          placeholder="Press enter to add tags"
        />
      </TagsInput>
    </>
  );
};

📖 AutoCompite


🔑 AutoCompite Component


가장 어려웠던 부분인 오토컴플릿 컴포넌트는 검색창에 input값을 입력하면 밑에 input 값과 유사한 추천 검색 옵션을 보여주는 자동 완성 기능이다. 이 부분에서는 상태를 관리해주는 적절한 로직을 잘 짜주는 것이 핵심이었다.

options를 변경해주는 부분을 useEffect에서 관리하니까 그냥 inputValue가 변할때 마다 알아서 options가 바뀌다보니 편하게 관리해줄 수 있었다. useEffect에서 관리를 해주지 않았다면 inputValue를 변하게 해주는 함수마다 options도 변하게 해주는 함수를 구현해주었어야 할 것이다.


📚 레퍼런스



🔎 소스코드


import { useState, useEffect } from 'react';
import styled from 'styled-components';

const deselectedOptions = [
  'rustic',
  'antique',
  'vinyl',
  'vintage',
  'refurbished',
  '신품',
  '빈티지',
  '중고A급',
  '중고B급',
  '골동품'
];

/* TODO : 아래 CSS를 자유롭게 수정하세요. */
const boxShadow = '0 4px 6px rgb(32 33 36 / 28%)';
const activeBorderRadius = '1rem 1rem 0 0';
const inactiveBorderRadius = '1rem 1rem 1rem 1rem';

export const InputContainer = styled.div`
  margin-top: 8rem;
  background-color: #ffffff;
  display: flex;
  flex-direction: row;
  padding: 1rem;
  border: 1px solid rgb(223, 225, 229);
  border-radius: ${activeBorderRadius};
  z-index: 3;
  box-shadow: 0;

  &:focus-within {
    box-shadow: ${boxShadow};
  }

  > input {
    flex: 1 0 0;
    background-color: transparent;
    border: none;
    margin: 0;
    padding: 0;
    outline: none;
    font-size: 16px;
  }

  > div.delete-button {
    cursor: pointer;
  }
`;

export const DropDownContainer = styled.ul`
  background-color: #ffffff;
  display: block;
  margin-left: auto;
  margin-right: auto;
  list-style-type: none;
  margin-block-start: 0;
  margin-block-end: 0;
  margin-inline-start: 0px;
  margin-inline-end: 0px;
  padding-inline-start: 0px;
  margin-top: -1px;
  padding: 0.5rem 0;
  border: 1px solid rgb(223, 225, 229);
  border-radius: 0 0 1rem 1rem;
  box-shadow: ${boxShadow};
  z-index: 3;

  > li:hover {
    background-color: lightgray;
    } 

  > li {
    padding: 0 1rem;
    &.selected {
      background-color: lightgray;
    }
  }
`;

export const Autocomplete = () => {
  /**
    ** Autocomplete 컴포넌트는 아래 3가지 state가 존재합니다. 필요에 따라서 state를 더 만들 수도 있습니다.
    **- hasText state는 input값의 유무를 확인할 수 있습니다.
    ** - inputValue state는 input값의 상태를 확인할 수 있습니다.
    ** - options state는 input값을 포함하는 autocomplete 추천 항목 리스트를 확인할 수 있습니다.
    */
  const [hasText, setHasText] = useState(false);
  //^ text가 들어오는 input창에 값이 들어왔는지 아닌지, 값의 유무를 갖는 상태
  const [inputValue, setInputValue] = useState('');
  //^  input창에 들어온 값을 갖는 상태

  const [options, setOptions] = useState(deselectedOptions);
  //^ input 값을 포함하는 자동완성 리스트 
  // *useEffect를 아래와 같이 활용할 수도 있습니다.
  const [selected, setSelected] = useState(-1);
  //^키보드로 option 선택할때 필요한 selected상태

  useEffect(() => {
    if (inputValue === '') { //^ 초기값
      setHasText(false); //^ input false 값 없음
      setOptions([]) //^ 값이 없으니 아래 리스트가 안나오게
    } else {
      if (inputValue !== '') {
        setOptions(deselectedOptions.filter(e => {
          return e.includes(inputValue)
        })) //^ 입력된 값을 포함하는 option들만 선택
        setHasText(true); //^ input true 값 있음 
      }
    }
  }, [inputValue]);

  // TODO : input과 dropdown 상태 관리를 위한 handler가 있어야 합니다.
  /**
   ** handleInputChange 함수는
   ** - input값 변경 시 발생되는 change 이벤트 핸들러입니다.
   **- input값과 상태를 연결시킬 수 있게 controlled component로 만들 수 있고
   ** - autocomplete 추천 항목이 dropdown으로 시시각각 변화되어 보여질 수 있도록 상태를 변경합니다.
   **
   * handleInputChange 함수를 완성하여 아래 3가지 기능을 구현합니다.
   **
   * onChange 이벤트 발생 시
   ** 1. input값 상태인 inputValue가 적절하게 변경되어야 합니다.
   ** 2. input값 유무 상태인 hasText가 적절하게 변경되어야 합니다.
   ** 3. autocomplete 추천 항목인 options의 상태가 적절하게 변경되어야 합니다.
   ** Tip : options의 상태에 따라 dropdown으로 보여지는 항목이 달라집니다.
   */
  const handleInputChange = (e) => { //^ input값 변경될때 
    setInputValue(e.target.value); //^ set으로 값 변경해줌.
    setHasText(true) //^ input값이 있으므로 true
  }
  /**
    ** handleDropDownClick 함수는
    ** - autocomplete 추천 항목을 클릭할 때 발생되는 click 이벤트 핸들러입니다.
    ** - dropdown에 제시된 항목을 눌렀을 때, input값이 해당 항목의 값으로 변경되는 기능을 수행합니다.
    **
    ** handleInputChange 함수를 완성하여 아래 기능을 구현합니다.
    **
    ** onClick 이벤트 발생 시
    ** 1. input값 상태인 inputValue가 적절하게 변경되어야 합니다.
    ** 2. autocomplete 추천 항목인 options의 상태가 적절하게 변경되어야 합니다.
    */
  const handleDropDownClick = (clickedOption) => { //^ dropdown에서 사용하면 될거같음 
    setInputValue(clickedOption) //^ 입력값을 inputValue 변경 
  };
  /**
   ** handleDeleteButtonClick 함수는
   ** - input의 오른쪽에 있는 X버튼 클릭 시 발생되는 click 이벤트 핸들러입니다.
   ** - 함수 작성을 완료하여 input값을 한 번에 삭제하는 기능을 구현합니다.
   **
   ** handleDeleteButtonClick 함수를 완성하여 아래 기능을 구현합니다.
   **
   *** onClick 이벤트 발생 시
   ** 1. input값 상태인 inputValue가 빈 문자열이 되어야 합니다.
   */
  const handleDeleteButtonClick = () => {
    setInputValue('') //^ 버튼 누르면 지우기

  };

  //* Advanced Challenge: 상하 화살표 키 입력 시 dropdown 항목을 선택하고, 
  // Enter 키 입력 시 input값을 선택된 dropdown 항목의 값으로 변경하는 handleKeyUp 함수를 만들고,
  //* 적절한 컴포넌트에 onKeyUp 핸들러를 할당합니다. state가 추가로 필요한지 고민하고, 필요 시 state를 추가하여 제작하세요.


  //& SSG
  const handleKeyUp = (event) => {
    //& option을 키보드로 선택할 수 있게해주는 핸들러 함수
    if (hasText) {
      //& input에 값이 있을때
      if (event.key === 'ArrowDown' && options.length - 1 > selected) {
        setSelected(selected + 1);
      }
      //& options.length에 -1을 해주는 이유는 selected의 최대값을 맞춰주기 위해서이다.
      //& 예를들어 밑에 option이 2개가 나왔다고 가정했을 때, selected값이 최대 1까지 변할 수 있게 해줘야한다. 
      //& 'ArrowDown'키를 누르면 selected는 0이 되고, 한번 더 누르면 1이 되고, 그 다음은 더이상 옵션이 없기 때문에 키가 안먹히게 해주는 것이다.

      if (event.key === 'ArrowUp' && selected >= 0) {
        //& 처음 조건을 이해했다면 여기는 자연스럽게 이해될 것이다.
        setSelected(selected - 1);
      }
      if (event.key === 'Enter' && selected >= 0) {
        //& Enter키로 option 선택
        handleDropDownClick(options[selected])
        setSelected(-1);
        //& Enter키를 눌러서 선택이 되면 다시 selected는 -1이 되야한다.
      }
    }
  }
  return (
    <div className='autocomplete-wrapper'>

      {/* TODO : input 엘리먼트를 작성하고 input값(value)을 state와 연결합니다. 
      handleInputChange 함수와 input값 변경 시 호출될 수 있게 연결합니다. */}
      {/* TODO : 아래 div.delete-button 버튼을 누르면 input 값이 삭제되어 dropdown이 없어지는 handler 함수를 작성합니다. */}
      <InputContainer>
        <input type='text'
          value={inputValue}
          defaultValue={inputValue}
          onChange={handleInputChange}
          onKeyUp={handleKeyUp}
        >
        </input>
        <div className='delete-button' onClick={handleDeleteButtonClick}>&times;</div>
      </InputContainer>
      {/* TODO : input 값이 없으면 dropdown이 보이지 않아야 합니다. 조건부 렌더링을 이용해서 구현하세요. */}
      {hasText && <DropDown options={options} handleComboBox={handleDropDownClick} selected={selected} />}
    </div>
  );
};

export const DropDown = ({ options, handleComboBox, selected }) => {
  return (
    <DropDownContainer>
      {/* TODO : input 값에 맞는 autocomplete 선택 옵션이 보여지는 역할을 합니다. */}
      {options.map((item, key) => {
        return (<li
          key={key}
          onClick={() => handleComboBox(item)}
          className={selected === key ? 'selected' : ''}
        >{item}</li>
        )
      })}
    </DropDownContainer>
  );
};

📖 ClickToEdit


🔑 ClickToEdit Component


ClickToEdit 컴포넌트는 input 창을 클릭하면 수정이 가능하고, input 창이 아닌 곳을 클릭하면 수정한 내용이 반영되는 기능을 가진 컴포넌트이다. 이 부분에서는 useRef를 사용해서 input창을 클릭했을 때만 값을 바꿀 수 있게 해주는 것이 핵심이었다.


📚 레퍼런스



🔎 소스코드


import { useEffect, useState, useRef } from 'react';
import styled from 'styled-components';

export const InputBox = styled.div`
  text-align: center;
  display: inline-block;
  width: 150px;
  height: 30px;
  border: 1px #bbb dashed;
  border-radius: 10px;
  margin-left: 1rem;
`;

export const InputEdit = styled.input`
  text-align: center;
  display: inline-block;
  width: 150px;
  height: 30px;
`;

export const InputView = styled.div`
  text-align: center;
  align-items: center;
  margin-top: 3rem;

  div.view {
    margin-top: 3rem;
  }
`;


//^ MyInput 컴포넌트는 ClickToEdit 컴포넌트의 자식 컴포넌트 
//^ value 를 전달 받으며 { value } 에는 { name, age } name의 상태값과 age의 상태값
export const MyInput = ({ value, handleValueChange }) => {
  const inputEl = useRef(null);
  const [isEditMode, setEditMode] = useState(false); //^ edit 모드 상태
  const [newValue, setNewValue] = useState(value);  //^ 출력값 상태

  useEffect(() => {
    if (isEditMode) {   //^ edit 모드가 true면 input창에 포커스가 생겨서 수정이 가능하도록 설정됨.
      inputEl.current.focus();
    }
  }, [isEditMode]);

  useEffect(() => {
    setNewValue(value);
  }, [value]);

  const handleClick = () => { //^ span 태그를 클릭하면 edit모드가 활성화 useEffect에 의해 포커싱 
    // TODO : isEditMode 상태를 변경합니다.

    setEditMode(true)

  };

  const handleBlur = () => { //^ input 창이 아닌 다른 곳을 클릭하면 edit 모드를 비활성화 시킴
    // TODO : Edit가 불가능한 상태로 변경합니다.
    setEditMode(false)
    handleValueChange(newValue); //^ input 창의 입력값을 newValue로 바꿔줌 
  };

  const handleInputChange = (e) => {   // TODO : 저장된 value를 업데이트합니다.
    setNewValue(e.target.value)  //^ input에 입력한 값을  newValue에 할당

  };

  return (
    <InputBox>
      {/*//^ edit 모드가 활성화 되면 input 태그 , 비활성화 되면 span태그  */}
      {isEditMode ? (
        // TODO : 포커스를 잃으면 Edit가 불가능한 상태로 변경되는 메소드가 실행되어야 합니다.
        // TODO : 변경 사항이 감지되면 저장된 value를 업데이트 되는 메소드가 실행되어야 합니다.
        <InputEdit
          type='text'
          value={newValue}
          ref={inputEl}
          onBlur={handleBlur}
          onChange={handleInputChange}
        />
      ) : (
        // TODO : 클릭하면 Edit가 가능한 상태로 변경되어야 합니다.
        <span
          onClick={handleClick}
        >{newValue}</span>
      )}
    </InputBox>
  );
}

const cache = {
  name: '김코딩',
  age: 20
};

export const ClickToEdit = () => {
  const [name, setName] = useState(cache.name);
  const [age, setAge] = useState(cache.age);

  return (
    <>
      <InputView>
        <label>이름</label>
        <MyInput value={name} handleValueChange={(newValue) => setName(newValue)} />
      </InputView>
      <InputView>
        <label>나이</label>
        <MyInput value={age} handleValueChange={(newValue) => setAge(newValue)} />
      </InputView>
      <InputView>
        <div className='view'>이름 {name} 나이 {age}</div>
      </InputView>
    </>
  );
};

0개의 댓글