Dynamic Web Development with Seaside

12.3Components All The Way Down

The changes to your code in this section are presented purely to help you explore the embedding of components: they are not an example of good UI design, and are not required to progress with the following sections.

Let’s define a component that manages our list of contacts using components all the way down. Figure 87 shows that we will build our application out of two components: PluggableContactListView and ContactView. In addition, PluggableContactListView will be composed of several ReadOnlyOneLinerContactView components. We really get a tree of components. This exercise shows that a component should be designed to be pluggable. It also shows how to plug components together.

With components all the way down

A Minimal Contact Viewer. We would like to have a compact contact viewer. First we will subclass the ContactView class to create the class ReadOnlyOneLinerContactView. This class has an instance variable parent which will hold a reference to the component that will contain it, since it should know how to invoke the contact editor.

ContactView subclass: #ReadOnlyOneLinerContactView
instanceVariableNames: 'parent'
classVariableNames: ''
poolDictionaries: ''
category: 'iAddress'
ReadOnlyOneLinerContactView>>parent: aParent
parent := aParent

When the user clicks on the contact name, we want the associated user object to pass itself to the parent for editing. A similar action should occur when removing a contact from the database. Note that this component does not include a form. This is because only one form should be present on a page at any time, so a component is much more reusable if it does not define a form.

ReadOnlyOneLinerContactView>>renderContentOn: html
html anchor
callback: [parent editContact: self contact];
with: self contact name.
html space.
html text: self contact emailAddress.
html space.
html anchor
callback: [parent removeContact: self contact];
with: '--'.
html break.

Class Definition. Now we define the class PluggableContactListView. Since this component will embed all the contact viewer components, we add an instance variable contactViewers, that will refer to them. We also define an instance variable to refer to the editor that will show the detailed information of the currently selected contact.

ContactListView subclass: #PluggableContactListView
instanceVariableNames: 'contactViewers editor'
classVariableNames: ''
poolDictionaries: ''
category: 'iAddress'

We will use an identity dictionary to keep track of the contact viewer associated with each contact. Initializing our top level component consists of first creating the editor and then creating the viewers for the existing contacts.

^ Contact contacts
super initialize.
editor := ContactView new.
contactViewers := IdentityDictionary new.
self contacts do: [ :each | self addContactViewerFor: each ]
PluggableContactListView>>addContactViewerFor: aContact
at: aContact
put: (ReadOnlyOneLinerContactView new
contact: aContact;
parent: self; " <-- added "
| name emailAddress |
name := self request: 'Name'.
emailAddress := self request: 'Email address'.
self addContact: (Contact name: name emailAddress: emailAddress)
PluggableContactListView>>editor: anEditor
editor := anEditor

Children accessing and rendering. We have now to specify that the contact viewers are embedded within the PluggableContactListView and how to render them.

^ contactViewers values
PluggableContactListView>>renderContentOn: html
contactViewers values
do: [ :each | html render: each. html break ]

We define a couple of methods to manage contacts.

PluggableContactListView>>addContact: aContact
Contact addContact: aContact.
self addContactViewerFor: aContact
PluggableContactListView>>removeContact: aContact
contactViewers removeKey: aContact.
Contact removeContact: aContact

Plugging everything together. Now we are ready to define a new version of IAddress. We simply subclass IAddress and pay attention to the fact that the list is now a component. So we initialize it, add it as part of the children of the component and render it.

IAddress subclass: #IAddressTwoComponents
instanceVariableNames: 'list'
classVariableNames: ''
poolDictionaries: ''
category: 'iAddress'

We pass the list component to the editor which has already been initialized in IAddress. We must also invoke the list’s askAndCreateContact method, since it is the list that manages the creation of contacts. The rendering of the component includes a form in which the other components are embedded.

super initialize.
list := PluggableContactListView new.
list editor: editor " <-- added "
^ super children , (Array with: list)
IAddressTwoComponents>>renderBarOn: html
html anchor
callback: [ list askAndCreateContact ];
with: 'Add contact'
IAddressTwoComponents>>renderContentOn: html
html form: [
self renderTitleOn: html.
self renderBarOn: html.
html break.
html render: list.
html horizontalRule.
html render: editor ]

Note of course that embedding such an editor under the list of contacts is not a really good UI design. We just use it as a pretext to illustrate component embedding.

Copyright © 23 April 2024 Stéphane Ducasse, Lukas Renggli, C. David Shaffer, Rick Zaccone
This book is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 license.

This book is published using Seaside, Magritte and the Pier book publishing engine.