ES6 – default + rest + spread

javascript

Biorąc pod uwagę całe lata narzekań, ECMAScript 2015 przynosi nam wiele nowości, czego rezultatem jest spora ilość usprawnień. Sprawiają one, że pisanie kodu w JavaScript jest bardziej intuicyjne oraz po prostu szybsze. Spójrzmy zatem na nowy sposób przekazywania parametrów do funkcji.


Zobacz całą serię: Let-s talk about ECMAScript 2015


default

Jest to małe, ale bardzo przydatne usprawnienie w przekazywaniu parametrów do funkcji. Wiemy przecież, że funkcje w języku JavaScript pozwalają na przekazanie do nich dowolnej ilości parametrów. Z pewnością nie raz spotkaliście się z podobnym kodem:

function inc(number, increment) {
  // set default to 1 if increment not passed
  // (or passed as undefined)
  increment = increment || 1;
  return number + increment;
}
console.log(inc(2, 2)); // 4
console.log(inc(2));    // 3

Operator logiczny OR (||) przypisuje w tym wypadku wartość 1 do zmiennej increment, ponieważ lewy człon wyrażenia to undefined, czyli false.

ES6 pozwoli pozbyć się tego typu wyrażeń, wykorzystując w tym celu parametry z domyślnymi wartościami. Funkcja inc z wykorzystaniem składni ES6 prezentuje się zatem następująco:

function inc(number, increment = 1) {
  return number + increment;
}
console.log(inc(2, 2)); // 4
console.log(inc(2));    // 3

Tym samym parametry z wartościami domyślnymi są opcjonalne.

Możemy również definiować domyślne wartości parametrów, które występują w środku sygnatury funkcji:

function sum(a, b = 2, c) {
  return a + b + c;
}
console.log(sum(1, 5, 10));         // 16 -> b === 5
console.log(sum(1, undefined, 10)); // 13 -> b as default

Wykorzystanie wartości domyślnej następuje w tym wypadku w momencie, gdy jako wartość parametru podamy undefined.

Wartości domyślne nie muszą być typami prymitywnymi. Jako domyślną wartość możemy przypisać choćby rezultat wykonania funkcji:

function getDefaultIncrement() {
  return 1;
}
function inc(number, increment = getDefaultIncrement()) {
  return number + increment;
}
console.log(inc(2, 2)); // 4
console.log(inc(2));    // 3

rest

Spróbujmy napisać wcześniejszą funkcję sum tak, by brała pod uwagę wszystkie parametry jakie do niej przekażemy. By zachować czystość kodu, pominiemy tutaj walidację. Jeżeli próbowalibyśmy użyć do tego zadania składni dobrze znanej z ES5, to z pewnością otrzymalibyśmy coś takiego:

function sum() {
   var numbers = Array.prototype.slice.call(arguments),
       result = 0;
   numbers.forEach(function (number) {
       result += number;
   });
   return result;
}
console.log(sum(1));             // 1
console.log(sum(1, 2, 3, 4, 5)); // 15

Przy takim podejściu nie wiemy od razu co robi dana funkcja. Nie wystarczy, że spojrzymy na jej sygnaturę. Musimy jeszcze przeskanować jej ciało, znaleźć obiekt arguments i wywnioskować, że chodzi tutaj o zsumowanie wszystkich parametrów przekazanych do funkcji.

Składnia ES6 znacznie lepiej radzi sobie z podobnym problemem . Wprowadza ona pojęcie rest parameters, gdzie parametr poprzedzony (trzema kropkami) staje się tablicą, do której „wpadają” wszystkie pozostałe parametry przekazane do funkcji.

Obiekt arguments zawierać będzie wszystkie parametry funkcji - te nazwane i nie.

Możemy użyć nowej składni ECMAScript 2015 i przepisać funkcję sum:

function sum(…numbers) {
  var result = 0;
  numbers.forEach(function (number) {
    result += number;
  });
  return result;
}
console.log(sum(1)); // 1
console.log(sum(1, 2, 3, 4, 5)); // 15

Jest jednak jedno ALE: nie możemy posiadać więcej niż jednego parametru typu …rest. W przeciwnym razie otrzymamy błąd jak niżej:

function sum(…numbers, last) { // causes a syntax error
  var result = 0;
  numbers.forEach(function (number) {
    result += number;
  });
  return result;
}

spread

Kolejne wyrażenie, to spread, które przypomina trochę konstrukcję rest ze względu na swoją notację z trzema kropkami. Dzieli tablicę na poszczególne wartości, które przekazywane są jako osobne parametry do ciała funkcji.

Po raz kolejny zdefiniujmy funkcję sum, do której przekażemy wyrażenie typu spread:

function sum(a, b, c) {
  return a + b + c;
}
var args = [1, 2, 3];
console.log(sum(…args)); // 6

Odpowiednik takiego kodu w ES5, to:

function sum(a, b, c) {
  return a + b + c;
}
var args = [1, 2, 3];
console.log(sum.apply(undefined, args)); // 6

Zamiast więc używać funkcji apply możemy od teraz posługiwać się „rozbiciem” tablicy na osobne parametry. Rozwiązanie prostsze i według mnie również bardziej czytelne.

Ciekawostką jest fakt, że wyrażenie spread możemy łączyć ze standardowym sposobem przekazywania parametru do funkcji:

function sum(a, b, c) {
  return a + b + c;
}
var args = [1, 2];
console.log(sum(…args, 3)); // 6

Rezultat jest dokładnie taki sam jak w poprzednim przykładzie. Tym razem tablica z dwiema wartościami zostaje rozbita na dwa parametry odpowiadające ab, zaś trzecim parametrem jest liczba 3, która odpowiada zmiennej c.

ES6 – arrow functions

javascript

Kolejnym elementem standardu ECMAScript 2015 będą bez wątpienia arrow functions. Znane również jako fat functions, pozwolą tworzyć kod bez – zbędnego w wielu przypadkach – słowa kluczowego function oraz ze zbindowanym this do ciała funkcji.


Zobacz całą serię: Let-s talk about ECMAScript 2015


Syntactic sugar

(w oryginalnym artykule użyłem właśnie tego nagłówka, jako, że polski odpowiednik brzmi idiotycznie)

Spójrzmy na sygnaturę nowej składni anonimowej funkcji:

([param] [, param]) => {
 statements
}
           param => expression
(param1, param2) => { block }

, którą możemy użyć w następujący sposób:

    () => { … }            // no argument
     x => { … }            // one argument
(x, y) => { … }            // several arguments
     x => { return x * x } // block
    x => x * x             // expression, same as above
Pamiętaj: każda tak tworzona funkcja jest anonimowa!

Wyrażenia lambda w JavaScript! Wspaniale :)

Zamiast pisać:

[3, 4, 5].map(function (n) {
 return n * n;
});

możemy po prostu zrobić coś takiego:

[3, 4, 5].map(n => n * n);

Już jest dobrze, a jest coś jeszcze..

Fixed „this” = lexical „this”

Funkcja tworzona przy pomocy konstrukcji z => binduje this wewnątrz ciała funkcji w miejscu jej deklaracji, a nie użycia. Tym samym nie musimy już używać funkcji bind, call czy apply. Nie musimy również tworzyć konstrukcji typu:

var self = this;

Dla mnie to naprawdę spore ułatwienie, a przy okazji sposób na lepszą optymalizację kodu.

// ES5
function FancyObject() {
 var self = this;
 
 self.name = 'FancyObject';
 setTimeout(function () {
  self.name = 'Hello World!';
 }, 1000);
}

// ES6
function FancyObject() {
 this.name = 'FancyObject';
 setTimeout(() => {
  this.name = 'Hello World!'; // properly refers to FancyObject
 }, 1000);
}

Konstrukcja funkcji za pomocą =>, to:

  • typeof zwracające function
  • instanceof zwracające Function

, ale niestety także kilka niedogodności:

  • Tak stworzona funkcja nie może być użyta w formie konstruktora i każde jej wywołanie ze słowem new rzuci wyjątek.
  • Fixed this oznacza, że wartość zbindowana wewnątrz funkcji nie może zostać zmieniona przez cały cykl jej życia.
  • Funkcja nie może być funkcją nazwaną.
  • Nie mamy samej deklaracji funkcji, stąd nie istnieje tutaj hoisting pozwalający użyć danej funkcji przed jej inicjalizacją.

ES6  – let + const

javascript

Pierwszy temat związany z nadchodzącym ECMAScript 2015, czyli letconst. Oba pojęcia bardzo mocno związane są ze scope deklarowanych zmiennych, co postaram się pokazać w porównaniu ze sposobem deklaracji zmiennych za pomocą słowa kluczowego var.


Zobacz całą serię: Let-s talk about ECMAScript 2015


Spójrzmy wpierw jednak na przykład pokazujący użycie var i deklaracji zmiennych znanych z ES5 i wcześniej.

var a = 1;
 
if (1 === a) {
 var b = 2; 
}
 
for (var c = 0; c < 3; c++) {
 // …
}
 
function letsDeclareAnotherOne() {
 var d = 4;
}
 
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(d); // ReferenceError: d is not defined
 
// window
console.log(window.a); // 1
console.log(window.b); // 2
console.log(window.c); // 3
console.log(window.d); // undefined
  1. Zmienna a deklarowana jest jako globalna.
  2. Zmienna b deklarowana jest wprawdzie wewnątrz bloku if, ale jest ona również widoczna poza nim.
  3. Podobnie jak deklaracja b, zmienna c deklarowana jest w bloku for, który nie ogranicza jej swoim zasięgiem.
  4. Dopiero zmienna d posiada swój własny scope, ponieważ ograniczona jest ciałem funkcji.
Variables in JavaScript are hoisted to the top!

Hoisting, to w JavaScript domyślne zachowanie w momencie deklaracji zmiennych = wynoszenie dekleracji na samą górę skryptu lub ciała funkcji.

Tym samym zmienna w języku JavaScript może być użyta zanim zostanie zainicjalizowana. Różnicę w dekleracji zmiennych, a ich inicjalizacji przedstawia poniższy przykład:

var n = 1;
 
(function () {
 console.log(n);
 var n = 2;
 console.log(n);
})();
 
console.log(n);

let

Spójrzmy teraz na nowy sposób deklaracji zmiennych, który bezpośrednio wpływa na ich zakres działania.

let a = 1;
 
if (1 === a) {
 let b = 2; 
}
 
for (let c = 0; c < 3; c++) {
 // …
}
 
function letsDeclareAnotherOne() {
 let d = 4;
}
 
console.log(a); // 1
console.log(b); // ReferenceError: b is not defined
console.log(c); // ReferenceError: c is not defined
console.log(d); // ReferenceError: d is not defined
 
// window
console.log(window.a); // 1
console.log(window.b); // undefined
console.log(window.c); // undefined
console.log(window.d); // undefined

Jest to ten sam przykład kodu, który podawałem wyżej dla ES5. Tym razem jednak widzimy różnice w wartościach, które otrzymamy w konsoli przeglądarki. Tylko zmienna a zadeklarowana została jako globalne, a wszystkie pozostałe mają zasięg blokowy.

const

W przypadku użycia const otrzymamy dokładnie takie same wyniki jak dla deklaracji zmiennych z użyciem słowa kluczowego let. const tym samym również pozwala deklarować zmienne o zasięgu blokowym. Różni się jednak od let (i var również) tym, że raz zainicjalizowana wartość zmiennej nie może zostać nadpisana. Można było się tego domyślać po nazwie const (stała).

{
 const PI = 3.141593;
 PI = 3.14; // throws “PI” is read-only
}
 
console.log(PI); // throws ReferenceError: PI is not defined

Każda próba nadpisania zmiennej utworzonej za pomocą słówka const skończy się rzuceniem wyjątku przez interpeter JavaScript.

Powyższy przykład w ES5 mógłby wyglądać następująco:

var PI = (function () {
 var PI = 3.141593;
 return function () { return PI; };
})();

ES6 – Let’s talk about ECMAScript 2015

javascript

Czas na powrót wraz z serią Let’s talk about ECMAScript 2015, którą miałem okazję tworzyć na @medium.com. Wracać tutaj nie zamierzałem, ale ostatecznie zdecydowałem, że przetłumaczę wszystkie artykuły na język polski, a tym samym wniosę trochę świeżości na bloga.


Zobacz całą serię: Let-s talk about ECMAScript 2015


Oficjalna specyfikacja ECMAScript 2015 niedawno ujrzała światło dzienne i tym samym przygoda z ES6 zaczyna stanowić codzienność w JavaScriptowym świecie.

ES6 to znacząca zmiana dla języka JavaScript. Poprzednia wersja istnieje na rynku od 2009 roku. W tym czasie pojawiły się (i / lub rozwijały) frameworki takie jak AngularJS, Backbone, Ember, Meteor, Aurelia, ReactJS, Ionic i wiele innych. Ich kolejne wersje będą już wykorzystywać dobrodziejstwa, które oferuje standard ECMAScript 2015.

ECMAScript 2015 to szereg nowości:

  • arrows
  • classes
  • enhanced object literals
  • template strings
  • destructuring
  • default + rest + spread
  • let + const
  • iterators + for..of
  • generators
  • unicode
  • modules
  • module loaders
  • map + set + weakmap + weakset
  • proxies
  • symbols
  • subclassable built-ins
  • promises
  • math + number + string + object APIs
  • binary and octal literals
  • reflect api
  • tail calls

Celowo zachowuję tutaj angielskie nazewnictwo, ponieważ tytuły kolejnych wpisów będą je odzwierciedlać. Znaczącą większość zakresu udało mi się opisać na bazie draftu specyfikacji, który istniał przed pojawieniem się oficjalnej wersji.

Producenci przeglądarek są na dobrej drodze, by z czasem w pełni wspierać nowy standard. Natomiast dzisiaj, dzięki takim narzędziom jak Babel, czy Traceur, możemy w dużej mierze korzystać z możliwości ES6 już teraz.

Zobacz: aktualne wsparcie przeglądarek dla ES6.

Wielokrotnie podkreślałem w swoich artykułach stwierdzenie: Future is bright. Jesteśmy na dobrej drodze by wypchnąć JavaScript na kolejny poziom.


ECMAScript 2015 - Let's talk about ES6

PS. Na bazie serii powstała również książka ECMAScript 2015 – Let’s talk about ES6, którą możecie poczytać ZA DARMO ONLINE.

Angular 1.3.0 (superluminal-nudge)

Po 8 miesiącach pracy około 400 osób, ponad 2000 zmianach, prawie 20 wersjach testowych i 6 wersjach RC, światło dzienne ujrzała nowa wersja frameworka AngularJS, oznaczona numerem 1.3.0Superluminal-nudge – bo tak nazwane zostało stabilne wydanie, przynosi nam ponad 400 poprawek, lepszą dokumentację oraz kilka nowości, o których za chwilę.

Jedną z najważniejszych informacji jest fakt, że nowa wersja AngularJS nie wspiera już przeglądarki Internet Explorer 8. Pozwoliło to twórcom na implementację funkcjonalności oraz wprowadzenie usprawnień, które w przeciwnym przypadku nie byłyby możliwe. Osoby, które w swoich projektach dalej wspierać będą IE8 zmuszone są tym samym do korzystania ze stabilnej gałęzi 1.2.x.

Wydanie 1.3.0, to przede wszystkim nowości:

  • one-time binding – poprzez użycie prefixu „::” przed wyrażeniem, które interpolujemy po stronie HTML, jesteśmy w stanie wyeliminować nadmierną ilość $watch, które powodują, że obserwowane są zmiany modelu. Wyrażenie bindowane jest tylko raz, w momencie rozwiązania wartości, po czym $watch zostaje usunięty. Tym samym wartości, które się nie zmieniają, a powinny zostać tylko wyświetlone, nie są już „odwiedzane” w pętli $digest, co przekłada się oczywiście na wydajność. Osoby, które do tej pory korzystały z dodatkowych bibliotek, takich jak: bindonce lub angular-once, będą mogły używać natywnej implementacji frameworka.
  • ngAria – czyli implementacja ARIA (Accessible Rich Internet Applications) – pozwalająca na przekazanie kontroli nad aplikacją klawiaturze i czytnikom tekstu, które tym samym umożliwiają pracę ludziom ze ślepotą i upośledzeniem wzroku.
  • ngMessages – nowa dyrektywa, która pozwala na interakcję z formularzami i ułatwia przekazywanie informacji do użytkownika o stanie poprawności wypełnianych danych
  • ngModelOptions – kolejna dyrektywa, która integruje się z formularzami i pozwala definiować oraz obsługiwać zdarzenia, które mają być realizowane w ramach podejmowanych akcji przez użytkownika. Dzięki niej jesteśmy w stanie zdefiniować choćby to, w jaki sposób aktualizowany jest model danych, który zdefiniowany został dla elementów formularza. Przykładowo: jesteśmy w stanie zdefiniować zdarzenie, które w momencie zdarzenie blur na elemencie input wyczyści jego wartość, gdy reguły walidacji nie zostaną spełnione, a tym samym nasz model danych pozostanie niezmieniony.
  • Strict DI – walidator, który w momencie jego włączenia w konfiguracji angular.bootstrap, pokaże nam miejsca, w których nie użyto adnotacji zależności, które pozwalają nam w pełni poprawnie zminimalizować kod.

Poza głównymi zmianami, pojawiło się także sporo nowości w module ngAnimate, oraz ngModel.

Pełna lista zmian dostępna jest w CHANGELOG projektu, a osoby zainteresowane migracją z wersji 1.2.x do 1.3.x mogą skorzystać z dokumentu opisującego tak zwane BREAKING CHANGES.

AngularJS #8 – Factory vs Service vs Provider

AngularJS



AngularJS daje nam możliwość pisania kodu na wiele różnych sposobów. Jedną z tendencji jaką zauważam na StackOverflow jest to, że ludzie starają się upchać do swoich kontrolerów wszystko co tylko możliwe – łącznie z logiką biznesową, pobieraniem / zapisywaniem danych, operacjach na DOM, czy przetwarzaniu wielkich struktur danych. Jest to wprawdzie domena tych mniej doświadczonych, jednak warto by każdy starał się odchudzać swoje kontrolery już na etapie pisania pierwszego kodu, a nie na etapie refaktoringu lub później (albo w ogóle!).

Kontrolery

Kontrolery w AngularJS mają to do siebie, że tworzone są (umieszczane w pamięci przeglądarki) w ramach tego jak są potrzebne i usuwane, gdy przestają być używane. Wyobraźmy sobie przechodzenie pomiędzy różnymi stronami naszej aplikacji, pomiędzy różnymi definicjami routingu, gdzie dla każdej strony wywoływany jest osobny kontroler. Następnie wyobraźmy sobie, że wszystkie elementy w kontrolerach tworzone i usuwane są za każdym takim przejściem ze strony do strony. Jest to niebywałe marnotrastwo zasobów, co wpływa także na wydajność pisanej przez nas aplikacji. Całą tą logikę odpowiedzialną chociażby za pobieranie danych lub ich zapisywanie po stronie API (backendu) jesteśmy w stanie przenieść do osobnych serwisów, które to wstrzyknięte do naszego kontrolera pozwolą zrealizować dokładnie taką samą funkcjonalność. Serwisy mają tą przewagę, że tworzone są jednorazowo (jako singletony) i wstrzykiwane mogą być jako zależność do wielu kontrolerów.

Factory vs Service vs Provider

Problem może pojawić się na etapie wyboru odpowiedniej drogi, która pozwoli nam zrealizować daną funkcjonalność. AngularJS daje nam obecnie trzy sposoby na odchudzenie naszych kontrolerów poprzez użycie:

  • Factory
  • Service
  • Provider

Poniżej postaram się omówić każdy z nich oraz wskazać różnice, które mogą zadecydować o konkretnym wyborze. Przykłady będą bajecznie proste, więc z pewnością każdy poradzi sobie z ich przyswojeniem. Celowo nie wykorzystuję tutaj $http, czy $resource, by nie komplikować i zaciemniać kodu. Znając oba mechanizmy możecie posłużyć się nimi do zbudowania pełnoprawnego kodu serwisu, który obsługiwać będzie logikę biznesową Waszej aplikacji. Przechodząc jednak do konkretów..

1 ) Factory

Factory to w zasadzie najprostsza wersja serwisów w AngularJS. Tworząc factory, tworzymy de facto obiekt z polami w formie zwykłych zmiennych lub funkcji, który następnie zwracamy poprzez użycie return. Wstrzyknięte do kontrolera factory jest dokładnie takim prostym obiektem. Posłużę się przykładem, który lepiej zilustruje to co mam na myśli.

[wp-js-fiddle url="https://jsfiddle.net/mrzepinski/69YQ8/1/" style="width:100%;height:400px;border:solid #4173A0 1px;"]

Spójrzcie od razu na zakładkę JavaScript. Stworzyłem tam naiwny kontroler, którego jedynym zadaniem jest ustawienie nazwy użytkownika poprzez wywołanie metody setUsername() serwisu User wstrzykniętego jako dependency do kontrolera oraz wywołanie metody sayHello() tegoż serwisu w celu pobrania wiadomości powitalnej dla ustawionej nazwy użytkownika.

Poza kontrolerem zdefiniowany został serwis User typu factory. W ciele tego serwisu mamy trzy zmienne lokalne (_username, _setUsername, _sayHello), które widoczne są tylko w ramach tego serwisu. Następnie zwracany jest nowy obiekt za pomocą polecenia return z dwoma polami (setUsername, sayHello), do których przypisane zostają referencje zmiennych lokalnych (funkcji), odpowiednio _setUsername oraz _sayHello. Równie dobrze jednak, nasz serwis factory mógłby wyglądać tak:

[wp-js-fiddle url="https://jsfiddle.net/mrzepinski/69YQ8/2/" style="width:100%;height:400px;border:solid #4173A0 1px;"]

Ograniczamy się w tym momencie do jednej zmiennej lokalnej w kontrolerze User.

Poprzez wstrzyknięcie factory User do kontrolera, otrzymujemy w nim dostęp do pól zwróconego obiektu. Działanie jest tak proste, że nie zamierzam tego komplikować kolejnymi wyjaśnieniami.

2) Service

Kolejny serwis nazwany został dosłownie Service. W porównaniu do FactoryService tworzony jest na zasadzie użycia słowa new, a następnie zostaje wstrzyknięty do kontrolera. Użycie konstruktora (new) powoduje, że wszystkie widoczne w serwisie zmienne przypisane muszą zostać do słowa this, które widoczne jest po wstrzyknięciu serwisu do kontrolera. Spójrzcie na ten sam przykład, ale z wykorzystaniem Service.

[wp-js-fiddle url="https://jsfiddle.net/mrzepinski/69YQ8/3/" style="width:100%;height:400px;border:solid #4173A0 1px;"]

Widać wyraźnie, że kod po stronie kontrolera pozostał taki sam. Serwis natomiast wygląda nieco inaczej. Nie ma już słowa return i zwracanego w ten sposób obiektu z metodami setUsername oraz sayHello. Zamiast tego, metody te przypisane zostały do słowa kluczowego this, które zwrócone zostanie do kontrolera w ramach wywołania konstruktora przy użyciu new przez $injector.

3) Provider

Provider jest jedynym serwisem, który użyty może być w ramach config naszej aplikacji. Dzięki temu jesteśmy w stanie dokonać ustawień naszego serwisu jeszcze zanim zostanie on wstrzyknięty i użyty w kontrolerze. Struktura provider jest nieco rozdmuchana, ale w gruncie rzeczy przypomina on factory oraz service. Poniżej ciągle ten sam przykład, ale z wykorzystaniem provider.

[wp-js-fiddle url="https://jsfiddle.net/mrzepinski/69YQ8/4/" style="width:100%;height:400px;border:solid #4173A0 1px;"]

Zmian jest sporo. Na samym końcu zobaczyć możecie nową sekcję: config. Służy ona do ustawiania parametrów naszej aplikacji, a w tym wypadku do ustawienia pola naszego serwisu. W samym provider mamy teraz zmienną _username przypisaną do słowa this i tylko takie zmienne widoczne są z poziomu config aplikacji. Następnie do this przypisana zostaje funkcja $get, która z kolei zwraca nam prosty obiekt za pomocą słowa return do kontrolera. Tylko obiekt zwrócony przez funkcję $get naszego providera widoczny jest po tej drugiej stronie (kontrolerze). W kontrolerze ubyło ustawianie nazwy użytkownika, która przeniesiona została do części config. Warto zauważyć, że do ciała funkcji config wstrzykiwany jest UserProvider, a nie User. Zapamiętajcie, że w ten sposób możemy dobrać się do pól przypisanych do this naszego providera. Gdybyśmy nasz provider nazywał się np. myProvider, to do config musielibyśmy wstrzyknąć myProviderProvider. Za każdym razem jest to dodanie słowa Provider do oryginalnej nazwy. Pozostaje również zapamiętać, że tylko serwis provider pozwala nam na ustawienie zmiennych w sekcji config aplikacji.

Krótko podsumowując

Tym oto prostym sposobem udało mi się (mam nadzieję) pokazać zasadnicze różnice pomiędzy różnymi typami serwisów w AngularJS. Zapraszam tym samym do podzielenia się swoimi wnioskami w komentarzach oraz do odchudzania swoich kontrolerów.

AngularJS #7 – HTML na sterydach – wprowadzenie do dyrektyw

AngularJS



AngularJS nie byłby bez dyrektyw tym samym frameworkiem. To właśnie dyrektywy są absolutną przewagą AngularJS. Tak jak w tytule – użycie dyrektyw, to HTML na sterydach. Dzięki nim jesteśmy w stanie nauczyć nasz HTML nowych sztuczek.

Dyrektywa

Czym jest więc wspomniana dyrektywa? W poprzednich wpisach kilka razy wspominałem o dodatkowych elementach, atrybutach, komentarzach HTML oraz klasach CSS, które możemy używać w HTML dzięki wykorzystaniu AngularJS. Te dodatkowe rzeczy, to nic innego jak dyrektywy. AngularJS w fazie kompilacji ($compile) skanuje drzewo DOM naszego dokumentu HTML, a następnie w miejsce wystąpień dyrektyw podpina funkcjonalności na poziomie samego DOM. Owe funkcjonalności mają zastosowanie na poziomie elementu, dla którego zdefiniowaliśmy dyrektywę oraz wszystkich jego dzieci. Dyrektywy pozwalają w ten sposób budować reużywalne komponenty, które pozwalają manipulować drzewem DOM oraz dostarczać mu nowych funkcjonalności.

Przykłady istniejących dyrektyw znajdziecie praktycznie w każdym miejscu. ng-appng-controllerng-bind istnieją w AngularJS. W poprzednich wpisach wykorzystałem chociażby dyrektywę ng-repeat, która pozwoliła mi zbudować dynamiczną listę w oparciu o kolekcję obiektów. Dyrektywy są najczęściej wykorzystywanym elementem AngularJS i stanowią o jego sile i przewadze nad innymi frameworkami JavaScript.

Opcje

Dyrektywy tworzone mogą być na kilka różnych sposobów. Posiadają także szereg parametrów konfiguracyjnych, które odpowiadają za samo funkcjonowanie oraz użycie dyrektyw. Przejdźmy przez większość z nich.

restrict

Jak już wcześniej wspomniałem – dyrektywy możemy wywoływać w HTML na kilka sposobów. Określa to parametr restrict, który przyjmuje wartości:

restrict: 'A'
<div my-directive></div>
restrict: 'C'
<div class="my-directive: expression;"></div>
restrict: 'E'
<my-directive></my-directive>
restrict: 'M'
<!-- directive: my-directive expression -->

Możemy oczywiście podać wszystkie wartości jednocześnie: restrict: ‚EMAC’. Najbardziej popularne i chyba najrozsądniejsze jest wykorzystanie atrybutów oraz elementów HTML jako sposobu wywołań dyrektyw. Najlepiej radzą sobie z tym również przeglądarki.

template

Kolejnym elementem jest atrybut template, który pozwala nam zdefiniować szablon, który wykorzystywać będzie nasza dyrektywa. Dzięki temu możemy bindować dane przekazywane do dyrektywy lub generowane przez kontroler (patrz niżej) samej dyrektywy.

template: '<div>My extra template is here</div>'

templateUrl

Szablony umieszczać możemy również w osobnych plikach. Nierzadko będą one rozbudowane, więc tym łatwiej będzie nam je trzymać w osobnym pliku HTML, gdzie nasze IDE pozwoli nam też podpowiadać składnię, niżeli pisać szablon w postaci łańcucha znaków w JavaScript. Szablony zdefiniowane w ten sposób ładowane są asynchronicznie i cachowane.

templateUrl: 'templates/my-extra-template.html'

controller

Dyrektywy posiadać mogą własne, lokalne kontrolery, gdzie definiować możemy metody operujące na danych. Pozwala to budować logikę dyrektywy, którą następnie możemy używać chociażby w szablonie.

compile

Atrybut dyrektywy, który definiuje jej działanie w czasie kompilacji – jeszcze przed podpięciem samej dyrektywy do drzewa DOM. Faza compile zwraca funkcję link (patrz niżej), pozwalającą na bindowanie logiki dla elementu w drzewie DOM.

link

Finalne bindowanie logiki dyrektywy dla elementu w drzewie DOM. Jest wynikiem fazy kompilacji następującej przed samym linkowaniem.

require

Pozwala nam określić zależności w postaci innych dyrektyw, które wymagane są do działania naszej dyrektywy. Spójrzmy na definicję dyrektywy:

app.directive('myDirective', function () {
    return {
        restrict: 'A',
        require: '^ngModel'
    };
});

oraz przykład jej wywołania:

<div my-directive ng-model="city"></div>

Gdyby zabrakło tutaj wywołania ng-model, to otrzymalibyśmy błąd mówiący nam o tym fakcie.

Zauważyliście zapewne znak ^ przed nazwą ngModel. Informuje on kompilator AngularJS by szukać wywołania dyrektywy ngModel także poza elementem, dla którego zdefiniowano dyrektywę myDirective.

Istnieje również opcja ?, która spowoduje, że w przypadku braku zdefiniowanej zależności nie otrzymamy błędu kompilacji dyrektywy.

scope

Jak możecie się domyślać – jest to lokalny scope w naszej dyrektywie. Wywołując dyrektywę w HTML możemy do niej przekazywać różne parametry.

Spójrzmy na kawałek kodu:

scope: {
    param1: '@',  // parametr przekazywany przez wartosc (one-way binding)
    param2: '=',  // parametr przekazywany przez referencje (two-way binding)
    param3: '&'   // wyrazenie, ktore moze zostac wywolane w ciele dyrektywy
}

Pozwoli nam to zdefiniować dyrektywę w następujący sposób:

<div my-directive param1="{{ onlyValue }}" param2="toWayBindingByReference" param3="functionInvokedInDirective()"></div>

replace

replace: true

Pozwala na zamianę ciała elementu, dla którego definiujemy w całości przez szablon dyrektywy.

transclude

Możemy w ten sposób pobrać zawartość elementu, dla którego definiujemy dyrektywę, a następnie przekazać ją do szablonu dyrektywy.

transclude: true

Wyobraźmy sobie HTML w postaci:

<div my-directive>
    <p>To template please!</p>
</div>

Wykorzystanie opcji transclude w dyrektywie pozwoli nam na pobranie <p>To template please!</p> oraz przekazanie do szablonu w następujący sposób:

app.directive('myDirective', function () {
    return {
        restrict: 'A',
        template: '<div ng-transclude></div>',
        transclude: true
    };
});

Tym samym paragraf p przekazany zostanie do div szablonu dyrektywy.

Pierwsza dyrektywa

Poniżej przykład budowy oraz wykorzystania dyrektywy, która pozwalać będzie na wstawienie w dane miejsce dokumentu HTML reużywalnego komponentu w postaci listy z tytułem.

[wp-js-fiddle url="https://jsfiddle.net/mrzepinski/Ggur3/2/" style="width:100%;height:400px;border:solid #4173A0 1px;"]

Egghead

„First Directive”

[youtube_sc url=”https://www.youtube.com/watch?v=xoIHkM4KpHM”]

„Directive Restrictions”

[youtube_sc url=”https://www.youtube.com/watch?v=AoDh1T_0Obg”]

I na koniec

Mam nadzieję, że starczy mi czasu i determinacji na kolejne wpisy o AngularJS. Na chwilę obecną nie jestem w stanie przewidzieć o czym będzie mowa następnym razem. Przewiduję, że natchnie mnie w pewnym momencie na napisanie bardziej praktycznego wpisu. Sam temat jest z pewnością wart uwagi.

AngularJS #6 – Karma – pierwszy test

AngularJS



Kolejnym wpisem miało być rozszerzenie o filtrach, ale uznałem, że jest tam zbyt mało materiału, by robić z tego osobny element całego cyklu o AngularJS. Sam temat filtrów jest poza tym na tyle prosty, że nie warto poświęcać na niego zbyt wiele czasu. Znacznie bardziej należałoby się skupić na elemencie testowania aplikacji i dlatego też chciałbym Was wprowadzić w świat Jasmine i Karma.

Karma

Karma jest narzędziem, które pozwala uruchamiać testy kodu JavaScript pod różnymi przeglądarkami, a w zasadzie ich emulowanym środowiskiem. Karma nie jest pełnoprawnym frameworkiem do testowania, a jedynie rusztowaniem, na którego bazie zbudować możemy pełny stack testujący JavaScript. Karma zbudowana jest w oparciu o serwer Node.js oraz technologię Socket.io. Pozwala na wykonywanie testów w różnych środowiskach pracy (deweloperskim, testowym, produkcyjnym itd.), a ponadto wykonuje wszystkie testy w separacji od kodu właściwego. Pozwala także na wykonywanie testów pod różnymi przeglądarkami jednocześnie. W połączeniu z Istanbul, który to potrafi wygenerować i pokazać nam pokrycie naszego kodu testami, Karma stanowi pełnoprawne środowisko testowe, które ułatwia nam, a w zasadzie pozwala na szybkie i bezproblemowe testowanie kodu JavaScript.

Pełny opis instalacji oraz konfiguracji znajdziecie na oficjalnej stronie projektu. Znajduje się tam także film, który bardzo trafnie prezentuje możliwości jakie daje nam Karma.

Jasmine

Spośród kilku frameworków do testowania kodu JavaScript, miałem do tej pory okazję wykorzystywać właśnie Jasmine. Jasmine, to behavior-driven development framework, który pozwala na pisanie naszych testów w sposób bardzo opisowy, a ponadto świetnie integruje się ze środowiskiem Karma. Nie jest on zależny od żadnego frameworka JavaScript i nie wymaga także drzewa DOM do działania. Jasmine pozwala nie tylko na pisanie testów jednostkowych, ale również tak zwanych testów e2e. Kilkadziesiąt przykładów znajdziecie na stronie oficjalnej i w zasadzie nie pozostaje nic innego jak zacząć pisać testy!

Główny konkurent JasmineMocha jest nieco bardziej rozbudowany i dojrzały, ale „próg wejścia” jest także nieco większy. Jest jeszcze QUnit, który używany jest w projektach jQuery, jQuery UI oraz jQuery Mobile, ale nie cieszy się taką popularnością community jak pozostałe dwa.

Let’s write some code!

Nie będę tutaj opisywał jak skonfigurować sobie Karmę oraz Jasmine by działały razem, bo jest to świetnie opisane oraz pokazane na stronach oficjalnych obu narzędzi.

Przypuśćmy zatem, że chcemy przetestować działanie metody w naszym kontrolerze. Niech będzie to przykład metody, która po wykonaniu funkcji sayHello() zmienia wartość parametru name w $scope kontrolera APP.ApplicationCtrl na ‚World’.

'use strict';

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

APP.ApplicationCtrl = function ($scope) {

    $scope.name = '';

    $scope.sayHello = function () {
        $scope.name = 'World';
    };

};

Klasa testowa wyglądać będzie następująco:

'use strict';

describe('APP.ApplicationCtrl', function () {

    var scope;

    // mockujemy nasz modul APP
    beforeEach(angular.mock.module('APP'));
    // mockujemy kontroler ApplicationCtrl
    // i przekazujemy do niego mock $scope utworzony na podstawie $rootScope
    beforeEach(angular.mock.inject(function ($rootScope, $controller) {
        // tworzymy pusty scope
        scope = $rootScope.$new();
        // tworzymy instancje kontrolera i przekazujemy do niego pusty $scope
        $controller('ApplicationCtrl', { $scope: scope });
    }));

    // .. i testujemy
    it('should set new value for $scope.name', function () {
        // having
        scope.name = '';

        // when
        expect(scope.name).toBe('');
        scope.sayHello();

        // then
        expect(scope.name).toBe('World');
    });

});

Wykonanie powyższego testu zwróci nam oczywiście pozytywny wynik. Tym oto prostym sposobem przetestowaliśmy banalną metodę naszego kontrolera.

Jasmine zawiera szereg predefiniowanych metod oraz asercji do testowania naszego kodu. Jesteśmy w stanie na przykład sprawdzić, czy dana metoda wykonała się w cyklu wykonywania innej z metod w naszym kontrolerze, a na dodatek sprawdzić, czy wykonała się określoną ilość razy. Jesteśmy w stanie porównywać całe kolekcje pod względem zawartości, czy emulować zapytania HTTP. Zachęcam do przyjrzenia się dokumentacji i własnych prób.

I na koniec jak zawsze http://egghead.io i prezentacja Karmy oraz Jasmine na równie prostym przykładzie.

[youtube_sc url=”https://www.youtube.com/watch?v=wFYID8eYQLs”]