Pokročilé příklady
- Ukázka dědičnosti pomocí ClassMakeru
- Implementace rozhraní
- Vzor Jedináček (Singleton)
- Vzor Decorator
- Vlastní události - implementace vzoru Observer
- Využití metody bind()
- Využití rozhraní SigInterface pro implementaci Observeru
- Oddělení vizuální interpretace od implementace
- Složitější ukázka více tříd s využitím komponent
- Spuštění kódu, když je manipulace s DOMem bezpečná
- Přidané prototypové metody
Na této stránce jsou zobrazeny pokročilé příklady použití JAvascriptové Knihovny.
V jednoduchých příkladech jsme si ukázali, že každá vytvářená třída pomocí ClassMakeru obsahuje povinné vlastnosti NAME, VERSION a CLASS. ClassMaker umožňuje navíc vytváření dedičností a implementaci rozhraní.
Jelikož dedičnost ani rozhraní nejsou standardní vlastnosti javascriptu, ale jsou doprogramované, chovají se mírně odlišně než u většiny standardních objektových jazyků.
Další volitelné vlastnosti objektu předávaném konstruktoru ClassMakeru jsou:
- EXTEND - odkaz na třídu, ze které objekt dědí
- IMPLEMENT - pole odkazů na třídy, které jsou implementovány jako rozhraní
- DEPEND - pole závislostí
Ukázka dědičnosti pomocí ClassMakeru
EXTEND očekává odkaz na již vytvořenou třídu, ze které dědí vlastnosti a metody. Toto dědění ale nefunguje na stejném principu jako v klasických programovacích jazycích, kdy defakto třídní předpisy se děděním zřetězí a z každého dítětě se dá odkazovat na rodiče (pomocí klíčového slova: parent či super). V našem případě jsou překopírovány veškeré vlastnosti a metody nadřazené třídy do třídy vytvářené i s jejich funkčností, pokud nejsou předefinovány v odvozené třídě. Volání metody předka v přepsané metodě teoreticky není možné, neboť všechny metody jsou překopírovány do kontextu this. Nicméně ClassMaker v každé vytvořené třídě vytváří metodu $super, která tuto funkčnost umožňuje.
Následující příklad vytváří obecnou třídu Vehicle, z které dědí třídy Bus a Car, z třídy Bus navíc dědí třída SchoolBus.
/**
* ukázka dědičnosti
*/
//vytvoření třídy Vehicle přes ClassMaker
Vehicle = SZN.ClassMaker.makeClass({
NAME: "Vehicle",
VERSION: "1.0"
});
//konstruktor Vehicle
Vehicle.prototype.$constructor = function(man, id) {
//definice vlastností
this.manufacturer = man; //výrobce
this.vehicleId = id; //SPZ
}
//vrati SPZ dopravního prostředku
Vehicle.prototype.getVehicleId = function() {
return this.vehicleId;
}
//metoda zobrazi vlastnosti objektu
Vehicle.prototype.showDetail = function() {
str = 'Třída: ' + this.sConstructor.NAME + '\n';
str += 'Výrobce: ' + this.manufacturer + '\n';
str += 'SPZ: ' + this.vehicleId + '\n';
return str;
}
//vytvoření třídy Car přes ClassMaker
Car = SZN.ClassMaker.makeClass({
NAME: "Car",
VERSION: "1.0",
EXTEND: Vehicle
});
//konstruktor Car
Car.prototype.$constructor = function(man, id, doors, maxSpeed) {
this.doors = doors;
this.maxSpeed = maxSpeed;
this.$super(man, id); //volani konstruktoru predka
}
Car.prototype.showDetail = function() {
str = 'Třída: ' + this.sConstructor.NAME + '\n';
str += 'Výrobce: ' + this.manufacturer + '\n';
str += 'SPZ: ' + this.vehicleId + '\n';
str += 'Maximální rychlost: ' + this.maxSpeed + '\n';
str += 'Počet dveří: ' + this.doors + '\n';
return str;
}
//vytvoření třídy Bus přes ClassMaker
Bus = SZN.ClassMaker.makeClass({
NAME: "Bus",
VERSION: "1.0",
EXTEND: Vehicle
});
//konstruktor Bus
Bus.prototype.$constructor = function(man, id, seats, wheels) {
this.seats = seats;
this.wheels = wheels;
this.$super(man, id); //volani konstruktoru predka
}
Bus.prototype.showDetail = function() {
str = 'Třída: ' + this.sConstructor.NAME + '\n';
str += 'Výrobce: ' + this.manufacturer + '\n';
str += 'SPZ: ' + this.vehicleId + '\n';
str += 'Počet sedadel: ' + this.seats + '\n';
str += 'Počet Kol: ' + this.wheels + '\n';
return str;
}
//vytvoření třídy SchoolBus přes ClassMaker
SchoolBus = SZN.ClassMaker.makeClass({
NAME: "SchoolBus",
VERSION: "1.0",
EXTEND: Bus
});
//konstruktor SchoolBus
SchoolBus.prototype.SchoolBus = function(man, id, seats, wheels, school) {
this.school = school;
this.$super(man, id, seats, wheels); //volani konstruktoru predka
}
SchoolBus.prototype.showDetail = function() {
str = 'Třída: ' + this.sConstructor.NAME + '\n';
str += 'Výrobce: ' + this.manufacturer + '\n';
str += 'SPZ: ' + this.vehicleId + '\n';
str += 'Počet sedadel: ' + this.seats + '\n';
str += 'Počet Kol: ' + this.wheels + '\n';
str += 'Autobus patří škole: ' + this.school + '\n';
return str;
}
car = new Car('Volvo', '1A2 35 46','5','240');
bus = new Bus('Scania', '2U3 55 66', '40', '6');
sbus = new SchoolBus('Ford', '2S8 33 48', '25', '4', 'SPŠ Elektrotechnická');
V příkladu jsou vytvořeny tři instance, od každé třídy jedna. Tedy jedno auto, jeden autobus a jeden školní autobus. Po kliknutí na budou vypsány jejich vlastnosti. V příkladu je definovaná metoda Vehicle::getVehicleId(), která vrací SPZ a dědí ji všechny objekty. Proto si můžeme dovolit ji volat nad každým objektem. Pak můžeme pro jeho SPZ.
V příkladu je možné si všimnout, že konstruktor se nejmenuje $constructor ale dle názvu třídy. ClassMaker při vytváření třídy zjišťuje existenci metody $constructor a tu spouští, pokud ji nenajde, tak se pokouší najít metodu s názvem stejným jako název třídy a tu spustí. Nepředefinované metody předka lze normálně volat a proto využijeme možnosti pojmenovaných konstruktorů. Tím pádem nemusíme využít volání callSuper.
Použití metody $super
Metoda $super, umožňuje přímo volat metodu předka bez předávání odkazu na volající metodu pomocí arguments.callee. K obejití tohoto omezení využívá nestandardní rozšíření arguments.caller, které implementují všechny moderní prohlížeče (Opera od verze 9.6). Tato metoda bude v knihovně JAK v budoucnu preferovaná a metoda callSuper bude je za zastaralou. V příkladu s auty, byla použita ve volání konstruktoru předka, v následujícím příkladě je použita ve volání metod předka z přepsaných metod:
/**
* zakladni trida svetla
**/
NormalLight = SZN.ClassMaker.makeClass({
NAME: 'NormalLight',
VERSION: '1.0'
});
NormalLight.prototype.$constructor = function(id) {
this.shine = false;
this.box = SZN.gEl(id);
}
NormalLight.prototype.isShining = function() {
this.box.innerHTML = 'NormalLight: Světlo ' + (this.shine ? 'svítí' : 'nesvítí') + '
' + this.box.innerHTML;
}
NormalLight.prototype.turnOn = function() {
this.box.innerHTML = 'NormalLight: Zapínám světlo
' + this.box.innerHTML;
this.shine = true;
}
NormalLight.prototype.turnOff = function() {
this.box.innerHTML = 'NormalLight: Vypínám světlo
' + this.box.innerHTML;
this.shine = false;
}
/**
* konkretni trida svetla s vetrakem
**/
LightWithFan = SZN.ClassMaker.makeClass({
NAME: 'LightWithFan',
VERSION: '1.0',
EXTEND: NormalLight
});
LightWithFan.prototype.turnOn = function() {
this.box.innerHTML = 'LightWithFan: Zapínám větrák
' + this.box.innerHTML;
this.$super();
}
LightWithFan.prototype.turnOff = function() {
this.box.innerHTML = 'LightWithFan: Vypínám větrák
' + this.box.innerHTML;
this.$super();
}
LightWithFan.prototype.isRunning = function() {
this.box.innerHTML = 'LightWithFan: Větrák ' + (this.shine ? 'běží' : 'neběží') + '
' + this.box.innerHTML;
}
light = new LightWithFan('lightsMessageBox');
Jsou definované dvě třídy, jedna obecná NormalLight pro světlo a jedna specializovaná pro světlo s větrákem LightWithFan, která dědí z třídy obecné a rozšiřuje její funkčnost. Vytvoříme si instanci světla s větrákem a budeme pozorovat chování přepsaných metod, které volají metody předka s pomocí $super.
V boxu níže se budou objevovat zprávy našeho světla. Světlo můžeme a a také se zeptat na stav: .
Požadované knihovny:- main.js
- classmaker.js
Implementace rozhraní
V objektových programovacích jazycích je možné předepsat jistou funkčnost pomocí navržení rozhraní a jeho implementaci v konkrétní třídě. V javascriptu nic takového neexistuje. Nicméně ClassMaker se k tomuto problému staví obdobě jako k problému dědění a to tak, že kopíruje prototypové metody z rozhraní do vytvářené třídy. Rozdíl mezi implementací rozhraní a děděním je v našem případě takový, že dědit můžeme pouze z jednoho rodiče, kdežto implementovat můžeme libovolný počet rozhraní. Druhá odlišnost je, že při implementaci se nespouští konstruktor implementované třídy.
Vyspělé jazyky umožnují zeptat se zda objekt dědí rozhraní (např. pomocí instanceof, is), toto v javascriptu není možné.
Tvorbu rozhranní má v ClassMakeru na starosti metoda makeInterface:
/**
* ukázka dědičnosti a implementace rozhraní
*/
//vytvoření obecného rozhraní, které má metodu pro výpis všech vlastnosti objektu
IDetails = SZN.ClassMaker.makeInterface({
NAME: "IDetails",
VERSION: "1.0"
});
IDetails.prototype.showDetails = function() {
str = "";
for (key in this) {
if (typeof(this[key]) == "string") {
str += key +": "+ this[key] +"\n";
}
}
return str;
}
//vytvoření třídy Vehicle2 přes ClassMaker
Vehicle2 = SZN.ClassMaker.makeClass({
NAME: "Vehicle",
VERSION: "1.0",
IMPLEMENT: [IDetails]
});
//konstruktor Vehicle2
Vehicle2.prototype.$constructor = function(man, id) {
//definice vlastností
this.manufacturer = man; //výrobce
this.vehicleId = id; //SPZ
}
//vrati SPZ dopravního prostředku
Vehicle2.prototype.getVehicleId = function() {
return this.vehicleId;
}
//vytvoření třídy Car2 přes ClassMaker
Car2 = SZN.ClassMaker.makeClass({
NAME: "Car2",
VERSION: "1.0",
EXTEND: Vehicle2
});
//konstruktor Car
Car2.prototype.$constructor = function(man, id, doors, maxSpeed) {
this.doors = doors;
this.maxSpeed = maxSpeed;
this.$super(man, id);
}
//vytvoření třídy Bus2 přes ClassMaker
Bus2 = SZN.ClassMaker.makeClass({
NAME: "Bus2",
VERSION: "1.0",
EXTEND: Vehicle2
});
//konstruktor Bus2
Bus2.prototype.$constructor = function(man, id, seats, wheels) {
this.seats = seats;
this.wheels = wheels;
this.$super(man, id);
}
//vytvoření třídy SchoolBus2 přes ClassMaker
SchoolBus2 = SZN.ClassMaker.makeClass({
NAME: "SchoolBus2",
VERSION: "1.0",
EXTEND: Bus2
});
//konstruktor SchoolBus
SchoolBus2.prototype.$constructor = function(man, id, seats, wheels, school) {
this.school = school;
this.$super(man, id, seats, wheels);
}
car2 = new Car2('Volvo', '1A2 35 46','5','240');
bus2 = new Bus2('Scania', '2U3 55 66', '40', '6');
sbus2 = new SchoolBus2('Ford', '2S8 33 48', '25', '4', 'SPŠ Elektrotechnická');
V tomto příkladě je výpis vlastností objektů vytvořen pomocí metody rozhraní, která je zděděna do všech vytvořených objektů. Po kliknutí na budou vypsány jejich vlastnosti.
Požadované knihovny:- main.js
- classmaker.js
Vzor Jedináček (Singleton)
Vzor Jedináček slouží k tomu, abychom zajistili pouze jednu instanci z dané třídy. Toto chování lze vynutit privátním konstruktorem a privátní statickou vlastností, ve které uchováváme konkrétní jednu instanci. Pro přístup k této instanci se většinou používá metoda getInstance, nebo factory.
Pokud v Javascriptu využíváme prototypovou dědičnost (JAK využívá), pak nelze definovat privátní vlastnosti a metody, tudíž nelze udělat privátní konstruktor. Nicméně pomocí ClassMakeru a jeho metody makeSingleton jsme schopni vytvořit takový konstruktor, který jde spustit pouze jednou a tudíž nejde vytvořit více než jedna instance.
Pro správné použití vytváří ClassMaker i metodu getInstance, která je jediná oprávněná vytvářet a vracet konkrétní instanci.
Vzor Singleton můžeme chtít použít např. pokud chceme vytvořit logger, který loguje události v kódu:
/**
* trida loggeru
*/
Logger = SZN.ClassMaker.makeSingleton({
NAME: 'Logger',
VERSION: '1.0'
});
/**
* konstruktor se zasobnikem zprav
*/
Logger.prototype.$constructor = function() {
this.messages = [];
}
/**
* metoda pro pridani zpravy
*/
Logger.prototype.log = function(message) {
this.messages.push(message);
}
/**
* metoda pro vypis zprav ze zasobniku do predaneho elementu
*/
Logger.prototype.show = function(elm) {
elm.innerHTML = this.messages.join('
\n');
}
/**
* metoda cisti zasobnik zprav
*/
Logger.prototype.clear = function() {
this.messages = [];
}
Obslužný kód tlačítek využívající instance jedináčka, získané voláním metody getInstance:
//metoda pro zalogovani promtu uzivatele
function logg() {
//vytvoreni instance loggeru
Logger.getInstance().log(prompt('Zalogovat:'));
}
//logovaci funkce volana automaticky, kazde 2 vteriny
function logTime() {
var logger = Logger.getInstance();
var date = new Date();
logger.log(date.format('d.m.Y H:i:s'));
}
var interval = null;
//zacatek automatickeho logovani
function startLog() {
interval = setInterval(logTime, 2000);
}
//konec automatickeho logovani
function stopLog() {
clearInterval(interval);
}
function showLog() {
Logger.getInstance().show(SZN.gEl('loggerMessageBox'));
}
//naveseni udalosti na buttony
SZN.Events.addListener(SZN.gEl('logger1'), 'click', window, 'logg');
SZN.Events.addListener(SZN.gEl('logStart'), 'click', window, 'startLog');
SZN.Events.addListener(SZN.gEl('logStop'), 'click', window, 'stopLog');
SZN.Events.addListener(SZN.gEl('showLog'), 'click', window, 'showLog');
Nyní máme instanci Loggeru lg, do které můžeme přidávát zprávy . Dále budeme chtít periodicky zaznamenávat událost, tlačítkem logování spustíme a tímto zastavíme. Logovací funkce získává voláním getInstance tu samou instanci. Nakonec bychom rádi získali výpis všech zpráv .
Výpis z Loggeru:
Požadované knihovny:- main.js
- classmaker.js
Vzor Decorator
Vzor Decorator se využívá především tam, kde by přidávání funkčnosti vedlo k rapidnímu zvýšení počtu tříd. Představme si, že máme tlačítka která odesílají formulář. Dále některá z nich mají po kliknutí navíc upozornit uživatele na nebezpečnou akci. Pak přijde další požadavek kdy některé z nich mají udělat AJAXový dotaz a data získaná, mají přidat do odesílaného formuláře. A takto požadavky rostou, pro každý typ tlačítka děláme třídu a nakonec dojde na to, že jedno tlačítko má umět všchno a my nemáme vícenásobnou dědičnost k dispozici. Zde nastupuje koncept Dekorátorů.
V původním vzoru je dekorátor třída, která dědí z třídy, kterou bude dekorovat. Instance dekorátoru v konstruktoru získává instanci dekorovaného objektu a volání metod, které neupravuje, deleguje na dekorovaný objekt. Takto můžeme dekorátory vesele balit na sebe jako šlupky cibule. Použití dekorátoru má ale jednu negativní vlastnost a to tu, že v kódu pracujícím s instancemi původní třídy najednou dostáváme "jiné objekty". Ve většině jiných jazyků to nemusí být problém, ale v JavaScriptu to problém je. Pokud v nějaké metodě navěšujeme události a metody při vzniku události jsou volány v kontextu this, pak se nezavolají metody ostatních dekorátorů, protože nemáme jak zjistit kdo koho odekoroval (princip dekorátorů).
Tato obtíž nás trápila, proto jsme přistoupili na tzv. Monkey Patch, kdy námi vytvořené dekorátory modifikují předanou instanci přímo, místo toho aby jí obalily. To má za následek, že this je vždy objekt instance, za druhé to vede k tomu, že metody dekorátoru jsou kopírovány do instance a při nevhodně zvolených názvech privátních metod v dekorátorech si je můžou dekorátory navzájem přepsat. V metodách dekorátorů lze použít metodu $super(), pak je zavolána předešlá metoda instance, tedy ta, která by byla volána, pokud by nebyla instance dekorována daným dekorátorem.
Dekorátory v JAKu musí vždy dědit z třídy SZN.AbstractDecorator. Tímto děděním získá dekorátor metodu decorate(). V této metodě musíme zajistit vlastní překopírování vlastních upravených metod na dekorovanou instanci. Dost často chceme překopírovat všechny definované metody, proto můžeme dědit z třídy SZN.AutoDecorator a metoda decorate() udělá vše za nás.
//trida osoby
Person = SZN.ClassMaker.makeClass({
NAME:'Person',
VERSION: '1.0'
});
//konstruktor
Person.prototype.$constructor = function(name) {
this.name = name;
}
Person.prototype.getName = function() {
return this.name;
}
//metoda kterou ziskam popis aiktiivty
Person.prototype.getActivity = function() {
return 'Jako každý '+this.getName()+' se akorád umím válet na gauči';
}
//trida dekoratoru, dedi z autoDecoratoru, tudiz se mi automaticky
//propisi metody do dekorovane instance
Sprinter = SZN.ClassMaker.makeSingleton({
NAME: 'Sprinter',
VERSION: '1.0',
EXTEND: SZN.AutoDecorator
});
//prepsani metody decorate, je zde ukazano volani metody predka (SZN.AutoDecorator.decorate)
// a take rozsireni dekorovane instance o novou vlastnost
Sprinter.prototype.decorate = function(instance, params) {
this.$super(instance, params);
instance.__sprinterActivity = 'Jako sprinter umím běhat stovku pod 10s.';
}
//upravena metoda getActivity dekoratorem, s ukazkou volani metody dekorovane
//instance tridy Person
Sprinter.prototype.getActivity = function() {
var act = this.$super();
return act + '\n' + this.__sprinterActivity;
}
var pepa = new Person('Pepa');
Sprinter.getInstance().decorate(pepa);
alert(pepa.getActivity());
Po kliknutí na tlačítko vyvoláme metodu getActivity() na dekorované instanci.
Kód v příkladu si zaslouží bližší komentář. Třída Person je běžná třída a tak je to správně. Dekorátor Sprinter jak je vidět dědí ze SZN.AutoDecorator, tudíž očekáváme automatické klonování prototypových metod na dekorovanou instanci. Za povšimnutí stojí, že dekorátor je vlastně Jedináčkem. Není důvod aby nebyl, protože z jeho instance je vše naklonováno do dekorované instance.
Dalším zajímavým momentem je metoda decorate(), která zajišťuje vlastní dekoraci předané instance. V této metodě můžeme instanci předat např. nové atributy, tak jak je naznačeno. this a $super() se zde vztahují k instanci dekorátoru. V ostatních metodách, které budou naklonovány dekorované instanci se this a $super() váže k dekorované instanci.
Na předposledním řádku je ukázáno jak dekorátor použít. Nicméně pokud vytváříme třídu, u které dopředu víme, že by mohla být dekorovatelná, může implementovat rozhranní IDecorable. Pak dostane do vínku metodu decorate(). Tato metoda zajistí získání instance dekorátoru a vlastní odekorování, viz ukázka níže:
//trida osoby
Person = SZN.ClassMaker.makeClass({
NAME:'Person',
VERSION: '1.0',
IMPLEMENT: [IDecorable]
});
...
...
var pepa = new Person('Pepa');
pepa.decorate(Sprinter);
alert(pepa.getActivity());
Metodě decorate() dekorátoru je možné kromě instance předat druhý parametr, kterým do dekorátoru předáme další parametry potřebné pro dekorování.
Požadované knihovny:- main.js
- classmaker.js
Vlastní události - implementace vzoru Observer
Pokud píšeme větší knihovnu, kde hrozí narůstání závislostí objektů, je vhodné využít návrhové vzory Observer nebo Mediator. Principem vzoru Observer je, že definujeme jeden objekt (třída Observer v odkazovaném text), který upozorňuje na vzniklé události všechny registrované posluchače.
V JAKu je Observerem třída Signals, která vystavuje metody:
- addListener - metoda přidává posluchače události.
- removeListener - odebere posluchače
- makeEvent - vytvoření události (vnitřně spustí notifikaci posluchačů navěšených na danou událost)
/**
* objekt auta, ktere budeme animovat
*/
BoxedCar = SZN.ClassMaker.makeClass({
NAME: "BoxedCar",
VERSION: "1.0"
});
//konstruktor, predavame, element ktery budeme animovat
BoxedCar.prototype.$constructor = function(elm) {
this.elm = null;
this.leftPosition = 0;
this.setVisualImplementation(elm);
//nabindovani do this.move metodu, tak aby pro volani pres interval byla volana v kontextu objektu (this)
this.move = SZN.bind(this, this.move);
}
//metoda ocekava DOM element, ktery bude animovat
BoxedCar.prototype.setVisualImplementation = function(elm) {
this.elm = elm;
position = SZN.Dom.getFullBoxPosition(elm);
elm.style.position = "absolute";
elm.style.top = position.top+"px";
elm.style.left = position.left+"px";
//uchování pozice pro animaci, abychom nemuseli aktuální pozici stále parsovat
this.leftPosition = position.left;
}
//metoda pro animaci auta
BoxedCar.prototype.move = function() {
this.leftPosition = this.leftPosition -15;
this.elm.style.left = this.leftPosition+'px';
//nastal crash
if (this.leftPosition <= 0) {
clearInterval(interval);
observer.makeEvent('crash', this, false, new Date().getTime());
}
}
/**
* objekt, který se dokáže navěsit na SigInterface a odchytávat a vypisovat události
*/
MessageDisplayer = SZN.ClassMaker.makeClass({
NAME: "MessageDisplayer",
VERSION: "1.0"
});
//konstruktor, predavame element (nejlepe P) do ktereho budeme vypisovat
MessageDisplayer.prototype.$constructor = function(elm) {
this.elm = elm;
this.elm.innerHTML = ""; //vyčištění obsahu
}
//metoda pro vypis udalosti na autu
MessageDisplayer.prototype.displayEvent = function(event) {
str = '<span style="color:';
if (event.type == 'crash') {
str += 'red';
} else if (event.type == 'start') {
str += 'orange';
} else if (event.type == 'stop') {
str += 'green';
} else {
str += 'black';
}
str += ';">';
str += new Date(event.timeStamp).toString() + ": " + event.type;
str += '</span>';
this.elm.innerHTML += str +'<br />';
}
/**
* objekt, který mění stavy tlačítek dle událostí
*/
ButtonDisabler = SZN.ClassMaker.makeClass({
NAME: "ButtonBisabler",
VERSION: "1.0"
});
//konstruktor, ocekava reference na objekty auta a tlacitka pro vypnuti
ButtonDisabler.prototype.$constructor = function(elmCar, elmStopper) {
this.elmCar = elmCar;
this.elmStopper = elmStopper;
}
//odchytavac udalsoti
ButtonDisabler.prototype.eventHandler = function(event) {
if (event.type == "start") {
this.elmCar.disabled = true;
this.elmStopper.disabled = false;
} else if (event.type == "stop") {
this.elmCar.disabled = false;
this.elmStopper.disabled = true;
} else {
this.elmCar.disabled = true;
this.elmStopper.disabled = true;
}
}
/**
* kód vykonaný při spuštění
*/
//vytvoření instance auta
bCar = new BoxedCar(SZN.gEl('boxedCar'));
//vytvoření instance zobrazovače událostí
ev = new MessageDisplayer(SZN.gEl('eventWindow'));
//vytvoreni instance objektu disablujiciho tlacitka
bd = new ButtonDisabler(SZN.gEl('boxedCar'), SZN.gEl('pauseCar'));
//proměnná uchovávající interval
var interval = null;
//navěšení události animace
SZN.Events.addListener(SZN.gEl('boxedCar'), 'click', window, 'animate');
function animate(e, elm) {
interval = setInterval(bCar.move, 300);
observer.makeEvent('start', elm, 'private', new Date().getTime());
}
//navěšení události pro zastavení animace
SZN.Events.addListener(SZN.gEl('pauseCar'), 'click', window, 'pauseAnimation');
function pauseAnimation(e, elm) {
clearInterval(interval);
observer.makeEvent('stop', elm, 'private', new Date().getTime());
}
//nastavení Observeru
var observer = new SZN.Signals();
observer.addListener(ev, 'crash', 'displayEvent');
observer.addListener(ev, 'start', 'displayEvent');
observer.addListener(ev, 'stop', 'displayEvent');
observer.addListener(bd, 'crash', 'eventHandler');
observer.addListener(bd, 'start', 'eventHandler');
observer.addListener(bd, 'stop', 'eventHandler');
Po kliknutí na tlačítko "Auto" bude spuštěna jeho animace nárazu do zdi. Náraz jde přerušit stiskem . Jakmile auto dojede k levému okraji okna, dojde k nárazu.
Navěšení posluchače nemusí být vždy na konkrétní událost. Jde omezit naslouchání pouze na konkrétní objekt. Pak je tento objekt čtvrtým parametrem metody addListener. V příkladu výše bychom tedy mohli navěšovat události konkrétně na instanci auta:
observer.addListener(bd, 'crash', 'eventHandler', bCar);
Velice snadno také můžeme napsat posluchače, který bude odposlouchávat veškeré poslané signály v rámci dané instance signálů. Stačí za název události dosadit znak *:
observer.addListener(window, '*', 'spy');
function spy(evt) {
var b = SZN.gEl('carObserverBox');
b.innerHTML = evt.type+': '+(evt.target instanceof BoxedCar ? 'BoxedCar' : 'HTMLElement button='+evt.target.innerHTML)+'
' + b.innerHTML; //.target.constructor.NAME
window.w = evt;
}
Posluchač do okna níže vypisuje komunikaci tlačítek a auta z předchozího případu a vlastně tak supluje tři navěšení metody displayEvent:
Ve většině aplikací není potřeba ani vytvářet vlastní ínstanci signálů jako výše:
//nastavení Observeru var observer = new SZN.Signals();
protože JAK vytváří jednu globální instanci ve svém jmeném prostoru automaticky. Tato instance je dostupná jako SZN.signals a pak můžeme rovnou psát:
SZN.signals.addListener(...);
Dále signály umožňují při vyslání předat uživatelsky definovaná data. Při volání metody makeEvent je očekáván jako poslední (pátý) nepovinný parametr, který je předán všem posluchačům. Tento parametr musí být objektem.
...
//vyslani signalu
SZN.signals.makeEvent('stop', elm, 'private', new Date().getTime(), {id: 123, name: 'blabla'});
...
...
//zpracovani signalu ve volane metode
function zpracovatelSignalu(event) {
alert(event.data.id);
}
Předaná data jsou do metody posluchače předaná v parametru události jako atribut data.
Požadované knihovny:- main.js
- classmaker.js
- signals.js
Využití metody bind()
Knihovna JAK vynucuje psaní kódu objektově tak, že do metod Events::addListener() nebo Signals::addListener() vyžaduje předat odkaz na objekt, ve kterém bude spuštěna předaná metoda. Aby v této metodě byl odkaz na klíčové slovo this objekt nad kterým je metoda volána a ne objekt Events či Signals, je vnitřně volána metoda SZN.bind().
... {
this.animate = SZN.bind(this, this.animate);
... }
Voláním této metody docílíme toho, že místo metody objektu je vytvořena anonymní metoda, která volá původní metodu s kontextem předaného objektu, v tomto případě "this".
Pokud chceme využít funkci setTimeout() nebo setInterval() pro volání metod objektů, musíme vždy předtím volanou metodu přebindovat, protože argumentem funkcí setInterval a setTimeout je jen odkaz na funkci a ne objekt.
... this.interval = setInterval(this.animate, 200); ...Požadované knihovny:
- main.js
Využití rozhraní SigInterface pro implementaci Observeru
Pro lehčí integraci volání událostí a získávání událostí ve vlastních třídách je možné využít rozhraní SigInterface. Toto rozhraní zapouzdřuje volání metod objektu Signals (Observer). Implementace použije globální objekt SZN.signals, nebo očekává, že objekt obsahuje vlastnost se jménem signals, která obsahuje instanci třídy SZN.Signals. Třídy implementující SigInterface rozhraní získají tyto metody:
- addListener - přihlášení k odběru událostí
- removeListener - odhlášení objektu od odběru událostí
- makeEvent - vytvoření události
Metody jsou volány vždy v kontextu objektu, který implementuje dané rozhraní. Potom auto z předchozího příkladu může vypadat jako v ukázce níže, kde jsou zvýrazněny změny.
/**
* objekt auta, ktere budeme animovat
*/
BoxedCar = SZN.ClassMaker.makeClass({
NAME: "BoxedCar",
VERSION: "1.0",
IMPLEMENT: [SZN.SigInterface]
});
//konstruktor, predavame element ktery budeme animovat
BoxedCar.prototype.$constructor = function(elm) {
this.elm = null;
this.leftPosition = 0;
this.setVisualImplementation(elm);
//nabindovani do this.move metodu, tak aby pro volani pres interval byla volana v kontextu objektu (this)
this.move = SZN.bind(this, this.move);
}
//metoda ocekava DOM element, ktery bude animovat
BoxedCar.prototype.setVisualImplementation = function(elm) {
this.elm = elm;
position = SZN.Dom.getFullBoxPosition(elm);
elm.style.position = "absolute";
elm.style.top = position.top+"px";
elm.style.left = position.left+"px";
//uchování pozice pro animaci, abychom nemuseli aktuální pozici stále parsovat
this.leftPosition = position.left;
}
//metoda pro animaci auta
BoxedCar.prototype.move = function() {
this.leftPosition = this.leftPosition -15;
this.elm.style.left = this.leftPosition+'px';
//nastal crash
if (this.leftPosition <= 0) {
clearInterval(interval);
this.makeEvent('crash', false);
}
}
Požadované knihovny:
- main.js
- classmaker.js
- signals.js
- sigingerface.js
Oddělení vizuální interpratace od implementace
Dobrým zvykem při psaní rozsáhlých aplikací je důsledné oddělení implementace algoritmů od jejich vizuální interpretace. Můžeme si to představit nad problémem s kalkulačkou. Pokud si odmyslíme kalkulačky s prefixovou logikou, tak všechny umí vypočítat zápis 4+4. Tedy funkčnost je stejná, mění se pouze jejich design. Pokud se tohoto důsledného oddělení držíme i při programování, vede nás to ke znovupoužitelnosti výkonného kódu.
V následujícím skriptu je velice jednoduchý objekt kalkulačky:
/**
* objekt kalkulačky
*/
Calculator = SZN.ClassMaker.makeClass({
NAME: "Calculator",
VERSION: "1.0"
});
//konstruktor kalkulačky
Calculator.prototype.$constructor = function() {
this.input = '';
this.output = '';
}
//metoda které předáme části výrazu, který budeme chtít vypočítat
Calculator.prototype.addInput = function(inp) {
this.input += inp+'';
}
//vymaže input zásobník
Calculator.prototype.clearInput = function() {
this.input = '';
}
//vrací obsah input zásobníku
Calculator.prototype.getInput = function() {
return this.input;
}
//metoda provadejici vypocet
Calculator.prototype.calculate = function() {
this.output = eval(this.input);
this.input = this.output;//dokud nedáme clearInput, tak chceme s výsledkem počítat
}
//získání výsledku
Calculator.prototype.getOutput = function() {
return this.output;
}
Poté je možné vytvořit třídu s vizuálním rozhraním:
/**
* objekt vizuální reprezentace kalkulačky
*/
VisualCalculator = SZN.ClassMaker.makeClass({
NAME: "VisualCalculator",
VERSION: "1.0"
})
//konstruktor, přebírá instanci nevizuálního části
VisualCalculator.prototype.$constructor = function(calc) {
this.calc = calc;
this.outputElement = null;
}
//metoda je navěšená na všechny tlačítka, která přidávají obsah do fronty pro vyhodnocení
//a dle nich volá metodu Calculator::addInput()
VisualCalculator.prototype.recieveInput = function(e, elm) {
this.calc.addInput(elm.value);
this.outputElement.value = this.calc.getInput();
}
VisualCalculator.prototype.clear = function(e, elm) {
this.calc.clearInput();
this.outputElement.value = '';
}
VisualCalculator.prototype.execute = function(e, elm) {
this.calc.calculate();
this.outputElement.value = this.calc.getOutput();
}
//zobrazení kalkulačky v předaném DOM elementu
VisualCalculator.prototype.showIn = function(elm) {
elm.innerHTML = "<input type='text' id='calc_output' readonly='readonly' /><br />";
elm.innerHTML += "<input type='button' value='7' id='calc_7' /> <input type='button' value='8' id='calc_8' /> <input type='button' value='9' id='calc_9' /> <input type='button' value='/' id='calc_div' /><br />";
elm.innerHTML += "<input type='button' value='4' id='calc_4' /> <input type='button' value='5' id='calc_5' /> <input type='button' value='6' id='calc_6' /> <input type='button' value='*' id='calc_multiple' /><br />";
elm.innerHTML += "<input type='button' value='1' id='calc_1' /> <input type='button' value='2' id='calc_2' /> <input type='button' value='3' id='calc_3' /> <input type='button' value='-' id='calc_minus' /><br />";
elm.innerHTML += "<input type='button' value='0' id='calc_0' /> <input type='button' value='c' id='calc_clear' /> <input type='button' value='=' id='calc_execute' /> <input type='button' value='+' id='calc_plus' />";
this.outputElement = SZN.gEl('calc_output');
SZN.Events.addListener(SZN.gEl('calc_0'), 'click', this, 'recieveInput');
SZN.Events.addListener(SZN.gEl('calc_1'), 'click', this, 'recieveInput');
SZN.Events.addListener(SZN.gEl('calc_2'), 'click', this, 'recieveInput');
SZN.Events.addListener(SZN.gEl('calc_3'), 'click', this, 'recieveInput');
SZN.Events.addListener(SZN.gEl('calc_4'), 'click', this, 'recieveInput');
SZN.Events.addListener(SZN.gEl('calc_5'), 'click', this, 'recieveInput');
SZN.Events.addListener(SZN.gEl('calc_6'), 'click', this, 'recieveInput');
SZN.Events.addListener(SZN.gEl('calc_7'), 'click', this, 'recieveInput');
SZN.Events.addListener(SZN.gEl('calc_8'), 'click', this, 'recieveInput');
SZN.Events.addListener(SZN.gEl('calc_9'), 'click', this, 'recieveInput');
SZN.Events.addListener(SZN.gEl('calc_plus'), 'click', this, 'recieveInput');
SZN.Events.addListener(SZN.gEl('calc_minus'), 'click', this, 'recieveInput');
SZN.Events.addListener(SZN.gEl('calc_div'), 'click', this, 'recieveInput');
SZN.Events.addListener(SZN.gEl('calc_multiple'), 'click', this, 'recieveInput');
SZN.Events.addListener(SZN.gEl('calc_clear'), 'click', this, 'clear');
SZN.Events.addListener(SZN.gEl('calc_execute'), 'click', this, 'execute');
}
calc = new Calculator();
vc = new VisualCalculator(calc);
Pokud kód spustíme a vizuální část, můžeme používat jednoduchou kalkulačku.
- main.js
- classmaker.js
Složitější ukázka více tříd s využitím komponent
V následujícím příkladu je ukázka zobrazení auta. V našem příkladě se auto sestává ze světel a motoru, kde obojí je připojeno jako komponenta.
V objektu, který skládá komponenty, je v konstruktoru třeba definovat pole komponent a jejich následnou registraci:
MyCar = SZN.ClassMaker.makeClass({
NAME: 'MyCar',
VERSION: '1.0',
IMPLEMENT: [SZN.Components, SZN.SigInterface]
});
MyCar.prototype.$constructor = function(name,setting){
//registrace komponent
this.components = [ {name:'motor', part:Engine},
{name:'svetla', part: Lights}];
this.addAllComponents();
//registrace signalů do kořenové komponenty
this.signals = new SZN.Signals(this, 'signals');
...
}
Pole komponent obsahuje literálové objekty s vlastnostmi name, part a setting. Jediná povinná vlastnost je part, která obsahuje odkaz na třídu, ze které bude komponenta vytvořena. Setting je objekt, který se předá konstruktoru komponenty, pokud je definován. Konstruktoru každé komponenty se předává owner, name a volitelně setting, owner je odkaz na vlastníka v hiearchii komponent a name je název pod kterým bude ve vlastníkovi vytvořena vlastnost, ve které bude instance dané komponenty.
Kořenová třída stromu komponent musí definovat proměnnou TOP_LEVEL = true. Poté je možné v každé komponentě libovolně zanořené ve stromu získat odkaz na kořenovou komponentu voláním this.getMain();.
Jelikož si v našem příkladu předáváme zprávy mezi komponentami přes rozhraní SigInterface, je pak možné v komponentách získat objekt this.signals z kořenové komponenty:
Lights = SZN.ClassMaker.makeClass({
NAME: 'Lights',
VERSION: '1.0',
IMPLEMENT: [SZN.Components, SZN.SigInterface]
});
Lights.prototype.$constructor = function(owner,name){
this._name = name;
this._owner = owner;
this.main = this.getMain(); //získání rodiče v hiearchii komponent, pokud je třeba
this.signals = this.setInterface('signals'); //získání objektu Signals z kořenové komponenty
...
}
V příkladu si můžete zobrazit auto, které můžete pojmenovat a určit mu barvu. Po vykreslení ho lze zapnout, nastartovat, rozjet, zastavit a vypnout.
Vytvořit auto jménem a s barvou
Dalším usnadněním v používání složité hiearchie komponent je registrace metod komponent do rodičovského objektu. Tedy místo volání např. car.engine.fuelPump.switch('off'); lze metodu switch objektu fuelPump propagovat jednak do objektu engine, nebo až car. Toho lze docílit tak, že po definování metody komponenty, této metodě přiřadíme statickou vlastnost access, která obsahuje řetězec s obsahem "public":
Lights.prototype.switch = function(state){
if (state == 'on') {
//zapni světla
} else {
//vypni světla
}
};
Lights.prototype.switch.access = 'public';
Pak v konstruktoru komponenty Lights je nutné volat registredMethod() pro registraci těchto metod:
Lights = SZN.ClassMaker.makeClass({
NAME: 'Lights',
VERSION: '1.0',
IMPLEMENT: [SZN.Components, SZN.SigInterface]
});
Lights.prototype.$constructor = function(owner,name){
this._name = name;
this._owner = owner;
this.main = this.getMain(); //získání rodiče v hiearchii komponent, pokud je třeba
this.signals = this.setInterface('signals'); //získání objektu Signals z kořenové komponenty
this.registredMethod(this.main);
}
Argumentem je odkaz na objekt, do kterého budeme metody propagovat, v aktuálním případě bude propagována do kořenové komponenty. V kořenové komponentě bude registrována metoda s názvem <třída><metoda>, tedy v našem případě lightsSwitch. Pokud potřebujeme, aby se metoda jmenovala úplně jinak (např. vytváříme strukturu komponent, která je ještě v betaberzi a bude se často měnit, ale chceme mít již stabilní rozhraní), pak ji můžeme přejmenovat za klíčovým slovem public při definici statické vlastnosti metody, tedy:
Lights.prototype.switch = function(state){
if (state == 'on') {
//zapni světla
} else {
//vypni světla
}
};
Lights.prototype.switch.access = 'public zapniSvetla';
Třídy k příkladu auta jsou ke stažení v car.zip.
Požadované knihovny:- main.js
- classmaker.js
- signals.js
- sigingerface.js
- components.js
Spuštění kódu, když je manipulace s DOMem bezpečná
Spustit kód, když je již DOM načten a inicializován, je možné několika způsoby. Z počátku bylo běžné tento kód navěsit na událost window.onload, ale nyní, když stránky obsahují mnoho velkých obrázků, je tato událost spuštěna až s velkým spožděním a uživateli se nemusí dostat po dlouhou dobu požadované funkčnosti.
Naopak v Seznamu používáme druhou možnost a to, že inicializaci manipulujícího skriptu vkládáme do stránky ihned za DOM elementy se kterými pracuje. To má za následek, že funkčnost je k dispozici skoro ihned, když je prvek zobrazen.
Třetí střední cestou je navěšení spuštění kódu až na DOMReady událost, která je dostupná ve většině JS knihoven. Spouštěč na této události byl do JAKu dopsán pro vás. Používá se jednoduše zavoláním:
SZN.Events.onDomReady(obj, 'func');
Pak je metoda func objektu obj zavolána, až když je DOM kompletně inicializován.
Požadované knihovny:- main.js
- classmaker.js
- events.js
Přidané prototypové metody
Knihovna JAK rozšiřuje prototyp Array o metody definované v JavaScriptu 1.6, resp. jen tehdy, pokud tyto metody již nejsou definovány. Proto je nutné ve všech vašich skriptech používat pro procházení polí konstrukcí for (var i = 0; i < arr.length; i++){} a ne for (key in arr){}, která se používá pro procházení objektů. Jinak v prohlížečích, kde tyto metody nejsou definovány prohlížečem ale pouze naším kódem, by tyto metody byly v tomto cyklu vypsány.
Dále jsou přidány čtyři nové prototypové vlastnosti. Tři do objektu String a jedna do Date. Do objektu String jsou přidány metody lpad a rpad pro doplnění řětězce na požadovanou velikost pomocí zadaného řetězce znaků a trim pro oříznutí bílých znaků zleva i zprava.
"2".lpad(); //vrátí: 02, výchozí doplňovaný znak je 0 a počet znaků v řetězci 2
"7".lpad(0,3); //vrátí: 007
"Ahoj".lpad('-=', 9); //vrátí: -=-=-Ahoj
"1".rpad(); //vrátí: 10
"Ahoj".rpad("!", 7); //vrátí: Ahoj!!!
"xyz".rpad('_abc',10); //vrátí: xyz_abc_ab
" test ".trim(); //vrátí: test bez mezer
Metody lpad a rpad očekávájí dva parametry. První je řetězec s výplní a druhým parametrem je počet znaků výsledného řetězce. Pokud nejsou zadány, jsou výchozí hodnoty 0 a 2, tedy výsledný řetězec by měl mít dva znaky a popřípadě bude doplňen nulou. Tyto hodnoty jsou výhodné pro třetí novou prototypovou vlastnost pro formátovaní data.
Objekt Date obsahuje novou prototypovou metodu format, která by měla zjednodušit výpis data v uživatelském formátu. Parametrem metody je formátovací řetězec. Tento ředězec má shodné formátovací vlastnosti jako metoda date v PHP.
var d = new Date();
d.setTime(1234567890 * 1000); //valentýn roku 2009
d.format('Y-m-d'); //vrátí: 2009-02-14
d.format('d.M.Y H:i:s'); //vrátí: 14.Feb.2009 00:31:30
//chceme české názvy
d._monthNames = ["Leden", "Únor", "Březen", "Duben", "Květen", "Červen", "Červenec", "Srpen", "Září", "Říjen", "Listopad", "Prosinec"];
d.format('d.F Y'); //vrátí: 14.Únor 2009
Objekt data dostává také do vínku 4 prototypové vlastnosti pro formátování data s názvy měsíců a dnů:
- _dayNames - pole dlouhých názvů dnů
- _dayNamesShort - pole zkratek dnů
- _monthNames - pole názvů měsíců
- _monthNamesShort - pole zkratek měsíců
Pokud budeme v aplikaci chtít formátovat data pro různé jazyky je vhodné předdefinovat tyto proměnné lokálně, viz ukázka výše, jinak je vhodné předdefinovat přímo prototypové vlastnosti.
