Comunicare Asincrona, Browser Sniffing, XHR, Firebug
Comunicare Asincrona, Browser Sniffing, XHR, Firebug
Reminder teoretic:
AJAX=
HTML: pt structurarea paginii, de obicei prin DIV si SPAN, dar nu obligatoriu (important: recapitulaţi de
la grafică diferenţele între DIV şi SPAN!)
+ CSS: pt formatarea elementelor din pagină, inclusiv poziţionare, ascundere etc. (recapitulaţi CSS de la
grafică!)
+ JavaScript: pt. modificarea structurii HTML şi a proprietăţilor CSS, în mod dinamic, prin functii asociate
evenimentelor ce pot avea loc asupra elementelor paginii; de obicei se realizează prin manipularea
arborelui DOM dar se pot folosi si frameworkuri precum JQuery, Prototype etc.
+ comunicare asincronă cu serverul = schimb de date ce nu întrerupte activitatea curentă a acelor părţi
din pagină care nu au nevoie imediată de datele cerute de la server; se implementează prin 2 metode:
obiectul XHR sau cadrele IFRAME invizibile (pentru situaţiile care nu pot fi implementate prin XHR, cum
ar fi uploadul de fişiere, gestiunea butoanelor Back-Forward din browser etc.)
+XML: acesta e formatul standard în care serverul ar trebui să îşi împacheteze răspunsul; însă nu e
obligatoriu, în practică se preferă ca serverul să răspund cu stringuri convenabile - de ex. valori separate
prin virgulă - sau stringuri JSON – o alternativă mult mai performantă la XML – sau chiar stringuri ce pot
fi convertite în cod JavaScript – aşa numita tehnică remote scripting.
AJAX e complet independent de server şi de limbajul folosit pe server, singurul lucru care contează este
ca serverul să fie capabil să răspundă cu stringuri la cererile ce vin de la browser (orice server face asta).
Deoarece sunteţi familiarizaţi cu PHP vom folosi scripturi PHP simple pentru a genera răspunsul.
Consideraţi ca temă să modificaţi exemplele astfel încât în PHP răspunsul să fie construit cu date dintr-o
bază de date MySQL.
Următorul exemplu prezintă mecanismul de bază al comunicării asincrone.
Se creează un formular ce solicită adresa poştală a utilizatorului. Imediat după ce codul poştal e tastat, la
momentul trecerii în următoarea casetă, codul poştal e trimis la server, care răspunde cu strada şi
celelalte detalii pe care le are stocate despre acel cod poştal.
<html>
<head>
<title>Formular</title>
<script type="text/javascript">
Crearea obiectului XHR
xhr.send(null)
}
Browser sniffing:
catch (e)
{xhr=false}
if (!xhr&&typeof(XMLHttpRequest)!='undefined')
xhr=new XMLHttpRequest()
return xhr
Testarea răspunsului:
- dacă răspunsul a sosit integral (readyState 4) si dacă e
răspuns normal (status 200)
Obs: nu legati cu && in acelasi IF! trebuie testate pe
rand, caci in unele browsere atributul status se creeaza
abia cand readyState devine 4, inainte de asta va da
eroare de variabila nedefinita (daca serverul intarzie)
function procesare()
if (xhr.readyState == 4)
if (xhr.status == 200)
Prelucrarea raspunsului:
{
- Raspunsul e asteptat ca text brut
raspuns=xhr.responseText cu 3 valori delimitate de virgula
(split sparge stringul intr-un
date=raspuns.split(',')
vector de 3 elemente, folosind
document.getElementById("TXoras").value=date[0] virgula ca separator)
else
document.getElementById("Eroare").innerHTML =codHTMLeroare
</script>
<h1>Introduceti datele</h1>
<table>
<tr>
Campul ce va
declansa
<td>Nume</td>
schimbul de date
<td><input type="text" id="TXnume" ></td> la momentul
ONBLUR
</tr>
<tr>
<td>CodPostal</td>
</tr>
<tr>
<td>Adresa</td>
</tr>
<tr>
Campurile ce vor fi
<td>Oras</td> completate cu
raspunsul serverului
<td><input type="text" id="TXoras" ></td>
(vezi IDurile cu care
sunt accesate din
</tr>
functia de procesare)
<tr>
<td>Judet</td>
</tr>
<tr>
</tr>
</table>
Paragraf gol, rezervat pentru mesajul de eroare
</form> (O variantă ar fi ca paragraful să conţină de la început
mesajul de eroare, dar să fie ascuns cu proprietatea CSS
"display:none")
<p id="Eroare"></p>
</body>
</html>
Scriptul server
(salvat in asa fel incat sa fie gasit de obiectul XHR de mai sus: cu numele codpostal.php in htdocs):
<?php
if ($_GET["CP"]=="400451")
else
?>
După cum indică scriptul server, acesta poate raspunde cu date corecte doar la codul 400451! In
rest, se returneaza "cod incorect" (intr-un scenariu realist, ar exista o baza de date cu toate
codurile si strazile/orasele asociate lor)
Verificati si inserarea erorii in pagina (prin oprirea serverului ceea ce va afecta codurile de
raspuns 4/200)
Urmariti consola Firebug pentru a monitoriza datele schimbate intre client si server
Fiecare transfer XHR apare in rubrica Console prin indicativul GET urmat de URLul contactat. Daca dati click pe
un transfer GET apar rubricile Params (datele trimise), Response (raspunsul serverului), Headers (antetul HTTP
integral). Aceleasi informatii se pot obtine si de la rubrica Net, dar aceea e mai aglomerata, caci afiseaza TOATE
transferurile (inclusiv imaginile, foile de stil si alte fisiere ce vin de la server). Rubrica Net o folositi cand vreti sa
testati transferul prin cadre invizibile (care nu se reflecta in consola, caci nu se fac prin XHR!).
Verificati si rubrica HTML, care va desena arborele DOM; pentru fiecare element selectat din DOM, in dreapta
ecranului apare rubrica DOM cu lista cu proprietatile obiectuale ale elementului (nodeName, nodeType,
childrenNodes, firstChild, firstElementChild, etc.)
Obs1: Firebug e disponibil doar in Firefox. Pentru Internet Explorer se pot folosi produse gratuite
similare, precum Fiddler.
De ce nu se pun paranteze la numele functiei? Daca s-ar pune, functia s-ar executa automat cand
scriptul executa acea linie si nu ar mai astepta pana la aparitia evenimentului!
Detalii: in JavaScript exista notiunea de "variabila de tip functie", adica o variabila a carei valoare e
corpul unei functii (si nu valoarea returnata!). Cu ajutorul variabilelor de tip functie se pot realiza
diverse trucuri de programare:
In principiu, cand vedeti o functie fara paranteze (ex: procesare in loc de procesare()) inseamna ca e
vorba de o variabila ce contine corpul functiei si nu de valoarea returnata)
EX:
a=f
a=function {……………}
(functiei nu i se mai da nume dupa cuvantul function! vom spune ca functia de la dreapta egalului este
o functie ANONIMA; metoda se practica frecvent la functiile asociate evenimentele: evenimentului I se
atribuie direct corpul functiei, fara ca aceasta sa fi fost definita inainte – asta presupune ca nu dorim
ca acea functie sa o mai apelam si altfel decat prin acel eveniment (in caz contrar, trebuie sa-I dam
nume)
Obs3:
- spre deosebire de siteurile clasice, datele trimise prin GET nu se mai vad in bara de adresa;
- erorile din scriptul server nu se mai manifesta printr-un mesaj de eroare afisat in browser (de genul
syntax error on line x) – mesajul e stocat in obiectul XHR (putem sa-l inseram in pagina).
Sarcina de lucru: Sa se extinda scriptul pentru a cauta codul postal intr-o baza de date a codurilor
postale (cu 5 inregistrari).
Următorul este un exemplu de trimitere a unui formular integral prin metoda POST, folosind XHR:
<html>
<script type="text/javascript">
function creareXHR()
try
catch (e)
catch (e)
{ xhr = false }
Extragerea
function trimite() explicită a
datelor din
{ formular
creareXHR()
valori=new Object()
valori["nume"]=document.getElementById("TXnume").value
valori["codpostal"]=document.getElementById("TXcodpostal").value
valori["adresa"]=document.getElementById("TXadresa").value
Concatenarea
valori["oras"]=document.getElementById("TXoras").value datelor din
formular într-un
valori["judet"]=document.getElementById("TXjudet").value QueryString cu
conversie de
sir="" caractere
nepermise
for (cheie in valori)
sir=sir+cheie+"="+encodeURIComponent(valori[cheie])+"&"
} Setarea antetului
HTTP pentru
xhr.open("POST","raspuns.php") trimiterea de
formulare
xhr.setRequestHeader ("Content-Type", "application/x-www-form-urlencoded")
xhr.onreadystatechange=procesare
xhr.send(sir)
function procesare()
if (xhr.readyState!=4) return
alert(xhr.responseText)
</script>
</head> Formularul NU
are METHOD,
ACTION si buton
SUBMIT! Rolul
acestora e
preluat de XHR
<body>
<h1>Introduceti datele</h1>
<form>
<table>
<tr>
<td>Nume</td>
</tr>
<tr>
<td>CodPostal</td>
</tr>
<tr>
<td>Adresa</td>
</tr>
<tr>
<td>Oras</td>
</tr>
<tr>
<td>Judet</td>
</tr>
<tr>
<td></td>
</tr>
</table>
</form>
<div id="rasp"></div>
</body>
</html>
Următorul exemplu arată cum trebuie procedat când aceeaşi pagină trebuie să realizeze mai multe
comunicări asincrone în timpul utilizării sale. În general e valabil acest lucru, deoarece paginile Web
complexe suportă diverse evenimente şi fiecare eveniment poate declanşa comunicări asincrone cu
diverse scripturi server - spre deosebire de siteurile clasice unde un formular poate primi răspunsuri
doar de la o pagină PHP (pe de altă parte, acea pagină poate implica mai multe pagini folosind require şi
include; aici însă acelaşi script JavaScript poate dialoga cu mai multe scripturi PHP prin ACELAŞI obiect
XHR).
Pagina de mai jos comunică cu 2 scripturi PHP şi pentru fiecare din acestea alocă o altă funcţie de
procesare a răspunsului (chiar dacă acestea sunt aproape identice – în cazuri realiste acestea diferă
semnificativ).
<html>
Funcţia de
<head> iniţializare
creează obiectul
<title>Formular</title> XHR şi pregăteşte
DIVul în care se
<script type="text/javascript"> va insera
răspunsul
serverului
function initializari()
tinta=document.getElementById('d')
catch (e)
xhr=new XMLHttpRequest()
}
function dialogscript1()
xhr.onreadystatechange=procesare1
xhr.open("GET","script1.php")
xhr.send(null)
function dialogscript2()
xhr.onreadystatechange=procesare2
xhr.open("GET","script2.php")
xhr.send(null)
Funcţiile care
configurează
obiectul XHR
function procesare1() pentru
comunicare cu 2
{ scripturi diferite,
şi 2 funcţii de
if (xhr.readyState == 4)
procesare diferite
if (xhr.status == 200)
{
raspuns=xhr.responseText
tinta.innerHTML=raspuns
else
function procesare2()
if (xhr.readyState == 4)
if (xhr.status == 200)
raspuns=xhr.responseText
tinta.innerHTML=raspuns
else
</script>
</head>
<body onload="initializari()">
</body>
</html>
Observaţie importantă: când ştim că pagina HTML va dialoga de mai multe ori, chiar cu mai multe
scripturi server, NU are rost să creăm obiectul XHR de FIECARE dată.
Ar trebui să ştiţi (de la grafică) că JavaScript are particularitatea că variabilele create de o funcţie rămân
accesibile şi celorlalte funcţii, nu se distrug.
Asta înseamnă că la onload putem apela o funcţie de iniţializare care să creeze obiectul XHR şi orice alte
variabile vor fi necesare în continuare, apoi evenimentele din pagină să apeleze funcţii ce folosesc
obiectele existente, fără să le mai creeze încă o dată!
In cazul de fata functia initializari creează obiectul XHR si obiectul tinta (unde se va insera raspunsul),
apoi functiile dialogscript1 si dialogscript2 (care schimba date cu scripturi diferite) reutilizeaza obiectele
existente.
Alta obs. importanta: scripturile PHP trebuie sa vina de pe ACELASI server ca si pagina HTML. Nici
obiectul XHR, nici cadrele invizibile nu pot primi date de la alte servere decat cel pe care se afla fisierul
ce le contine (de fapt cadrele invizibile pot, dar pagina principală nu are voie să acceseze conţinutul
lor!)
Alta obs: Exemplul aici prezentat presupune că cele două dialoguri nu au loc deodată. Dacă a doua
cerere XHR începe înainte să se fi terminat prima, răspunsul primeia se PIERDE! Asta înseamnă că
trebuie să folosim o variabilă flag care să indice dacă obiectul XHR s-a eliberat:
In cazul de faţă e vorba ca după citirea unui răspuns flagul să fie comutat pentru a indica eliberarea
obiectului XHR:
raspuns=xhr.responseText
xhrliber=true
Apoi înaintea fiecărui apel XHR se testează acest lucru şi dacă XHRul nu este liber, se creează unul nou:
if (xhrliber)
xhr.open(…)
xhr.send(…)
else
creeazaxhr()
Eventual, se creează ambele obiecte XHR în funcţia de iniţializare, împreună cu câte un flag pentru
fiecare, apoi la iniţierea fiecărei comunicări asincrone se testează mai întâi care XHR e liber.
De obicei se lucrează cu 2 obiecte XHR care preiau alternativ sarcinile de comunicare asincronă, cu
posibilitatea de a realiza schimburi paralele cu serverul. E recomandat să nu ne bazăm pe mai mult de 2
obiecte XHR transferând date în paralel, căci unele browsere nu suportă mai mult de 2 transferuri
simultane spre acelaşi server (recent Firefox a mărit la 6 această limită).
Lab2
Urmatorul exemplu numara cate cereri HTTP se fac dinspre browser spre server, in sesiunea curenta.
Pagina client:
<html>
<head>
<script type="text/javascript">
var xhr
function modifica()
try
{
} catch (e)
try
} catch (e)
Browser sniffing pt IE si Mozilla
{
xhr = false
}
Browser sniffing pt IE si Mozilla
Trimiterea variabilelor a si b spre
script.php
if (!xhr && typeof XMLHttpRequest !="undefined")
xhr.open("GET","numaratoare.php?a=1&b=2")
if (xhr.readyState != 4) return;
document.getElementById("mesaj").innerHTML+="<br>"+ xhr.responseText
}
xhr.send(null)
Daca raspunsul e ok (readystate=4)
} se adauga raspunsul la continutul
existent al DIVului "mesaj" (pe un
</script>
DIV gol rezervat pt raspunsul rand nou, vezi <br>-ul)
</head> serverului
Obs: testul de status 200 lipseste, pt
a scurta exemplul
<body>
<div id="mesaj"></div>
</body>
</html>
Declansarea comunicarii asincrone
- e specificat evenimentul
(MouseOver)
- e specificata functia ce instantiaza
XHR si se ocupa de schimbul de date
Scriptul server (trebuie sa aiba numele numaratoare.php si sa se afle in acelasi folder cu pagina client,
de exemplu htdocs!)
<?php
session_start();
if (isset($_SESSION["a"])) //se verifica daca exista variabila in sesiune; daca da, se incrementeaza
$_SESSION["a"]++;
Observatii:
Scriptul numara dialogurile care au loc intre client si server (la primul schimb creeaza o variabila
in sesiune, apoi o tot mareste la fiecare cerere HTTP);
Pe langa textul care indica numarul conectarii asincrone, se returneaza si datele sosite la server,
citite direct din $_SERVER["QUERY_STRING"] (puteau fi luate si una cate una din $_GET["a"] sau
$_GET["b"]);
Atentie, numaratoarea continua chiar si dupa Refresh scriptului (datorita sesiunii), dar si dupa
restartarea browserului! Si chiar dupa restartarea Apacheului!
Cum e posibil asta in conditiile in care variabilele sesiune sunt in mod implicit stocate pe server si
sesiunea se distruge automat la inchiderea browserului sau oprirea serverului?
Versiunile recente ale browserelor au implementat un mecanism automat de a transfera sesiunea intr-
un cookie, la momentul opririi browserului. La repornirea browserului, acesta isi recupereaza sesiunea
din cookie! Evident ca nici resetarea serverului nu afecteaza acest proces.
- sa punem in codul PHP comanda session_destroy() dupa un anumit numar de schimburi de date;
- sa stergem manual cookieul in care browserul transfera continutul sesiunii (cookieul creat de localhost,
in Firefox, cu Privacy – remove individual cookies – search dupa localhost si Remove Cookie cand e gasit);
- sa dezactivam din browserul optiunea de salvare automata a sesiunii (in Firefox, General – When
Firefox Starts show my windows and tabs from last time se va inlocui cu Show a blank page).
Practic, dacă nu luăm nici una din aceste măsuri, nu mai este nici o diferenţă (de comportament) între
sesiune şi cookie.
Urmăriți consola Firebug în timpul transferului:
<?php
session_start();
if (isset($_COOKIE["b"]))
setcookie("b",$_COOKIE["b"]+1,time()+3600);
else
setcookie("b",1,time()+3600);
print "<b>Aceasta e prima conectare a acestui calculator<b><br>";
if (isset($_SESSION["a"]))
$_SESSION["a"]++;
else
$_SESSION["a"]=1;
if ($_SESSION["a"]==10)
session_destroy();
?>
De data aceasta se fac 2 numărători în paralel: una prin cookie, care măsoară de câte ori a revenit
acelaşi calculator; una prin sesiune, care măsoară dialogurile din sesiunea curentă. Sesiunea e resetată
automat la 10.
<html>
Modificarea dinamica a atributului SRC
<head> al cadrului, pentru ca acesta sa
acceseze scriptul server vizat. URLul
<script type="text/javascript"> scriptului are atasate date de tip GET,
deci modificarea lui SRC are ca efect si
function trimite(cod) trimiterea datelor!
cadru=document.getElementById("cadruint")
cadru.src="codpostal.php?CodPostal="+cod
}
function citestedate(cdr)
documentcadru=cdr.contentWindow.document
function procesare(rasp)
document.getElementById("TXjudet").value=vector[1]
document.getElementById("TXadresa").value=vector[2]
</script>
</head>
<body>
<td>Nume</td>
</tr>
<tr>
<td>CodPostal</td>
</tr>
<tr>
Declansarea transferului prin
<td>Adresa</td> evenimentul Blur
</tr>
<tr>
<td>Oras</td>
</tr>
<tr>
<td>Judet</td>
</tr>
<tr>
<td></td>
</tr>
</table>
</form>
</body>
</html>
<?php
if ($_GET["CodPostal"]==400451)
else
?>
A se observa:
cadrul nu a fost facut invizibil (pentru a vedea in clar raspunsul serverului);
in Firebug nu se mai poate folosi rubrica Console (aceasta monitorizeaza doar obiectul XHR) dar se poate folosi
rubrica Net (care monitorizeaza traficul dintre client si server, oferind toate detaliile schimbului de date)
o onLoad (aplicat cadrului, pentru a apela functia de procesare a raspunsului atunci cand raspunsul a
sosit); in varianta XHR, partea aceasta era inlocuita de handlerul onReadyStateChange;
Pagina client:
<html>
<head>
<script type="text/javascript">
function trimite(cod)
Trimiterea datelor, prin manevrarea
{
atributului SRC al cadrului
cadru=document.getElementById("cadruint")
cadru.src="raspunspush.php?CodPostal="+cod
function procesare(rasp)
{
Procesarea raspunsului (desi pagina
vector=rasp.split(",") client nu contine nici un apel al acestei
functii!)
document.getElementById("TXoras").value=vector[0]
document.getElementById("TXjudet").value=vector[1]
document.getElementById("TXadresa").value=vector[2]
</script>
Cadrul (fara onLoad si fara functie de
</head> extragere a datelor!)
<body>
</iframe>
<h1>Introduceti datele</h1>
<form >
<table>
<tr>
<td>Nume</td>
</tr>
<tr>
<td>CodPostal</td>
</tr>
<tr>
<td>Adresa</td>
</tr>
<tr>
<td>Oras</td>
</tr>
<tr>
<td>Judet</td>
</tr>
<tr>
<td></td>
</tr>
</table>
</form>
</body>
</html>
<?php
if ($_GET["CodPostal"]==400451)
else
print "<html><head><script>
function impingedate()
continutcadru=document.body.innerHTML
parent.procesare(continutcadru)
}
Liniile cu rosu sunt nucleul tehnicii
</script> push: codul generat pentru a umple
cadrul se ocupa:
</head> - de impingerea datelor spre pagina
parinte la momentul onLoad (la sosirea
<body onload=impingedate()>" datelor in cadru)
- de apelarea functiei de procesare din
.$raspuns.
pagina parinte
"</body></html>";
?>
Observatii:
in pagina client nu exista nici un eveniment si nici un apel explicit al functiei de procesare a
raspunsului (procesare()); deoarece procesarea e declansata prin remote scripting (JavaScript
generat din PHP!)
serverul nu trimite doar date, ci si scriptul client (JavaScript) ce se ocupa de impingerea datelor
pe care le insoteste inspre pagina principala!
Lab3
Următorul exemplu rezolvă din nou scenariul cu codul poştal, de data aceasta folosind mecanismul
Ajax.Request şi clasa Element, oferite de frameworkul Prototype.
Pentru aceasta, downloadați mai întâi frameworkul Scriptaculous (care conține și Prototype în folderul
lib): http://script.aculo.us/downloads. Dați folderului numele scriptaculous și copiați-l în htdocs.
<html>
Acum eroarea este
inserată și stilizată cu
ajutorul clasei Element!!
(data trecută am făcut-o
cu inserare de cod,
folosind innerHTML)
<head>
</script>
<script>
function proceroare(obiectxhr)
Element.update("Eroare","Mesaj eroare:"+obiectxhr.responseText)
Element.setStyle("Eroare",{color:'green'})
function procesareraspuns(obiectxhr)
{
$ este o versiune optimizată a lui
document.getElementById()
camp.setValue(vector[i])
function date(cod)
{
configurari={method:"get",parameters:"CP="+cod,onSuccess:procesareraspuns,onFailure:proceroare}
new Ajax.Request("codpostal.php",configurari)
}
Configurarea XHR e realizată în sintaxa JSON, cu
cele 2 evenimente, onSuccess şi onFailure,
pentru care s-au pregătit funcţii.
</script>
</head>
<body>
<h1>Introduceti datele</h1>
<table>
<tr>
<td>Nume</td>
</tr>
<tr>
<td>CodPostal</td>
</tr>
<tr>
<td>Adresa</td>
</tr>
<tr>
<td>Oras</td>
<td><input type=text id=desubstituit1></td>
</tr>
<tr>
<td>Judet</td>
</tr>
<tr>
<td></td>
</tr>
</table>
câmpurile vizate au primit IDuri similare, care să
</form> poată fi parcurse printr-un ciclu FOR
<p id=Eroare></p>
</body>
</html>
Următorul exemplu e similar, dar foloseşte alte facilităţi Prototype: o funcţie iteratoare, $$, generarea
erorii cu Element.insert.
<html>
<head>
Funcția ce se va aplica fiecărui
<script type="text/javascript" src="scriptaculous/lib/prototype.js"> element dintr-un vector (vectorul
INPUT-urilor în care inserăm
</script> valori).
<script>
function substituie(x,pozitie)
function proceroare(obiectxhr)
vector=raspuns.split(',')
desubstituit=$$('.desubstituit')
desubstituit.each(substituie)
function date(cod)
config={method:"get",parameters:"CP="+cod,onSuccess:procesareraspuns,onFailure:proceroare}
new Ajax.Request("codpostal.php",config)
}
Acum formularul are ID,
</script> necesar la
Element.insert, pentru a
</head> şti DUPĂ ce element să
se facă inserarea!
<body>
<h1>Introduceti datele</h1>
<table>
<tr>
<td>Nume</td>
</tr>
<tr>
<td>CodPostal</td>
</tr>
<tr>
<td>Adresa</td>
</tr>
<tr>
<td>Oras</td>
</tr>
<tr>
<td>Judet</td>
</tr>
<tr>
<td></td>
Nu am mai rezervat un
<td><input type=submit value=Trimite ></td> DIV pentru inserarea
erorii, el este CREAT de
</tr> Element.insert (în timp
ce Element.update îi
</table> substituia doar
conţinutul!)
</form>
</body>
</html>
Pagina client
<html>
<head>
</script>
<script type="text/javascript">
function proceroare(obiectxhr)
{
document.getElementById("Eroare").innerHTML="<font
color=red>Eroare"+obiectxhr.responseText+"</font>"
Marcatorul al carui continut se
}
substituie
function date(cod)
new Ajax.Updater("t1","sursa.php",
method: "get",
parameters: "CodPostal="+cod,
onFailure: proceroare
})
}
Fara onSuccess! Continutul e inserat
automat!
<body>
<h1>Introduceti datele</h1>
<form>
<table id=t1>
<tr>
<td>Nume</td>
</tr>
<tr>
<td>CodPostal</td>
</tr>
<tr>
<td>Adresa</td>
</tr>
<tr>
<td>Oras</td>
</tr>
<tr>
<td>Judet</td>
</tr>
<tr>
<td></td>
</tr>
</table>
</form>
<p id=Eroare></p>
</body>
</html>
if ($_GET["CodPostal"]==400451)
$oras='Cluj Napoca';
$judet='Cluj';
else
$oras='cod incorect';
$judet='cod incorect';
$adresa='cod incorect';
}
Serverul rescrie integral conţinutul
$cod=$_GET["CodPostal"]; tabelului, cu noile date inserate
(parţial redundant!)
print "<tr>
<td>Nume</td>
</tr>
<tr>
<td>CodPostal</td>
</tr>
<tr>
<td>Adresa</td>
</tr>
<tr>
<td>Oras</td>
</tr>
<tr>
<td>Judet</td>
</tr>
<tr>
<td></td>
</tr>";
?>
Obs:
2. Exemplul este ineficient deoarece realizeaza refresh redundant (se reincarca tot continutul tabelului,
inclusiv marcatori care exista deja in pagina client!)
De ce această redundanţă? Pentru că nu putem substitui doar câteva rânduri de tabel! Ele
ar trebui să fie cuprinse într-un DIV pe care să-l folosim ca ţintă pentru Updater dar…
marcatorul TABLE avea ca noduri fiu DIVuri (doar TR!) – orice nod-fiu nepermis al lui TABLE
este împins în afara tabelului!
Sarcina: Modificati exemplul pentru a minimiza refreshul redundant! (sa vina de la server doar cele 3
campuri ce se actualizeaza):
Variante de soluţii:
1. executăm de câte un Ajax.Updater pentru fiecare celulă (TD) al cărei conţinut se modifică;
2. nu mai încadrăm formularul într-un tabel, ci într-o structură de DIVuri care să arate ca un tabel
(astfel încât să putem defini un DIV convenabil care să substituie doar câmpurile ce suferă
modificări)
3. celulele ale căror câmpuri se substituie se grupează într-o singură celulă (ROWSPAN) pe care o
folosim ca ţintă (îi dăm ID) pentru Updater.
Exemplu PeriodicalUpdater
<html>
<head>
</script>
<script>
function initializare()
config={method:"get",frequency:1,decay:2}
perup=new Ajax.PeriodicalUpdater("scor","sursascoruri.php",config)
}
</script>
</head>
<body onload="initializare()">
</body>
</html>
<?php
?>
Mod de utilizare: - la încărcarea paginii încep updateurile periodice (puteţi să le urmăriţi în Firebug). Vor
deveni tot mai rare datorită decayului de 2, care dublează intervalul dintre două updateuri.
La un moment dat intraţi în scriptul server şi scrieţi 1-0 în loc de 0-0. După câteva secunde, noul scor
apare în pagină şi decayul se resetează (cererile devin din nou dese).
<html>
<head>
</script>
<script>
function finalizare()
function confirmare()
function initializare()
config={method:"get",frequency:1,decay:2,onSuccess:confirmare,onComplete:finalizare}
perup=new Ajax.PeriodicalUpdater("scor","sursascoruri.php",config)
}
</script>
</head>
<body onload="initializare()">
</body>
</html>
Lab 4
Scriptaculous, pe lângă că oferă funcția de clonare, se și bazează pe aceasta în alte funcții, cum ar fi cele
legate de Autocompleter (pentru poziționarea listei de sugestii imediat sub textboxul în care se face
căutarea).
În general clonarea spațială se folosește în 3 moduri:
pentru a plasa un element în dreptul altuia (lângă, dedesubt, deasupra etc.) prin clonare parțială
(cu un anumit decalaj, limitat la o axă);
paginile să se creeze cu o declarație <!DOCTYPE html> pe prima linie, ceea ce face browserele să
intre în modul “standard” și să dea rezultate mult mai uniforme (dimensionarea și poziționarea
sunt printre operațiile cele mai afectate de diferențele dintre browsere, iar prezența unei
declarații DOCTYPE face să dispară mare parte din diferențe);
funcția clonePosition să fie folosită pe elemente fără border și padding, altfel apar diverse
decalaje ce pot necesita ajustări manuale.
Pentru a sesiza mai clar efectele, vom crea 2 DIVuri de dimensiuni și culori diferite, cu chenare pentru a
le putea vedea clar limitele.
<!DOCTYPE html>
<html>
<head>
<style>
</style>
function cloneaza()
Element.clonePosition('d2','d1')
</script>
</head>
<body>
Text oarecare
</body>
</html>
În această formă de bază, clonePosition nu face decât să cloneze dimensiunile: al doilea DIV se mărește
până la dimensiunile primului (aproximativ, căci apare o abatere dată de grosimea chenarului celui de-al
2-lea DIV; dacă ar avea și un padding, abaterea ar fi chiar mai mare – ne vom ocupa mai târziu de
eliminarea manuală a acestei abateri).
Pentru a clona atât dimensiunile cât și poziția, e necesar ca elementul care primește noua
poziție să fie scos din fluxul natural al textului, deci să fie definit cu poziționare ABSOLUTĂ sau
FIXĂ!
Poate fi definit și cu poziționare RELATIVĂ, care se face relativ la poziția curentă (deci e utilă
dacă vrem să mutăm un element sincron cu alt element, dar păstrând distanța relativă dintre
ele).
Avertisment: prin poziționare absolută, DIVul este scos din fluxul natural al textului și locul pe care îl
ocupa este invadat (aici de „Text oarecare”, care urmează sub div). DIVul este mutat la coordonatele
precizate unde va acoperi eventualul conținut din zona respectivă. De aceea elementele cu poziționare
absolută de obicei sunt elemente ce apar temporar peste conținutul paginii (deci au un background
definit, de ex. casete de dialog cu afișare temporară). Uneori sunt inițial ascunse și sunt făcute vizibile din
când în când pentru a afișa diverse mesaje sau conținut. În general elementele care se mișcă mult în
pagină se definesc cu poziționare absolută pentru a nu fi influențate de restul paginii.
În urma poziționării absolute, se vede că butonul de clonare aplică acum și clonare de poziție dar... cu o
abatere neplăcută.
Această abatere e prezentă din cauză că browserele impun implicit o bordură pe marginea paginii, de
care clonarea poziției nu ține cont. Soluția pentru evitarea abaterii este una dintre următoarele:
Clonarea parțială
Mai frecventă decât clonarea totală a poziției, este necesitatea de a poziționa un element în dreptul
altuia. Pentru aceasta, clonePosition ne permite să impunem un decalaj suplimentar:
Element.clonePosition('d2','d1',{offsetLeft:200})
... aplicând un decalaj al marginii stângi cu o valoare egală cu lățimea primului DIV, al doilea va fi
poziționat la dreapta primului
Element.clonePosition('d2','d1',{offsetTop:100})
...aplicând un decalaj al marginii de sus cu o valoare egală cu înălțimea primului DIV, al doilea va fi
poziționat dedesubt
Obs:
Dacă trebuie neapărat să avem chenare vizibile și micile lor suprapuneri sunt supărătoare, vom
adăuga și grosimea chenarelor la decalaj: 204, respectiv 104 în aceste exemple (dublul grosimii
lui border); la fel, dacă primul DIV are padding, îl vom adăuga și pe acesta la decalaj (nu și
bordura margin, care nu e inclusă în dimensiunea elementelor!).
Alte opțiuni:
Element.clonePosition('d2','d1',{setTop:false})
Element.clonePosition('d2','d1',{setLeft:false})
Element.clonePosition('d2','d1',{setWidth:false})
Element.clonePosition('d2','d1',{setHeight:false})
Combinând aceste opțiuni, putem clona fie doar dimensiunile, fie doar poziția, fie diverse combinații
între ele limitate la axa X sau axa Y.
E mai eficient dacă realizăm acest calcul tot prin programare, mai ales dacă există șanse ca aceste valori
să se schimbe dinamic.
De data aceasta clonarea cu offset de 200 sau 100 va fi mult mai deranjantă, căci trebuie să adunăm și
grosimea paddingului. Pentru a ne scuti de astfel de calcule, Prototype ne oferă funcțiile
getWidth/getHeight care ne returnează dimensiunile calculate ale elementelor (incluzând border și
padding!)
Modificăm funcția de clonare pentru a utiliza dimensiunile calculate în locul celor declarate:
function cloneaza()
latime=Element.getWidth('d1')
Element.clonePosition('d2','d1',{offsetLeft:latime})
Dacă în acest caz abaterile s-au rezolvat simplu, în cazul clonării de dimensiune, suntem nevoiți să facem
ajustări mai detaliate, căci clonePosition nu ne oferă opțiune de aplicare a unei abateri de dimensiune și
trebuie să manipulăm direct CSSul.
Adăugați un padding și la al doilea DIV,ceea ce va face ca abaterea de dimensiune să fie și mai puternică:
#d2 {width:100px;height:50px;color:blue;
Mai exact, abaterea este 2*(border+padding), deci 14 pixeli. Deci dacă vrem ca al doilea DIV să
primească exact dimensiunile primului, indiferent dacă are border și padding, vom aplica o ajustare
manuală. Ajustarea este indicată prin comentarii în codul funcției de clonare (a doua parte a funcției):
function cloneaza()
latime=Element.getWidth('d1')
Element.clonePosition('d2','d1',{offsetLeft:latime})
inaltime1=Element.getHeight('d1')
chenar=Element.getStyle('d2','border-top-width')
pad=Element.getStyle('d2','padding-top')
//functia parseInt e necesara caci functiile de mai sus returneaza valorile ca string, nu ca numere
inaltime2=parseInt(inaltime1)-2*parseInt(chenar)-2*parseInt(pad)
Element.setStyle('d2',{height:inaltime2+'px'})
}
Autocompleter
<!DOCTYPE html>
<html>
<head>
<style>
div#sugestii
div#sugestii ul
div#sugestii ul li.selected
{background-color:#ffb;}
div#sugestii ul li
</head>
<body>
Caseta de cautare:<br/>
<div id="sugestii">
<ul>
</ul>
</div>
</body>
</html>
Am creat un textbox cu IDul tbox (în mod normal e nevoie și de un buton care să declanșeze
căutarea pe server, dar îl vom neglija căci nu dorim să implementăm căutarea efectivă ci doar
generarea sugestiilor).
Imediat după el am creat un DIV rezervat pentru afișarea sugestiilor, cu IDul “sugestii”.
Unul din elementele listei are class=selected. Și acest lucru este impus de Scriptaculous!
Scriptaculous va genera automat acest atribut pe elementul deasupra căruia se află mouseul
deci trebuie să-i pregătim o formatare distinctă de restul listei.
Verificați cum arată în acest moment pagina. În ce privește poziționarea sugestiilor imediat sub textbox,
avem două variante de lucru:
Să definim noi poziționarea (grupând textboxul și sugestiile în același DIV, să le putem manevra
ca un întreg) și să dezactivăm poziționarea automată oferită de Autocompleter.
Pasul2. Formatăm lista de sugestii pentru a putea fi afișată sub forma unui meniu.
să dispară spațiul liber din jurul listei și din fața elementelor listei (acest aspect este mai
problematic, căci e tratat diferit de browsere – la unele acest spațiu liber e controlat cu margin,
la altele cu padding, deci va trebui să punem ambele proprietăți la zero);
Toate aceste modificări se aplică în momentul în care modificăm stilul CSS al listei, astfel:
div#sugestii ul
list-style-type:none;
margin:0px;
padding:0px;
}
div#sugestii ul li
{ cursor:pointer}
În această fază creăm un script foarte simplu, care să returneze o listă oarecare, pentru a ne convinge că
mecanismul funcționează. Ulterior vom extinde scriptul pentru a returna doar elemente care încep cu
literele tastate în textbox.
<?php
?>
Aici trebuie să ne alegem evenimentul la care să se creeze mecanismul Autocompleter. De obicei acesta
se creează la evenimentul ONLOAD, pentru a fi disponibil imediat după încărcarea paginii. Alternativ, ar
putea fi generat printr-un eveniment ONFOCUS sau ONCLICK (când se intră cu mouseul în textbox) sau
prin alte evenimente.
Adăugați mai întâi liniile de apel al funcțiilor Scriptaculous și Prototype (în HEAD):
<head>
<script>
function initializare()
</script>
..............
<body onload=”initializare()”>
În plus, se poate șterge lista UL scrisă deja (era provizorie, doar cât să-i putem face stiluri; de aici încolo,
lista va fi inserată automat din sugestiile venite de la server!)
Corpul paginii va rămâne:
<body onload="initializare()">
Caseta de cautare:<br/>
<div id="sugestii"></div>
</body>
După sosirea listei UL, Autocompleter aplică o clonare de poziție, pentru a plasa lista de sugestii imediat
sub textbox! Reamintim că la clonarea de poziție poate apare o mică abatere datorată marginii implicite
pe care browserele o pun în jurul întregii pagini.
O dezactivăm cu
O altă metodă de depășire a bugului este să dezactivăm poziționarea automată oferită de Scriptaculous,
caz în care va trebui să facem manual poziționarea (să ne asigurăm că sugestiile și textboxul sunt
grupate în același DIV, unul după altul sau poziționat cu CSS). Dezactivarea clonării de poziție are loc
dacă DIVul de sugestii primește poziționare relativă (nu o faceți în acest exemplu, aplicați cealaltă
măsură):
<?php
print '<ul>';
print '<li>'.$sursadate[$i].'</li>';
print '</ul>';
?>
Mai departe, în ciclul de parcurgere a sursei de date trebuie să punem un filtru – să se extragă doar
valorile care încep cu literele tastate în textbox. Pentru aceasta, trebuie să ne asigurăm că textboxul
trimite date la server, adăugându-i un atribut NAME:
Apoi, să capturăm această valoare la server (nu avem nevoie de Submit, Autocompleterul se ocupă de
trimiterea asincronă a conținutului din textbox, după fiecare tastă apăsată sau cu o frecvență pe care o
putem modifica prin opțiunile suplimentare de la instanțierea Autocompleter!):
$litere=$_POST['litere'];
Construim o expresie regulată care să verifice dacă un string începe cu literele sosite prin POST (case-
insensitive):
$expreg="/^".$litere."/i";
Includem în script testarea expresiei regulate pentru fiecare element din vectorul de pe server:
<?php
$litere=$_POST['litere'];
$expreg="/^".$litere."/i";
print '<ul>';
if (preg_match($expreg,$sursadate[$i])==1)
print '<li>'.$sursadate[$i].'</li>';
print '</ul>';
?>
Obs: Am avut nevoie de expresia regulată deoarece sugestiile sunt stocate într-un vector. Dacă avem
sugestiile într-o bază de date, în loc de expresia regulată vom folosi interogări cu LIKE. Asta ar însemna
să programați și căutarea efectivă.
Asta presupune:
încadrarea textboxului într-un formular, care va avea un buton Search (de tip submit) și ca
destinație un script PHP care face două lucruri:
o memorarea cuvântului căutat într-un tabel dedicat pentru cuvinte căutate; apoi, scriptul
sugestii.php va fi modificat să scoată sugestiile din acest tabel cu interogări LIKE
E posibil să dorim ca fiecare sugestie să vină cu niște informații suplimentare care să-l ajute pe utilizator
în selectarea sugestiei dorite, FĂRĂ CA informațiile suplimentare să fie inserate în textbox la momentul
deciziei.
...acesta va deveni parte din sugestie și va fi inserat în textbox, când utilizatorul își selectează sugestia
dorită. Pentru a preveni acest lucru, Autocompleter va neglija orice text din listă care apare într-un SPAN
cu CLASS=informal:
Partea din SPAN va fi afișată în lista de sugestii, dar nu va fi preluată la selectarea unei sugestii. Evident,
și acele informații suplimentare ar trebui preluate dintr-o sursă de date și nu concatenate în acest fel.
Opțiuni suplimentare:
1.Dacă estimăm că va dura mai mult căutarea sugestiilor, trebuie să oferim și un indicator grafic (un gif
animat, gen clepsidră) care să sugereze o stare de așteptare (cu indicator).
2.Dacă vrem ca schimbul de date să fie mai rar, putem preciza după câte caractere tastate să se inițieze
procesul Autocompleter (cu minChar) sau la câte secunde (cu frequency).
3. Dacă vrem să trimitem și alte date decât literele tastate, o putem face cu parameters.
4. Dacă vrem ca în același textbox să se tasteze mai multe valori separate cu un delimitator (ca la
introducerea mai multor adrese de e-mail), o putem face cu tokens.
Downloadați un gif animat sugestiv (căutați pe Google după spinner.gif). Salvați imaginea pe server cu
numele animatie.gif.
Inserați între textbox și DIVul cu sugestii imaginea. Faceți-o invizibilă și dați-i un ID:
Apoi modificați linia de creare a Autocompleterului, cu opțiunile suplimentare indicator (IDul imaginii) și
minChars (după câte caractere să apară sugestiile):
{indicator:"asteapta",minChars:2, parameters:”a=100&b=200”})'
callback – funcția se va executa în locul comunicării asincrone (deci dacă o folosim va trebui să
implementăm explicit în această funcție comunicarea asincronă cu Ajax.Request); funcția
primește obligatoriu 2 argumente: numele textboxului și datele de trimis la server;
Adăugați funcția:
function f1(elem)
document.getElementById('tbox').value=elem.innerHTML
Aceasta realizează inserarea argumentului în textbox, dar are în plus și o afișare de mesaj. Pentru a-i
vedea efectul, adăugați evenimentul updateElement la opțiunile suplimentare:
new Ajax.Autocompleter("tbox", "sugestii", "sugestii.php",
{indicator:"asteapta",minChars:2, updateElement:f1})'
Așadar, printr-o astfel de funcție putem realiza operații suplimentare la momentul selectării unei
sugestii, dar are dezavantajul că trebuie să programăm manual inserarea sugestiei în textbox. În schimb
dacă programăm o funcție pentru evenimentul afterUpdateElement putem păstra mecanismul implicit
de inserare (cu diferența că funcția acestui eveniment trebuie să aibă 2 argumente – textboxul și
elementul selectat).
function f2(camp,qstring)
Și o asociem evenimentului:
{indicator:"asteapta",minChars:2, callback:f2})'>
Funcția afișează o alertă la momentul la care AR TREBUI să se realizeze comunicarea asincronă (după
tastarea a 2 caractere). Deci dacă personalizăm o astfel de funcție, trebuie să includem în ea și codul
comunicării asincrone, precum și inserarea listei de rezultate în DIVul de sugestii. Altfel spus, folosirea
funcției callback ne obligă să programăm manual aproape tot mecanismul Autocompleter.
Autocompleter local
E posibil ca lista de sugestii să nu vină de la server, ci să fie generată la nivelul clientului. În acest caz,
funcția de inițializare poate arăta astfel:
function initializare()
lista=['Ana','Andrei','Bebe','Bogdan']
fie sugestii venite de la server pe alte căi decât prin Autocompleter (de ex: date venite ca XML,
JSON și mutate într-un vector cu ajutorul JavaScript);
fie sugestii generate direct în JavaScript (de ex. căutările făcute de un utilizator, dar memorate în
cookieurile sale în loc să fie memorate în baza de date – asta înseamnă că fiecare utilizator va
primi doar sugestii legate de ce a mai căutat el înainte, nu și cele căutate de alți utilizatori -
adesea se practică o combinație între cele 2: Google ne oferă și o listă de căutări făcute de noi
(din cookie), dar și una făcută de restul lumii).
Nu îi vom programa căutarea efectivă pe server, dar avem nevoie de el pentru a-i programa în JavaScript
crearea cookieului cu căutarea curentă:
function memoreaza()
{
//aici ar veni codul ce comunica cu scriptul server responsabil cu cautarea, pe baza de Ajax.Request
//pentru asta ne bazam pe variabila nrcookie cu care vom numara cookieurile existente
nrcookie++
numecookie='cautare'+nrcookie
valcookie='='+$F('tbox')
document.cookie=numecookie+valcookie+setaricookie
Din comentarii se vede că am folosit variabila nrcookie, care nu există. În ea intenționăm să numărăm și
să numerotăm cookieurile, de aceea, la încărcarea paginii, primul lucru care trebuie făcut este să vedem
câte cookieuri există (dacă nu există nici unul, se va inițializa cu valoarea 1)
function initializare()
//extragem cookieurile existente într-un vector – pe disc ele sunt fișiere text cu valori delimitate de ;
toatecookie=document.cookie.split(';')
//aflăm câte cookieuri există – dacă nu e nici unul, lungimea implicită va fi 1, deci nu e necesar un test
nrcookie=toatecookie.length
//trecem vectorul de cookies printr-un iterator, pentru a-i extrage valorile
lista=toatecookie.collect(extrage)
Următoarea funcție este cea care asigură extragerea valorilor (apelată de funcția iteratoare collect):
function extrage(elem)
return elem.split('=')[1]
Funcția primeșe de la iterator fiecare element al vectorului, și, bazându-se pe faptul că elementele au
forma nume=valoare, sparge perechea într-un alt vector și îi ia al doilea element (valoarea de după
egal).
Slider
Mecanismul Slider este la bază un grup de 2 DIVuri – unul lung (șina) și unul mic (mânerul care se trage
de-a lungul șinei). Pentru a simplifica poziționarea lor, ideal este ca:
fiecare să aibă un fundal sugestiv – în acest exemplu le vom colora fundalul ca să creeze
impresia de bară și mâner dar în practică li se pune pe background câte o imagine sugestivă
(PNG cu background transparent) folosind proprietatea background: url(……..).
<!DOCTYPE html>
<html>
<head>
<style>
#sina {background:red;width:200px;height:10px}
#maner {background:blue;width:10px;height:10px}
</style>
<script>
function initializeaza()
new Control.Slider("maner","sina",{})
</script>
</head>
<body onload="initializeaza()">
<div id="sina">
<div id="maner"></div>
</div>
</body>
</html>
Observați dimensiunile convenabil setate – lungime mare pentru șină, cu înălțime mică,respectiv un
pătrățel mic, egal cu înălțimea șinei, pentru mâner. Repet, în proprietatea background se pot stoca și
imagini care să facă Sliderul mai estetic.
De obicei un Slider e folosit pentru a controla o gamă de valori posibile ce influențează alte elemente din
pagină. În cazul cel mai simplu, gama de valori va fi pur și simplu afișată într-un textbox:
<input id="valori" type="text"/>
Pentru a lega Sliderul de textbox, trebuie să definim o funcție pentru evenimentul onSlide, funcție ce
primește obligatoriu valoarea curentă a Sliderului – pe care noi trebuie să o transferăm prin cod mai
departe, spre textboxul cu ID=valori:
function initializeaza()
new Control.Slider("maner","sina",{onSlide:afiseaza})
function afiseaza(valoare)
$('valori').setValue(valoare)
valorile generate de Slider sunt în intervalul 0-1 (putem modifica intervalul cu opțiunile range și
values);
afișarea valorii în textbox se face doar când tragem mânerul (drag); ar trebui să se facă
actualizarea și când dăm un simplu click undeva pe șină (pentru aceasta avem evenimentul
onChange).
Modificați sliderul:
function initializeaza()
{
new Control.Slider("maner","sina",{range:$R(10,50),onSlide:afiseaza,onChange:afiseaza})
Obs:Alături de range putem folosi și values:[10,20,30,40,50] care va face ca valorile sliderului să fie
discrete (mânerul va sări din 10 în 10, nu va putea fi poziționat oriunde în interval).
<body onload="initializeaza()">
<div id="sina">
<div id="maner"></div>
</div>
</body>
Apoi modificăm funcția de legătură cu Sliderul, adăugând operații de ajustare a mărimii de font (pt
primul paragraf) și de ajustare a poziției verticale (pentru al doilea) în funcție de valoarea curentă:
function afiseaza(valoare)
{
$('valori').setValue(valoare)
Element.setStyle('tinta1',{fontSize:valoare+'px'})
Element.setStyle('tinta2',{position:'relative',top:valoare+'px'})
Observați:
faptul că numele proprietăților CSS e puțin diferit: nu am folosit font-size, ci fontSize (deoarece e
acoladele acelea nu sunt cod CSS, ci cod JavaScript, iar în JavaScript toate proprietățile cu
cratimă în nume suferă această mică conversie);
Dacă dorim ca modificarea aplicată în pagină să nu fie EXACT modificarea Sliderului, ci o valoare
proporțională, înmulțim valoarea cu proporția dorită:
Element.setStyle('tinta1',{fontSize:valoare*0.5+'px'})
Element.setStyle('tinta2',{position:'relative',top:valoare*2+'px'})
Dacă dorim să putem tasta în textbox o valoare și aceasta să mute Sliderul automat, avem nevoie de:
sld=new Control.Slider("maner","sina",{range:$R(10,50),onSlide:afiseaza,onChange:afiseaza})
- definim funcția respectivă, în care testăm dacă tasta apăsată este Enter (codul 13):
function seteaza(v,e)
eveniment=e||window.event
if (eveniment.keyCode==13)
sld.setValue(v)
Observații:
cu un IF verificăm proprietatea keyCode a evenimentului (Enter are codul 13), apoi cu setValue
aplicăm o setare de valoare Sliderului.
http://www.cambiaresearch.com/c4/702b8cd1-e5b0-42e6-83ac-25f0306e3e25/Javascript-Char-Codes-
Key-Codes.aspx
dacă folosim evenimentul onblur (dar atunci mutarea mânerului se va face la apăsarea lui Tab)
dacă punem alături și un buton la apăsarea căruia să mutăm mânerul.
Totuși în practică e mai intuitiv ca valoarea tastată într-un textbox să fie confirmată printr-un Enter
simplu, mai ales că nu e vorba de un formular care să trebuiască trimis, deci n-are sens să creăm și un
buton.
În plus, am folosit acest exemplu ca pretext pentru a arăta cum se capturează evenimentul de apăsare a
unei taste anume.
InPlaceEditor
Acest mecanism substituie un text din pagină cu un mic formular la apăsarea unui click pe text, apoi
stochează valoarea din formular înapoi în corpul paginii.
Ori scopul principal al lui InPlaceEditor e să permită inserarea de conținut permanent și pentru toți
utilizatorii în pagină, de exemplu comentariul pe un blog sau pe un site. Asta înseamnă că obligatoriu
trebuie implicat serverul.
<!DOCTYPE html>
<html>
<head>
<script>
function initializare()
Doar IDul şi scriptul
{ server sunt obligatorii.
Acoladele conţin un
new Ajax.InPlaceEditor("edt","sursaeditor.php",{rows:10,cols:10})
obiect JSON cu opţiuni
} suplimentare (aici
dimensiunile casetei)
</script>
E preferabil ca toate
</head>
editoarele InPlace din pagină
</html> să fie iniţializate la încărcarea
paginii (desigur, ele pot fi
<body onload="initializare()"> iniţializate şi la primul click pe
comentariu).
<div>
</div>
</div>
</body>
<?php
print $_POST["value"];
?>
Obs: În mod convenţional, scriptul server trebuie să îşi ia textul tastat din variabila cu numele value,
trimisă de InPlaceEditor prin metoda POST.
Acest exemplu de script server este minimal – nu face decât să recepționeze conținutul de la client și să
îl întoarcă înapoi pentru a fi inserat în pagină.
obligatoriu: stocarea comentariului în baza de date, pentru a fi afișat tot timpul și pentru toți
viitorii utilizatori;
Ca opțiuni suplimentare, avem posibilitatea să personalizăm textele care apar pe butoanele Ok, Cancel
sau în timpul schimbului de date cu serverul, dacă să fie butoane sau linkuri etc.:
okText:"salveaza",cancelText:"anuleaza",savingText:"asteapta"})
Avem și posibilitatea să personalizăm stilurile aplicate asupra formularului inserat și asupra DIVului în
perioada sa de așteptare (Saving…). În special al doilea este important căci ne permite să afișăm un
indicator de așteptare (spre deosebire de Autocompleter, nu îl are implementat ca opțiune dedicată).
Stilurile trebuie să aibă următoarele nume standard (sunt 2 CLASSuri pe care InPlaceEditor le generează
automat, prin convenție):
<style>
.inplaceeditor-form {background:green}
</style>
Primul corespunde perioadei de așteptare (îi alocăm un GIF animat), al doilea corespunde formularului
(evident, poate fi personalizat pentru fiecare componentă a formularului, cunoscând faptul că acesta
conține o casetă textbox sau textarea, și 2 butoane sau 2 linkuri). Mai departe, în definirea
mecanismului, avem grijă ca textul de așteptare Saving… să nu mai apară, să nu se suprapună cu
imaginea.
new Ajax.InPlaceEditor("edt","sursaeditor.php",{rows:10,cols:10,savingText:"" })
În plus, ca și la celelalte instrumente, putem să alocăm funcții câtorva evenimente care au loc în timpul
procesului:
function initializare()
new Ajax.InPlaceEditor("edt","sursaeditor.php",
{rows:10,cols:10,onEnterEditMode:f1,onLeaveEditMode:f2,ajaxOptions:{onSuccess:f3}})
function f1()
function f2()
{
function f3()
Obs:
și aici putem personaliza o funcție callback, dacă dorim să reprogramăm întregul mecanism.
Avem și posibilitatea de a realiza comutarea editorului pe alte căi decât prin click pe paragraful original:
</div>
new Ajax.InPlaceEditor("edt","sursaeditor.php",
{rows:10,cols:10,externalControl:"but",externalControlOnly:true})
Pentru a opri definitiv funcționalitatea, avem funcția dispose() ce se poate apela oriunde în pagină, dacă
dăm un nume obiectului InPlaceEditor:
ed=new Ajax.InPlaceEditor("edt","sursaeditor.php",{rows:10,cols:10 })
……apoi, undeva în pagină creăm un buton sau alt mecanism care să apeleze oprirea….
Sarcina de lucru:
Modificati mecanismul astfel incat pagina sa afiseze 2 articole de blog, fiecare cu cate 1 comentariu
sub el (luate dintr-o baza de date cu articole si comentarii).
Sub comentarii sa mai fie cate un DIV pentru adaugare de comentariu nou (tot cu
InPlaceEditor, dar de data aceasta pe server va avea loc adaugare de comentariu in baza
de date, nu substituire!).
Sugestie privind licenta: cei care faceti siteuri Web la licenta, ganditi-va cum controlati faptul ca un
utilizator sa isi poata modifica doar PROPRIILE comentarii (idee: baza de date va trebui sa contina si
IDul autorului iar dupa logare si crearea sesiunii, in PHP se genereaza InPlaceEditoare doar pentru
comentariile ce au ca autor userul logat).
Lab 5
Imediat ce tastați, vi se atrage atenția că acesta nu e un document XML bine format. Acest mesaj apare
automat în partea de jos a documentului sau, dacă apăsați butonul de testare a bunei formări, în zona
de output.
Obs: Atunci când zona de output se încarcă prea tare cu mesaje, o puteți curăța. De asemenea, puteți
reveni oricând la mesajele precedente.
Un exemplu de subiect pentru proba practică (sau pt scris) ar fi să identificați care sunt regulile de bună
formare pe care le încalcă documentul?
al treilea element nu are voie să conțină spațiul, acesta fiind rezervat pentru separarea de
atribute; practic se consideră că PRET e marcatorul iar TOTAL e un atribut lipsit de valoare, deci
eronat (orice atribut trebuie să aibă o valoare între ghilimele).
1
În text voi folosi majuscule de câte ori mă refer la marcatori și atributele lor, ca să fie mai ușor de citit textul.
<?xml version="1.0" encoding="UTF-8"?>
<comanda>
<!-- acesta e un comentariu -->
<produs denumire="Televizor" id="p1" pret="100"/>
<produs id="p2" pret="200">Calculator</produs>
<produs>
<id>p3</id>
<pret>300</pret>
<denumire>Ipod</denumire>
</produs>
<prettotal>600</prettotal>
Aceasta este o comanda de produse
</comanda>
După cum am discutat la curs, e un document cu o structură nerecomandată, neregulată, dar ne permite
să exersăm diverse aspecte.
În timpul tastării, observați că Oxygen are o serie de facilități de tastare prin care încearcă să vă
împiedice de la a crea elemente prost formate: de câte ori scrieți un marcator vă adaugă automat
închiderea lui, de câte ori modificați un marcator vă modifică și perechea lui, de câte ori deschideți
ghilimele vi le închide automat etc. În principiu trebuie să depui efort pentru a scrie prost un document
XML și acest efort va fi puternic penalizat la proba practică.
Dați un click pe primul produs. Veți remarca o serie de aspecte printre care: generarea automată a unei
căi Xpath absolute care ar putea returna elementul selectat; sublinierea cu gri a elementului selectat
este importantă pentru căile RELATIVE, care vor fi calculate de la nodul subliniat; în dreapta se mai
poate vedea și zona de gestiune a atributelor elementului selectat
Alt aspect de interfață important e modul de vizualizare a documentului:
modul Author este varianta formatată, cu condiția să se fi creat stiluri CSS pentru marcatori (nu
e cazul deocamdată, vom primi o eroare CSS dacă încercăm să intrăm pe Author);
modul Grid este un alt mod de afișare a arborelui DOM, care e util pentru că ne arată exact câți
copii are fiecare nod (în timp ce în modul Text acest lucru nu e întotdeauna evident).
De exemplu, la întrebarea câți copii are nodul COMANDA?, ați putea fi tentați să răspundeți cu 6:
În realitate numărul de copii este 11, pentru că mai există încă 5 Enteruri considerate noduri text
(însoțite uneori de spații sau Taburi pt indentare!):
Primele trei sunt noduri text, dar sunt copiii celui de-al treilea PRODUS, nu al lui COMANDA;
Ultimele două sunt ale lui COMANDA, dar fac parte din același nod text ca și propoziția pe care o
încadrează. Deci ultimul nod text este de fapt {ENTER}Aceasta este o comanda de
produse{ENTER}
Prezența tuturor Enterurilor va afecta rezultatele căilor Xpath bazate pe poziție. În programare se evită
în general prezența Enterurilor prin câteva metode:
dacă documentul XML e luat dintr-un fișier sau string, avem grijă ca în acel fișier/string să scriem
totul pe un singur rând, fără Enteruri (dezavantaj: dificultate de citire și editare!)
dacă se construiește documentul XML direct prin program, nod cu nod, această problemă nu
mai apare căci funcțiile de lipire a nodurilor la arborele DOM (appendChild, insertChild etc.)
generează un XML curat, fără noduri invizibile!
În Oxygen vom păstra Enterurile pentru citirea mai ușoară a exemplelor. Oricum, am amintit deja că
vizualizarea în mod Grid ne evidențiază mai bine aceste noduri invizibile.
Afișarea în Grid se face pe nivele, cu posibilitatea de a detalia sau restrânge conținutul fiecărui element,
folosind săgețile. Prntre elemente se pot vedea nodurile invizibile, cu numele și valoarea (rând gol).
La ultimul nod text se poate vedea că a fost reunit cu un Enter (rând gol) și un Tab în față, iar în spate cu
un Enter (comparativ, valoarea 600 de deasupra, nu are nici un rând gol sau spațiere).
Obs: în versiunea curentă Oxygen are un bug: dacă dați click pe ultimul PRODUS din Grid, nu veți vedea
nodurile invizibile deși are și acesta 3. O metodă mai sigură de a vedea corect arborele DOM complet
este opțiunea Tools- Tree Editor:
La vizualizarea cu Tree Editor, pentru a vedea toate nodurile, mai trebuie să activați butoanele care fac
vizibile atributele, nodurile invizibile (white spaces) și comentariile.
Documentul
(nodul document)
Nod comentariu produs produs produs Nod text prettotal Aceasta este o
Nod text Nod text Nod text Nod text comanda de produse
invizibil invizibil (nod element) produs invizibil (nod element) produs invizibil (nod element) produs invizibil(nod element) produs (nod text
vizibil+invizibil)
Id=”p1”
Id=”p2”
pret=”100” pret=”200”
denumire=”Televizor”
Nod
id Nod
pret Nod
denumire Nod
Calculator text(nod element) produstext(nod element) produstext(nod element) produstext 600
(nod text vizibil) invizibil invizibil invizibil invizibil (nod text vizibil)
p3 300 Ipod
(nod text vizibil) (nod text vizibil) (nod text vizibil)
Confuzia 1: Rădăcina
Problema e că în nici unul din cele 2 cazuri termenul standard nu e root, așa că autorii folosesc în mod
liber cuvântul rădăcină pentru a se referi ba la unul, ba la celălalt. Eu în general folosesc termenul
rădăcină pentru a mă referi la marcatorul care conține toți ceilalți marcatori (elementul document),
considerând că toată lumea știe că acesta trebuie obligatoriu precedat de declarația <?xml...?>.
Confuzia vă poate afecta cel mai des atunci când creați un arbore DOM prin programare, nod cu nod.
Există tentația de a crea toate elementele, a le lipi între ele, dar a uita că și elementul rădăcină trebuie
lipit la document.
$doc=new DOMDocument();
$doc->appendChild($radacina)
căutările Xpath generice după funcția node() vor returna ORICE nod (de orice tip: text,
elemente, comentarii etc.) dar nu și atribute (căutarea generică de atribute se face cu @*);
nodurile au o ordine relevantă (le accesăm de obicei pe bază de poziție) în timp ce atributele de
obicei se accesează pe bază de nume (ordinea lor e irelevantă – 2 marcatori care au aceleași
atribute în ordini diferite sunt considerați echivalenți); totuși, sunt și softuri care permit
parcurgerea atributelor pe bază de poziție (în ordinea în care au fost scrise) dar nu e bine să ne
bazăm pe acea ordine;
atributele vor fi ignorate de funcțiile care caută pe baza relațiilor de rudenie (copil/frate/etc.);
chiar și în standardul DOM, atributele nu fac parte din vectorul childNodes, au propriul vector,
numit attributes.
Cel mai sigur este să vă gândiți la atribute ca la niște proprietăți ale nodurilor de tip element și nu ca la
noduri-copil ale acestora. Din acest motiv în desenarea arborelui DOM nu am desenat atributele pe
următorul nivel (4), ci între nivele (3 și 4) sugerând astfel faptul că nu sunt chiar noduri-copil, ci au un
statut aparte.
În sfârșit, o confuzie care afectează mai mult la nivel de programare este semnificația termenilor valoare
de nod/ nume de nod.
Conform standardului DOM, orice nod (și orice atribut) trebuie să aibă nume și valoare. În cazul
atributelor acest lucru e evident, dar la elemente și noduri text e mai puțin evident. De exemplu care e
numele și valoarea în cazul <produs>Televizor</produs> ???
Răspunsul intuitiv ar fi că “produs” este numele și “televizor” valoarea. În realitate, există două viziuni:
A. Standardul DOM specifică astfel:
În acest exemplu avem 2 noduri, unul de tip element, cu un fiu de tip text. FIECARE din acestea are un
nume și o valoare!
Orice programator care lucrează cu standardul DOM va trebui să țină de acest fapt (stringul "Televizor"
nu e valoarea directă produsului, ci valoarea primului copil al nodului PRODUS). Pentru accesarea
stringului Televizor, prin funcții DOM va fi necesară o construcție de genul (PHP):
$x=..... (stocarea elementului PRODUS într-o variabilă prin getElementBy…, childNodes[] sau alte
metode)
B. Deoarece această viziune nu e tocmai intuitivă, multe limbaje (atât de programare, cât și XML
Schema, XSLT sau Xpath) ocolesc sau extind standardul DOM cu niște funcții care să trateze acest caz în
mod mai intuitiv: „produs” este numele, „Televizor” este valoarea.
$text=$x (variabila $x nu va mai conține un pointer spre nodul PRODUS, ci chiar conținutul textual al
acestuia!)
Comparați cele două sintaxe. Le vom exemplifica într-un seminar viitor, când vom construi un XML prin
programare. Varianta A ar trebui să funcționeze în orice limbaj (fiind standardizată), varianta B nu
neapărat (dar fiind foarte populară, e disponibilă sub forma a diverse biblioteci pentru majoritatea
limbajelor).
În programare, trebuie să fiți atenți care din variante este suportată de limbajul cu care lucrați. Multe
limbaje le suportă pe AMBELE:
În JavaScript există suport universal pentru standardul DOM (dar diferă de la browser la browser
numele clasei); pentru a putea folosi varianta B există extensia E4X, dar nu e recomandată căci
Internet Explorer refuză să o implementeze (poate fi testată în Firefox dar și acolo are unele
buguri).
Ca mod de gândire, Xpath e mai aproape de versiunea B (consideră că valoarea unui element e chiar
conținutul său).
Limbajul XPath
Reminder: Xpath este limbajul fundamental de interogare a documentelor XML, indiferent de scopul lor
(fie că sunt documente propriu-zise, programe scrise în limbaje cu taguri sau structuri de date de
transferat, termenul oficial este tot documente XML).
Xpointer – o variantă mai complexă a lui Xpath care, în plus față de acesta, poate accesa și
altceva decât noduri din arborele DOM, de exemplu locații (poziții din documente), fragmente
(orice porțiune dintre două poziții, de exempu bucăți de taguri, bucăți de atribute) sau colecții
de fragmente;
XML Signature și XML Encryption – pentru semnarea digitală sau criptarea unor noduri din
document.
Ca și SQL, Xpath suportă și anumite calcule aplicate asupra rezultatelor, iar de la versiunea Xpath 2.0,
suportă inclusiv:
interogări condiționale if A then B else C, unde A,B,C sunt interogări Xpath (dacă interogarea A
găsește soluții, atunci execută interogarea B, altfel pe C),
iterative: for $i in X return Y, unde X, Y sunt interogări Xpath ,iar X e o interogare care a
returnat mai multe rezultate (pentru fiecare rezultat al interogării X, execută interogarea Y);
expresii regulate.
Xpath 2.0 e încă slab implementat. În JavaScript și PHP deocamdată se pot rula doar interogări Xpath
1.0.
Spre deosebire de SQL, Xpath poate doar citi informații, nu și modifica/insera/șterge. Pentru acestea se
apelează la funcțiile standard DOM (dacă modificările se fac direct pe documentul inițial) sau la limbajele
de transformare XSLT/Xquery (dacă dorim să generăm un nou document în urma modificărilor).
Interogările Xpath se execută în caseta dedicată din stânga sus, unde se poate selecta și versiunea Xpath
în care dorim să lucrăm.
Obs: Xpath 2.0 SA reprezintă interogări Schema Aware, pentru documente cu vocabular – de exemplu
dacă prin interogare căutăm un element care nu are cum să existe în document (din cauză că este
interzis de vocabular), atunci nu are sens să se mai proceseze interogarea. Interogările de tip SA sunt cele
care verifică dacă are sau nu sens interogarea, în funcție de regulile impuse de vocabularul
documentului.
În timpul tastării unei inteorgări, apar liste de sugestii și un Help contextual, foarte util în familiarizarea
cu cuvintele cheie din XPath
În mod implicit, interogările sunt căi absolute, încep de la rădăcină. Dacă dorim căi relative, trebuie să
alegem nodul curent, printr-un click pe marcatorul dorit, care va fi subliniat cu gri.
Executarea unei interogări nu dă rezultatul ca un simplu string, ci indică și poziția din document la care s-
a găsit rezultatul:
/comanda/produs/id
/comanda[1]/produs[3]/id[1] – p3
....indică faptul că rezultatul se află în primul COMANDA, al treilea PRODUS, primul ID și are valoarea
“p3”.
/comanda/produs/@id
...caută atributul ID al elementelor PRODUS din COMANDA (se vor găsit 2 soluții, cu valorile „p1” și “p2”)
/comanda/produs
/comanda
Căi generice
/comanda/*
...toate elementele din COMANDA (4 soluții)
/comanda/text()
....toate nodurile text din COMANDA (6 soluții, din care 5 sunt nodurile invizibile)
/comanda/node()
....toate nodurile din COMANDA, indiferent de tip (11 soluții din care 6 text, 4 element, 1 comentariu)
/comanda/comment()
/comanda/produs/@*
....toate atributele din toate elementele PRODUS din COMANDA (5 soluții, 3 la primul PRODUS, 2 la al
doilea)
/comanda/produs/node()
...toate nodurile din toate elementele PRODUS din COMANDA (se observă că nu s-au returnat atributele!
8 soluții, din care la al doilea PRODUS un text, iar la al treilea 3 elemente și 4 noduri invizibile)
Dacă dorim și nodurile și atributele scriem două interogări separate prin bară verticală:
/comanda/produs/node()|/comanda/produs/@*
În acest fel, Xpath 1.0 permite reunirea soluțiilor din mai multe căi. Xpath 2.0 oferă și tehnici mai
avansate (intersecție, diferență etc.)
/comanda/produs/id='p3'
...există nodul ID cu conținutul (valoarea) p3 în vreunul din elementele PRODUS din COMANDA? (true)
/comanda/produs/pret>100
....există vreun PRET cu valoare peste 100 în vren PRODUS din COMANDA? (true)
/comanda/*=600
/comanda/*/pret=300
/comanda/*/@*="Televizor"
....există vreun atribut cu valoarea “Televizor” în oricare din fiii lui COMANDA? (true)
/*/*="Calculator"
/*/*/*="Ipod"
Ca regulă generală, interogările booleene se termină cu o expresie logică care NU apare între paranteze
pătrate (dacă apare, înseamnă că e o restricție aplicată nodului precedent, vezi mai jos).
Căi cu salt
Când nu suntem siguri pe care ramură și pe ce nivel se găsește rezultatul se pot realiza căutări cu salturi.
În general nu sunt recomandate datorită performanțelor slabe pe arbori DOM foarte mari (trebuie
parcurse toate ramurile descendente!). Saltul este indicat prin //
//@*=100
//*="Ipod"
/comanda//@id="p1"
/comanda//@id
...returnează toate atributele ID din COMANDA, indiferent de nivelul pe care se află (2 soluții)
//@id|//id
...returnează toate IDurile din document, indiferent că sunt atribute sau elemente, indiferent cui aparțin
(3 soluții)
...returnează primul PRODUS din COMANDA (atenție, în Xpath-ul din Oxygen numerotarea începe de la
1, dar în unele browsere poate începe de la zero!)
/comanda/produs[last()]
//@*[1]
//*[last()]
...returnează toate elementele ce apar ultimele în cadrul nodului de care aparțin (3 soluții, inclusiv
rădăcina, care e ultima din document!)
/comanda/produs[id]
/comanda/produs[@id]
/comanda/produs[text()]
...returnează acele PRODUSE din COMANDA care conțin noduri text (2 soluții)
//*[text()="Calculator"]
Restricțiile permit folosirea lui or, not, and pentru condiții mai complexe:
/comanda/produs[@id or id]
....returnează acele PRODUSE din COMANDA care au ID fie ca atribut, fie ca element (3 soluții; ne
scutește de a scrie două căi separate prin |)
/comanda/produs[not(@id)]
//produs[not(node())]
//produs[not(@*)]
...returnează acel PRODUS care ocupă prima poziție (între frați) și are atribut ID (1 soluție)
(când combinăm restricția de poziție cu altele nu putem folosi produs[1 and @id], deoarece în expresii
booleene orice număr diferit de zero e convertit în true, de aceea apelăm la funcția position)
Restricțiile nu se aplică doar pe ultima treaptă din cale. Putem să le aplicăm și undeva la mijlocul căii:
/comanda/produs[@id]/text()
...returnează nodurile text din acele PRODUSE cu atribut ID, din COMANDA (1 soluție)
Căi cu axe
Axele sunt direcțiile pe care înaintează calea. În mod implicit, o cale merge în jos. În mod explicit putem
schimba direcția căii cu:
. pentru a face referire la nodul curent (folosit pentru a pune condiții asupra nodului curent).
La foldere nu are sens să o luăm în sus în căi absolute: C:\folder1\folder2\.. e echivalent cu C:\folder1!
Aici însă are sens, căci e posibil să ajungem la un nod și pe altă cale decât coborând prin părintele său!
(prin salt cu // sau o traversare orizontală peste frați)
//text()/..
//id/ancestor::node()
//text()[.=”Calculator”]
Vedeți cum s-a pus condiția asupra nodului curent cu ajutorul punctului!
Dacă am fi scris //text()=”Calculator” am fi obținut o interogare booleană – există noduri text
egale cu “Calculator”??
/comanda/produs[3]/following::node()
...returnează toate nodurile care urmează în document după al 3-lea PRODUS din COMANDA (4, nu doar
frații!)
/comanda/produs[3]/following-sibling::node()
/comanda/produs[3]/preceding::text()
...returnează toate nodurile text ce preced al 3-lea PRODUS din COMANDA (5, din care 4 noduri
invizibile)
/comanda/produs[3]/preceding::*/@*
...returnează toate atributele nodurilor precedente PRODUSULUI al 3-lea din COMANDA (5 soluții)
/comanda/produs[@id=”p2”]/preceding::node()[preceding-sibling::comment()]/..
..returnează părintele acelui element care e precedat de un frate de tip comentariu și care precede un
PRODUS cu ID=”p2”, din COMANDA
...altfel spus:
se coboară în COMANDA
dintre aceștia se alege doar acel predecesor care are un frate precedent de tip comentariu, cu
[preceding-sibling::comment()]
Putem folosi restricții la oricare pas al căii, nu doar la selecția rezultatului de pe ultimul pas.
Dacă o cale Xpath devine prea lungă, Oxygen vă deschide un panou suplimentar pentru continuarea
tastării.
Funcția normalize-space elimină caracterele albe (spațiu, Enter, Tab) de la începutul și sfârșitul unui
string, iar în interiorul stringului păstrează maxim un spațiu între cuvinte.
//text()[normalize-space(.)=""]
...returnează toate nodurile invizibile din document (detectate prin faptul că, după ce li se aplică
eliminarea de caractere albe, nu mai rămâne nimic din ele; 9 soluții)
//text()[normalize-space(.)!=""]
//text()[contains(.,"3")]
...returnează toate nodurile text care conțin caracterul 3 (2 soluții, observați din nou punctul folosit
pentru a pune o condiție asupra nodului curent)
//node()[contains(.,"3")]
...returnează toate nodurile indiferent de tip care conțin caracterul "3" (6 soluții! Pe lângă Idul și Pretul
returnate și în cazul precedent, aici apar și toate nodurile care conțin acel ID și acel PRET, deci inclusiv
PRODUSUL al 3-lea și elementul rădăcină: de reținut – contains caută în toți descendenții, nu doar în
conținutul textual direct)
//node()[count(*)=3]
//node()[count(node())=1]
Mai multe funcții se prezintă în secțiunea următoare (toate pot fi folosite atât ca restricții cât și pentru
un calcul aplicat rezultatului final).
În această categorie intră căile asupra căror rezultate se aplică o funcție, de obicei una de agregare (sum,
count etc.).
count(/comanda/node())
... returnează numărul de noduri fiu ale rădăcinii (numărul 11, nu 11 soluții!)
contains(/comanda/produs[3],"3")
... returnează true, căci al 3lea PRODUS din COMANDA conține caracterul 3
sum(//produs/@pret)
sum(//@pret | //pret )
concat(//produs[2]/@id,//produs[2])
...returnează "p2Calculator", concatenarea rezultatelor celor două căi (Idul plus conținutul celui de-al
doilea PRODUS)
name(//*[@id='p2'])
name(/comanda/produs[1]/@*[2])
Important: funcția name e mecanismul prin care putem afla numele unui element sau atribut la care am
ajuns pe alte căi decât cu ajutorul numelui (poziție, relație cu alte noduri etc.)
string(/comanda)
... convertește rădăcina în string (rezultatul fiind toate conținuturile textuale concatenate între ele)
substring(//produs[@pret=200],2,3)
substring-before(/comanda/text()[normalize-space(.)!=""], "com")
...returnează "Aceasta este o ", adică subșirul decupat de la început până la apariția lui "com", din acel
nod text care nu este invizibil (există și varianta substring-after)
string-length(/comanda/produs[2])
translate(//prettotal,"0","9")
translate(//produs[count(node())=1],"acr","xy")
...returnează "Cxlyulxto", obținut prin substituirea lui a cu x, a lui c u y și a lui r cu nimic (datorită faptului
că al 3lea argument e mai scurt ca al doilea); modificarea se aplică aspra valorii acelui PRODUS care are
exact 1 nod fiu (deci al 2lea)
boolean(/comanda/produs)
...există vreun PRODUS în COMANDA? (true, funcția boolean e folosită pentru a converti orice tip de
interogare într-un booleană, dacă ne interesează doar existența unei soluții, nu și soluția în sine)
...este PRODUS numele primului element fiu din COMANDA? (true, funcția boolean se combină cu o
declarație de tip self ce exprimă numele nodului curent, pentru a-i testa numele)
Important: funcția position nu poate fi folosită pentru a calcula poziția unui nod!
Deci nu putem calcula position(/comanda/produs[id]) pentru a obține poziția între frați a PRODUSULUI
ce are un fiu ID!
count(/comanda/produs[id]/preceding-sibling::node())
Important: XPath 1.0 este foarte limitat în ce privește funcțiile de agregare – are doar sum și count. Alte
operații (medie, produs etc.) se pot realiza fie cu XPath 2.0 (ce permite structuri FOR și IF chiar în căi), fie
se pasează responsabilitatea spre limbajul de programare ce solicită interogarea.
Căi relative
Căile relative se calculează relativ la un nod curent. În Oxygen putem seta nodul curent printr-un click pe
un marcator (care primește subliniere gri). Dați un click pe al 3lea PRODUS.
following::*
../*[1]
...returnează primul nod fiu al părintelui nodului curent (primul nod invizibil)
count(*)
string-length(*[last()])
preceding-sibling::node()
...returnează precedenții frați ai nodului curent (7 soluții, inclusiv noduri invizibile și comentariul)
count(preceding-sibling::*[not(@*)])
preceding-sibling::*/@pret=100
id="p3"
boolean(self::produs)
...care e poziția, între frați, a nodului curent? (7) – deoarece nu putem folosi funcția position(), se
apelează la acest truc de numărare a fraților precedenți
Lab 6
XML Schema
XML Schema este cel mai popular limbaj pentru crearea de vocabulare XML. Reamintim că un vocabular
XML este un set de reguli (suplimentar celor de bună formare) ce limitează marcatorii, atributele și
modul lor de utilizare, iar documentele construite după un vocabular pot fi validate pentru a verifica
dacă au respectat regulile.
Există mai multe limbaje de creare a vocabularelor, dintre care DTD face parte chiar din standardul XML,
însă XML Schema a căpătat întâietate datorită unor avantaje precum:
permite validare parțială/multiplă (când un document are marcatori din mai multe vocabulare);
permite definire unor restricții de tip "cheie primară/cheie străină" (deci și relații între
documente) mai flexibile decât DTD;
este el însuși un limbaj XML, deci poate fi procesat ca arbore DOM, la fel ca orice document XML
(în timp ce DTD nu e de tip XML, necesită alt fel de interpretor).
Nodul text să accepte doar una din valorile Televizor, Calculator, Ipod;
Filozofia Tipurilor
Filozofia XML Schema se bazează pe noțiunea de TIP, care e mai largă decât la alte limbaje. Un tip poate
fi:
TIP PREDEFINIT: orice tip de date clasic (string, integer, plus câteva specifice XMLului precum
tipul ID – stringuri irepetabile, sau tipul Name – stringuri fără spații, sau tipul token – stringuri cu
maxim un spațiu între cuvinte) ;
TIP SIMPLU: o submulțime a unui TIP PREDEFINIT (o listă de stringuri, un interval de valori
numerice etc.) sau o reuniune de submulțimi;
TIP COMPLEX: un model complex obținut prin îmbinarea de marcatori, noduri text și atribute, ce
se dorește a fi reutilizat (o structură internă reutilizabilă a unui nod element).
În general e o idee bună ca pentru fiecare marcator și atribut prevăzut a apărea prin documente:
În felul acesta, regulile capătă oarecare modularitate – se pot edita regulile la nivelul unui TIP, fără a
afecta marcatorii care nu au legătură cu acesta.
Obs: În general se evită crearea unui TIP pentru elementul rădăcină, având în vedere unicitatea lui,nu se
prea pune problema reutilizării sale. Sunt totuși situații în care și structura rădăcinii se poate reutiliza
ducând la recursivitate, când copiii rădăcinii moștenesc structura rădăcinii, nepoții la fel, etc. ramificând
arborele DOM în maniera fractalilor, teoretic la infinit. Practic nu se merge la infinit dacă structurii
recursive i se alocă un caracter opțional (astfel încât nodurile de pe ultimul nivel să poată să nu mai
conțină nimic).
Planificarea tipurilor
Tipurile se definesc într-o manieră bottom-up, pe baza cerințelor, pornid de la nodurile simple ale
viitoarelor documente (atributele și nodurile text) apoi urcând în arbore, prin noduri tot mai complexe.
În cazul de față:
1. Tipuri pt atribute:
pentru atributul Pret nu are sens să creăm un tip, există TIP PREDEFINIT pentru numere pozitive
pentru elementul Onorat nu are sens să creăm un tip, există TIP PREDEFINIT pentru valori
booleene.
vom evita să creăm un TIP pentru Comanda, căci nu dorim recursivitatea structurii rădăcinii.
Ca regulă generală:
TIPURILE SIMPLE se creează pentru atribute sau elemente ce vor conține doar text;
TIPURILE COMPLEXE se creează pentru elemente care pot avea și altceva în afară de un conținut
text (atribute sau elemente copil).
Crearea vocabularului
Creăm un fișier de tip XML Schema. Spre deosebire de documentele XML, vocabularele în Oxygen pot fi
create cu un foarte comod mod Design care ne permite să construim regulile în mod grafic și să generăm
automat codul sursă XML Schema.
În modul Design, cea mai mare parte din muncă se realizează în meniul click dreapta, cu primele două
opțiuni:
Edit Attributes ne permite să configurăm TIPURILE sau alte componente ale vocabularului
Avertismente:
Panoul Attributes din dreapta oferă teoretic aceleași opțiuni cu Edit Attributes, dar Edit
Attributes adesea oferă și unele opțiuni în plus, așa că evitați să folosiți panoul.
Crearea tipurilor
Trebuie să-i spunem din ce TIP PREDEFINIT îl vom crea, și ce filtru vom aplica:
în secțiunea Facets definim filtrul: Enumerations, apoi cu click dreapta – Add adăugăm pe rând
valorile permise
TIPUL SIMPLU pentru CodProdus:
filtru: length 5
TIPUL COMPLEX pentru Produs:
alegem TIPUL conținutului: BaseType = ModelDenumiri (iată cum integrăm TIPURILE SIMPLE
într-o structură de TIP COMPLEX); precizarea acestui tip va impune și obligativitatea denumirii
(fiind definită clar lista de valori, nu e permis șirul vid, deci absența conținutului!)
Le stabilim limitele de ocurență, caracterul opțional (min.ocur.=0) sau obligatoriu (min.ocur.=1), precum
și TIPUL conținutului:
o TIP: Type=xs:boolean
Tipurile
Vocabularul propriu-zis
În modul Text puteți vizualiza codul sursă. Nu e foarte complicat, dar e mult de scris (se poate observa
pe codul sursă că limbajul XML Schema însuși e un vocabular XML).
Validarea
Creăm un document XML nou. De data aceasta, la creare apăsăm Customize pentru a indica vocabularul,
în caseta Schema URL.
Încă de la creare, documentul va conține un element Produs, iar rădăcina va fi însoțită de o serie de
atribute care să facă legătura cu vocabularul (noNamespaceSchemaLocation).:
<?xml version="1.0" encoding="UTF-8"?>
<Comanda xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="file:/C:/Users/2/Documents/vocabular.xsd">
<Produs CodProdus="" Pret=""></Produs>
</Comanda>
Prezența unui Produs vid e explicabilă prin caracterul său obligatoriu. Când un document e construit pe
baza unui vocabular, se pot genera automat o serie de noduri obligatorii sau valori implicite.
Putem testa pe rând regulile vocabularului, scriind un document corect, apoi încălcând regulile una câte
una, pentru a ne convinge de efectul acestora.
Deși lipsește elementul Onorata, nu e o problemă, acesta fiind definit cu caracter opțional. Nici faptul că
valoarea Ipod se repetă nu e o problemă, important e să nu se iasă din lista de valori permise. Tot valid e
și următorul document:
<?xml version="1.0" encoding="UTF-8"?>
<Comanda xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="file:/C:/Users/2/Documents/vocabular.xsd">
<Produs CodProdus="aaaaa" Pret="2">Televizor</Produs>
<Onorata>true</Onorata>
</Comanda>
Modificarea vocabularelor
Nu e obligatoriu ca la crearea unui vocabular să se lucreze cu TIPURI, așa cum am lucrat în acest mod. Se
pot impune toate restricțiile direct pe nodurile vocabularului, fără a mai crea tipuri.
mai multe atribute/elemente pot să aibă același tip, deci aceeași gamă de valori/structură
internă permisă (vezi exemplul următor);
se pot crea tipuri din alte tipuri (s-a văzut deja cum ModelProduse include ModelDenumiri și
ModelCoduri);
se pot aduce modificări la un tip fără a afecta restul vocabularului, care nu are legătură cu acel
tip (vezi mai jos).
Codurile de produse să aibă structura: litera P, urmată de exact 3 cifre, urmate de cel puțin
încă un caracter oarecare.
Iată o calitate foarte importantă a XML Schema – posibilitatea de a constrânge valori cu expresii
regulate!
Expresia regulată exprimă exact structura pe care o doream: să înceapă cu P, să urmeze un caracter din
intervalul [0-9] repetat de exact 3 ori, apoi un caracter oarecare repetat minim o dată (punctul e locțiitor
pt un caracter, + e indicator de repetiție cu obligativitate: minim o dată)
De nodul Choice se vor lipi toate alternativele între care oferim posibilitatea de a alege:
o cum Onorata va fi una din variante, o tragem cu mouseul până se lipește de nodul
Choice
o click dreapta pe nodul Choice – Append Child – Element și denumim noul element cu
numele ProdusIndisponibil.
XSLT
XSLT este limbajul de transformare a documentelor XML și se bazează pe Xpath pentru a extrage
informații dintr-un document sursă și a le pune într-un document rezultat (cu structură diferită). De
obicei XSLT e folosit pentru a genera pagini HTML din date XML, deci poate fi folosit și ca instrument
AJAX (conversia răspunsului de la server în cod HTML).
O foaie XSLT conține una sau mai multe reguli de substituire cu două componente:
match este calea Xpath a elementelor care vor
<xsl:template match="/"> suferi substituția
conținutul lui xsl:template este noul conținut
Text oarecare
care va intra în locul celui detectat de match.
</xsl:template>
Exemplul de față selectează întreg documentul (calea XPath "/" reprezintă documentul în ansamblul său)
și îl înlocuiește cu stringul "Text oarecare".
Setările esențiale sunt în prima și ultima categorie. Categoria FO Processor se folosește doar dacă facem
transformări de tip XSL-FO (o alternativă la CSS, dedicată formatării de cod XML).
Avem deja completată calea foii XSLT curente (codul currentFileURL e suficient). Trebuie să selectăm
manual adresa XMLului original.
În acest fel am precizat ce se va transforma (comanda.xml) și cum se va transforma (foaia curentă din
Oxygen). Mai trebuie să precizăm ce să se întâmple cu rezultatul transformării, în categoria Output:
O concluzie importantă este că XSLT nu conservă implicit conținutul fișierului original! Dacă nu punem
nimic în regula de substituire, pur și simplu se returnează un document gol. Dacă vrem să conservăm
întocmai unele elemente din fișierul original, trebuie să definim explicit o regulă de conservare (=o
regulă care substituie acele elemente cu ele însele). Revenim mai târziu la regulile de conservare.
Obs: Dacă outputul transformării este un fișier XML, caracterele invizibile (spațiu, Tab, Enter) din
interiorul regulilor se conservă. Dacă outputul e HTML, acestea se ignoră. Deci pe un rezultat XML nu e
totuna dacă regula e:
sau
<xsl:template match="/>
Text oarecare
</xsl:template>
Substituirile iterative sunt printre cele mai populare în XSLT, pentru că permit să se parcurgă (cu un ciclu
FOR) toate rezultatele unei interogări XPath și să se aplice o transformare fiecăreia dintre acestea.
Adesea ele sunt îmbinate cu regulile de substituire alternativă (IF și CASE) care aplică substituții diferite
în funcție de o interogare XPath booleană.
În exemplul următor, pentru fiecare produs din COMANDA se va returna cuvântul Produs scris cu italic
(într-o pagină HTML):
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:template match="/">
<xsl:for-each select="comanda/produs">
<i>Produs</i>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Obs:
Rezultat:
for ($i=0;$i<3;$i++)
print "<i>Produs</i>".$i."<br/>";
O problemă spinoasă pentru începători e modul în care XSLT tratează conceptul de variabilă. De fapt
numele este impropriu – în XSLT 1.0 ele se comportă mai mult ca și constantele – li se poate atribui o
singură dată o valoare (dar valoarea poate să varieze, dacă li se atribuie o interogare Xpath ce dă mai
multe valori, deci nu sunt chiar constante!).
Aceasta face teoretic imposibil ciclul FOR de mai sus (care necesită ca $i să își schimbe valoarea de mai
multe ori).
(dacă valoarea este una simplă, echivalentul lui $i=0 în PHP, sau dacă valoarea e cod XML obținut printr-
o interogare XPAth, care se trece în select, în locul lui 0)
<xsl:variable name="i">
<i>Produs</i>
</xsl:variable>
<xsl:value-of select="$i"/>
(dacă valoarea e simplă sau dacă e cod XML din care vrem să luăm doar conținutul textual)
<xsl:copy-of select="$i"/>
(dacă valoarea e cod XML și vrem să-l luăm așa cum e, cu tot cu marcatori)
Problema este că nu putem inițializa de 2 ori aceeași variabilă. Rostul lor uzual este să asigure
reutilizabilitatea unei bucăți de cod XML. De exemplu precedenta transformare poate să aibă și
următoarea formă, având în vedere că nu facem decât să reutilizăm aceeași bucată de cod:
Începătorii sunt tentați să ignore faptul că variabilelor XSLT nu li se pot reatribui valori, și vor încerca să
implementeze un ciclu FOR care să afișeze Produs1, Produs2, Produs3 astfel:
count(preceding-sibling::produs)
(calea e relativă la fiecare produs în parte, acestea fiind deja selectate de ciclul FOR).
La executarea ei, se vede că apar cuvintele Produs1, Produs2, Produs3 unul sub altul (am concatenat și
un BR după număr).
Rețineți acest truc și faptul că sunt numeroase situațiile în care lipsa de flexibilitate a variabilelor din
XSLT poate fi compensată prin generarea unor valori variabile cu funcții Xpath.
În general linia
<xsl:value-of select=".........."/>
este una din cele mai des întâlnite din XSLT. Cu ajutorul ei se pot genera:
Obs:Mai remarcați și faptul că adăugarea de conținut nou (HTML în acest caz) se face prin simpla scriere
de cod nou în interiorul lui xsl:template, fără să fie necesar un operator de concatenare explicit!
Atunci când nici Xpath nu ne ajută în a depăși problema rigidității variabilelor, mai avem o soluție la
îndemână – regulile recursive.
<xsl:template match="/">
<xsl:call-template name="generare">
<xsl:with-param name="listaproduse" select="comanda/produs"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="generare">
<xsl:param name="i" select="0"/>
<xsl:param name="listaproduse"/>
<xsl:if test="$listaproduse">
<i>Produs<xsl:value-of select="$i"/></i>
<xsl:call-template name="generare">
<xsl:with-param name="i" select="$i+1"/>
<xsl:with-param name="listaproduse" select="$listaproduse[position()>1]"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
a doua nu are match, dar are name, deci poate fi apelată ca o funcție.
xsl:with-param indică numele și valoarea argumentelor cu care se apelează funcția (în interiorul
funcției, acestea trebuie declarate cu xsl:param și cu aceleași nume ca și argumentele din apel).
<xsl:call-template name="generare">
<xsl:with-param name="listaproduse" select="comanda/produs"/>
</xsl:call-template>
s-ar traduce în programarea clasică prin construcții de forma (sintaxa e orientativă, nu aparține unui
limbaj anume):
generare(listaproduse)
if (listaproduse)
write "Produs"+i
i=i+1
generare(listaproduse,i)
Observați diferența între modul de lucru al variabilelor și modul de lucru al parametrilor. Parametrii își
pot schimba valoarea la apelul unei "funcții" deci nu sunt la fel de rigizi ca variabilele! De aceea putem
simula cicluri FOR prin funcții care se apelează pe ele însele cu un argument $i care tot crește la fiecare
apel.
$i, inițializat cu zero la primul apel și incrementat apoi la fiecare apel ce urmează;
Condiția de continuare a recursivității e <xsl:if test="$listaproduse">, adică oprim ciclul atunci când
vectorul listaproduse rămâne gol (s-au terminat de eliminat produsele din el).
Cum aminteam, recursivitatea se folosește adesea atunci când dorim să implementăm cicluri FOR în care
o variabilă își tot schimbă valoarea, iar această schimbare nu poate fi pasată spre Xpath (de exemplu
când calculăm produsul unor elemente, căci nu există funcția "product" în Xpath2).
2
XPath 1.0 are doar 2 funcții de agregare: sum și count.Restul operațiilor iterative (medie, produs, etc.) trebuie să
aibă loc în limbajul care găzduiește Xpath. Xpath 2.0 în schimb permite să se implementeze cicluri FOR chiar în căi,
făcând posibile astfel de operații iterative direct în căi Xpath. În general Xpath 2.0 și XSLT 2.0 simplifică mult
exemplele discutate aici, însă le-am putea testa doar în Oxygen, sunt încă slab suportate în mediile de programare
(iar în browsere deloc și nici nu s-au anunțat planuri de dezvoltare a browserelor în această direcție, tendința
acestora fiind să consolideze rolul lui JavaScript, nu să îl substituie cu XSLT!).
Reguli de substituire alternativă
Observați că până aici am generat un document nou fără să folosim conținutul existent în cel original (nu
chiar deloc, am folosit poziția produselor în ultimul exemplu). Rolul uzual al unei transformări XSLT este
să genereze cod și conținut nou concatenat cu cod și conținut din cel vechi. În următorul exemplu, în
locul cuvintelor Produs1, Produs2, Produs3, vom genera denumirile produselor.
Această sarcină e puțin complicată de faptul că documentul XML original are o structură neregulată –
primul produs are denumirea într-un atribut, al doilea îl are în conținutul textual, al treilea îl are într-un
element-fiu. Deci, chiar dacă parcurgem cu un ciclu FOR cele 3 produse, va trebui să implementăm o
structură CASE care să extragă denumirea de la caz la caz:
Dacă nu găsește nici una din variante (otherwise), va considera că denumirea se află în
conținutul textual, deci valoarea elementului ("." reprezintă nodul curent în Xpath).
Observați că
toate căile Xpath din atributele test și select sunt RELATIVE la nodul curent (produsul la care s-a
ajuns cu xsl:for-each);
la rândul său, atributul select de la for-each e o cale RELATIVĂ la nodul selectat de match.
Rețineți că în general, XSLT folosește căi RELATIVE la nodul pe care s-a poziționat instrucțiunea-părinte
(cea cu match e părinte pt for-each, acesta e părinte pt choose și when etc.). Asta cu excepția
atributului match care folosește căi absolute (chiar dacă nu punem slash la începutul lor!)
Structura CASE nu e singura metodă prin care putem obține cele 3 denumiri! După cum am văzut la
exemplul cu numerotarea, XSLT conlucrează cu Xpath într-o manieră flexibilă – unele sarcini pot fi
transferate spre Xpath, inclusiv filtrarea/selecția de elemente.
<hr/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Între cele 3 variante am pus operatorul Xpath | care returnează reuniunea soluțiilor celor 3 interogări.
Cum fiecare PRODUS e doar în una din cele 3 situații, se va returna valoarea dorită pt. fiecare caz!
O a treia variantă ar fi să evităm cu totul ciclul FOR (ceea ce nu e chiar recomandat, dar aici numărul de
produse e suficient de mic încât să le putem selecta individual).
Nodurile care nu sunt produse le ștergem (cu o regulă generală de substituire cu șirul vid).
Primul aspect e rezolvat astfel (observați cum match nu mai selectează rădăcina, ci produsele):
Produsele se substituie;
Dintre restul nodurilor se conservă doar nodurile de tip text (inclusiv cele invizibile)! Aceasta e o
regulă implicită de funcționare în XSLT: orice nod care nu e afectat de nici o substituire e
eliminat, cu excepția nodurilor text.
Deci mai trebuie să punem o regulă care să elimine și aceste noduri text.
A doua regulă se aplică tuturor nodurilor de tip text și, neconținând nimic, le substituie pe toate cu șirul
vid, deci le șterge.
Obs1. În atributele match ni se permite să nu mai punem cele 2 slashuri, deci match="text()" e echivalent
cu match="//text()". Acest lucru e permis deoarece oricum valoarea lui match va fi considerată cale
absolută de căutare. E o mică abatere de la sintaxa XPath standard, dar e suportată de XSLT datorită
frecvenței cu care se fac match-uri prin căutare (fără să știm exact unde e elementul de care avem
nevoie).
Dacă percepem documentul XML ca o structură de date (sau chiar ca o bază de date), interogările XPAth
joacă rolul lui SQL, iar transformările XSLT joacă rolul unui generator de rapoarte. "Rapoartele" finale
sunt fie pagini Web (caz în care XSLT joacă rolul PHPului), fie bucăți de pagini Web (tabele, structuri de
div-uri) ce urmează să fie inserate într-o pagină mai mare cu metode Ajax.
Modificăm în continuare exemplul pentru a genera din codul XML o pagină HTML mai cuprinzătoare,
care să conțină:
Un titlu;
Am amintit la un moment dat că un document XML nu ar trebui să conțină noduri calculate (de ex.
totaluri) decât dacă e într-un stadiu final, gata de listare/afișare pentru userul final. Nodurile calculate de
obicei se generează la momentul producerii formei finale a documentului (ca la generarea de rapoarte
pentru baze de date). În cazul de față am putea prelua totalul direct din documentul original. Vom
presupune totuși că el nu există acolo și îl vom calcula în cadrul transformării.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:template match="/">
<html>
<head>
<style>
#cap {color:red}
</style>
</head>
<body>
<h1>Lista produselor din comanda</h1>
<table border="1">
<tr id="cap"><td>Denumire</td><td>Pret</td></tr>
<xsl:for-each select="comanda/produs">
<tr>
<td>
<xsl:value-of
Observați că s-a generat o pagină Web completă, cu tot cu CSS (ar putea conține chiar și JavaScript!) Din
acest exemplu reiese posibilitatea de a folosi XSLT ca generator dinamic de pagini Web, asemănător cu
rolul pe care îl are PHP!
Regula de conservare
S-a văzut la început că XSLT nu conservă implicit codul XML original. Dacă dorim să se conserve anumite
elemente, trebuie să creăm o regulă de conservare care să substituie elementele cu ele însele:
Această regulă substituie elementul rădăcină cu el însuși, producând o copie a documentului original
(pentru execuție, modificați scenariul de transformare să nu se mai returneze html în browser).
<xsl:template match="/">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
Această regulă produce tot o copie a întregului document, dar elimină nodurile invizibile.
<xsl:template match="/comanda/produs">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
Această regulă conservă toate elementele produs. Pe lângă ele, datorită regulii implicite, se conservă și
nodurile text din afara produselor. Studiați diferența față de următorul exemplu:
<xsl:template match="/comanda/produs">
<xsl:copy/>
</xsl:template>
</xsl:stylesheet>
În acest caz, am folosit xsl:copy în loc de xsl:copy-of. Diferența e că se conservă produsele dar FĂRĂ
conținut și atribute! Această tehnică se folosește când vrem să conservăm elemente existente dar să le
redefinim conținutul și atributele:
<xsl:template match="/comanda/produs">
<xsl:copy>
<xsl:attribute name="NouAtribut">NouaValoare</xsl:attribute>
<NouFiu>Nou continut</NouFiu>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Nu e obligatoriu să folosim xsl:copy pentru conservare. Putem pur și simplu să retastăm elementul
original, cu modificările dorite aplicate asupra lui:
<xsl:template match="/comanda/produs">
<produs PretNou="{@pret|pret}">
<NouFiu>Nou continut</NouFiu>
</produs>
</xsl:template>
</xsl:stylesheet>
Observați cum am creat un atribut PretNou care își ia valoarea fie din atributul vechi pret, fie din
elementul pret (depinde pe care-l gasește).
<xsl:template match="/comanda/produs">
<xsl:copy>
<xsl:value-of select="@pret|pret"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
În acest exemplu, conținutul produselor e redefinit să fie egal cu prețul (luat fie din atributul pret, fie din
elementul-fiu pret, de la caz la caz).
În următorul caz conservăm produsele, dar și conținutul lor textual.
<xsl:template match="/comanda/produs">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Instrucțiunea xsl:apply-templates se utilizează de obicei împreună cu xsl:copy și are rolul ca, după ce s-a
făcut copia goală a elementului, să o umple cu rezultatul eventualelor reguli care mai există pentru fiii
elementului. În cazul de față, neexistând alte reguli, se aplică regula implicită de conservare a nodurilor
de tip text, deci obținem produsele fără atribute dar cu conținutul textual. Evident, avem și posibilitatea
de a defini reguli proprii care să fie invocate de xsl:apply-templates:
<xsl:template match="/comanda/produs">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="produs/*|produs/text()[normalize-space(.)!='']">
Aici a fost candva un fiu (vizibil)
</xsl:template>
</xsl:stylesheet>
De data aceasta, xsl:apply-templates caută dacă există reguli pentru fiii produselor și, când o găsește pe
a doua (aplicabilă fiilor de tip element și fiilor de tip text vizibil), o aplică. Conform acesteia, toți fiii din
produs care sunt ori element, ori nod text vizibil, vor fi înlocuiți cu textul din interiorul regulii.
<xsl:template match="/comanda">
<xsl:copy>
<xsl:attribute name="client">Pop Ion</xsl:attribute>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="produs">
<xsl:copy>
<xsl:attribute name="PretNou">
<xsl:value-of select="@pret|pret"/>
</xsl:attribute>
<ID><xsl:value-of select="@id|id"/></ID>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="text()[normalize-space(.)!='']"/>
</xsl:stylesheet>
Prima regulă:
le creează atributul PretNou cu valoarea luată din prețurile existente (fie din atributul vechi pret,
fie din elementul pret)
le adaugă subelemente ID cu valoarea luată din id-urile vechi (atribute sau elemente)
și mai adaugă rezultatul eventualelor reguli care mai există pentru fiii produselor.
A treia regulă caută noduri text vizibile și le șterge. Ea e apelată atât de prima regulă (ștergând nodurile
600 și "Aceasta este o comanda" care s-ar crea datorită regulii implicite de conservare a textului) cât și
de a doua regulă (ștergând textul vechi din produse, care s-ar conserva datorită aceleiași reguli
implicite).
Toate nodurile din documentul original care nu intră sub incidența nici unei reguli de substituire
se elimină, cu excepția nodurilor text care se conservă (și concatenează între ele);
Dacă un același nod e afectat de mai multe reguli de substituire, i se aplică regula cu match-ul
cel mai specific (ex: match="comanda/produs" va avea prioritate față de match="produs", care e
mai specific decât match="node()").
1. XSLT 1.0 e suportat de orice browser. Foaia de transformare se atașează documentului XML original
într-o manieră asemănătoare cu modul de atașare a unei foi de stiluri CSS, apoi pur și simplu se deschide
documentul original în browser și el va apare gata transformat.
2. În context Ajax, putem aplica XSLT din JavaScript asupra răspunsului XML venit de la server, pentru a
produce o bucată de cod HTML inserabilă în pagină.
Procedura implică browser sniffing căci are sintaxă diferită de la un browser la altul;
Transformarea XSLT executată în JavaScript (deci în browser) poate substitui operațiile clasice de
manipulare a paginii prin JavaScript.
3. Putem aplica XSLT și pe server, inclusiv în PHP, astfel încât să livrăm spre JavaScript un răspuns gata
transformat, gata de inserat (de ex. cu Ajax.Updater).
Transformarea XSLT executată în PHP e frecvent folosită când codul XML a sosit în PHP din surse
externe (de la un serviciu Web, alte servere, un depozit de documente) și dorim să folosim în
site doar anumite informații din codul original (ca și în cazul JavaScript, XSLT devine o alternativă
la operațiile clasice de manipulare DOM).
În practică, cel mai des întâlnite sunt transformările pe server, deoarece documentele XML circulă mai
mult între servere decât între server și client (unde JSON tinde să fie preferat).
Din acest moment, dacă deschideți fișierul XML în browser, veți vedea pagina HTML rezultată din
transformarea sa.
Lab 7
Construirea unui răspuns XML în PHP se poate realiza pe mai multe căi:
construire nod cu nod a unui arbore DOM (metodă folosită dacă datele provin din baza de date
și trebuie convertite în XML);
generarea de cod XML nou dintr-un fișier XML existent, printr-o transformare XSLT sau XQuery.
Dintre acestea primele două sunt mai simple, ultimele două însă sunt mult mai frecvent folosite,
deoarece cel mai des XMLul se construiește din date obținute din alte surse (bază de date, servicii Web,
depozit de documente), mai rar este disponibil gata de utilizare într-un string sau fișier. Sunt totuși și
situații în care XMLul e disponibil pe server în fișiere, de exemplu când se lucrează cu depozite de
documente XML (am sugerat la curs că ele pot teoretic substitui funcționalitatea unei baze de date
relaționale).
Odată ajuns în JavaScript, datele pot fi extrase din răspunsul XML prin:
transformări XSLT (caz în care și foaia de transformare trebuie cerută de la server, preferabil tot
prin AJAX pentru a nu întrerupe procesele din pagină!)
interogări Xpath;
Vom prezenta câteva din aceste combinații posibile, plus metoda în care datele sunt transferate în
format JSON, care e cea mai facilă, dar nu are posibilități la fel de complexe (nu putem aplica
transformări XSLT sau validări pe JSON, ci apelăm doar la programare clasică – parcurgere de vectori și
obiecte).
Varianta server 1. Codul XML e disponibil pe server într-un string
Varianta cea mai simplă este atunci când avem deja codul XML stocat într-un string PHP (scrieți-l pe un
singur rând ca să nu apară noduri invizibile și salvați cu numele xml.php):
<?php
header('Content-type:application/xml');
print $sirxml;
?>
Obs:
header setează tipul de răspuns pe care PHP îl dă browserului – dacă nu setăm tipul XML,
browserul îl va recepționa ca text simplu și va trebui realizat efort suplimentar în JavaScript
pentru a-l converti în XML;
Puteți verifica faptul că în browser ajunge un arbore DOM și nu un simplu string, accesând direct scriptul
de mai sus (http://localhost/xml.php), apoi eliminați linia header și vedeți cum arată răspunsul în
browser în lipsa declarației Content-type!)
Construim o pagină HTML care să solicite acest cod XML prin Prototype, la trecerea mouseului peste un
DIV:
<!DOCTYPE html>
<html>
<head>
<script src="scriptaculous/lib/prototype.js"></script>
<script>
function cheamaxml()
config={method:"GET",onSuccess:proceseaza}
new Ajax.Request("xml.php",config)
function proceseaza(xhr)
alert(xhr.responseText)
</script>
</head>
<body>
</div>
</body>
</html>
Obs:
nu am pus parameters, căci nu trimitem nimic la server, doar solicităm răspunsul acestuia (deci
e un răspuns static, care nu e influențat de trimiterea de date din browser);
metoda XSLT:
o avantaj: putem genera cod HTML fără a interacționa (prea mult) cu arborele DOM al
paginii (mare pare din rolul JavaScript e preluat de XSLT);
metoda Xpath:
o avantaj: interogări mult mai concise decât funcțiile DOM, și cu rezultate mai complexe;
o dezavantaj: funcțiile JavaScript care găzduiesc interogările (și le preiau rezultatele) sunt
încă prost implementate (diferă mult între browsere, au sintaxă greoaie);
metoda E4X:
o dezavantaj: nesuportat de IE, incomplet suportat de alte browsere; dacă tot dorim să
folosim o sintaxă simplificată, lucrăm de la bun început cu JSON.
În pagina HTML, în funcția de procesare a răspunsului, folosiți mesaje alert pentru a testa diverse
metode de extragere a datelor din arborele DOM al răspunsului XML (testați alertele pe rând, câte una,
eu le scriu pe toate în aceeași funcție, cu explicații sub formă de comentarii):
function proceseaza(xhr)
raspuns=xhr.responseXML
radacina=raspuns.documentElement
alert(radacina.nodeName)
alert(radacina.firstChild.nodeName)
alert(radacina.firstChild.nodeValue)
alert(radacina.firstChild.firstChild.nodeValue)
alert(radacina.getAttribute('Client'))
alert(radacina.getElementsByTagName('Produs')[0].childNodes[0].nodeValue)
alert(radacina.getElementsByTagName('Produs')[1].childNodes[0].nodeValue)
/*afiseaza continuturile textuale ale celor doua produse: Televizor, apoi Ipod;
din nou, vedeti necesitatea de a accesa textul ca nod fiu ( cu firstChild sau, mai general, vectorul
childNodes), căci în DOM textul nu e considerat valoare a elementului, ci nod fiu*/
alert(radacina.getElementsByTagName('Produs')[0].firstChild.nodeName)
//afiseaza numele nodului text din primul Produs - #text, acesta e numele standard al nodurilor text
alert(radacina.getElementsByTagName('Produs')[0].parentNode.nodeName)
alert(radacina.childNodes[0].nextSibling.getAttribute('ID'))
alert(radacina.firstChild.attributes.length)
alert(radacina.firstChild.attributes[1].value)
/*afiseaza valoarea celui de-al doilea atribut al primului fiu al radacinii - 100
alert(radacina.firstChild.attributes[1].name)
alert(radacina.hasAttributes())
alert(radacina.hasAttribute('Client'))
alert(radacina.firstChild.firstChild.hasChildNodes())
//indica daca primul fiu al primului fiu al radacinii are proprii fii - false
alert(raspuns.nodeType)
alert(radacina.nodeType)
alert(radacina.firstChild.firstChild.nodeType)
//afiseaza tipul primului fiu al primului fiu al radacinii - 3 (nod de tip text)
alert(radacina.firstChild.attributes[1].nodeType)
alert(radacina.firstChild.firstChild.isElementContentWhitespace)
//afiseza true la nodurile text care sunt invizibile; foarte utila la eliminarea lor! – aici false
alert(radacina.getElementById('P1').firstChild.nodeValue)
standardul DOM nu recunoaste atributul ID, decat daca acesta e insotit de un vocabular ce il declara ca
fiind de tip ID; cum obiectul XHR nu executa validare XML, nu putem folosi getElementById!!!*/
Obs:Spre deosebire de arborele DOM al paginii HTML, unde getElementById e foarte util, aici nu poate fi
folosit datorită limitării explicate în comentariu. Suntem nevoiți să căutăm IDul pe alte căi- înlocuiți
ultima alertă cu un ciclu FOR:
produse=radacina.getElementsByTagName('Produs')
for (i=0;i<produse.length;i++)
if (produse[i].getAttribute('ID')=='P1')
alert(produse[i].firstChild.nodeValue)
…sau, dacă tot avem Prototype la îndemână, cu o funcție iteratoare ce detectează primul element (din
vector) care respectă o condiție
produse=radacina.getElementsByTagName('Produs')
prod=$A(produse).find(testeaza)
//trece toate elementele din produse prin functia testeaza pana cand testeaza returneaza true
alert(prod.firstChild.nodeValue)
…evident, e nevoie să definim și funcția-argument prin care trec toate elementele vectorului:
function testeaza(elem)
return elem.getAttribute('ID')=='P1'
Obs importante:
funcția $A(), oferită de Prototype, convertește un obiect într-un vector; poate părea
redundant, căci știm că funcția getElementsByTagName returnează vectori oricum, dar lucrul
acesta e valabil doar pe arborele paginii (HTML DOM) nu și pe arborele răspunsului XML (aici
funcția returnează un obiect-colecție – chiar dacă putem să-l parcurgem cu FOR, nu putem să-l
trecem printr-o funcție iteratoare până nu facem o conversie explicită de la obiect la vector!).
nu putem folosi optimizările sintatice Prototype în locul funcțiilor DOM (nu va funcționa $$()
în locul lui getElementsByTagName, când e vorba de XML); chiar dacă au același nume,
funcțiile DOM se comportă puțin diferit când le executăm asupra paginii HTML și când le
executăm asupra unui cod XML.
Rezumat:
textele dintr-un cod XML nu sunt considerate valori ale elementelor ce le conțin, ci valori ale
nodurilor-fiu (de tip text) ale elementelor respective!
getElementById nu se poate folosi pe un arbore DOM provenit din XML (decât dacă e validat
față de un vocabular, operație pe care XHR nu o asigură);
funcțiile optimizate Prototype nu se pot folosi asupra arborelui DOM provenit din XML…;
…în schimb funcțiile standard DOM pot fi folosite și asupra paginii HTML! (dar de obicei nu e
nevoie, avem funcțiile Prototype sau JavaScript mai ușor de folosit)
câteva instrumente esențiale:
o length (aplicat lui childNodes sau attributes) – numărul de noduri sau atribute
Toate aceste exemple sunt funcții de ACCES (ele obțin informații de la arbore, dar nu îl modifică).
Standardul DOM oferă și o gamă largă de funcții de MANIPULARE (care modifică structura arborelui
DOM) – acestea sunt importante acolo unde se construiește cod XML, ceea ce se întâmplă mai des pe
server decât în JavaScript. Totuși, destul de frecvent se face asta și în JavaScript, când dorim să
construim cod HTML prin metode XML, ca alternativă la inserarea de conținut nou cu innerHTML!
Vom modifica exemplul astfel încât, în locul multiplelor alerte, să generăm în pagină (în DIVul gol cu
ID='x') un tabel HTML de forma:
Denumire Pret
Televizor 100
Ipod 30
Total:130
function proceseaza(xhr)
raspuns=xhr.responseXML
radacina=raspuns.documentElement
//citeste raspunsul
client=radacina.getAttribute('Client')
tabel="<table border='1'><tr><td>Denumire</td><td>Pret</td></tr>"
total=0
produse=radacina.getElementsByTagName('Produs')
for (i=0;i<produse.length;i++)
{
tabel=tabel+"<tr><td>"+produse[i].firstChild.nodeValue+"</td>"
tabel=tabel+"<td>"+produse[i].getAttribute("Pret")+"</td></tr>"
total=total+parseInt(produse[i].getAttribute("Pret"))
//pentru fiecare produs gasit, adauga un rand nou la codul HTML al tabelului si actualizeaza totalul
locatie=document.getElementById("x")
locatie.innerHTML=titlu+tabel+subsol
Obs:
parseInt converteste un string intr-un numar (avand in vedere ca valorile extrase din XML sunt
implicit tratate ca stringuri);
b.Inserarea datelor în pagină nod cu nod (funcții DOM standard găzduite de JavaScript)
function proceseaza(xhr)
raspuns=xhr.responseXML
radacina=raspuns.documentElement
client=radacina.getAttribute("Client")
titlu=document.createElement("h1")
text=document.createTextNode("Comanda lui"+client)
titlu.appendChild(text)
tabel=document.createElement("table")
tabel.setAttribute("border","1")
captabel=document.createElement("tr")
celula=document.createElement("td")
textcelula=document.createTextNode("Denumire")
celula.appendChild(textcelula)
captabel.appendChild(celula)
celula=document.createElement("td")
textcelula=document.createTextNode("Pret")
celula.appendChild(textcelula)
captabel.appendChild(celula)
tabel.appendChild(captabel)
//s-a adaugat capul de tabel la tabel
total=0
produse=radacina.getElementsByTagName('Produs')
for (i=0;i<produse.length;i++)
rand=document.createElement("tr")
celula=document.createElement("td")
text=document.createTextNode(produse[i].firstChild.nodeValue)
celula.appendChild(text)
rand.appendChild(celula)
celula=document.createElement("td")
text=document.createTextNode(produse[i].getAttribute("Pret"))
celula.appendChild(text)
rand.appendChild(celula)
tabel.appendChild(rand)
total=total+parseInt(produse[i].getAttribute("Pret"))
}
subsol=document.createElement("tr")
celula=document.createElement("td")
celula.setAttribute("colspan","2")
celula.setAttribute("align","right")
text=document.createTextNode("Total:"+total)
celula.appendChild(text)
subsol.appendChild(celula)
tabel.appendChild(subsol)
locatie=document.getElementById("x")
locatie.appendChild(titlu)
locatie.appendChild(tabel)
Obs importante:
Se observă că adăugarea de conținut nod cu nod e mai anevoioasă decât inserarea cu innerHTML. Totuși
e importantă din 2 motive:
o avem un loc rezervat în pagină (un DIV gol) gata să primească noul conținut;
o nou conținut e disponibil sau poate fi creat (prin concatenări) sub formă de string;
când nu există un nod gol rezervat iar conținutul nu e disponibil ca string, avem variantele:
o să facem inserare ca ultim fiu (ne poziționăm pe părintele locației în care inserăm și
folosim appendChild);
o să facem inserare ca frate precedent (ne poziționăm pe fratele înaintea căruia vrem să
inserăm și folosim insertBefore);
Metoda cu funcții DOM standard este mai des folosită la nivelul serverului (PHP) unde se construiesc
adesea răspunsuri XML în această manieră.
Am văzut la început funcțiile de ACCESARE (citire) a informațiilor din arbore. Acum observați funcțiile de
MANIPULARE (scriere):
o începătorii fac adesea gafa de a crede că aceste funcți asigură și alipirea nodurilor la
document! alipirea se face cu funcțiile de mai jos:
appendChild, insertBefore (apelate la nivel de nod) – prin acestea se "lipesc" noduri noi la
arborele DOM; appendData, insertData (la nivel de nod text) – se adaugă text suplimentar în
nod;
replaceChild (apelat de la nivelul unui element), replaceData(apelat de la nivelul unui nod text)
– permite substituirea conținutului;
Toate aceste funcți pot returna nodul creat/inserat/modificat/sters (dacă vrem să-i aducem ulterioare
modificări)! De exemplu dacă vrem să mutăm un nod dintr-o poziție în alta ne putem baza că
removeChild ni-l returnează după ce îl șterge din poziția originală, deci îl putem reinsera ușor în altă
poziție (fără să-l mai creăm o dată).
function proceseaza(xhr)
raspuns=xhr.responseXML
radacina=raspuns.documentElement
client=radacina.getAttribute('Client')
titlu=new Element("h1")
titlu.update("Comanda lui"+client)
//crearea titlului
tabel=new Element("table",{border:1})
rand=new Element("tr")
celula=new Element("td")
celula.update("Denumire")
rand.insert(celula)
celula=new Element("td")
celula.update("Pret")
rand.insert(celula)
tabel.insert(rand)
produse=radacina.getElementsByTagName('Produs')
vectproduse=$A(produse)
vectproduse.each(genereaza)
subsol=new Element("tr")
celula=new Element("td",{align:"right",colspan:2})
celula.update("Total:"+total)
tabel.insert(celula)
//crearea subsolului
$('x').insert(titlu)
$('x').insert(tabel)
//inserarea in DIV
//in continuare, urmeaza functia iteratoare care genereaza randurile tabelului si calculeaza totalul
function genereaza(elem)
rand=new Element("tr")
celula=new Element("td")
celula.update(elem.firstChild.nodeValue)
rand.insert(celula)
celula=new Element("td")
celula.update(elem.getAttribute("Pret"))
rand.insert(celula)
tabel.insert(rand)
total=total+parseInt(elem.getAttribute("Pret"))
Obs.:
funcțiile update (substituire de conținut) și insert (adăugare) pot fi folosite pe noduri XML la fel
ca pe noduri HTML (ca alternativă la appendChild/insertBefore)! cele două funcții pot fi
apelate în oricare din formele:
o Element.update(țintă,conținut-nou)
o țintă.update(conținut-nou)
o $('id-țintă').update(conținut-nou)
după cum s-a văzut deja, funcția $A e necesară pentru a face ca vectorul rezultat din
getElementsByTagName să poată fi procesat de Prototype (în acest caz cu o funcție
iteratoare).
O altă metodă de a insera datele răspunsului XML în pagina Web e să aplicăm o transformare XSLT la
nivelul browserului. Toate browserele moderne suportă transformări XSLT 1.0 (bazate pe interogări
Xpath 1.0) dar legat de versiunea 2.0 viitorul este incert.
O primă problemă e că transformarea XSLT trebuie adusă de la server tot într-o manieră asincronă, deci
prin Ajax.Request (dacă lucrăm pe Prototype). Deci vom avea două comunicări asincrone – una care
aduce răspunsul XML și una care aduce foaia XSLT. Odată ajunse ambele în browser, executăm
transformarea.
Pas1. Salvăm următoarea foaie XSLT pe server (în htdocs) cu numele transformare.xsl:
Pas2. Modificăm scriptul xml.php: dacă primește prin GET variabila "foaie", deschide de pe disc foaia
XSLT și o livrează browserului. Dacă nu, livrează codul XML din exemplele precedente, luat din același
string (nu dați Enteruri în el):
<?php
header('Content-type:application/xml');
if (isset(GET["foaie"])
{
$foaie=new DOMDocument();
$foaie->load('transformare.xsl');
print $foaie->saveXML();
else
print $sirxml;
?>
Pas3. Scriem pagina client (scrieți una nouă, căci vom reveni la cea precedentă):
<!DOCTYPE html>
<html>
<head>
<script src="scriptaculous/lib/prototype.js"></script>
<script>
function cheamaxml()
config={method:"GET",onSuccess:cheamaxslt}
new Ajax.Request("xml.php",config)
/*
S-a solicitat raspunsul XML de la xml.php.
*/
function cheamaxslt(xhr)
raspunsxml=xhr.responseXML
config={method:"GET",parameters:"foaie",onSuccess:proceseaza}
new Ajax.Request("xml.php",config)
/*
S-a solicitat foaia XSLT, folosind variabila foaie, fara valoare - e suficient ca e trimisa ( caci scriptul PHP
testeaza doar existenta ei, cu if (isset($_GET['foaie'])) )
*/
function proceseaza(xhr)
raspunsxslt=xhr.responseXML
if (Prototype.Browser.IE)
rezultat=raspunsxml.transformNode(raspunsxslt)
$("x").update(rezultat)
else
procesorxslt=new XSLTProcessor()
procesorxslt.importStylesheet(raspunsxslt)
rezultat=procesorxslt.transformToFragment(raspunsxml,document)
$("x").appendChild(rezultat)
</script>
</head>
<body>
</div>
</body>
</html>
Obs:
Extragerea datelor cu Xpath direct din JavaScript e teoretic facilă (în ce privește limbajul Xpath) dar
practic e destul de complicată (datorită funcțiilor JavaScript care găzduiesc interogările Xpath și le
gestionează rezultatele). În plus, sunt diferențe mari între browsere în ce privește aceste funcții.
Reveniți la pagina client așa cum arăta înainte de exemplul cu XSLT (cu un singur Ajax.Request). Scriptul
PHP poate sa rămână cel cu ultimele modificari, căci conține un IF care ne va da codul XML inițial cu
condiția să nu trimitem variabila GET "foaie").
Vom rescrie funcția proceseaza() pentru a extrage diverse informații din XML cu XPath.
function proceseaza(xhr)
raspuns=xhr.responseXML
radacina=raspuns.documentElement
rez=raspuns.evaluate('count(Produs)',radacina,null,XPathResult.ANY_TYPE,null)
/*detalii:
- functia trebuie apelata de catre documentul pe care se face interogarea (aici, raspuns);
- tipul iterator pentru interogari ce returneaza noduri sau colectii de noduri (chiar daca e returnat un
singur nod sau atribut!)
- ultimul argument e neimportant (e variabila in care sa se stocheze rezultatul, dar in cazul de fata
indicam asta prin atribuire)
- odata ce s-a obtinut un rezultat, valoarea acestuia se extrage prin diferite metode in functie de tipul
rezultatului
*/
/*va afisa 1, codul corespunzator tipului numeric; tipul numeric e returnat de:
- functii XPath precum count sau sum (dacă am pus argumentul ANY_TYPE)
*/
rez=raspuns.evaluate('name(//@*[.=100])',radacina,null,XPathResult.ANY_TYPE,null)
/*va afisa 2, codul corespunzator tipului string; tipul string e returnat de:
- functii XPath ce returnează stringuri (dacă am pus argumentul ANY_TYPE)
*/
//va afisa valoarea string a rezultatului - Pret (numele atributului cu valoarea 100)
rez=raspuns.evaluate('Produs="Televizor"',radacina,null,XPathResult.ANY_TYPE,null)
/*va afisa 3, codul corespunzator tipului boolean; tipul boolean e returnat de:
*/
//va afisa valoarea booleana a rezultatului - true (este adevarat ca exista un Produs cu valoarea
Televizor)
rez=raspuns.evaluate('Produs',radacina,null,XPathResult.ANY_TYPE,null)
/*va afisa 4, codul corespunzator tipului iterator (set de noduri); tipul iterator e returnat de orice
interogare care returnează noduri sau atribute (și nu rezultate de funcții XPath), chiar dacă s-a găsit o
singură soluție:
- după returnare, rezultatul se parcurge cu un ciclu FOR sau WHILE, care apeleaza in mod repetat functia
iterateNext() ce returneaza urmatorul nod din setul rezultat
*/
produs=rez.iterateNext()
while (produs)
produs=rez.iterateNext()
rez=raspuns.evaluate('..',radacina.firstChild.firstChild,null,XPathResult.ANY_TYPE,null)
- in acest caz am folosit ca nod curent primul nepot al radacinii, iar prin interogare am solicitat
parintele sau
*/
alert(rez.iterateNext().nodeName)
rez=document.evaluate('div[@id]',document.body,null,XPathResult.ANY_TYPE,null)
diferențe:
alert(rez.iterateNext().innerHTML)
//va afisa conținutul din DIVul care are ID ("Aici se va insera informatia din XML")
Pentru Internet Explorer implementarea e mult mai simplă. Diferențele principale sunt:
se folosesc două funcții: selectNodes (pentru interogări care returnează mai multe noduri) sau
selectSingleNode (pentru interogări care returnează un nod);
ambele funcții returnează noduri XML (nu trebuie să indicăm tipul rezultatului);
ambele funcții folosesc ca nod curent obiectul care le apelează (nu trebuie să indicăm nodul
curent);
limitări:
o nu se pot executa interogări ce returnează valori calculate (cu funcții XPath) ci numai
interogări ce returnează noduri/valori de noduri;
o nu se pot interoga elemente din pagina HTML, cu numai din răspunsul XML.
function proceseaza(xhr)
raspuns=xhr.responseXML
radacina=raspuns.documentElement
raspuns.setProperty("SelectionLanguage","XPath")
/*aceasta linie e necesara, altfel IE va numerota nodurile incepand de la zero (iar XPath le numara de la
1!)*/
rez=radacina.selectNodes("Produs")
for (i=0;i<rez.length;i++)
//s-au returnat toate produsele și s-au parcurs afișând un mesaj pentru fiecare
rez=radacina.selectSingleNode("Produs[1]/@Pret")
alert(rez.value)
rez2=rez.selectSingleNode("..")
alert(rez2.nodeName)
/*s-a returnat pretul primului produs, care apoi a fost folosit ca nod curent (apelator) in urmatoarea
interogare, ce returneaza parintele acelui pret*/
Obs:
În general se consideră că interogările XPath sunt mai rapide decât accesarea cu funcții DOM
standard (getElement….). De fapt numeroase frameworkuri AJAX (inclusiv Prototype)
apelează, în implementarea unor funcții precum $, $$, la XPath și nu la funcțiile getElement,
ceea ce face ca funcțiile Prototype, deși mult mai puternice, să aibă performanțe
asemănătoare cu getElementsByTagName).
Totuși, dpdv sintactic, XPath încă nu e o soluție larg adoptată – diferențele mari între
browsere (ce necesită dublarea codului sursă) și complicațiile inutile ale sintaxei din Firefox au
descurajat în general adopția acestei tehnici ca alternativă la funcțiile standard DOM.
Varianta E4X e o sintaxă JavaScript mai ușor de folosit decât funcțiile DOM:
radacina.Produs[0]
în loc de
radacina.firstChild.firstChild.nodeValue
radacina.Produs.@Pret
în loc de
radacina.firstChild.getAttribute('Pret')
Nu vom insista totuși asupra acestei sintaxe, deoarece IE refuză deocamdată să o implementeze, iar
celelalte browsere o implementează incomplet. Nu vom insista asupra acestei sintaxe, datorită lipsei de
viabilitate.
sintaxa căilor Xpath (cu condiția ca Xpath să își simplifice modul de executare și stocare a
rezultatelor!) – doar că se folosesc puncte în loc de slashuri;
sintaxa JSON, caz în care e preferabil ca răspunsul să se dea de la bun început direct în JSON.
Ultimele exemple au prezentat diferite moduri de procesare a răspunsului XML după ce acesta a ajuns la
browser. În continuare mutăm discuția la nivelul serverului, pentru a vedea cum lucrează PHP cu
structuri XML.
În cazurile precedente, codul XML a fost livrat de PHP în modul cel mai simplu, ca string. În coninuare
vom prelua codul XML dintr-un fișier existent pe server. Construiți următorul fișier cu Oxygen, și salvați-l
pe server în htdocs cu numele comanda.xml:
încărcăm fișierul într-un obiect de tip XML (clasă numită DOMDocument în PHP);
de obicei fișierele create prin tastare conțin noduri invizibile (Enterurile, Taburile, spațiile
folosite în timpul tastării pentru aranjarea codului vor fi considerate noduri text).
Prezența nodurilor invizibile va afecta traversarea pe bază de frați (nextSibling, previousSibling) sau pe
bază de poziție (childNodes,firstChild,lastChild).
Important: Internet Explorer elimină automat nodurile invizibile, dar alte browsere nu!
Construim scriptul PHP (salvați-l cu numele xml2.php) care deschide acest fișier și îl trimite la browser:
<?php
header('Content-type:application/xml');
$cmdxml=new DOMDocument();
$cmdxml->load('comanda.xml');
print $cmdxml->saveXML();
?>
Obs:
Construim pagina client, similară cu cea din primele exemple (dar acum solicită răspuns de la xml2.php),
și punem câteva alerte pentru a ne convinge de prezența câmpurilor invizibile:
<!DOCTYPE html>
<html>
<head>
<script src="scriptaculous/lib/prototype.js"></script>
<script>
function cheamaxml()
config={method:"GET",onSuccess:proceseaza}
new Ajax.Request("xml2.php",config)
function proceseaza(xhr)
raspuns=xhr.responseXML
radacina=raspuns.documentElement
alert(radacina.firstChild.nodeName)
alert(radacina.firstChild.nodeValue)
alert(radacina.childNodes.length)
alert(radacina.getElementsByTagName('Produs')[0].nextSibling.nodeName)
alert(radacina.firstChild.isElementContentWhitespace)
//testeaza daca primul fiu al radacinii e un nod invizibil - true!
</script>
</head>
<body>
</div>
</body>
</html>
Obs:
Se poate observa că în Internet Explorer, aceleași alerte dau alte rezultate (IE ignoră nodurile
invizibile!)
Pentru cazurile în care nodurile invizibile ne-ar putea influența, se practică 2 abordări:
se creează o funcție recursivă ce parcurge tot arborele DOM și elimină nodurile invizibile
detectate;
se creează o foaie XSLT care transformă arborele DOM într-unul echivalent, dar fără noduri
invizibile.
Ambele strategii se pot aplica atât la client (JavaScript), cât și la server (PHP). Adesea e preferabil să aibă
loc în PHP, pentru a nu se transfera degeaba octeți în plus, care oricum trebuie eliminați.
Vom exemplifica funcția recursivă în JavaScript și transformarea în PHP. Exemplele se pot adapta la orice
limbaj cu modificări minime (date de specificul sintactic).
Funcția recursivă de eliminare a nodurilor invizibile în JavaScript:
function eliminaNoduriInv(nod)
var fiu=nod.childNodes[i]
nod.removeChild(fiu)
if (fiu.nodeType==1)
return nod
Obs:
funcția primește ca argument orice nod și returnează același nod, curățat de noduri invizibile;
e important să apară cuvântul var! în programarea recursivă din JavaScript trebuie să-l punem
datorită particularității JavaScript că funcțiile își pot refolosi variabilele rămase de la apelul
precedent
o altfel spus, se testează dacă nodul este de tip text, iar valoarea lui are măcar un caracter
diferit de cele invizibile (e vorba de o expresie regulată cu codul \S ce reprezintă orice
caracter diferit de cele invizibile);
Pentru a testa succesul acestei funcții, vom afișa codul XML înainte și după transformare. Vom profita de
ocazie pentru a arăta și cum se poate face conversia din XML în string, în JavaScript. Și această operație
are diferențe mari între browsere:
function proceseaza(xhr)
raspuns=xhr.responseXML
radacina=raspuns.documentElement
if (Prototype.Browser.IE)
sirxml=radacina.xml
alert(sirxml)
/*daca ne aflam in IE nu are sens sa aplicam curatarea (totusi, in mesaje alert, codul XML arata ca si cum
n-ar fi fost curatat!)*/
else
serializator=new XMLSerializer()
sirxml=serializator.serializeToString(radacina)
alert("Inainte de curatare:\n "+sirxml)
radacinacurata=eliminaNoduriInv(radacina)
sirxml=serializator.serializeToString(radacinacurata)
Obs:
din nou, am detectat browserul cu Prototype (reminder: în lipsa lui Prototype, testarea
existenței obiectului document.all duce la același rezultat);
IE afișează codul XML ca și cum n-ar fi fost curățat (dar vă puteți convinge că a fost,
numărând/afișând nodurile din document prin alerte cu informații extrase din nodul DOM);
<Comanda Client="PopIon">
</Comanda>
<xsl:template match="/">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>
<?php
header('Content-type:application/xml');
$sursa=new DOMDocument();
$sursa->load('comanda.xml');
$foaie=new DOMDocument();
$foaie->load('transformare2.xsl');
$procesor=new XSLTProcessor();
$procesor->importStylesheet($foaie);
$rezultat=$procesor->transformToXML($sursa);
print $rezultat;
?>
Obs:
instrumentele sunt aceleași cu cele pe care le-am folosit în JavaScript pentru a executa
transformarea sub Firefox:
o clasa XSLTProcessor;
fie cuplați pagina client la acest script și afișați modul în care ajunge răspunsul acestui script în
Firefox (după cum am văzut, în IE nu se vede diferența în alerte);
fie accesați direct acest script din localhost și vă uitați cum arată răspunsul în Firebug (în
browser codul XML va fi afișat formatat, doar în Firebug, la rubrica Net puteți vedea efectiv șirul
de caractere din care lipsesc nodurile invizibile).
Evident, în aceeași manieră putem executa în PHP transformarea ce producea tabelul HTML.
În sfârșit, codul XML poate fi generat pe server și cu funcțiile standard DOM, prin construirea nod cu
nod. Metoda este ceva mai anevoioasă (am aplicat-o deja în JavaScript) dar e garantat că nu creează
noduri invizibile. E folosită adesea pentru a genera dinamic cod XML din date preluate din diverse surse
(array-uri, baze de date etc.)
PHP oferă mai multe clase pentru gestiunea și crearea de cod XML. Dintre acestea, două sunt mai des
folosite:
funcțiile DOM standard (clasa DOMDocument);
Următoarele exemple testați-le direct în browser (http://localhost/fisier.php), fără a mai executa pagina
client (doar cât să vă convingeți cum arată documentul XML generat):
<?php
header("Content-type:application/xml");
$raspuns=new DOMDocument();
$radacina=$raspuns->createElement("Comanda");
$radacina->setAttribute("Client","PopIon");
$produs=$raspuns->createElement("Produs");
$produs->setAttribute("ID","P1");
$produs->setAttribute("Pret","100");
$text=$raspuns->createTextNode("Televizor");
$produs->appendChild($text);
$radacina->appendChild($produs);
$produs=$raspuns->createElement("Produs");
$produs->setAttribute("ID","P2");
$produs->setAttribute("Pret","30");
$text=$raspuns->createTextNode("Ipod");
$produs->appendChild($text);
$radacina->appendChild($produs);
$raspuns->appendChild($radacina);
print $raspuns->saveXML();
?>
Observați linia boldată, prin care elementul rădăcină e inserat în nodul-document (rădăcina reală a
documentului).
<?php
header("Content-type:application/xml");
$radacina=new SimpleXMLElement("<Comanda/>");
$radacina->addAttribute("Client","PopIon");
$produs=$radacina->addChild("Produs","Televizor");
$produs->addAttribute("ID","P1");
$produs->addAttribute("Pret","100");
$produs=$radacina->addChild("Produs","Ipod");
$produs->addAttribute("ID","P2");
$produs->addAttribute("Pret","30");
print $radacina->asXML();
?>
nu mai trebuie să lipim elementul rădăcină la document (funcția asXML() de la final se ocupă de
asta implicit);
crearea și alipirea unui nou element cu conținut textual simplu se face într-o singură linie, cu
addChild (dincolo erau 4 linii: crearea elementul, crearea textului, lipirea textului la element,
lipirea elementului la radacina).
$radacina->$Produs[0]
poate fi folosit pentru a accesa textul conținut în primul produs, ca alternativă la metoda standard:
$radacina->getElementsByTagName("Produs")[0]->firstChild->nodeValuesimplexmlelement
Dacă în loc de XML se preferă transferarea de date în format JSON, totul devine mult mai simplu, și mai
performant (dar nu există posibilități avansate precum transformarea XSLT, interogarea datelor,
validarea cu vocabulare).
În PHP, codul JSON se poate crea ușor din orice array existent.
<?php
header("Content-type:application/json");
$Produs1=array("ID"=>"P1","Pret"=>"100","Denumire"=>"Televizor");
$Produs2=array("ID"=>"P2","Pret"=>"30","Denumire"=>"Ipod");
$Produse=array($Produs1,$Produs2);
$Raspuns=array("Comanda"=>array("Client"=>"PopIon","Produse"=>$Produse));
print json_encode($Raspuns);
?>
Creați pagina client care solicită acest cod și generează un tabel cu denumirile și prețurile produselor:
<!DOCTYPE html>
<html>
<head>
<script>
function cheamajson()
config={method:"GET",onSuccess:proceseaza}
new Ajax.Request("json.php",config)
function proceseaza(xhr)
eval("raspuns="+xhr.responseText)
client=raspuns.Comanda.Client
total=0
produse=raspuns.Comanda.Produse
for (i=0;i<produse.length;i++)
tabel=tabel+"<tr><td>"+produse[i].Denumire+"</td>"
tabel=tabel+"<td>"+produse[i].Pret+"</td></tr>"
total=total+parseInt(produse[i].Pret)
locatie=document.getElementById("x")
locatie.innerHTML=titlu+tabel+subsol
</script>
</head>
<body>
</div>
</body></html>
Obs:
funcția eval asigură conversia răspunsului JSON din text în obiect JavaScript; în practică e
recomandat ca în loc de eval să se folosească o bibliotecă de funcții JSON precum
https://github.com/douglascrockford/JSON-js, care are în plus un mecanism de securitate ce
verifică dacă nu cumva în răspunsul JSON există și comenzi JavaScript (care s-ar executa automat
de către eval);
raspuns.Comanda.Produse[0].Pret în loc de
raspuns.documentElement.getElementsByTagName("Produs")[0].getAttribute("Pret")
avantajele de performanță pot fi măsurate atât în Firebug (numărul de biți și timpul de transfer
comparativ cu XML) sau prin cronometrarea în JavaScript a procedurii de generare a tabelului (în
cazul cronometrării avantajele nu vor fi semnificative pe aceste exemple datorită numărului mic
de date transferate/interogate).