Modal UI 컴포넌트는 기존의 브라우저 페이지 위에 새로운 윈도우 창이 아닌, 레이어를 까는 것을 말합니다.
- 시작하기
Modal 컴포넌트 구현에 필요한 정보가 자세히 적혀있습니다. 확인 후 과제를 진행하세요.
Modal 컴포넌트 테스트는 주석 처리가 되어있지 않습니다. 바로 진행해주세요.
Modal 컴포넌트는 아래와 같은 state가 존재합니다. 필요에 따라서 state를 더 만들 수도 있습니다.
isOpen state는 모달 창의 열고 닫힘 여부를 확인할 수 있습니다.
ModalBtn 컴포넌트는 모달 창 제어를 위해 핸들러 함수 openModalHandler를 작성합니다.
openModalHandler 함수는
ModalBtn 클릭 시 발생되는 change 이벤트 핸들러입니다.
클릭할 때마다 상태가 Boolean 값으로 변경됩니다.
ModalView 컴포넌트를 작성하고 isOpen 상태가 true 일 경우에만 렌더링합니다.
ModalBackdrop 컴포넌트를 작성하고 isOpen 상태가 true 일 경우에만 렌더링합니다.
- 요구사항
Styled Components 라이브러리를 활용해 ModalContainer ModalBackdrop ModalBtn ModalView 컴포넌트의 CSS를 자유롭게 구현합니다.
ModalContainer : Modal을 구현하는데 필요한 컴포넌트를 감싸주는 컨테이너 컴포넌트 역할을 합니다.
ModalBackdrop : Modal이 떴을 때의 배경을 깔아주는 역할을 합니다.
ModalBtn : Modal 창을 키고 끌 수 있는 버튼입니다.
ModalView : Modal 창 컴포넌트입니다.
구현한 Styled Components들을 Modal 컴포넌트 내부에서 활용합니다.Modal 버튼 기능 테스트
ModalBtn 을 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 변경하는 메서드가 실행되어야 합니다.
조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때만 모달창과 배경이 뜰 수 있게 구현해야 합니다.
조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때는 ModalBtn의 내부 텍스트가 'Opened!' 로 Modal이 닫힌 상태(isOpen이 false인 상태)일 때는 ModalBtn 의 내부 텍스트가 'Open Modal'이 되도록 구현해야 합니다.
//css in Js
import { useState } from 'react';
import styled from 'styled-components';
export const ModalContainer = styled.div`
background-color : white;
display : flex;
width:100%;
height:70%;
justify-content : center;
align-items : center;
`;
export const ModalBackdrop = styled.div`
position : fixed;
width:86%;
height:50%;
border-radius:10px;
background-color :rgba(0,0,0,0.3);
display : flex;
justify-content : center;
align-items : center;
`;
export const ModalBtn = styled.button`
background-color: #FCC8DC;
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',
}))`
border-radius : 10px;
background-color : white;
display : flex;
flex-direction:column;
align-items : center;
width:300px;
height:200px;
>.exitBtn{
font-size : 25px;
margin: 10px 0px 45px 0px;
cursor: pointer;
}
.helloModal{
font-size : 20px;
font-weight: bold;
}
`;
//Js
export const Modal = () => {
const [isOpen, setIsOpen] = useState(false);
const openModalHandler = () => {
// TODO : isOpen의 상태를 변경하는 메소드를 구현합니다.
setIsOpen(!isOpen);
};
return (
<>
<ModalContainer>
<ModalBtn
// TODO : 클릭하면 Modal이 열린 상태(isOpen)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
onClick={openModalHandler}
>
{isOpen === true ? "Opened":"Open Modal" /* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때는 ModalBtn의 내부 텍스트가 'Opened!' 로 Modal이 닫힌 상태(isOpen이 false인 상태)일 때는 ModalBtn 의 내부 텍스트가 'Open Modal'이 되도록 구현해야 합니다. */}
</ModalBtn>
{isOpen === true ?
<ModalBackdrop onClick={openModalHandler}>
<ModalView onClick={(e) => e.stopPropagation()}>
<div className='exitBtn' onClick={openModalHandler}>×</div> //× : X버튼 표현식
<div className='helloModal'>💗Hello Modal💗</div>
</ModalView>
</ModalBackdrop>
:null /* TODO : 조건부 렌더링을 활용해서 Modal이 열린 상태(isOpen이 true인 상태)일 때만 모달창과 배경이 뜰 수 있게 구현해야 합니다. */}
</ModalContainer>
</>
);
};
배웠던 점 및 어려웠던 점
css in JS 방법을 충분히 학습할 수 있었고,
모달창과 모달창을 키는 버튼을 겹쳐야 하는데, 그 방법으로는 margin을 음수값을 주거나 position:fixed로 위치고정하는 방법이다.
상태변경함수를 사용하여,
모달버튼 클릭시 isOpen=true, X버튼 클릭시 isOpen=false로 값을 변경한다.
isOpen=true라면 모달창과 모달백업창이 나타나게 하고
isOpen=false라면 null로 한다.
e.stopPropagation() : 부모엘리먼트로 부터 이벤트 전파를 막아주는 메서드!
쓰지 않는다면 이벤트함수를 주지 않은 하위엘리먼트를 클릭했을 때도 모달창이 닫히는 이벤트가 생긴다. 이를 이벤트 버블링/캡쳐링이라고 표현한다.
- 시작하기
Toggle 컴포넌트 구현에 필요한 정보가 자세히 적혀있습니다. 확인 후 과제를 진행하세요.
우선 Toggle 컴포넌트 테스트의 주석을 해제하고 시작합니다.
Toggle 컴포넌트는 아래와 같은 state가 존재합니다. 필요에 따라서 state를 더 만들 수도 있습니다.
isOn state는 토글 버튼의 on/off 여부를 확인할 수 있습니다.
ToggleContainer 컴포넌트는 토글 버튼 제어를 위해 핸들러 함수 toggleHandler를 작성합니다.
toggleHandler 함수는
ToggleContainer 클릭 시 발생되는 change 이벤트 핸들러입니다.
클릭할 때마다 상태가 Boolean 값으로 변경됩니다.
- 요구사항
Styled Components 라이브러리를 활용해 ToggleContainer Desc 컴포넌트의 CSS를 자유롭게 구현합니다.
ToggleContainer : Toggle을 구현하는데 필요한 컴포넌트를 감싸주는 컨테이너 컴포넌트 역할을 합니다.
Desc : Toggle Switch의 상태를 설명하는 텍스트를 담는 컴포넌트입니다.
ToggleContainer 내부에 .toggle-container .toggle-circle 클래스를 가진 div 요소를 각각 생성합니다.
생성한 요소에 조건부 스타일링을 활용해 Toggle Switch가 ON인 상태일 경우에만 toggle--checked 클래스를 두 요소 모두에 추가합니다.
기본 CSS에서는 템플릿 리터럴과 삼항 연산자를 활용해 조건부 스타일링을 적용할 수 있습니다.<div className={`toggle-container ${isOn ? "toggle--checked" : ""}`} />
조건부 렌더링을 활용해 Toggle Switch가 ON인 상태일 경우에 Desc 컴포넌트 내부의 텍스트를 'Toggle Switch ON' 으로 Toggle Switch가 OFF인 상태일 경우에는 'Toggle Switch OFF'로 변경합니다.
토글 스위치가 부드럽게 옮겨지는 애니메이션 효과를 주기 위해서는 CSS의 transition 속성을 활용할 수 있습니다. 토글을 기능을 다 구현했다면 시도해 보세요!
//css in js
import { useState } from 'react';
import styled from 'styled-components';
const ToggleContainer = styled.div`
position: relative;
margin-top: 8rem;
left: 47%;
cursor: pointer;
display : flex;
flex-direction: column;
> .toggle-container {
width: 50px;
height: 24px;
border-radius: 30px;
background-color: #8b8b8b;
}
>.toggle--checked{
transition:0.5s;
background-color : #FCC8DC;
}
.toggle--nochecked{
transition:0.5s;
background-color : rgba(0,0,0,0.2);
}
> .toggle-circle {
position: absolute;
top: 1px;
left: 1px;
width: 22px;
height: 22px;
border-radius: 50%;
background-color: #ffffff;
}
>.toggle--checked{
transition:0.5s;
left:28px;
}
.toggle--nochecked{
transition:0.5s;
left:0px;
}
`;
const Desc = styled.div`
margin-top : 10px;
margin-left : -40px;
>.switch--on{
margin-left : -22px;
color : #FCC8DC;
}
>.switch--off{
color: rgba(0,0,0,0.2);
}
`;
//JS
export const Toggle = () => {
const [isOn, setisOn] = useState(false);
const toggleHandler = () => {
// TODO : isOn의 상태를 변경하는 메소드를 구현합니다.
setisOn(!isOn);
};
return (
<>
<ToggleContainer
onClick={toggleHandler}
// TODO : 클릭하면 토글이 켜진 상태(isOn)를 boolean 타입으로 변경하는 메소드가 실행되어야 합니다.
>
{/* TODO : 아래에 div 엘리먼트 2개가 있습니다. 각각의 클래스를 'toggle-container', 'toggle-circle' 로 지정하세요. */}
{/* TIP : Toggle Switch가 ON인 상태일 경우에만 toggle--checked 클래스를 div 엘리먼트 2개에 모두 추가합니다. 조건부 스타일링을 활용하세요. */}
<div className={`toggle-container ${isOn ? "toggle--checked" : "toggle--nochecked"}`} />
<div className={`toggle-circle ${isOn ? "toggle--checked" : "toggle--nochecked"}`} />
{isOn === true ?
<Desc><p className='switch--on'>💗Toggle Switch ON💗</p></Desc>
:<Desc><p className='switch--off'>Toggle Switch OFF</p></Desc>
/* TIP: Toggle Switch가 ON인 상태일 경우에 Desc 컴포넌트 내부의 텍스트를 'Toggle Switch ON'으로, 그렇지 않은 경우 'Toggle Switch OFF'가 됩니다. 조건부 렌더링을 활용하세요. */}
</ToggleContainer>
{/* TODO : Desc 컴포넌트를 활용해야 합니다. */}
</>
);
};
배웠던 점 및 어려웠던 점
<div className={`toggle-container ${isOn ? "toggle--checked" : "toggle--nochecked"}`}
클래스이름을 toggle-container는 무조건 넣고,
상태변경함수 isOn이 true라면,toggle--checked를 추가로 넣고
false라면 toggle--nochecked를 추가로 넣는다.
<Desc><p className='switch--on'>💗Toggle Switch ON💗</p></Desc>
Desc 태그에 클래스이름을 바로 넣으면 오류가 떠서,
p태그를 새로 생성하여 클래스이름을 넣었고,
저 클래스이름의 역할은 설명텍스트 색 변화이다.
isOn=true라면 토글컨테이너와 설명텍스트 색깔이 핑크색, 토글원이 오른쪽으로 옮겨진다.
isOn=false라면 토글컨테이너와 설명텍스트 색깔이 원래대로 회색, 토글원이 원래대로 왼쪽으로 옮겨진다.
- 시작하기
Tab 컴포넌트 구현에 필요한 정보가 자세히 적혀있습니다. 확인 후 과제를 진행하세요.
우선 Tab 컴포넌트 테스트의 주석을 해제하고 시작합니다.
Tab 컴포넌트는 아래와 같은 state가 존재합니다. 필요에 따라서 state를 더 만들 수도 있습니다.
currentTab state는 현재 tab의 index를 확인할 수 있습니다.
TabMenu 컴포넌트는 토글 버튼 제어를 위해 핸들러 함수 selectMenuHandler 를 가집니다.
selectMenuHandler 함수는
TabMenu 클릭 시 발생되는 change 이벤트 핸들러입니다.
클릭할 때마다 상태가 index 값으로 변경됩니다.
li 요소를 이용해 메뉴를 생성하고, 각 메뉴를 눌렀을 때 뷰가 전환되도록 handler(selectMenuHandler) 함수를 작성합니다.
조건부 스타일링과 currentTab 상태를 이용하여 클릭한 Tab 메뉴만 className(submenu focused)과 CSS 가 변경되도록 구현합니다. 조건부 스타일링은 앞서 토글에서 구현한 것과 같이 템플릿 리터럴과 삼항 연산자를 활용할 수 있습니다.
- 요구사항
Component
Styled Components 라이브러리를 활용해 TabMenu Desc 컴포넌트의 CSS를 자유롭게 구현합니다.
TabMenu : Tab을 구현하는데 필요한 컴포넌트를 감싸주는 컨테이너 컴포넌트 역할을 합니다.
Desc : Toggle Switch의 상태를 설명하는 텍스트를 담는 컴포넌트입니다.TabMenu
TabMenu 내부에 .submenu 클래스명을 가진 li 요소들을 map 을 이용한 반복을 통해 생성합니다.
TabMenu 내부에 .submenu 클래스명을 가진 li 요소의 textContent 는 각 요소의 name 입니다.currentTab
조건부 렌더링을 활용해서 Tab 메뉴가 선택된 상태일 때, 선택된 Tab 메뉴 li 요소의 클래스명만 submenu focused 가 되어야 하고, 선택되지 않은 나머지는 submenu 가 되도록 구현해야 합니다.
TabMenu 를 클릭하면 현재 선택된 탭의 인덱스 값을 전달받아 currentTab 상태를 변경하는 selectMenuHandler 메서드가 실행되어야 합니다.
TabMenu 를 클릭하면 현재 선택된 탭 메뉴만 .focused CSS가 적용되어야 합니다.
TabMenu 를 클릭하면 Desc 컴포넌트의 content 의 내용이 해당 탭의 content로 바뀌어야 합니다.
//css in js
import { useState } from 'react';
import styled from 'styled-components';
// TODO: Styled-Component 라이브러리를 활용해 TabMenu 와 Desc 컴포넌트의 CSS를 구현합니다.
const TabMenu = styled.ul`
background-color: #FFECF3;
color: rgba(73, 73, 73, 0.5);
font-weight: bold;
font-size : 20px;
display: flex;
align-items: center;
list-style: none;
margin-bottom: 7rem;
border-top-right-radius: 10px;
height : 50px;
justify-content: space-around;
.submenu {
${'' /* 기본 Tabmenu 에 대한 CSS를 구현합니다. */}
cursor: pointer;
height:50px;
width:33%;
text-align:center;
padding-top:10px;
}
.focused {
${'' /* 선택된 Tabmenu 에만 적용되는 CSS를 구현합니다. */}
background-color:#FCC8DC;
}
& div.desc {
text-align: center;
}
`;
const Desc = styled.div`
text-align: center;
`;
//JS
export const Tab = () => {
// TIP: Tab Menu 중 현재 어떤 Tab이 선택되어 있는지 확인하기 위한
// currentTab 상태와 currentTab을 갱신하는 함수가 존재해야 하고, 초기값은 0 입니다.
const [currentTab, setCurrentTab] = useState(0);
const menuArr = [
{ name: 'Tab1', content: 'Tab menu ONE' },
{ name: 'Tab2', content: 'Tab menu TWO' },
{ name: 'Tab3', content: 'Tab menu THREE' },
];
const selectMenuHandler = (index) => {
// TIP: parameter로 현재 선택한 인덱스 값을 전달해야 하며, 이벤트 객체(event)는 쓰지 않습니다
// TODO : 해당 함수가 실행되면 현재 선택된 Tab Menu 가 갱신되도록 함수를 완성하세요.
setCurrentTab(index);
};
return (
<>
<div>
<TabMenu>
{/*TODO: 아래 하드코딩된 내용 대신에, map을 이용한 반복으로 코드를 수정합니다.*/}
{/*TIP: li 엘리먼트의 class명의 경우 선택된 tab 은 'submenu focused' 가 되며,
나머지 2개의 tab은 'submenu' 가 됩니다.*/}
{menuArr.map((i, index) => {
return (
<li key={index} onClick={()=>selectMenuHandler(index)} className={`${currentTab===index ? "submenu focused" : "submenu"}`}>{i.name}</li>
)
})}
</TabMenu>
<Desc>
{/*TODO: 아래 하드코딩된 내용 대신에, 현재 선택된 메뉴 따른 content를 표시하세요*/}
<p>{menuArr[currentTab].content}</p>
</Desc>
</div>
</>
);
};
배웠던 점 및 어려웠던 점
클릭한 탭과 클릭하지 않은 탭의 css차이는 클래스이름으로 하였다.
submenu, focused
클릭한 탭은 두 클래스이름 모두 갖고, 클릭하지 않은 탑은 submenu 만 갖는다.
{menuArr.map((i, index) => {
return (
<li key={index} onClick={()=>selectMenuHandler(index)} className={`${currentTab===index ? "submenu focused" : "submenu"}`}>{i.name}</li>
)
})}
map으로 menuArr의 요소들을 하나씩 li요소로 만들어서 뿌려준다.
현재탭의 인덱스와 클릭한 인덱스가 같다면 클래스이름을 submenu, focused 둘다 주고
같지 않다면(=클릭하지 않은 인덱스라면) 클래스이름을 submenu만 준다.
onclick이벤트에서 그냥 이벤트함수자체만 전달할때도 있고 매개변수를 넣어 전달해야 할 때가 있으므로 2개를 구분하여 코드 작성방법을 기억해야 한다.
- 시작하기
Tag 컴포넌트 구현에 필요한 정보가 자세히 적혀있습니다. 확인 후 과제를 진행하세요.
우선 Tag 컴포넌트 테스트의 주석을 해제하고 시작합니다.
Tag 컴포넌트는 아래와 같은 state가 존재합니다. 필요에 따라서 state를 더 만들 수도 있습니다.
tags state는 배열의 형태입니다.
초기값으로 initialTags 를 가지고 있습니다.
TagsInput 컴포넌트는 핸들러 함수 addTags를 가집니다.
addTags 함수는
input 창에 Enter 키를 누를 때 발생되는 change 이벤트 핸들러입니다.
Enter를 입력할 때마다 입력한 값이 state에 추가됩니다.
span.tag-close-icon 에는 아직 실제 작동하는 삭제 아이콘(x)이 없습니다. 삭제 아이콘을 만들고, 이 버튼이 동작하도록 handler (removeTags ) 함수를 작성합니다.
hint : HTML Entities 라는 키워드로 구글링 해서 참고해 보세요.
- 요구사항
input 기능 테스트
input 창에 텍스트를 입력 후 Enter 키를 누르면 태그가 추가되어야 합니다. 마우스 클릭이 아닌 Enter 키를 통해 태그가 추가되도록 하며, Enter 키가 눌리면 태그를 추가하는 addTags 메서드가 실행되어야 합니다.
addTags 메서드는 기본적으로 태그를 추가하는 기능 이외에 아래 세 가지 기능도 수행할 수 있어야 합니다.
이미 입력되어 있는 태그인지 검사하여 이미 입력되어 있다면 추가하지 말아야 합니다.
아무것도 입력하지 않은 상태에서는 Enter 키를 눌러도 addTags 메서드가 실행되지 않아야 합니다.
태그가 추가되고 나면 input 창이 비워져야 합니다.삭제 기능 테스트
기본적으로 tags 배열 안의 모든 태그들이 화면에 보여야 합니다.
태그 이름 옆에 삭제 아이콘(x)이 표시되도록 하고, 아이콘을 클릭하면 해당 태그를 삭제하는 removeTags 메서드가 실행되어야 합니다.
removeTags 메서드가 삭제 아이콘(x)이 눌린 태그를 삭제하도록 removeTags 메서드를 완성해야 합니다.
//css in js
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: 450px;
padding: 0 8px;
border: 2px solid #FFECF3;
border-radius: 6px;
> ul {
display: flex;
flex-wrap: wrap;
padding: 0;
margin: 10px 0 0 0;
> .tag {
width: auto;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 8px;
font-size: 14px;
list-style: none;
border-radius: 10px;
margin: 0 8px 8px 0;
background: #FCC8DC;
> .tag-close-icon {
display: block;
width: 16px;
height: 16px;
line-height: 17px;
text-align: center;
font-size: 14px;
margin-left: 8px;
color: rgba(0,0,0,0.5);
border-radius: 50%;
cursor: pointer;
}
}
}
> input {
flex: 1;
border: none;
height: 46px;
font-size: 14px;
padding: 4px 0 0 5px;
:focus {
outline: transparent;
}
}
&:focus-within {
border: 3px solid #FCC8DC;
}
`;
//js
export const Tag = () => {
const initialTags = ['CodeStates', 'kimcoding'];
const [tags, setTags] = useState(initialTags);
const removeTags = (indexToRemove) => {
// TODO : 태그를 삭제하는 메소드를 완성하세요.
setTags([...tags].filter((v,idx) => idx!==indexToRemove));
//태그의 인덱스가 삭제버튼클릭한 인덱스와 다른 것만 필터
};
const addTags = (event) => {
// TODO : tags 배열에 새로운 태그를 추가하는 메소드를 완성하세요.
// 이 메소드는 태그 추가 외에도 아래 3 가지 기능을 수행할 수 있어야 합니다.
// - 이미 입력되어 있는 태그인지 검사하여 이미 있는 태그라면 추가하지 말기
// - 아무것도 입력하지 않은 채 Enter 키 입력시 메소드 실행하지 말기
// - 태그가 추가되면 input 창 비우기
if(window.event.keyCode == 13 && !tags.includes(event.target.value) && event.target.value !== ""){
setTags([...tags, event.target.value]);
event.target.value = "";
}
};
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)}>×
{/* TODO : tag-close-icon이 tag-title 오른쪽에 x 로 표시되도록 하고,
삭제 아이콘을 click 했을 때 removeTags 메소드가 실행되어야 합니다. */}
</span>
</li>
))}
</ul>
<input
className="tag-input"
type="text"
onKeyUp={(event) => {addTags(event)
/* 키보드의 Enter 키에 의해 addTags 메소드가 실행되어야 합니다. */
}}
placeholder="Press enter to add tags"
/>
</TagsInput>
</>
);
};
배웠던 점 및 어려웠던 점
onKeyUp={(event) => {addTags(event)}}
/* 키보드의 Enter 키에 의해 addTags 메소드가 실행되어야 합니다. */
//이벤트를 인수로 가지고 들어가는 이벤트 함수를 쓰는 코드!
{tags.map((tag, index) => (...
onClick={() => removeTags(index)}))}
//함수에 index를 전달인자로 가지고 있고 removeTags함수가 index를 전달인자로 가지고 갈 수 있다.
setTags([...tags].filter((v,idx) => idx!==indexToRemove));
//태그의 인덱스가 삭제버튼클릭한 인덱스와 다른 것만 필터
if(window.event.keyCode == 13 && !tags.includes(event.target.value) && event.target.value !== ""){
setTags([...tags, event.target.value]);
event.target.value = "";
}
//기존 태그에 새로운 태그 추가 방법
//enter(13)키를 누르고 input text가 이미 태그에 있지 않고 input text가 빈값이 아니면
//기존 태그들이 모인 배열에 새로운 태그를 추가한 후, input text는 지워준다.
- 시작하기
Autocomplete 컴포넌트 구현에 필요한 정보가 자세히 적혀있습니다. 확인 후 과제를 진행하세요.
Autocomplete 컴포넌트는 아래 3가지 state가 존재합니다. 필요에 따라서 state를 더 만들 수도 있습니다.
hasText state는 input 값의 유무를 확인할 수 있습니다.
inputValue state는 input 값의 상태를 확인할 수 있습니다.
options state는 input 값을 포함하는 autocomplete 추천 항목 리스트를 확인할 수 있습니다.
input 요소를 만들고, input 값 제어를 위해 state와 핸들러 함수 handleInputChange를 작성합니다.
handleInputChange 함수는
input 값 변경 시 발생되는 change 이벤트 핸들러입니다.
input 값과 상태를 연결시킬 수 있게 controlled component로 만들 수 있고
autocomplete 추천 항목이 dropdown으로 시시각각 변화되어 보일 수 있도록 상태를 변경합니다.
input 값에 맞는 옵션만 dropdown으로 보일 수 있도록 state와 핸들러 함수 handleDropDownClick 를 작성합니다. deselectedOptions을 초기값으로 이용해야 합니다.
handleDropDownClick 함수는
autocomplete 추천 항목을 클릭할 때 발생되는 click 이벤트 핸들러입니다.
dropdown에 제시된 항목을 눌렀을 때, input값이 해당 항목의 값으로 변경되는 기능을 수행합니다.
InputContainer의 자식으로 div.delete-button 가 있습니다. 이 버튼을 누르면 input 값을 초기화할 수 있어야 합니다.
handleDeleteButtonClick 함수는
input의 오른쪽에 있는 X버튼 클릭 시 발생되는 click 이벤트 핸들러입니다.
handleDeleteButtonClick 함수를 완성하여 input값을 한 번에 삭제하는 기능을 구현합니다.
- 요구사항
input 기능 테스트
input 요소에 onChange 이벤트 핸들러가 불려와야 합니다.
input 값을 삭제할 수 있는 버튼(div.delete-button)이 있어야 합니다.
삭제 버튼 클릭 시 input value가 삭제되어야 합니다.drop down 기능 테스트
input 값이 포함된 자동 완성 추천 drop down 리스트가 보여야 합니다.
drop down 항목을 마우스로 클릭 시, input 값 변경에 따라 drop down 목록이 변경되어야 합니다.
drop down 항목을 마우스로 클릭 시, input 값이 변경되어야 합니다.
drop down 항목을 마우스로 클릭 시, input 값이 이미 있어도 input 값이 drop down 항목의 값으로 변경되어야 합니다.도전하기
키보드 방향키로 drop down 목록을 조정할 수 있도록 구현합니다.
키보드 엔터키를 누르면 선택한 추천 검색어가 input 값이 되도록 구현합니다.
//css in js
import {
useEffect,
useState,
} from 'react';
import styled from 'styled-components';
const deselectedOptions = [
'rustic',
'antique',
'vinyl',
'vintage',
'refurbished',
'신품',
'빈티지',
'중고A급',
'중고B급',
'중고C급',
'골동품'
];
/* 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 : 0 30px;
margin-top: 8rem;
background-color: #ffffff;
display: flex;
flex-direction: row;
padding: 1rem;
border: 3px solid #FFE1EF;
border-radius: ${inactiveBorderRadius};
z-index: 3;
box-shadow: 0;
&:focus-within {
box-shadow: 0px 10px 50px 0px rgba(252,135,188,0.2);
border: 3px solid #FFACD2;
}
> 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;
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 : 0 30px;
margin-top: -13px;
padding: 0.5rem 0;
border: 1px solid rgb(223, 225, 229);
border-top : 1px solid rgba(0,0,0,0.1);
border-radius: 0 0 1rem 1rem;
box-shadow: 0px 20px 20px 0px rgba(252,135,188,0.1);
border: 3px solid #FFACD2;
border-top:1px solid #FFE9F3;
z-index: 3;
> li {
padding: 5px 1rem;
}
> li:hover,li.selected{
background-color:#FFE9F3;
}
`;
//js
export const Autocomplete = () => {
/**
* Autocomplete 컴포넌트는 아래 3가지 state가 존재합니다. 필요에 따라서 state를 더 만들 수도 있습니다.
* - hasText state는 input값의 유무를 확인할 수 있습니다.
* - inputValue state는 input값의 상태를 확인할 수 있습니다.
* - options state는 input값을 포함하는 autocomplete 추천 항목 리스트를 확인할 수 있습니다.
*/
const [hasText, setHasText] = useState(false);
const [inputValue, setInputValue] = useState('');
const [options, setOptions] = useState(deselectedOptions);
const [selected, setSelected] = useState(-1);
// useEffect를 아래와 같이 활용할 수도 있습니다.
useEffect(() => {
if (inputValue === '') {
setHasText(false);
}
}, [inputValue]);
// TODO : input과 dropdown 상태 관리를 위한 handler가 있어야 합니다.
const handleInputChange = (event) => {
/**
* handleInputChange 함수는
* - input값 변경 시 발생되는 change 이벤트 핸들러입니다.
* - input값과 상태를 연결시킬 수 있게 controlled component로 만들 수 있고
* - autocomplete 추천 항목이 dropdown으로 시시각각 변화되어 보여질 수 있도록 상태를 변경합니다.
*
* handleInputChange 함수를 완성하여 아래 3가지 기능을 구현합니다.
*
* onChange 이벤트 발생 시
* 1. input값 상태인 inputValue가 적절하게 변경되어야 합니다.
* 2. input값 유무 상태인 hasText가 적절하게 변경되어야 합니다.
* 3. autocomplete 추천 항목인 options의 상태가 적절하게 변경되어야 합니다.
* Tip : options의 상태에 따라 dropdown으로 보여지는 항목이 달라집니다.
*/
setInputValue(event.target.value);
setHasText(!hasText);
setOptions([...deselectedOptions].filter(v => v.includes(event.target.value)));
};
const handleDropDownClick = (clickedOption) => {
/**
* handleDropDownClick 함수는
* - autocomplete 추천 항목을 클릭할 때 발생되는 click 이벤트 핸들러입니다.
* - dropdown에 제시된 항목을 눌렀을 때, input값이 해당 항목의 값으로 변경되는 기능을 수행합니다.
*
* handleInputChange 함수를 완성하여 아래 기능을 구현합니다.
*
* onClick 이벤트 발생 시
* 1. input값 상태인 inputValue가 적절하게 변경되어야 합니다.
* 2. autocomplete 추천 항목인 options의 상태가 적절하게 변경되어야 합니다.
*/
setInputValue(clickedOption);
setOptions([clickedOption]);
};
const handleDeleteButtonClick = () => {
/**
* handleDeleteButtonClick 함수는
* - input의 오른쪽에 있는 X버튼 클릭 시 발생되는 click 이벤트 핸들러입니다.
* - 함수 작성을 완료하여 input값을 한 번에 삭제하는 기능을 구현합니다.
*
* handleDeleteButtonClick 함수를 완성하여 아래 기능을 구현합니다.
*
* onClick 이벤트 발생 시
* 1. input값 상태인 inputValue가 빈 문자열이 되어야 합니다.
*/
setInputValue("");
};
// Advanced Challenge: 상하 화살표 키 입력 시 dropdown 항목을 선택하고, Enter 키 입력 시 input값을 선택된 dropdown 항목의 값으로 변경하는 handleKeyUp 함수를 만들고,
// 적절한 컴포넌트에 onKeyUp 핸들러를 할당합니다. state가 추가로 필요한지 고민하고, 필요 시 state를 추가하여 제작하세요.
const handleKeyUp = (event) => {
if (window.event.code === "ArrowUp" && selected > 0) {
setSelected(selected - 1);
setInputValue(options[selected - 1]);
console.log(options[selected - 1]);
}
if (window.event.code === "ArrowDown" && selected < options.length - 1 && event.nativeEvent.isComposing === false) {
setSelected(selected + 1);
setInputValue(options[selected + 1]);
/*keydown 이벤트에서 선택된 li태그에 backgroundColor속성을 넣으려고 했다가 실패한 코드
위쪽 li태그쪽 css에 추가하면 해결된다.
const arr = Array.from(document.querySelectorAll(".keyUpColor"));
const element = arr.filter((el,idx) => idx===(selected + 1))[0];
console.log(element);
element.style.backgroundColor = 'rgba(0,0,0,0.2)';
*/
}
if (event.code === "Enter" && selected >= 0) {
handleDropDownClick(options[selected]);
setSelected(-1);
}
};
return (
<div className='autocomplete-wrapper'>
<InputContainer>
<input placeholder='검색어를 입력하세요' value={inputValue} onKeyDown={(event)=>handleKeyUp(event)} onChange={(event)=>handleInputChange(event)}>
</input>
{/* TODO : input 엘리먼트를 작성하고 input값(value)을 state와 연결합니다. handleInputChange 함수와 input값 변경 시 호출될 수 있게 연결합니다. */}
{/* TODO : 아래 div.delete-button 버튼을 누르면 input 값이 삭제되어 dropdown이 없어지는 handler 함수를 작성합니다. */}
<div className='delete-button' onClick={handleDeleteButtonClick}>×</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((el,idx) => {
return(
<li key={idx} className={selected === idx ? "selected":""} onClick={()=>handleComboBox(el)}>{el}</li>
)
})}
</DropDownContainer>
);
};
배웠던 점 및 어려웠던 점
제일 시간을 많이 쏟은 컴포넌트이다.
일단 위의 다른 컴포넌트의 기본 코드들 보다 훨씬 길었고, advanced chellenage로 키보드이벤트도 추가로 만들라는 과제였기 때문이다.
일단 원리는 input창을 클릭해서 텍스트를 쓰면 onchange함수로 input창의 text가 바뀌고, deselectedOptions배열의 요소들 중 input창 텍스트의 글자가 포함되는 요소가 있다면 드롭다운으로 요소들을 차례로 보여준다.
드롭다운 요소들중 클릭한 요소가 input창의 텍스트가 되고 드롭다운도 선택한 텍스트 하나만 보이게 한다.
또한 arrowUp/Down키로 클릭하면 드롭다운 요소들을 차례로 위/아래로 선택하게 하고
선택된 요소는 input창의 텍스트가 된다. 여전히 드롭다운 요소들은 그대로 있다.
이 상태에서 enter를 누르면 선택한 텍스트가 input창과 드롭다운에 하나로 보인다.
<keyDown 이벤트 쓰는 방법 기억하기(window 생략가능)>
window.event.code === "ArrowUp"
window.event.code === "ArrowDown"
event.code === "Enter"
키를 눌렀을 때 위아래로 움직이는 건 상태변경함수 selected를 사용했다.
selected를 초기값으로 -1로 할당하고
down키를 누르면 +1(최대 드롭다운 배열의 마지막인덱스보다 하나작을때까지 사용가능, 마지막 인덱스까지 갈 수 있다.)
up키를 누르면 -1(1일때까지만 사용가능, 마지막엔 0이 된다.)
키를 눌렀을 때 해당되는 드롭다운배열의 인덱스에 해당하는 값이 input창의 텍스트가 된다. 또한 그 드롭다운배열의 요소 li태그에 클래스이름 "selected" 를 할당하고 selected는 배경색을 분홍색으로 만든다.
{hasText ? <DropDown options={options} handleComboBox={handleDropDownClick} selected={selected}/>:[]}
//텍스트가 있으면 드롭다운에 options, handleDropDownClick, selected를 넘겨준다.
export const DropDown = ({ options, handleComboBox, selected}) => {
return (...);};
event.nativeEvent.isComposing === false
keyup/down/press 이벤트함수의 실행이 중복되는 경우가 있는데,
이는 js 자체에서 실행하는 거라 따로 막는 코드를 작성해야 한다.
event.nativeEvent.isComposing : 이벤트가 발생했는지 여부를 리턴한다.
true라면 이벤트가 발생했다. false라면 이벤트가 발생하지 않았다.
false로 고정시켜 이벤트가 발생하지 않은 경우(중복이 생긴다면 첫번째)만 함수가 실행된다.
- 시작하기
ClickToEdit 컴포넌트 구현에 필요한 정보가 자세히 적혀있습니다. 확인 후 과제를 진행하세요.
먼저 MyInput 컴포넌트를 구현하세요. MyInput 컴포넌트를 ClickToEdit 컴포넌트 내부에서 활용할 수 있습니다.
MyInput 컴포넌트는 아래와 같은 state가 존재합니다. 필요에 따라서 state를 더 만들 수도 있습니다.
isEditMode state는 boolean 값을 상태로 가집니다.
newValue state는 보이게 될 값을 상태로 가지며, isEditMode 가 true 일 때 업데이트됩니다.
ClickToEdit 컴포넌트는 아래와 같은 state가 존재합니다. 필요에 따라서 state를 더 만들 수도 있습니다.
name , age 를 state로 가지며, 앞서 작성한 MyInput 컴포넌트를 활용해 state를 업데이트할 수 있습니다.
ClickToEdit 컴포넌트가 자식으로 가지는 MyInput 컴포넌트에 handleValueChange 메소드를 props로 받아 value 를 업데이트하세요.
- 요구사항
MyInput 컴포넌트
조건부 렌더링을 활용하여 Edit가 가능한 상태일 경우에는 InputEdit 컴포넌트를 불러와야 합니다.
input 요소에 onChange 이벤트 핸들러가 불러져 와야 합니다. onChange가 실행되면 저장된 value를 변경시켜 줍니다.
input 요소에 onBlur 이벤트 핸들러가 불러져 와야 합니다. onBlur 이벤트 핸들러가 무슨 역할을 하는지 구글링을 통해서 찾아보세요!
input 요소가 포커스를 잃었을 때, Edit가 불가능한 상태로 변경되어야 합니다.
Edit가 불가능한 상태일 경우에는 저장된 value를 보여줄 수 있는 요소를 불러와야 합니다.
이 요소를 클릭하면 Edit 가 가능한 상태로 변경되어야 합니다.input 창 클릭 기능 테스트
입력 가능 상태로 변경할 수 있는 onClick 이벤트 핸들러가 span 요소에 있어야 합니다.
포커스가 제외되는 이벤트 onBlur의 핸들러가 input 요소에 있어야 합니다.
텍스트 영역을 클릭하면 입력 가능 상태로 변경되어야 합니다.
입력 가능 상태일 때 변화가 감지되면 새로운 값을 설정하는 메서드가 실행되어야 합니다.
입력 가능 상태일 때 input이 아닌 다른 곳을 클릭하면 입력 불가 상태가 되어야 합니다.
입력 가능 상태일 때 input이 아닌 다른 곳을 클릭하면 input의 값이 span에 담겨야 합니다.
//css in js
import {
useEffect,
useRef,
useState,
} from 'react';
import styled from 'styled-components';
export const InputBox = styled.div`
text-align: center;
display: inline-block;
width: 150px;
height: 30px;
margin-left: 1rem;
border-bottom: 3px solid #FF77BE;
&:hover{
cursor: pointer;
}
`;
export const InputEdit = styled.input`
text-align: center;
display: inline-block;
width: 150px;
height: 30px;
margin-top:-30px;
margin-left: -0.05px;
`;
export const InputView = styled.div`
text-align: center;
align-items: center;
margin-top: 3rem;
> label{
color:#FF77BE;
}
div.view {
margin-top: 3rem;
color:#FF77BE;
}
`;
//js
export const MyInput = ({ value, handleValueChange }) => {
const inputEl = useRef(null);
const [isEditMode, setEditMode] = useState(false);
const [newValue, setNewValue] = useState(value);
useEffect(() => {
if (isEditMode) {
inputEl.current.focus();
}
}, [isEditMode]);
useEffect(() => {
setNewValue(value);
}, [value]);
const handleClick = () => {
// TODO : isEditMode 상태를 변경합니다.
setEditMode(true); //수정가능한 상태로 만들어준다.
};
const handleBlur = () => {
// TODO : Edit가 불가능한 상태로 변경합니다.
handleValueChange(newValue);
setEditMode(false);//수정불가능한 상태로 만들어준다.
};
const handleInputChange = (e) => {
// TODO : 저장된 value를 업데이트합니다.
setNewValue(e.target.value);
};
return (
<InputBox>
{isEditMode ? (
<InputEdit
spellCheck={false}
type='text'
value={newValue}
ref={inputEl}
// TODO : 포커스를 잃으면 Edit가 불가능한 상태로 변경되는 메소드가 실행되어야 합니다.
onBlur={handleBlur}
// TODO : 변경 사항이 감지되면 저장된 value를 업데이트 되는 메소드가 실행되어야 합니다.
onChange={handleInputChange}
/>
) : (
<span
// TODO : 클릭하면 Edit가 가능한 상태로 변경되어야 합니다.
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} <br/> 나이는 {age}세 입니다.</div>
</InputView>
</>
);
};
배웠던 점 및 어려웠던 점
<InputBox>
{isEditMode ? (
<InputEdit
spellCheck={false}
type='text'
value={newValue}
ref={inputEl}
// TODO : 포커스를 잃으면 Edit가 불가능한 상태로 변경되는 메소드가 실행되어야 합니다.
onBlur={handleBlur}
// TODO : 변경 사항이 감지되면 저장된 value를 업데이트 되는 메소드가 실행되어야 합니다.
onChange={handleInputChange}
/>
) : (
<span
// TODO : 클릭하면 Edit가 가능한 상태로 변경되어야 합니다.
onClick={handleClick}
>{newValue}</span>
)}
</InputBox>
자동완성 컴포넌트와 마찬가지로 코드가 너무 길어서 어디서부터 손봐야할지 처음엔 감이 안잡혔다. 일단 이벤트함수들을 모두 작성하니까 각이 잡혔다.
isEditmode=false일 때, input을 클릭했을 때 isEditmode=true가 되면서 input이 수정가능한 상태로 만들기
isEditmode=true상태에서 다른 곳을 클릭하여 포커스를 잃으면 input 수정불가능한 상태로 만들고 설명텍스트에 값 업데이트 하기
onBlur={handleBlur} : 포커스가 된 곳에서 다른 곳을 클릭하여 포커스를 벗어나면 작동하는 이벤트 함수
spellCheck={false} : js의 기본세팅중 하나인 input값 오타체크를 위한 빨간지렁이줄을 없애는 방법