기본 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>
DOM 요소 선택 및 OOP 렌더링
class ProjectInput {
// 1
templateElement: HTMLTemplateElement;
hostElement: HTMLDivElement;
element: HTMLFormElement;
constructor() {
this.templateElement = document.getElementById(
"project-input"
// 2 3
)! as HTMLTemplateElement;
this.hostElement = document.getElementById("app")! as HTMLDivElement;
// 4
const importedNode = document.importNode(
this.templateElement.content,
true
);
// 5
this.element = importedNode.firstElementChild as HTMLFormElement;
this.attach();
}
// 6
private attach() {
this.hostElement.insertAdjacentElement("afterbegin", this.element);
}
}
const prjInput = new ProjectInput();
- 저장될 타입을 명확히 합니다.
- 느낌표를 붙여서 타입스크립트에게 모든 것이 괜찮고 그 ID로 엘리멘트에 접근할 것이라고 말합니다.
- getElementById는 어떤 엘리멘트가 마지막에 반환될지 모르기에 타입 캐스팅을 이용해 알려줍니다.
- 탬플릿 코드 사이의 html 코드를 참조하고 깊은 복사를 위해 true로 설정합니다.
- template 안에 있는 form을 지정합니다.
- hostElement 여는 태그가 시작되는 곳에(afterbegin) 삽입합니다.
DOM 요소와 상호 작용
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;
// 1
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();
}
// 3
private submitHandler(event: Event) {
event.preventDefault();
console.log(this.titleInputElement.value);
}
// 2
private configure() {
this.element.addEventListener("submit", this.submitHandler.bind(this));
}
private attach() {
this.hostElement.insertAdjacentElement("afterbegin", this.element);
}
}
const prjInput = new ProjectInput();
- element에 스타일링을 위해 ID를 연결합니다.
- bind를 통해 this가 submitHandler가 아닌 클래스를 참조하게 합니다.
- 유효성 검사를 위한 메서드이고 preventDefault를 호출하여 기본 양식 제출을 방지합니다.
Autobind 데코레이터 생성 및 사용하기
위 2번인 bind 이용을 데코레이터를 통해 구현해 봅니다.
// 1 1-5
function autobind(_: any, _2: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value; // 1-1
const adjDescriptor: PropertyDescriptor = { // 1-2
configurable: true, // 1-3
get() { // 1-4
const boundFn = originalMethod.bind(this);
return boundFn;
},
};
return adjDescriptor;
}
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();
}
// 2
@autobind
private submitHandler(event: Event) {
event.preventDefault();
console.log(this.titleInputElement.value);
}
private configure() {
this.element.addEventListener("submit", this.submitHandler);
}
private attach() {
this.hostElement.insertAdjacentElement("afterbegin", this.element);
}
}
const prjInput = new ProjectInput();
- autobind 데코레이터 함수를 생성합니다.
- descriptor.value를 통해 원래 메서드에 접근합니다.
- adjDescriptor를 생성하여 PropertyDescriptor를 수정합니다.
- configurable을 true로 설정하여 언제든 수정할 수 있게 합니다.
- 게터 함수를 통해 바인드를 설정합니다.
- _를 이용하여 이 값들을 사용하지 않을 것을 알고 있지만 다음 인자가 필요하기에 이 값들을 수용하게 할 수 있습니다.
- 데코레이터를 연결합니다.
사용자 입력 가져오기
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;
}
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();
}
// 1 2
private gatherUserInput(): [string, string, number] | undefined {
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]; // 3
}
}
// 5
private clearInputs() {
this.titleInputElement.value = '';
this.descriptionInputElement.value = '';
this.peopleInputElement.value = '';
}
@autobind
private submitHandler(event: Event) {
event.preventDefault();
// 4
const userInput = this.gatherUserInput();
if (Array.isArray(userInput)) {
const [title, desc, people] = userInput;
console.log(title, desc, people);
this.clearInputs();
}
}
private configure() {
this.element.addEventListener("submit", this.submitHandler);
}
private attach() {
this.hostElement.insertAdjacentElement("afterbegin", this.element);
}
}
const prjInput = new ProjectInput();
- 입력 정보를 저장할 메서드를 생성합니다.
- 기본적으로 튜플 형태로 반환하나 유효성 검증을 통과하지 못할 경우 반환 값이 없기에 undefined를 유니언 타입으로 설정해 줍니다.
- 입력 요소에서 추출한 값은 사실상 디폴트가 텍스트이기 때문에 +를 붙여 숫자로 바꿔줍니다.
- 튜플은 기본적으로 자바스크립트에서 배열이기 때문에 userInput이 배열일 경우 즉, 유효성 검증을 통과한 경우로 조건을 설정합니다.
- 입력 시 입력 창을 초기화하는 메서드를 만들고 연결합니다.
재사용 가능한 검증 기능 생성
// 1
interface Validatable {
value: string | number;
required?: boolean;
minLength?: number;
maxLength?: number;
min?: number;
max?: number;
}
function validate(validatableInput: Validatable) {
let isValid = true;
// 2
if (validatableInput.required) {
isValid = isValid && validatableInput.value.toString().trim().length !== 0;
}
// 3
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;
}
...
class ProjectInput {
...
private gatherUserInput(): [string, string, number] | undefined {
const enteredTitle = this.titleInputElement.value;
const enteredDescription = this.descriptionInputElement.value;
const enteredPeople = this.peopleInputElement.value;
// 4
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];
}
}
...
}
재사용 가능한 검증을 만들기 위해 클래스 위에 다음과 같이 작성합니다.
- 객체의 기본 구조를 인터페이스를 통해 만듭니다.
- 필수 조건일 경우 길이가 0이면 false를 반환하게 합니다.
- 문자열일 경우 즉, 프로젝트에서 title과 description에서 최소 길이, 최대 길이를 설정할 수 있습니다. minLength가 0으로 설정되면 falsy 한 값이 되기에 위 설정에서 꼬일 수 있기에 != null을 추가해 null과 undefined과 아닐 경우를 추가합니다.
- 각 입력 데이터마다 검증 조건 객체를 설정합니다.