search check home clock-o tag tags chevron-left chevron-right chevron-up chevron-down twitter facebook github rss comment comments terminal code

[JS] Jak zbudować interfejs koszyka zakupów używając DragsterJS?

[JS] Jak zbudować interfejs koszyka zakupów używając DragsterJS?

Tematem dzisiejszego wpisu będzie tworzenie prototypu koszyka zakupowego z wykorzystaniem DragsterJS. Zobaczysz jak łatwo można zaimplementować atrakcyjny interakcyjnie koszyk zakupowy.

Co to jest DragsterJS?

DragsterJS jest to biblioteka udostępniająca interfejs drag & drop, czyli przeciągnij i upuść. Dzięki niej można zbudować różnego rodzaju atrakcyjne interfejsy użytkownika polegające na przeciąganiu elementów z miejsca na miejsce. Jest to narzędzie wykorzystywane przeze mnie w celu implementacji zaawansowanych interfejsów edycji landing page w eZ Platform Enterprise Edition.

Założenia prototypu koszyka zakupów

W celu zbudowania naszego prototypu warto zadecydować jakie powinny być jego podstawowe możliwości. Na potrzeby tego wpisu wymagania nie będą one zbyt rozbudowane. Minimalny zestaw wymagań wygląda następująco:

  • Wyświetlanie listy produktów, które można przeciągać do koszyka zakupowego,
  • Koszyk zakupowy zawierający przestrzeń, gdzie można upuszczać elementy,
  • Po upuszczeniu elementu do koszyka, powinna się zaktualizować lista wybranych elementów oraz koszt całkowity zakupu produktów.

To są 3 proste wymagania, które zrealizujemy za pomocą kodu przedstawionego w dalszej części wpisu.

dragsterjs-shopping-cart

Struktura HTML oraz style CSS prototypu

Zanim przejdziemy do implementacji interfejsu przeciągnij-i-upuść musimy utworzyć kod HTML, który będzie przypominał listę produktów oraz koszyk oraz dodać style CSS, aby to jakoś wyglądało. Najpierw zaczniemy od kodu HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
    <div class="container">
        <div class="items region region--drag-only">
            <div class="item" data-id="1" data-price="3" data-name="Product 1">
                <h3>Product 1</h3>
                <img src="" alt="Product 1 - price: $3" />
                <p>Product 1 description</p>
            </div>
            <div class="item" data-id="2" data-price="11.99" data-name="Product 2">
                <h3>Product 2</h3>
                <img src="" alt="Product 2 - price: $11.99" />
                <p>Product 2 description</p>
            </div>
            <div class="item" data-id="3" data-price="8.59" data-name="Product 3">
                <h3>Product 3</h3>
                <img src="" alt="Product 3 - price: $8.59" />
                <p>Product 3 description</p>
            </div>
            <div class="item" data-id="4" data-price="2.5" data-name="Product 4">
                <h3>Product 4</h3>
                <img src="" alt="Product 4 - price: $2.5" />
                <p>Product 4 description</p>
            </div>
        </div>
        <div class="shopping-cart">
            <h3>Your items</h3>
            <div class="shopping-cart__dropzone region"></div>
            <div class="shopping-cart__summary">
                <ul class="shopping-cart__items"></ul>
                <div class="shopping-cart__total-price" data-total="0"></div>
            </div>
        </div>
    </div>

Interfejs podzieliliśmy na dwie sekcje: .items i .shopping-cart. Należy również pamiętać o dołączeniu skryptu biblioteki DragsterJS oraz pliku w którym będziemy budować naszą interakcję z interfejsem:

1
2
<script src="dragster.min.js"></script>
<script src="shopping-cart-demo.js"></script>

Powyższe skrypty najlepiej jest dołączyć na końcu kodu strony przed znacznikiem końcowym </body>

Po implementacji stylów CSS te sekcje staną się dwiema kolumnami znajdującymi się obok siebie. Style CSS, które będą odpowiedzialne za wygląd naszego prototypu wyglądają następująco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
    * {
        box-sizing: border-box;
    }
 
    html {
        font-size: 16px;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
        line-height: 1.4;
    }
 
    body {
        width: 80vw;
        margin: auto;
        color: #1e1e1e;
    }
 
    .container {
        display: flex;
        flex-wrap: nowrap;
    }
 
    .items {
        flex: 1 1 25%;
    }
 
    .shopping-cart {
        flex: 1 1 75%;
    }
 
    .item {
        cursor: move;
        background: #eee;
        border: dashed 1px #ddd;
        padding: .5rem;
        border-radius: .25rem;
    }
 
    .item h3,
    .shopping-cart h3 {
        margin: 0 0 .5rem;
    }
 
    .item img {
        font-style: italic;
        font-size: .75rem;
    }
 
    .item p {
        margin: .5rem 0;
    }
 
    .dragster-draggable+.dragster-draggable {
        margin-top: .25rem;
    }
 
    .shopping-cart {
        padding-left: 1rem;
    }
 
    .shopping-cart__dropzone {
        height: 5rem;
        border: 1px dashed #1e1e1e;
        border-radius: .25rem;
        position: relative;
    }
 
    .shopping-cart__dropzone:after {
        content: 'Drop here';
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
    }
 
    .shopping-cart__dropzone:hover {
        background: #eee;
    }
 
    .shopping-cart__items {
        margin: 0;
        padding: 0;
        list-style-position: inside;
    }
 
    .shopping-cart__total-price {
        border-top: 1px solid #1e1e1e;
        padding-top: 1rem;
        font-weight: 700;
        opacity: 0;
    }
 
    .dragster-temp {
        transform: translateY(1rem);
    }

Jest tutaj kilka rzeczy na które warto zwrócić uwagę. Pierwszą z nich jest wykorzystanie podstawowych czcionek systemowych (różnych w zależności od systemu operacyjnego użytkownika) w wyświetlaniu tekstu na stronie. Jest to trend, który pojawia się na różnych stronach technicznych, gdzie tak naprawdę rodzaj czcionki nie jest najbardziej istotny. Wykorzystanie czcionek systemowych zrealizowano za pomocą następującego kawałka kodu CSS: font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;. Wydaje mi się, że zapewnia to ciekawy efekt końcowy.

Kolejną rzeczą na którą warto zwrócić uwagę jest zbudowanie layoutu prototypu za pomocą modelu Flexbox. Dzięki własnościom CSS: display: flex; i między innymi flex: 1 1 25%; jestem w stanie zbudować prosty layout koszyka zakupowego.

Implementacja DragsterJS

DragsterJS posiada bardzo wiele możliwości. Obsługa dodatkowych akcji podczas interakcji drag'n'drop jest zapewniona przez przekazanie callback'ów w konfiguracji instancji DragsterJS.

Pierwszą rzeczą którą należy wykonać, aby zbudować prototyp koszyka zakupowego jest uruchomienie instancji DragsterJS i przekazanie do niej konfiguracji, gdzie zdefiniujemy odpowiednie zmienne:

1
2
3
4
5
6
7
8
9
10
11
12
13
const dragster = new Dragster({
    // selektor CSS regionów interfejsu, gdzie będzie zachodziła interakcja drag'n'drop
    regionSelector: '.region',
    // selektor CSS elementów interfejsu, które można będzie przeciągać do koszyka
    elementSelector: '.region--drag-only .item',
    // klasa CSS regionu w którym możliwe będzie tylko przeciąganie elementów z miejsca;
    // nie będzie możliwe upuszczanie elementów do tego regionu
    dragOnlyRegionCssClass: 'region--drag-only',
    // flaga, która informuje aby kopiować elementy po upuszczeniu ich w nowe miejsce
    cloneElements: true,
    // funkcja, która jest wywoływana po upuszczeniu elementu w nowym miejscu
    onAfterDragDrop: afterDropCallback
});

W kodzie powyżej za pomocą komentarzy wyjaśniłem po co są wybrane przeze mnie opcje konfiguracyjne. Dzięki nim sprawimy, że nasz prototyp ożyje i będzie funkcjonalny. Warto zauważyć, że wykorzystano dwie opcje: cloneElements: true oraz dragOnlyRegionCssClass: 'region--drag-only'. Dzięki nim możemy być pewni, że produkty z naszej listy nie znikną po upuszczeniu ich w nowe miejsce oraz, że przypadkowo nie zamienimy ich miejscami na liście produktów.

Pełny kod, który spełni wszystkie wymagania wymienione w pierwszej części wpisu, wygląda następująco:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
(function() {
    const droppedItems = {};
    const dropzone = document.querySelector('.shopping-cart__dropzone');
    const itemsList = document.querySelector('.shopping-cart__items');
    const totalPrice = document.querySelector('.shopping-cart__total-price');
    const ITEM_SELECTOR = '.item';
    /**
     * Zaktualizuj wartość wszystkich wybranych produktów
     */
    const updateTotalPriceLabel = () => {
        let total = 0;
 
        Object.keys(droppedItems).forEach(key => {
            const item = droppedItems[key];
 
            total = total + (item.count * parseFloat(item.price));
        });
 
        totalPrice.innerHTML = `$${total.toFixed(2)}`;
        totalPrice.style.opacity = '1';
    };
    /**
     * Wyświetl elementy znajdujące się w koszyku zakupowym
     */
    const renderItems = () => {
        let fragment = document.createDocumentFragment();
 
        Object.keys(droppedItems).forEach(key => {
            const item = droppedItems[key];
            let element = document.createElement('li');
 
            element.innerHTML = `${item.name} (${item.count}) - $${item.count * parseFloat(item.price)}`;
 
            fragment.append(element);
        });
 
        itemsList.innerHTML = '';
        itemsList.append(fragment);
    };
    /**
     * Zresetuj listę produktów znajdujących się w koszyku zakupowym
     */
    const clearDropzone = () => dropzone.innerHTML = '';
    /**
     * Utwórz strukturę danych dotyczących produktów umieszczonych w koszyku.
     * Aktualizuje liczbę wybranych elementów.
     */
    const updateItemsData = (items) => {
        items.forEach(item => {
            if (droppedItems[item.id]) {
                droppedItems[item.id].count = droppedItems[item.id].count + 1;
            } else {
                droppedItems[item.id] = item;
            }
        });
    };
    /**
     * Tworzy obiekt definiujący element dodany do koszyka zakupowego.
     */
    const createItemsListItem = (element) => {
        return {
            id: element.dataset.id,
            name: element.dataset.name,
            price: element.dataset.price,
            count: 1
        };
    };
    /**
     * Funkcja wywoływana po upuszczeniu elementu w koszyku zakupowym.
     * Ma na celu:
     * 1. aktualizację stanu wybranych produktów znajdujących się w koszyku,
     * 2. wyczyszczenie strefy upuszczania produktów z elementów znajdujących się w niej,
     * 3. wyświetlenie wybranych elementów,
     * 4. aktualizację etykiety zawierającej całkowitą wartość koszyka zakupów
     */
    const afterDropCallback = () => {
        updateItemsData([...dropzone.querySelectorAll(ITEM_SELECTOR)].map(createItemsListItem));
        clearDropzone();
        renderItems();
        updateTotalPriceLabel();
    };
    const dragster = new Dragster({
        regionSelector: '.region',
        elementSelector: '.region--drag-only .item',
        dragOnlyRegionCssClass: 'region--drag-only',
        cloneElements: true,
        onAfterDragDrop: afterDropCallback
    });
})();

Podsumowanie

Mam nadzieję, że udało mi się Ciebie zaciekawić projektem DragsterJS. Dzięki niemu, możesz zbudować interaktywne elementy swojej strony internetowej czy też aplikacji internetowej. Nie ważne, czy strona bądź aplikacja będą uruchamiane w przeglądarce na komputerze stacjonarnym/laptopie czy też za pomocą komórki/tabletu. Interfejs będzie działał.

Jeśli masz jakieś uwagi odnośnie funkcjonalności DragsterJS to proszę, abyś je zamieszczono w Issues znajdującym się w repozytorium na Githubie lub możesz zaproponować usprawnienie do biblioteki przy pomocy pull request'a do repozytorium DragsterJS.

Zapraszam również do: