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ů)

  1. Dart – Čo? Prečo? 2.8.2013
  2. Dart – Úvod do jazyka 23.8.2013
  3. Dart – Ponorme sa hlbšie 6.9.2013
  4. Dart – v DOMe 19.9.2013
  5. Dart – Futures 4.10.2013
  6. Dart ­– Streams 17.10.2013
  7. Dart – Používame JavaScript 1.11.2013
  8. Dart Typesystem 19.11.2013
  9. Dart – Neznesiteľná ľahkosť asynchrónneho bytia 2.12.2013

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í.

Hierarchia elementov

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 Streamu. 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í Streams a Futures. Máte sa na čo tešiť, je to fakt cool!

S kamošmi som založil VacuumLabs, špecializujeme sa na webové aplikácie. Momentálne frčíme na Pythone a pokukujeme po Darte. Nikdy neodmietam pozvanie na dobré české pivo.

Zatím nebyl přidán žádný komentář, buďte první!

Přidat komentář
Zdroj: https://www.zdrojak.cz/?p=9786