-
Notifications
You must be signed in to change notification settings - Fork 41
/
Copy pathSeaside.pillar
1519 lines (1219 loc) · 64.6 KB
/
Seaside.pillar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{ "title": "Seaside by example" }
@cha:seaside
Seaside is a framework for building web applications in Smalltalk, originally
developed by Avi Bryant and Julian Fitzell in 2002. Once mastered, Seaside makes
web applications almost as easy to write as desktop applications. Seaside is
really interesting for developing fast complex applications. For example
*http://allstocker.com* is a Seaside commercial applications or the Quuve,
FinWorks and CableExpertise application from *http://www.pharo.org/success*
have been developed in Seaside.
Seaside is unusual in that it is thoroughly object-oriented: there are no HTML
templates, no complicated control flows through web pages, and no encoding of
state in URLs. Instead, you just send messages to objects. What a nice idea!
!!Why do we need Seaside?
Modern web applications try to interact with the user in the same way as desktop
applications: they ask the user questions and the user responds, usually by
filling in a form or clicking a button. But the web works the other way around:
the user's browser makes a request of the server, and the server responds with a
new web page. So web application development frameworks have to cope with a host
of problems, chief among them being the management of this ''inverted'' control
flow. Because of this, many web applications try to forbid the use of the
browser's ''Back'' button due to the difficulty of keeping track of the state of
a session. Expressing non-trivial control flows across multiple web pages is
often cumbersome, and multiple control flows can be difficult or impossible to
express.
Seaside is a component-based framework that makes web development easier in
several ways. First, control flow can be expressed naturally using message
sends. Seaside keeps track of which web page corresponds to which point in the
execution of the web application. This means that the browser's ''Back'' button
works correctly.
Second, state is managed for you. As the developer, you have the choice of
enabling backtracking of state, so that navigation ''Back'' in time will undo
side-effects. Alternatively, you can use the transaction support built into
Seaside to prevent users from undoing permanent side-effects when they use the
back button. You do not have to encode state information in the URL — this too
is managed automatically for you.
Third, web pages are built up from nested components, each of which can support
its own, independent control flow. There are no HTML templates — instead valid
HTML is generated programmatically using a simple Smalltalk-based protocol.
Seaside supports Cascading Style Sheets (CSS), so content and layout are cleanly
separated.
Finally, Seaside provides a convenient web-based development interface, making
it easy to develop applications iteratively, debug applications interactively,
and recompile and extend applications while the server is running.
!!Getting started
!!!The Seaside community
The Seaside.st website (*http://www.seaside.st*) contains many Seaside-related
resources, including downloads, documentation and tutorials. (Keep in mind that
Seaside has evolved considerably over the years, and not all available material
refers to the latest version.) There are also several active mailing lists,
which you can find at *http://www.seaside.st/community/mailinglist*.
In addition, the Wiki on the Seaside GitHub repository
(*https://github.com/seasidest/seaside/wiki*) provides some crucial
documentation, including the Release Notes for each version of Seaside.
!!!Installing Seaside using the one-click experience image
+Start up the Seaside One-Click Experience image.>file://figures/seasideOneClickExperience.png|label=fig:seasideOneClickExperience+
The easiest way to get started is to download the ==Seaside One-Click Experience
3.1== from the Pharo Downloads section of the Seaside web site
(*http://www.seaside.st/download/pharo*). This is a prepackaged version of the
latest stable version of Seaside for Mac OSX, Linux and Windows, built on Pharo
4. The current latest Seaside version is 3.2. These are full Pharo
distributions, which include the Pharo VM as well as a development image with
many Seaside-related packages preloaded, and a helpful Seaside control panel
that starts when you open the image.
Download and launch the Seaside One-Click image (feel free to refer to the
==Getting Started== section of
Chapter *: A Quick Tour of Pharo>../PharoTour/PharoTour.pillar@cha:tour* for an
in-depth discussion of launching Pharo images). Once started, you should see
something similar to Figure *@fig:seasideOneClickExperience* - a familiar
Pharo development environment with some windows pre-opened, such as welcome
messages as well as a Seaside Control Panel.
!!!Starting the Seaside server
The easiest way to start and stop the Seaside web server is through the
==Seaside Control Panel==. If you're using the Seaside One-Click Experience
image, it should already be open (and running a server adapter) when you open
the image for the first time. You can manually open it at any time, however,
by evaluating ==WAPharoServerAdapterSpecBrowser open.==
Right-clicking on the top pane of the Seaside Control Panel lets you add new
server adapters. For example, you can add a ==ZnZincServerAdaptor==, specify
a port number, then start it using the ==Start== button on the control panel,
which launches a new Seaside server that listens on that port.
[[[caption=Starting and stopping Seaside using the Zinc Server adaptor|label=scr:startingStoppingSeaside
ZnZincServerAdaptor startOn: 8080. "start on port 8080"
ZnZincServerAdaptor stop.
]]]
You can also start and stop the Seaside web server from a playground, by
sendning the ==startOn:== and ==stop== messages to your server adapter of
choice.
!!!The Seaside welcome page
Once the Seaside server is running, navigate to *http://localhost:8080/* in
your web browser. You should see a web page that looks like Figure
*@fig:seasideStartup*.
The Welcome page contains links to some sample Seaside applications. It also
links to various documents and resources, and (in the sidebar) links to the
Configuration and Browse applications that allow you to interact with Seaside
applications registered in your image.
Let's look at one of the example applications that demonstrates the Counter
component: click on the ==Counter== link.
This page is a small Seaside application: it displays a counter component that
can be incremented or decremented by clicking on the __++__ and __–\–__ links
(see Figure *@fig:counter*).
Play with the counter by clicking on these links. Use your browser's
''Back'' button to go back to a previous state, and then click on __++__ again.
Notice how the counter is correctly incremented with respect to the currently
displayed state, rather than the state that the counter was in when you started
using the ''Back'' button.
Notice the toolbar at the bottom of the web page in Figure *@fig:seasideStartup*.
Seaside supports a notion of ''sessions'' to keep track of the state of the
application for different users. ==New Session== will start a new session on the
counter application. ==Configure== allows you to configure the settings of your
application through a web interface. (To close the ==Configure==
view, click on the __x__ in the top right corner.) ==Halos== provides a
way to explore the state of the application running on the Seaside server.
==Profile== and ==Memory== provide detailed information about the run-time
performance of the application. ==XHTML== can be used to validate the generated
web page, but works only when the web page is publicly accessible from the
Internet, because it uses the W3C validation service.
+The Seaside Welcome application at ==http://localhost:8080/==.>file://figures/seasideStartup.png|label=fig:seasideStartup+
+The counter.>file://figures/counter.png|label=fig:counter+
!!!Single components
Seaside applications are built up from pluggable ''components''. In fact,
components are ordinary Smalltalk objects. The only thing that is special about
them is that they are instances of classes that inherit from the Seaside
framework class ==WAComponent==. We can explore components and their classes
from the Pharo image, or directly from the web interface using halos.
+Halos.>file://figures/counterHalos.png|label=fig:counterHalos+
Click on the ==Halos== link at the bottom of the page to toggle on halo
functionality. You'll see a number of nested components each with their own
halo icons, including the Counter component (you may have to scroll down a bit
to reach it), like the one seen in Figure *@fig:counterHalos*.
At the top left of the component, the text ==WACounter== tells us the class of
the Seaside component that implements its behavior. Next to this are three
clickable icons. The first is the ==Class Browser== (the notepad with pencil
icon), which opens up a (web-based) Seaside class browser on this component's
class. The second is the ==Object Inspector== (notepad with magnifying glass
icon), which opens a web-based object inspector on the actual ==WACounter==
instance. The third is the ==CSS Style Manager== (the coloured circles icon),
opens a Seaside view to display the CSS style sheet for this component.
At the top right of the component, the ==Render== / ==Source== links let you
toggle between the Rendered and (formatted) Source views of the component's
HTML source code. Experiment with all of these links. Note that the __++__ and
__-\-__ links are also active in the source view.
The Seaside Class Browser and Object Inspector can be very convenient when the
server is running on another computer, especially when the server does not have
a display, or if it is in remote place. However, when you are first developing a
Seaside application, the server will be running locally, and it is easy to use
the ordinary Pharo development tools in the server image.
+Debugging the running Counter application in the Pharo image.>file://figures/haltingCounter.png|label=fig:haltingCounter+
Using the Object Inspector link in the web browser, open an inspector on the
underlying Pharo counter object, type ==self halt== and click the ==doIt==
button.
The form submits, and the browser will hang, spinning. Now switch to the Pharo
Seaside image. You should see a pre-debugger window (Figure
*@fig:haltingCounter*) showing a ==WACounter== object executing a ==halt==.
Examine this execution in the debugger, and then ==Proceed==. Go back to the web
browser and notice that the Counter application is running again.
!!!Multiple components
Seaside components can be instantiated multiple times and in different contexts.
+Independent subcomponents.>file://figures/multiCounterHalos.png|label=fig:multiCounterHalos+
Now, point your web browser to *http://localhost:8080/examples/multicounter*.
You will see a simple application composed of a number of independent instances
of the ==WACounter== component, nested inside a ==WAMultiCounter==. Increment
and decrement several of the counters. Verify that they behave correctly even if
you use the ''Back'' button. Toggle the halos to see how the application is
built out of nested components. Use the Seaside class browser to view the
implementation of ==WAMultiCounter==. You should see two methods on the class
side (==description==, and ==initialize==) and three on the
instance side (==children==, ==initialize==, and ==renderContentOn:==). Note
that an application is simply a component that is willing to be at the root of
the component containment hierarchy; this willingness is indicated by defining a
class-side method ==canBeRoot== to answer ==true==.
You can use the Seaside web interface to configure, copy or remove individual
applications (which are root-level components). Try making the following
configuration change.
Point your web browser to *http://localhost:8080/config*.
If you are asked for a login, supply the default login and password (==admin==
and ==seaside==). You can see a list of a registered applications and
dispatchers. Change into the ==examples== dispatcher list by clicking on the
link on the left side. With the buttons at the topbar, we can add new request
handlers, open the current, copy, remove or set it as the default page. We want
to add a new example application. Select ==Add==, enter the name ==counter2==
and choose the type ==Application==, click ==OK== (see Figure *@fig:counter2*).
On the next screen, set the ==Root Component== to
==WACounter==, apply the changes (Figure *@fig:counter2_config*).
Now we have a new counter
installed at *http://localhost:8080/examples/counter2*. Use the same
==Remove==-button in the configuration interface to remove this entry point.
+Add a new application.>file://figures/counter2.png|width=100|label=fig:counter2+
+Configure the new application.>file://figures/counter2_config.png|width=100|label=fig:counter2_config+
Seaside operates in two modes: ''development'' mode, which is what we have seen
so far, and ''deployment'' mode, in which the toolbar is not available.
You can put a Seaside Application into deployment mode by removing the
inherited ==Root Decoration Class== (again, see Figure *@fig:counter2_config*).
Alternatively, you can disable the development toolbar for all new
aplications by evaluating the code:
[[[
AAdmin applicationDefaults
removeParent: WADevelopmentConfiguration instance
]]]
If you want to enable the password protection, you need to add the
==WAAuthConfiguration== in the ==Inherited Configuration==. Choose
==WAAuthConfiguration== from the list of possible parents, after adding this
configuration, you can define the ==Login== and ==Password== setting.
!!Seaside components
@sec:components
As we mentioned in the previous section, Seaside applications are built out of
components. Let's take a closer look at how Seaside works by implementing the
''Hello World'' component.
Every Seaside component should inherit directly or indirectly from
==WAComponent==, as shown in Figure *@fig:WACounter*. (Incidentally, the
'WA' prefix often used in Seaside code stands for 'Web Application'.)
Define a subclass of ==WAComponent== called ==WAHelloWorld==.
Components must know how to render themselves. Usually this is done by
implementing the method ==renderContentOn:==, which gets as its argument an
instance of ==WAHtmlCanvas==, which knows how to render HTML.
Implement the following method, and put it in a protocol called ==rendering==:
[[[
WAHelloWorld >> renderContentOn: html
html text: 'hello world'
]]]
Now we must inform Seaside that this component is willing to be a standalone
application.
Implement the following method on the class side of ==WAHelloWorld==.
[[[
WAHelloWorld class >> canBeRoot
^ true
]]]
We are almost done!
Point your web browser at *http://localhost:8080/config*, add a new
entry point called ==hello==, and set its root component to be ==WAHelloWorld==.
Now navigate to *http://localhost:8080/hello*. That's it! You
should see a web page similar to Figure *@fig:WAHelloWorld*.
+''Hello World'' in Seaside.>file://figures/WAHelloWorld.png|label=fig:WAHelloWorld+
!!!State backtracking and the ==Counter== application
The ''counter'' application is only slightly more complex than the ''hello
world'' application.
@sec:backtracking
+The ==WACounter== class, which implements the ''counter'' application. Methods with underlined names are on the class-side; those with plain-text names are on the instance side.>file://figures/WACounter.png|label=fig:WACounter+
The class ==WACounter== is a standalone application, so ==WACounter class== must
answer ==true== to the ==canBeRoot== message. It must also register itself as
an application; this is done in its class-side ==initialize== method, as shown
in Figure *@fig:WACounter*.
==WACounter== defines two methods, ==increase== and ==decrease==, which will be
triggered from the __++__ and __–\–__ links on the web page. It also defines an
instance variable ==count== to record the state of the counter. However, we also
want Seaside to synchronize the counter with the browser page: when the user
clicks on the browser's ''Back'' button, we want seaside to ''backtrack'' the
state of the ==WACounter== object. Seaside includes a general mechanism for
backtracking, but each application has to tell Seaside which parts of its state
to track.
A component enables backtracking by implementing the ==states== method on the
instance side: ==states== should answer an array containing all the objects to
be tracked. In this case, the ==WACounter== object adds itself to Seaside's
table of backtrackable objects by returning ==Array with: self==.
!!!!!Backtracking caveat
There is a subtle but important point to watch for when declaring objects for
backtracking. Seaside tracks state by making a ''copy'' of all the objects
declared in the ==states== array. It does this using a ==WASnapshot== object;
==WASnapshot== is a subclass of ==IdentityDictionary== that records the objects
to be tracked as keys and shallow copies of their state as values. If the state
of an application is backtracked to a particular snapshot, the state of each
object entered into the snapshot dictionary is overwritten by the copy saved in
the snapshot.
Here is the point to watch out for: In the case of ==WACounter==, you might
think that the state to be tracked is a number — the value of the ==count==
instance variable. However, having the ==states== method answer ==Array with:
count== won't work. This is because the object named by ==count== is an integer,
and integers are immutable. The ==increase== and ==decrease== methods don't
change the state of the object ==0== into ==1== or the object ==3== into ==2==.
Instead, they make ==count== hold a different integer: every time the count is
incremented or decremented, the object named by ==count== is ''replaced'' by
another. This is why ==WACounter>>states== must return ==Array with: self==.
When the state of a ==WACounter== object is replaced by a previous state, the
''value'' of each of the instance variable in the object is replaced by a
previous value; this correctly replaces the current value of ==count== by a
prior value.
!!Rendering HTML
The purpose of a web application is to create, or ''render'', web pages. As we
mentioned in Section *@sec:components*, each Seaside component is responsible
for rendering itself. So, let's start our exploration of rendering by seeing how
the counter component renders itself.
!!!Rendering the Counter
The rendering of the counter is relatively straightforward; the code is shown in
Figure *@fig:WACounter*. The current value of the counter is displayed as an
HTML heading, and the increment and decrement operations are implemented as
HTML anchors (that is, links) with callbacks to blocks that will send
==increase== and ==decrease== to the counter object.
We will have a closer look at the rendering protocol in a moment. But before we
do, let's have a quick look at the multi-counter.
!!!From Counter to MultiCounter
==WAMultiCounter==, shown in Figure *@fig:WAMultiCounter* is also a standalone
application, so it overrides ==canBeRoot== to answer ==true==. In addition, it
is a ''composite'' component, so Seaside requires it to declare its children by
implementing a method ==children== that answers an array of all the components
it contains. It renders itself by rendering each of its subcomponents, separated
by a horizontal rule. Aside from instance and class-side initialization methods,
there is nothing else to the multi-counter!
+WAMultiCounter.>file://figures/WAMultiCounter.png|label=fig:WAMultiCounter+
!!!More about rendering HTML
As you can see from these examples, Seaside does not use templates to generate
web pages. Instead it generates HTML programmatically. The basic idea is that
every Seaside component should override the method ==renderContentOn:==; this
message will be sent by the framework to each component that needs to be
rendered. This ==renderContentOn:== message will have an argument that is an
''HTML canvas'' onto which the component should render itself. By convention,
the HTML canvas parameter is called ==html==. An HTML canvas is analogous to the
graphics canvas used by Morphic (and most other drawing frameworks) to abstract
away from the device-dependent details of drawing.
Here are some of the most basic rendering methods:
[[[
html text: 'hello world'. "render a plain text string"
html html: '–'. "render an HTML incantation"
html render: 1. "render any object"
]]]
The message ==render: anyObject== can be sent to an HTML canvas to render
==anyObject==; it is normally used to render subcomponents. ==anyObject== will
itself be sent the message ==renderContentOn:== this is what happens in the
multi-counter (see Figure *@fig:WAMultiCounter*).
!!!Using brushes
@sec:brushes
A canvas provides a number of ''brushes'' that can be used to render (''i.e.'',
''paint'') content on the canvas. There are brushes for every kind of HTML
element — paragraphs, tables, lists, and so on. To see the full protocol of
brushes and convenience methods, you should browse the class ==WACanvas== and
its subclasses. The argument to ==renderContentOn:== is actually an instance of
the subclass ==WARender==.
We have already seen the following brush used in the counter and multi-counter
examples:
[[[
html horizontalRule.
]]]
+RenderingDemo.>file://figures/RenderingDemo.png|label=fig:RenderingDemo+
In Figure *@fig:RenderingDemo* we can see the output of many of the basic
brushes offered by Seaside. (The source code for method
==SeasideDemo >> renderContentOn:== defined below is in the package
==PBE-SeasideDemo== in the project
*http://www.squeaksource.com/PharoByExample*.) The root component
==SeasideDemo== simply renders its subcomponents, which are instances of
==SeasideHtmlDemo==, ==SeasideFormDemo==, ==SeasideEditCallDemo== and
==SeasideDialogDemo==, as shown below.
[[[label=scr:renderdemo
SeasideDemo >> renderContentOn: html
html heading: 'Rendering Demo'.
html heading
level: 2;
with: 'Rendering basic HTML: '.
html div
class: 'subcomponent';
with: htmlDemo.
"render the remaining components ..."
]]]
Recall that a root component must always declare its children, or Seaside will
refuse to render them.
[[[
SeasideDemo >> children
^ { htmlDemo . formDemo . editDemo . dialogDemo }
]]]
Notice that there are two different ways of instantiating the ==heading== brush.
The first way is to set the text directly by sending the message ==heading:==.
The second way is to instantiate the brush by sending ==heading==, and then to
send a cascade of messages to the brush to set its properties and render it.
Many of the available brushes can be used in these two ways.
@@important If you send a cascade of messages to a brush including the message ==with:==, then ==with:== should be the ''final'' message. ==with:== both sets the content and renders the result.
In method ==renderContentOn: == above, the first heading is at level 1, since
this is the default. We explicitly set the level of the second heading to 2. The
subcomponent is rendered as an HTML ''div'' with the CSS class ''subcomponent''.
(More on CSS in Section *@sec:css*.) Also note that the argument to the
==with:== keyword message need not be a literal string: it can be another
component, or even — as in the next example — a block containing further
rendering actions.
The ==SeasideHtmlDemo== component demonstrates many of the most basic brushes.
Most of the code should be self-explanatory.
[[[
SeasideHtmlDemo >> renderContentOn: html
self renderParagraphsOn: html.
self renderListsAndTablesOn: html.
self renderDivsAndSpansOn: html.
self renderLinkWithCallbackOn: html
]]]
It is common practice to break up long rendering methods into many helper
methods, as we have done here.
A word of advice: Don't put all your rendering code into a single method.
Instead, split it into helper methods named using the pattern ==render\*On:==.
All rendering methods go in the ==rendering== protocol. Don't send
==renderContentOn:== from your own code, use ==render:== instead.
Look at the following code. The first helper method,
==SeasideHtmlDemo>>renderParagraphsOn:==, shows you how to generate HTML
paragraphs, plain and emphasized text, and images. Note that in Seaside simple
elements are rendered by specifying the text they contain directly, whereas
complex elements are specified using blocks. This is a simple convention to help
you structure your rendering code.
[[[
SeasideHtmlDemo >> renderParagraphsOn: html
html paragraph: 'A plain text paragraph.'.
html paragraph: [
html
text: 'A paragraph with plain text followed by a line break. ';
break;
emphasis: 'Emphasized text ';
text: 'followed by a horizontal rule.';
horizontalRule;
text: 'An image URI: '.
html image
url: self squeakImageUrl;
width: '50']
]]]
The next helper method, ==SeasideHtmlDemo>>renderListsAndTablesOn:==, shows you
how to generate lists and tables. A table uses two levels of blocks to display
each of its rows and the cells within the rows.
[[[
SeasideHtmlDemo >> renderListsAndTablesOn: html
html orderedList: [
html listItem: 'An ordered list item'].
html unorderedList: [
html listItem: 'An unordered list item'].
html table: [
html tableRow: [
html tableData: 'A table with one data cell.']]
]]]
The next example shows how we can specify ==class== or ==id== attributes for
==div== and ==span== elements (for use with CSS). Of course, the messages
==class:== and ==id:== can also be sent to the other brushes, not just to
==div== and ==span==. The method ==SeasideDemoWidget>>style== defines how
these HTML elements should be displayed (see Section *@sec:css*).
[[[
SeasideHtmlDemo >> renderDivsAndSpansOn: html
html div
id: 'author';
with: [
html text: 'Raw text within a div with id ''author''. '.
html span
class: 'highlight';
with: 'A span with class ''highlight''.']
]]]
Finally we see a simple example of a link, created by binding a simple callback
to an ''anchor'' (''i.e.'', a link). Clicking on the link will cause the
subsequent text to toggle between ==true== and ==false== by toggling the
instance variable ==toggleValue==.
[[[
SeasideHtmlDemo >> renderLinkWithCallbackOn: html
html paragraph: [
html text: 'An anchor with a local action: '.
html span with: [
html anchor
callback: [toggleValue := toggleValue not];
with: 'toggle boolean:'].
html space.
html span
class: 'boolean';
with: toggleValue ]
]]]
Note that actions should appear only in callbacks.
The code executed while rendering should not change the state of the
application!
!!!Forms
Forms are rendered just like the other examples that we have already seen. Here
is the code for the ==SeasideFormDemo== component in Figure *@fig:RenderingDemo*.
[[[
SeasideFormDemo >> renderContentOn: html
| radioGroup |
html heading: heading.
html form: [
html span: 'Heading: '.
html textInput on: #heading of: self.
html select
list: self colors;
on: #color of: self.
radioGroup := html radioGroup.
html text: 'Radio on:'.
radioGroup radioButton
selected: radioOn;
callback: [radioOn := true].
html text: 'off:'.
radioGroup radioButton
selected: radioOn not;
callback: [radioOn := false].
html checkbox on: #checked of: self.
html submitButton
text: 'done' ]
]]]
Since a form is a complex entity, it is rendered using a block. Note that all
the state changes happen in the callbacks, not as part of the rendering.
There is one Seaside feature used here that is worth special mention, namely the
message ==on:of:==. In the example, this message is used to bind a text input
field to the variable ==heading==. Anchors and buttons also support this
message. The first argument is the name of an instance variable for which
accessors have been defined; the second argument is the object to which this
instance variable belongs. Both observer (==heading==) and mutator
(==heading:==) accessor messages must be understood by the object, with the
usual naming convention. In the case of a text input field, this saves us
the trouble of having to define a callback that updates the field as well as
having to bind the default contents of the HTML input field to the current value
of the instance variable. Using ==on: #heading of: self==, the ==heading==
variable is updated automatically whenever the user updates the text input
field.
The same message is used twice more in this example, to cause the selection of a
colour on the HTML form to update the ==color== variable, and to bind the result
of the checkbox to the ==checked== variable. Many other examples can be found in
the functional tests for Seaside. Have a look at the package
==Seaside-Tests-Functional==, or just point your browser to
*http://localhost:8080/tests/functional*. Select ==WAInputPostFunctionalTest==
and click on the ==Restart== button to see most of the features of forms.
Don't forget, if you toggle ==Halos==, you can browse the source code of the
examples directly using the Seaside class browser.
!!CSS: Cascading style sheets
@sec:css
Cascading Style Sheets (*http://www.w3.org/Style/CSS/*), or CSS for short, are a
standard way for web applications to separate style from content. Seaside relies
on CSS to avoid cluttering your rendering code with layout considerations.
You can set the CSS style sheet for your web components by defining the method
==style==, which should return a string containing the CSS rules for that
component. The styles of all the components displayed on a web page are joined
together, so each component can have its own style. A better approach can be to
define an abstract class for your web application that defines a common style
for all its subclasses.
Actually, for deployed applications, it is more common to define style sheets as
external files. This way the look and feel of the component is completely
separate from its functionality. (Have a look at ==WAFileLibrary==, which
provides a way to serve static files without the need for a standalone server.)
If you already are familiar with CSS, then that's all you need to know.
Otherwise, read on for a very brief introduction to CSS.
Instead of directly encoding display attributes in the paragraph and text
elements of your web pages, CSS lets you define different ''classes'' of
elements, and place all display considerations in a separate style sheet.
To put it another way, a CSS style sheet consists of a set of rules that specify
how to format given HTML elements. Each rule consists of two parts. There is a
''selector'' that specifies which HTML elements the rule applies to, and there
is a ''declaration'' which sets a number of attributes for that element.
[[[label=scr:democss|caption===SeasideDemoWidget== common style sheet.
SeasideDemoWidget >> style
^ '
body {
font: 10pt Arial, Helvetica, sans-serif, Times New Roman;
}
h2 {
font-size: 12pt;
font-weight: normal;
font-style: italic;
}
table { border-collapse: collapse; }
td {
border: 2px solid #CCCCCC;
padding: 4px;
}
#author {
border: 1px solid black;
padding: 2px;
margin: 2px;
}
.subcomponent {
border: 2px solid lightblue;
padding: 2px;
margin: 2px;
}
.highlight { background-color: yellow; }
.boolean { background-color: lightgrey; }
.field { background-color: lightgrey; }
'
]]]
The previous method illustrates a simple style sheet for the rendering demo
shown earlier in Figure *@fig:RenderingDemo*. The first rule specifies a
preference for the fonts to use for the ==body== of the web page. The next few
rules specify properties of second-level headings (==h2==), tables (==table==),
and table data (==td==).
The remaining rules have selectors that will match HTML elements that have the
given ==class== or ==id== attributes. CSS selectors for class attributes start
with a ==.== and those for id attributes with ==#==. The main difference
between class and id attributes is that many elements may have the same class,
but only one element may have a given id (i.e., an ''identifier''). So,
whereas a class attribute, such as ==highlight==, may occur multiple times on
any page, an id must identify a ''unique'' element on the page, such as a
particular menu, the modified date, or author. Note that a particular HTML
element may have multiple classes, in which case all the applicable display
attributes will be applied in sequence.
Selector conditions may be combined, so the selector ==div.subcomponent== will
only match an HTML element if it is both a ==div== ''and'' it has a class
attribute equal to ==subcomponent==.
It is also possible to specify nested elements, though this is seldom necessary.
For example, the selector ==p span== will match a span within a paragraph
but not within a div.
There are numerous books and web sites to help you learn CSS. For a dramatic
demonstration of the power of CSS, we recommend you to have a look at the CSS
Zen Garden (*http://www.csszengarden.com/*), which shows how the same content
can be rendered in radically different ways simply by changing the CSS style
sheet.
!!Managing control flow
Seaside makes it particularly easy to design web applications with non-trivial
control flow. There are basically two mechanisms that you can use:
""1."" A component can ''call'' another component by sending ==caller call:
callee==. The caller is temporarily replaced by the callee, until the callee
returns control by sending ==answer:==. The caller is usually ==self==, but
could also be any other currently visible component.
""2."" A workflow can be defined as a ''task''. This is a special kind of
component that subclasses ==WATask== (instead of ==WAComponent==).
Instead of defining ==renderContentOn:==, it defines no content of its own, but
rather defines a ==go== method that sends a series of ==call:== messages to
activate various subcomponents in turn.
!!!Call and answer
Call and answer are used to realize simple dialogues.
There is a trivial example of ==call:== and ==answer:== in the rendering demo of
Figure *@fig:RenderingDemo*. The component ==SeasideEditCallDemo== displays a
text field and an ==edit== link. The callback for the edit link calls a new
instance of ==SeasideEditAnswerDemo== initialized to the value of the text
field. The callback also updates this text field to the result which is sent as
an answer.
(We underline the ==call:== and ==answer:== sends to draw attention to them.)
[[[
SeasideEditCallDemo >> renderContentOn: html
html span
class: 'field';
with: self text.
html space.
html anchor
callback: [self text: (self call: (SeasideEditAnswerDemo new text: self text))];
with: 'edit'
]]]
What is particularly elegant is that the code makes absolutely no reference to
the new web page that must be created. At run-time, a new page is created in
which the ==SeasideEditCallDemo== component is replaced by a
==SeasideEditAnswerDemo== component; the parent component and the other peer
components are untouched.
It is important to keep in mind that ==call:== and ==answer:== should never be
used while rendering. They may safely be sent from within a callback, or from
within the ==go== method of a task.
The ==SeasideEditAnswerDemo== component is also remarkably simple. It just
renders a form with a text field. The submit button is bound to a callback that
will answer the final value of the text field.
[[[
SeasideEditAnswerDemo >> renderContentOn: html
html form: [
html textInput
on: #text of: self.
html submitButton
callback: [ self answer: self text ];
text: 'ok'.
]
]]]
That's it.
Seaside takes care of the control flow and the correct rendering of all the
components. Interestingly, the ''Back'' button of the browser will also work
just fine (though side effects are not rolled back unless we take additional
steps).
!!!Convenience methods
Since certain call–answer dialogues are very common, Seaside provides some
convenience methods to save you the trouble of writing components like
==SeasideEditAnswerDemo==. The generated dialogues are shown in Figure
*@fig:dialogs*. We can see these convenience methods being used within
==SeasideDialogDemo>>renderContentOn:==
+Some standard dialogs.>file://figures/dialogs-flat.png|label=fig:dialogs+
The message ==request:== performs a call to a component that will let you edit a
text field. The component answers the edited string. An optional label and
default value may also be specified.
[[[
SeasideDialogDemo >> renderContentOn: html
html anchor
callback: [ self request: 'edit this' label: 'done' default: 'some text' ];
with: 'self request:'.
...
]]]
The message ==inform:== calls a component that simply displays the argument
message and waits for the user to click ==Ok==. The called component just
returns ==self==.
[[[
...
html space.
html anchor
callback: [ self inform: 'yes!' ];
with: 'self inform:'.
...
]]]
The message ==confirm:== asks a question and waits for the user to select
either ==Yes== or ==No==. The component answers a boolean, which can be used to
perform further actions.
[[[
...
html space.
html anchor
callback: [
(self confirm: 'Are you happy?')
ifTrue: [ self inform: ':-)' ]
ifFalse: [ self inform: ':-(' ]
];
with: 'self confirm:'.
]]]
A few further convenience methods, such as ==chooseFrom:caption:==, are defined
in the ==convenience== protocol of ==WAComponent==.
!!!Tasks
@sec:tasks
A task is a component that subclasses ==WATask==. It does not render anything
itself, but simply calls other components in a control flow defined by
implementing the method ==go==.
==WAConvenienceTest== is a simple example of a task defined in the package
==Seaside-Tests-Functional==. To see its effect, just point your browser to
*http://localhost:8080/tests/functional*, select
==WAFlowConvenienceFunctionalTest== and click ==Restart==.
[[[
WAFlowConvenienceFunctionalTest >> go
[ self chooseCheese.
self confirmCheese ] whileFalse.
self informCheese
]]]
This task calls in turn three components. The first, generated by the
convenience method ==chooseFrom: caption:==, is a ==WAChoiceDialog== that asks
the user to choose a cheese.
[[[
WAFlowConvenienceFunctionalTest >> chooseCheese
cheese := self
chooseFrom: #('Greyerzer' 'Tilsiter' 'Sbrinz')
caption: 'What''s your favorite Cheese?'.
cheese isNil ifTrue: [ self chooseCheese ]
]]]
The second is a ==WAYesOrNoDialog== to confirm the choice (generated by the
convenience method ==confirm:==).
[[[
WAFlowConvenienceFunctionalTest >> confirmCheese
^self confirm: 'Is ', cheese, ' your favorite cheese?'
]]]
Finally a ==WAFormDialog== is called (via the convenience method ==inform:==).
[[[
WAFlowConvenienceFunctionalTest >> informCheese
self inform: 'Your favorite cheese is ', cheese, '.'
]]]
The generated dialogues are shown in Figure *@fig:chooseCheese*.
+A simple task.>file://figures/chooseCheese-flat.png|label=fig:chooseCheese+
!!!Transactions
We saw in Section *@sec:backtracking* that Seaside can keep track of the
correspondence between the state of components and individual web pages by
having components register their state for backtracking: all that a component
need do is implement the method ==states== to answer an array of all the objects
whose state must be tracked.
Sometimes, however, we do not want to backtrack state: instead we want to
''prevent'' the user from accidentally undoing effects that should be permanent.
This is often referred to as "the shopping cart problem". Once you have
checked out your shopping cart and paid for the items you have purchased, it
should not be possible to go ''Back'' with the browser and add more items to the
shopping cart!
Seaside allows you to enforce restriction this by defining a task within which
certain actions are grouped together as ''transactions''. You can backtrack
within a transaction, but once a transaction is complete, you can no longer go
back to it. The corresponding pages are ''invalidated'', and any attempt to go
back to them will cause Seaside to generate a warning and redirect the user to
the most recent valid page.
+The Sushi Store.>file://figures/sushiStore|label=fig:sushiStore+
The Seaside ''Sushi Store'' is sample application that illustrates many of the
features of Seaside, including transactions. This application is bundled with
your installation of Seaside, so you can try it out by pointing your browser at
*http://localhost:8080/seaside/examples/store*. (If you cannot find it in your
image, there is a version of the sushi store available on SqueakSource from
*http://www.squeaksource.com/SeasideExamples/*.)
The sushi store supports the following workflow:
-Visit the store.
-Browse or search for sushi.
-Add sushi to your shopping cart.
-Checkout.
-Verify your order.
-Enter shipping address.
-Verify shipping address.
-Enter payment information.
-Your fish is on its way!
If you toggle the halos, you will see that the top-level component of the sushi
store is an instance of ==WAStore==. It does nothing but render the title bar,
and then it renders ==task==, an instance of ==WAStoreTask==.
[[[
WAStore >> renderContentOn: html
"... render the title bar ..."
html div id: 'body'; with: task
]]]
==WAStoreTask== captures this workflow sequence. At a couple of points it is
critical that the user not be able to go back and change the submitted
information.
Purchase some sushi and then use the ''Back'' button to try to put more sushi
into your cart.
You will get the message ''That page has expired.''
Seaside lets the programmer say that a certain part of a workflow acts like a
transaction: once the transaction is complete, the user cannot go back and undo
it. This is done by sending ==isolate:== to a task with the transactional block
as its argument. We can see this in the sushi store workflow as follows:
[[[
WAStoreTask >> go
| shipping billing creditCard |
cart := WAStoreCart new.
self isolate:
[[ self fillCart.
self confirmContentsOfCart ]
whileFalse ].
self isolate:
[ shipping := self getShippingAddress.
billing := (self useAsBillingAddress: shipping)
ifFalse: [ self getBillingAddress ]
ifTrue: [ shipping ].
creditCard := self getPaymentInfo.
self shipTo: shipping billTo: billing payWith: creditCard ].
self displayConfirmation.
]]]
Here we see quite clearly that there are two transactions. The first fills the
cart and closes the shopping phase. (The helper methods such as ==fillCart==
take care of instantiating and calling the right subcomponents.) Once you have
confirmed the contents of the cart you cannot go back without starting a new
session. The second transaction completes the shipping and payment data. You can
navigate back and forth within the second transaction until you confirm payment.
However, once both transactions are complete, any attempt to navigate back will
fail.
Transactions may also be nested. A simple demonstration of this is found in the