Začínáme s AngularJS

node.js logo

Dnešní díl bude věnován populárnímu MVC frameworku AngularJS. Představíme si výhody jeho používání, základní pojmy, které se k němu vážou, a samozřejmě si také vytvoříme první aplikaci.

Seriál: Node.js - s JavaScriptem na serveru (13 dílů)

  1. JavaScript na serveru: Začínáme s Node.js 23.11.2010
  2. JavaScript na serveru: Patří budoucnost Node.js? 21.9.2012
  3. JavaScript na serveru: Architektura a první Hello World 5.10.2012
  4. JavaScript na serveru: moduly a npm 12.10.2012
  5. JavaScript na serveru: začínáme programovat e-shop 19.10.2012
  6. JavaScript na serveru: MongoDB, Mongoose a AngularJS 26.10.2012
  7. JavaScript na serveru: Testování a kontinuální integrace 2.11.2012
  8. JavaScript na serveru: REST API 9.11.2012
  9. JavaScript na serveru: implementace REST API 16.11.2012
  10. JavaScript na serveru: nástroje a dokumentace 23.11.2012
  11. Začínáme s AngularJS 30.11.2012
  12. AngularJS direktivy a testování 7.12.2012
  13. JavaScript na serveru: CoffeeScript a šablonovací systémy 14.12.2012

Podobně jako v předchozích dílech si i dnes můžete aplikaci stáhnout přes git příkazem git checkout -f dil11, popř. projít zdrojové kódy na Githubu (vše je ve složce public). Kromě toho si můžete celou aplikaci prohlédnout v demoverzi.

Ačkoliv je seriál věnován převážně Node.js, nelze se vyhnout technologiím a nástrojům, které patří do jeho ekosystému. Velmi často se v Node.js vytvářejí aplikace, které používají hodně klientského JavaScriptu a komunikují se serverem přes API. Ostatně to je i případ vyvíjené aplikace v rámci tohoto seriálu. Pro tyto případy je nutností použít některý z frameworků, který programátora odstíní od rutinních úkolů a zjednoduší mu práci. A jedním z těchto frameworků je i AngularJS.

AngularJS samozřejmě není jediný framework svého druhu. Přehled všech významných MVC frameworků přináší projekt TodoMVC, kde je v každém frameworku vytvořena naprosto stejná aplikace. Můžete si prohlédnout kódy aplikace ve všech frameworcích a rozhodnout se pro ten, který se vám líbí nejvíce. I kdybyste nakonec preferovali jiný framework, je dobré poznat blíže AngularJS, protože přináší mnoho inovativních myšlenek.

AngularJS lze výborně kombinovat s populárními JavaScriptovými knihovnami jako jQuery nebo Google Closure. Příkladem jsou projekty AngularUI nebo NGX library od LMC.

Přednosti AngularJS a základní pojmy

AngularJS obsahuje řadu zajímavých myšlenek, přičemž mezi nejužitečnější koncepty frameworku se obvykle uvádí Two Way Data-Binding, implementace Dependency Injection, testovatelnost, direktivy a znovupoužitelnost komponent. V dalším textu si vysvětlíme, co si pod danými pojmy představit.

Two Way Data-Binding

Jestliže se zeptáte programátorů, kteří pracují s Angularem, jakou vlastnost považují vůbec za nejužitečnější, nejčastěji vám odpoví, že koncept Two Way Data-Binding, který řeší synchronizaci stavů mezi modelem a view. Do češtiny bychom mohli pojem přeložit jako “dvoucestná synchronizace dat”.

Oč jde? Představme si jednoduchou aplikaci, ve které máme jedno textové pole a pod ním chceme vypsat, co uživatel do pole napsal. Živá ukázka je k dispozici v dokumentaci frameworku, vše vypadá takto:

Ve chvíli, kdy uživatel napíše nějaký řetězec do textového pole, se tento text přenese do modelu (to je ta první cesta) a následně se z modelu přepíše do ostatních částí view (to je ta druhá cesta). To lze samozřejmě řešit i v jiných frameworcích, ale v případě AngularJS je tohle výborně zautomatizováno. Zdrojový kód ukázky vypadá takto:

<p> Enter name: <input type="text" ng-model="name"><br>
Hello <span ng-bind="name"></span>!</p>

Textovému inputu přidáváme direktivu ng-model, která říká, že co uživatel napíše do pole, se přesune do modelu name. Druhý řádek obsahuje element span s direktivou ng-bind, která zase říká, že se do span mají vypsat data z modelu name. To je vše. Nic víc není potřeba! 

Jedná se o deklarativní způsob implementace. To jest, programátor nemusí imperativně popisovat, jak data synchronizovat. Namísto toho pouze deklaruje „vztah“ a Angular zajistí synchronizaci.

Velmi dobrou ukázkou tohoto principu je spreadsheet (např. Excel), kde nadefinujete pouze vztahy, rovnice. Např. A2 = B1 + A1 a více se nestaráte. V imperativní prostředí by bylo potřeba popsat: pokud se změní B1 nebo A1, spočítej B1 + A1 a nastav výslednou hodnotu do A2.

Celý proces vyjádřen graficky vypadá takto:

Na obrázku jsou zmíněny tři základní pojmy:

  • template (šablona), která říká, jak aplikace má vypadat, to je to HTML v ukázce;
  • model je objekt reprezentující entitu (vč. polí i primitivních typů);
  • view (pohled) vznikne, když se do šablony vloží data z modelu (transformace na DOM);

Co kdybychom dvoucestnou synchronizaci řešili ručně? Vytvořili bychom handler pro událost onChange u textového pole pro funkci, která by změnu přepsala zpět do šablony, tedy v jQuery by to bylo takto:

<p> Enter name: <input type="text" id="name"><br>
Hello <span id="name-value"></span>!</p>
//propsání změny
$('#name').change(function(){
  $('#name-value').val($(this).val());
});

Oboustranná synchronizace je u větších javascriptových aplikací velmi častá a vidíte, že i pro velmi primitivní příklad je potřeba v jQuery napsat hodně kódu. Proto se také uvádí, že aplikace v AngularJS může snížit množství kódu i na 10 % původního množství kódu. Tak to bylo v případě několika interních aplikací v Google (např. v aplikaci Feedback se díky Angularu snížil počet řádků z původních 16 000 napsaných v Javě v GWT na 1 000 řádků v JavaScriptu v Angularu).

Obsah textového pole můžeme chtít vypsat na více místech a pro každou změnu bychom museli neustále měnit obsah funkce, která data propisuje do pohledu. Dokážete představit, že udržovat takto napsanou velkou aplikaci je nesmírně náročné.

V tomhle jednoduchém případě je pouze jeden způsob, jak změnit model (uživatel změní hodnotu v inputu). Takže je potřeba sledovat tuto změnu a aktualizovat view. V případě komplexní aplikace nastanou další způsoby, jak model změnit (například dostaneme aktualizovaná data ze serveru). V tu chvíli je potřeba myslet na to, kde všude je tento model použit a je potřeba jej aktualizovat.

V případě Angularu nás to nezajímá. Pouze deklarujeme vztah a víc se o synchronizaci nestaráme. Je jedno, jak se model změnil. Výsledkem je kód, kde je logika nezávislá na prezentaci a tím pádem je snadnější ho testovat.

Drobná, i když vítaná, výhoda je také v tom, že není potřeba neustále pro všechno psát nová ID, šablony jsou tak menší a přehlednější.

Dependency Injection (DI)

O DI na Zdrojáku vycházel před rokem seriál, takže pokud náhodou nevíte, co si pod tímto pojmem představit, projděte si seriál Jak na Dependency Injection. Kromě toho je dostupná přednáška o DI v Angularu od Vojty Jíny (povídání o DI začíná na 0:37:34).

Jedná se o návrhový vzor, který řeší závislost mezi jednotlivými komponentami programu.

AngularJS obsahuje zabudovaný subsystém, které řeší DI napříč celou vyvíjenou aplikací. Umožňuje vždy přesně specifikovat, které jiné komponenty jsou uvnitř konkrétní komponenty používané.

Podívejme se na to, jak vypadají třeba deklarace controlleru pro zobrazení detailu stránky v naší aplikaci:

function PagesShowCtrl($scope, $routeParams, $location, page) {
  //obsah controlleru
}

Controlleru se při vytvoření předávají 4 parametry, přes které se předávají závislosti. Uvnitř controlleru se nepracuje s ničím jiným než právě s těmito závislostmi. Díky tomu se všechny části aplikace výborně testují.

Jak již bylo řečeno, AngularJS obsahuje subsystém, který řeší sám předávání závislostí. Jakmile vytváří novou instanci controlleru, zkontroluje si jeho závislosti v deklaraci a všechny předá jako parametry.

Tyto závislosti se nazývají services (služby) a více informací o nich získáte v dokumentaci projektu.

Služby jsou buď interní, které jsou dodávány přímo s frameworkem, nebo si je můžeme vytvořit sami. Interní služby se prefixují znakem dolaru ($). Ve výše uvedeném případě používáme tři služby: 

  • $scope řeší kontext dat a je důležitý pro koncept data bindingu, 
  • $routeParams pracuje s parametry v URL,
  • $location umožňuje práci s URL

Aplikace pracuje pouze se $scope, zapisuje/čte a binding se stará o synchronizaci mezi $scope a prezentací (DOM). Díky tomu je logika nezávislá na prezentaci a snadno se testuje.

Jako poslední parametr je v ukázce předána služba page, kterou jsme si vytvořili sami a která slouží k dotazování na REST API stránek naší aplikace.

Testovatelnost

Jedna z oblastí, na kterou je framework zaměřen, je testovatelnost kódu. Přispívá k tomu implementace DI (viz výše), ale také nástroje pro testování, které díky frameworku vznikly.

Chcete-li začít stavět novou aplikaci, je výhodné použít jako výchozí šablonu angular-seed, kterou si můžete na Githubu “forknout” a upravit pro vlastní potřeby. V projektu najdete předpřipravené prostředí pro jednotkové a end2end testy.

Díky AngularJS vznikl v Node.js projekt Testacular, který umožňuje automatizovaně spouštět testy nad různými prohlížeči. Jedná se o alternativu k JsTestDriver, o kterém jsme psali na Zdrojáku nedávno.

Díky Angularu také vznikl projekt jasmine-node, který přenesl Jasmine, jeden z nejpoužívanějších javascriptových testovacích frameworků, do prostředí Node.js.

Pro end2end testy v AngularJS existuje také scenario runner, který funguje podobně jako testování se Seleniem. V těchto testech si píšete scénář, jak má uživatel procházet aplikací, kam má třeba kliknout, napsat text a všechny reakce aplikace testuje, zda vrací správné výsledky. I když to nemusí být na první pohled zřejmé, tyto testy se píší velice rychle a lze jimi pokrýt testování důležitých častí aplikace během pár minut.

Výstup z test runneru pak může vypadat takto:

Kromě toho všeho je na stránkách projektu dostupný špičkový tutoriál, ve kterém se uživatel během chvíle naučí základy práce s Angularem a u každé ukázky je rozebráno, jak danou část testovat. Jednotkové testováníend2end testy jsou podrobně popsány v dokumentaci.

Klientská část naší aplikace bude samozřejmě také používat jednotkové i end2end testy. Blíže se tématu a nástroji Testacular budeme věnovat v příštím díle.

Direktivy

Direktivy představují způsob, jak naučit HTML novým trikům. Přes direktivy můžeme např. říct, že při kliknutí na nějaký textový element se text promění v textové pole a po kliknutí mimo tento element se nad změněnými daty provede nějaká operace (třeba se odešlou na server).

V takovém případě bychom vytvořili přes direktivy nový element (např. s názvem inlineEdit), který by mohl v kódu vypadat takto:

<inlineEdit update='send()'>{{text}}</inlineEdit>

A k tomu v controlleru přidáme akci send():

function MyCtrl() {
  this.text = 'lorem ipsum set dolorem...';
  this.send = function(text) {
    // … neco provede
  }
}

Při načtení pohledu se proměnná {{text}} změní za text “lorem ipsum set dolorem…”. Když uživatel na element klikne, promění se v textové pole. Po dokončení editace se zavolá akce send() controlleru MyCtrl.

Tuto direktivu jsme si vytvořili sami a její kód zde neuvádím. Je to pouze ukázka praktického příkladu, protože inline editace můžeme používat napříč celou aplikací a pokud si direktivu jednou vytvoříme, vždy pak již stačí pouze vložit nový element inlineEdit a implementovat v controlleru akci, která bude zavolána po dokončení editace. Direktivu zmiňuji také proto, protože si její implementaci v průběhu seriálu opravdu vytvoříme a budeme používat v mnoha částech administrace.

AngularJS má opět celou řadu předpřipravených direktiv, jejich seznam s ukázkami a testy je dostupný v API dokumentaci.

Implementace

Nyní už víme, jak AngularJS funguje, můžeme se podívat, jak kód naší aplikace pro obsluhu stránek vypadá.

Bootstrap

Nejjednodušší příklad může vypadat např. takto:

<!doctype html>
<html lang="en" ng-app="zdrojak">
<head>
  <meta charset="utf-8">
  <title>Zdrojak Eshop</title>
</head>
<body>
  <div ng-view></div>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.2/angular.min.js"></script>
  <!-- další js soubory... -->
</body>
</html>

Takto vypadá základní šablona projektu.

Všimněte si direktivy ng-app na druhém řádku. Ta říká, že budeme chtít iniciovat projekt v AngularJS a načte modul s názvem „zdrojak“ (viz část routování).

Pak je zde ještě direktiva ng-view, která načítá šablonu pro danou routu (rozebereme dále).

Šablony

Šablony jsou klasické HTML stránky. Podívejme se, jak vypadá např. formulář pro vložení nové stránky:

<h1>Nová stránka</h1>
<form class="well" ng-submit="create()">

  <fieldset>
    <label class="control-label" for="title">Nadpis:</label>
    <input ng-model="page.title" class="input-xxlarge" required>

    <label class="control-label" for="content">Obsah:</label>
    <textarea ng-model="page.content" cols="300" rows="10" required></textarea>

  </fieldset>
  <div class="form-actions">
      <input type="submit" value="Vytvořit" class="btn btn-primary">
  </div>
</form>

Zde používáme direktivu ng-submit obsahující název akce, která se zavolá při odeslání formuláře. A také direktivu ng-model, se kterou jsme se seznámili již dříve.

Zajímavá je také šablona pro výpis všech stránek v databázi:

<ul>
  <li ng-repeat="page in pages"><a ng-href="/pages/{{page.url}}">{{page.title}}</a></li>
</ul>

Přes API získáme pole, ve kterém je každý prvek reprezentován jedním objektem stránky. Direktiva ng-repeat pole projde a pro každý objekt page vypíše jeden element li s titulkem stránky.

Routování

Aby framework věděl, jakou šablonu má pro jakou URL načíst, použije se služba $route poskytnutá přes $routeProvider. Nastavení pravidel pro šablony a URL může vypadat takto:

angular.module('zdrojak', [‘zdrojakServices’])
  .config(function($routeProvider) {
    $routeProvider.when('/', {templateUrl: '/index/index.html', controller: IndexIndexCtrl});
    $routeProvider.when('/pages', {templateUrl: '/pages/index.html', controller: PagesIndexCtrl});
    $routeProvider.when('/pages/new', {templateUrl: '/pages/new.html',controller: PagesNewCtrl});
    $routeProvider.otherwise({redirectTo: '/'});
  })
  .config(function($locationProvider) {
    $locationProvider.html5Mode(true)
  }
);

Vytváříme zde modul zdrojak, který se načte při inicializaci projektu (vzpomeňte na hodnotu v direktivě ng-app). Říkáme zde, že je tento modul závislý na službě $routeProvider, která se přidá jako parametr.

Dále pro každou routu přes metodu when() specifikujeme:

  • URL, pro které pravidlo platí
  • šablonu, která se má načíst 
  • controller, který se má načíst a zavolat

Pokud pravidlům neodpovídá žádná URL, použije se podmínka definovaná v metodě otherwise(), v našem případě dojde k přesměrování na úvodní stránku.

V další části chceme pracovat se službou $location, ke které se dostaneme přes $locationProvider a říkáme zde, že chceme používat HTML5 mode pro URL, který umožní dynamicky měnit URL přes HTML5 History API (jinak by se používala verze, kdy se URL přidávají za znak #).

Controllery

V dřívější části jsme si již controllery ukázali, pojďme se podívat, jak může vypadat nejjednodušší implementace controlleru pro vkládání.

function PagesNewCtrl($scope, $location, page) {
  $scope.page = {};
  $scope.create = function() {
    page.create($scope.page, function(){
      $location.path('/pages');
    });
  }
}

V controlleru používáme službu $scope, která odkazuje na aktuálně používaný kontext aplikace (mohli bychom místo toho používat $this). Dále vytváříme akci create(), která bude zavolána po odeslání formuláře (ukázali jsme si ho výše) K dispozici budeme mít objekt $scope.page a jeho položky content a title, které přes metodu create() naší servisní třídy page odešleme na server. Jakmile bude stránka vložena, použijeme službu $location, přes kterou uživatele přesměrujeme na novou adresu (služba neprovede HTTP přesměrování, pouze načte novou šablonu, změní URL a zavolá controller pro příslušnou routu).

V akci create() neřešíme stav, kdy na serveru dojde k nějaké chybě. Také v aplikaci zatím neřešíme podporu pro obfuskaci (parametry metod se při ní změní), i na to má AngularJS nástroje. Vše doplníme později.

Services

Nakonec vytvoříme naší službu page, která bude komunikovat s API:

angular.module('zdrojakServices', ['ngResource'])
  .factory('page', function($resource){
    return $resource('/api/v1/pages/:page', {}, {
      index: {method:'GET', isArray:true},
      show: {method:'GET'},
      create: {method:'POST'},
      update: {method:'PUT'},
      remove: {method:'DELETE'}
  });
});

Vytváříme zde službu page, který obsahuje závislost na službu $resource. Ta slouží pro komunikaci se serverem a my nad ní vytváříme pouze obal pro příjemnější a jednodušší práci v controllerech. Říkáme dále, že základní URL pro stránky je /api/v1/pages a pokud předáme službě objekt s vlastností page (viz implementace controlleru PagesDeleteCtrl()), přidá se jeho hodnota k adrese /api/v1/pages (takže např. pro mazání stránky s ID 5 bude URL /api/v1/pages/5).

Dále předáváme objekt, ve kterém definujeme rozhraní pro komunikaci se stránkami. Vytváříme zde 5 metod, které jsou pojmenovány stejně jako akce v controlleru PageController na straně serveru. U metody index navíc říkáme, že očekáváme vrácení pole všech stránek.

AngularJS je rozsáhlý framework, který toho nabízí ještě mnohem více. Pokud vám není některá zmíněná část frameworku zcela jasná, netrapte se tím. V dalších dílech seriálu se ke všemu ještě několikrát vrátíme.

Co dále

V příštím díle uzavřeme část seriálu, ve kterém jsme vytvářeli prostředí pro vývoj našeho e-shopu. Podíváme se mimo jiné na testování a nástroj Testacular.

Na tvorbě tohoto článku se svými připomínkami podíleli také Vojta Jína (github) a Pavel Lang (skolajs.cz). Díky!

Jakub pracoval na několika zajímavých projektech, za nejvýznamnější považuje vytvoření e-commerce řešení Shopio.cz. Poslední rok se plně věnuje Node.js, frameworku AngularJS a NoSQL databázím.

Komentáře: 7

Přehled komentářů

Twista angular :)
Jakub Mrozek Re: angular :)
koudejak Metody Services
Dominion Angular je fajn
vidya awesome
Ugo super fw
Dutohlavek Chybný link
Zdroj: http://www.zdrojak.cz/?p=3747