상속 & 제네릭 추가하기
// 1
abstract class Component<T extends HTMLElement, U extends HTMLElement> {
templateElement: HTMLTemplateElement;
hostElement: T;
element: U;
constructor(
templateId: string,
hostElementId: string,
insertAtStart: boolean,
newElementId?: string
) {
this.templateElement = document.getElementById(
templateId
)! as HTMLTemplateElement;
this.hostElement = document.getElementById(hostElementId)! as T;
const importedNode = document.importNode(
this.templateElement.content,
true
);
this.element = importedNode.firstElementChild as U;
if (newElementId) {
this.element.id = newElementId;
}
this.attach(insertAtStart);
}
private attach(insertAtBeginning: boolean) {
this.hostElement.insertAdjacentElement(
insertAtBeginning ? "afterbegin" : "beforeend",
this.element
);
}
// 2
abstract configure(): void;
abstract renderContent(): void;
}
인풋 클래스와 리스트 클래스에 공통적인 부분이 많아 추상 제네릭 클래스를 만들고 리팩터링 합니다.
- 공통적인 세 인자를 집어넣고 클래스마다 hostElement와 element가 다르므로 매개변수로 설정합니다.
- 추상 메서드를 추가해 컴포넌트를 상속받는 모든 클래스는 이 두 메서드를 추가시키게 합니다.
기존 코드 ▼
더보기
class ProjectList {
templateElement: HTMLTemplateElement;
hostElement: HTMLDivElement;
element: HTMLElement;
assignedProjects: Project[];
constructor(private type: 'active' | 'finished') {
this.templateElement = document.getElementById(
'project-list'
)! as HTMLTemplateElement;
this.hostElement = document.getElementById('app')! as HTMLDivElement;
this.assignedProjects = [];
const importedNode = document.importNode(
this.templateElement.content,
true
);
this.element = importedNode.firstElementChild as HTMLElement;
this.element.id = `${this.type}-projects`;
projectState.addListener((projects: Project[]) => {
const relevantProjects = projects.filter(prj => {
if (this.type === 'active') {
return prj.status === ProjectStatus.Active;
}
return prj.status === ProjectStatus.Finished;
});
this.assignedProjects = relevantProjects;
this.renderProjects();
});
this.attach();
this.renderContent();
}
private renderProjects() {
const listEl = document.getElementById(
`${this.type}-projects-list`
)! as HTMLUListElement;
listEl.innerHTML = '';
for (const prjItem of this.assignedProjects) {
const listItem = document.createElement('li');
listItem.textContent = prjItem.title;
listEl.appendChild(listItem);
}
}
private renderContent() {
const listId = `${this.type}-projects-list`;
this.element.querySelector('ul')!.id = listId;
this.element.querySelector('h2')!.textContent =
this.type.toUpperCase() + ' PROJECTS';
}
private attach() {
this.hostElement.insertAdjacentElement('beforeend', this.element);
}
}
class ProjectList extends Component<HTMLDivElement, HTMLElement> {
assignedProjects: Project[];
constructor(private type: "active" | "finished") {
// 1
super("project-list", "app", false, `${type}-projects`); // 2
this.assignedProjects = [];
this.configure(); // 4
this.renderContent();
}
private renderProjects() {
const listEl = document.getElementById(
`${this.type}-projects-list`
)! as HTMLUListElement;
listEl.innerHTML = "";
for (const prjItem of this.assignedProjects) {
const listItem = document.createElement("li");
listItem.textContent = prjItem.title;
listEl.appendChild(listItem);
}
}
// 4
configure() {
projectState.addListener((projects: Project[]) => {
const relevantProjects = projects.filter((prj) => {
if (this.type === "active") {
return prj.status === ProjectStatus.Active;
}
return prj.status === ProjectStatus.Finished;
});
this.assignedProjects = relevantProjects;
this.renderProjects();
});
}
// 3
renderContent() {
const listId = `${this.type}-projects-list`;
this.element.querySelector("ul")!.id = listId;
this.element.querySelector("h2")!.textContent =
this.type.toUpperCase() + " PROJECTS";
}
}
- super 생성자에 정보를 전달해 주고 기존과 겹치는 코드를 전부 삭제합니다.
- super가 끝나기 전까지는 this.type을 사용할 수 없기에 type를 사용해야 합니다. 생성자 함수가 type을 인수로 수용하기 때문에 문제없습니다.
- 추상 클래스에서 공개 메서드로 설정했기에 private를 삭제해야 합니다. (비공개 추상 메서드는 지원이 안됩니다.)
- configure 메서드가 원래 없었기에 선언만 하고 빈칸으로 내버려 두어도 되지만 생성자 함수에 있던 기능을 옮겨와 생성자 함수에 연결해 줬습니다.
기존 코드 ▼
더보기
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 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];
}
}
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;
projectState.addProject(title, desc, people);
this.clearInputs();
}
}
private configure() {
this.element.addEventListener('submit', this.submitHandler);
}
private attach() {
this.hostElement.insertAdjacentElement('afterbegin', this.element);
}
}
class ProjectInput extends Component<HTMLDivElement, HTMLFormElement> {
titleInputElement: HTMLInputElement;
descriptionInputElement: HTMLInputElement;
peopleInputElement: HTMLInputElement;
constructor() {
// 1
super("project-input", "app", true, "user-input");
// 3
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();
}
// 2
configure() {
this.element.addEventListener("submit", this.submitHandler);
}
renderContent () {};
private gatherUserInput(): [string, string, number] | undefined {
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];
}
}
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;
projectState.addProject(title, desc, people);
this.clearInputs();
}
}
}
- super 생성자에 정보를 전달해 주고 기존과 겹치는 코드를 전부 삭제합니다.
- 공개 메서드이기에 private를 삭제하고 보통 공개 메서드는 비공개 메서드보다 앞에 둡니다.
- 리스트 클래스와 다르게 configure 메서드에 넣으면 다음과 같이 생성자 초기화에서 cofigure 메서드를 불러온다는 것을 이해, 확인하지 못한다고 에러가 나옵니다. 따라서 이 초기화는 생성자에 유지합니다.
type Listener<T> = (items: T[]) => void;
class State<T> {
protected listeners: Listener<T>[] = [];
addListener(listenerFn: Listener<T>) {
this.listeners.push(listenerFn);
}
}
class ProjectState extends State<Project> {
private projects: Project[] = [];
private static instance: ProjectState;
private constructor() {
super();
}
static getInstance() {
if (this.instance) {
return this.instance;
}
this.instance = new ProjectState();
return this.instance;
}
addProject(title: string, description: string, numOfPeople: number) {
const newProject = new Project(
Math.random().toString(),
title,
description,
numOfPeople,
ProjectStatus.Active
);
this.projects.push(newProject);
for (const listenerFn of this.listeners) {
listenerFn(this.projects.slice());
}
}
}
상태 클래스도 제네릭과 상속을 이용하여 리팩터링 했습니다.
클래스로 프로젝트 항목 렌더링
class ProjectItem extends Component<HTMLUListElement, HTMLLIElement> {
private project: Project;
constructor(hostId: string, project: Project) {
super('single-project', hostId, false, project.id);
this.project = project;
this.configure();
this.renderContent();
}
configure() {}
renderContent() {
this.element.querySelector('h2')!.textContent = this.project.title;
this.element.querySelector('h3')!.textContent = this.project.people.toString();
this.element.querySelector('p')!.textContent = this.project.description;
}
}
개별 리스트 클래스를 새로 만듭니다. html 파일도 다음과 같이 추가했습니다.
...
<template id="single-project">
<li>
<h2></h2>
<h3></h3>
<p></p>
</li>
...
private renderProjects() {
const listEl = document.getElementById(
`${this.type}-projects-list`
)! as HTMLUListElement;
listEl.innerHTML = "";
for (const prjItem of this.assignedProjects) {
const listItem = document.createElement("li");
listItem.textContent = prjItem.title;
listEl.appendChild(listItem);
}
}
기존에 있던 리스트 클래스에 있는 리스트를 렌더링 하는 메서드를 다음과 같이 고칠 수 있습니다.
private renderProjects() {
const listEl = document.getElementById(
`${this.type}-projects-list`
)! as HTMLUListElement;
listEl.innerHTML = "";
for (const prjItem of this.assignedProjects) {
new ProjectItem(this.element.querySelector('ul')!.id, prjItem);
}
}
게터 사용하기
게터를 잘 활용해서 데이터를 가져오면서 어떻게 변화시킬지 연습해 봅시다.
class ProjectItem extends Component<HTMLUListElement, HTMLLIElement> {
private project: Project;
// 1
get persons() {
if(this.project.people === 1) {
return '1 person';
} else {
return `${this.project.people} persons`;
}
}
constructor(hostId: string, project: Project) {
super('single-project', hostId, false, project.id);
this.project = project;
this.configure();
this.renderContent();
}
configure() {
}
renderContent() {
this.element.querySelector('h2')!.textContent = this.project.title;
this.element.querySelector('h3')!.textContent = this.persons + ' assigned'; // 2
this.element.querySelector('p')!.textContent = this.project.description;
}
}
배정된 인원에 따른 표현을 다르게 하기 위해 코드를 추가합니다.
- 게터를 생성해 1명이면 person 아니면 그 인원 수가 반환되게 만듭니다.
- 게터를 호출합니다.
다음은 마지막 드래그 앤 드롭 이벤트입니다...
'TYPESCRIPT' 카테고리의 다른 글
모듈과 네임스페이스 (6) | 2023.04.06 |
---|---|
연습 프로젝트 4 (6) | 2023.04.04 |
연습 프로젝트 2 (13) | 2023.03.31 |
연습 프로젝트 1 (6) | 2023.03.30 |
데코레이터 (8) | 2023.03.28 |