Jako příklad si vezměme komponentu rezervačního systému, která bude zobrazovat informace o kongresu pořádaném jednou nebo více firmami.
Metoda pro vykreslení této komponenty bude schématicky vypadat nějak takto
renderContentOn: html
html form: [
html table: [
self renderMenuOn: html.
self renderPopisOn: html.
self renderFirmyOn: html.
self renderPocetOsobOn: html.
" atd. " ].
html submitButtonWithText: 'Uložit' ].
Volaná metoda renderFirmyOn: pak bude mít přibližně následující obsah
renderFirmyOn: html
html tableRow: [
html tableData: [ html text: 'Firmy:' ].
html tableData: [
html anchorWithAction: [ self pridejFirmu] text: 'Nová' .
html space.
html anchorWithAction: [ self najdiFirmu] text: 'Najít' ] ].
html tableRow: [
html attributeAt: 'colspan' put: '2';
tableData: [firmy do: [:firm | html render: firm ] ] ].
Tedy informace o firmách jsou prezentovány pomocí samostatných vnořených komponent, které se vykreslují pomocí zprávy render:. Dokud bude obsah této komponenty skryt pomocí obalujícího dekorátoru, je vše v pořádku. Problém ale nastane v okamžiku, kdy tento dekorátor rozbalíme.
Struktura komponenty pro kongres a firmu je totiž víceméně shodná. Také se jedná o formulářovou komponentu s potvrzovacím tlačítkem. To bohužel znamená, že ve výsledném HTML kódu budeme mít jeden formulář zanořený ve druhém, což je nepřípustná konstrukce. Rovněž Seaside si s tímto stavem neporadí a při pokusu o potvrzení jednoho z formulářů se nám dostane zmateného hlášení o tom, že doba platnosti stránky již vypršela.
Tento problém je hodně palčivý, protože výrazným způsobem omezuje znuvupoužitelnost komponent. Každá komponenta by měla být navržena tak, aby se dala použít v libovolné situaci, a nyní jsme nuceni je rozlišovat podle toho, zda mohou svůj obsah obalovat formulářem, či nikoliv, aby výsledná stránka žádné vnořené formuláře neobsahovala.
U komponent jako RichEdit je prakticky vždy jisté, že budou do nějakého formuláře zanořeny, ale u komponenty pro editaci firmy si již něčím takovým jistí být nemůžeme, protože by velmi snadno mohla figurovat na zcela samostatné stránce.
V budoucích verzích Seaside pravděpodobně bude renderer místo přímého vykreslování XHTML kódu pracovat s budováním syntaktického stromu generované stránky, takže bude schopen tuto situaci ošetřit. Prozatím se ale o řešení tohoto problému musíme postarat sami. Naštěstí to není nijak složité.
Pro tento účel opět budeme muset využít vlastní rozšířenou třídu základní komponenty WAComponent, kterou jsme si v minulých dílech nazvali UserComponent. Princip bude podobný jako v případě hledání kořene, cílové komponenty apod. Budeme rekurzivně hledat kořenovou komponentu formuláře.
UserComponet >> formRoot
^ (parent respondsTo: #formRoot)
ifTrue: [ parent formRoot ]
ifFalse: [ nil ].
Použití ale tentokrát bude o něco komplikovanější, protože potenciální formulářové kořenové komponenty si svou rolí nemohou být zcela jisté a musí se na ni ptát svých rodičů. Tuto akci vložíme do metody formRootOrSelf ve třídě UserComponent.
formRootOrSelf
| formRoot |
formRoot := (parent respondsTo: #formRoot)
ifTrue: [ parent formRoot ]
ifFalse: [ nil ].
^ formRoot ifNil: [ self ].
Formulář už není možné vykreslovat pomocí jednoduché zprávy form:, ale musíme si pro tento účel vytvořit vlastní metodu. Pokud komponenta zjistí, že není kořenovou formulářovou komponentou, svůj obsah zabalí místo tagu form do kontejneru.
UserComponent >> formOn: html with: aBlock
self formRoot == self
ifTrue: [ html form: aBlock. ]
ifFalse: [ html div: aBlock ].
Komponenty, které budou při renderování své dceřiné komponenty balit do formuláře, musí přetížit svoji metodu formRoot následujícím způsobem:
KongresEditor >> formRoot
^ self formRootOrSelf
a používat při renderování svou metodu formOn:with:
renderContentOn: html
self formOn: html with: [
html table: [
self renderMenuOn: html.
self renderPopisOn: html.
self renderFirmyOn: html.
self renderPocetOsobOn: html.
" atd. " ].
html submitButtonWithText: 'Uložit' ].
Tím bohužel naše útrapy nekončí. Například použitá komponenta pro RichEdit musí využívat ke své činnosti podporu Javascriptu, konkrétně u formulářů musí umět reagovat na událost onSubmit(). Sama tato komponenta však svůj obalující formulář vykreslovat nemusí a většinou to ani nedělá, protože tak činí některá z jejích rodičovských komponent.
Rodičovské formulářové komponentě musíme dát nějakým způsobem vědět, že má formulář renderovat s definovanými atributy. Bohužel nám nezbude nic jiného než si slovník atributů vložit do instančních proměnných našich uživatelských komponent.
Naši základní komponentní třídu UserComponent si upravíme tak, aby navíc obsahovala instanční proměnnou attributes, kterou postupně budeme naplňovat atributy roztříděnými do kategorií (tu budeme mít zatím jen jednu nazvanou formAttributes). Atributy se pak budou v případě potřeby aplikovat na vykreslovaný tag form.
WAComponent subclass: #UserComponent
instanceVariableNames: 'parent attributes'
classVariableNames: ''
poolDictionaries: ''
category: 'Rezervace'
attributes
^attributes
attributes: anObject
attributes := anObject
addAttribute: attributeAssociation category: aSymbol
self attributes ifNil: [self attributes: Dictionary new].
(self attributes at: aSymbol ifAbsentPut: Dictionary new) add: attributeAssociation.
formOn: html with: aBlock
self formRoot == self
ifTrue: [
| attribs |
attribs := self attributes at: #formAttributes ifAbsent: Dictionary new.
attribs associationsDo: [ :attr |
html attributeAt: attr key put: attr value.
].
html form: aBlock. ]
ifFalse: [
html div: aBlock ]
Komponenty, které budou formulářové atributy rozšiřovat, tak učiní ve své inicializační metodě.
RichEdit >> initialize
self formRoot addAttribute: (#onSubmit -> 'updateRTEs();') category: #formAttributes
Instanční proměnnou attributes necháváme neinicializovanou (má implicitní hodnotu nil). Skutečný slovník atributů vytvoříme až v okamžiku, kdy bude skutečně potřeba.
Zatím jsme neřešili situaci, kdy potřebuje stejné atributy formuláře měnit více než jedna dceřiná komponenta. Spojování atributů samozřejmě závisí na jejich charakteru a o tom, jak konkrétně se to bude provádět, musí rozhodnout programátor. V našem případě, kdy jsme pracovali s událostí onSubmit(), by bylo nejvhodnější dodávané atributy konkatenovat a pro jistotu doplňovat oddělující středník.
addAttribute: attributeAssociation category: aSymbol
| category |
self attributes ifNil: [self attributes: Dictionary new].
category := self attributes at: aSymbol ifAbsentPut: Dictionary new.
((category = #formAttributes) and: [attributeAssocitation key = #onSubmit])
ifTrue: [ category at: #onSubmit put:
(category at: #onSubmit ifAbsent: ['']), ';', attributeAssociation value.
^ self].
category add: attributeAssociation.
V následujícím pokračování se mimo jiné budeme zabývat tím, jak Seaside řeší problémy větvení toku řízení a zpětné navigace v prohlížečích.