D3.js für engagierte Angular-Entwickler, Teil 6: Drag-and-Drop-Animationen mit D3.js

Drag and Drop mit Angular und D3.js-Animationen
Keine Kommentare

Animierte Drag-and-Drop-Funktionen mit Angular und D3.js erstellen: Das hat seine Tücken. Nachdem wir aber schon wissen, wie Drag and Drop mit Angular und eigenem Ghost geht, ist auch die Animation mit D3.js mit diesen Tipps gar nicht mehr so schwer.

In den ersten beiden Teilen „Drag and Drop mit Angular“ und „Ein nützlicher Ghost“ haben wir es bereits geschafft, unsere Drag and Drop Funktionalität sowie unser eigenes Ghost-Element Browser-kompatibel zu implementieren.

Kommen wir endlich zum interessantesten Teil: den Animationen. Wenn ihr noch keine Erfahrungen mit der Realisation von D3.js Animationen in Angular habt, dann empfehle ich, euch zunächst dem Artikel meines Kollegen Stephan Rauh D3.js für engagierten Angular-Entwickler: Bubble-Charts erstellen und insbesondere dem dritten Teil dieser Serie D3.js Animationen mit Angular zu widmen.

D3.js für engagierte Angular-Entwickler – die Artikelserie

Teil 1: Bubble-Charts
Teil 2: Pie-Charts
Teil 3: Animierte Diagramme
Teil 4: Drag & Drop mit Angular
Teil 5: Drag & Drop mit eigenem Ghost
Teil 6: Drag & Drop mit Animation in D3.js

Drag and Drop Animationen mit D3.js

Um D3.js.Methoden verwenden zu können, müssen wir diese erst in unserem Projekt installieren. Ich würde grundsätzlich empfehlen die Typ-Definitionen ebenfalls mit zu installieren.

npm install d3 –save
npm install @types/d3 --save-dev

Anschließend importieren wir die Bibliothek D3.js in unsere Komponente.

import * as d3 from 'd3';

Nun können wir mit der Implementierung unserer Animationen beginnen.

Die Abbruch-Animation

Als erstes beschäftigen wir uns mit der einfacheren Abbruch-Animation. Wenn wir das Element außerhalb der Landezone loslassen, soll es animiert zurück an seine Ursprungsposition gleiten. Dazu ergänzen wir in unserem Ereignis dragend einfach eine entsprechende Animation.

Die aktuelle Position des Ghost-Elementes haben wir in unserem Objekt dragItemStyle gespeichert, die ursprüngliche Position des Elementes ist beim Element selbst in dragging hinterlegt. Als nächstes erzeugen wir unsere Interpolationsmethoden und unsere Factory-Funktion.

Das Einblenden des originalen Elementes, sowie das Zurücksetzen des dragging-Objektes kann nun erst nach Abschluss der Animation erfolgen. Dafür können wir das D3.js-Event end verwenden.

public dragEnd(event: DragEvent, item: Item){
  d3.select(event.target as Element)
    .transition()
    .duration(500)
    .attrTween('animate', () => {
      const i = {
        left: d3.interpolate(this.dragItemStyle.left, this.dragging.left),
        top: d3.interpolate(this.dragItemStyle.top, this.dragging.top)
      };
      return (t: number): string => {
        this.dragItemStyle.left = i.left(t);
        this.dragItemStyle.top = i.top(t);
        return '';
      }
    })
    .on('end', () => {
      item.visible = true;
      this.dragging = undefined;
    });
};

Unsere Drop-Animation

Kommen wir nun zur letzten und aufwändigeren Animation: der Drop-Animation. Beim Fallenlassen des Elementes auf unserer Landezone wollen wir, dass unser Ghost-Element per Animation das Aussehen des Zielelementes annimmt.

Dafür müssen wir zunächst die Unterschiede zusammenstellen. In unserem Beispiel sind dies neben der Position (left + top), die Dimension (width + height), die Zeilenhöhe (line-height), sowie der Rahmenradius (border-radius). Dazu ergänzen wir diese innerhalb unseres DragItemStyle-Interface.

interface DragItemStyle {
  left: number;
  top: number;
  width: number;
  height: number;
  lineHeight: number;
  borderRadius: number;
}

In unserer dragStart-Methode müssen wir anschließend die neuen Einträge entsprechend als Ausgangszustand des Ghost definieren. Zur Vereinfachung habe ich diese Werte hier direkt eingetragen. Über die Funktion window.getComputedStyle könnte man diese aber auch automatisch ermitteln.

public dragStart(event: DragEvent, item: Item){
  ...
  this.dragItemStyle = {
    left: item.left,
    top: item.top,
    width: 80,
    height: 80,
    lineHeight: 80,
    borderRadius: 50
  };
  ...
};

Anschließend müssen wir die neuen Style-Eigenschaften noch mit unserem HTML-Template verknüpfen.

<div *ngIf="dragging"
    class="dragging-item"
    [style.background-color]="dragging.color"
    [style.left]="dragItemStyle.left+'px'"
    [style.top]="dragItemStyle.top+'px'"
    [style.width]="dragItemStyle.width+'px'"
    [style.height]="dragItemStyle.height+'px'"
    [style.line-height]="dragItemStyle.lineHeight+'px'"
    [style.border-radius]="dragItemStyle.borderRadius+'%'"
  >
    {{ dragging.text }}
  </div>

Da unser Zielelement aber während der Animation noch nicht sichtbar sein darf, ergänzen wir die visible-Bindung auch für dieses.

<div *ngFor="let item of dropped"
  class="dropped-item"
  [style.background-color]="item.color"
  [style.visibility]="item.visible!==false ? 'visible' : 'hidden'"
>
  {{ item.text }}
</div>

Kommen wir nun endlich zur Animationsroutine. Wir wollen einen fließenden Übergang – unsere Zielelemente in der Landezone haben aber keine absolute Positionierung. Entweder wir berechnen die Zielposition und -dimension von Hand, was aufwändig und fehleranfällig wäre, oder wir lassen den Browser die Arbeit für uns übernehmen. Letzteres klingt besser, oder?

Da wir das Element in unserer drop-Methode bereits von dem Array draggable in das Array dropped verschoben und lediglich per visibility: hidden verborgen haben, können wir über die Methode getBoundingClientRect die entsprechende Position und Dimension abrufen. Um das Zielelement referenzieren zu können, müssen wir sicherstellen, dass Angulars Rendering-Zyklus bereits einmal ausgeführt wurde. Bis unsere attrTween-Methode der Animation ausgeführt wird, ist dies der Fall. Erzeugen wir nun auch unsere Interpolationsfunktionen und die Factory-Funktion innerhalb dieser Methode. Das Einblenden des Zielelementes sowie das Zurücksetzen des dragging Objektes erfolgt wieder nach Abschluss der Animation im D3.js-Event end.

public drop(event: DragEvent){
  event.preventDefault();
  const index = this.draggable.indexOf(this.dragging);
  this.draggable.splice(index, 1);
  this.dropped.push(this.dragging);
  const droppedIndex = this.dropped.length - 1;
  d3.select(event.target as Element)
    .transition()
    .duration(1000)
    .attrTween('animate', () => {
      const target = 
        (this.element.nativeElement as HTMLElement)
        .querySelectorAll('.dropped-item')[droppedIndex];
      const box = target.getBoundingClientRect();
      const i = {
        left: d3.interpolate(this.dragItemStyle.left, box.left),
        top: d3.interpolate(this.dragItemStyle.top, box.top),
        width: d3.interpolate(this.dragItemStyle.width, box.right-box.left),
        height: d3.interpolate(this.dragItemStyle.height, box.bottom-box.top),
        lineHeight: d3.interpolate(this.dragItemStyle.lineHeight, 20),
        borderRadius: d3.interpolate(this.dragItemStyle.borderRadius, 0)
      };
      return (t: number): string => {
        this.dragItemStyle.left = i.left(t);
        this.dragItemStyle.top = i.top(t);
        this.dragItemStyle.width = i.width(t);
        this.dragItemStyle.height = i.height(t);
        this.dragItemStyle.lineHeight = i.lineHeight(t);
        this.dragItemStyle.borderRadius = i.borderRadius(t);
        return '';
      };
    })
    .on('end', () => {
      this.dragging.visible = true;
      this.dragging = undefined;
    });
};

Drag and Drop während einer laufenden Animation

Wir haben leider noch ein Problem. Während eine Animation noch läuft, kann der Benutzer bereits mit der nächsten Drag-Aktion beginnen und dies führt zu Ausnahmen. Das Problem lässt sich elegant lösen: Wir können mehrere Animationen gleichzeitig ermöglichen, indem aus der Eigenschaft dragging ein Array gemacht wird und beim Loslassen in den Ereignissen dragend und drop der jeweils letzte Eintrag aus diesem Array verwendet wird. Zusätzlich sind jedoch noch einige Absicherungen und Erweiterungen zu implementieren, die den Rahmen hier eindeutig sprengen würden.

Um jedoch die möglichen Ausnahmen in unserem Beispiel nicht zu ignorieren, ergänzen wir noch eine letzte Zeile in unserer dragStart-Methode. Wir stellen einfach sicher, dass unser dragging-Objekt nicht mehr auf ein Element referenziert. Andernfalls lassen wir zur Vereinfachung den neuen Drag-Vorgang nicht zu. Das erreichen wir, indem innerhalb des Events-Handlers der Wert false zurückgeben wird.

public dragStart(event: DragEvent, item: Item){
  if(this.dragging){ return false; }
  ...
};

Fazit

Drag and Drop zu implementieren, war schon immer eine Herausforderung – insbesondere im Browser. Durch die HTML5-Erweiterungen wird das Thema wesentlich vereinfacht, auch wenn noch nicht alle Browser diese identisch implementiert haben. Aber Angular bietet uns hier eine gute Unterstützung. Natürlich gibt es auch fertige Komponenten, die das Thema behandeln und wesentlich einfacher zu verwenden sind. Leider bringen solche Komponenten immer wieder Restriktionen mit sich, die oft nur mit viel Aufwand oder mit einer eigenen Implementierung umgangen werden können. Selbstverständlich lassen sich die Animationen auch über CSS Transitions darstellen, auch hier stößt man jedoch öfters auf Restriktionen und seltsames Browserverhalten. Mit unserer Implementierung erhalten wir maximale Flexibilität und können sogar auf Drag-and-Drop-Anforderungen von außerhalb des Browsers reagieren, wie Dateien oder Texte. Versucht doch einfach mal, eines eurer Elemente in ein Word-Dokument oder eine neue E-Mail zu ziehen – für dieses Ergebnis doch eigentlich ganz simpel, oder?

Unsere Redaktion empfiehlt:

Relevante Beiträge

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu:
X
- Gib Deinen Standort ein -
- or -