Ich hatte kürzlich die Gelegenheit, an einem fantastischen Forschungs- und Entwicklungsprojekt bei Netguru zu arbeiten. Das Ziel des Projekts (Codename: „Wordguru“) war es, ein Kartenspiel zu entwickeln, das jeder mit seinen Freunden spielen kann. Das Ergebnis sehen Sie hier.
Ein Teil des Entwicklungsprozesses war die Erstellung eines interaktiven Kartenstapels. Der Kartenstapel hatte eine Reihe von Anforderungen, darunter:
- Er sollte einige Karten aus der Sammlung enthalten.
- Die erste Karte sollte interaktiv sein.
- Der Benutzer sollte die Karte in verschiedene Richtungen wischen können, die die Absicht signalisieren, die Karte anzunehmen, abzulehnen oder zu überspringen.
Dieser Artikel erklärt, wie Sie dies erstellen und mit Vue.js und interact.js interaktiv gestalten. Ich habe ein Beispiel für Sie erstellt, auf das Sie sich beziehen können, während wir den Prozess der Erstellung einer Komponente, die für die Anzeige dieses Kartenstapels zuständig ist, und einer zweiten Komponente, die für das Rendern einer einzelnen Karte und die Verwaltung von Benutzerinteraktionen darin verantwortlich ist, durchgehen.
Schritt 1: Erstellen Sie die GameCard-Komponente in Vue
Beginnen wir mit der Erstellung einer Komponente, die eine Karte anzeigt, aber noch ohne Interaktionen. Wir werden diese Datei GameCard.vue nennen und in der Komponentenvorlage einen Karten-Wrapper und das Schlüsselwort für eine bestimmte Karte rendern. Dies ist die Datei, mit der wir während dieses Beitrags arbeiten werden.
// GameCard.vue
<template>
<div
class="card"
:class="{ isCurrent: isCurrent }"
>
<h3 class="cardTitle">{{ card.keyword }}</h3>
</div>
</template>
Im Skriptteil der Komponente empfangen wir die Prop card, die unseren Karteninhalt enthält, sowie eine Prop isCurrent, die der Karte bei Bedarf ein besonderes Aussehen verleiht.
export default {
props: {
card: {
type: Object,
required: true
},
isCurrent: {
type: Boolean,
required: true
}
}
},
Schritt 2: Erstellen Sie die GameCardStack-Komponente in Vue
Jetzt, da wir eine einzelne Karte haben, erstellen wir unseren Kartenstapel.
Diese Komponente empfängt ein Array von Karten und rendert für jede Karte die GameCard. Sie wird auch die erste Karte als aktuelle Karte im Stapel markieren, damit ihr ein spezielles Styling angewendet wird.
// GameCardsStack.vue
<template>
<div class="cards">
<GameCard
v-for="(card, index) in cards"
:key="card"
:card="card"
:is-current="index === 0"
/>
</div>
</template>
<script>
import GameCard from "@/components/GameCard";
export default {
components: {
GameCard
},
props: {
cards: {
type: Array,
required: true
}
}
};
</script>
Hier ist, was wir bisher sehen, unter Verwendung der Stile aus der Demo

An diesem Punkt sieht unsere Karte vollständig aus, ist aber nicht sehr interaktiv. Lassen Sie uns das im nächsten Schritt beheben!
Schritt 3: Fügen Sie dem GameCard-Komponente Interaktivität hinzu
Unsere gesamte Interaktivitätslogik wird in der GameCard-Komponente leben. Beginnen wir damit, dem Benutzer das Ziehen der Karte zu ermöglichen. Wir werden interact.js zum Ziehen verwenden.
Wir setzen die anfänglichen Werte von interactPosition im Skriptteil auf 0. Dies sind die Werte, die die Reihenfolge einer Karte im Stapel angeben, wenn sie von ihrer ursprünglichen Position bewegt wird.
<script>
import interact from "interact.js";
data() {
return {
interactPosition: {
x: 0,
y: 0
},
};
},
// ...
</script>
Als Nächstes erstellen wir eine berechnete Eigenschaft, die für die Erstellung eines transform-Werts zuständig ist, der auf unser Kartenelement angewendet wird.
// ...
computed: {
transformString() {
const { x, y } = this.interactPosition;
return `translate3D(${x}px, ${y}px, 0)`;
}
},
// ...
Im mounted Lifecycle-Hook verwenden wir interact.js und seine draggable-Methode. Diese Methode erlaubt es uns, bei jedem Ziehen des Elements eine benutzerdefinierte Funktion aufzurufen (onmove). Sie stellt auch ein event-Objekt zur Verfügung, das Informationen darüber enthält, wie weit das Element von seiner ursprünglichen Position gezogen wurde. Jedes Mal, wenn der Benutzer die Karte zieht, berechnen wir eine neue Position der Karte und setzen sie auf die Eigenschaft interactPosition. Dies löst unsere berechnete Eigenschaft transformString aus und setzt den neuen Wert von transform auf unserer Karte.
Wir verwenden den onend-Hook von interact, der es uns ermöglicht, zu lauschen, wenn der Benutzer die Maustaste loslässt und den Zug beendet. An diesem Punkt setzen wir die Position unserer Karte zurück und bringen sie in ihre ursprüngliche Position zurück: { x: 0, y: 0 }.
Wir müssen auch sicherstellen, dass das Kartenelement aus dem Interactable-Objekt entfernt wird, bevor es zerstört wird. Dies tun wir im `beforeDestroy`-Lifecycle-Hook, indem wir interact(target).unset() verwenden. Dadurch werden alle Event-Listener entfernt und interact.js vergisst das Ziel vollständig.
// ...
mounted() {
const element = this.$refs.interactElement;
interact(element).draggable({
onmove: event => {
const x = this.interactPosition.x + event.dx;
const y = this.interactPosition.y + event.dy;
this.interactSetPosition({ x, y });
},
onend: () => {
this.resetCardPosition();
}
});
},
// ...
beforeDestroy() {
interact(this.$refs.interactElement).unset();
},
// ...
methods: {
interactSetPosition(coordinates) {
const { x = 0, y = 0 } = coordinates;
this.interactPosition = {x, y };
},
resetCardPosition() {
this.interactSetPosition({ x: 0, y: 0 });
},
},
// ...
Wir müssen unserer Vorlage noch eine Sache hinzufügen, damit dies funktioniert. Da unsere berechnete Eigenschaft transformString einen String zurückgibt, müssen wir ihn auf die Kartenkomponente anwenden. Dies tun wir, indem wir an das :style-Attribut binden und dann den String an die transform-Eigenschaft übergeben.
<template>
<div
class="card"
:class="{ isCurrent: isCurrent }"
:style="{ transform: transformString }"
>
<h3 class="cardTitle">{{ card.keyword }}</h3>
</div>
</template>
Damit ist unsere Interaktion mit der Karte abgeschlossen – wir können sie herumziehen!
Sie haben vielleicht bemerkt, dass das Verhalten nicht sehr natürlich ist, insbesondere wenn wir die Karte ziehen und loslassen. Die Karte kehrt sofort in ihre ursprüngliche Position zurück, aber es wäre natürlicher, wenn die Karte mit einer Animation in die Ausgangsposition zurückkehren würde, um den Übergang zu glätten.
Hier kommt transition ins Spiel! Aber das Hinzufügen zu unserer Karte führt zu einem weiteren Problem: Es gibt eine Verzögerung beim Folgen der Karte, da transition immer auf das Element angewendet wird. Wir wollen es nur dann angewendet haben, wenn der Zug endet. Dies können wir erreichen, indem wir noch eine Klasse (isAnimating) an die Komponente binden.
<template>
<div
class="card"
:class="{
isAnimating: isInteractAnimating,
isCurrent: isCurrent
}"
>
<h3 class="cardTitle">{{ card.keyword }}</h3>
</div>
</template>
Wir können die Animationsklasse hinzufügen und entfernen, indem wir die Eigenschaft isInteractAnimating ändern.
Der Animationseffekt sollte anfänglich angewendet werden und das tun wir, indem wir unsere Eigenschaft in data setzen.
Im `mounted`-Hook, wo wir interact.js initialisieren, verwenden wir einen weiteren interact-Hook (onstart) und ändern den Wert von isInteractAnimating auf false, sodass die Animation während des Ziehens deaktiviert ist.
Wir aktivieren die Animation im onend-Hook wieder, und das lässt unsere Karte beim Loslassen des Zugs sanft in ihre ursprüngliche Position animieren.
Wir müssen auch die berechnete Eigenschaft transformString aktualisieren und eine Absicherung hinzufügen, um einen String nur dann neu zu berechnen und zurückzugeben, wenn wir die Karte ziehen.
data() {
return {
// ...
isInteractAnimating: true,
// ...
};
},
computed: {
transformString() {
if (!this.isInteractAnimating) {
const { x, y } = this.interactPosition;
return `translate3D(${x}px, ${y}px, 0)`;
}
return null;
}
},
mounted() {
const element = this.$refs.interactElement;
interact(element).draggable({
onstart: () => {
this.isInteractAnimating = false;
},
// ...
onend: () => {
this.isInteractAnimating = true;
},
});
},
Jetzt sehen die Dinge gut aus!
Unser Kartenstapel ist bereit für die zweite Interaktionsrunde. Wir können die Karte herumziehen, aber es passiert eigentlich nichts – die Karte kehrt immer an ihren ursprünglichen Platz zurück, aber es gibt keinen Weg, zur zweiten Karte zu gelangen.
Dies wird sich ändern, wenn wir Logik hinzufügen, die es dem Benutzer erlaubt, Karten anzunehmen und abzulehnen.
Schritt 4: Erkennen Sie, wann eine Karte angenommen, abgelehnt oder übersprungen wurde
Die Karte hat drei Arten von Interaktionen:
- Karte annehmen (nach rechts wischen)
- Karte ablehnen (nach links wischen)
- Karte überspringen (nach unten wischen)
Wir müssen einen Ort finden, an dem wir erkennen können, ob die Karte von ihrer ursprünglichen Position weggezogen wurde. Wir wollen auch sicher sein, dass diese Prüfung nur dann stattfindet, wenn wir das Ziehen der Karte beendet haben, damit die Interaktionen nicht mit der gerade abgeschlossenen Animation kollidieren.
Wir haben diesen Ort bereits für den sanften Übergang während der Animation verwendet – es ist der onend-Hook, der von der interact.draggable-Methode bereitgestellt wird.
Springen wir zum Code.
Zuerst müssen wir unsere Schwellenwerte speichern. Diese Werte sind die Abstände, die die Karte von ihrer ursprünglichen Position weggezogen hat und die es uns ermöglichen zu bestimmen, ob die Karte angenommen, abgelehnt oder übersprungen werden sollte. Wir verwenden die X-Achse für rechts (annehmen) und links (ablehnen), und dann die Y-Achse für die Abwärtsbewegung (überspringen).
Wir setzen auch die Koordinaten, an denen wir eine Karte platzieren möchten, nachdem sie angenommen, abgelehnt oder übersprungen wurde (Koordinaten außerhalb des Blickfelds des Benutzers).
Da sich diese Werte nicht ändern werden, behalten wir sie in der static-Eigenschaft unserer Komponente, auf die mit this.$options.static.interactYThreshold zugegriffen werden kann.
export default {
static: {
interactYThreshold: 150,
interactXThreshold: 100
},
Wir müssen in unserem onend-Hook prüfen, ob einer unserer Schwellenwerte erreicht wurde, und dann die entsprechende Methode aufrufen. Wenn kein Schwellenwert erreicht wurde, setzen wir die Karte auf ihre Ausgangsposition zurück.
mounted() {
const element = this.$refs.interactElement;
interact(element).draggable({
onstart: () => {...},
onmove: () => {...},
onend: () => {
const { x, y } = this.interactPosition;
const { interactXThreshold, interactYThreshold } = this.$options.static;
this.isInteractAnimating = true;
if (x > interactXThreshold) this.playCard(ACCEPT_CARD);
else if (x < -interactXThreshold) this.playCard(REJECT_CARD);
else if (y > interactYThreshold) this.playCard(SKIP_CARD);
else this.resetCardPosition();
}
});
}
Okay, jetzt müssen wir eine playCard-Methode erstellen, die für die Handhabung dieser interaktiven Aktionen verantwortlich ist.
Schritt 5: Richten Sie die Logik zum Annehmen, Ablehnen und Überspringen von Karten ein
Wir erstellen eine Methode, die einen Parameter akzeptiert, der uns die beabsichtigte Aktion des Benutzers mitteilt. Abhängig von diesem Parameter setzen wir die endgültige Position der aktuellen Karte und lösen das Ereignis für Annahme, Ablehnung oder Überspringen aus. Gehen wir Schritt für Schritt vor.
Zuerst entfernt unsere playCard-Methode das Kartenelement aus dem Interactable-Objekt, damit es keine Ziehereignisse mehr verfolgt. Dies tun wir mit interact(target).unset().
Zweitens setzen wir die endgültige Position der aktiven Karte entsprechend der Absicht des Benutzers. Diese neue Position ermöglicht es uns, die Karte zu animieren und sie aus dem Blickfeld des Benutzers zu entfernen.
Als Nächstes emittieren wir ein Ereignis nach oben an die Elternkomponente, damit wir unsere Karten bearbeiten können (z. B. die aktuelle Karte ändern, mehr Karten laden, Karten mischen usw.). Wir möchten dem DDAU-Prinzip (Data Down Actions Up) folgen, das besagt, dass eine Komponente davon absehen sollte, Daten zu mutieren, die ihr nicht gehören. Da unsere Karten an unsere Komponente übergeben werden, sollte sie ein Ereignis nach oben an den Ort emittieren, von dem diese Karten stammen.
Zuletzt blenden wir die gerade gespielte Karte aus und fügen einen Timeout hinzu, der es der Karte ermöglicht, aus dem Blickfeld heraus zu animieren.
methods: {
playCard(interaction) {
const {
interactOutOfSightXCoordinate,
interactOutOfSightYCoordinate,
} = this.$options.static;
this.interactUnsetElement();
switch (interaction) {
case ACCEPT_CARD:
this.interactSetPosition({
x: interactOutOfSightXCoordinate,
});
this.$emit(ACCEPT_CARD);
break;
case REJECT_CARD:
this.interactSetPosition({
x: -interactOutOfSightXCoordinate,
});
this.$emit(REJECT_CARD);
break;
case SKIP_CARD:
this.interactSetPosition({
y: interactOutOfSightYCoordinate
});
this.$emit(SKIP_CARD);
break;
}
this.hideCard();
},
hideCard() {
setTimeout(() => {
this.isShowing = false;
this.$emit("hideCard", this.card);
}, 300);
},
interactUnsetElement() {
interact(this.$refs.interactElement).unset();
this.interactDragged = true;
},
}
Und da haben wir es!
Zusammenfassung
Fassen wir zusammen, was wir gerade erreicht haben:
- Zuerst haben wir eine Komponente für eine einzelne Karte erstellt.
- Als Nächstes haben wir eine weitere Komponente erstellt, die die Karten in einem Stapel rendert.
- Drittens haben wir interact.js implementiert, um interaktives Ziehen zu ermöglichen.
- Dann haben wir erkannt, wann der Benutzer eine Aktion mit der aktuellen Karte ausführen möchte.
- Schließlich haben wir die Logik etabliert, um diese Aktionen zu verarbeiten.
Puh, wir haben viel behandelt! Hoffentlich gibt Ihnen das sowohl einen neuen Trick für Ihre Werkzeugkiste als auch einen praktischen Anwendungsfall für Vue. Und wenn Sie schon einmal etwas Ähnliches bauen mussten, teilen Sie es bitte in den Kommentaren mit, denn es wäre toll, sich auszutauschen.
Schöner Artikel! Das war eine großartige Erklärung, wie man einer einfachen Vue-App Interaktivität hinzufügt.
Ihre Demo und Ihr tatsächliches Produkt funktionieren bei Touch-Interaktionen unter Edge überhaupt nicht.
Kann ich grob fragen, wie viele Entwicklerstunden es gedauert hat, bis dies funktionierte? Meine Designkollegen sehen so etwas oft im Web und erwarten, dass ich es allein in ein oder zwei Tagen erledige, was mir unrealistisch erscheint.
Es hat etwa 3 Tage gedauert, bis es funktionierte (erste Iteration mit einigen weiteren Fehlerbehebungen, keine Event-Bus-Integration).
Wenn Sie Vue verwenden, habe ich die Funktionalität in ein npm-Paket namens
vue2-interactextrahiert (https://vue2-interact.netlify.com/docs/vue2InteractDraggable/#basic-usage).Beachten Sie den Kommentar unten bezüglich des Edge-Browsers (wird gerade untersucht).