W te i nazad

Ładnie wygląda pierwszy odcinek w pokolorowanym HTML'u?
Pomóż przerobić bieżący!
Wdzięczność gwarantowana:-)

W bieżącym odcinku naszym celem będzie uzyskanie komunikacji pomiędzy xml’em i wygenerowanym z niego HTML’em. W obu kierunkach. Oczywiście z wykorzystaniem transformacji, bo o nich jest serial. Każdy przykład ma osobnego css’a. Nie mówię, że są dobre czy ładne – mają do spełnienia pewne zadania. Kursokonferencja jest z xslt a nie kaskadowych arkuszy styli.

Proszę o wyrozumiałość w kwestii js’a – po pierwsze mówimy o xslt, po drugie starałem się jak najmniej rozdzielać kod dla obu środowisk, po trzecie na co dzień działam tylko z msxml’em i większość rzeczy w js robię całkiem inaczej niż tutaj.

Będę głęboko zobowiązany za testowanie i konsultację skryptów. Zwłaszcza pod Mozillą

Najpierw trochę zamieszania, ponieważ przechodzimy do rzeczy, na które IE i Mozilla patrzą w różny sposób. IE dostęp do xml’a i transformacji, która go obrabiała podaje na talerzu:

  xmlDoc=document.XMLDocument;
  xslDoc=document.XSLDocument;

Mimo długich poszukiwań nie udało mi się znaleźć gotowego sposobu na uzyskania odpowiednika dla Mozilii. Wymyśliłem wiec własny. Pewnie są lepsze, ale nigdy nie ukrywałem, że prywatnie lepiej mi się pracuje na produktach z Redmont niż tych innych (widać choćby po sposobie kolorowaniu kodu). Dlatego, jeżeli ktoś ma/zna lepsze/ładniejsze/koszerniejsze rozwiązania– chętnie się ich nauczę. Mnie wyszło, że trzeba xml’a i xsl’a zassać w Mozilli od nowa.

Do HTML’a produkowanego przez transformację dodajemy skrypt i obsługę załadowania dokumentu:

<xsl:template match="/">
<html>
  <head>
    <link type="text/css" rel="stylesheet" href="style.css"/>
    <title>Pierwsze bajery</title>
    <script type="text/javascript" src="script.js"/>
  </head>
  <body onload="init()">
    <xsl:apply-templates select="*"/>
  </body>
</html>
</xsl:template>

Następnie popełniamy skrypt:

var xmlDoc,xslDoc,xslProc,ieEnv=false;
var file=document.location.href;
function init(){
  if(document.XMLDocument){

Jeżeli mamy do czynienia z IE – robi się samo:

xmlDoc=document.XMLDocument; xslDoc=document.XSLDocument;

zaznaczymy na przyszłość, że mamy do czynienia z msxml:

ieEnv=true; }else{

Dla Mozilli (jeżeli się ktoś uprze by to oglądać w czymś jeszcze innym to będzie to jego problem):

xslProc=new XSLTProcessor(); xmlDoc=document.implementation.createDocument("", "", null); xmlDoc.addEventListener("load", xmlLoad,false); xmlDoc.load(file.replace(/\.\w+$/,'.xml')); }; }; function xmlLoad(){

Po załadowaniu xml’a ładujemy - w analogiczny sposób – xsl’a.

xslDoc=document.implementation.createDocument("", "", null); xslDoc.addEventListener("load",xslLoad,false); xslDoc.load(file.replace(/\.\w+$/,'.xsl')); }; function xslLoad(){ xslProc.importStylesheet(xslDoc);

Przyda się za chwilę.

};

Można by na tym skończyć, gdyby nie bolesna świadomość dwukrotnego ładowania dokumentów do przeglądarki L. Przy pracy lokalnej nie ma to większego znaczenia (jeżeli pierwsze załadowanie zmuli system operacyjny, do drugie będzie tylko dalszym ciągiem zmulenia).

Przy pracy zdalnej moglibyśmy zacząć generować niepotrzebny transfer, co po pierwsze kosztuje, a po drugie denerwuje (na przykład na GPRS’ie).

Rozwiążemy problem krótkim html’owym plikiem, zawierającym to co wyprodukowałby szablon z match="/":

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>Małe zamieszanie :-(</title>
    <link type="text/css" rel="stylesheet" href="style.css"/>
    <script type="text/javascript" src="script.js"></script>
  </head>
  <body onload="init()"></body>
</html>

Przerabiamy skrypt. Dla sportu tak, aby zachowywał się poprawnie zarówno w przypadku uruchomienia xml’a i html’a:

function init(){
  if(document.XMLDocument){

Tutaj bez zmian.

xmlDoc=document.XMLDocument; xslDoc=document.XSLDocument; ieEnv=true; }else{

Mała poprawka na uruchomienie htm’a w egzotycznej przeglądarce...

try{

Jak poprzednio.

xslProc=new XSLTProcessor(); xmlDoc=document.implementation.createDocument("", "", null); xmlDoc.addEventListener("load", xmlLoad,false); xmlDoc.load(file.replace(/\.\w+$/,'.xml')); }catch(e){

....cały czas jest szansa na uruchomienie htm’a w IE:

try{ xmlDoc=new ActiveXObject('msxml2.domdocument'); }catch(e){ alert('Skorzystaj z przeglądarki obsługującej xml\'a!\n(oraz kodowanie UTF-8...)');

Jeżeli jeszcze ktoś nie wie: TAK. Nie lubię Opery. I nie dlatego że jest Operą (w końcu IE 4.0 też nie obsługiwało XML’a), ale dlatego że gromadka jej nadaktywnych użytkowników zachowuje się jak połączenie Młodzieży Wszechpolskiej z Amway’em. Sorry – nie mogłem się powstrzymać.

return; };

Odreagowałem po ostatnim return :-) Jeżeli jesteśmy w tym miejscu, to znaczy, że jednak htm zapakowany do IE, więc msxml’owe:

xslDoc=new ActiveXObject('msxml2.domdocument'); xmlDoc.async=false; xmlDoc.load(file.replace(/\.\w+$/,'.xml')); xslDoc.async=false; xslDoc.load(file.replace(/\.\w+$/,'.xsl')); document.body.innerHTML=xmlDoc.documentElement.transformNode(xslDoc); ieEnv=true;

I za to najbardziej lubię MS. Szybkie, krótkie proste transformNode(). Mogliby jednak popracować nad transformNodeToObject() przy wyjściu innym niż xml L

W MSDN’ie można znaleźć kupę przykładów z 'msxml2.XSLtaransform'? Powodzenia! Po pierwsze xslDocument nie jest typu 'msxml2.FreeThreadedDOMDocument' (więc .loadXML() trzeba by było zastosować), po drugie po takim numerze stracilibyśmy url ‘a transformacji i posypały by się document().

}; };

To będzie potrzebne znacznie później – na udajemy, że nie widzimy.

evtsHndlrs(); }; function xmlLoad(){

Tutaj też bez zmian.

xslDoc=document.implementation.createDocument("", "", null); xslDoc.addEventListener("load",xslLoad,false); xslDoc.load(file.replace(/\.\w+$/,'.xsl')); }; function xslLoad(){ xslProc.importStylesheet(xslDoc); if(file.match(/\.htm$/)){

Jeżeli załadowany htm, wrzucamy do body wynik transformacji:

document.body.appendChild(xslProc.transformToFragment(xmlDoc.documentElement.document));

Chylę czoła przed Mozillą. IE z MSXML’em z tego co mi wiadomo nie potrafi wykonać bezpośredniej transformacji węzła xml do węzła html (MSDN jest podejrzanie enigmatyczny w temacie przeglądarkowego createDocumentFragment...).

}; };

Tyle skryptu. Jeszcze tylko drobna modyfikacja xsl’a:

<xsl:template match="*" mode="pthElem">
  <xsl:text>/*[</xsl:text>
  <xsl:value-of select="count(preceding-sibling::node())"/>

Było: count(preceding-sibling::*) + 1

<xsl:text>]</xsl:text> </xsl:template>

Operację przeprowadzamy w celu uniknięcia wnikania w następnym przykładzie w różnice pomiędzy Mozillowym .evaluate(...) a IE’owym .selectNodes(...). – korzystać będziemy ze standardowej kolekcji .childNodes (dlatego ...::node() zamiast ...::*), która w js’ie numerowana jest od zera (w transformacja numeruje od 1).

Wułala. Zobaczmy jak działa:

03/1.js 03/1.xsl 03/1.xml 03/1.htm

Zwróćmy uwagę na to, że przy ładowaniu htm’a transformujemy xmlDoc.documentElement - korzystamy z promowanej przeze mnie konstrukcji z match="/". No i nie mogę odżałować, braku bezpośredniego dostępu do xml’a i xsl’a w Mozilli. Ale może kiedyś będzie.

Ale nie jest źle – IE’owcy (patrząc uważnie) mogli przeanalizować kilka nie do końca oczywistych zjawisk. Prostota dochodzenia do efektów w tym środowisku ma swoje złe strony. Przywiązywanie się do przykładów bez próby zrozumienia dlaczego działają w ten sposób prowadzi na manowce. Jedno z moich ulubionych powiedzeń: Ech... Żeby życie było tak proste jak przykłady z MSDN’a....

U mnie Mozilla ponumerowała węzły w wygenerowanych ścieżkach inaczej niż IE. Pytania konkursowe:

  1. Dlaczego?
  2. Jak temu zapobiec?

W te...

Wracamy do xslt. Zmodyfikujemy transformację w celu uzyskania możliwości edycji danych wstawionych z xml’a. Do pozycji słownika lokalizującego (!names.xml) dodałem informację o kolejności wyświetlania atrybutów w węzłach (tymczasowo – po jednym z kolejnych odcinków nie będzie to już potrzebne).

Zmiany w transformacji

Tym razem listę stawek VAT wczytujemy do parametru globalnego – ponieważ będzie wykorzystywana przy malowaniu pozycji.

<xsl:param name="vatRate" select="document('vatRate.xml')/*"/>

Zmiana css’a – w poprzednim chyba się bałagan zrobił:

<xsl:template match="/"> <html> <head> <link type="text/css" rel="stylesheet" href="style-2.css"/>

Szablon domyślnego sposobu pokazywania węzłów:

<xsl:template match="*"> <div class="node {name()}"> <xsl:apply-templates select="." mode="nodeHead"/> <xsl:apply-templates select="." mode="nodeCont"/> </div> </xsl:template>

Osobny szablon dla nazwy węzła (rozdzieliłem to, bo sposób wyświetlania zawartości węzłów będzie się różnił):

<xsl:template match="*" mode="nodeHead"> <xsl:variable name="xPath"><xsl:call-template name="getPath"/></xsl:variable> <div class="info">

Dodamy podglądanie zawartości węzła

<div class="nodeName" onclick="showXml('{$xPath}')"><xsl:call-template name="nameFromDict"/></div> </div> </xsl:template>

Domyślny szablon dla zawartości węzła:

<xsl:template match="*" mode="nodeCont"> <div class="content">

Wykorzystujemy listę atrybutów dodaną do słownika z polskimi nazwami węzłów:

<xsl:apply-templates select="." mode="attsWrite"/> <xsl:apply-templates select="*"/> </div> </xsl:template>

Zmodyfikowany szablon do wybierania atrybutów w pożądanej kolejności:

<xsl:template match="*" mode="attsWrite">

Zamiast wartości domyślnej ustawionej na sztywno pobieramy z zewnętrznego pliku ($names jest globalne):

<xsl:param name="list" select="$names/*[@name=name(current())]/@atts"/> <xsl:variable name="nextList" select="substring-after($list,' ')"/> <xsl:apply-templates select="@*[name()=substring-before($list,' ')]"/> <xsl:if test="$nextList!=''"> <xsl:apply-templates select="." mode="attsWrite"> <xsl:with-param name="list" select="$nextList"/> </xsl:apply-templates> </xsl:if> </xsl:template>

Domyślny sposób wyświetlania atrybutów:

<xsl:template match="@*"> <xsl:variable name="xPath"> <xsl:call-template name="getPath"/> <xsl:value-of select="concat('/@',name())"/> </xsl:variable> <div class="{name()}"> <span class="name" title="{$xPath}"><xsl:call-template name="nameFromDict"/><xsl:text>:</xsl:text></span>

Dajemy sobie szansę na modyfikowanie wartości dokumenu:

<input id="{generate-id()}_{name()}" value="{.}" onchange="setValue('{$xPath}',this)" name="{generate-id()}_{name()}"/>

Dodatkowo id – na razie dla czystej formalności.

</div> </xsl:template>

Teraz załatwiamy różnice w wyświetlaniu poszczególnych węzłów (ogólny znajduje się szablonie <xsl:template match="*">). Dla węzła faktury podmieniamy div na form (dla formalności – korzystamy z inputów i selectów):

<xsl:template match="doc:Invoice"> <form onsubmit="return false" class="node {name()}" id="Form1"> <xsl:apply-templates select="." mode="nodeHead"/> <xsl:apply-templates select="." mode="nodeCont"/> </form> </xsl:template>

Dla sprzedawcy wrzucamy same wartości (bez edycji pól):

<xsl:template match="doc:Vendor/@*"> <div class="{name()}"> <div class="lbl60"><xsl:call-template name="nameFromDict"/>:</div> <div><xsl:value-of select="."/></div> </div> </xsl:template>

Kod pocztowy i miasto bez opisu pola:

<xsl:template match="doc:Vendor/@Zip | doc:Vendor/@Post"> <div class="{name()}"> <div><xsl:value-of select="."/></div> </div> </xsl:template>

Przy danych nabywcy pozwalamy na zmianę wartości (kod i miasto jak poprzednio – bez mian):

<xsl:template match="doc:Customer/@*"> <xsl:variable name="xPath"> <xsl:call-template name="getPath"/> <xsl:value-of select="concat('/@',name())"/> </xsl:variable> <div class="{name()}"> <div class="lbl60"><xsl:call-template name="nameFromDict"/>:</div> <input id="Text1" value="{.}" onchange="setValue('{$xPath}',this)" NAME="Text1"/> </div> </xsl:template> <xsl:template match="doc:Customer/@Zip | doc:Customer/@Post"> <xsl:variable name="xPath"> <xsl:call-template name="getPath"/> <xsl:value-of select="concat('/@',name())"/> </xsl:variable> <div class="{name()}"> <input id="Text2" value="{.}" onchange="setValue('{$xPath}',this)" NAME="Text2"/> </div> </xsl:template>

Szkielet tabelki pozycjami faktury zasadniczo bez zmian ale w szablonie z mode="nodeCont" (nazwa wyświetli się domyślnym szablonem mode="nodeHead"):

<xsl:template match="doc:Positions" mode="nodeCont"> <table class="content" ID="Table1"> <thead> <th>Lp</th> <xsl:apply-templates select="*[1]" mode="namesWrite"/> ...

Nazwy kolumn ze słownika:

<xsl:template match="*" mode="namesWrite"> <xsl:param name="list" select="$names/*[@name=name(current())]/@atts"/> ...

Wiersze ponumerujemy, i damy na nich onclicka pokazującego węzeł:

<xsl:template match="doc:Item"> <xsl:variable name="xPath"> <xsl:call-template name="getPath"/> <xsl:value-of select="concat('/@',name())"/> </xsl:variable> <tr class="Item"> <td class="Lp" onclick="showXml('{$xPath}')"> <xsl:value-of select="position()"/> </td> <xsl:apply-templates select="." mode="attsWrite"/> </tr> </xsl:template>

Inputy z wartościami w ten sam sposób co wcześniej, tyle że w td i dla odpowiednich ustawiamy formatowanie wyświetlania liczb. Nie wrzucam całego kodu bo poza stawką VAT nic ciekawego się w nim nie dzieje.

<xsl:template match="doc:Item/@vatRate"> <xsl:variable name="xPath"> <xsl:call-template name="getPath"/> <xsl:value-of select="concat('/@',name())"/> </xsl:variable> <td class="{name()}"> <select id="Select1" value="{.}" onchange="setValue('{$xPath}',this)" name="Select1"> <xsl:apply-templates select="$vatRate/*" mode="writeOpts"> <xsl:with-param name="value" select="."/> </xsl:apply-templates> </select> </td> </xsl:template>

Dodajemy listę opcji:

<xsl:template match="*" mode="writeOpts"> <xsl:param name="value"/> <option value="{@name}"> <xsl:if test="@name=$value"> <xsl:attribute name="selected">true</xsl:attribute> </xsl:if> <xsl:value-of select="@name"/> </option> </xsl:template>

Sumowanie przez odwołanie do pozycji słownika stawek bez zmian, generowanie ścieżek również.

Dopiszemy kawałek skryptu obsługującego klikanie.

W obu funkcjach przekazujemy ścieżkę – zlokalizujmy węzeł który symbolizuje:

function locateNodeByPath(pth){
  var xmlLoc=xmlDoc.documentElement,pos
  pth=pth.split('/');
  for(var k=2;k<pth.length;k++){

Od zera nie – bo ścieżka zaczyna się od slash’a. Od jeden też nie bo ustawiliśmy wcześniej w zmiennej .documentElement który jest pierwszym elementem ścieżki.

Dlaczego w ten sposób? Wygenerowane ścieżki mają dla pierwszego elementu wartość jeden. Transformacja przed głównym elementem widzi jeszcze instrukcję sterującą (tak tę która spowodowała jej uruchomienie). W DOM’ie na liście węzłów znajduje się jeszcze prolog dokumentu więc .documentElement jest trzecim elementem (z ideksem 2). Dla transformacji drugim.

Jeżeli trafiliśmy na atrybut – kończymy – tutaj lokalizujemy węzły

if(pth[k].match(/@\w+$/)){break;};

Wydłubujemy indeks

pos=pth[k].match(/\[\d+\]/)[0].slice(1,-1);

Wybieramy kolejny węzeł ze ścieżki:

xmlLoc=xmlLoc.childNodes[pos] }; return xmlLoc };

Obsługa zmian wartości (onchange inputów i selectów):

function setValue(pth,obj,val,nam){ var xmlLoc=locateNodeByPath(pth),nam; val=val?val:obj.value;

Jeżeli nie przekazano (a nie przekazano) nazwy atrybutu – wydłubujemy do ze ścieżki:

nam=nam?nam:pth.match(/\/@\w+$/)[0].substr(2); xmlLoc.setAttribute(nam,val) };

Wyglądać może że jest trochę komplikacji ze względu na atrybuty. Ale tekstów nie ustawiało by się w tak prosty sposób. No i ja lubię atrybuty J

Na koniec kliknięcie w pokazujące xml’a:

function showXml(pth){ var xmlLoc=locateNodeByPath(pth); if(ieEnv){

Nie unikniemy rozróżnienia środowiska (ale dwa razy się już udało!).

alert(xmlLoc.xml); }else{ alert((new XMLSerializer()).serializeToString(xmlLoc)); }; };

Patrzymy.

03/2.js 03/2.xsl 03/2.xml 03/2.htm

Klikając w elementy pogrubione można obejrzeć xml’a węzła i stwierdzić, że wartości w dokumencie naprawdę się aktualizują J

Odpowiedź na pytanie konkursowe numer 1: Po takim załadowaniu dokumentu białe znaki pomiędzy elementami (moja) Mozilla potraktowała jako węzły tekstowe.

... i nazad

Niektórych pewnie denerwowało nieustanne generowanie ścieżek. Z moich doświadczeń wynika, że nie ma to zasadniczego wpływu na wydajność pokazywania dokumentów o wiele większych niż nasz przykładowy. Ale można bez ścieżek. Wymaga to jednak wyprodukowania HTML’a o bardzo regularnej strukturze, z wieloma na pozór niepotrzebnymi tagami.

Zaczniemy od posprzątania transformacji (która w celu pokazywania coraz to nowych rzeczy osiągnęła stadium uniemożliwiające jej dalszy rozwój.

Daj odpowiednią ilość czasu programiście a dojdzie do wniosku, że trzeba wszystko napisać na nowo J Ale jakbyśmy zaczęli od tego co za chwilę się będzie działo...

Ponieważ HTML ma precyzyjnie oddawać strukturę dokumentu przepraszamy się z match="node()" oraz <xsl:apply-templates/> co uodporni nas na różnice w parsowaniu xml’a pomiędzy IE a Mozillą (i to jest pierwsza odpowiedź na drugie pytanie konkursowe, będzie jeszcze jedna).

<xsl:template match="/">
  <html>
    <head><link type="text/css" rel="stylesheet" href="3.css...
    <body onload="init()">
      <xsl:apply-templates/>
    </body>
  </html>
</xsl:template>

<xsl:template match="node()">

Ważne: to jest jedyny sposób wchodzenia w węzły w tej transformacji. Żadnych skoków na boki przy pomocy mode="cosTam".

<xsl:variable name="tagName">

Na węzła który nas do szablonu tym razem przywiódł podejmujemy decyzję o HTML’owym tagu:

<xsl:choose> <xsl:when test="not(self::*)">pre</xsl:when>

pre {display:none} i już! Trawimy dokumenty bez względu na to ile czasu mogliśmy poświęcić na szukanie preserveWhitespaces z document.implementation,. Przestaliśmy się również obawiać instrukcji sterujących oraz komentarzy wstawionych w dokument przez trole...

<xsl:when test="self::doc:Invoice">form</xsl:when>

to jest najwyższy element, więc form – żeby się nic nie irytowało, że dalej będą input‘y i select’y.

<xsl:when test="self::doc:Positions">table</xsl:when>

jeżeli lista pozycji ładujemy ją do tabelki.

<xsl:when test="self::doc:Item">tr</xsl:when>

Pozycji faktury należy się wiersz. Cała reszta w div’ach:

<xsl:otherwise>div</xsl:otherwise>

Jeżeli chcesz – drogi uczestniku – powiedzieć że przecież można by wszystko na div’ach, to odpowiadam: nie psuj mi cholera puenty!

</xsl:choose> </xsl:variable>

Tworzymy na wyjściu element (czyli HTML’owego taga) o nazwie ustalonej powyżej:

<xsl:element name="{$tagName}">

Atrybuty tworzyliśmy już w sposób kanoniczny. Elementy – jak widać – też można.

<xsl:attribute name="class">

Klasa każdego taga (html’owego) reprezentującego węzeł (nie element) xml’a to node:

<xsl:text>node </xsl:text>

druga klasa to nazwa węzła (dla tekstów w Mozilli jej nie będzie):

<xsl:value-of select="translate(name(),'-','_')"/>

Kłania się notacja węgierska – moim prywatnym sprawdzająca się genialnie przy przetwarzaniu xml’i. Czasami trzeba na szybko na boku przetworzyć listę nazw elementów i ich wartości – jeden delimiter do dyspozycji więcej, Nie polecam tworzenia dokumentów z myślnikami i podkreśleniami w nazwach.

</xsl:attribute> <xsl:call-template name="nodeHead"/> <xsl:call-template name="nodeCont"/> <xsl:call-template name="nodeOpts"/>

Tak. Tak nie lubiane przeze mnie <xsl:call-template... Tutaj akurat ma zastosowanie. Nie mam jeszcze wyświetlonych wszystkich informacji a chcę mieć pewność że transformacja wróci przy następnych węzłach do tego samego szablonu.

</xsl:element> </xsl:template>

Gdyby to było wszystko w transformacji to otrzymalibyśmy następująca strukturę HTML’a na wyjściu (naprawdę posypało by się, bo nie ma nazwanych szablonów, które wołamy przez <xsl:call-template ...):

<form ID="Form2"> brak szczegółów Invoice <div> brak szczegółów Vendor </div> <div> brak szczegółów Customer </div> <table ID="Table2"> brak szczegółów Positions (ale ich akurat nie ma) <tr> brak szczegółów Item </tr> </table> </form>

Szczegółami zajmują się nazwane szablony nodeHead (odpowiedzialny za ewentualne opisy węzłów), nodeCont (odpowiedzialny za wyświetlanie zawartości) oraz nodeOpts (chcielibyśmy móc dodać lub usunąć pozycję na fakturze). Żeby nie rozpraszać uwagi wrzucimy wartości występujące w elementach:

<xsl:template name="nodeCont"> <xsl:variable name="tagName">

Podejmujemy decyzję w jakim tagu umieścić zawartość:

<xsl:choose>

Dla pozycji faktury nic – wyjściu jesteśmy w tr, w td powinny wylądować kolejne atrybuty, nie ma w co zapakować L:

<xsl:when test="self::doc:Item"/>

Dla listy pozycji tbody (na wyjściu jesteśmy w table):

<xsl:when test="self::doc:Positions">tbody</xsl:when>

Reszta w div:

<xsl:otherwise>div</xsl:otherwise> </xsl:choose> </xsl:variable> <xsl:choose> <xsl:when test="string($tagName)!=''">

Produkujemy element o ustalonej nazwie (jeżeli jest co produkować):

<xsl:element name="{$tagName}">

nadajemy mu klasę w celu zaznaczenia klasy (sic!) zwracanego elementu:

<xsl:attribute name="class">content</xsl:attribute>

Wartości w węźle – znany szablon przerabiamy na nazwany (kwestia konsekwencji).

<xsl:call-template name="attsWrite"/>

Na koniec wysyłamy do dopasowania wszystkie węzły podrzędne. Zgodnie z założeniami dopasuje się poprzedni szablon.

<xsl:apply-templates/> </xsl:element> </xsl:when> <xsl:otherwise>

A jeżeli nie miało być węzła – same wyświetlamy same wartości.

<xsl:call-template name="attsWrite"/>

Że poszczególne elementy nie będą od siebie o tę samą liczbę parent? Wiem. Powinny. Ale na razie ciiii...

</xsl:otherwise> </xsl:choose> </xsl:template>

Sposób wyświetlania wartości też nieco zmodyfikujemy, ale w globalny sposób, więc o tem potem.

Póki co zajmiemy się opcjami (<xsl:call-template name="nodeOpts"/>) Opisy później, dobrze wytresowany klient i tak wie gdzie kliknąć J.

<xsl:template name="nodeOpts">

Ustalamy nazwę węzła w którym wyświetlimy opcje:

<xsl:variable name="tagName"> <xsl:choose>

Dla pozycji td (na wyjściu jesteśmy w tr):

<xsl:when test="self::doc:Item">td</xsl:when>

Dla listy pozycji tfoot (na wyjściu jesteśmy w table):

<xsl:when test="self::doc:Positions">tfoot</xsl:when>

Dla reszty div, którego css’em schowamy dla porządku dla węzłów których opcje z biznesowego punktu widzenia nie mają sensu na tym etapie. Kiedyś – kto wie...

<xsl:otherwise>div</xsl:otherwise> </xsl:choose> </xsl:variable> <xsl:element name="{$tagName}"> <xsl:attribute name="class">options</xsl:attribute> <xsl:apply-templates select="." mode="nodeOpts"/>

Warto zauważyć, że weszliśmy tutaj dopasowaniem match="node()" – ale opcje dla instrukcji sterujących czy komentarzy to już może w ramach jakiejś chałtury...

</xsl:element> </xsl:template>

Odruchowo szablon dla najbardziej ogólnego przypadku:

<xsl:template match="*" mode="nodeOpts"> <i>brak opcji dla <xsl:value-of select="name()"/></i> </xsl:template>

Dla Item (na wyjściu jesteśmy w td)– usuwanie bieżącej pozycji z faktury:

<xsl:template match="doc:Item" mode="nodeOpts"> <div class="red" onclick="delElem(this)">x</div> </xsl:template>

Dla Positions (na wyjściu jesteśmy w tfoot) sumowania – generalnie po staremu:

<xsl:template match="doc:Positions" mode="nodeOpts"> <tr class="sum"> <td colspan="4">

Dodamy tylko opcję dodania nowego elementu...

<div class="green" onclick="newElem(this,'Item','{$names/*[@name='Item']/@atts}')">Nowa pozycja</div>

Coś musi wiedzieć jakie wartości mogą wystąpić w węźle. Wie słownik węzłów, z którego korzystamy przy decydowaniu o kolejności wyświetlania wartości.

</td> <td class="total" colspan="2">Razem:</td> ...

... oraz zaślepkę na td z opcjami dla węzła.

<td> </td> </tr>

Pewnie wszyscy zapomnieli: to jest generowanie zestawienia sprzedaży według stawek realizowane zewnętrznym słownikiem stawek...

<xsl:apply-templates select="$vatRate/*"/> </xsl:template>

Dla innych węzłów w tym odcinku nie przewidujemy opcji.

Pora na wypełnienie i udostępnienie gdzie trzeba wartości dokumentu (<xsl:call-template name="attsWrite"/>):

<xsl:template name="attsWrite"> <xsl:param name="list" select="$names/*[@name=name(current())]/@atts"/> <xsl:variable name="nextList" select="substring-after($list,' ')"/> <xsl:apply-templates select="@*[name()=substring-before($list,' ')]"/> <xsl:if test="$nextList!=''"> <xsl:call-template name="attsWrite"> <xsl:with-param name="list" select="$nextList"/> </xsl:call-template> </xsl:if> </xsl:template>

Żadnych rewelacji. Zmiana jednego wołania na drugie.

Nie przez przypadek unikałem do teraz nazwanych szablonów. Przy wielu rzeczach (jak choćby powyższej) można było je spokojnie stosować. Ale w przykładach pałętających się po sieci są one zdecydowanie nadużywane. Nie istnieją sytuacje, w których trzeba używać nazwanych szablonów. Istnieją sytuacje, w których ich używanie jest wygodne. Niestety wygodne jest ich używanie przy różnego rodzaju kursach (tak jak nieszczęsnego <xsl:for-each).

Zajmiemy się pojedynczymi wartościami z dokumentu (znaczy atrybutami). Tak jak przy węzłach (match="node()") stworzymy dokładnie jeden szablon przetwarzający atrybuty w dokumencie. W słowniku nazw (!names.xml) pozycjom przybyło kilka atrybutów automatyzujących sposób wyświetlania wartości.

<xsl:template match="@*">
  <xsl:variable name="tagName">
    <xsl:choose>

Jeżeli atrybut jest w węźle Item - komórka tabeli (na wyjściu jesteśmy wewnątrz tr):

<xsl:when test="parent::doc:Item">td</xsl:when>

W pozostałych przypadkach:

<xsl:otherwise>div</xsl:otherwise> </xsl:choose> </xsl:variable> <xsl:element name="{$tagName}"> <xsl:attribute name="class">

Przez klasę kontener atrybutu dostaje informację o tym, że dotyczy atrybutu:

<xsl:text>att </xsl:text>

Druga klasa – nazwa atrybutu:

<xsl:value-of select="name()"/> </xsl:attribute>

Konsekwentnie nazwane szablony dla opisu i wartości:

<xsl:call-template name="attName"/> <xsl:call-template name="attValue"/> </xsl:element> </xsl:template>

Opisy:

<xsl:template name="attName">

Korzystam z listy nazw atrybutów, które są wyświetlane (dodatkowy atrybut w słowniku nazw):

<xsl:if test="contains($names/*[@name=name(current()/..)]/@labels,name())">

Pobieram informację o rozmiarze przewidzianym na opis pola (dodatkowy atrybut w słowniku nazw):

<xsl:variable name="size" select="$names/*[@name=name(current())]/@label"/> <div> <xsl:if test="string($size)!=''">

I jeżeli rozmiar określono ustawiam bezpośrednio stylem:

<xsl:attribute name="style"> <xsl:text>width:</xsl:text> <xsl:value-of select="$size"/> <xsl:text>px</xsl:text> </xsl:attribute> </xsl:if>

Nazwa ze słownika starym sposobem (tutaj nic nie kombinowałem J):

<xsl:call-template name="nameFromDict"/> <xsl:text>:</xsl:text> </div> </xsl:if> </xsl:template>

Wartości:

<xsl:template name="attValue">

Wybieramy nazwę tag’a w którym umieścimy dane:

<xsl:variable name="tagName"> <xsl:choose>

Sprzedawca bez możliwości edycji:

<xsl:when test="parent::doc:Vendor">div</xsl:when>

Stawka VAT ze słownika:

<xsl:when test="name()='vatRate'">select</xsl:when>

Powoli zaczyna być widać skąd moja prywatna konwencja nazywania atrybutów z wartościami których aplikacja/użytkownik nie mogą zmieniać w dowolny sposób.

Pozostałe pola trywialnym input’em:

<xsl:otherwise>input</xsl:otherwise> </xsl:choose>

Kolejny dodany do pozycji słownika nazw atrybut:

</xsl:variable> <xsl:variable name="size" select="$names/*[@name=name(current())]/@data"/>

Tworzymy kontenerek dla wartości:

<xsl:element name="{$tagName}"> <xsl:if test="string($size)!=''"> <xsl:attribute name="style"> <xsl:text>width:</xsl:text> <xsl:value-of select="$size"/> <xsl:text>px</xsl:text> </xsl:attribute> </xsl:if>

I jeszcze informacja dla przeglądarki, że ma podjąć reakcję na akcję elementu:

<xsl:if test="$tagName!='div'">

I ten if świadczy o tym, że prawdopodobnie lepszy byłby tutaj zwykły szablon a nie nazwany. Skąd ten wniosek? Nie wiem. Albo przewrażliwienie, albo intuicja J

<xsl:attribute name="id"> <xsl:value-of select="concat(generate-id(),'_',name())"/> </xsl:attribute> </xsl:if> <xsl:apply-templates select="." mode="attData"/> </xsl:element> </xsl:template>

Pozostaje jeszcze wpisanie wartości do kontenerka. Najpierw – wyjątkowo – szablon niedomyślny - wrzut tekstu w div dla danych sprzedawcy:

<xsl:template match="doc:Vendor/@*" mode="attData"> <xsl:value-of select="."/> </xsl:template>

Stawka VAT – obsługa extra:

<xsl:template match="doc:Item/@vatRate" mode="attData"> <xsl:if test="string(@vatRate)=''">

Dla nowych pozycji – wybrana pierwsza wartość z selec’a wprowadzałaby w błąd.

<option vlaue="">[--]</option> </xsl:if>

Tutaj po staremu

<xsl:apply-templates select="$vatRate/*" mode="writeOpts"> <xsl:with-param name="value" select="."/> </xsl:apply-templates> </xsl:template>

Pierwsza część dopasowania ogólnego:

<xsl:template match="@*" mode="attData"> <xsl:attribute name="value">

... zostało nam ustawienie value w wyrzuconych na wyjście inputach...

<xsl:apply-templates select="." mode="attFormat"/> </xsl:attribute> </xsl:template>

Szablon dla dopasowania ogólnego jest niepotrzebny. Pamiętamy, że apply atrybutu bez dopasowanego szablonu zwraca jego wartość.

Wartości liczbowe – formatujemy jak dotychczas.

<xsl:template match="doc:Item/@Price" mode="attFormat"> <xsl:value-of select="format-number(.,'0.0000')"/> </xsl:template> <xsl:template match="doc:Item/@Netto | doc:Item/@VatAmount | doc:Item/@Brutto" mode="attFormat"> <xsl:value-of select="format-number(.,'0.00')"/> </xsl:template>

Że co? Że sposób formatowania wartości można również trzymać w słowniku z nazwami??? Dobrze kombinujesz!!!

Pominięte opisy węzłów (<xsl:call-template name="nodeHead"/>) – w transformacji. Tam się nic nowego nie dzieje.

To była dziesiąta transformacja w naszej kursokonferencji. Ale dopiero pierwsza, która mi się w miare podoba. Nie jest sztuką namalować xsl’em jakiś obrazek. Sztuką nie jest produkować we własnym zakresie przy każdym malowaniu papieru i kredek.

Drugą częścią tego etapu jest css – popełniłem go z pomocą Krzyśka Kozłowskiego (któremu jeszcze raz serdecznie dziękuję). Obiecał, że niebawem skomentuje szablon – na razie można prześledzić jego etapy powstawania – najlepiej usunąć z niego wszystko, a następnie wklejać kolejne sekcje.

Gdyby nie tabelka... Gdyby nie tabelka to transformacja była by znacznie krótsza. Ale wykorzystanie tabelki pozwoliło pokazać istotę rozwiązania! Te choose w kilku miejscach pozwalało zatrzymać się na chwilę i przypomnieć co i dlaczego w danym momencie przetwarza transformacja. To by się dało na div’ach. To się powinno na divach. Ale jeszcze nie tym razem.

Został jeszcze skrypt, który też nieco się zmienił.

W transformacji tu i ówdzie nadaliśmy tagom identyfikatory . Inputy i selecty dostały je bo wypada.

Kilka diw’ów , i td w celu pokazania że można sprowokować akcje bez jawnego odwołania, a dzięki analizie przechwyconego zdarzenia. Nie sugeruję, że tak trzeba – informuję że tak można. Prywatnie tę metodę wykorzystuję przy dynamicznym menu kontekstowym a akcje aktualizacji pól deklaruję jawnie.

Tutaj aktualizację załatwimy globalnie – przy pracy zdalnej brak stu onclick=”cosTam()” wyrzuconych przez serwer może mieć znaczenie.

Ponownie proszę o wyrozumiałość w kwestii js’a – to jest po pierwsze kursokonferencja z xsl’a, a po drugie starałem się jak najmniej rozdzielać kod dla obu środowisk. Będę głęboko zobowiązany za testowanie i konsultację skryptów.

Do roboty. W zmiennych globalnych dodajemy oValue

var xmlDoc,xslDoc,xslProc,ieEnv=false,oValue;

function init(){
  ...

Pod koniec funkcji init() przechwytujemy obsługę kliknięcia (wyświtlanie xml’i ęzłów):

document.onclick=docClick;

Zmiany wartości (tego IE nie rozumie):

document.onchange=docChange;

Aktywowania i deaktywowania elementu (to dla IE):

document.onactivate=docActivate;

document.ondeactivate=docChange;

};

Obsługa aktywowania (IE) sprowadza się do sprawdzenia czy element może posiadać wartość i zapamiętania jej w zmiennej globalnej. Aktywowanie elementu bez wartości usuwa zapamiętaną wartość.

function docActivate(e){ var eob=event.srcElement,eid=eob.id; switch(eob.nodeName){ case 'INPUT':case 'SELECT':oValue=eob.value;return; }; };

Zmiana wartości (Mozilla) i deaktywowanie (IE) razem:

function docChange(e){ var eob=e?e.target:event.srcElement,eid=eob.id,nam,pth; switch(eob.nodeName){ case 'INPUT':case 'SELECT':break; default:oValue=null;return;

Inne elementy – koniec akcji.

};

To dla IE – jeżeli wartość się nie zmieniła:

if(oValue==eob.value){return;};

Dalej dla obu przeglądarek na podstawie budowy HTML’a tworzymy ścieżkę węzłów:

pth=pathFromHtml(eob);

A nazwę atrybutu pobieramy z identyfikatora elementu (ale jest ona także w klasie parentElement):

eid=eid.split('_'); nam=eid.slice(-1)[0];

Znaną już funkcją ustawiamy wartość w xml’u:

setValue(pth+'/@'+nam,eob); };

Budowanie ścieżki do węzła:

function pathFromHtml(eob){ var path='',pob,qob,k;

Podążamy w górę HTML’a:

for(pob=eob;pob!=document;pob=pob.parentNode){

Dla tagów z klasą zaczynającą się od node ...

if(pob.className.match(/^node/)){ qob=pob.previousSibling

... przechodzimy przez węzły poprzedzające...

for(k=0;qob;qob=qob.previousSibling){

... w celu zliczenia tych z node (ślicznie zliczają się niewidoczne pre! – pierwsze rozwiązanie zadania konkursowego nr 2.)

if(qob.className.match(/^node/)){ k++; }; };

I budujemy element ścieżki:

path='/*['+k+']'+path; }; }; return path; }

Klasę node nadawał jeden szablon nie powinno być zatem żadnych niespodzianek (głównie w tym celu przerabialiśmy transformację).

Ustawianie wartości z małym dodatkiem:

function setValue(pth,obj,val,nam){ var xmlLoc=locateNodeByPath(pth),nam; val=val?val:obj.value; nam=nam?nam:pth.match(/\/@\w+$/)[0].substr(2); xmlLoc.setAttribute(nam,val) switch(nam){ case 'Price':case 'Count': case 'Netto': case 'vatRate': case 'VatAmount': case 'Brutto':

Dla konkretnych elementów – tutaj wartości liczbowych pozycji faktury – wykonujemy dodatkowe działania przeliczające wartości (proszę się pobawić samemu):

//...

W niektórych przypadkach konieczne będzie przebudowanie kawałka HTML’a (bo zmiana niektórych atrybutów zmienia podsumowania:

rewriteHtmlParent(xmlLoc,obj); break; }; };

Dlaczego nie wykonujemy tych przeliczeń zależnych pól w transformacji? Przecież by się dało... Nie zapominajmy, że modyfikujemy naszym zadaniem jest modyfikowanie dokumentu a nie jego wyglądu!

Z moich doświadczeń wynika, że jeżeli zależności jest mało i są wewnątrz dokumentu to lepiej jest to zrobić JS’em przy obsłudze zdarzenia. Jeżeli jest ich więcej i sięgają do innych dokumentów wydajniej wychodzi napisanie drugiej transformacji, porównującej zmodyfikowany dokument z jego oryginalną wersją, która ustawia co trzeba w pliku modyfikowanym i plikach skojarzonych. I dopiero drugą transformacją tworzymy HTML’a. Ale o tym kiedy indziej. O walidacji również (chociaż spokojnie można ją zrealizować tutaj).

Szukanie sekcji HTML’a przedstawiającej nadrzędny węzeł zadanego xml’a:

function rewriteHtmlParent(xmlLoc,eob){

Ustawiamy węzeł na nadrzędny:

xmlLoc=xmlLoc.parentNode;

Idziemy do HTML’a bieżącego węzła (byliśmy w HTML’u atrybutu):

while(!eob.className.match(/^node/)){eob=eob.parentNode;}; eob=eob.parentNode;

Idziemy do HTML’a węzła do przebudowania (nadrzędnego):

while(!eob.className.match(/^node/)){eob=eob.parentNode;}; newHtml(xmlLoc,eob); };

Podmiana HTML’a:

function newHtml(xmlLoc,eob){ if(ieEnv){

W IE brutalny hack:

eob.outerHTML=xmlLoc.transformNode(xslDoc);

Metoda koszerna: zapamiętać nadrzędny, zapamiętać węzeł następujący, usunąć wskazany, dodać do dokumentu pre, ustawić mu innerHTML na rezultat transformacji w zależności czy był węzeł następujący insertBefore lub appendChild sklonowanych dzieci dodanego pre na koniec usunięcie pre. Straszne zamieszanie... Uprasza się MS o popracowanie nad transformNodeToObject przy wyjściu nie-xml’owym.

}else{

W Mozilli w cywilizowany sposób:

docNew=xslProc.transformToFragment(xmlLoc,document); eob.parentNode.replaceChild(docNew,eob); }; };

I ucieka focus... Ale wiemy co modyfikowaliśmy, możemy wiedzieć jak element opuściliśmy, więc można go ustawić powownie. Zwłaszcza, że wygenerowane identyfikatory są taki same! Taki mały urok generate-id().

Analizę akcji przy dodawaniu i usuwaniu elementów oraz wyświetlaniu xml’a węzłów po click’u pozostawiam czytelnikom (to nie jest kursokonferencja z js’aJ). Jest tam drugie rozwiązanie zadania konkursowego numer dwa – ja wybrałem jednak to nieingerujące w dokument źródłowy.

W ramach komentarza – zasadniczo zawsze zmodyfikowanie zawartości elementu (dodanie/usunięcie elementu) wymusza przebudowanie jego reprezentacji (istotne zwłaszcza przy ograniczeniach liczby węzłów podrzędnych). Prywatnie nawet przy zmianie pozycji węzłów (w góre/w dół) podmieniam HTML’a całości zestawu (bo w xsl’u robi się samo). Mimo, że przy takiej konstrukcji HTML’a można to zrobić js’em (sprawdzając krótką pętlą czy wcześniej/później jest tag z klasą node).

Prrrrrosze (to z Misia)

03/3.js 03/3.xsl 03/3.xml 03/3.htm 03/3.css

Po dodaniu przeliczania pól zależnych, miejsca na podpisy wystawiającego i odbierającego, węzła z formą płatności (da się zrobić słownie xml’em), osobnego css’a dla media=print można wystawiać faktury J

Oczywiście nie musimy robić tego wszystkiego na kliencie! Po pierwszej fascynacji technologią przychodzi wniosek, że może jednak lepiej te cuda na serwerze czynić a do przeglądarki wysyłać HTML’a. Wtedy przerabiamy nieco skrypt, część zostawiamy w przeglądarce, część wykonujemy na serwerze (błogosławieni korzystający z IIS’a – praktycznie nic nie trzeba przepisywać...). Komunikacja robiącym ostatnio furrorę xmlHttpRequest.

A w IE jest to od wersji 5.0 czyli już ubiegłym tysiącleciu można było szaleć... Sensownie zaczęło działać w IE 5.5 wraz z pojawieniem się msxml2, którego ma obecnie wersję 4.0, szybszą od 2.0/3.0 (niewielkie różnice) 3-4 razy. Jeżeli ktoś nie ma serwerze 4-ki na serwerze niech w te pędy po nią leci.

Zauważmy, że nasza transformacja wie coraz mniej o biznesowym sensie dokumentu. Coraz więcej informacji wyprowadzamy na zewnątrz. Podążając nieuchronnie w kierunku: głupiej transformacji, o której w następnym odcinku.

Ładnie wygląda pierwszy odcinek w pokolorowanym HTML'u?
Pomóż przerobić bieżący!
Wdzieczność gwarantowana:-)
UWAGA: To jest robocza wersja dokumentu i jego załączników. Udostępniona zasadniczo w celu testowania oraz zgłaszania uwag i spostrzeżeń.

Zasady korzystania:

  1. Do celów edukacyjnych, niezwiązanych z wykonywaną pracą zawodową (na przykład uczniowie, studenci i innihobbyści nie zarabiający pieniędzy szeroko rozumianą działalnością informatyczną) – na własne potrzebybezpłatnie i bez ograniczeń, ale w przypadku osiągania dochodów prośba o rozważenie skorzystania zasad zawartych w punkcie 2a.
  2. Do celów edukacyjnych związanych bezpośrednio z wykonywaną pracą/sposobem osiągania przychodów (na przykładprogramiści i koderzy) – na własne potrzeby (w tym wykonywania pracy zawodowej/osiągania przychodów) bez ograniczeń po spełnieniu następujących wymagań:
    1. Wsparcie kwotą jaką korzystający uzna za słuszną:
      Fundacja SERCE – Europejskie Centrum Przyjaźni Dziecięcej
      Organizacja Pożytku Publicznego
      58-100 Świdnica, ul. Kościelna 15
      numer konta: 42 1020 5138 0000 9102 0067 8961
      tytuł wpłaty: darowiznana rzecz organizacji pożytku publicznego
      Będzie mi miło jeżeli dostanę na maila (skompresowany:-) skan/zrzut z ekranu z informacją o wpłacie.
    2. W przypadku bezpośredniego korzystania z udostępnionego kodu (w tym zmodyfikowanego, bez ingerencji w istotęrozwiązań) proszę o zamieszczenie w kodzie informacji o kursie z jego adresem: http://szomiz.republika.pl/.
    3. Przy wysyłaniu do przeglądarki wyników działania kodu wykonanego na serwerze zamieszczenie informacji okorzystaniu z kursu w tagu META.
    4. Poinformowanie mnie (jeżeli nie zabraniają tego zobowiązania wobec osób trzecich) o projektach i rozwiązaniachw których kod udostępniony w kursie został wykorzystany.
  3. Do wszelkich innych celów, zwłaszcza związanych z wykonywaną/prowadzoną działalnością edukacyjna, szkoleniową,wydawniczą lub publikacyjną – wykorzystanie możliwe wyłącznie po indywidualnym określeniu zasad i warunków pomiędzy autorem kursu a korzystającym.
  4. Osoby, wnoszące swój wkład w treści zawarte w kursie udzielają autorowi kursu nieodpłatnej licencji umożliwiającej co wykorzystanie i udostępnienie na określonych tutaj zasadach.
  5. Oczywiście nie biorę żadnej odpowiedzialności za rezultaty działania kodu i jego modyfikacji :-)
© Sławomir Zimosz - wszelkie prawa zastrzeżone.
Przedruk, publikacja części lub całości bez wiedzy i zgody autora zabronione.
Kontakt: szomiz@poczta.onet.pl