Polish (Polski) translation by Agnieszka Górczyńska (you can also view the original English article)
W poprzedniej części zobaczyłeś, jak rozpocząć pracę nad utworzeniem w AngularJS prostego koszyka na zakupy. Wprawdzie przygotowaliśmy projekt, ale okazał się zbyt prosty, aby móc go określić mianem aplikacji AngularJS. W tej części serii opracujemy własną dyrektywę AngularJS przeznaczoną do implementacji wymaganej funkcjonalności.
Rozpoczęcie pracy
Pracę rozpoczynamy od sklonowania z serwisu GitHub kodu źródłowego utworzonego w poprzednim artykule.
1 |
git clone https://github.com/jay3dec/AngularShoppingCart_Part1.git |
Następnie przechodzimy do katalogu projektu i instalujemy wymagane zależności.
1 |
cd AngularShoppingCart_Part1 |
2 |
npm install |
Po zainstalowaniu zależności można uruchomić serwer.
1 |
node server.js |
Jeżeli w przeglądarce internetowej wpiszesz adres http://localhost:3000/, zobaczysz uruchomioną aplikację.
Utworzenie dyrektywy
Wyświetlane na stronie cart.html
produkty i opcje powtarzają się. Opracujemy więc dyrektywę AngularJS przeznaczoną do tworzenia produktów i opcji na podstawie dostarczonych danych. W celu zachowania prostoty przyjmujemy założenie, że zawartość koszyka na zakupy przedstawia się następująco:
1 |
[{
|
2 |
'item': 'Hard Disk', |
3 |
'id': 'HD', |
4 |
'selected': 0, |
5 |
'prices': [{ |
6 |
'size': '200GB', |
7 |
'price': '2000' |
8 |
}, { |
9 |
'size': '400GB', |
10 |
'price': '4000' |
11 |
}]
|
12 |
}, { |
13 |
'item': 'CPU', |
14 |
'id': 'CPU', |
15 |
'selected': 0, |
16 |
'prices': [{ |
17 |
'size': 'i3', |
18 |
'price': '20000' |
19 |
}, { |
20 |
'size': 'i5', |
21 |
'price': '25000' |
22 |
}]
|
23 |
}, { |
24 |
'item': 'Monitor', |
25 |
'id': 'MON', |
26 |
'selected': 0, |
27 |
'prices': [{ |
28 |
'size': '16\'', |
29 |
'price': '3000' |
30 |
}, { |
31 |
'size': '19\'', |
32 |
'price': '5000' |
33 |
}]
|
34 |
}, { |
35 |
'item': 'Optical Mouse', |
36 |
'id': 'MOU', |
37 |
'selected': 0, |
38 |
'prices': [{ |
39 |
'size': 'Optical', |
40 |
'price': '350' |
41 |
}, { |
42 |
'size': 'Advanced', |
43 |
'price': '550' |
44 |
}]
|
45 |
}, { |
46 |
'item': 'RAM', |
47 |
'id': 'RM', |
48 |
'selected': 0, |
49 |
'prices': [{ |
50 |
'size': '4GB', |
51 |
'price': '4000' |
52 |
}, { |
53 |
'size': '8GB', |
54 |
'price': '8000' |
55 |
}]
|
56 |
}, { |
57 |
'item': 'USB Keyboard', |
58 |
'id': 'KEY', |
59 |
'selected': 0, |
60 |
'prices': [{ |
61 |
'size': 'Standard', |
62 |
'price': '2500' |
63 |
}, { |
64 |
'size': 'Advanced', |
65 |
'price': '4500' |
66 |
}]
|
67 |
}]
|
W kontrolerze CartCtrl
umieść poniższe dane.
1 |
$scope.shopData = [{ |
2 |
'item': 'Hard Disk', |
3 |
'id': 'HD', |
4 |
'selected': 0, |
5 |
'prices': [{ |
6 |
'size': '200GB', |
7 |
'price': '2000' |
8 |
}, { |
9 |
'size': '400GB', |
10 |
'price': '4000' |
11 |
}]
|
12 |
}, { |
13 |
'item': 'CPU', |
14 |
'id': 'CPU', |
15 |
'selected': 0, |
16 |
'prices': [{ |
17 |
'size': 'i3', |
18 |
'price': '20000' |
19 |
}, { |
20 |
'size': 'i5', |
21 |
'price': '25000' |
22 |
}]
|
23 |
}, { |
24 |
'item': 'Monitor', |
25 |
'id': 'MON', |
26 |
'selected': 0, |
27 |
'prices': [{ |
28 |
'size': '16\'', |
29 |
'price': '3000' |
30 |
}, { |
31 |
'size': '19\'', |
32 |
'price': '5000' |
33 |
}]
|
34 |
}, { |
35 |
'item': 'Optical Mouse', |
36 |
'id': 'MOU', |
37 |
'selected': 0, |
38 |
'prices': [{ |
39 |
'size': 'Optical', |
40 |
'price': '350' |
41 |
}, { |
42 |
'size': 'Advanced', |
43 |
'price': '550' |
44 |
}]
|
45 |
}, { |
46 |
'item': 'RAM', |
47 |
'id': 'RM', |
48 |
'selected': 0, |
49 |
'prices': [{ |
50 |
'size': '4GB', |
51 |
'price': '4000' |
52 |
}, { |
53 |
'size': '8GB', |
54 |
'price': '8000' |
55 |
}]
|
56 |
}, { |
57 |
'item': 'USB Keyboard', |
58 |
'id': 'KEY', |
59 |
'selected': 0, |
60 |
'prices': [{ |
61 |
'size': 'Standard', |
62 |
'price': '2500' |
63 |
}, { |
64 |
'size': 'Advanced', |
65 |
'price': '4500' |
66 |
}]
|
67 |
}];
|
W pliku cart.html
usuń powtarzające się elementy o klasie .panel
. Kod HTML będzie tworzony dynamicznie za pomocą dyrektywy ngRepeat i danych umieszczonych w $scope.shopData
. W pierwszej kolumnie znacznika <div> o klasie .row
umieść poniższy kod HTML.
1 |
<div class="col-xs-7 col-md-8 col-sm-8 col-lg-8"> |
2 |
<div class="panel panel-primary" ng-repeat="q in shopData"> |
3 |
<div class="panel-heading"> |
4 |
<h3 class="panel-title">{{q.item}}</h3> |
5 |
</div>
|
6 |
|
7 |
</div>
|
8 |
</div>
|
Jak możesz zobaczyć w powyższym fragmencie kodu, użycie dyrektywy ngRepeat
pozwala na przeprowadzenie iteracji przez dane shopData
i wygenerowanie kodu HTML. Zapisz wprowadzone zmiany i ponownie uruchom serwer. Po odświeżeniu strony zobaczysz na niej nowo dodane produkty.



Kolejnym krokiem jest wyświetlenie opcji dla poszczególnych produktów, na przykład pojemności dysku, szybkości procesora, ceny itd. (spójrz na przedstawione wcześniej dane JSON). Do tego celu przygotujemy własną dyrektywę. W AngularJS dyrektywa to komponent, który oferuje najpotężniejsze możliwości. Więcej informacji szczegółowych na temat dyrektyw AngularJS znajdziesz w oficjalnej dokumentacji.
Opracujemy własną dyrektywę o nazwie checkList
. Otwórz plik cart.js
, a następnie wprowadź przedstawiony poniżej kod:
1 |
.directive('checkList', function() { |
2 |
return { |
3 |
restrict: 'E', |
4 |
template: function(elem, attrs) { |
5 |
return '<div class="panel-body">\ |
6 |
<div class="radio">\ |
7 |
<label><input type="radio">Option1</label>\ |
8 |
</div>\ |
9 |
<div class="radio">\ |
10 |
<label><input type="radio">Option2</label>\ |
11 |
</div>\ |
12 |
<div class="radio">\ |
13 |
<label><input type="radio">Option2</label>\ |
14 |
</div>\ |
15 |
</div>' |
16 |
}
|
17 |
};
|
18 |
})
|
Jak wcześniej wspomniałem, dyrektywa nosi nazwę checkList
. Powyższa dyrektywa ma dwa parametry: restrict
i template
. Parametr restrict
definiuje sposób wywołania dyrektywy. Ponieważ wymienionemu parametrowi przypisaliśmy wartość E, dyrektywa będzie wywoływania podobnie jak znaczniki:
1 |
<check-list></check-list>
|
Z kolei parametr template
zawiera kod HTML zastępujący dyrektywę checkList
na wygenerowanej stronie. Tutaj wykorzystaliśmy dokładnie ten sam statyczny kod HTML, który poznałeś już wcześniej. Teraz możemy już wywołać dyrektywę checkList
na stronie cart.html
.
1 |
<div class="panel panel-primary" ng-repeat="q in shopData"> |
2 |
<div class="panel-heading"> |
3 |
<h3 class="panel-title">{{q.item}}</h3> |
4 |
</div>
|
5 |
<check-list></check-list>
|
6 |
</div>
|
Zapisz wprowadzone zmiany i odśwież stronę koszyka na zakupy. Zobaczysz wyświetlone statyczne opcje HTML dla poszczególnych produktów.



Musimy nieco zmodyfikować dyrektywę, aby odczytywała dane zdefiniowane w $scope.shopData
. Przede wszystkim, zamiast powtarzać opcje w dyrektywie, wykorzystamy ngRepeat do iteracji przez przygotowane dane. Po wprowadzeniu poniższych zmian, dyrektywa checkList
będzie działała w sposób dynamiczny.
1 |
template: function(elem, attrs) { |
2 |
return '<div class="panel-body">\ |
3 |
<div class="radio" ng-repeat="i in option">\ |
4 |
<label><input type="radio">{{i.size}} Rs.{{i.price}}</label>\ |
5 |
</div>\ |
6 |
</div>' |
7 |
}
|
Jak widać w powyższym fragmencie kodu, dyrektywie należy przekazać pewne dane. Dlatego też musimy zdefiniować atrybut o nazwie option
i za jego pomocą przekazać dyrektywie wymagane dane. W pliku cart.html
dodaj atrybut option
, jak pokazano poniżej:
1 |
<div class="panel panel-primary" ng-repeat="q in shopData"> |
2 |
<div class="panel-heading"> |
3 |
<h3 class="panel-title">{{q.item}}</h3> |
4 |
</div>
|
5 |
<check-list option="q.prices"></check-list> |
6 |
</div>
|
W celu uzyskania w dyrektywie dostępu do przekazanego atrybutu option
konieczne jest zdefiniowanie zakresu. W następujący sposób zdefiniuj zakres scope
w dyrektywie checkList
:
1 |
.directive('checkList', function() { |
2 |
return { |
3 |
restrict: 'E', |
4 |
scope: { |
5 |
option: '=' |
6 |
},
|
7 |
template: function(elem, attrs) { |
8 |
return '<div class="panel-body">\ |
9 |
<div class="radio" ng-repeat="i in option">\ |
10 |
<label><input type="radio">{{i.size}} Rs.{{i.price}}</label>\ |
11 |
</div>\ |
12 |
</div>' |
13 |
}
|
14 |
};
|
15 |
})
|
Tym samym dyrektywie zostanie przekazana lista cen różnych produktów, których dane znajdują się w $scope.shopData
. Zapisz wprowadzone zmiany i ponownie uruchom serwer. Po odświeżeniu strony zobaczysz, że poszczególne produkty na stronie mają przypisane odpowiednie opcje.



Jeżeli teraz spróbujesz kliknąć przycisk opcji wybranego produktu to przekonasz się, że zostają zaznaczone obie opcje. Aby mieć gwarancję wyboru tylko jednej opcji, musimy odpowiednio pogrupować przyciski. To wymaga przekazania z widoku HTML do dyrektywy kolejnego atrybutu o nazwie name
. W dyrektywie check-list
umieść nowy atrybut o nazwie name
. Wartością atrybutu name
będzie ID
, co zapewni unikatowe rozróżnianie poszczególnych produktów.
1 |
<check-list name="q.id" option="q.prices"></check-list> |
Do zakresu dyrektywy dodajemy kolejną zmienną, która będzie dostępna w szablonie dyrektywy.
1 |
scope: { |
2 |
option: '=', |
3 |
name: '=' |
4 |
}
|
W kodzie HTML parametru template
przekazana wartość będzie użyta jako nazwa przycisku opcji, który grupuje opcje poszczególnych produktów.
1 |
<input type="radio" name="{{name}}" |
Zapisz wprowadzone zmiany i odśwież stronę. Spróbuj zaznaczyć opcje dla wybranego produktu, a przekonasz się, że możesz wybrać tylko jedną z nich.
Obliczenie kwoty całkowitej na podstawie wybranych opcji
Na podstawie produktów wybranych przez użytkownika można wyświetlić kwotę całkowitą wszystkich produktów znajdujących się w koszyku. Utworzymy teraz funkcję $scope.total()
odpowiedzialną za obliczenie wspomnianej sumy. Gdy użytkownik wybierze produkt, wtedy nastąpi uaktualnienie zmiennej w danych JSON $scope.shopData
. Następnie jest przeprowadzana iteracja przez wspomniane dane JSON, aby obliczyć wartość całkowitą wybranych produktów. Poniżej przedstawiłem kod źródłowy funkcji $scope.total()
:
1 |
$scope.total = function() { |
2 |
var t = 0; |
3 |
|
4 |
for (var k in $scope.shopData) { |
5 |
t += parseInt($scope.shopData[k].selected); |
6 |
}
|
7 |
|
8 |
return t; |
9 |
|
10 |
}
|
Obecnie element <div> wyświetla na stałe zdefiniowaną wartość Rs 100. Wprowadzamy modyfikację, aby zamiast na stałe zdefiniowanej wartości następowało wywołanie przygotowanej wcześniej funkcji.
1 |
<h2>Rs. {{total()}}</h2> |
Zapisz wprowadzone zmiany i odśwież stronę. Zauważ, że mimo wyboru różnych opcji, wartość całkowita nie ulega zmianie. Takie zachowanie wynika z faktu przypisania wartości początkowej 0 zmiennej selected
w danych JSON. Wymieniona wartość nie jest uaktualniana po wybraniu opcji. Musimy więc nie tylko przekazać dyrektywie wybraną wartość, ale jeszcze uaktualnić ją po zaznaczeniu przycisku opcji. Zmodyfikuj kod widoku HTML w taki sposób, aby zawierał dodatkowy atrybut selected
w elemencie dyrektywy checkList
.
1 |
<check-list name="q.id" selected="q.selected" option="q.prices"></check-list> |
Do zakresu dyrektywy dodaj zmienną selected
. Tym samym zyskasz możliwość dostępu do wymienionego atrybutu z poziomu dyrektywy.
1 |
scope: { |
2 |
option: '=', |
3 |
name: '=', |
4 |
selected: '=selected' |
5 |
}
|
Wprowadzamy kolejne modyfikacje w kodzie. W dyrektywie ngModel
przycisku opcji ustawiamy selected
, natomiast w dyrektywie ngValue
ustawiamy i.price
. Teraz po zaznaczeniu przycisku opcji nastąpi uaktualnienie wartości atrybutu selected
w danych JSON $scope.shopData
.
1 |
<input type="radio" ng-model="$parent.selected" ng-value="{{i.price}}" name="{{name}}"> |
Zapisz wprowadzone zmiany i odśwież stronę. Spróbuj zaznaczać różne przyciski opcji. Zwróć uwagę, że w wyświetlana w ramce Total
suma jest obliczana na podstawie wybranych opcji.



Podsumowanie
W tej części serii opracowaliśmy własną dyrektywę i wykorzystaliśmy ją w naszej prostej aplikacji koszyka na zakupy. W kolejnej części zobaczysz, jak zachować stałe położenie elementu Total na górze ekranu podczas przewijania strony. Zaimplementujemy także stronę finalizacji zamówienia wyświetlającej wybrane produkty i ceny. Wspomniana strona będzie zawierać przycisk pozwalający klientowi na powrót do koszyka i zmianę wybranych produktów.
Kod źródłowy przedstawiony w artykule znajdziesz w serwisie GitHub. Jeżeli masz jakiekolwiek uwagi, sugestie lub znalazłeś błąd w tekście, koniecznie napisz o tym w komentarzu.