Image Credit

Dynamic content loading in Willow

Maximiliano Tabacman
mercap-blog
Published in
4 min readFeb 10, 2021

--

Here is the sixth of our series about developing web applications using Willow. This one is about changing part of the content of your application without reloading the whole page.

The series so far:
1) Introducing Willow
2) Your first Willow application
3) Rendering content in Willow
4) Component Suppliers in Willow
5) User Interaction in Willow
6) Dynamic content loading in Willow (this post!)

Why not load the whole page?

As web applications grow in complexity, there is also an increase in the amount of information that must be generated in the server, then sent over to the browser, then presented to the user.

To ensure the best user experience, this time must be reduced to the minimum possible. And once the interaction with the user starts, there should be no more waiting times. This can be achieved by dynamically loading and changing only the parts of your application that relate to the actions of the user, while leaving the rest untouched. Willow also provides affordances to create an application where the user can start interacting before all the components are loaded, but that is outside the scope of the present post.

What can I change after loading?

In terms of the HTML content of your application, the AJAX techniques allow for any component to be changed or removed.

Willow focuses those features by requiring the declaration of containers, which are then updated when needed. They are represented in the ContainerWebView class, which can be created using any valid block of code that receives a Seaside canvas and returns a component. The most common usage is for an HTML div, which you can obtain using:

ContainerWebView
wrapping: [ :canvas | "Your dynamic content" ]
intoElementBuiltUsing: [ :canvas | canvas div ]
applying: [ … ]

The component supplier of your application provides messages for the 2 most common containers. Sending divisionContainerWrapping:applying: will create a div, while sending inlineContainerWrapping:applying: will create a span.

How do I change the content of containers?

Since containers are created wrapping a block, anything inside them will be evaluated every time they are rendered by Seaside. To command such a rendering, you just need to instruct it as part of the interaction of a triggering object.

To see it in action, let’s reuse the ToDoApplication>>#renderTasksOn: message of our application, to change the task list for something completely different: a div containing the current time, with a button to refresh it on demand.

renderTasksOn: aCanvas
| button container |

button := self componentSupplier
asynchronicButtonLabeled: 'Refresh'
applying: [ ].
container := self componentSupplier
divisionContainerWrapping:
[ :canvas | canvas render: Time now ]
applying: [ ].
button on trigger render: container. aCanvas
render: container;
render: button

How can I use containers in a To-Do application?

Glad you asked! The missing piece in our application is allowing it to add new tasks by filling the text field and clicking a button.

Since this is a tutorial on how to properly use Willow, we will now list a number of changes from the current implementation of the application, to ensure a tidy and compact number of methods in the project.

First, we’ll re-design our PendingTask class to allow a custom description.

PendingTask class>>
describedBy: aDescription
^ self new initializeDescribedBy: aDescription
PendingTask>>
initializeDescribedBy: aDescription
description := aDescription
renderContentOn: aCanvas
aCanvas listItem: description

Now we’ll make sure that ToDoApplication includes a list of tasks, and knows about (1) a container for the tasks, (2) a text field to declare new tasks and (3) a button to confirm that the text is the description of a new task. Since we’ll depend on the component supplier to be initialized before creating the components, we need to perform the initialization of the instance variables in the initialRequest: method.

initialRequest: request
super initialRequest: request.
tasks := OrderedCollection new.
self initializeTasksContainer.
self initializeTaskDescriptionField.
self initializeAddTaskButton.
self configureInteractions
initializeTasksContainer
tasksContainer := self componentSupplier
divisionContainerWrapping:
[ :canvas | canvas unorderedList: tasks ]
applying: [ ]
initializeTaskDescriptionField
taskDescriptionField := self componentSupplier
singleLineTextFieldApplying:
[ :theField | theField setPlaceholderTo: 'Write your task' ]
initializeAddTaskButton
addTaskButton := self componentSupplier
asynchronicButtonLabeled: 'Click me'
applying: [ :theButton | theButton beDisabled ]
configureInteractions
taskDescriptionField on trigger
serializeIt;
enable: addTaskButton.
addTaskButton on trigger
disable;
serverDo: [ tasks add: (PendingTask describedBy: taskDescriptionField model) ];
setValueTo: [ '' ] withoutTriggeringChangeOf: taskDescriptionField;
render: tasksContainer

Note that taskDescriptionField receives a command to serializeIt because we need the value in the server to be updated when in changes in the browser. Since the enable: command does not require calling the server, it is not enough to guarantee an updated value.

Now all that remains is updating our #contentView

contentView
^ [ :canvas |
canvas
heading: 'My To-Do';
render: tasksContainer;
render: taskDescriptionField;
render: addTaskButton ]

And we can safely remove addTaskButton, taskDescriptionField, renderTasksOn:.

You now have a fully functioning To-Do list. The tasks are kept in memory in this example. For ideas on how to persist information outside of a Pharo image, see the Sagan project.

--

--