Hlavní navigace

Seaside (4)

21. 3. 2005
Doba čtení: 9 minut

Sdílet

Při dnešní procházce po mořském pobřeží se budeme podrobně věnovat problematice používání kaskádových stylů. Několik řádků věnujeme i využití Javascriptu.

Při generování XHTML kódu jednotlivých komponent je na místě snažit se pomocí smalltalkovského rendereru vytvořit pouze logickou strukturu a data. O tom, co a jak se konkrétně zobrazí uživateli, rozhodují kaskádové styly.

Stejně jako se HTML kód generuje pro každou komponentu samostatně, tak i styly se přiřazují každé komponentě zvlášť. Dělá se to velmi jednoduše pomocí instanční metody style, která má za úkol vrátit textový řetězec obsahující stylesheet dané komponenty.

style
^ '
.MyComponent { border: 2px solid red; }
.MyComponent h1{ font-family: sans-serif;  font-size: 16pt; }
'

Při renderování máme několik možností, jak styl k XHTML entitám přiřadit, výsledek je ale vždy stejný, jako bychom jednoduše definovali atribut class následujícímu elementu

renderContentOn:  html

    html attributeAt: #class put: #MyComponent;
        div: [  html heading: 'My component'; text: 'My first component' ].

Protože se jedná o často používanou operaci, je k dispozici jednodušší metoda cssClass.

html

    html cssClass: #MyComponent;
        div: [ html heading: 'My component'; text: 'My first component' ].

Například u elementů div lze toto volání dále zpřehlednit pomocí metody divClass:with:, která jako první parametr přijímá jméno třídy stylu a jako druhý obsah kontejneru.

renderContentOn:  html

    html divClass: #MyComponent with: [
        html heading: 'My component'; text: 'My first component' ].

Při definici stylu se definice tříd uvozují tečkou. Hash (#) pak slouží pro definování stylů, které jsou elementům přiřazovány podle jejich identifikátoru nebo podle jména. Tyto atributy jsou zejména potřebné při práci s Javascriptem. Identifikátor by měl být v každé stránce unikátní, což v případě komponent znamená, že by měl být generován programem. Seaside automatické přiřazování identifikátorů všem elementům neprovádí.

Elementům se identifikátory a jména definují buď pomocí prostého přidání atributu, nebo pomocí metod rendereru jménem cssId: acssName:

style

    ^ '
#MyList { background-color: lightGray;}
#MyList li { list-style-type: circle;  }
'

renderContentOn:  html

    html cssId: #MyList; div: [
        html list: #( 'Item 1' 'Item 2' 'Item 3') ].

Nastupuje designér

Pokud ve vývojovém týmu existuje specialista na grafiku a práci s kaskádovými styly, nebylo by moc vhodné nutit ho pracovat přímo se Smalltalkem, neb by mohl váš tým velmi rychle dobrovolně opustit. Seaside nabízí základní prostředky, které dovolují práci programátorů a designérů separovat. Nicméně i tak se grafici budou nuceni vypořádat s něčím hodně odlišným od toho, na co byli pravděpodobně zvyklí ve své předchozí kariéře.

Práce designérů probíhá přímo z webového rozhraní. V první řadě je nutné, aby aplikace neběžela v tzv. deployment módu a na stránce se tedy zobrazovalo menu s nástroji Seaside. Ty, které je budou zajímat, jsou tzv. halos. Jedná se o stejný koncept, který je použit v grafickém prostředí Morphic k práci s morphy, kdy se kolem zvoleného objektu objeví několik užitečných ikonek.

Podobně se při zapnutí zobrazování halos kolem každé komponenty objeví rámeček, ve kterém je vepsáno jméno komponenty a vykresleno několik drobných ikonek a odkazy s majuskulemi R a S. Okamžitě tak lze získat přehled o hierarchii a skladbě komponent ve vygenerované stránce.

První malá ikonka (maticový klíč) slouží k zobrazení webové obdoby smalltalkovského browseru nad třídou komponenty. Ikonka s okem ukáže jednoduchý inspector zpřístupňující objekty referencované instančními proměnnými komponenty. Tyto dva nástroje jsou určeny především programátorům.

Pro designéra je nejdůležitější ikonka s tužkou, která pro danou komponentu umožní definovat kaskádový styl pomocí velmi jednoduché stránky s textovým polem, kam se vepíše stylesheet. Při jeho uložení je upravena a překompilována přímo metoda style modifikované komponenty.

Odkaz R přepíná pro určenou komponentu na výsledné vyrenderované zobrazení, odkaz S v přehledné formě ukáže vygenerovaný XHTML kód komponenty se zvýrazněnou syntaxí. Pokud komponenta obsahuje další vnořené komponenty, zobrazí se i jejich kód.

V praxi samozřejmě budou muset programátoři i designéři vždy úzce spolupracovat a vzájemně ovlivňovat svoji práci. Je tomu tak u každého webového frameworku. Seaside je naštěstí dostatečně jednoduchá a flexibilní na to, aby ji vývojový tým mohl snadno přizpůsobit svým potřebám a dělbě práce.

Generované styly

Při tvorbě složitějších aplikací pravděpodobně rychle narazíme na situaci, kdy popsaný systém nebude dostatečně flexibilní. V první řadě se jedná o případy, kdy mezi třídami komponent používáme dědičnost. Například

MyAbstractComponent superclass = WAComponent.
MyComponent superclass = MyAbstractComponent.

Seaside se styly pracuje tak, že je přidává jako odkazy na generované dokumenty do cílové stránky. Přidává takto ovšem jen styly těch komponent, které se na dané stránce skutečně nacházejí. Obecně to neplatí o stylech rodičovských obecnějších komponent. Seaside totiž styl získá tak, že komponentě prostě zašle zprávu style. To znamená, že pokud v odvozené komponentě rozšiřujeme styly komponenty původní, musíme je oba zkonkatenovat.

style
    ^ (super style ifNil: [String new]), '
.Component p { font-size: 9pt; }
'

Test na nil je vhodný pro případ, kdy rodičovská komponenta přece jenom žádný styl nevrací.

Použití jakéhokoliv jiného kódu metody style, než je jen pouhé vrácení řetězce se stylesheetem, má jedno nepříjemné úskalí. Webové rozhraní pro editaci stylu komponenty rovněž pracuje s pouhým voláním metody style. Proto bude designérovi předhozen stylesheet komponenty včetně stylesheetu rodiče dohromady a při modifikaci a uložení stylu se jako celek přegeneruje do metody style editované komponenty. Původní programátorem upravený obsah bude ztracen. Styly rodičovské komponenty mohou být v takovém případě ve vygenerované stránce obsaženy dvakrát.

Bojovat s tím jednoduše nejde. Nejpřímější řešení je zakázat editovat vzhled přes webové rozhraní u těch komponent, které styly generují programově, tedy se zásahem programátora. Například tak, že u třídy WAComponent vytvoříte metodu jménem hasGeneratedSty­les tím, že bude vracet false. U tříd, které naopak styl generují, bude pak vracet true Následně upravíte ve třídě WAViewer metodu renderCssOn: tak, aby provedla test, zda komponenta generuje kaskádový styl, a v tom případě nezobrazila uživateli editační prvky pro jeho změnu a odkázala ho do patřičných mezí. Lze samozřejmě vymyslet i sofistikovanější metody a do zdrojových kódů Seaside zasáhnout razantnějším způsobem tak, aby nejlépe vyhovovala vašim potřebám či potřebám vyvíjené aplikace.

Aktuální implementace je určena spíše pro případy, kdy abstraktní třídy pomáhají generovat kód a o přiřazení stylů se starají až konkrétní třídy. V abstraktních třídách se pak nepoužívají přímo jména stylů, ale objekt se na ně vždy dotáže sebe sama. Například v následujícím případě byla pro vrácení jména konkrétního stylu použita metoda divClass

renderHeaderOn: html

    html divClass: self divClass with: [ html text: self model listString ]

Knihovny stylů

Pro zlepšení organizace práce s komponentami a jejich vzhledem lze používat vhodné kombinace přiřazování stylů elementům. Tedy vytvořit si např. následující stylesheet

style

^ '
.boxed {
    border: 1px solid black;
    padding: 2px;
    margin: 1px;
}

.highlighted {
    background-color: lightGreen;
}
'

a poté třídy stylů aplikovat při renderování následujícím způsobem

renderContentOn:  html

    html divClass: 'boxed highlighted' with: [
    html text: 'Super offer' ].

Při tomto přístupu není přiřazování stylů do jednotlivých komponent příliš praktické. Aby se tyto obecné styly daly snadno sdílet mezi více nezávislými třídami komponent, přímo se nabízí možnost využít pro ně tzv. knihovny stylů.

O co jde? Knihovny stylů jsou jednoduché třídy odvozené od třídy WAStyleLibrary Neobsahují nic jiného než metody, které vracejí stylesheety jako řetězce. Jako příklad může posloužit třída WAStandardStyles, která obsahuje například stylesheet sourceStyle pro zvýrazňování syntaxe HTML, dále stylesheetkal­seyTabs pro dělání záložek atd.

Knihovna stylů se přiřazuje v konfigurační aplikaci. Všechny stylesheety použité knihovny jsou automaticky dodávány do každé stránky vaší aplikace. Metody se stylesheety uvnitř tříd knihoven stylů není třeba nikde dále registrovat, protože Seaside k zjištění jejich seznamu používá reflexivní vlastnosti Smalltalku.

Přirozeně nemusí metody v knihovnách stylů být jen jednoduchým vrácením řetězce. Stylesheety je možné například načítat z externích souborů, a přiblížit se tak konvenčnímu způsobu práce s nimi, nebo je jakkoliv jinak generovat programově.

Velmi rozumné je do knihoven umístit například i styly pro formátování elementu body. Běžně se tyto styly sice definují v kořenových komponentách, ale umístění do knihovny je obecnější řešení. Můžeme se tak vyhnout problémům, které mohou nastat, pokud do aplikace umístíme více komponent konstruovaných tak, aby byly kořenové.

Javascript

Pro Javascript platí prakticky totéž, co pro styly, pouze se v komponentách umisťuje místo do metody style do metody script Knihovny scriptů se odvozují od třídy WAScriptLibrary V současnosti na rozdíl od stylů není možné je editovat přes webové rozhraní jinak než pomocí browseru.

Při vytváření komponent je nutné mít na paměti, že komponenty mohou být ve stránce umístěny hned několikrát, a proto je s unikátními identifikátory potřeba zacházet opatrně.

Příklad

Trochu rozsypaného čaje na závěr nikdy neuškodí. Mějme třídu komponenty a u ní definovanou instanční proměnnou items

WAComponent subclass: #MyComponent
    instanceVariableNames: 'items'
    classVariableNames: ''
    poolDictionaries: ''
    category: 'MyWebApplication'

V její inicializační metodě naplníme proměnnou item kolekcí obsahující asociace položky a popisu. Ve reálné aplikaci by se jednalo spíše o kolekci objektů reprezentujících nějaká data.

initialize

    super initialize.
    items := {
        #item1 -> 'description of item 1'.
        #item2 -> 'description of item 2'.
        #item3 -> 'description of item 3'.
    }.

V metodě pro renderování komponenty si nejdříve vytvoříme prvek, v němž budeme zobrazovat popisky položek, na které najedeme myší. V našem případě se bude jednat o textový vstupní prvek, ten jsme tu ještě neměli. Necháme si pro něj od Seaside vygenerovat identifikátor. Poté každý prvek zobrazíme jako položku seznamu (metody key/value získávají klíč resp. hodnotu asociace). Zároveň u každého prvku nadefinujeme jako atribut událost onMouseOver a jako volání Javascriptové funkce ShowDescription, kde první parametr bude identifikátor elementu, ve kterém má být popis prvku zobrazen, a druhý index prvku.

renderContentOn:  html

     | id |

     id := html ensureId.
     html inputWithType: #text.

   items withIndexDo: [:item :index |
    html attributeAt: #onMouseOver put: ('ShowDescription("', id, '",', (index-1) asString, ')').
        html listItem: item key ].

Pokud bychom potřebovali mít prvek s generovaným identifikátorem až za místem použití tohoto identifikátoru, nelze už metodu ensureId použít, protože automaticky naplňuje kolekci atributů rendereru a přiřazený identifikátor by se pak aplikoval na následující element.

renderContentOn:  html

     | id |

     id := html context advanceKey.

   items withIndexDo: [:item :index |
    html attributeAt: #onMouseOver put: ('ShowDescription("', id, '",', (index-1) asString, ')').
        html listItem: item key ].

   html cssId: id; inputWithType: #text.

Nakonec nám zbývá vytvořit příslušný Javascript. Popisy v tomto příkladu nejsou záměrně předávány přímo. V reálu je také spíše zapotřebí, aby aplikace byla schopná z Javascriptu identifikovat, s jakým datovým objektem se ve skutečnosti pracovalo. Ať už pomocí indexu v kolekci, nebo nějakého jiného identifikátoru.

Na začátku jsme si programově vytvořili pole s popisy. Abychom neustále nepoužívali relativně pomalou konkatenaci řetězců, využijeme proud. Poslední prvek musíme kvůli spojující čárce řešit samostatně.

root_podpora

script

    | descs |

    descs := String streamContents: [ :s |
        items allButLast do: [:item | s nextPutAll: ('''', item value, ''' , ') ].
        items ifNotEmpty: [ s nextPutAll: '''', items last value, '''' ].
        ].

^ '
    Descriptions = [ ', descs, ' ];

    function ShowDescription(label, index)
    {
        document.getElementById(label).value= Descriptions[index];
    }
'

Tento příklad je pouze ilustrační a trpí jedním poměrně zásadním nedostatkem. Data v Javascriptu jsou definována globálně, což bude dělat problémy, pokud komponentu umístíme do stránky vícekrát. Opravit to lze například tím, že s každou komponentou vygenerujeme patřičné pole dat již v renderovací metodě a nazveme je unikátním identifikátorem. Metodě ShowDescription pak musíme toto pole předat jako parametr nebo spáchat nějaké jiné Javascriptové harakiri.

    html script: 'Descriptions', id, '  = [ ', descs, ' ]'.

Zatím jsme se z pohledu Seaside plácali s podružnostmi. To se ale pokusíme napravit příště, kdy konečně zabrousíme do oblasti práce s daty.

Byl pro vás článek přínosný?