# 개요
Ionic 프레임워크 상에서 Drag & Drop 을 통한 Sortable한 리스트를 구현하는 방법은, 크게 두가지가 존재한다.
Ionic 기본 제공 : <ion-reorder-group>, <ion-reorder-item> 태그를 사용하여, 간편하게 구현
ng2-dragula : node angular 패키지를 설치하여, 구현
기본 제공되는 태그로 손쉽게, 아이콘 문제까지 해결할 수 있으나, 더 고차원적인 drag and drop이 필요한 관계로, 기본 기능을 쓰되, 고차원 기능은 3rd party 라이브러리를 사용하게 되었다.

# ng2-dragula 설치
npm install ng2-dragula —save

# dragula 모듈 global import
app/app.module.ts
...
Import { DragulaModule } from ‘ng2-dragula’

@NgModule({
Import: [
 ...
 DragulaModule.forRoot() 
,
 ...
]
...
})

# src/polyfill.ts 파일 수정
아래의 코드 추가 (패키지 상의 문제로 아래의 코드를 임시로 추가해주어야함)
(window as any).global = window;

# src/global.scss에 스타일 추가
이제는 Drag 애니메이션을 나이스 하게 보여줄 스타일 적용이 필요하다.
nodemodule 내의 dragula.css 스타일을 global 적용할 수도 있으나, 아래의 스타일 적용을 추천한다.

/* in-flight clone */
.gu-mirror {
  position: fixed !important;
  margin: 0 !important;
  z-index: 9999 !important;
  opacity: 0.8;
  -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";
  filter: alpha(opacity=80);
  pointer-events: none;
}
 
/* high-performance display:none; helper */
.gu-hide {
  left: -9999px !important;
}
 
/* added to mirrorContainer (default = body) while dragging */
.gu-unselectable {
  -webkit-user-select: none !important;
  -moz-user-select: none !important;
  -ms-user-select: none !important;
  user-select: none !important;
}
 
/* added to the source element while its mirror is dragged */
.gu-transit {
  opacity: 0.2;
  -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";
  filter: alpha(opacity=20);
}

여기까지가 Dragula 패키지의 기본 설정이다.

# Drag & Drop 로직 추가하기
페이지 중 하나에 Drag & Drop 기능을 적용하기를 원한다면, Dragula 모듈 파일을 해당 페이지의 Module 파일에 import 해주어야 한다.
app/home/home.module.ts
...
import { DragulaModule } from 'ng2-dragula';
...
@NgModule({
  imports: [
    ...
    DragulaModule
  ],
  ...
})
...
위까지 작업이 끝났다면, 본격적으로 패키지를 가지고, 작업이 가능하다.

# Dragula 패키지의 메인 기능
같은 Dragula name을 공유하고 있다면, 여러 Group들 간에 Object를 이동시키는 것이 가능하다.
해당 예시는 2가지의 다른 배열 데이터를 가지고 실습을 수행할 것이다.
Dragula name은 "tasks"이다.  2가지의 그룹의 dragula name은 모두 "tasks"로 통일한다.


# Dragula Service를 통해, 페이지 내 Drag Event 구독하기
가장 먼저 Page component의 constructor에 DragulaService를 추가해줌으로써, Drag Event를 손쉽게 구독할 수 있다.
- drag() : 하나의 아이템이 현재 Dragging 중일 때, 호출된다. background를 바꾸는 등의 조치를 가할 수 있다.
- removeModel() : 하나의 아이템이 어떠한 Group에도 포함되지 않는 곳으로  Dropped 될 떄, 호출된다. 아이템은 소멸되며, 작은 Toast 메시지를 보내는 등의 조치를 가할 수 있다.
- dropModel() : 하나의 아이템이 자기 자신이 아닌 새로운 Group에 Dropped 될 때 호출된다. 다시 background를 정상으로 되돌리는 조치를 가할 수 있다.
- createGroup() : group에 대한, 여러가지 기본 제공 옵션값을 변경할 수 있다. 대표적으로 spilled 속성과 revert 속성이 있다. spilled 속성을 지정할 경우, group들 바깥으로, drop할 경우 아이템이 제거 된다. 반면 revert 속성의 경우, 다시 제자리로 돌아오게 된다.

참고로, background를 바꾸는 방법은 두가지 정도가 존재한다. HTML item의 color css 속성을 설정하든지, 아니면 [color] 프로퍼티를 변경하는 것이다.
또한, 해당 Service 제공 함수들은 다양한 값들을 제공하지만, 필요한 값만 골라서 사용가능하다.
app/home/home.page.ts 중

import { Component, OnDestroy} from '@angular/core';
import { DragulaService } from 'ng2-dragula';
import { ToastController } from '@ionic/angular';
import { Subscription } from 'rxjs';
 
...

export class HomePage implement OnDestroy {
  todos = [
    { value: 'Buy Milk', color: 'primary' },
    { value: 'Write new Post', color: 'primary' }
  ];
  todos2 = [
    { value: 'Schedule newsletter', color: 'secondary' },
    { value: 'Find new Ionic Academy topics', color: 'secondary' }
  ];
  dragSubs = new Subscription();
  constructor(private dragulaService: DragulaService, private toastController: ToastController) {
this.dragulaService.createGroup('todos', { removeOnSpill: true //false
});

//모든 인자값 표시
this.dragSubs.add(this.dragulaService.drag("todos") .subscribe(({ name, el, source }) => { // ... }) ); this.dragSubs.add(this.dragulaService.drop("todos") .subscribe(({ name, el, target, source, sibling }) => { // ... }) );
// some events have lots of properties, just pick the ones you need this.dragSubs.add(this.dragulaService.dropModel("todos") // WHOA // .subscribe(({ name, el, target, source, sibling, sourceModel, targetModel, item }) => { .subscribe(({ sourceModel, targetModel, item }) => { // ... }) ); // You can also get all events, not limited to a particular group this.dragSubs.add(this.dragulaService.drop() .subscribe(({ name, el, target, source, sibling }) => { // ... }) );

//필요한 인자값만 표시
this.dragSubs.add(this.dragulaService.drag("todos") //Observable을 제공
.subscribe(({ name, el, source }) => { el.setAttribute('color', 'danger'); }) ); this.dragSubs.add(this.dragulaService.removeModel("todos") .subscribe(({ item }) => { this.toastController.create({ message: 'Removed: ' + item.value, duration: 2000 }).then(toast => toast.present()); }) );
// some events have lots of properties, just pick the ones you need this.dragSubs.add(this.dragulaService.dropModel("todos") .subscribe(({ item }) => { item['color'] = 'success'; }) );
  }

ngOnDestroy(){ this.dragSubs.unsubscribe(); }
 
}

# Ui 만들기
- View의 최상단에 forceOverscroll 속성을 false로 반드시 지정해주어야, 리스트 내의 아이템을 dragging 하는 동안 이상하게 scroll되는 현상을 막을 수 있으므로 유의한다.
- drag할 각 group 엘리먼트 dragula="그룹명" 이라는 것을 동일하게 적용하는 것이 중요하다.(drag & drop을 원하는 그룹위치에)
- 2 way binding 기능 또한 제공한다. 정보를 포함하고 있는 실제 배열데이터와 Dragula 리스트간의 2way binding을 원할 경우 설정 가능하다.
drag해서 지워질 경우, 실제 데이터도 지워지게 할 수 있음
app/home/home.page.html

...
</ion-header>
 
<ion-content forceOverscroll="false"> //필수!
  <ion-grid>
...
    <ion-button expand="block" fill="outline" (click)="addTodo()">
      <ion-icon name="add" slot="start"></ion-icon>
      Add Todo
    </ion-button>
    <ion-row no-padding class="matrix">
      <ion-col size="6" class="q1">
        <div class="q-header">Do</div>
        <ion-list dragula="todos" [(dragulaModel)]="todos" lines="none">
          <ion-item *ngFor="let item of todos" [color]="item.color" expand="block" text-wrap>
            {{ item.value }}
          </ion-item>
        </ion-list>
      </ion-col>
 
      <ion-col size="6" class="q2">
        <div class="q-header">Schedule</div>
        <ion-list dragula="todos" [(dragulaModel)]="todos2" lines="none">
          <ion-item *ngFor="let item of todos2" [color]="item.color" expand="block" text-wrap>
            {{ item.value }}
          </ion-item>
        </ion-list>
      </ion-col>
    </ion-row>
  </ion-grid>
 
  <ion-row class="delete-area" align-items-center justify-content-center>
    <ion-icon name="trash" color="medium"></ion-icon>
  </ion-row> //The delete area is basically just a UI indication to drop it(그냥 그룹 밖으로 드래깅 하면 지워짐)
 
</ion-content>

# 스타일 지정하기
스타일 또한, group에 해당하는 list element의 경우, 반드시 최소 길이를 지정하거나 높이를 100%로 지정하여, list가 비어 있을지라도, drag & drop할 공간을 남겨주어야 한다.
app/home/home.page.scss
.q1, .q2, .q3, .q4 {
    border: 4px solid #fff;
}
 
.matrix {
    margin-top: 30px;
 
    ion-col {
        --ion-grid-column-padding: 0px;
        min-height: 150px;
    }
 
    .list {
        padding: 0px;
        height: 100%;
    }
 
    ion-item {
        margin-bottom: 2px;
    }
}
 
.q-header {
    background-color: var(--ion-color-light);
    height: 20px;
    text-align: center;
}
 
.delete-area {
    border: 2px dashed var(--ion-color-medium);
    margin: 10px;
    height: 100px;
    ion-icon {
       font-size: 64px;
    }
}


# 참고 : drag option
  <div dragula="DRAGULA_FACTS">
    <div>...</div>
    <div>...</div>
  </div>
  <div dragula="DRAGULA_FACTS">
    <div>...</div>
    <div>...</div>
  </div>
  • 타입 1(핸들 아이콘을 통해 옮기기)
<div class="container" dragula="HANDLES" id="left">
  <div *ngFor="...">
    <span class="handle">...</span>
    <p>Other content<p>
  </div>
</div>

export class HandleComponent {

  public constructor(private dragulaService: DragulaService) {
    dragulaService.createGroup("HANDLES", {
      moves: (el, container, handle) => {
        return handle.className === 'handle';
      }
    });
  }

}

  • 타입2(기본)
<div class='container'0 [dragula]="MANY_ITEMS" [(dragulaModel)]='many'>
    <div *ng0For='let text of many' [innerHtml]='text'></div>
</div>
<div class='container' [dragula]="MANY_ITEMS" [(dragulaModel)]='many2'>
    <div *ngFor='let text of many2' [innerHtml]='text'></div>
</div>

export class NgForComponent {
  MANY_ITEMS = 'MANY_ITEMS';
  public many = ['The', 'possibilities', 'are', 'endless!'];
  public many2 = ['Explore', 'them'];

  subs = new Subscription();

  public constructor(private dragulaService:DragulaService) {
    this.subs.add(dragulaService.dropModel(this.MANY_ITEMS)
      .subscribe(({ el, target, source, sourceModel, targetModel, item }) => {
        console.log('dropModel:');
        console.log(el);
        console.log(source);
        console.log(target);
        console.log(sourceModel);
        console.log(targetModel);
        console.log(item);
      })
    );
    this.subs.add(dragulaService.removeModel(this.MANY_ITEMS)
      .subscribe(({ el, source, item, sourceModel }) => {
        console.log('removeModel:');
        console.log(el);
        console.log(source);
        console.log(sourceModel);
        console.log(item);
      })
    );
  }

  ngOnDestroy() {
    this.subs.unsubscribe();
  }

}

  • 타입3(기본 예시)
<div dragula="DRAGULA_EVENTS"> ... </div>
<div dragula="DRAGULA_EVENTS"> ... </div>

export class EventsComponent {
  BAG = "DRAGULA_EVENTS";
  subs = new Subscription();
  public constructor(private dragulaService: DragulaService) {
    this.subs.add(dragulaService.drag(this.BAG)
      .subscribe(({ el }) => {
        this.removeClass(el, 'ex-moved');
      })
    );
    this.subs.add(dragulaService.drop(this.BAG)
      .subscribe(({ el }) => {
        this.addClass(el, 'ex-moved');
      })
    );
    this.subs.add(dragulaService.over(this.BAG)
      .subscribe(({ el, container }) => {
        console.log('over', container);
        this.addClass(container, 'ex-over');
      })
    );
    this.subs.add(dragulaService.out(this.BAG)
      .subscribe(({ el, container }) => {
        console.log('out', container);
        this.removeClass(container, 'ex-over');
      })
    );
  }

  ngOnDestroy() {
    this.subs.unsubscribe();
  }

  addClass() { ... }
  removeClass() { ... }
}

# 원하는 컴포넌트 페이지에 DargulaService Import
Import { DragulaService } from ’ng2-dragula/ng2-dragula’

Constructor(private dragula : DragulaService){
this.dragula.setOptions(‘bag-items’,{
revertOnSpill : true; //바깥이동시, 다시 제자리로
removeOnSpill : false,
copy...
});

}

# 기타 옵션 적용
  isContainer: function (el) {
    return false; // only elements in drake.containers will be taken into account
  },
  moves: function (el, source, handle, sibling) {
    return true; // elements are always draggable by default
  },
  accepts: function (el, target, source, sibling) {
    return true; // elements can be dropped in any of the `containers` by default
  },
  invalid: function (el, handle) {
    return false; // don't prevent any drags from initiating by default
  },
  direction: 'vertical',             // Y axis is considered when determining where an element would be dropped
  copy: false,                       // elements are moved by default, not copied
  copySortSource: false,             // elements in copy-source containers can be reordered
  revertOnSpill: false,              // spilling will put the element back where it was dragged from, if this is true
  removeOnSpill: false,              // spilling will `.remove` the element, if this is true
  mirrorContainer: document.body,    // set the element that gets mirror elements appended
  ignoreInputTextSelection: true     // allows users to select input text, see details below
# 기타 옵션 설명
invalid: function (el, handle) {
  return el.tagName === 'A';
}


# 출처

https://devdactic.com/ionic-4-drag-drop/

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기