Moje zdjęcie
Software Craftsman's Blog Po Polsku - Marcin Pieciukiewicz
Praktyczne programowanie w Java i Scala

niedziela, 8 września 2013

Przykład użycia AngularJS - prosty kalkulator

Niedawno moją uwagę zwrócił interesujący framework dla JavaScript o nazwie AngularJS. Jest to framework stworzony przez Google w celu ułatwienia tworzenia bogatych aplikacji dla przeglądarek internetowych. W tym artykule chciałbym przedstawić przykład aplikacji stworzonej przy użyciu AngularJS, która będzie wykorzystywała kilka istotnych funkcjonalności dostarczanych przez ten framework. Oczywiście sam dopiero zaczynam poznawać AngularJS, dlatego w tym artykule ograniczę się do najbardziej podstawowych cech AngularJS.

1. Koncepcja aplikacji
Nasza aplikacja będzie bardzo prostym kalkulatorem matematycznym, który umożliwi użytkownikowi wykonywanie czterech podstawowych operacji: dodawania, odejmowania, mnożenia i dzielenia na liczbach całkowitych. Oczekujemy mniej więcej takiego wyglądu tego kalkulatora.



2. Biblioteka AngularJS
AngularJS jest rozprowadzany w postaci pojedyńczego pliku JavaScript, który jest dostępny na stronie angularjs.org. Wystarczy wybrać wersję stabilną (stable) oraz zminimalizowaną (mininified) tego pliku. Ta wersja pliku jest nosi nazwę angular.min.js. W czasie pisania tego artykułu, najnowszą stabilną wersją była 1.0.7.

3. Struktura aplikacji
Nasza aplikacja będzie umieszczona w następującej strukturze katalogów:
Calculator                  Główny katalog projektu
|--lib                      
|  |--js                    Biblioteki JavaScript
|--app
   |--js                    Pliki źródłowe JavaScript
   |--style                 Pliki CSS
   |--view                  Pliki HTML
Po utworzeniu powyższej struktury katalogów kopiujemy pobrany plik angular.min.js do katalogu Calculator/lib/js.
Następnie utwórzmy podstawowe pliki źródłowe w których będziemy definiować naszą aplikację. Będziemy potrzebowali trzech plików:
  • Calculator/index.html - podstawowy plik warstwy widoku
  • Calculator/app/js/app.js - podstawowy plik ze źródłami aplikacji
  • Calculator/app/style/default.css - plik zawierający style kaskadowe

4. Aplikacja AngularJS
W następnym kroku stworzymy szkielet HTML naszej aplikacji, w którym zdefiniujemy podstawowe elementy HTML i dołączymy nasze pliki źródłowe. Wynikowy plik index.html powinien wyglądać następująco:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html ng-app="calculatorApplication">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <script src="lib/js/angular.min.js" type="text/javascript"></script>
    <script src="app/js/app.js" type="text/javascript"></script>
    <link href="app/style/default.css" rel="stylesheet" type="text/css">
</head>
<body>

<div ng-view></div>

</body>
</html>
Poza standardowym nagłówkiem html są tam dwa elementy specyficzne dla aplikacji AngularJS:
  • <html ng-app="calculatorApplication">
  • Parametr ng-app mówi silnikowi AngularJS, że wszystko wewnątrz tagu html jest aplikacją kontrolowaną przez AngularJS. I że jest to moduł o nazwie calculatorApplication.
  • <div ng-view></div>
  • Parametr ng-view wskazuje, że ten element będzie kontenerem w którym AngularJS umieści stworzony widok.
Jak widać framework rozszerza standard HTML o własne atrybuty, które mogą być interpretowane przez silnik AngularJS.

Po utworzeniu struktury HTML aplikacji powinniśmy zdefiniować zachowanie aplikacji. Zrobimy to przez edycję pliku app.js i dodanie następującego kodu:
var app = angular.module("calculatorApplication", []);
Ta konstrukcja stworzy moduł AngularJS nazwany calculatorApplication. Puste nawiasy kwadratowe oznaczają pustą listę zależności wymaganych przez naszą aplikację.
W następnym kroku chcemy zdefiniować podstawowe zachowanie. Chcielibyśmy, aby nasza aplikacja miała stronę startową zawierającą wiadomość powitalną oraz link do kalkulatora.
Ta strona powinna wyglądać następująco (w wolnym tłumaczeniu: To jest strona startowa. Możesz przejść do kalkulatora):

Jak widać adres url strony ma interesującą końcówkę #/home. Jest to po prostu adres podstrony w naszej aplikacji. Jest to standardowy sposób w jaki definiuje się strony w AngularJS.
Następnie chcielibyśmy skojarzyć adres /home z jakąś zawartością. W tym celu dodajmy definicję kontrolera dla tej strony zmieniając plik app.js w następujący sposób:
var app = angular.module("calculatorApplication", []).config(function ($routeProvider) {
    $routeProvider.when("/home", {
        templateUrl: "app/view/home.html",
        controller: "HomeController"
    });

    $routeProvider.otherwise({
        redirectTo: "/home"
    });
});

app.controller("HomeController", function () {
});
W tej części kodu dzieje się kilka interesujących rzeczy. Po pierwsze wywołujemy metodę config i przekazujemy jej jako parametr funkcję, która skonfiguruje naszą aplikację. Ta funkcja przyjmuje parametr $routeProvider, który jest odpowiedzialny za skojarzenie adresów url (m.in. sufiks /home) z odpowiednim zachowaniem. W naszym przypadku chcemy użyć szablonuu home.html jako wzorca widoku i kontrolera HomeController gdy w adresie znajduje się sufiks /home. W każdym innym przypadku chcemy, aby przeglądarka została przekierowana na ten adres.
Dodatkowo zdefiniowaliśmy kontroler o nazwie HomeController, który nie obsługuje żadnej specyficznej funkcjonalności.

Ostatnią rzeczą, którą musimy zrobić jest zdefiniowanie szablonu naszej strony głównej. W tym celu należy stworzyć plik Calculator/app/view/home.html i umieścić w nim następującą zawartość:
<div>
    <div>
        This is home page:
    </div>
    <div>
        You can go to <a href="#/calculator">Calculator</a>.
    </div>
</div>
Jak widać jest to bardzo prosty kod html, który wyświetli wiadomość powitalną i link do strony z kalkulatorem (której jeszcze nie stworzyliśmy).

5. Pierwsza próba działania aplikacji
Spróbujmy teraz sprawdzić, czy nasza aplikacja działa. Można to zrobić na dwa sposoby:
  • Umieścić ją w serwerze http i otworzyć adres pliku index.html w przeglądarce
  • Otworzyć plik index.html prosto z systemu plików
Na razie wykorzystajmy tę drugą możliwość ;)

Uwaga!
Z powodu zasad bezpieczeństwa niektóre przeglądarki internetowe mogą blokować dostęp do plików lokalnych dla wywołań AJAX z poziomu javascript'u. Ten przypadek występuje przynajmniej w przeglądarkach Google Chrome oraz Internet Explorer. Nie wiem jak to rozwiązać w przypadku IE, ale jeżeli chcesz testować aplikację z Chroma, musisz uruchomić przeglądarkę z parametrem --allow-file-access-from-files jak zostało to opisane w pytaniu na stackoverflow. To rozwiązanie nie jest konieczne jeżeli dostajesz się do aplikacji przez serwer http.
W przypadku Mozilla Firefox nasza aplikacja powinna działać poprawnie bez dodatkowych zmian.

6. Strona kalkulatora
Żeby dodać kalkulator do naszej aplikacji musimy stworzyć szablon html, który będzie definiował wygląd naszego kalkulatora. Umieścimy go w pliku Calculator/app/view/calculator.html:
<div class="calculator">
    <div class="display">0</div>
    <div class="digitsKeyboard">
        <div class="digitKey">1</div>
        <div class="digitKey">2</div>
        <div class="digitKey">3</div>
        <div class="digitKey">4</div>
        <div class="digitKey">5</div>
        <div class="digitKey">6</div>
        <div class="digitKey">7</div>
        <div class="digitKey">8</div>
        <div class="digitKey">9</div>
        <div class="digitKey">0</div>
        <div class="equalSignKey">=</div>
    </div>
    <div class="operationsKeyboard">
        <div class="operationKey">/</div>
        <div class="operationKey">*</div>
        <div class="operationKey">+</div>
        <div class="operationKey">-</div>
    </div>
</div>
Kalkulator składa się z wyświetlacza display, klawiatury z cyframi i znakiem równości digitsKeyboard oraz klawiatury z klawiszami operacji matematycznych operationsKeyboard. Dodatkowo musimy zdefinować styl CSS, który będzie opisywał szczegółowy wygląd naszego kalkulatora. W tym celu należy umieścić następujący kod CSS w pliku Calculator/app/style/default.css:
.calculator {
    display: inline-block;
    background-color: black;
    height: 240px;
    width: 208px;
    overflow: hidden;
}

.display {
    background-color: #DDDDDD;
    border: 1px solid black;
    font-family: monospace;
    font-size: 27px;
    font-weight: bold;
    height: 30px;
    padding: 0 4px;
    text-align: right;
    width: 198px;
    overflow: hidden;
}

.digitsKeyboard {
    overflow: hidden;
    width: 156px;
    float: left;
}

.digitKey {
    width: 50px;
    height:50px;
    background-color: #AABBCC;
}

.equalSignKey {
    width: 102px;
    height: 50px;
    background-color: #AACCBB;
}

.operationsKeyboard {
    overflow: hidden;
    width: 52px;
    height:208px;
    float: left;
}

.operationKey {
    width: 50px;
    height:50px;
    background-color: #CCAABB;
}

.digitKey, .equalSignKey, .operationKey {
    cursor: pointer;
    font-family: monospace;
    font-size: 33px;
    border: 1px solid black;
    line-height: 50px;
    text-align: center;
    float: left;
}

.digitKey:hover, .equalSignKey:hover, .operationKey:hover {
    opacity: 0.8;
}
.digitKey:active, .equalSignKey:active, .operationKey:active {
    opacity: 0.7;
}

Ostatnią rzeczą do wykonania jest zmapowanie adresu /calculator w aplikacji. W tym celu musimy dodać stronę kalkulatora do $routeProvider w ten sam sposób w jaki zdefiniowaliśmy stronę główną. Należy więc dodać poniższy kod do pliku Calculator/app/js/app.js:
$routeProvider.when("/calculator", {
    templateUrl: "app/view/calculator.html",
    controller: "CalculatorController"
}); 
Oraz zdefiniować nowy, pusty (na razie) kontroler kalkulatora:
app.controller("CalculatorController", function () {
});
Możesz teraz spróbować otworzyć kalkulator w przeglądarce. Wystarczy otworzyć plik index.html i kliknąć w link kalkulatora. Wtedy powinieneś zobaczyć kalkulator na ekranie.

7. Definicja stałych wartości
Dodamy teraz trochę życia do kontrolera CalculatorController. Żeby to zrobić zacznijmy od zmiany funkcji przekazanej w deklaracji kontrolera:
app.controller("CalculatorController", function ($scope) {
});
Jak widać dodaliśmy parametr $scope do definicji funkcji. Jest to obiekt w którym przechowywany jest stan naszej aplikacji. Dla naszych potrzeb wystarczy wiedzieć, że możemy zdefiniować w nim dowolne pole, które będzie dostępne w innych miejscach aplikacji.

Zacznijmy od zdefiniowania listy klawiszy kalkulatora, wraz z parametrami przez umieszczenie tego kodu w funkcji kontrolera:
Klawisze cyfr
$scope.digitKeys = [
    {label: "1", value: 1}, {label: "2", value: 2}, {label: "3", value: 3},
    {label: "4", value: 4}, {label: "5", value: 5}, {label: "6", value: 6},
    {label: "7", value: 7}, {label: "8", value: 8}, {label: "9", value: 9},
    {label: "0", value: 0}
];
Wykorzystaliśmy pole digitKeys do zdefiniowania tablicy obiektów reprezentujących każdą cyfrę na klawiaturze kalkulatora. Każdy klawisz ma przypisaną etykietę label, która będzie wyświetlana na danym klawiszu oraz wartość value, która będzie wykorzystana do obliczeń.
Klawisz równości
$scope.equalSignKey = {label: "="};
Tutaj zdefiniowaliśmy klawisz równości, który wymaga jedynie podania etykiety. Ta definicja została stworzona jedynie po to, aby zachować spójność w sposobie definiowana klawiszy. Innym rozwiązaniem byłoby umieszczenie znaku = bezpośrednio w pliku html.
Klawisze operacji
$scope.operationKeys = [
    {label: "/", operation: function (a, b) {return a / b}},
    {label: "*", operation: function (a, b) {return a * b}},
    {label: "+", operation: function (a, b) {return a + b}},
    {label: "-", operation: function (a, b) {return a - b}}
];
Ostatni zestaw klawiszy reprezentuje operacje matematyczne. Jak widać, w każdym obiekcie umieściliśmy funkcję, dzięki czemu będziemy mogli w łatwy sposób wykonać żądaną operację.

8. Dynamiczne generowanie klawiszy kalkulatora
Po zdefiniowaniu klawiszy w kontrolerze CalculatorController możemy je użyć do uproszczenia naszego szablonu html przez dynamiczne generowanie klawiszy. Zacznijmy od najprostrzego przypadku, czyli przycisku równości:
<div class="equalSignKey">
    {{ equalSignKey.label }}
</div>
W tym przypadku wykorzystaliśmy konstrukcję {{ }}, żeby przekazać AngularJS że w tym miejscu chcemy umieścić wartość equalSignKey.label. Angular wyszuka tę wartość w obiekcie $scope i powiąże ją z odpowiednią częścią pliku html.

Podobnie postąpimy w przypadku klawiszy cyfr, ale w tym celu wykorzystamy dyrektywę o nazwie ng-repeat, która umożliwia iterowanie po tablicy elementów i stworzenie tagu html dla każdego z nich:
<div class="digitKey" ng-repeat="key in digitKeys">
    {{ key.label }}
</div>
W tym przypadku AngularJS iteruje po elementach z tablicy $scope.digitKeys i przypisuje kolejne elementy do zmiennej key. Dzięki temu nie musimy powtarzać kodu html dla każdego klawisza.

Postąpimy tak samo dla klawiszy operacji:
<div class="operationKey" ng-repeat="key in operationKeys">
     {{ key.label }}
</div>
A więc wynikowy plik calculator.html powinien wyglądać następująco:
<div class="calculator">
    <div class="display">0</div>
    <div class="keyboard">
        <div class="digitKey" ng-repeat="key in digitKeys">
            {{ key.label }}
        </div>
        <div class="equalSignKey" ng-click="compute()">
            {{ equalSignKey.label }}
        </div>
    </div>
    <div class="operations">
        <div class="operationKey" ng-repeat="key in operationKeys">
            {{ key.label }}
        </div>
    </div>
</div>

W tym momencie można ponownie przetestować czy nasz kalkulator jest wyświetlany poprawnie.

9. Działanie kalkulatora
Teraz chcemy, aby nasz kalkulator zaczął działać jak prawdziwy, dlatego musimy zdefiniować zmienne, które będą przechowywały aktualny stan kalkulatora. Potrzebujemy pięć takich zmiennych:
  • displayValue - aktualna wartość wyświetlana na ekranie kalkulatora
  • valueA - pierwsza (lewa) wartość wykorzystywana do obliczeń
  • valueB - druga(prawa) wartość wykorzystywana do obliczeń
  • selectedOperation - która operacja matematyczna została wybrana przez użytkownika
  • clearValue - czy naciśnięcie następnej cyfry powinno wyczyścić ekran?
Zdefiniujmy je w obiekcie $scope i zainicjujmy je domyślnymi wartościami. Dodajmy ten kod wewnątrz kontrolera CalculatorController:
$scope.displayValue = 0;
$scope.valueA = 0;
$scope.valueB = 0;
$scope.selectedOperation = null;
$scope.clearValue = true;

10. Powiązanie zmiennej displayValue z wyświetlaną wartością
Gdy mamy już zmienną displayValue, chcielibyśmy, aby została wyświetlona na ekranie naszego kalkulatora. I to jest właśnie miejsce gdzie najlepsza cecha AngularJS wchodzi do gry. Wystarczy umieścić znacznik {{ displayValue }} wewnątrz calculator.html zmieniając <div class="display">0</div> tak, aby wyglądał tak:
<div class="display">{{ displayValue }}</div>
To wszystko!
Od tego momentu AngularJS będzie robił swoje sztuczki, zapewniając, że wyświetlana wartość będzie równa wartości zmiennej. Jeżeli zmienisz wartość przypisaną do zmiennej, automatyczne zmieni się wartość wyświetlana na ekranie kalkulatora.
To nie wszystko, gdybyśmy umożliwili użytkownikowi bezpośrednią edycję ekranu, to AngularJS automatycznie zmieniłby wartość zmiennej. Czyli takie powiązanie działa w obie strony, co znacznie upraszcza pisanie naszej aplikacji.

11. Zachowanie przycisków kalkulatora
Mamy już wszystkie elementy kalkulatora na miejscu, czas więc tknąć w niego trochę życia. Głównym pomysłem jest skojarzenie przycisków z odpowiednim zachowaniem. Aby to zrobić użyjemy dyrektywy ng-click. Zmieńmy szablon calculator.html:

<div class="calculator">
    <div class="display">{{ displayValue }}</div>
    <div class="digitsKeyboard">
        <div class="digitKey" ng-repeat="key in digitKeys" 
                ng-click="digitClicked(key.value)">
            {{ key.label }}
        </div>
        <div class="equalSignKey" ng-click="compute()">
            {{ equalSignKey.label }}
        </div>
    </div>
    <div class="operationsKeyboard">
        <div class="operationKey" ng-repeat="key in operationKeys" 
                ng-click="operationClicked(key.operation)">
            {{ key.label }}
        </div>
    </div>
</div>

W tej części kodu dodaliśmy trzy dyrektywy three ng-click, które zawierają proste wywołania funkcji JavaScript:
  • Dla klawiszy z cyframi: ng-click="digitClicked(key.value)"
  • Dla klawiszy operacji: ng-click="operationClicked(key.operation)"
  • Dla znaku równości: ng-click="compute()"
Jak widać mogliśmy przekazać dane zdefiniowane wcześniej w zmiennych AngularJS w ten sam sposób w jaki przekazywaliśmy je wcześniej w blokach {{ }}. Chcemy, aby naciśnięcie konkretnego przycisku wywołało odpowiednią, przypisaną do niego funkcję.

Zdefiniujmy teraz te trzy funkcje wewnątrz kontrolera CalculatorController. Co ważne, te funkcje zostaną zdefiniowane jako część obiektu $scope:
  • Funkcja do obsługi przycisków z cyframi:
$scope.digitClicked = function (digit) {
    if ($scope.clearValue) {
        $scope.displayValue = digit;
        $scope.clearValue = false;
    } else {
        $scope.displayValue = $scope.displayValue * 10 + digit;
    }
    $scope.valueB = $scope.displayValue
};
Po kliknięciu w przycisk cyfry chcemy zaktualizować wartość wyświetlaną na ekranie, przez zamianę aktualnie wyświetlanej liczby, lub przez zwiększenie wyświetlanej liczby o kolejną cyfrę (wystarczy pomnożyć wyświetlaną liczbę przez 10 i do wyniku dodać wybraną cyfrę). Dodatkowo musimy zapamiętać wyświetlaną liczbę jako drugą wartość, którą wkyorzystamy do wykonania operacji matematycznej.
  • Funkcja do obsługi przycisków operacji:
$scope.operationClicked = function (operation) {
    $scope.selectedOperation = operation;
    $scope.valueA = $scope.displayValue;
    $scope.valueB = $scope.displayValue;
    $scope.clearValue = true;
};
W przypadku, gdy użytkownik kliknie w przycisk operacji musimy po prostu zapamiętać, którą operację wybrał i przypisać wyświetlaną wartość do obydwu zmiennych, które będziemy mogli wykorzystać do wykonania tej operacji. Dodatkowo musimy ustawić flagę clearValue, tak że przy następnym kliknięciu w cyfrę zastąpi ona wyświetlaną na ekranie liczbę.
  • Funkcja do obsługi przycisku równości:
$scope.compute = function () {
    if($scope.selectedOperation != null) {
        $scope.displayValue = Math.floor(
                       $scope.selectedOperation($scope.valueA, $scope.valueB));
        $scope.clearValue = true;
        $scope.valueA = $scope.displayValue;
    }
}
Ostatnia funkcja jest odpowiedzialna za wyliczenie wyniku, jeżeli wcześniej wybrano operację (przypisaną do pola $scope.selectedOperation). Jest to wykonywane przez proste wywołanie zapamiętanej funkcji. Dodatkowo ustawiana jest flaga clearValue, a wynik obliczenia jest zapamiętywany jako pierwszy składnik kolejnej operacji.

Podsumowanie
To wszystko, mamy już w pełni funkcjonalny kalkulator dla przeglądarek internetowych. Jak widzisz AngularJS jest prostym w użyciu (przynajmniej przy tworzeniu kalkulatora) i bardzo pomocnym frameworkiem dla aplikacji napisanych w JavaScript. Daje nam nowe możliwości wiązania stanu aplikacji z warstwą widoku opartą na html, co znacznie upraszcza tworzenie dynamicznych rozwiązań.

Pełny kod źródłowy tego kalkulatora jest dostępny na github.com.