Seaside (8)

25. 4. 2005
Doba čtení: 4 minuty

Sdílet

Komponenty v Seaside jsou zcela samostatné entity, což usnadňuje možnost jejich opakovaného použití. Existuje však jedna situace, kdy je tato vlastnost v přímém protikladu ke standardům formátu HTML, a s níž se dnes pokusíme vypořádat - vnořené formuláře.

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.

Seaside 8a

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.

Seaside 8b

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.

bitcoin školení listopad 24

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.

Seriál: Seaside

Autor článku