📌 Drag & Drop
📖 Drag & Drop 구현을 위한 인터페이스 활용하기
💎 app.ts
interface Draggable {
dragStartHandler(event: DragEvent): void;
dragEndHandler(event: DragEvent): void;
}
interface DragTarget {
dragOverHandler(event: DragEvent): void;
dropHandler(event: DragEvent): void;
dragLeaveHandler(event: DragEvent): void;
}
class ProjectItem
extends Component<HTMLUListElement, HTMLLIElement>
implements Draggable
{
private project: Project;
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.configures();
this.renderContent();
}
@autobind
dragStartHandler(event: DragEvent) {
console.log(event);
}
dragEndHandler(_: DragEvent) {
console.log("DragEnd");
}
configures() {
this.element.addEventListener("dragstart", this.dragStartHandler);
this.element.addEventListener("dragend", this.dragEndHandler);
}
renderContent() {
this.element.querySelector("h2")!.textContent = this.project.title;
this.element.querySelector("h3")!.textContent = this.persons + " assigned";
this.element.querySelector("p")!.textContent = this.project.description;
}
}
- DragTarget의 dragOverHandler : 드래그하는 대상이 유효한 드래그 타깃이라는 것을 브라우저와 자바스크립트에 알려줘야함. → 드롭을 할 수 있게
- DragTarget의 dropHandler : 실제 드롭이 일어나면 반응하는 역할 → 드롭에 대한 처리
- DragTarget의 dragLeaveHandler : 사용자가 드래그 했을 때 배경색을 바꾼다던지 시각적인 피드백 제공에 유용
💎 index.html
<template id="single-project">
<li draggable="true">
<h2></h2>
<h3></h3>
<p></p>
</li>
</template>
- 해당 아이템이 드래그를 할 수 있도록 속성
draggable을 참으로 설정

📖 드래그 이벤트 및 UI의 현재 상태 반영하기
class ProjectList
extends Component<HTMLDivElement, HTMLElement>
implements DragTarget
{
assignedProjects: Project[];
constructor(private type: "active" | "finished") {
super("project-list", "app", false, `${type}-projects`);
this.assignedProjects = [];
this.configures();
this.renderContent();
}
@autobind
dragOverHandler(_: DragEvent) {
const listEl = this.element.querySelector("ul")!;
listEl.classList.add("droppable");
}
@autobind
dragLeaveHandler(_: DragEvent) {
const listEl = this.element.querySelector("ul")!;
listEl.classList.remove("droppable");
}
dropHandler(_: DragEvent) {}
configures() {
this.element.addEventListener("dragover", this.dragOverHandler);
this.element.addEventListener("dragleave", this.dragLeaveHandler);
this.element.addEventListener("drop", this.dropHandler);
projectState.addListener((projects: Project[]) => {
console.log(projects);
const relevantProjects = projects.filter((prj) => {
if (this.type === "active") {
return prj.status === ProjectStatus.Active;
} else {
return prj.status === ProjectStatus.Finished;
}
});
this.assignedProjects = relevantProjects;
this.renderProjects();
});
}
renderContent() {
const listId = `${this.type}-projects-list`;
this.element.querySelector("ul")!.id = listId;
this.element.querySelector("h2")!.textContent =
this.type.toUpperCase() + " PROJECTS";
}
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);
}
}
}

📖 드롭할 수 있는 영역 추가하기
💎 ProjectItem
class ProjectItem
extends Component<HTMLUListElement, HTMLLIElement>
implements Draggable
{
private project: Project;
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.configures();
this.renderContent();
}
@autobind
dragStartHandler(event: DragEvent) {
console.log(event);
event.dataTransfer!.setData("text/plain", this.project.id);
event.dataTransfer!.effectAllowed = "move";
}
dragEndHandler(_: DragEvent) {
console.log("DragEnd");
}
configures() {
this.element.addEventListener("dragstart", this.dragStartHandler);
this.element.addEventListener("dragend", this.dragEndHandler);
}
renderContent() {
this.element.querySelector("h2")!.textContent = this.project.title;
this.element.querySelector("h3")!.textContent = this.persons + " assigned";
this.element.querySelector("p")!.textContent = this.project.description;
}
}
event.dataTransfer : drag 이벤트의 특수 속성 → 이 속성을 이용해 DragEvent에 데이터를 첨부할 수 있다.
event.dataTransfer.effectAllowed : 커서의 모양을 제어하는 역할
💎 ProjectList
class ProjectList
extends Component<HTMLDivElement, HTMLElement>
implements DragTarget
{
assignedProjects: Project[];
constructor(private type: "active" | "finished") {
super("project-list", "app", false, `${type}-projects`);
this.assignedProjects = [];
this.configures();
this.renderContent();
}
@autobind
dragOverHandler(event: DragEvent) {
if (event.dataTransfer && event.dataTransfer.types[0] === "text/plain") {
event.preventDefault();
const listEl = this.element.querySelector("ul")!;
listEl.classList.add("droppable");
}
}
@autobind
dragLeaveHandler(_: DragEvent) {
const listEl = this.element.querySelector("ul")!;
listEl.classList.remove("droppable");
}
dropHandler(event: DragEvent) {
console.log(event.dataTransfer?.getData("text/plain"));
}
configures() {
this.element.addEventListener("dragover", this.dragOverHandler);
this.element.addEventListener("dragleave", this.dragLeaveHandler);
this.element.addEventListener("drop", this.dropHandler);
projectState.addListener((projects: Project[]) => {
console.log(projects);
const relevantProjects = projects.filter((prj) => {
if (this.type === "active") {
return prj.status === ProjectStatus.Active;
} else {
return prj.status === ProjectStatus.Finished;
}
});
this.assignedProjects = relevantProjects;
this.renderProjects();
});
}
renderContent() {
const listId = `${this.type}-projects-list`;
this.element.querySelector("ul")!.id = listId;
this.element.querySelector("h2")!.textContent =
this.type.toUpperCase() + " PROJECTS";
}
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);
}
}
}
if (event.dataTransfer && event.dataTransfer.types[0] === "text/plain") : 드래그 이벤트에 첨부된 데이터가 text/plain 형식인지 검사
event.preventDefault() : 자바스크립트의 기본값은 드래그를 허용하지 않음. → 드래그를 허용한다!

📖 Drag & Drop 마무리
💎 ProjectList
class ProjectList
extends Component<HTMLDivElement, HTMLElement>
implements DragTarget
{
assignedProjects: Project[];
constructor(private type: "active" | "finished") {
super("project-list", "app", false, `${type}-projects`);
this.assignedProjects = [];
this.configures();
this.renderContent();
}
@autobind
dragOverHandler(event: DragEvent) {
if (event.dataTransfer && event.dataTransfer.types[0] === "text/plain") {
event.preventDefault();
const listEl = this.element.querySelector("ul")!;
listEl.classList.add("droppable");
}
}
@autobind
dragLeaveHandler(_: DragEvent) {
const listEl = this.element.querySelector("ul")!;
listEl.classList.remove("droppable");
}
@autobind
dropHandler(event: DragEvent) {
const prjId = event.dataTransfer?.getData("text/plain")!;
projectState.moveProject(
prjId,
this.type === "active" ? ProjectStatus.Active : ProjectStatus.Finished
);
}
configures() {
this.element.addEventListener("dragover", this.dragOverHandler);
this.element.addEventListener("dragleave", this.dragLeaveHandler);
this.element.addEventListener("drop", this.dropHandler);
projectState.addListener((projects: Project[]) => {
console.log(projects);
const relevantProjects = projects.filter((prj) => {
if (this.type === "active") {
return prj.status === ProjectStatus.Active;
} else {
return prj.status === ProjectStatus.Finished;
}
});
this.assignedProjects = relevantProjects;
this.renderProjects();
});
}
renderContent() {
const listId = `${this.type}-projects-list`;
this.element.querySelector("ul")!.id = listId;
this.element.querySelector("h2")!.textContent =
this.type.toUpperCase() + " PROJECTS";
}
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);
}
}
}
💎 ProjectState
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);
this.updateListerers();
}
moveProject(projectId: string, newState: ProjectStatus) {
const project = this.projects.find((prj) => prj.id === projectId);
if (project && project.status !== newState) {
project.status = newState;
this.updateListerers();
}
}
private updateListerers() {
for (const listenerFn of this.listeners) {
listenerFn(this.projects.slice());
}
}
}

🔗 레파지토리에서 보기
🔗 MDN | Drag & Drop API