Blog webdeveloperski Patryk yarpo Jar

Drzewo w dijit

Autor wiadomości Lipiec 21, 2011

Widget dijit.Tree służy do wyświetlania odpowiedniego drzewka. Znaleźć można naprawdę wiele gotowych skryptów realizujących takie zadanie. Ja jednak chciałbym w tym i w kilku kolejnych wpisach pokazać, co dobrego drzemie w rozwiązaniu proponowanym w Dojo Toolkit (jeśli nie używałeś - polecam quick start).

Przykładowe drzewo:

Podział na warstwy

Dojo jest rozwiązaniem nad wyraz dojrzałym. Stąd też należy raczej oczekiwać pewnego podziału na warstwy. Dla osoby, która pierwszy raz ma styczność z Dojo może wydawać się to trudne (co nie jest do końca prawdą). Trzeba przyznać, że warto jest douczyć się trochę, by wykorzystać pełnię możliwości tego treeview.

Aby stworzyć drzewo w Dojo wymagane są trzy warstwy:

  1. data store
  2. model
  3. widok

Data store jest pojemnikiem na dane. Działa podobnie do baz danych - posiada rekordy (zwane "items"), a każdy item posiada swój unikatowy identyfikator. Prosty przykład:

{ "identifier": "name",
    "items": [
        { "name": "Patryk Jar", "site": "yarpo.pl" },
        { "name": "Jan Kowalski", "site": "kowalski.pl" }
    ]
}

Jak widać, główne pola to `identifier', który określa jakie pole itema jest identyfikatorem ("bazodanowy" klucz główny). Kolejnym polem jest `items', będący tablicą. Całość jak widać wykorzystuje literał obiektowy JS. Osobiście polecam, aby był to JSON (purystyczne podejście - data store może także pobierać dane z serwera za pomocą JSON Rest).

Dodatkowo istnieje możliwość przypisania podwęzłów (ang. subnodes). Jeśli wykorzystamy `dojo.data.ItemFileReadStore' to kod JSON będzie wyglądał tak:

{   "identifier": "id",
"label": "name",
"items": [
{ "id": "AF", "name":"Africa", "type":"continent",
            "children":[
                {"_reference":"KE"}
            ]
        },
{ "id" : "KE", "name":"Kenya", "type":"country"}
     ]
}

Powyższy kod jest fragmentem danych wyświetlanych na drzewie z przykładowego drzewa na początku wpisu. Identyfikatorem ("kluczem głównym") itema jest pole `id'. Na drzewie wyświetlana jest zawartość pola `name' (wskazana przez `label'). Kenya (id = KE) jest dzieckiem (potomkiem, podwęzłem, subnodem) itema (węzła) Africa (id = AF). Pełny kod JSON przykładowego drzewa.

Dodatkowo warto wiedzieć, że wykorzystany tu data store (dojo.data.ItemFileReadStore) służy jedynie do odczytania danych. Nie można za jego pomocą dodawać węzłów do drzewa, o czym za chwilę :).

Kod HTML:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
	<meta http-equiv="content-type" content="text/html; charset=utf-8">
	<script src="http://ajax.googleapis.com/ajax/libs/dojo/1.6/dojo/dojo.xd.js" djConfig="parseOnLoad: true"></script>
	<script type="text/javascript">
		dojo.require("dojo.data.ItemFileReadStore");
		dojo.require("dijit.Tree");
	</script>
	<link rel="stylesheet" type="text/css" href="http://ajax.googleapis.com/ajax/libs/dojo/1.6/dijit/themes/claro/claro.css" />
</head>
<body>
	<div dojoType="dojo.data.ItemFileReadStore" jsId="continentStore"
		url="/download/examples/dojo/tree/countries-small.json"></div>
	<div dojoType="dijit.tree.ForestStoreModel" jsId="continentModel" store="continentStore"
		query="{type:'continent'}" rootId="continentRoot" rootLabel="Continents"
		childrenAttrs="children">
	</div>
	<div dojoType="dijit.Tree" id="mytree" model="continentModel" openOnClick="true"></div>
</body>
</html>

demo online

Jest to trochę okrojony przykład ze strony Dojo. Warto przyjrzeć się kilku aspektom:

  • Dojo załączane jest z serwera googla (można lokalnie, ale dzięki temu możesz skopiować wprost kod i będzie działał - plik z JSON musisz jednak ładować lokalnie), podobnie jak odpowiednie style. Claro jest jedną z dostępnych za darmo skórek. IMO najładniejszą;
  • W tagu <script/> ładującym dojo znajduje się niestandardowy parametr `djConfig'. Pozwala on na konfigurację działania frameworka. Tu ustawia, aby od razu zostały sparsowane wszystkie widgety.
  • Do ładowania odpowiednich modułów dojo służy `dojo.require'.
  • Powyższy przykład używa metody deklaratywnej. Metoda ta pozwala na tworzenie widgetów w oparciu o kod HTML z niestandardowymi atrybutami.

Przeglądając kod możesz dostrzec pewną zależność - najpierw tworzony jest data store, który jest podawanany do modelu. Model jest podawany do drzewa. Można to przedstawić w ten sposób:

store < model < tree

Wykorzystany tu model to `dijit.tree.ForestStoreModel'. Pozwala on na tworzenie lasu. Czym jest las?! Las to zbiór drzew. Tak więc w takim modelu może być więcej niż jeden korzeń (korzeń = główny węzeł). Korzeniami w tym przykładzie są wszystkie węzły (itemy) o type = "contynent". Jeśli chciałbyś mieć tylko i wyłącznie jeden korzeń, to zainteresuj się modelem `dijit.tree.TreeStoreModel'.

Szukanie węzłów

Skoro mamy już zbudowane proste drzewo, przeszukajmy je. Danych w drzewie nie szukamy w strukturze DOM. Wszystkie operacje na drzewie wykonujemy na data store. No właśnie - wszystkie. Ale wcześniej pisałem, że `dojo.data.ItemFileReadStore' służy jedynie do odczytu danych :/. Więc jak to jest?

Każdy data store dostarczany przez Dojo (lub napisany przez użytkownika - tak, można napisać samemu) dostarcza zestawu funkcjonalności. Funkcjonalności te zostały podzielone na:

Wykorzystany przez nas `dojo.data.ItemFileReadStore' wspiera:

  • Read API
  • Identity API

Zatem pozwala na odczytanie danych, jak i identyfikację każdego itema. Jako prosty przykład, niech posłuży taki kod:

function search()
{
	var id = dojo.byId('identity').value;
	// zauważ, że continentStore jest zmienna globalną
	continentStore.fetchItemByIdentity({
		identity: id,
		onItem: function(item) { alert("znalazłem: " + item.name ); },
		onError: function() { alert("błąd!"); }
	});
}

Przykład demo (pamiętaj, aby podawać id, nie nazwę. Czyli np. "KE", "AF", itp.)

Skąd się wzięła zmienna `continentStore'? Przypomnę może ten fragment kodu:

<div dojoType="dojo.data.ItemFileReadStore" jsId="continentStore"
     url="/download/examples/dojo/tree/countries-small.json"></div>

W metodzie deklaratywnej można często używać atrybutu `jsId', dzięki któremu dany widget jest dostępny z poziomu JavaScript właśnie pod wskazaną nazwą.

Metoda `fetchItemByIdentity' oczekuje jako parametr obiektu z polami:

  • `identity' - czyli identyfikator ("klucz główny") itema
  • `onItem' - funkcja wywołana (ang. callback) asynchronicznie, gdy zostanie znaleziony item o zadanym identity
  • `onError' - callback obsługujący błędy

Prócz metody `fetchItemByIdentity' jest jeszcze metoda `fetch', dzięki której możemy znaleźć item (lub kilka) zadając dokładniejsze zapytanie:

function search()
{
	var country = dojo.byId('identity').value;
	continentStore.fetch({
		query: {"name" : country},
		onComplete: function(items, request) {
			alert("znalazłem: " + items.length + "elementów");
		},
		onError: function() { alert("błąd!"); }
	});
}

demo online (tu podajesz nazwę widoczną na drzewie - pole `label' w item, np. "Africa", "China")

W wyniku działania tej metody otrzymujemy tablicę wyników. Aby dostać się do jakiegoś elementu stosujemy np. `items[0].name'.

Warto wiedzieć o:

 Dodawanie węzłów

Aby móc dodawać węzły musimy skorzystać z dojo.data Write API. Wykorzystamy zatem `dojo.data.ItemFileWriteStore'. Ten data store implementuje wszystkie 4 dojo.data API (odczyt, identyfikacje, zapis, monitorowanie).

Aby było jaśniej - prosty kod:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html dir="ltr">
<head>
    <meta http-equiv="content-type" content="text/html; charset=utf-8">
    <script src="http://ajax.googleapis.com/ajax/libs/dojo/1.6/dojo/dojo.xd.js" djConfig="parseOnLoad: true"></script>
    <script type="text/javascript">
        dojo.require("dojo.data.ItemFileWriteStore"); // ZMIENIONE
        dojo.require("dijit.Tree");
    </script>
    <link rel="stylesheet" type="text/css" href="http://ajax.googleapis.com/ajax/libs/dojo/1.6/dijit/themes/claro/claro.css" />
</head>

<body class=" claro ">
    <!-- ZMIENIONE -->
    <div dojoType="dojo.data.ItemFileWriteStore" jsId="continentStore" url="/download/examples/dojo/tree/countries-small.json"</div>
    <!-- tak samo reszta -->
    <div dojoType="dijit.tree.ForestStoreModel" jsId="continentModel" store="continentStore"
        query="{type:'continent'}" rootId="continentRoot" rootLabel="Continents"
        childrenAttrs="children">
    </div>
    <div dojoType="dijit.Tree" id="mytree" model="continentModel" openOnClick="true"></div>
</body>
</html>

demo online

Jak widać wykorzystanie ItemFileWriteStore nie wymaga zmian w kodzie JSON. A zmiany w poprzednim kodzie są wręcz kosmetyczne. Co więcej poprzednie przykłady (wyszukiwania) nadal działają:

Jednym ze sposobów dodania nowego węzła jest złapanie jakiegoś itema i dodanie do niego węzła potomnego. Przykładowy kod JS realizujący to zadanie jest widoczny na poniższym listingu:

function addNode()
{
	var newNodeLabel = dojo.byId('nodeLabel').value,
	addToNode = dojo.byId('nodeParent').value;
	continentStore.fetchItemByIdentity({
		identity: addToNode,
		onItem: function(item) {
			// dodajemy nowy węzeł
			continentStore.newItem(
				// nowy węzeł zgodny ze strukturą istniejących itemów
				{"id": newNodeLabel + (+new Date()), "name": newNodeLabel, "type":"city"},
				// wskaż miejsce dodania
				{"parent": item, attribute:"children"}
			);
			// zapisujemy - teraz zostaną pokazane zmiany na drzewie
			continentStore.save({
				onComplete: function() { alert("dodało: " + newNodeLabel); },
				onError: function() { alert("Wystąpił błąd"); }
			});
		},
		onError: function() { alert("Błąd!"); }
	});
	return false;
}

demo online

Taki sposób dodawania węzłów oczywiście nie posiada opcji zapisu na serwerze (o czym kiedyś napiszę jeszcze, bo jest to możliwe). Oczywiście można by zrobić ręczny zapis za pomocą mechanizmów ajaksowych. Zostawiam to jednak jako zadanie domowe:)

Usuwanie węzłów

Aby móc usuwać węzły musimy skorzystać z dojo.data Write API. To usuwania służy metoda `store.deleteItem'.

function delNode()
{
	var nodeToDelete = dojo.byId('nodeLabel').value;

	continentStore.fetch({
		query: {"name" : nodeToDelete},
		onComplete: function(items, request)
		{
			// usuń wszystkie pasujące itemy
			for (var i = 0; i &lt; items.length; i++)
			{
			continentStore.deleteItem(items[i]);
			}
			// zapisujemy aktualny stan - uwidaczniając go na drzewie
			continentStore.save({
				onComplete: function() { alert("usunięto: " + nodeToDelete); },
				onError: function() { alert("Wystąpił błąd"); }
			});
		},
		onError: function() { alert("Błąd!"); }
	});
	return false;
}

demo online

W najbliższym czasie postaram się opisać więcej możliwości Dojo, uwzględniając szczególnie bibliotekę widgetów dijit oraz drzewo.

Co dalej:

Komentarze (0) Trackbacks (0)

Brak komentarzy.


Leave a comment

 

Brak trackbacków.