본문 바로가기
TYPESCRIPT

연습 프로젝트 4

by 일태찡 2023. 4. 4.

 

드로그 앤 드롭 구현을 위한 인터페이스 활용하기

 

// 1
interface Draggable {
  dragStartHandler(event: DragEvent): void;
  dragEndHandler(event: DragEvent): void;
}

...

class ProjectItem
  extends Component<HTMLUListElement, HTMLLIElement>
  implements Draggable // 2
{
  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.configure();
    this.renderContent();
  }

  // 3
  @autobind
  dragStartHandler(event: DragEvent) {
    console.log(event);
  }

  dragEndHandler(_: DragEvent) {
    console.log("DragEnd");
  }

  // 4
  configure() {
    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;
  }
}

 

  1. 드래깅 가능한 요소를 렌더링 하는 클래스에 더하는 인터페이스를 만듭니다.
  2. 아이템 클래스와 인터페이스를 연결합니다.
  3. 이벤트리스너가 this가 되지 않기 위해 예전에도 만든 데코레이터를 이용합니다.
  4. 렌더링 요소에 접근해 이벤트를 추가합니다.

 

    <template id="single-project">
      <li draggable="true"> // 여기
        <h2></h2>
        <h3></h3>
        <p></p>
      </li>
    </template>

 

html 파일에서 드래그할 대상의 속성을 설정해야 합니다.

그럼 다음과 같이 드래그할 대상을 잡고 내릴 수 있게 됩니다.

 

 

 

드래그 이벤트 및 UI의 현재 상태 반영하기

 

// 1
interface DragTarget {
  dragOverHandler(event: DragEvent): void;
  dropHandler(event: DragEvent): void;
  dragLeaveHandler(event: DragEvent): void;
}

...

class ProjectList
  extends Component<HTMLDivElement, HTMLElement>
  implements DragTarget // 2
{
  assignedProjects: Project[];

  constructor(private type: "active" | "finished") {
    super("project-list", "app", false, `${type}-projects`);
    this.assignedProjects = [];
    this.configure();
    this.renderContent();
  }

  // 3
  @autobind
  dragOverHandler(_: DragEvent) {
    // 5
    const listEl = this.element.querySelector('ul')!;
    listEl.classList.add('droppable');
  }
  
  @autobind
  dragLeaveHandler(_: DragEvent) {
    // 6
    const listEl = this.element.querySelector('ul')!;
    listEl.classList.remove('droppable');
  }  

  configure() {
    this.element.addEventListener('dragover', this.dragOverHandler);// 4
    this.element.addEventListener('dragleave', this.dragLeaveHandler);
    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();
    });
  }

  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);
    }
  }
}

 

  1. 드래그와 드롭을 할 클래스에 인터페이스를 만듭니다.
  2. 리스트 클래스에 인터페이스를 연결합니다.
  3. 이벤트리스너가 this가 되지 않기 위해 예전에도 만든 데코레이터를 이용합니다.
  4. 렌더링 요소에 접근해 이벤트를 추가합니다.
  5. 드래그할 대상이 머무는 공간에 클래스 이름을 추가해 주는 코드입니다. 
  6. 드래그할 대상이 떠난 공간에 클래스 이름을 삭제하는 코드입니다.

 

 5-6번은 다음과 같이 css 파일이 들어있기 때문에 구성하였습니다.

 

.droppable {
  background: #ffe3ee;
}

#finished-projects .droppable {
  background: #d6e1ff;
}

 

다음과 같이 드롭이 가능한 장소에 색이 추가되어 더 알아보기 쉽게 합니다.

 

 

 

드롭할 수 있는 영역 추가하기

 

class ProjectItem
  extends Component<HTMLUListElement, HTMLLIElement>
  implements Draggable
{

  ...
  
  @autobind
  dragStartHandler(event: DragEvent) {
    event.dataTransfer!.setData("text/plain", this.project.id); // 1
    event.dataTransfer!.effectAllowed = "move"; // 2
  }

  dragEndHandler(_: DragEvent) {
    console.log("DragEnd");
  }

  configure() {
    this.element.addEventListener("dragstart", this.dragStartHandler);
    this.element.addEventListener("dragend", this.dragEndHandler);
  }
  
  ...
    
  }
}

class ProjectList
  extends Component<HTMLDivElement, HTMLElement>
  implements DragTarget
{

  ...
  
  @autobind
  dragOverHandler(event: DragEvent) {
    // 3
    if (event.dataTransfer && event.dataTransfer.types[0] === "text/plain") {
      event.preventDefault(); // 4
      const listEl = this.element.querySelector("ul")!;
      listEl.classList.add("droppable");
    }
  }

  @autobind
  dragLeaveHandler(_: DragEvent) {
    const listEl = this.element.querySelector("ul")!;
    listEl.classList.remove("droppable");
  }

  // 5
  dropHandler(event: DragEvent) {
    console.log(event.dataTransfer!.getData('text/plain'));
  }

  configure() {
    this.element.addEventListener("dragover", this.dragOverHandler);
    this.element.addEventListener("dragleave", this.dragLeaveHandler);
    this.element.addEventListener("drop", this.dropHandler);
    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();
    });
  }

  ...
  
}

 

  1. 드래그 이벤트의 데이터 전송 프로퍼티이고 setData의 첫 번째 인수는 데이터 포맷의 식별자, 두 번째 인수는 데이터입니다.
  2. 커서의 모양을 조절하고 브라우저에 의도를 더 잘 알려줄 수 있습니다.
  3. 드롭이 가능한 영역에만 데이터 전송을 하게 세운 데이터 포맷을 조건으로 연결합니다.
  4. 자바스크립트 드래그 앤 드롭 이벤트의 디폴트는 드롭을 허용하지 않기 때문에 그 디폴트를 막아줘야 합니다.
  5. 커서를 드롭했을 때 데이터를 전달하는 함수입니다.

 

 

위와 같이 드롭했을 때 콘솔창에 어떤 숫자가 찍히는 걸 볼 수 있습니다.

그건 바로 랜덤으로 생성한 id였고 잘 넘어왔습니다!

 

 

 

드로그 앤 드롭 마무리하기

 

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.updateListeners(); // 2
  }

  // 1
  moveProject(projectId: string, newStatus: ProjectStatus) {
    const project = this.projects.find((prj) => prj.id === projectId);
    if (project && project.status !== newStatus) { // 4
      project.status = newStatus;
      this.updateListeners(); // 2
    }
  }

  // 2
  private updateListeners() {
    for (const listenerFn of this.listeners) {
      listenerFn(this.projects.slice());
    }
  }
}

...

class ProjectList
  extends Component<HTMLDivElement, HTMLElement>
  implements DragTarget
{

  ...

  @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");
    // 3
    projectState.moveProject(
      prjId,
      this.type === "active" ? ProjectStatus.Active : ProjectStatus.Finished
    );
  }

  configure() {
    this.element.addEventListener("dragover", this.dragOverHandler);
    this.element.addEventListener("dragleave", this.dragLeaveHandler);
    this.element.addEventListener("drop", this.dropHandler);
    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();
    });
  }

  ...

}

...

 

  1. 프로젝트 상태 클래스에 변경 메서드를 추가합니다.
  2. 상태를 갱신하는 새로운 메서드가 생겼기에 기존에 생성자 함수에 있던 리스너 갱신 함수를 따로 빼고 생성자와 변경 메서드에 추가합니다.
  3. 추가한 메서드를 연결합니다.
  4. 조건을 추가하여 같은 곳에 드롭할 경우 리렌더링을 방지합니다.

 

짜잔...

 

 

 

드래그 앤 드롭 API 레퍼런스

https://developer.mozilla.org/ko/docs/Web/API/HTML_Drag_and_Drop_API

 

HTML 드래그 앤 드롭 API - Web API | MDN

HTML 드래그 앤 드롭 인터페이스는 파이어폭스와 다른 브라우저에서 어플리케이션이 드래그 앤 드롭 기능을 사용하게 해줍니다. 이 기능을 이용해 사용자는 draggable 요소를 마우스로 선택해 droppa

developer.mozilla.org

'TYPESCRIPT' 카테고리의 다른 글

웹팩(Webpack) 사용하기  (6) 2023.04.07
모듈과 네임스페이스  (6) 2023.04.06
연습 프로젝트 3  (6) 2023.04.03
연습 프로젝트 2  (13) 2023.03.31
연습 프로젝트 1  (6) 2023.03.30