Kontrolery

Kontrolery w Angularze dostarczają logiki biznesowej, aby kierować zachowaniami widoku, na przykład, użytkownik klika w przycisk albo wpisuje jakiś tekst do formularza. W dodatku kontrolery przygotowują model dla szablonu widoku. Według zasad kontroler nie powinienem manipulować Document Object Model(DOM) bezpośrednio. Takie zachowanie ma za zadanie uprościć pisanie testów jednostkowych kontrolera.

Przypisanie domyślnej wartości do modelu

Chcemy przypisać domyślną wartość do scope w kontekście naszego kontrolera. Aby tego dokonać, użyjemy dyrektywy ng-controller w naszym szablonie:

//index.html


  
    
    
    
  
  
    

{{wartosc}}

Następnie, definiujemy zmienną scope w kontrolerze:

//app.js
var app = angular.module('MojaApka', []);

app.controller('MojKontroler', function($scope) {
  $scope.wartosc = "Jakas wartosc";
});

W zależności gdzie użyjemy dyrektywy ng-controller, powinniśmy przypisać scope. Scope jest hierarchiczny i jest zgodny z hierarchią węzłów DOM. W naszym przykładzie, wyrażenie wartosc jest poprawnie zainicjalizowana, ponieważ wartosc jest ustawiona w naszym kontrolerze MojKontroler. Warto pamiętać, że powyższa aplikacja nie zadziała, jeżeli przypisanie wartości do zmiennej będzie zadeklarowane poza zakresem kontrolera:

//To nie zadziała.

{{wartosc}}

W tym przypadku {{wartosc}} nie będzie wyrenderowany w wyniku czego zmienna {{wartosc}} będzie mieć wartość null albo undefined.

Zmiana wartości w modelu za pomocą kontrolera

Chcemy inkrementować wartość w modelu o 1 używająć kontrolera. Poniżej przykład funkcji, która inkrementuje zmiany w scope:

var app = angular.module('MojaApka', []);

app.controller('MojKontroler', function($scope) {
  $scope.wartosc = 1;

  $scope.inkrementacjaWartosci = function(inkrementacja) {
    $scope.wartosc += inkrementacja;
  };
});

Taka funkcja może być użyta w wyrażeniu, my użyjemy ng-init:

{{wartosc}}

Dyrektywa ng-init jest wykonywana, kiedy strona zostanie wczytana i zaczytuje funkcje inkrementacjaWartosci, która jest zdefiniowana w MojKontroler. Zadeklarowanie funkcji jest podobne do zadeklarowania wartosc, ale musi posiadać odpowiednie nawiasy. Ktoś zapyta czemu nie zrobić wartosc = wartosc + 1, takie zachowanie jest możliwe do inkrementowania wewnątrz wyrażenia, ale takie podejście spowoduję, że nasza funkcja będzie bardziej skomplikowana. Napisanie funkcji w kontrolerze pozwala odseparować logikę biznesową od widoku i pozwala w łatwy sposób napisać testy jednostkowe dla tego przypadku.

Enkapsulacja wartości w modelu z kontrolera

Chcemy otrzymać model przez funkcje zamiast bezpośredniego dostępu do zakresu(scope) szablonu, aby mieć możliwość hermetyzacji wartości z modelu. Do osiągnięcia celu zdefiniujemy getter i zwrócimy wartość z modelu:

var app = angular.module('MojaApka', []);

app.controller('MojKontroler', function($scope) {
  $scope.wartosc = 1;

  $scope.getInkrementacjaWartosci = function() {
    return $scope.wartosc + 1;
  };
});

Teraz w szablonie możemy użyć wyrażenia do wywołania powyższego kod:

{{getInkrementacjaWartosci()}}

Kontroler MojKontroler definiuje funkcje getInkrementacjaWartosci, która używa zmiennej wartosc i zwraca ją powiększoną o 1.

Zmiany w zakresie(Scope)

Chcemy, aby zmiany w modelu reagowały na nowe akcje, które wprowadzimy. W naszym przykładzie chcemy ustawić inną wartość w modelu zależnego, od wartości którego nasłuchujemy. Mówiąc prościej, zmieniając jedną wartość chcemy interakcji w drugiej wartości. Aby rozwiązać ten problem, musimy użyć funkcji $watch w kontrolerze:

//app.js
var app = angular.module('MojaApka', []);

app.controller('MojKontroler', function($scope) {
  $scope.nazwa = "";

  $scope.$watch("nazwa", function(nowaWartosc, staraWartosc) {
    if ($scope.nazwa.length > 0) {
      $scope.powitanie = "Siema " + $scope.nazwa;
    }
  });
});

W naszym przykładzie, użyjemy wartości która zostanie wprowadzona do input’a:

//index.html


  
    
    
    
  
   
    

{{powitanie}}

Wartość w zmiennej powitanie będzie zmieniona zawsze, kiedy nastąpi zmiana w zmiennej nazwa oraz gdy wartość nie będzie pusta.

Pierwszym argumentem funkcji $watch jest zmienna nazwa, to wyrażenie typowe dla Angulara, dzięki czemu można użyć bardziej skomplikowanych wyrażeń (na przykład: [wartosc1, wartosc2] | json) albo zdarzeń w funkcji JavaScript. W naszym przykładzie potrzebujemy zwrócić String w funkcji watch:


//app.js
var app = angular.module('MojaApka', []);

app.controller('MojKontroler', function($scope) {
  $scope.nazwa = "";

  $scope.$watch(function() {
    return $scope.nazwa;
  }, function(nowaWartosc, staraWartosc) {
    console.log("Zmiane1 wyłapano: " + nowaWartosc);
    console.log("Zmiane2 wyłapano: " + staraWartosc);
  });
});

Drugi argument funkcji jest uruchamiany, kiedy wyrażenie podczas obliczeń zwróci inną wartość. Pierwszy parametr jest nowa wartością a drugi parametr jest starą wartością. Prościej mówiąc, staraWartosc przechowuje zawartość poprzedniego stanu. Dla lepszego zobrazowania przykładu warto go uruchomić samemu i zobaczyć co wyświetla się na konsoli przeglądarki.

Przetwarzanie modelów pomiędzy zagnieżdżonymi kontrolerami

Chcemy przetwarzać, współdzielić model pomiędzy dwoma zagnieżdżonymi hierarchicznymi kontrolerami. Aby tego dokonać, użyjemy obiektu JavaScript zamiast prymitywnych typów czy bezpośrednich odniesień do zakresu ($parent scope). Poniższy przykład przedstawia szablon z naszym kontrolerem MojKontroler i zagnieżdżony kontroler MojZagniezdzonyCtrl:



  
    
    
  
  
    




Plik js zawiera definicje kontrolera z zakresem oraz domyślnymi wartościami:

var app = angular.module("MojaApka", []);
app.controller("MojKontroler", function ($scope) {
  $scope.nazwa = "Piotr";
  $scope.uzytkownik = {
    nazwa: "Paweł"
  };
});
app.controller("MojZagniezdzonyCtrl", function ($scope) {});

Wszystkie domyślne wartości zostały zdefinowane w MojKontroler, który jest rodzicem dla MojZagniezdzonyCtrl. Kiedy występują zmiany w pierwszym polu input, zmiany będą synchroniczne z innymi polami które są powiązane z zmienna nazwa. Wszystkie zmienne współdzielą ten sam zakres tak długo, jak tylko czytają z tej samej zmiennej. Jeśli zmienimy zagnieżdzoną wartość, kopia w zakresię należąca do MojZagniezdzonyCtrl będzie utworzona. Od teraz, zmiany w pierwszym polu input będą zmieniame tylko w zagnieżdżonym polu z input’em, który ma referencje do zakresu rodzica przez wyrażenie $parent.nazwa.

Zachowanie wartości opartych na obiekcie są różne w tym przypadku. Czy zmienimy zagnieżdżenia albo zakres pola input w MojKontroler, zmiany będą zawsze synchroniczne. W Angularze zakres dziedziczy właściwości z zakresu nadrzędnego ($parent scope). Obiekty są więc odniesieniami i pozostają w synchronizacji, podczas gdy prymitywne typy są synchronizowane tylko tak długo, jak długo nie są zmieniane w zakresie podrzędnym (dziecko).

Generalnie zaleca się, aby nie używać $parent.nazwa, zamiast tego lepiej używać obiektu do współdzielenia właściwości modelu. Jeżeli używamy $parent.nazwa, w zagnieżdżonym kontrolerze MojZagniezdzonyCtrl to musimy pamiętać, że wymaga on nie tylko pewnego atrybutu modelu, ale także prawidłowy zakres(scope) hierarchii, z którym pracuje.

Dzielenie kodu pomiędzy kontrolery używając serwisów

Chcemy dzielić logikę biznesową pomiędzy kontrolery. Aby tego dokonać wykorzystamy Service w naszej logice biznesowej i użyjemy wstrzykiwania zależności, aby użyć ten serwis w naszych kontrolerach. Szablon poniżej daje dostęp do listy użytkowników z dwóch kontrolerów:

//index.html


  
    
    
    
  
   
    
  • {{uzytkownik}}
Pierwszy użytkownik: {{pierwszyUzytkownik}}

Implementacja serwisu i kontrolera znajdują się w pliku js poniżej, implementuje serwis użytkownika, a kontrolery ustawiają początkowo zakres:

//app.js
var app = angular.module("MojaApka", []);
app.factory("SerwisUzytkownika", function () {
  var uzytkownicy = ["Piotr", "Daniel", "Ola"];
  return {
    wszyscy: function () {
      return uzytkownicy;
    },
    pierwszy: function () {
      return uzytkownicy[0];
    }
  };
});
app.controller("MojKontroler", function ($scope, SerwisUzytkownika) {
  $scope.uzytkownicy = SerwisUzytkownika.wszyscy();
});
app.controller("InnyKontroler", function ($scope, SerwisUzytkownika) {
  $scope.pierwszyUzytkownik = SerwisUzytkownika.pierwszy();
});

Metoda factory tworzy singleton SerwisUzytkownika, który zwraca dwie funkcje, otrzymując wszystkich użytkowników i tylko jednego użytkownika. Kontrolery pobierają SerwisUzytkownika wstrzyknięty przez dodanie go do funkcji kontrolera jako parametr.

Używając wstrzykiwania zależności mamy teraz dobrą okazję aby przetestować nasz kontroler poprzez wstrzyknięcie zależność SerwisUzytkownika. Jedynym minusem jest to, że nie można zmodyfikować kodu z góry bez naruszania go, ponieważ mechanizm wstrzykiwania opiera się na dokładnej reprezentacji string z SerwisUzytkownika. Dlatego zaleca się definiowanie zależności za pomocą adnotacji w linii prostej, które działają nawet wtedy, gdy zostały zmodyfikowane:

app.controller("InnyKontroler", ["$scope", "SerwisUzytkownika", 
  function ($scope, SerwisUzytkownika) {
    $scope.pierwszyUzytkownik = SerwisUzytkownika.pierwszy();
  }
]);

Składnia wygląda trochę śmiesznie, ale ponieważ ciągi w tablicach nie są zmieniane podczas procesu upraszczania, to takie podejście rozwiązuje nasz problem. Warto pamiętać, że można zmieniać nazwy parametrów funkcji, ponieważ mechanizm wstrzykiwania zależy wyłącznie od kolejności definicji tablicy.

Można spróbować innego sposobu, aby uzyskać ten sam wynik mianowicie użyjemy adnotacji $inject:

var innyKontroler = function ($scope, SerwisUzytkownika) {
  $scope.pierwszyUzytkownik = SerwisUzytkownika.pierwszy();
};
innyKontroler.$inject = ["$scope", "SerwisUzytkownika"];

Powyższy wymaga od nas tymczasowej zmiennej do wywołania serwisu $inject.

 
Pozdro 🙂
 

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

*