Definirea unui arbore de configurare pentru construcția interfeței utilizator este una din metodele cele mai simple și directe pentru dezvoltarea rapidă a unei aplicații moderne web.
JUL folosește acest arbore de configurare pentru a crea instanțele componentelor și pentru a atribui relații de apartenență părinte-copil într-o secvențialitate controlată și automată.
Recapitulare a separării structurii de logică
Există două proprietăți ale unui obiect de configurare ce permit separarea structurii și a logicii asociate acelei configurații:
- Proprietatea id – identifică unic o componentă. Folosind această proprietate, putem simplifica un arbore de configurare în configurare a structurii în format arborescent și u mapare cheie-valoare cu configurare suplimentară pentru nodurile arborelui. Cheile din această a doua parte (logica de configurare) sunt valorile id. Deoarece partea de logică este un obiect de mapare cu chei, toate cheile (adică id-urile componentelor) trebuie să fie unice în aceasta. Trebuie adăugat că proprietatea id va fi pasată obiectului rezultant de configurare la instanțierea componentei.
var oConfig = { xclass: 'MyApp.widgets.Avatar', id: 'avatar-one', children: [ {xclass: 'MyApp.widgets.Hat', id: 'hat-two'}, {xclass: 'MyApp.widgets.Suit', id: 'suit-three'} ] }; var oLogic = { 'avatar-one': { userId: 121, thumbnail: 'face1.png', onHover: function() { console.log('Avatar hint'); } }, 'hat-two': { color: 'blue', size: 4 }, 'suit-three': { color: 'green', match: /(office|meeting|trip)/ } }; var oParser = new JUL.UI.Parser(); var oAvatar = oParser.create(oConfig, oLogic); // apelurile runtime rezultante sunt echivalentul a: var oHat = new MyApp.widgets.Hat({ id: 'hat-two', color: 'blue', size: 4 }); var oSuit = new MyApp.widgets.Suit({ id: 'suit-three', color: 'green', match: /(office|meeting|trip)/ }); oAvatar = new MyApp.widgets.Avatar({ id: 'avatar-one', children: [oHat, oSuit], userId: 121, thumbnail: 'face1.png', onHover: function() { console.log('Avatar hint'); } });
- Proprietatea id de legătură – servește aceluiași scop ca și proprietatea id dar cu câteva diferențe. Proprietatea de legătură este ștearsă după agregarea porților de structură și de logică. Această proprietate poate fi de asemenea un vector, caz în care configurările adiționale referite de elementele sale sunt aplicate secvențial configurația finală. Astfel de vector este construita automat la utilizarea metodei JUL.UI.include() (a se vedea referința API).
var oConfig = { xclass: 'MyApp.widgets.Avatar', id: 'the-avatar', cid: 'c-avatar', children: [ {xclass: 'MyApp.widgets.Hat', cid: 'c-hat'}, {xclass: 'MyApp.widgets.Suit', cid: 'c-suit'} ] }; var oLogic = { 'c-avatar': { userId: 121, thumbnail: 'face1.png', onHover: function() { console.log('Avatar hint'); } }, 'c-hat': { color: 'blue', size: 4 }, 'c-suit': { color: 'green', match: /(office|meeting|trip)/ } }; var oParser = new JUL.UI.Parser(); var oAvatar = oParser.create(oConfig, oLogic); // apelurile runtime rezultante sunt echivalentul a: var oHat = new MyApp.widgets.Hat({ color: 'blue', size: 4 }); var oSuit = new MyApp.widgets.Suit({ color: 'green', match: /(office|meeting|trip)/ }); oAvatar = new MyApp.widgets.Avatar({ id: 'the-avata', children: [oHat, oSuit], userId: 121, thumbnail: 'face1.png', onHover: function() { console.log('Avatar hint'); } });
Compunere și moștenire avansate
A pune obiecte de configurare în arbore este forma cea mai simplă de a construi structura UI. Dar, cu ajutorul id-ului de legătură șo a metodei JUL.UI.include(), parser-ul poate combina în moduri avabsate configurațiile componentelor.
- Prima metodă este includerea unui obiect de configurare mai simplu într-unul mai complex. Acest proces este împărțit în două: includerea configurației de bază în structura de destinație și părții de logică a configurației de bază în logica de configurare a configurației curente. Apoi, putem repeta acest proces de mărite (extindere) a unui obiect de configurare de câte ori avem nevoie. Un exemplu al acestei metode este dat mai jos.
var MyApp = { config: {}, objects: {}, version: '1.0' }; MyApp.config.shapeUi = { xclass: 'MyLib.Shape', cid: 'mylib.shape', origin: {x: 0, y: 0} }; MyApp.config.shapeLogic = { 'mylib.shape': { color: 'grey', bgColor: 'white', getOrigin: function() { console.log('Shape origin'); } } }; MyApp.config.polygonUi = { include: MyApp.config.shapeUi, xclass: 'MyLib.Polygon', cid: 'mylib.polygon', corners: 0, sizes: [] }; MyApp.config.polygonLogic = { include: MyApp.config.shapeLogic, 'mylib.polygon': { getArea: function() { console.log('Polygon area'); } } }; MyApp.config.triangleUi = { include: MyApp.config.polygonUi, xclass: 'MyLib.Triangle', cid: 'mylib.triangle', corners: 3, sizes: [3, 4, 5] }; MyApp.config.triangleLogic = { include: MyApp.config.polygonLogic, 'mylib.triangle': { draw: function() { console.log('Triangle draw'); }, getArea: function() { console.log('Triangle area'); } } }; var oParser = new JUL.UI.Parser(); MyApp.objects.triangle = oParser.create(MyApp.config.triangleUi, MyApp.config.triangleLogic); // apelurile runtime rezultante sunt echivalentul a: MyApp.objects.triangle = new MyLib.Triangle({ origin: {x: 0, y: 0}, corners: 3, sizes: [3, 4, 5], color: 'grey', bgColor: 'white', getOrigin: function() { console.log('Shape origin'); }, getArea: function() { console.log('Triangle area'); }, draw: function() { console.log('Triangle draw'); } });
Acest tip de extindere se numește moștenire explicită spre deosebire de moștenirea bazată pe prototipuri sau pe clase pe care o numim moștenire implicită. Deși am putea folosi moștenirea implicită pentru extinderea unei configurații de componentă, JUL folosește moștenirea explicită pentru a asigura o serializare consistentă a obiectelor de configurare. Deoarece JUL.UI.obj2str() folosește intern JSON pentru a serializa obiectele, o moștenire bazată pe prototipuri ar complica procesul de serializare, date fiind diferențele prototipurilor obiectelor de-a lungul diverselor medii runtime.
- A doua metodă este în introducerea de configurații de bază în diverse puncte în arborele de configurare, numită compunerea componentelor. Procesul constă în referirea către configurațiile de bază cu proprietatea include a nodului respectiv și adăugarea logicii configurației de bază la vectorul include a mapării logicii de destinație. Un exemplu de astfel de proces este dat nai jos.
var MyApp = { config: {}, objects: {}, version: '1.0' }; MyApp.config.circleUi = { xclass: 'MyLib.Circle', cid: 'mylib.circle', origin: {x: 0, y: 0}, diameter: 1 }; MyApp.config.circleLogic = { 'mylib.circle': { fillColor: 'white', draw: function() { console.log('Draw circle'); } } }; MyApp.config.rectangleUi = { xclass: 'MyLib.Rectangle', cid: 'mylib.rectangle', origin: {x: 0, y: 0}, dimensions: {w: 0, h: 0}, }; MyApp.config.rectangleLogic = { 'mylib.rectangle': { fillColor: 'blue', draw: function() { console.log('Draw rectangle'); } } }; MyApp.config.logoUi = { include: MyApp.config.rectangleUi, id: 'the-logo', dimensions: {w: 10, h: 7}, children: [ {include: MyApp.config.rectangleUi, id: 'inner-area', origin: {x: 1, y: 1}, dimensions: {w: 8, h: 5}, children: [ {include: MyApp.config.circleUi, id: 'left-disc', origin: {x: 2, y: 2}, diameter: 3}, {include: MyApp.config.circleUi, id: 'right-disc', origin: {x: 2, y: 5}, diameter: 3} ]} ] }; MyApp.config.logoLogic = { include: [MyApp.config.circleLogic, MyApp.config.rectangleLogic], 'the-logo': { draw: function() { console.log('Draw logo'); } }, 'inner-area': { fillColor: 'yellow' }, 'left-disc': { fillColor: 'red' }, 'right-disc': { fillColor: 'green' } }; var oParser = new JUL.UI.Parser(); MyApp.objects.logo = oParser.create(MyApp.config.logoUi, MyApp.config.logoLogic); // apelurile runtime rezultante sunt echivalentul a: var oLeft = new MyLib.Circle({ id: 'left-disc', origin: {x: 2, y: 2}, diameter: 3, fillColor: 'red', draw: function() { console.log('Draw circle'); } }); var oRight = new MyLib.Circle({ id: 'right-disc', origin: {x: 2, y: 5}, diameter: 3, fillColor: 'green', draw: function() { console.log('Draw circle'); } }); var oInner = new MyLib.Rectangle({ id: 'inner-area', origin: {x: 1, y: 1}, dimensions: {w: 8, h: 5}, children: [oLeft, oRight], fillColor: 'yellow', draw: function() { console.log('Draw rectangle'); } }); MyApp.objects.logo = new MyLib.Rectangle({ id: 'the-logo', origin: {x: 0, y: 0}, dimensions: {w: 10, h: 7}, children: [oInner], fillColor: 'blue', draw: function() { console.log('Draw logo'); } });
Compunerea componentelor poate fi combinată cu moștenirea explicită pentru a obține includeri complexe ale arborilor de configurare.
- A treia metodă este construirea unei noi componente folosind o funcție constructor pentru a agrega configurarea componentei într-o instanță de componentă. Această metodă poate folosi cele două metode anterioare pentru a construi configurația componentei și apoi apelează parser-ul JUL pentru a crea instanța componentei în cadrul funcției constructor. Un exemplu este dat în continuare.
var MyApp = { config: {}, objects: {}, widgets: {}, vesrsion: '1.0' }; MyApp.config.logoUi = { xclass: 'MyLib.Ellipse', cid: 'c-logo', dimensions: {dx: 18, dy: 12}, children: [ {xclass: 'MyLib.Triangle', cid: 'c-inner', origin: {x: 8, y: 1}, sizes: [7, 7, 7], children: [ {xclass: 'MyLib.Circle', cid: 'c-center', origin: {x: 4, y: 2}, diamerer: 4} ]} ] }; MyApp.config.logoLogic = { 'c-logo': { fillColor: 'green', Draw: function() { console.log('Draw logo'); } }, 'c-inner': { fillColor: 'red', draw: function() { console.log('draw inner'); } }, 'c-center': { fillColor: 'blue', draw: function() { console.log('Draw center'); } } }; MyApp.parser = new JUL.UI.Parser(); MyApp.widgets.Logo = function(oConfig) { var oApplied = {include: MyApp.config.logoUi}; JUL.apply(oApplied, oConfig || {}); var oAppliedLogic = {include: MyApp.config.logoLogic}; var sId = oApplied.id || oApplied.cid; if (!sId) { sId = 'a-random-id'; oApplied.cid = sId; } oAppliedLogic[sId] = {}; JUL.apply(oAppliedLogic[sId], oConfig || {}); delete oAppliedLogic[sId].xclass; delete oAppliedLogic[sId].id; delete oAppliedLogic[sId].cid; oApplied.xclass = MyApp.config.logoUi.xclass || MyApp.parser.defaultClass; return MyApp.parser.create(oApplied, oAppliedLogic); }; MyApp.config.ui = { xclass: 'MyLib.Rectangle', id: 'the-ui', dimensions: {w: 22, h: 16}, children: [ {xclass: 'MyApp.widgets.Logo', id: 'the-logo', origin: {x: 2, y: 2}} ] }; MyApp.config.logic = { 'the-ui': { fillColor: 'green', show: function() { console.log('Show UI'); } }, 'the-logo': { fillColor: 'yellow' } }; MyApp.objects.ui = MyApp.parser.create(MyApp.config.ui, MyApp.config.logic); // apelurile runtime rezultante sunt echivalentul a: var oCenter = new MyLib.Circle({ origin: {x: 4, y: 2}, diamerer: 4, fillColor: 'blue', draw: function() { console.log('Draw center'); } }); var oInner = new MyLib.Triangle({ origin: {x: 8, y: 1}, sizes: [7, 7, 7], children: [oCenter], fillColor: 'red', draw: function() { console.log('draw inner'); } }); var oLogo = new MyLib.Ellipse({ id: 'the-logo', origin: {x: 2, y: 2}, dimensions: {dx: 18, dy: 12}, children: [oInner], fillColor: 'yellow', Draw: function() { console.log('Draw logo'); } }); // următorul apel va returna obiectul anterior oLogo new MyApp.widgets.Logo({ id: 'the-logo', origin: {x: 2, y: 2}, fillColor: 'yellow' }); MyApp.objects.ui = new MyLib.Rectangle({ id: 'the-ui', dimensions: {w: 22, h: 16}, children: [oLogo], fillColor: 'green', show: function() { console.log('Show UI'); } });
Pentru o modalitate standard de a încapsula elemente complexe într-o componentă, consultați vă rog pagina de proiect a JWL Library.
Moștenire circulară și meta-inițializare a parser-ului
Un parser JUL este o instanță a JUL.UI.Parser() și moștenește automat toți membrii JUL.UI. Dar parser-ul însușii are o metodă numită Parser() care poate fi folosită pentru a construi un parser nou derivat din cel curent. Metoda Parser() acceptă ca parametru un obiect de configurare ai cărui membri pot suprascrie membrii moșteniți ai parser-ului. Mai mult, modificarea unui membru moștenit din unul din părinții ascendenți (ex. obiectul JUL.UI) se va reflecta în toți parser-ii descendenți în care acel membru nu este suprascris. Numim aceasta moștenire circulară.
Parser-ul JUL se pune în acțiune atunci când este apelată metoda sa create(), care la rândul său utilizează obiectele de configurare a structurii li a logicii pentru a construi un arbore de componente. De obicei, pentru fiecare nod în structura de configurare, parser-ul creează o componentă folosind membri ai parser-ului precum proprietatea clasă, proprietatea copii, proprietatea id și așa mai departe, ca meta-informație. Dar dacă nodul procesat curent are o proprietate specială – numită implicit parserConfig, parser-ul pornește un nou parser derivat bazat pe acea proprietate pentru a construi acea ramură a arborelui de configurare. Numim aceasta meta-inițializare a parser-ului.
// derivarea unui parser dintr-o clasă de bază var oXmlParser = new JUL.UI.Parser({ defaultClass: 'xml', useTags: true, topDown: true }); // derivarea unui parser din cel anterior var oHtmlParser = new oXmlParser.Parser({ defaultClass: 'html', customFactory: 'JUL.UI.createDom' }); // setarea unei proprietăți pentru o clasă de bază și pentru toți parser-ii în care proprietatea nu e suprascrisă JUL.UI.childrenProperty = 'childNodes'; // crearea unei configurații pentru arbore DOM mixt var oConfig = { tag: 'div', css: 'svg-wrap', childNodes: [ {tag: 'h3', html: 'A SVG image'}, {tag: 'div', css: 'svg-img', childNodes: [ { // următoarea proprietate meta-informație va auto-deriva un parser nou pentru această ramură de configurație parserConfig: {defaultClass: 'svg', childrenProperty: 'nodes'}, tag: 'svg', width: 100, height: 100, viewBox: '0 0 100 100', nodes: [ {tag: 'circle', cx: 50, cy: 50, r: 48, fill: 'none', stroke: '#000'}, {tag: 'path', d: 'M50,2a48,48 0 1 1 0,96a24 24 0 1 1 0-48a24 24 0 1 0 0-48'}, {tag: 'circle', cx: 50, cy: 26, r: 6}, {tag: 'circle', cx: 50, cy: 74, r: 6, fill: '#FFF'} ] } ]} ] }; // crearea unui arbore DOM și atașarea lui la elementul body oHtmlParser.create(oConfig, null, document.body);
<!-- generated DOM --> <div class="svg-wrap"> <h3>A SVG image</h3> <div class="svg-img"> <svg:svg width="100" height="100" viewBox="0 0 100 100"> <svg:circle cx="50" cy="50" r="48" fill="none" stroke="#000"/> <svg:path d="M50,2a48,48 0 1 1 0,96a24 24 0 1 1 0-48a24 24 0 1 0 0-48"/> <svg:circle cx="50" cy="26" r="6"/> <svg:circle cx="50" cy="74" r="6" fill="#FFF"/> </svg:svg> </div> </div>
Serializare consistentă și conversie XML la JUL
În afară de construirea componentelor dintr-un arbore de configurare, JUL urmărește să stocheze persistent și să recupereze obiectele de configurare (adică serializare). Cel nai sigur mod de a face aceasta este de a produce codul JavaScript ce va construi la rulare obiectul de configurare dorit. Pentru a îndeplini acest obiectiv, JUL folosește o serializare extinsă bazată pe standardul JSON. În plus față de regulile JSON, JUL e capabil să serializeze o serie de tipuri native de date JavaScript precum Function, RegEx, Date sau tipuri particulare de date cu ajutorul utilitarei JUL.UI._jsonReplacer(). Ținta acestei serializări poate fi fie cod JavaScript fie JSON, dar scopul este cod JavaScript echivalent indiferent unde este rulată serializarea. Numim aceasta serializare consistentă, adică procesul în care conversia obiectului de configurare produce același obiect runtime (cod JavaScript) când este executat codul rezultant. Codul propriu-zis poate diferi prin salturi de linie, spații libere, formatarea tipurilor numerice sau de tip data, dar va produce întotdeauna aceleași efecte runtime. Ca simplă observație, serializarea nu implică faptul că forma serializată trebuie să fie aceeași cu obiectul ce se serializează, deși acesta este scopul în cazul obiectelor de configurare. De reținut că obiectul runtime poate avea diverși membri prototipali sau funcționalitate extinsă depinzând de mediul unde este rulat codul. Dar obiectele de configurare JUL nu depind de prototipuri și sunt folosite doar pentru a citi configurația stocată în membrii lor. Mai multe detalii despre serializare pot fi găsite în referința metodei JUL.UI.obj2str().
var oConfig = { firstString: 'First\t\x22string\x22\t', secondString: "Second 'string'\n", firstNumber: parseFloat(Math.PI.toFixed(4)), secondNumber: 1.2e4, dates: { first: new Date(Date.UTC(2020, 5, 1)), second: '2030-02-10T15:00:30Z' }, matches: [ /(one|two|three)\s+times/i, new RegExp('<h1[\\s\\S]+?\/h1>', 'g') ], events: [ {name: 'click', handler: "function(oEvent) {\n\tconsole.log(oEvent.type + ' triggered');\n}"}, {name: 'change', handler: [ function(oEvent) { console.log('Before ' + oEvent.type + ' triggered'); }, function(oEvent) { console.log('After ' + oEvent.type + ' triggered'); } ]} ] }; var oParser = new JUL.UI.Parser(); console.log(oParser.obj2str(oConfig)); // linia anterioară produce următorul cod JavaScript în toate mediile majore: oConfig = { firstString: 'First\t"string"\t', secondString: 'Second \'string\'\n', firstNumber: 3.1416, secondNumber: 12000, dates: { first: new Date(/*Mon, 01 Jun 2020 00:00:00 GMT*/1590969600000), second: new Date(/*Sun, 10 Feb 2030 15:00:30 GMT*/1896966030000) }, matches: [/(one|two|three)\s+times/i, /<h1[\s\S]+?\/h1>/g], events: [ {name: 'click', handler: function(oEvent) { console.log(oEvent.type + ' triggered'); }}, {name: 'change', handler: [function(oEvent) { console.log('Before ' + oEvent.type + ' triggered'); }, function(oEvent) { console.log('After ' + oEvent.type + ' triggered'); }]} ] };
O altă facilitate a JUL este conversia de la limbajele bazate pe XML la obiectele de configurare JUL. Aceasta este realizată de metoda JUL.UI.xml2jul() care transformă un arbore XML (sau un cod sursă) într-un arbore de configurare JUL (sau într-un cod sursă echivalent). Conversia ține cont de meta-informația parser-ului curent. Pentru a o vedea în acțiune, vizitați vă rog exemplul online XML2JUL.
Fabricantul personalizat
Când construim componente din obiectul de configurare, JUL combină configurațiile de structură și de logică într-un obiect de configurare runtime pentru fiecare componentă de construit. În mod predefinit, configurația runtime este pasată unei funcții constructor localizată de calea punctată stocată în proprietatea clasă pentru această configurație. Există de asemenea și o proprietate clasă implicită care se aplică tuturor nodurilor în care proprietatea clasă nu este specificată.
Utilizatorul poate schimba acest comportament prin setarea proprietății JUL.UI.customFactory la un callback de fabricare personalizat care va primit configurația calculată runtime. Un exemplu tipic de astfel de callback este metoda JUL.UI.createDom() ce pateu construi mai multe tipuri de elemente DOM (HTML, SVG, XUL etc.) în funcție de facilitățile browser-ului.
// utilizarea unui fabricant JUL personalizat pentru a construi o fereastră XUL var oConfig = { tag: 'window', id: 'window-main', height: 450, hidden: true, title: 'JUL News Reader', width: 720, children: [ {tag: 'toolbox', children: [ {tag: 'menubar', children: [ {tag: 'toolbargrippy'}, {tag: 'menu', label: 'File', children: [ {tag: 'menupopup', children: [ {tag: 'menuitem', id: 'menuitem-exit', label: 'Exit'} ]} ]}, {tag: 'menu', label: 'View', children: [ {tag: 'menupopup', children: [ {tag: 'menuitem', id: 'menuitem-show-articles', checked: true, label: 'Show articles', type: 'checkbox'} ]} ]}, {tag: 'menu', label: 'Options', children: [ {tag: 'menupopup', children: [ {tag: 'menuitem', id: 'menuitem-autorefresh', label: 'Autorefresh every minute', type: 'checkbox'} ]} ]} ]} ]}, {tag: 'hbox', children: [ {tag: 'spacer', width: 7}, {tag: 'label', control: 'textbox-url', value: 'Address'}, {tag: 'spacer', width: 7}, {tag: 'textbox', id: 'textbox-url', flex: 1, value: 'http://feeds.skynews.com/feeds/rss/technology.xml'}, {tag: 'spacer', width: 5}, {tag: 'button', id: 'button-go', label: 'Go', tooltiptext: 'Get the news', width: 50} ]}, {tag: 'hbox', flex: 1, children: [ {tag: 'vbox', id: 'vbox-articles', width: 260, children: [ {tag: 'listbox', id: 'listbox-articles', flex: 1, children: [ {tag: 'listhead', children: [ {tag: 'listheader', label: 'Article', width: 500} ]}, {tag: 'listbody'} ]} ]}, {tag: 'splitter'}, {tag: 'vbox', width: '100%', children: [ {tag: 'description', id: 'description-title'}, {tag: 'description', id: 'description-date'}, {tag: 'hbox', id: 'hbox-image', hidden: true, pack: 'center', children: [ {tag: 'image', id: 'image-article', width: 200} ]}, {tag: 'description', id: 'description-content'} ]} ]} ] }; var oParser = new JUL.UI.Parser({ customFactory: 'JUL.UI.createDom', defaultClass: 'xul', topDown: true, useTags: true }); var oWindow = oParser.create(oConfig); oWindow.show();
Va urma
Acestea au fost câteva concepte avansate despre utilizarea JUL — limbajul UI JavaScript. Alte concepte precum replacer-i JSON particularizați, prefixe pentru serializare, decoratori ai codului, accesarea și setarea scopului callback-urilor, escaping al caii punctate și ala mai departe, vor fi discutate într-un articol viitor.