Dart – v DOMe

Dokončíme implementáciu pexesa v Darte začatú v minulom dieli. Spoznáme prácu s DOM v Darte, preskúmame štandardnú HTML knižnicu dodávanú s Dartom. Poďme na to!
Seriál: Úvod do Dartu (9 dílů)
- Dart – Čo? Prečo? 2. 8. 2013
- Dart – Úvod do jazyka 23. 8. 2013
- Dart – Ponorme sa hlbšie 6. 9. 2013
- Dart – v DOMe 19. 9. 2013
- Dart – Futures 4. 10. 2013
- Dart – Streams 17. 10. 2013
- Dart – Používame JavaScript 1. 11. 2013
- Dart Typesystem 19. 11. 2013
- Dart – Neznesiteľná ľahkosť asynchrónneho bytia 2. 12. 2013
Nálepky:
V minulom dieli sme vytvorili funkčný model reprezentujúci hru pexeso. Ostáva pridať interakciu s hráčmi, čaká nás práca s HTML, CSS a DOM elementami.
Knižnica dart:html
je štandardná knižnica na prácu s prehliadačom. Ak túto knižnicu vo svojom projekte používate, kód vyžaduje spustenie v prehliadači a nebude fungovať bez balíčka browser
v závislostiach projektu.
Základným stavebným prvkom DOM je v Darte trieda Element
– predok všetkých HTML aj SVG elementov. Hierarchická štruktúra elementov je zaznamenaná v odkaze na rodiča final Element Element.parent
a v zozname detí List<Element> Element.children
. Ak chceme pridať alebo odobrať potomka elementu, stačí modifikovať Element.children
. Pozor, nakoľko Element.parent
je final
, nemôžeme prvok na stránke premiestniť prepísaním jeho hodnoty. Toto obmedzenie dáva zmysel – ak by bolo možné modifikovať Element.parent
, nebolo by implicitne jasné, kam sa element pridá, ak má rodič viacero detí.
Pre najpoužívanejšie elementy sú dostupné samostatné triedy, takže nájdeme napríklad AnchorElement
, ButtonElement
, DivElement
, SpanElement
a 58 ďalších. Ostatné elementy, pre ktoré neexistujú priamo triedy, je možné vytvoriť konštruktorom Element.tag(String tag)
, akceptujúcim ako parameter názov html tagu, z ktorého element vytvárame.
Ďalšou príjemnou vlastnosťou je jednoduchá práca s CSS triedami. Triedy elementu sú dostupné cez CssClassSet Element.classes
. Tento okrem implementácie rozhrania Set
(typ reprezentujúci množinu v Darte) poskytuje ďalšie šikovné funkcie, ktorá sa zídu pri práci. Príkladom je funkcia toggle(String value, [bool shouldAdd])
, ktorá pridá triedu value
, ak sa ešte medzi triedami nenachádza, a naopak ju odstráni v prípade, že sa už medzi triedami nachádza. Funkcia tiež akceptuje nepovinný parameter shouldAdd
, ktorým je možné vynútiť pridanie triedy shouldAdd = true
, alebo jej odstránenie shouldAdd = false
.
Nakoniec spomeňme funkciu Element.queryAll(String selectors)
. Táto je ekvivalent jQuery $(selector)
– vráti zoznam potomkov elementu, ktorý vyhovujú zadaným CSS selectors
. Pre pohodlnosť existuje ešte Element.query(String selectors)
, ktorá vráti len prvý vyhovujúci element.
V knižnici sa okrem toho nachádzajú globálne objekty window
, document
a globálna verzia funkcií query
, queryAll
(ekvivalent window.query
a window.queryAll
).
Pexesová kartička
Úvod máme za sebou, takže pokračujme v pexese. Chceme vytvoriť triedu, ktorá bude zobrazovať jednu kartičku, reagovať na kliknutia a otáčať sa. Otvorme súbor web/main.dart
a zapíšme do neho takúto štruktúru:
/**
* Memory game.
*/
import 'dart:html';
import 'dart:async';
import 'package:pexeso/pexeso_model.dart';
/**
* Pexeso card representation.
*/
class Card {
final HtmlElement element;
final id;
bool turned;
/**
* Create the card around the DOM of [element] with an identificator [id].
*/
Card(this.element, this.turned, this.id);
/**
* Creates the card with DOM given the [backImage] and [frontImage] and
* appends it to [container].
*/
factory Card.withDom(Element container, String backImage, String frontImage, id) {
var back = new ImageElement(src: backImage)
..className = 'back';
var front = new ImageElement(src: frontImage)
..className = 'front';
var div = new DivElement()
..className = 'pexeso-card'
..children.addAll([back, front]);
container.children.add(div);
return new Card(div, false, id);
}
}
O kartičke evidujeme tri informácie: DOM element
, ktorý jej zodpovedá, či je otočená (turned
) a identifikátor (id
). V konštruktore Card.withDom
pripravíme kartičku kompletne s DOM štruktúrou.
<div class='pexeso-card'>
<img src="[backImage]" class='back' />
<img src="[frontImage]" class='front' />
</div>
A CSS pravidlá zabezpečia, že naraz vidíme len jeden obrázok.
.pexeso-card.turned .back {
display: none;
}
.pexeso-card.turned .front {
display: block;
}
V tomto momente narážame na náš prvý problém – chceli by sme, aby hodnota položky turned
zodpovedala prítomnosti/neprítomnosti triedy turned
v <div class='pexeso-card'>
. Bolo by príjemné, keby sa pri priradení true
do premennej turned
automaticky trieda pridala a pri priradení false
ubrala.
Máme šťastie, v Darte na tento účel existujú gettery a settery. Getter je špeciálna funkcia, ktorej výstupná hodnota sa použije pri snahe čítať hodnotu premennej triedy, ktorá sa v triede nenachádza. Setter je naopak funkcia, ktorá sa zavolá pri snahe do takejto premennej priradiť. Navonok, z hľadiska používateľa, simulujú gettery a settery správanie obyčajných premenné, no my do nich vieme ukryť dodatočnú funkcionalitu.
Premenujme premennú turned
na _turned
, takže nebude viditeľná mimo knižnice a doplňme nasledovný kód:
class Card {
...
bool _turned;
bool get turned => _turned;
void set turned (bool value) {
_turned = value;
element.classes.toggle('turned', value);
}
/**
* Create the card around the DOM of [element] with an identificator [id].
*/
Card(this.element, turned, this.id) {
this.turned = turned;
}
...
}
Pridali sme getter a setter na premennú turned (=> _turned
je ekvivalentný zápis pre () {return _turned;}
), ktorý pri zápise okrem zaznamenania hodnoty pridá/zmaže elementu triedu turned
podľa priradenej hodnoty. Museli sme tiež kozmeticky upraviť konštruktor, lebo turned
už nie je premenná.
Bolo by príjemné, keby sa kartička sama otočila po kliknutí. To vieme zabezpečiť pridaním nasledovného kódu:
class Card {
...
/**
* Create the card around the DOM of [element] with an identificator [id].
*/
Card(this.element, bool turned, this.id) {
this.turned = turned;
this.element.onClick.listen((event) {
this.turned = true;
});
}
...
}
Zavesili sme listener na onClick
udalosť elementu, ktorý pri kliknutí otočí kartičku. Pre zaujímavosť, onClick
je objekt typu Stream
, ktorý reprezentuje nekonečný prúd nepravidelne prichádzajúcich dát. Stream si bližšie rozoberieme v ďalšom dieli, teraz nám stačí vedieť, že sa zvyknú používať aj na pracovanie s eventmi.
Posledné, čo našej kartičke chýba, je schopnosť vykričať do sveta „Bola som otočená!“. Pridáme vlastný Stream
udalostí onTurn
. Aby sme do Stream
vedeli pridávať udalosti, potrebujeme StreamController
. Pozrime sa na to!
class Card {
...
final StreamController<Card> _onTurnController;
Stream<Card> get onTurn => _onTurnController.stream;
/**
* Create the card around the DOM of [element] with an identificator [id].
*/
Card(this.element, bool turned, this.id)
: _onTurnController = new StreamController() {
this.turned = turned;
this.element.onClick.listen((event) {
if (!this.turned) {
this.turned = true;
_onTurnController.add(this);
}
});
}
...
}
Pridali sme StreamController _onTurnController
a getter onTurn
, ktorý vracia k nemu príslušný Stream. Do onClick
listeneru sme pridali kontrolu, či už karta nebola otočená a pri otočení karty pridáme kartu do onTurn
Stream
u. Ktokoľvek počúvajúci na onTurn
takto vie zaregistrovať otočenie karty.
Dokončenie
A teraz už treba len dokončiť pár drobností. Pridáme tri globálne premenné:
final List turnedCards = [];
Game game;
HtmlElement scoreBoard;
reprezentujúce práve otočené kartičky, inštanciu hry a skóre.
Funkciu na update skóre:
void updateScore() {
scoreBoard.text = "Player1: ${game.score[0]} Player2: ${game.score[1]}";
}
Ešte treba pridať funkcionalitu, ktorá zmanažuje otáčanie kartičiek.
void onTurn(Card card) {
if (turnedCards.length >= 2) {
for (var c in turnedCards) {
c.turned = false;
}
turnedCards.clear();
}
turnedCards.add(card);
if (turnedCards.length < 2) {
return;
}
if (game.turnCards(turnedCards[0].id, turnedCards[1].id)) {
turnedCards.clear();
}
updateScore();
}
V momente, keď otočíme tretiu kartičku, predošlé dve sa zakryjú. Ak sme práve otočili druhú kartičku, zavolá sa game.turnCards
a na základe výsledku buď ponecháme kartičky otočené (našiel sa pár), alebo ich po otočení ďalšej skryjeme. Po vykonaní každého ťahu ešte aktualizujeme skóre.
Ostáva už len pripraviť hrací plán. Pridáme index.html s nasledovným obsahom:
<!DOCTYPE html>
<html>
<head>
<title>Pexeso</title>
<link rel="stylesheet" type="text/css" href="stylesheet.css" />
</head>
<body>
<div id="score"></div>
<div id="board"></div>
<script type="application/dart" src="main.dart"></script>
<!-- for this next line to work, your pubspec.yaml file must have a dependency on 'browser' -->
<script src="packages/browser/dart.js"></script>
</body>
</html>
a funkciu na prípravu hry
void prepareGame() {
game = new Game.withCards(2, 32);
scoreBoard = query('#score');
var container = query('#board');
for (var i = 0; i < game.cards.length; i++) {
new Card.withDom(container, "cards/back.jpg",
"cards/${game.cards[i]}.jpg", i)
..onTurn.listen(onTurn);
}
updateScore();
}
a nakoniec vstupný bod celého programu.
void main() {
prepareGame();
}
Pexeso je hotové!
Počas programovania sme samozrejme poctivo písali unittesty, pre zdĺhavosť ich tu však neuvádzam. V kompletnom zdrojovom kóde na githube nájdete testy, obrázky aj štýly.
Nabudúce sa porozprávame o asynchrónnom programovaní v podaní Stream
s a Future
s. Máte sa na čo tešiť, je to fakt cool!