- Published on
Drag & Drop 토이 프로젝트 part 1
Drag & Drop 토이 프로젝트 part 1
시작하기
app.css
* {
box-sizing: border-box;
}
html {
font-family: sans-serif;
}
body {
margin: 0;
}
label,
input,
textarea {
display: block;
margin: 0.5rem 0;
}
label {
font-weight: bold;
}
input,
textarea {
font: inherit;
padding: 0.2rem 0.4rem;
width: 100%;
max-width: 30rem;
border: 1px solid #ccc;
}
input:focus,
textarea:focus {
outline: none;
background: #fff5f9;
}
button {
font: inherit;
background: #ff0062;
border: 1px solid #ff0062;
cursor: pointer;
color: white;
padding: 0.75rem 1rem;
}
button:focus {
outline: none;
}
button:hover,
button:active {
background: #a80041;
border-color: #a80041;
}
.projects {
margin: 1rem;
border: 1px solid #ff0062;
}
.projects header {
background: #ff0062;
height: 3.5rem;
display: flex;
justify-content: center;
align-items: center;
}
#finished-projects {
border-color: #0044ff;
}
#finished-projects header {
background: #0044ff;
}
.projects h2 {
margin: 0;
color: white;
}
.projects ul {
list-style: none;
margin: 0;
padding: 1rem;
}
.projects li {
box-shadow: 1px 1px 8px rgba(0, 0, 0, 0.26);
padding: 1rem;
margin: 1rem;
}
.projects li h2 {
color: #ff0062;
margin: 0.5rem 0;
}
#finished-projects li h2 {
color: #0044ff;
}
.projects li h3 {
color: #575757;
font-size: 1rem;
}
.project li p {
margin: 0;
}
.droppable {
background: #ffe3ee;
}
#finished-projects .droppable {
background: #d6e1ff;
}
#user-input {
margin: 1rem;
padding: 1rem;
border: 1px solid #ff0062;
background: #f7f7f7;
}
Index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>ProjectManager</title>
<link rel="stylesheet" href="app.css" />
<script src="dist/app.js" defer></script>
</head>
<body>
<template id="project-input">
<form>
<div class="form-control">
<label for="title">Title</label>
<input type="text" id="title" />
</div>
<div class="form-control">
<label for="description">Description</label>
<textarea id="description" rows="3"></textarea>
</div>
<div class="form-control">
<label for="people">People</label>
<input type="number" id="people" step="1" min="0" max="10" />
</div>
<button type="submit">ADD PROJECT</button>
</form>
</template>
<template id="single-project">
<li></li>
</template>
<template id="project-list">
<section class="projects">
<header>
<h2></h2>
</header>
<ul></ul>
</section>
</template>
<div id="app"></div>
</body>
</html>
index.html파일에서 스타일링을 위해app.css파일을 가져오고, 자바스크립트 로직을 위해 컴파일된app.js파일을 로드함body본문의template태그를 이용해 즉시 렌더링되지 않는html코드를 지정- 자바스크립트와 타입스크립트를 통해 접근하며, 원할 때 렌더링하고 통제할 수 있음
DOM 요소 선택 및 OOP 렌더링
실행
npm start명령어를 통해html파일을 제공하는 개발 서버를 가져옴- 탭을 새로 열어
tsc -w명령어를 통해 compliation in watch mode를 시작
app.ts
class ProjectInput {
templateElement: HTMLTemplateElement
hostElement: HTMLDivElement
element: HTMLFormElement
constructor() {
this.templateElement = document.getElementById('project-input')! as HTMLTemplateElement
this.hostElement = document.getElementById('app')! as HTMLDivElement
const importedNode = document.importNode(this.templateElement.content, true)
this.element = importedNode.firstElementChild as HTMLFormElement
this.attach()
}
private attach() {
this.hostElement.insertAdjacentElement('afterbegin', this.element)
}
}
const prjInput = new ProjectInput()
ProjectInput에constructor를 추가하여 템플릿과 렌더링되어야 하는 위치에 접근this.templateElement,this.hostElement프로퍼티를 추가하여 템플릿 내용을 렌더링하려는 엘리먼트에 대한 참조를 취득- 타입스크립트는 HTMLElement를 알 수 없으므로, 해당 프로퍼티와 엘리먼트를 매칭할 수 있도록 선언해줌
- 확실히 있는 엘리먼트에 대해서
!를 붙여 null이 아님을 명시 getElementById는 어떤 엘리먼트가 정확히 어떤HTML엘리먼트 버전인지 모르기때문에 타입 캐스팅(as)을 통해 타입을 지정해야 함
const importedNode = document.importNode(this.templateElement.content, true);- 전역 문서 객체에 제공되는 메서드인
document.importNode에 템플릿 엘리먼트의 포인터를 입력함 - 템플릿 코드 사이의 html 코드를 참조하여 두 번째 인자로 깊은 복사를 이용해 가져올 것인지 여부를넘겨줌
- 전역 문서 객체에 제공되는 메서드인
private attach() {}- 선택 사항으로 렌더링 로직을 분리하기 위해
attachprivate 메서드를 정의 this.hostElement를 가져온 후 렌더링하고 싶은 위치를 정하고insertAdjacentElement를 호출HTMLElement를 삽입하기 위해 자바스크립트 브라우저에서 제공하는 기본 메서드로서, 대상으로 삼은 엘리먼트의 처음 부분 뒤에 삽입할 수 있음
this.element는 삽입하려는 노드를 가리키는 확실한 프로퍼티를 가리킴
- 선택 사항으로 렌더링 로직을 분리하기 위해
const prjInput = new ProjectInput();- 클래스 아래에 새로운 상수를 생성해
ProjectInput클래스를 인스턴스화하여 호출
- 클래스 아래에 새로운 상수를 생성해
DOM 요소와 상호작용
- 다양한 양식에 접근해, 양식이 제출되었을 때 값을 읽을 수 있도록 이벤트 리스너를
submit으로 설정하고 사용자 입력의 유효성을 검사해야 함 - 버튼과
form에 접근해submit,input이벤트를 통해 가장 최근의 값을 가져와야 함 - 엘리먼트에
ID를 할당한 후, 접근하고 클래스의 프로퍼티로 저장하여 엘리먼트를 참조
app.ts
class ProjectInput {
templateElement: HTMLTemplateElement
hostElement: HTMLDivElement
element: HTMLFormElement
titleInputElement: HTMLInputElement
descriptionInputElement: HTMLInputElement
peopleInputElement: HTMLInputElement
constructor() {
this.templateElement = document.getElementById('project-input')! as HTMLTemplateElement
this.hostElement = document.getElementById('app')! as HTMLDivElement
const importedNode = document.importNode(this.templateElement.content, true)
this.element = importedNode.firstElementChild as HTMLFormElement
this.element.id = 'user-input'
this.titleInputElement = this.element.querySelector('#title') as HTMLInputElement
this.descriptionInputElement = this.element.querySelector('#description') as HTMLInputElement
this.peopleInputElement = this.element.querySelector('#people') as HTMLInputElement
this.configure()
this.attach()
}
private submitHandler(event: Event) {
event.preventDefault()
}
private configure() {
this.element.addEventListener('submit', this.submitHandler.bind(this))
}
private attach() {
this.hostElement.insertAdjacentElement('afterbegin', this.element)
}
}
const prjInput = new ProjectInput()
titleInputElement,descriptionInputElement,peopleInputElement의 필드를 추가하고 생성자 메서드 안에HTMLFormElement하위에 있는HTMLInputElement를querySelector를 입력해 가져옴private configure(){}- 이벤트 리스너를 설정해
this.element와addEventListener를 입력 this키워드가submitHandler에서 클래스를 가리키지 않았으므로bind메서드를 통해this를 전달
- 이벤트 리스너를 설정해
private submitHandler(){}- 이벤트 객체를 받는 메서드를 통해 이벤트 간 리스너를 바인딩해야 함
- 입력 값에 접근하고 유효성 검사를 위해
event.preventDefault를 호출하여 기본form의submit Event를 방지
Autobind 데코레이터 생성 및 사용
- 앱 여러 곳에서
bind호출을 해야하는 경우, 데코레이터를 이용하면 작업이 훨씬 쉬워질 수 있음 bind를 제거하고,autobind decorator를 추가
tsconfig.json
{
"compilerOptions": {
/* Basic Options */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"lib": [
"dom",
"es6",
"dom.iterable",
"scripthost"
], /* Specify library files to be included in the compilation. */
"sourceMap": true /* Generates corresponding '.map' file. */,
"outDir": "./dist" /* Redirect output structure to the directory. */,
"rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
"removeComments": true /* Do not emit comments to output. */,
"noEmitOnError": true,
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
/* Additional Checks */
"noUnusedLocals": true /* Report errors on unused locals. */,
"noUnusedParameters": true /* Report errors on unused parameters. */,
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
/* Experimental Options */
"experimentalDecorators": true /* Enables experimental support for ES7 decorators. */
},
"exclude": [
"node_modules" // would be the default
]
}
decorator를 사용하기 위해서tsconfig.json파일에서Experimetal Options인"experimentalDecorators"키 값을true로 설정해야 함
app.ts
autobind decorator// autobind decorator function autobind(_: any, _2: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value const adjDescriptor: PropertyDescriptor = { configurable: true, get() { const boundFn = originalMethod.bind(this) return boundFn }, } return adjDescriptor }- 데코레이터는 함수이므로 함수 선언 또는 함수 표현식으로 정의할 수 있음
- 함수 선언을 이용해 3개의 인자를 전달 받음
target과 바인딩할methodName,descriptor를 전달 받음
descriptor.value- 원래 정의했던 메서드를 정의
const adjDescriptor:PropertyDescriptor = {}- 조정된 디스크립터를 생성하고
configurable을true로 설정해서 언제든 수정할 수 있게 설정 getter를 통해 함수에 접근할 때 실행되게bound함수를 설정- 추출한
originalMethod를 이용해bind(this)를 호출하고bound함수를 반환
- 조정된 디스크립터를 생성하고
return adjDescriptor;- 해당 메서드 데코레이터에서 조정된 디스크립터를 반환
- 실제로 쓰이지 않는 인수 값(target, methodName)은 두 가지 방법으로 해결할 수 있음
tsconfig에서restrict해당하는 옵션 값(noUnusedParameters: false)을 조정- 밑줄(
_)을 사용해 타입스크립트와 자바스크립트에 이 값들을 당장 사용하지 않을 것이라고 암시를 줄 수 있음
ProjectInput Classclass ProjectInput { templateElement: HTMLTemplateElement hostElement: HTMLDivElement element: HTMLFormElement titleInputElement: HTMLInputElement descriptionInputElement: HTMLInputElement peopleInputElement: HTMLInputElement constructor() { this.templateElement = document.getElementById('project-input')! as HTMLTemplateElement this.hostElement = document.getElementById('app')! as HTMLDivElement const importedNode = document.importNode(this.templateElement.content, true) this.element = importedNode.firstElementChild as HTMLFormElement this.element.id = 'user-input' this.titleInputElement = this.element.querySelector('#title') as HTMLInputElement this.descriptionInputElement = this.element.querySelector('#description') as HTMLInputElement this.peopleInputElement = this.element.querySelector('#people') as HTMLInputElement this.configure() this.attach() } @autobind private submitHandler(event: Event) { event.preventDefault() } private configure() { this.element.addEventListener('submit', this.submitHandler) } private attach() { this.hostElement.insertAdjacentElement('afterbegin', this.element) } } const prjInput = new ProjectInput()submitHandler에@autobind를 추가
수동으로
bind를 호출할 필요없이, 데코레이터를 통해 많은 메서드에 적용할 수 있고 시간 절약과 잠재적인 오류를 방지할 수 있음
사용자 입력 가져오기
- 필수 입력 값을 취합해서
council에 제출할 수 있음 - 사용자의 모든 입력을 취합해서, 자유 튜플값으로써의 반환 타입을 규정
app.ts
class ProjectInput {
// ...
private gatherUserInput(): [string, string, number] | void {
const enteredTitle = this.titleInputElement.value
const enteredDescription = this.descriptionInputElement.value
const enteredPeople = this.peopleInputElement.value
if (
enteredTitle.trim().length === 0 ||
enteredDescription.trim().length === 0 ||
enteredPeople.trim().length === 0
) {
alert('Invalid input, please try again!')
return
} else {
return [enteredTitle, enteredDescription, +enteredPeople]
}
}
private clearInputs() {
this.titleInputElement.value = ''
this.descriptionInputElement.value = ''
this.peopleInputElement.value = ''
}
@autobind
private submitHandler(event: Event) {
event.preventDefault()
const userInput = this.gatherUserInput()
if (Array.isArray(userInput)) {
const [title, desc, people] = userInput
console.log(title, desc, people)
}
}
}
private gatherUserInput(): [string, string, number] | void{}- 튜플은 세개의 구성요소를 가짐
- 첫 번째와 두 번째 구성요소는 열이고 세번째 구성요소는 숫자를 가져옴
- 값의 타이틀 입력 요소에서 모든 입력 항목을 얻음(
title,description,people)- 취득한
title,description,people값의 여백을 제거하여 각각의 값이 길이가 0일 경우에 유효하지 않은input값임을 알려줌 - 값이 유효하다면 튜플을 반환함
enteredPeople값의 경우+를 이용해number type으로 변환
- 취득한
if (Array.isArray(userInput)){}- 사용자 입력 타입이 튜플과 등가인지 확인할 방법이 없음
- 튜플은 자바스크립트가 아니므로 튜플인지 여부를 확인하려면 결국 튜플은 배열임을 기억해야함
- 타입스크립트에서는 특별하지만 자바스크립트 영역에 있으면 배열임
Array.isArray(userInput)- 튜플은 배열이므로
destructuring으로userInput의 값을 추출
- 튜플은 배열이므로
- 사용자 입력 타입이 튜플과 등가인지 확인할 방법이 없음
private clearInputs() {}- 모든 입력값을 비우는 함수를 정의
재사용 가능한 검증 기능 생성
검증 객체 구조 정의
// validation
interface Validatable {
value: string | number
required?: boolean
minLength?: number
maxLength?: number
min?: number
max?: number
}
- 검증 가능한 객체 구조를 정의
- 검증 함수가 대상을 인지하고 속성 등을 정확하게 추출하기 위해서 인터페이스를 정의
required- 필수 명제로는
booleanvalue를 취함
- 필수 명제로는
minLength, maxLengthinput value(문자열)의 길이
min, max- 수치 값이 특정 숫자 이싱인지, 특정 최대값 이하인지 확인
- 수치외에는 모두 선택 사항이어야함
- ?(
Optional Operator)를 사용
- ?(
검증 함수 정의
function validate(validatableInput: Validatable) {
let isValid = true
if (validatableInput.required) {
isValid = isValid && validatableInput.value.toString().trim().length !== 0
}
if (validatableInput.minLength != null && typeof validatableInput.value === 'string') {
isValid = isValid && validatableInput.value.length >= validatableInput.minLength
}
if (validatableInput.maxLength != null && typeof validatableInput.value === 'string') {
isValid = isValid && validatableInput.value.length <= validatableInput.maxLength
}
if (validatableInput.min != null && typeof validatableInput.value === 'number') {
isValid = isValid && validatableInput.value > validatableInput.min
}
if (validatableInput.max != null && typeof validatableInput.value === 'number') {
isValid = isValid && validatableInput.value < validatableInput.max
}
return isValid
}
위에서 정의한 인터페이스의 구조를 가지는
validatable한validatableInput를 인자로 받음초기에 참(
true) 값인 유효한 변수를 생성- 디폴트는
true값이지만 검증 중 하나라도 실패하면false로 설정
- 디폴트는
필수 값(required) 검증if (validatableInput.required) { isValid = isValid && validatableInput.value.toString().trim().length !== 0; }input value가 공란임을 검증하는 조건식isValid는 초기값으로true이므로,validatableInput.value값을 검증- 문자열인지 명확하지 않으므로,
toString()메서드를 통해 문자열로 변환하고trim()메서드와 문자열의 길이인length가 0이 아님을 검증
- 문자열인지 명확하지 않으므로,
minLength,maxLength검증if (validatableInput.minLength != null && typeof validatableInput.value === 'string') { isValid = isValid && validatableInput.value.length >= validatableInput.minLength } if (validatableInput.maxLength != null && typeof validatableInput.value === 'string') { isValid = isValid && validatableInput.value.length <= validatableInput.maxLength }input value의 최소, 최대 길이가null이 아니고,string인type임을 검증하는 조건식- 특정 최소 혹은 최대 길이가
null이나undefined가 같지 않음을 검증validatableInput.minLength != null,validatableInput.maxLength != null
- 특정 최소 길이와, 최대 길이가 문자열의 길이 조건에 부합해야 함
- 타입 가드를 추가해서 문자열과 등가인지 확인
validatableInput.value가 최소 길이보다 작거나, 최대 길이보다 큰지 확인isValid = isValid && validatableInput.value.length <= validatableInput.minLength;isValid = isValid && validatableInput.value.length <= validatableInput.maxLength;
min,max값 검증if (validatableInput.min != null && typeof validatableInput.value === 'number') { isValid = isValid && validatableInput.value > validatableInput.min } if (validatableInput.max != null && typeof validatableInput.value === 'number') { isValid = isValid && validatableInput.value < validatableInput.max }- 위의 검증 로직과 유사하게 최소, 최대 값이
numbertype이며,null또는undefined를 가지지 말아야 함
- 위의 검증 로직과 유사하게 최소, 최대 값이
검증 가능 객체 구성
class ProjectInput {
// ...
private gatherUserInput(): [string, string, number] | void {
const enteredTitle = this.titleInputElement.value
const enteredDescription = this.descriptionInputElement.value
const enteredPeople = this.peopleInputElement.value
const titleValidatable: Validatable = {
value: enteredTitle,
required: true,
}
const descriptionValidatable: Validatable = {
value: enteredDescription,
required: true,
minLength: 5,
}
const peopleValidatable: Validatable = {
value: +enteredPeople,
required: true,
min: 1,
max: 5,
}
if (
!validate(titleValidatable) ||
!validate(descriptionValidatable) ||
!validate(peopleValidatable)
) {
alert('Invalid input, please try again!')
return
} else {
return [enteredTitle, enteredDescription, +enteredPeople]
}
}
}
titleValidatable,descriptionValidatable,peopleValidatable를 선언하고 Validatable 인터페이스의 구조를 가지는 값을 할당
if (
!validate(titleValidatable) ||
!validate(descriptionValidatable) ||
!validate(peopleValidatable)
) {
alert("Invalid input, please try again!");
return;
} else {
return [enteredTitle, enteredDescription, +enteredPeople];
}
validate함수에 검증하고자 하는 값을 매개변수로 넘겨줌- validate 함수 조건문 중 하나라도
false라면Invalid input, please try again!경고창을 노출
Referenced