Location: PHPKode > scripts > B-Forms > b-forms/b-forms-manual.html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
	<META HTTP-EQUIV="CONTENT-TYPE" CONTENT="text/html; charset=windows-1251">
	<TITLE>B-Forms Manual</TITLE>
	<META NAME="GENERATOR" CONTENT="OpenOffice.org 1.1.2  (Win32)">
	<META NAME="CREATED" CONTENT="20040913;18183102">
	<META NAME="CHANGEDBY" CONTENT="Alexei Peterkin">
	<META NAME="CHANGED" CONTENT="20041209;10560700">
	<STYLE>
	<!--
		@page { size: 21cm 29.7cm }
	-->
	</STYLE>
</HEAD>
<BODY LANG="en-US" DIR="LTR">
<H1>B-Forms</H1>
<H2><META HTTP-EQUIV="Content-Language" CONTENT="en-us">Introduction 
</H2>
<P>I have designed B-Forms as a library for quick development of web
based applications. It is not very well suited for high-traffic sites
since it is not very fast. (At least I think so, but I have not
really tested it). But for sites with a reasonably small number of
users it should work well.</P>
<P>The concept behind the package is &quot;creatively&quot; copied
from Oracle Forms, simplifying the original idea to the limitations
of web-based operation and skipping, at least for now base-table
blocks. Maybe I will add them in the future.</P>
<P>Creating a form consists of the following steps:</P>
<UL>
	<LI><P STYLE="margin-bottom: 0cm">Define the structure of your <I>form</I>,
	which consists of <I>blocks</I> (almost the same as database tables,
	although currently there is no database behind them) and fields (or
	<I>properties</I>), that are part of blocks. Buttons are also
	treated as fields belonging to blocks, although, for obvious reasons
	they do not have values.</P>
	<LI><P STYLE="margin-bottom: 0cm">Once you have defined the
	structure of the form, write code to display the form. You have two
	options when displaying the form:</P>
	<UL>
		<LI><P STYLE="margin-bottom: 0cm">use a standard form layout
		provided with the library starting with version 1.1 &ndash;
		suitable for most web-applications,</P>
		<LI><P STYLE="margin-bottom: 0cm">or write your own HTML code for
		the form, where instead of using the &lt;input&gt; elements, you
		call special functions that will generate the necessary inputs for
		you. 
		</P>
	</UL>
	<LI><P STYLE="margin-bottom: 0cm">At this point the form can display
	empty inputs and buttons, but cannot do anything yet. For it to be
	useful, you need to define triggers.</P>
	<P STYLE="margin-bottom: 0cm">Triggers are called at predefined
	moments of the form processing, and allow you to fully control the
	operations of the form, sometimes augmenting, or sometimes
	completely replacing the default behavior. Triggers are simple
	programmer-defined PHP-functions however with strictly defined
	names.</P>
	<LI><P>When inside a trigger, you can call predefined functions of
	B-Forms (or your own functions that call predefined functions).
	Calling predefined functions may in turn, fire more triggers. For
	example, if you call the validate() method on the form, it fires
	validation triggers. Calling save() method triggers both the
	validation and saving triggers.</P>
</UL>
<P>When making my design decisions, I followed the following
principles:</P>
<UL>
	<LI><P STYLE="margin-bottom: 0cm">Trying to be as simple as possible
	&ndash; implementing as little as possible to get it working. As a
	result there are much fewer triggers than in the original concept,
	and I haven't done base table blocks (blocks that automatically
	operate on rows of the associated table).</P>
	<LI><P STYLE="margin-bottom: 0cm">Most objects in most subject
	domains can be identified by a single primary ID. Thus, unlike the
	Oracle Forms, I have built in the ID field in all blocks. Usually
	this field corresponds to a numeric id that is the primary key of
	the corresponding database table. (Of course, base table blocks in
	oracle have built-in row id, but that is a different thing).</P>
</UL>
<P STYLE="margin-bottom: 0cm"><BR>
</P>
<P STYLE="margin-bottom: 0cm">The benefits are:</P>
<P STYLE="margin-bottom: 0cm"><BR>
</P>
<UL>
	<LI><P STYLE="margin-bottom: 0cm">Depending on whether the form is
	opened the first time or is restored after a submit, different
	triggers are called so that you do not have to worry about it.</P>
	<LI><P STYLE="margin-bottom: 0cm">When processing submitted form,
	the data is automatically restored for you into the form object
	structure.</P>
	<LI><P STYLE="margin-bottom: 0cm">All hidden fields in the form are
	digitally signed to prevent tampering with them. When the data of
	the form is restored after the submit, the electronic signature is
	validated.</P>
	<LI><P STYLE="margin-bottom: 0cm">You can easily handle multi-record
	blocks, where you have more then one record of the same type. And
	you want to edit them all at the same time.</P>
	<LI><P STYLE="margin-bottom: 0cm">If using auto-detect-changes mode
	(although it can be quite heavy on the size of your form, since it
	has to contain all the original values as hidden fields), the
	library will only call saving triggers for those records that have
	been changed by the user.</P>
</UL>
<P STYLE="margin-bottom: 0cm"><BR>
</P>
<P>Currently, the package has limitations, which are:</P>
<UL>
	<LI><P STYLE="margin-bottom: 0cm">I have developed it using PHP
	4.3.0, I have seen it work on PHP 4.3.6 and 4.3.8. But I do not know
	if it runs on older versions. I suspect that it does not work with
	PHP5 &ndash; I will probably have to do a special version.</P>
	<LI><P STYLE="margin-bottom: 0cm">Prior to version 1.1 the library
	required <I>magic_quotes_gpc</I> PHP setting to be Off. As of 1.1 it
	is no longer required. B-Forms works well with register_globals set
	to Off, but I have not tested how it would behave if it was set to
	On.</P>
	<LI><P STYLE="margin-bottom: 0cm">There must be only one form on a
	page. The form itself can be quite complicated and consist of
	several parts, but there should be only one.</P>
	<LI><P>The package relies on http redirect for navigation after form
	processing. That is, when a user submits a form, the form is
	processed by the same URL that displayed it, and in case the
	processing is successful, redirects the browser to a new location
	that will display the next page. This makes the use of [Back] button
	difficult, however allows to do [Refresh] of the results easily,
	since the browser won't ask you if you want to post the data again. 
	</P>
</UL>
<P>If you want to participate in the development (by adding new
functionality to existing classes, by adding new classes, by
improving this manual), or just want a feature that may be useful to
other people, please do not hesitate to contact me. I will also be
glad to answer questions &ndash; questions will help me build a FAQ
and improve this manual. You can contact me at <I>alpeter at users
dot sourceforge dot net</I>.</P>
<H2>Tutorial</H2>
<P>Throughout the tutorial we will part of a simple blog application
&ndash; only those parts that require forms.</P>
<H3>Creating a simple form</H3>
<P>Let us start with a very simple form: this form allows us to
create or edit topics in the blog. This page should be called with
one parameter: <I>topic</I> specifying which topic is to be edited.
If the parameter is not specified, the page will create a new topic.</P>
<P>The structure of the page looks like this:</P>
<PRE>&lt;?
require_once(&quot;b-forms/b-forms.inc&quot;); // Or wherever you have installed it
require_once(&quot;b-forms/layout.inc&quot;);  // Optional, only if you want to use
                                     // the standard form layout

// Define form structure
$form = new Form(&quot;denied.html&quot;);  // Or whatever your tempering-detected-access-denied-page is
........

// Define triggers
........

$form-&gt;process();

// Generate HTML-view of the form.
........

?&gt;</PRE><P>
The call to $form-&gt;process() does all the dirty work of
determining whether the form is opened for the first time or should
be restored, and fires all the required triggers.</P>
<H4>Defining form structure</H4>
<P>But back to the beginning &ndash; let us define the structure of
the form. 
</P>
<P>Our topics will be extremely simple objects &ndash; they will only
consist of numeric IDs, and names. The following code defines the
form:</P>
<PRE>..............
// Define the form structure

$form = new Form(&quot;denied.html&quot;);

$block = &amp; new Block(&quot;topic&quot;);
$block-&gt;add_property(new TextProperty(&quot;name&quot;, &quot;Name&quot;, &quot;&quot;, TRUE, 64));
$block -&gt; add_property(new ButtonProperty(&quot;save&quot;, &quot;Save&quot;, TRUE));
$block -&gt; add_property(new ButtonProperty(&quot;delete&quot;, &quot;Delete&quot;));
$block -&gt; add_property(new ButtonProperty(&quot;cancel&quot;, &quot;Cancel&quot;));

$form -&gt; add_block($block);
...............</PRE><P>
The first thing is to create a Form object. The constructor takes two
parameters: the location of the page to which the use will be
redirected if the form detects tempering with its data (if the
electronic signature will be broken), and an optional parameter
$auto_detect_changes. The auto-detect-changes functionality was added
in version 1.1. To maintain backwards compatibility by default this
parameter is FALSE, so that scripts using previous version of the
software do not break. In this example we will not use
auto-detection.</P>
<P>The next step is creating a block. Please note the usage of the
reference operator (&amp;). Because of the way PHP handles objects by
always copying them instead of referencing, B-Forms will not work
without this &amp;. Creating the block is simple &ndash; the
constructor only takes block name. 
</P>
<P>Then we create the name property (the <I>id</I> property is
created automatically). This is done by calling method <I>add_property</I>
on the block. This method takes two parameters: the property object,
and the display object that will be used to display the property. The
second parameter is optional &ndash; if you don't specify it, the
default display will be used for this property. Default display
depends on the type of the property, and in case of TextProperty it
is TextBox, with the size of the TextBox equal to the length of the
TextProperty &ndash; exactly what we need here.</P>
<P STYLE="font-style: normal">The TextProperty constructor takes 5
parameters:</P>
<UL>
	<LI><P STYLE="font-style: normal">$name: the name of the property</P>
	<LI><P STYLE="font-style: normal">$label: the label for this
	property which can be optionally used when generating the form, and
	which will be used when generating error message for missing
	required fields;</P>
	<LI><P STYLE="font-style: normal">$default_value: if the form is
	used to create a new object, the property will take on this default
	value.</P>
	<LI><P STYLE="font-style: normal">$required: TRUE or FALSE,
	depending whether this field is mandatory or optional.</P>
	<LI><P STYLE="font-style: normal">$size: the max length of this
	field &ndash; will appear as MAXLEN attribute of the INPUT tag.</P>
</UL>
<P STYLE="font-style: normal">The TextBox display constructor takes
two optional parameters:</P>
<UL>
	<LI><P STYLE="font-style: normal">$size: the displayed length of the
	field (the SIZE attribute of the INPUT tag).</P>
	<LI><P STYLE="font-style: normal">$extras: any text that you may
	want inserted inside the tag. It can be something like
	<I>class=&rdquo;dataentry&rdquo;</I> or <I>onChange=&rdquo;onChange();&rdquo;</I>.</P>
</UL>
<P STYLE="font-style: normal">Buttons are added using the same
method, an again we do not specify the display &ndash; there is only
one kind of display for them, and it is the default. The only time
when you need to specify a display for a button is when you want to
specify $extras, which carry the same meaning as in the TextBox
display..</P>
<P STYLE="font-style: normal">The constructor for ButtonProperty
takes 3 parameters:</P>
<UL>
	<LI><P STYLE="font-style: normal">$name: how the button will
	identified internally</P>
	<LI><P STYLE="font-style: normal">$label: what the user will see on
	the button</P>
	<LI><P STYLE="font-style: normal">$default: this is an optional
	parameter, by default set to FALSE. If TRUE, this button becomes the
	default action on the form &ndash; submitting the form by pressing
	the Enter key without clicking on any buttons will trigger this
	button. If you by mistake define more than one default button, the
	first one will fire.</P>
</UL>
<P STYLE="font-style: normal"><B>Important: </B>block and property
names should not start with and underscore (_). If you do use names
that start with an underscore you may overwrite internal variables of
the block.</P>
<H4>Generating HTML-view of the form</H4>
<P>Now, let's make sure that the form shows something in the browser
by adding the following code at the end. I will oversimplify the HTML
to keep accent on the form. 
</P>
<PRE>........
echo &quot;&lt;html&gt;&lt;body&gt;\n&quot;;
echo &quot;&lt;h1&gt;&quot;.($form-&gt;topic-&gt;is_record_existing()?&quot;Edit&quot;:&quot;Create&quot;).
     &quot; topic&lt;/h1&gt;\n&quot;;
if (isset($error))
   echo &quot;&lt;h2&gt;$error&lt;/h2&gt;&quot;;

$form-&gt;start_form();

label(&quot;topic&quot;,&quot;name&quot;);
echo &quot;: &quot;;
field(&quot;topic&quot;,&quot;name&quot;);

echo &quot;&lt;br/&gt;\n&quot;;
field(&quot;topic&quot;,&quot;save&quot;);
if ($form-&gt;topic-&gt;is_record_existing())
   field(&quot;topic&quot;,&quot;delete&quot;);
field(&quot;topic&quot;, &quot;cancel&quot;);

$form-&gt;end_form();
echo &quot;&lt;/body&gt;&lt;/html&gt;\n&quot;;
............</PRE><P>
Although it looks a bit messy, the structure of this code is very
simple. First, we output the title of the form. Here we use the
following construct:</P>
<PRE STYLE="margin-bottom: 0.5cm">($form-&gt;topic-&gt;is_record_existing()?&quot;Edit&quot;:&quot;Create&quot;)</PRE><P>
As you have noticed, to refer to a block within a form we just use
its name after the -&gt; operator. Our form has one block <I>topic</I>
which by default has one record. Records can be in several
alternative states, two of which are of importance to us right now:</P>
<UL>
	<LI><P>RS_NEW &ndash; a record that previously did not exist in the
	database. When in auto-detect-changes mode, status RS_NEW means not
	only that then record was not originally in the database, but also
	that the user did not make any changes to this new record &ndash;
	basically no saving would be required. In our example status of
	RS_NEW means that the form is opened for creation of a new record.</P>
	<LI><P>RS_OLD &ndash; this record has existed before and is opened
	for editing. When in auto-detect-changes mode, status RS_OLD means
	that the record has existed before and that the user has not changed
	any data in this record.</P>
</UL>
<P>The <I>is_record_existing()</I> call returns TRUE if the record
status is RS_OLD, and FALSE if it is RS_NEW. Instead of calling this
method, you can call <I>get_record_status()</I> method and compare
the values yourself. However, this will not work for forms running in
auto-detect-changes mode. <I>Is_record_existing() </I>will.</P>
<P>By default the block creates all records as new, unless you
specifically tell it to create an RS_OLD record, when you are about
to fill it with the data from the database. If I ever get to
implement base-table blocks, these statuses will be set
automatically, along with automatic loading of the data.</P>
<P STYLE="font-style: normal">Second, we check if there is an error
and display it. $error is a global variable that should contain the
error that was encountered during form validation. When the form is
displayed the first time, it will obviously be empty. If however the
user has tried to save a topic without the name, this code will be
executed again, but the $error variable will contain text, explaining
that the name field should be filled.</P>
<P STYLE="font-style: normal">Third, we start the form by calling
$form-&gt;start_form(). This call does three things: creates the FORM
tag, fires the pre-display processing (which we will discuss later)
and sets the global variable $_form that will be later used by the
<I>field()</I> and <I>label()</I> calls. Functions field() and
label() are created as typing shortcuts to save on typing
$form-&gt;field and $form-&gt;label every time. Please make sure you
do not overwrite $_form variable, or you'll screw your form
generation.</P>
<P STYLE="font-style: normal">Generating labels and fields requires a
call to the corresponding function with two parameters: the name of
the block, and the name of the field. You do not need to manually
generate hidden fields. The <I>label()</I> call also surrounds your
label with &lt;label&gt;&lt;/label&gt; tag, so that clicking the
label will activate the corresponding field (of course if the browser
supports it).</P>
<P STYLE="font-style: normal">Note, that we only generate the delete
button if the record is in status RS_OLD.</P>
<P STYLE="font-style: normal">And in the end, we call
$form-&gt;end_form(), which does three things: generates all hidden
fields and the electronic signature, closes the form and unsets the
$_form variable.</P>
<H4>Using standard layouts to generate forms</H4>
<P>Now that we have seen how you can manually generate the form
layout, we will from now on abandon manual generation. It is
difficult, takes a lot of time, and is only worth it when the layout
you are planning to produce has to be really irregular. For the rest
of this tutorial we will use standard layouts.</P>
<P>To generate a form using a standard layout, we first need include
b-forms/layout.inc file, and the define a layout. I usually define
all my layouts right when I define the form &ndash; later you will
see why, although in this example it is not very important:</P>
<PRE>require_once(&quot;b-forms/layout.inc&quot;); 

.............
$form = new Form(&quot;denied.html&quot;);
$bl = new BaseLayout(); // Create a default single-row layout
.............</PRE><P>
Also we now need to include a link to a stylesheet that will assist
our standard layout:</P>
<PRE STYLE="margin-bottom: 0.5cm">echo &quot;&lt;link rel=\&quot;stylesheet\&quot; media=\&quot;screen, projection\&quot; type=\&quot;text/css\&quot; href=\&quot;layout.css\&quot; /&gt;\n&quot;;</PRE><P>
And then our-form generation code (between the calls to
$form-&gt;start_form() and $form-&gt;end_form()) will be look like
this:</P>
<PRE STYLE="margin-bottom: 0.5cm">$bl-&gt;show_block(&quot;topic&quot;);</PRE><P>
Yes, that's right &ndash; only one line. The BaseLayout class will
show all visible entry fields with their labels one per line, and all
the buttons at the bottom. The entry fields (and later &ndash;
buttons) are displayed in the order you define them using the
$block-&gt;add_property() call. You can change the appearance of the
forms, generate by BaseLayout by means of either CSS stylesheet, or
the BaseLayoutConfig class, where you can tune up the HTML that the
BaseLayout will generate. More details on this in the reference
section.</P>
<P>But at this point, the new code will not do exactly the same
thing: the delete button will always be there, since standard layouts
by themselves cannot possibly know when to hide it. For that we will
need triggers, and that is our next subject.</P>
<H4 STYLE="font-style: normal">Adding triggers</H4>
<P STYLE="font-style: normal">In order for this form to become more
useful than just a web-page that does not do anything, we will now
add triggers. The first trigger we need is the trigger that will load
the data when the form is opened:</P>
<PRE>function form_on_open() {
   global $form, $blog_link, $HTTP_GET_VARS;

   if (isset($HTTP_GET_VARS[&quot;topic&quot;])) {

      $query = sprintf(&quot;SELECT name FROM topics WHERE id = %d&quot;,$HTTP_GET_VARS[&quot;topic&quot;]);
      $result = mysql_query($query, $blog_link);

      $num_rows = mysql_num_rows($result);

      if ($num_rows &gt; 0) {
         $row = mysql_fetch_row($result);

         $form-&gt;topic-&gt;append(RS_OLD);

         $form-&gt;topic-&gt;id = $HTTP_GET_VARS[&quot;topic&quot;];
         $form-&gt;topic-&gt;name = $row[0];
      }
   }
}</PRE><P>
The function implementing this trigger should always be called
form_on_open(). Here I use a global variable $blog_link that holds an
active link to mysql database containing our blog, defined outside
the scope of this example in an include file.</P>
<P STYLE="font-style: normal">If there is no <I>topic</I> parameter
in the request, then we really do not have anything to do. But if the
topic has been specified, we load the data from the database and set
the fields to the correct values. The first thing we do, is add a
record to the block by calling $form-&gt;topic-&gt;append(RS_OLD).
The created record is automatically set to status RS_OLD, which
signifies the fact the the object existed in the database before we
opened the form (so that saving will require an UPDATE rather then an
INSERT).</P>
<P>All blocks start out without any records. Records must be appended
before you start data operation on the block. You can append records
manually by calling $block-&gt;append(). It takes one optional
parameter: the status with which the new record will be created,
default value &ndash; RS_AUTO &ndash; will create records in status
RS_NEW for multi-row blocks or if the form is not in
auto-detect-changes mode. For single row blocks in
auto-detect-changes mode it will create a record in status RS_INSERT,
but that, together with multi-row blocks is the subject of our next
example. In this example, append() will create a record instatus
RS_NEW.</P>
<P>However, we do not call $form-&gt;topic-&gt;append(RS_NEW)
explicitly here, since we don't have to do any special initialization
for the new record. A new record will be automatically appended after
all ON_OPEN triggers have completed their job. If we did have some
initialization to do, we would have to call $block-&gt;append(RS_NEW)
manually to create this record, or rely on the ON_APPEND trigger
mechanism that will be discussed later.</P>
<P STYLE="font-style: normal">Notice, that you refer to the values of
fields as $form-&gt;block_name-&gt;field_name, making it very simple
to deal with the fields.</P>
<P STYLE="font-style: normal">Now let's add a trigger to handle the
cancel button:</P>
<PRE>function topic_cancel_on_action($rownum = -1) {
   close_db(); // Or some other function that will release resources

   header(&quot;Location: /examples/&quot;);
   exit;
}</PRE><P>
There are several rules to keep in mind when writing action triggers:</P>
<UL>
	<LI><P>Functions should be called
	&lt;block_name&gt;_&lt;button_name&gt;_on_action, like
	topic_cancel_on_action in our example.</P>
	<LI><P>Function should take one optional parameter ($rownum = -1).
	Right now we do not need it, but this is needed for multi-row
	blocks, where you also need to know on which row the button was
	pressed.</P>
	<LI><P>If the action processing is successful, the trigger should
	not return. Instead, it should transmit the new location and exit.
	If you need to close some resources before exiting, this is a good
	place to do it, and this is why I call <I>close_db()</I> function &ndash;
	I always prefer explicit connection closing.</P>
	<LI><P>If the action trigger function returns, the form will be
	displayed again. In order to make sure that the user understands why
	he is seeing the form again, there should be some visible result,
	even if in the form of an error message, stored in the global $error
	variable.</P>
</UL>
<P>Since processing of a cancel button is always successful, the code
above is the simplest action trigger you are ever gonna get. Now
let's add the saving and deleting triggers:</P>
<PRE>function topic_save_on_action($rownum = -1) {
    global $blog_link, $form;

    if ($form-&gt;validate()) {
       if ($form-&gt;topic-&gt;get_record_status() == RS_OLD) {

          $query = sprintf(
             &quot;UPDATE topics &quot;.
             &quot;SET    name = '%s' &quot;.
             &quot;WHERE  id = %d&quot;,
             mysql_escape_string($form-&gt;topic-&gt;name),
             $form-&gt;topic-&gt;id);

        }
        else {
           $query = sprintf(&quot;INSERT INTO topics (name) VALUES ('%s')&quot;,
                            mysql_escape_string($form-&gt;topic-&gt;name));
        }

        @mysql_query($query, $blog_link);
        
        // Check if the query executed successfully.
        if (mysql_errno()) {
           $error = mysql_error();
           return;
        }

        close_db();
        header(&quot;Location: /examples/&quot;);
        exit;
    }
}

function topic_delete_on_action($rownum = -1) {
   global $blog_link, $form;

   $query = sprintf(&quot;DELETE FROM topics WHERE id=%d&quot;, $form-&gt;topic-&gt;id);
   mysql_query($query, $blog_link);

   // Check if the query executed successfully.
   if (mysql_errno()) {
      $error = mysql_error();
      return;
   }

   close_db();
   header(&quot;Location: /examples/&quot;);
   exit;
}</PRE><P>
In the <I>topic_save_on_action()</I> trigger, we encounter the usage
of built-in validation. Since we have not defined any validation
triggers ourselves, it will run the standard routine of checking for
presence of all mandatory fields. It will return TRUE if the
validation was successful. If not, the $error variable will contain
an error message with the name of the first missing field found.</P>
<P>If the form validated successfully, we check the status of the
record. If it is RS_OLD, then we run an update statement, if it is
RS_NEW we do an insert. After that we do the usual trigger exit
routine. 
</P>
<P>If validation failed, the trigger does nothing and returns,
resulting in the form being displayed again, but with an error
message.</P>
<P>There is only one trigger remaining: the trigger that will hide
the delete button if the record we display is RS_NEW and therefore
cannot be deleted.</P>
<PRE>function form_pre_display() {
   global $form;
   if (!$form-&gt;topic-&gt;is_record_existing())
      $form-&gt;topic-&gt;_properties[&quot;delete&quot;]-&gt;visible = FALSE;
}</PRE><P>
PRE_DISPLAY triggers are fired when you call $form-&gt;start_form()
function. At this point all the form data initialization should have
completed, but the form itself has not yet been displayed. This is
the best time to tune the appearance of our form. In our case, if the
record is new, then we set the visible attribute of the delete button
property to FALSE.</P>
<P>Please note, that in order to access properties as objects (and
not as values) you have to use the following construct:</P>
<PRE STYLE="margin-bottom: 0.5cm">$form-&gt;block_name-&gt;_properties[&quot;field_name&quot;]-&gt;...</PRE><P>
Well &ndash; this is it &ndash; we have got a fully functional form.
You can find the full text of the example in the <I>examples</I>
directory of the library distribution archive. It will contain calls
to functions init_db() and close_db() that are necessary for testing
these examples, but they are irrelevant for the tutorial purposes.</P>
<H3>Multi-row forms</H3>
<P>The idea behind multi-row forms is that you want to edit several
objects of the same type at the same time. For example, instead of
editing one topic at a time, we could have loaded all topics into one
form, and then save the changes to all of them at the same time.
However, this is not generally accepted in the web world because HTTP
protocol is stateless, and it is not easy to implement locking of the
records that you have opened and/or started editing. As a result
collisions between different users are quite possible. If you edit
only one row at a time &ndash; the chance of collision is much lower.
</P>
<P>However, multi-row blocks are very useful when you edit compound
objects. To continue our example we will create a form that allows
creation/editing of entries for our blog. To make them compound, we
will allow one entry to be posted to several topics at the same time.</P>
<H4>Defining form structure</H4>
<P>First let's define the form structure. It will now require two
blocks and two layouts to display them. We will also from now on
start using the auto-detect-changes mode.</P>
<PRE>// Prepare the form
$form = new Form(&quot;denied.html&quot;, TRUE);
$bl = &amp; new BaseLayout();
$tl = &amp; new TableLayout(FALSE);

$block = &amp; new Block(&quot;entry&quot;);
$block-&gt;add_property(new TextProperty(&quot;title&quot;, &quot;Title&quot;, &quot;&quot;, TRUE, 128), new TextBox(80));
$block-&gt;add_property(new DateProperty(&quot;post_date&quot;, &quot;Post date&quot;, &quot;&quot;, FALSE));
$block-&gt;add_property(new TextProperty(&quot;brief&quot;, &quot;Intro&quot;, &quot;&quot;, TRUE, 10000), new TextArea(80, 15));
$block-&gt;add_property(new TextProperty(&quot;full&quot;, &quot;Content&quot;, &quot;&quot;, FALSE, 100000), new TextArea(80, 25));
$block-&gt;add_property(new LayoutElement(&quot;Post in topics&quot;), new InlineBlock(&amp;$bl, &quot;topic&quot;, &amp;$tl));

$block -&gt; add_property(new ButtonProperty(&quot;save&quot;, &quot;Save&quot;, TRUE));
$block -&gt; add_property(new ButtonProperty(&quot;cancel&quot;, &quot;Cancel&quot;));
$block -&gt; add_property(new ButtonProperty(&quot;delete&quot;, &quot;Delete&quot;));

$form -&gt; add_block($block);

$block = &amp; new Block(&quot;topic&quot;, TRUE);
$block -&gt; add_property(new CheckBoxProperty(&quot;include_fl&quot;, &quot;&quot;, FALSE));
$block -&gt; add_property(new TextProperty(&quot;name&quot;, &quot;Name&quot;, &quot;&quot;, TRUE, 64), new TextDisplay());

$form -&gt; add_block($block);</PRE><P>
Here we notice several new things:</P>
<UL>
	<LI><P>Form constructor has got the second argument: TRUE which will
	turn on the auto-detect-changes mode.</P>
	<LI><P>We define a new layout &ndash; TableLayout &ndash; which we
	will use to display the mult-row block. The FALSE argument tells it
	that is should not generate the header row that will contain the
	column labels. In our simple case of a checkbox and a topic name
	lables are not needed. Just like with BaseLayout, TableLayout output
	can be customized either through the use of CSS stylesheet, or by
	working with the TableLayoutConfig class &ndash; read more in the
	reference section.</P>
	<LI><P>Since we do not want our <I>title</I> property to display the
	full 128 characters, we cannot use the default display, so we create
	a new TextBox, whose constructor takes a parameter: the length of
	the field on screen &ndash; 80 (equivalent of the INPUT's SIZE
	attribute).</P>
	<LI><P>We use a new type of property: DateProperty. This property
	handles dates, currently only in the format of MySQL database:
	YYYY-MM-DD HH:MM:SS. It takes the same parameters as TextProperty,
	except the last one: instead of $length, it has a boolean $date_only
	parameter, which if omitted takes the default value of FALSE. TRUE
	corresponds to date+time, while FALSE corresponds to only date. The
	default display (which is also a TextBox) is sufficient.</P>
	<LI><P>We now have TextArea, taking $width and $height parameters.</P>
	<LI><P>And one more property type: LayoutElement. This is a dummy
	property, that does not have any values. Together with display
	InlineBlock it becomes a placeholder for where block &ldquo;topic&rdquo;
	should be displayed. The LayoutElement constructor takes one
	argument: the label. If it is non-empty, the label will be displayed
	in front of the block as if the block was just another field. If the
	label is empty, the block will expand to take over the label space.
	However, this is a subject of customizing the BaseLayoutConfig
	class. 
	</P>
	<P>InlineBlock takes three parameters: a reference to a layout
	inside which the block will be placed, the name of the block, and a
	reference to the layout that will be used to render the block. It is
	OK that we have not yet at this point defined the topic block &ndash;
	InlineBlock will just remember the name, and use it later &ndash;
	when we generate the form.</P>
	<LI><P>The <I>topic</I> block constructor has the second parameter
	TRUE &ndash; this tells it that it is going to be a multi-row block.</P>
	<LI><P STYLE="font-style: normal">We see CheckBoxProperty used, its
	constructor takes three parameters: <I>name</I>, <I>label</I> (not
	needed here) and <I>default_value</I>. CheckBoxProperties have
	values of TRUE and FALSE, and its default display is what we need.</P>
	<LI><P STYLE="font-style: normal">We also see a new type of display
	for TextProperty &ndash; TextDisplay. It displays uneditable text
	and stores its value in a hidden field, so that when the form is
	restored &ndash; the value is there.</P>
</UL>
<H4>Generating HTML-view</H4>
<P>Amazinly, now with these 2 layouts and one LayoutElement, the form
generation looks just like in a much more simple example above:</P>
<PRE>// Never forget to call this function before you do any output!!!
$form-&gt;process();

echo &quot;&lt;html&gt;&lt;body&gt;&lt;head&gt;\n&quot;;
echo &quot;&lt;link rel=\&quot;stylesheet\&quot; media=\&quot;screen, projection\&quot; type=\&quot;text/css\&quot; href=\&quot;layout.css\&quot; /&gt;\n&quot;;
echo &quot;&lt;/head&gt;&lt;h1&gt;&quot;;
if ($form-&gt;entry-&gt;is_record_existing())
   echo &quot;Edit&quot;;
else
   echo &quot;Create&quot;;
echo &quot; entry&lt;/h1&gt;\n&quot;;

if (isset($error)) echo &quot;&lt;h2&gt;$error&lt;/h2&gt;\n&quot;;

$form-&gt;start_form();
$bl-&gt;show_block(&quot;entry&quot;);
$form-&gt;end_form();

echo &quot;&lt;/body&gt;&lt;/html&gt;\n&quot;;</PRE><P>
To generate the form we generate the main block, in our case <I>entry</I>.
The <I>topic</I> block will be generated automatically by the
InlineBlock display of the LayoutElement that we added to block
<I>entry</I>.</P>
<H4>Defining the triggers</H4>
<P>When working with multi-row blocks, the values of fields become
arrays. So, if in the previous example $form-&gt;topic-&gt;id was a
scalar variable, $form-&gt;topic-&gt;id in this example is an array
of ids. So if we want the id from the second row, we use
$form-&gt;topic-&gt;id[1]. Same with all other field values.</P>
<P><B>Important:</B> all functions and methods dealing with fields
take a $rownum parameter, which is optional. When dealing with single
row blocks, you must remember that you should either omit this
optional parameter, or use -1. Zero will not work, since the form
will look for the 0<SUP>th</SUP> element in an array of values, while
in case of single-row blocks the value is scalar.</P>
<P>A word about the data model in the database. We have tables
entries and topics, and they are linked through an intermediary table
called entry_topics, which only consists of two fields: the ID of the
entry, and the ID of the topic. To post an entry to a topic we just
need to insert a record into this table with two respective IDs.</P>
<P>Let's start with filling the form with the data. We have already
seen usage of FORM_ON_OPEN trigger. We could use the same trigger to
initialize both blocks on our form, but we will not. Instead, we will
use a separate ON_OPEN trigger for each block. This triggers should
be name &lt;BLOCK_NAME&gt;_ON_OPEN, and they will be fired in the
same order as we added blocks to the form.</P>
<PRE>function entry_on_open() {
   global $form, $blog_link, $HTTP_GET_VARS;

   if (isset($HTTP_GET_VARS[&quot;entry&quot;])) {
      $query = sprintf(
         &quot;SELECT entries.id, &quot;.             //0
         &quot;       entries.title, &quot;.          //1
         &quot;       entries.brief, &quot;.          //2
         &quot;       entries.post_date, &quot;.      //3
         &quot;       entries.full &quot;.            //4
         &quot;FROM   entries &quot;.
         &quot;WHERE  entries.id = %d &quot;,
         $HTTP_GET_VARS[&quot;entry&quot;]);

      $result = @mysql_query($query, $blog_link);
      $num_rows = mysql_num_rows($result);

      if (!$num_rows) {
         header(&quot;Location: &quot;.$form-&gt;_denied_target);
         close_db();
         exit;
      }
      $row = mysql_fetch_row($result);

      $form-&gt;entry-&gt;append(RS_OLD);

      $form-&gt;entry-&gt;id = $row[0];
      $form-&gt;entry-&gt;title = $row[1];
      $form-&gt;entry-&gt;brief = $row[2];
      $form-&gt;entry-&gt;post_date = $row[3];
      $form-&gt;entry-&gt;full = $row[4];

   }
}</PRE><P>
Here we see an alternative strategy for handling cases when submitted
IDs are wrong. In our topic editing example, we assumed that if
submitted ID was not found in the database, we just created a new
topic (with whatever ID would be automatically assigned to it). Here,
we redirect to a denied page instead. Note the usage of
$form-&gt;_denied_target: this is the variable that stores the denied
target that we passed to the constructor of the form. Of course this
particular piece of code is not a very user-friendly way of handling
it &ndash; the <I>denied</I> page would not know what was wrong &ndash;
it will not be able to show a good error message. But for the purpose
of this example it will do.</P>
<PRE>function topic_on_open() {
   global $form, $blog_link;

   $query = sprintf(
         &quot;SELECT id, name, topic_id &quot;.  // 0, 1, 2
         &quot;FROM topics &quot;.
         &quot;LEFT JOIN entry_topics ON topic_id = id AND entry_id = %d &quot;.
         &quot;ORDER BY name&quot;,
         $form-&gt;entry-&gt;id);

      // Because of the left join, topic_id column will be null for those
      // topics for which there is no row in the entry_topics for this entry.
      // And it will be not null, if the entry is already included in that topic.
      //
      // When the form is opened for creation, the $form-&gt;entry-&gt;id will be null,
      // sprintf() will convert it into 0, and no entry_topics will be found.

   $result = mysql_query($query, $blog_link);
   $num_rows = mysql_num_rows($result);
   for ($i=0; $i&lt;$num_rows; $i++) {
      $row = mysql_fetch_row($result);
      if ($row[2]) { // Entry already belongs to the topic
         $form-&gt;topic-&gt;append(RS_OLD);
         $form-&gt;topic-&gt;include_fl[$i] = TRUE;
      }
      else { // NULL, Entry does not yet belong to the topic
         $form-&gt;topic-&gt;append(RS_NEW);
      }
      $form-&gt;topic-&gt;id[$i]=$row[0];
      $form-&gt;topic-&gt;name[$i]=$row[1];
   }
}</PRE><P>
The <I>topic </I>block should be filled with all topics available,
but only topics that currently include this entry should be checked.
To do this we use OUTER JOIN: we get all topics, but the third column
will be NOT NULL only for those topics, that already include this
entry.</P>
<P>Notice, that we only create RS_OLD records on those ENTRY_TOPICS
records, that actually exist in the database. You will see how it
becomes useful later.</P>
<P>Let's skip the cancel button trigger, since it is virtually the
same. If you are gonna use copy-paste for it, as I usually do, do not
forget to change the function name to reflect the different name of
the block &ndash; it should now be called <I>entry_cancel_on_action().
</I>
</P>
<P STYLE="font-style: normal">We will also skip the delete button
trigger: it looks very similar. Just do not forget to delete
ENTRY_TOPICs for this entry. And if you are writing a fully blown
blog &ndash; do not forget to delete entry comments too. Let's look
at the save button trigger.</P>
<PRE>function entry_save_on_action($rownum = -1) {
   global $blog_link, $domain, $form, $post_date;

   $post_date = $form-&gt;entry-&gt;post_date;
   if ($post_date == &quot;&quot;)
      $post_date = &quot;SYSDATE()&quot;;
   else
      $post_date = &quot;'$post_date'&quot;;    </PRE><P>
First, let us prepare the post-date value, which will be used later
either to insert a new entry or update an existing entry. If the user
has entered a date, then we will just enclose it in quotes, otherwise
we will put a call to SYSDATE() SQL function. 
</P>
<PRE>   if ($form-&gt;save()) {

      close_db();
      header(&quot;Location: /examples/&quot;);
      exit;
   }
}</PRE><P>
Here we encounter the built-in saving mechanism. The $form-&gt;save()
call validates the form, and attempts to save it if the validation
was a success.It returns TRUE if the saving was successful, or FALSE
if not. In the latter case the global variable $error should contain
the description of the $error encountered.</P>
<P>Of course, the form does not know how to actually save the data,
so it will fire saving triggers, and you should place the proper
saving logic into those triggers. The saving mechanism will go
through all defined blocks in the order they were added to the form,
and then through each record on the form. Depending on the status of
the record, it will call either &lt;BLOCKNAME&gt;_ON_UPDATE,
&lt;BLOCKNAME&gt;_ON_INSERT, or &lt;BLOCKNAME&gt;_ON_DELETE trrigger
with the record's number as the parameter, or -1 for single-row
blocks. The triggers will be fired only for those records that
actually require saving. So, for example a new record that was not
edited by the user will not be saved. If you want to save such
records, you may want to call <I>$block -&gt; mark_changed()</I>
method on those records.</P>
<P>Let's define the saving triggers for our block <I>entry</I>. For
the post-date value we will use the value we already prepared in the
global variable.</P>
<PRE>function entry_on_update($rownum = -1) {
   global $form, $blog_link, $post_date;

   $query = sprintf(
         &quot;UPDATE entries &quot;.
         &quot;SET    title = '%s', &quot;.
         &quot;       brief = '%s', &quot;.
         &quot;       full = '%s', &quot;.
         &quot;       post_date = %s &quot;.
         &quot;WHERE  id = %d&quot;,
         mysql_escape_string($form-&gt;entry-&gt;title),
         mysql_escape_string($form-&gt;entry-&gt;brief),
         mysql_escape_string($form-&gt;entry-&gt;full),
         $post_date,
         $form-&gt;entry-&gt;id);

   mysql_query($query, $blog_link);
}

function entry_on_insert($rownum = -1) {
   global $form, $blog_link, $post_date;

   $query = sprintf(
      &quot;INSERT INTO entries &quot;.
      &quot;   (title, brief, full, post_date) &quot;.
      &quot;VALUES ('%s', '%s', '%s', %s)&quot;,
      mysql_escape_string($form-&gt;entry-&gt;title),
      mysql_escape_string($form-&gt;entry-&gt;brief),
      mysql_escape_string($form-&gt;entry-&gt;full),
      $post_date);

   mysql_query($query, $blog_link);

   // We need to retrieve the autoincrement id assigned to our entry.
   // It will be required for saving the topics of this entry
   $form-&gt;entry-&gt;id = mysql_insert_id($blog_link);
}</PRE><P STYLE="font-style: normal">
As we see, the automatic saving mechanism makes our code very simple:
all we have to do is actually save the data. All other decisions have
already been made for us.</P>
<P STYLE="font-style: normal">Now let's define the saving triggers
for the <I>topic</I> block.</P>
<PRE>// This trigger will only be called for records, that are in status RS_INSERT
// and these are records that previously did not exist in the form, thus the
// checkbox was originally off, and now the record has changed, thus the
// checkbox is on.
function topic_on_insert($rownum = -1) {
   global $form, $blog_link;

   $query = sprintf(
         &quot;INSERT INTO entry_topics (entry_id, topic_id) VALUES (%d, %d)&quot;,
         $form-&gt;entry-&gt;id,
         $form-&gt;topic-&gt;id[$rownum]);

   mysql_query($query, $blog_link);

}

// This trigger will only be called for records in status RS_UPDATE, and these
// are those records that were originally in the database, thus the checkbox was
// on. Since the record has changed to be in status RS_UPDATE, the checkbox must
// be off now. So this is a record to be deleted. However, will will not delete it now,
// - just build a list of ids.
function topic_on_update($rownum = -1) {
   global $form, $delete_list, $join;

   $delete_list .= $join.$form-&gt;topic-&gt;id[$rownum];
   $join = ', ';
}</PRE><P STYLE="font-style: normal">
In the comments section you see two new record statuses. Actually,
there is a total of 6 meaningful record statuses, but you should
never compare them manually and use the status probing functions
instead (<I>is_record_changed(), is_record_deleted(),
is_record_existing() </I>). In addition to RS_NEW and RS_OLD statuses
which we have seen earlier, the additional 4 are:</P>
<UL>
	<LI><P STYLE="font-style: normal">RS_INSERT &ndash; RS_NEW becomes
	RS_INSERT if the user edits it or you call <I>mark_changed()</I>
	method on the block.</P>
	<LI><P STYLE="font-style: normal">RS_UPDATE &ndash; RS_OLD becomes
	RS_UPDATE on the same conditions.</P>
	<LI><P STYLE="font-style: normal">RS_DELETE &ndash; RS_OLD becomes
	RS_DELETE if the record is marked for deletion using the
	<I>mark_deleted() </I>call on the block.</P>
	<LI><P STYLE="font-style: normal">RS_CANCEL &ndash; RS_NEW becomes
	RS_CANCEL if the record is marked for deletion using the
	<I>mark_deleted() </I>call on the block.</P>
</UL>
<P STYLE="font-style: normal">The saving mechanism only call
respective triggers for records in status RS_INSERT, RS_UPDATE, and
RS_DELETE. All other records will simply be ignored as they do not
require any actual saving.</P>
<P STYLE="font-style: normal">In addition to the ON_INSERT,
ON_UPDATE, and ON_DELETE triggers, there are two more saving
triggers: FORM_PRE_SAVE and FORM_POST_SAVE. The PRE_SAVE trigger can
be used to set up a save point, if you are using a transactional
database. In our example, we will only use the POST_SAVE trigger &ndash;
this is where we will actually delete the unneeded entry topics based
on the $delete_list that we collected in TOPIC_ON_UPDATE trigger. It
is more efficient since we will always do only one DELETE.</P>
<PRE>function form_post_save() {
   global $form, $blog_link, $delete_list;

   if ($delete_list) {
      $query = sprintf(
         &quot;DELETE FROM entry_topics &quot;.
         &quot;WHERE entry_id = %d &quot;.
         &quot;AND topic_id IN (%s)&quot;,
         $form-&gt;entry-&gt;id,
         $delete_list);
      mysql_query($query, $blog_link);
   }
}</PRE><P STYLE="font-style: normal">
Well, this is pretty much it with this example. The full text can be
found in the <I>examples</I> section of the library distribution
archive.</P>
<H3>Properties vs. Displays</H3>
<P>In order to use B-Forms efficiently, it is important to understand
the difference between Properties and Displays, and how the work
together.</P>
<P>Properties correspond to the data structure &ndash; they should
roughly mirror your database structure. Unfortunately, due to
historic reasons, some of the property names, CheckBoxProperty and
ButtonProperty namely, may be quite misleading. They should have been
called BooleanProperty and ActionProperty instead. (I am planning to
do this renaming some time in the future &ndash; however, it will
require renaming operations in all the code that uses these property
types &ndash; and since all the code uses ButtonProperty, I have been
postponing this renaming for quite a while).</P>
<P>Displays are widgets that actually display the values to the user
and let the user manipulate the values. So, if you have a numeric
field, you can work with it using several different displays: 
</P>
<UL>
	<LI><P>TextDisplay will allow you to show the number in a read-only
	form.</P>
	<LI><P>TextBox will allow the user to edit the number directly.</P>
	<LI><P>DropDown will allow the user to select from a list of values.
	Furthermore, you can replace the numbers with text labels. For
	example:</P>
</UL>
<PRE>$block -&gt; add_property(new NumericProperty('gender','Gender','',TRUE,1),
                       new DropDown( array(1=&gt;'Male', 2=&gt;'Female', 3=&gt;'Other')));</PRE>
<UL>
	<LI><P>RadioButtons will allow the user to do the same, but using a
	different visual representation. Due to the large size on screen,
	RadioButtons are currently not recommended for use with multi-row
	blocks. The same gender example will look like:</P>
</UL>
<PRE>$block -&gt; add_property(new NumericProperty('gender','Gender','',TRUE,1),
                       new RadioButtons( array(1=&gt;'Male', 2=&gt;'Female', 3=&gt;'Other')));</PRE><P>
When dealing with DropDowns and RadioButtons, several property types
will suffice: TextProperty, NumericProperty, or even the base class
Property.</P>
<H3>Triggers</H3>
<H4>Trigger scope</H4>
<P>There are four types of triggers:</P>
<UL>
	<LI><P>form-level triggers &ndash; these triggers handle the whole
	form, and they are FORM_ON_OPEN, FROM_ON_VALIDATE,
	FORM_WHEN_VALIDATE, FORM_AFTER_RESTORE, FORM_PRE_DISPLAY,
	FORM_PRE_SAVE, FROM_POST_SAVE;</P>
	<LI><P>block-level triggers &ndash; these triggers handle the whole
	block on the block definition level (regardless of how many rows
	there are in the block). They are: &lt;BLOCKNAME&gt;_ON_OPEN,
	&lt;BLOCKNAME&gt;_AFTER_RESTORE, &lt;BLOCKNAME&gt;_PRE_DISPLAY;</P>
	<LI><P>record-level triggers &ndash; these triggers handle a
	particular record of a block. They are: &lt;BLOCKNAME&gt;_ON_VALIDATE,
	&lt;BLOCKNAME&gt;_WHEN_VALIDATE, &lt;BLOCKNAME&gt;_ON_APPEND,
	&lt;BLOCKNAME&gt;_ON_INSERT, &lt;BLOCKNAME&gt;_ON_UPDATE,
	&lt;BLOCKNAME&gt;_ON_DELETE;</P>
	<LI><P>property-level triggers &ndash; these triggers handle a
	particular field or button of one record of a block. They are:
	&lt;BLOCKNAME&gt;_&lt;FIELDNAME&gt;_ON_VALIDATE,
	&lt;BLOCKNAME&gt;_&lt;FIELDNAME&gt;_WHEN_VALIDATE,
	&lt;BLOCKNAME&gt;_&lt;FIELDNAME&gt;_ON_ACTION,
	&lt;BLOCKNAME&gt;_&lt;FIELDNAME&gt;_PRE_RENDER,
	&lt;BLOCKNAME&gt;_&lt;FIELDNAME&gt;_POST_RENDER</P>
</UL>
<H4>Trigger types</H4>
<P>There are two types of triggers:</P>
<UL>
	<LI><P>Triggers that add functionality to the functionality already
	predefined by the B-Forms library. All triggers except ON_VALIDATE
	triggers are of this type.</P>
	<LI><P>Triggers that replace the default functionality of the form &ndash;
	currently only ON_VALIDATE triggers belong to this type.</P>
</UL>
<H4>Trigger sequence during $form-&gt;process() 
</H4>
<P>When the form is opened for the first time (not restored from a
submit), the following sequence is fired:</P>
<PRE>1. form_on_open()
2. for each block:  &lt;block_name&gt;_on_open()
3. (conditionally) &lt;block_name&gt;_on_append() for those blocks that have AUTO-APPEND on, and do not yet have rows.</PRE><P>
You generally have a choice of whether you want to fill all your
blocks with data in one big form_on_open function, or you can have
separate function for each block. Block-level triggers are fired in
the order blocks were added to the form. 
</P>
<P>In addition to loading the data, ON_OPEN triggers can be used for
access control: to ensure that the current user has access to this
data object.</P>
<P>If the form is restored, the triggers are fired in the following
order:</P>
<PRE>1. form_after_restore()
2. for each block:  &lt;block_name&gt;_after_restore()
3. &lt;block&gt;_&lt;button&gt;_on_action($rownum) on the button that was pressed  OR
   &lt;block&gt;_&lt;button&gt;_on_action() on the default button if no button was pressed and default button is defined.</PRE><P>
Form-level and block-level AFTER RESTORE triggers are as
interchangeable as ON_OPEN triggers. I use them for two types of
activity:</P>
<UL>
	<LI><P>Data-level access control. To prevent the situation where the
	form was sent to one user, but submitted by another (through some
	complex hack), it is useful to check that when the form is submitted
	the current user still has the right to modify this particular
	object.</P>
	<LI><P>Display setup. By display I mean the displays that show
	properties. A common case for this usage is drop downs, where the
	actual field is numeric, but the drop down options are text, and the
	content of the drop down should be loaded from the database
	depending on the data that you normally load in <I>form_on_open()</I>.
	The drop down data should be loaded from the database when the form
	is opened and when it is restored. I usually define a function for
	that and then call it from both triggers:</P>
</UL>
<P>An example of the latter usage would be a moderator's page to
pre-moderate several online chats in parallel (all messages for all
chats come to the same moderator). The catch is if the moderator also
has to categorize messages, and the set of categories differs between
different chats. So you first load the message to be moderated, and
only then you can load the set of categories based on the chat, to
which the loaded message belongs.</P>
<PRE>$block-&gt;add_property(new NumericProperty(&quot;&quot;, &quot;category&quot;, &quot;Category&quot;, &quot;&quot;, TRUE)); // Default display

function load_categories() {
    global $link, $form;

    $categories = array();

    // Load categories for the chat to which the message belongs. It should
    // produce an associative array in the form 
    //
    //      array ($category_id =&gt; $category_name, ...)
    //
    //    in the order that values should be displayed in the drop down.
    ..............

    <SPAN LANG="ru-RU"><FONT FACE="Courier New, monospace">$form-&gt;message-&gt;_properties[&quot;category&quot;]-&gt;display = new DropDown($categories);</FONT></SPAN>
}

function form_on_open() {
   ...........
   load_categories();
}

function form_after_restore() {
   ...........
   load_categories();
}</PRE><P>
Here, we first create the <I>category</I> property with default
display, and then replace its display with the proper DropDown
object. Alternatively, we could setup the display as DropDown object
with empty list, and then set the value of that list. That would have
to be done with the following line:</P>
<PRE>$block-&gt;add_property(new NumericProperty(&quot;&quot;, &quot;category&quot;, &quot;Category&quot;, &quot;&quot;, TRUE), new DropDown(array()));

<FONT FACE="Courier New, monospace"><FONT SIZE=2><SPAN LANG="ru-RU">............</SPAN></FONT></FONT>
        $form-&gt;message-&gt;_properties[&quot;category&quot;]-&gt;display-&gt;values = $categories;
<FONT FACE="Courier New, monospace"><FONT SIZE=2><SPAN LANG="ru-RU">............</SPAN></FONT></FONT></PRE><H4>
Trigger sequence during $form-&gt;validate()</H4>
<P>If you call $form-&gt;validate() (and you do not need to call it
unless you are not using $form-&gt;validate()), you usually call it
from the trigger that handles the Save or OK button.</P>
<P>Validation sequence more or less fully mirrors Oracle Forms
sequence, and is the most complex one in B-Forms. If you want to see
how it works, set $form-&gt;_debug_triggers to TRUE and you will see
all the attempts on triggers as they fired.</P>
<P>Validation happens on three levels: form level, record level, and
property level. On each level, there you can define two mutually
exclusive triggers: ON_VALIDATE and WHEN_VALIDATE. If you define a
ON_VALIDATE trigger, it completely replaces all predefined validation
on this level and below. If you don't then the predefined validation
is executed and after that the WHEN_VALIDATE trigger is fired (or
course, if it is defined). 
</P>
<P>If auto-detect-changes mode is on, the record-level validation
only happens for records in status RS_INSERT or RS_UPDATE.</P>
<P>The only way to explain how this sequence operates is to use
pseudo-code:</P>
<PRE>if form_on_validate defined {
   fire form_on_validate
}
else {

   foreach block {
      foreach changed &amp; ~deleted row of the block {
         
         if block_on_validate defined {         
            fire block_on_validate for row
         }
         else {

            foreach property on the block {
               if block_property_on_validate defined {
                  fire block_property_on_validate for row
               }
               else {
                  do default validation of the property

                  fire block_property_when_validate for row
               }
            }           
 
            fire block_when_validate for row
         }
      }
   } 

   fire form_when_validate
}</PRE><P>
The rules of thumb are:</P>
<UL>
	<LI><P>ON_VALIDATE triggers are searched top-down until found. When
	found, the validation of the whole sub-tree starting with that node
	is replaced with this trigger.</P>
	<LI><P>WHEN_VALIDATE triggers are fired only if ON_VALIDATE are not
	defined, and they are fired bottom-up, starting from properties,
	then going to the block record level, and then to the form level.</P>
	<LI><P>I usually do not use ON_VALIDATE triggers, but they may save
	a lot of useless trigger-firing for very simple forms.</P>
	<LI><P>The way WHEN_VALIDATE triggers work is very logical and
	useful: first, individual fields of individual rows are validated &ndash;
	to ensure that each field has proper value and is present if it is
	unconditionally required. Then the record level triggers are fired:
	now you can check that the whole record makes sense, for example
	conditionally required fields, such as a mandatory college name if
	the user specifies that (s)he has a college degree. And in the end
	the form level trigger is fired &ndash; to ensure that all blocks
	make sense together. Here you can validate cross-block and/or
	cross-record conditions.</P>
</UL>
<P>Validation triggers should be functions named to the trigger
standard that do not return any value. They indicate success or
failure through the value of a global variable $error. If it is empty
after the trigger function returned, then the validation was OK. If
it is not empty, then the validation process stops, and the text of
the $error variable is considered to be the error message. Actually,
the whole validation process only runs until the first error is
found.</P>
<H4>Trigger sequence during $form-&gt;save()</H4>
<P>This method automates the saving process. In auto-detect-changes
mode, this method first checks if any changes were detected. If not &ndash;
it will just return TRUE as if the save operation was successful. If
changes are detected, then it calls the $form-&gt;validate() method,
that executes the validation sequence described above. If validation
is not successful, $form-&gt;save() will return with value FALSE.
Assuming the validation was successful, the method will fire the
following sequence of triggers:</P>
<PRE>1. form_pre_save()
2. for each block:  
       for each record on the block:
            if record status = RS_INSERT: &lt;blockname&gt;_on_insert(record number)
            if record status = RS_UPDATE: &lt;blockname&gt;_on_update(record number)
            if record status = RS_DELETE: &lt;blockname&gt;_on_delete(record number)
3. form_post_save()</PRE><P>
FORM_PRE_SAVE is useful for setting up a savepoint in a transactional
database, FORM_POST_SAVE is useful for COMMIT, or some form-wide
operations that can be done with one SQL statement (like deletion of
entry topics in the second example in the tutorial).</P>
<P>The ON_INSERT, ON_UPDATE and ON_DELETE triggers should usually do
what their names imply. 
</P>
<P>If you detect a problem in any of these 5 triggers, you simply set
the global variable $error to the description of the problem (and do
a ROLLBACK if you are using a transactional database). As soon as
$error is not null, the saving sequence is aborted, the $form-&gt;save()
call returns FALSE, which would normally (as in the tutorial
examples) just make the action trigger return, and the form will be
again displayed to the user, showing the error message provided.</P>
<H4>Trigger sequence during $form-&gt;start_form()</H4>
<P>Besides all the other functionality of form generation, this
method fires the PRE_DISPLAY triggers:</P>
<PRE>1. form_pre_display()
2. for each block:  &lt;block_name&gt;_pre_display()</PRE><P>
As we have seen in the tutorial examples, these triggers can be used
to manipulate visibility of certain elements on the form based on the
data that has been loaded into the blocks. These triggers are only
useful when you are using standard layouts to generate your form. If
you are generating the layout yourself by calling <I>field()</I> and
<I>label()</I> functions, then you can manage visibility yourself.</P>
<H4>ON_APPEND triggers</H4>
<P>ON_APPEND triggers fire when $block-&gt;append() method is called
to create a new record (statuses RS_NEW and RS_INSERT). It fires
after the record has been added and all the fields have received
their default values. This is a record level trigger, so unless it is
a single row block, the $rownum parameter will contain the number of
the record just appended.</P>
<P>If you want to avoid firing this trigger because you are appending
a record in order to load an existing object into it, then you should
call $block-&gt;append(RS_OLD). 
</P>
<P>This trigger can be used in two cases:</P>
<UL>
	<LI><P>Default values for the new record are not fixed, but should
	be calculated each time (so that you cannot use the $default_value
	parameter of the property).</P>
	<LI><P>The block in question is a dependent block, and some of its
	properties must be initialized to have proper references to the
	master object. However, I find this an extremely rare situation in
	web-application development. Web-application developers tend to make
	different UI design decisions.</P>
</UL>
<H4>Rendering triggers</H4>
<P>Sometimes you want more then just displaying the text of a
read-only field &ndash; you may, for example, make it a link. You
have two options for doing this:</P>
<UL>
	<LI><P>Generate the form manually, using <I>label() </I>and <I>field()</I>
	calls. Sometimes it makes sense. But if your form is a rather
	complex table that is automatically generated for you by the
	standard TableLayout, you don't want to do such a laborous thing. In
	such a case you go with the second option.</P>
	<LI><P STYLE="font-style: normal">Use the rendering triggers
	PRE_RENDER and POST_RENDER. These are property level triggers, that
	are fired respectively right before and right after a particular
	instance of the property is rendered.</P>
</UL>
<P STYLE="font-style: normal">So, if in our entry-editing example we
would want to make our topic names ($form-&gt;topic-&gt;name)
clickable by surrounding them with a &lt;label&gt; tag, we could add
the following two triggers:</P>
<PRE>// This function will be called every time just before the field 'name' 
// on block 'topic' is generated.
function topic_name_pre_render($rownum) {
global $form;
echo '&lt;label for=&quot;'.
$form-&gt;topic-&gt;_properties['include_fl']-&gt;get_form_name($rownum).
'&quot;&gt;';
}

// This function will be called every time immediately after the field 'name' 
// on block 'topic' is generated.
function topic_name_post_render($rownum) {
echo '&lt;/label&gt;';
<SPAN LANG="ru-RU"><FONT SIZE=2><FONT FACE="Courier New CYR">}</FONT></FONT></SPAN>
</PRE><H3>
Standard Layouts</H3>
<P>Starting with release 1.2 there are three standard layouts: one
for single-row blocks (BaseLayout) and two for multi-row blocks
(TableLayout and TemplateLayout).</P>
<H4>BaseLayout</H4>
<P>This layout shows single-row blocks in a very simple
line-per-property manner. For example, the following peace of code</P>
<PRE>&lt;?php
...........

// Prepare the form
$form = new Form(&quot;denied.html&quot;);
$bl = &amp; new BaseLayout();

$block = &amp; new Block(&quot;person&quot;);
$block-&gt;add_property(New TextProperty(&quot;user_name&quot;, &quot;Username&quot;, &quot;&quot;, TRUE, 32));
$block-&gt;add_property(new TextProperty(&quot;first_name&quot;, &quot;First Name&quot;, &quot;&quot;, TRUE, 32));
$block-&gt;add_property(new TextProperty(&quot;last_name&quot;, &quot;Last Name&quot;, &quot;&quot;, TRUE, 32));
$block-&gt;add_property(new LayoutElement(&quot;Access rights&quot;), new SectionHeader(&amp;$bl));
$block-&gt;add_property(new CheckBoxProperty(&quot;admin_fl&quot;, &quot;Administrator&quot;, FALSE));
$block-&gt;add_property(new CheckBoxProperty(&quot;manager_fl&quot;, &quot;Manager&quot;, FALSE));

$block -&gt; add_property(new ButtonProperty(&quot;save&quot;, &quot;Save&quot;, TRUE));
$block -&gt; add_property(new ButtonProperty(&quot;cancel&quot;, &quot;Cancel&quot;));
$form -&gt; add_block($block);

...........

$form-&gt;process();

echo &quot;&lt;html&gt;&lt;body&gt;&lt;head&gt;\n&quot;;
echo &quot;&lt;link rel=\&quot;stylesheet\&quot; media=\&quot;screen, projection\&quot; type=\&quot;text/css\&quot; href=\&quot;layout.css\&quot; /&gt;\n&quot;;
echo &quot;&lt;/head&gt;&quot;;

echo &quot;&lt;h4&gt;User Details&lt;/h4&gt;\n&quot;;
if(isset($error)) echo &quot;&lt;h5&gt;$error&lt;/h5&gt;\n&quot;;

$form-&gt;start_form();

$bl-&gt;show_block(&quot;person&quot;);

$form-&gt;end_form();
echo &quot;&lt;/body&gt;&lt;/html&gt;\n&quot;;

.............
?&gt;</PRE><P>
Will generate the following layout (except it will look different if
a stylesheet is used):</P>
<H4>User Details</H4>
<FORM NAME="mainform" ACTION="../../../../examples/editperson.php" METHOD="POST">
	<TABLE CELLPADDING=2 CELLSPACING=2>
		<TR>
			<TD>
				<P><FONT FACE="Arial, sans-serif"><FONT SIZE=2>Username:</FONT></FONT></P>
			</TD>
			<TD>
				<P><INPUT TYPE=TEXT NAME="person_user_name" SIZE=32 MAXLENGTH=32>
								</P>
			</TD>
		</TR>
		<TR>
			<TD>
				<P><FONT FACE="Arial, sans-serif"><FONT SIZE=2>First Name:</FONT></FONT></P>
			</TD>
			<TD>
				<P><INPUT TYPE=TEXT NAME="person_first_name" SIZE=32 MAXLENGTH=32>
								</P>
			</TD>
		</TR>
		<TR>
			<TD>
				<P><FONT FACE="Arial, sans-serif"><FONT SIZE=2>Last Name:</FONT></FONT></P>
			</TD>
			<TD>
				<P><INPUT TYPE=TEXT NAME="person_last_name" SIZE=32 MAXLENGTH=32>
								</P>
			</TD>
		</TR>
		<TR>
			<TD COLSPAN=2>
				<P><FONT FACE="Arial, sans-serif"><FONT SIZE=2><B>Access rights</B></FONT></FONT></P>
			</TD>
		</TR>
		<TR>
			<TD>
				<P><FONT FACE="Arial, sans-serif"><FONT SIZE=2>Administrator:</FONT></FONT></P>
			</TD>
			<TD>
				<P><INPUT TYPE=CHECKBOX NAME="person_admin_fl" VALUE="Y"> 
				</P>
			</TD>
		</TR>
		<TR>
			<TD>
				<P><FONT FACE="Arial, sans-serif"><FONT SIZE=2>Manager:</FONT></FONT></P>
			</TD>
			<TD>
				<P><INPUT TYPE=CHECKBOX NAME="person_manager_fl" VALUE="Y"> 
				</P>
			</TD>
		</TR>
		<TR>
			<TD COLSPAN=2></TD>
		</TR>
		<TR>
			<TD COLSPAN=2>
				<P><INPUT TYPE=SUBMIT NAME="person_save" VALUE="Save"> <INPUT TYPE=SUBMIT NAME="person_cancel" VALUE="Cancel"><INPUT TYPE=HIDDEN NAME="restore" VALUE="true"><INPUT TYPE=HIDDEN NAME="person_rs" VALUE="0"><INPUT TYPE=HIDDEN NAME="person_id" VALUE="-1"><INPUT TYPE=HIDDEN NAME="signature" VALUE="2e39d7f4154a524dd1a7c0daf6e75bda">
								</P>
			</TD>
		</TR>
	</TABLE>
</FORM>
<P><BR><BR>
</P>
<P>In this example we define layout using:</P>
<PRE STYLE="margin-bottom: 0.5cm">$bl = &amp; new BaseLayout();</PRE><P>
and then use it with the following line of code:</P>
<PRE STYLE="margin-bottom: 0.5cm">$bl-&gt;show_block(&quot;person&quot;);</PRE><P>
It displays all data properties one per line in the order they were
added to the block, and then all control properties in one line at
the bottom, also in the order they were added to the block.</P>
<P>To insert the &ldquo;Access rights&rdquo; header, we use a special
&ldquo;dummy&rdquo; property type called LayoutElement. The
constructor takes one optional parameter $label, which in case of
SectionHeader display displays as a sub-header in the form.</P>
<P>There is another display for LayoutElement &ndash; Separator. This
is just a vertical spacer and it does not require a label from
LayoutElement.</P>
<P>When BaseLayout generates the form, it uses BaseLayoutConfig class
for the source of the HTML tags to be used. You can customize the
look of your forms in your application by defining an include file
that will contain your own config class, for example, if you want to
avoid the colon after the labels, you can to the following:</P>
<PRE>&lt;?
  require_once(&quot;b-forms/layout.inc&quot;);
  
  class MyBaseLayoutConfig extends BaseLayoutConfig {
     var $label_close = &lt;/td&gt;&quot;;  
  }
?&gt;</PRE><P>
Then, when creating a layout you should use the following code:</P>
<PRE>.........
require_once(my_config_include_file);

$bl = new BaseLayout(new MyBaseLayoutConfig());
.........</PRE><P>
If you provide an instance of a config class to the constructor of
BaseLayout, it will use your config instead of the default one.</P>
<P>For detailed contents of the config class see class reference.</P>
<H4>TableLayout</H4>
<P>This layout shows all properties in the order of adding to the
block as table columns, and records &ndash; as table rows.</P>
<P>For example, the following code:</P>
<PRE>&lt;?
............

$form = new Form(&quot;denied.html&quot;, TRUE);

// Prepare layouts
$tl = &amp; new TableLayout();

//Multi-row block &quot;record&quot;, for holding project records
$block = &amp; new Block(&quot;record&quot;, TRUE);
$block -&gt; add_property(new TextProperty(&quot;project_name&quot;, &quot;Project Name&quot;, &quot;&quot;, FALSE, 64), new TextBox(30));
$block -&gt; add_property(new ButtonProperty(&quot;delete&quot;, &quot;Delete&quot;));
for ($i=0; $i&lt;7; $i++)
  $block -&gt; add_property(new NumericProperty(&quot;hours_$i&quot;, $days[$i], &quot;&quot;, FALSE, 2, 1));

$form -&gt; add_block($block);

.............

$form-&gt;start_form();
$tl-&gt;show_block(&quot;record&quot;);
$form-&gt;end_form();

.............
?&gt;</PRE><P>
will generate a form that looks something like (with a proper
stylesheet it would look much better):</P>
<FORM NAME="mainform" ACTION="../../../../examples/tr.php" METHOD="POST">
	<TABLE WIDTH=645 BORDER=0 CELLPADDING=2 CELLSPACING=0>
		<COL WIDTH=296>
		<COL WIDTH=48>
		<COL WIDTH=37>
		<COL WIDTH=37>
		<COL WIDTH=37>
		<COL WIDTH=37>
		<COL WIDTH=37>
		<COL WIDTH=38>
		<COL WIDTH=43>
		<TR>
			<TD WIDTH=296>
				<P>Project Name</P>
			</TD>
			<TD WIDTH=48>
				<P>&nbsp;</P>
			</TD>
			<TD WIDTH=37>
				<P>Mon</P>
			</TD>
			<TD WIDTH=37>
				<P>Tue</P>
			</TD>
			<TD WIDTH=37>
				<P>Wed</P>
			</TD>
			<TD WIDTH=37>
				<P>Thu</P>
			</TD>
			<TD WIDTH=37>
				<P>Fri</P>
			</TD>
			<TD WIDTH=38>
				<P>Sat</P>
			</TD>
			<TD WIDTH=43>
				<P>Sun</P>
			</TD>
		</TR>
		<TR>
			<TD WIDTH=296>
				<P><INPUT TYPE=TEXT NAME="record_project_name_0" VALUE="Project 1" SIZE=30 MAXLENGTH=64>
								</P>
			</TD>
			<TD WIDTH=48>
				<P><INPUT TYPE=SUBMIT NAME="record_delete_0" VALUE="Delete"> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_0_0" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_1_0" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_2_0" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_3_0" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_4_0" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=38>
				<P><INPUT TYPE=TEXT NAME="record_hours_5_0" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=43>
				<P><INPUT TYPE=TEXT NAME="record_hours_6_0" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
		</TR>
		<TR>
			<TD WIDTH=296>
				<P><INPUT TYPE=TEXT NAME="record_project_name_1" VALUE="Project 2" SIZE=30 MAXLENGTH=64>
								</P>
			</TD>
			<TD WIDTH=48>
				<P><INPUT TYPE=SUBMIT NAME="record_delete_1" VALUE="Delete"> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_0_1" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_1_1" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_2_1" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_3_1" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_4_1" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=38>
				<P><INPUT TYPE=TEXT NAME="record_hours_5_1" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=43>
				<P><INPUT TYPE=TEXT NAME="record_hours_6_1" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
		</TR>
		<TR>
			<TD WIDTH=296>
				<P><INPUT TYPE=TEXT NAME="record_project_name_2" VALUE="Project 3" SIZE=30 MAXLENGTH=64>
								</P>
			</TD>
			<TD WIDTH=48>
				<P><INPUT TYPE=SUBMIT NAME="record_delete_2" VALUE="Delete"> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_0_2" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_1_2" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_2_2" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_3_2" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_4_2" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=38>
				<P><INPUT TYPE=TEXT NAME="record_hours_5_2" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=43>
				<P><INPUT TYPE=TEXT NAME="record_hours_6_2" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
		</TR>
		<TR>
			<TD WIDTH=296>
				<P><INPUT TYPE=TEXT NAME="record_project_name_3" VALUE="Project 4" SIZE=30 MAXLENGTH=64>
								</P>
			</TD>
			<TD WIDTH=48>
				<P><INPUT TYPE=SUBMIT NAME="record_delete_3" VALUE="Delete"> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_0_3" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_1_3" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_2_3" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_3_3" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_4_3" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=38>
				<P><INPUT TYPE=TEXT NAME="record_hours_5_3" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=43>
				<P><INPUT TYPE=TEXT NAME="record_hours_6_3" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
		</TR>
		<TR>
			<TD WIDTH=296>
				<P><INPUT TYPE=TEXT NAME="record_project_name_4" VALUE="Project 5" SIZE=30 MAXLENGTH=64>
								</P>
			</TD>
			<TD WIDTH=48>
				<P><INPUT TYPE=SUBMIT NAME="record_delete_4" VALUE="Delete"> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_0_4" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_1_4" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_2_4" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_3_4" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_4_4" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=38>
				<P><INPUT TYPE=TEXT NAME="record_hours_5_4" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=43>
				<P><INPUT TYPE=TEXT NAME="record_hours_6_4" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
		</TR>
		<TR>
			<TD WIDTH=296>
				<P><INPUT TYPE=TEXT NAME="record_project_name_5" VALUE="Project 6" SIZE=30 MAXLENGTH=64>
								</P>
			</TD>
			<TD WIDTH=48>
				<P><INPUT TYPE=SUBMIT NAME="record_delete_5" VALUE="Delete"> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_0_5" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_1_5" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_2_5" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_3_5" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=37>
				<P><INPUT TYPE=TEXT NAME="record_hours_4_5" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=38>
				<P><INPUT TYPE=TEXT NAME="record_hours_5_5" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
			<TD WIDTH=43>
				<P><INPUT TYPE=TEXT NAME="record_hours_6_5" SIZE=4 MAXLENGTH=4> 
				</P>
			</TD>
		</TR>
	</TABLE>
</FORM>
<P><BR><BR>
</P>
<P>The usage of TableLayout is very similar to the usage of
BaseLayout, with the following exceptions:</P>
<UL>
	<LI><P>TableLayout does not support &ldquo;dummy&rdquo; properties
	(LayoutElements).</P>
	<LI><P>Data and control properties are not separated, and are
	displayed in the common order they were defined.</P>
	<LI><P>The HTML config class is unsurprisingly called
	TableLayoutConfig. It has different config parameters, see the class
	reference for details.</P>
	<LI><P>The constructor of TableLayout takes two optional paremeters:
		</P>
	<UL>
		<LI><P>$show_labels &ndash; TRUE by default, specifies whether you
		actually want the column headers (our second example of editing a
		blog entry used a TableLayout without column headers).</P>
		<LI><P>$config &ndash; an instance of a config class, you can
		provide an instance of your own config class just as we saw with
		the BaseLayout.</P>
	</UL>
</UL>
<H4>Block within a block</H4>
<P>You can embed TableLayouts within BaseLayouts using LayoutElement
with display InlineBlock, an example of which was the blog-entry
example.</P>
<PRE>// Prepare the form
$form = new Form(&quot;denied.html&quot;, TRUE);
$bl = &amp; new BaseLayout();
$tl = &amp; new TableLayout(FALSE);

........

$block-&gt;add_property(new LayoutElement(&quot;Post in topics&quot;), new InlineBlock(&amp;$bl, &quot;topic&quot;, &amp;$tl));

........</PRE><P>
When the main block is generated using its layout (which is the first
parameter to the InlineBlock constructor), the specified block
(&ldquo;topic&rdquo; in this case) will automatically be generated in
place of the dummy property, using the layout provided as the thirs
paremeter to the InlineBlock constructor.</P>
<P>If the LayoutElement does not have a label, the inner block will
be rendered without a label and for the full width of the form. If
you want a layout without a label, but as wide as the data column of
the form, they you can use &ldquo;&amp;nbsp;&rdquo; as the label.</P>
<H4>TemplateLayout</H4>
<P>TemplateLayout is designed for multi-row blocks, for cases more
complex than TableLayout can handle. For example, if you want to
display each record in several short lines rather than one long line.
</P>
<P>Basically, TemplateLayout merges the best of two worlds: the
automatic generation of multiple rows and manual layout of each row &ndash;
through the use of a template. A template is a user-defined function
that takes one parameter: the number of the record and generates the
HTML-code for the given record using <I>label() </I>and <I>field()</I>
calls. 
</P>
<P STYLE="font-style: normal">Just before the first row, the function
is called with TL_START value of the $rownum parameter, and
immediately after the last row, this function is called with TL_END
value of the $rownum parameter. If there are no records, TL_START and
TL_END calls are not made. This allows you to open and close the
layout (for example, open and close the &lt;table&gt; tags).</P>
<P STYLE="font-style: normal">On average, it will look like this:</P>
<PRE>function template_function($rownum) {
global $form;

if ($rownum == TL_START) {
echo '&lt;table&gt;';
return;
}

if ($rownum == TL_END) {
echo '&lt;/table&gt;';
return;
}

echo '&lt;tr class=&quot;'.($rownum%2==0?'odd':'even').'&quot;&gt;'; // Record numbers start with 0!

// ... your code to generate the row

echo &quot;&lt;/tr&gt;\n&quot;;
}

$tl = &amp; new TemplateLayout('template_function');
$tl - &gt;show_block('...your-block-name...');
</PRE><H2>
Common Problems</H2>
<P>In this section I will address common problems encountered by
developers, including myself. Currently there is only one :)</P>
<H3>Fatal error: Call to a member function on a non-object in
....\b-forms\b-forms.inc on line 9XX</H3>
<P STYLE="font-weight: medium">In version 1.0 there was no validation
of parameters that you provide when calling <I>label()</I> and
<I>field()</I> fucntions. As a result, when you provide erraneous
values, this is the error message you would normally get. It is not
at all a problem of the library, it is the problem of the value you
provided. Very often comes from copy-paste type of new module
creation, which I personally use very often. In version 1.0.1 I have
added validation of the provided values, so that you will get a more
meaningful message, like:</P>
<P STYLE="font-weight: medium"><B>Fatal error</B>: field(): property
name1 does not exist in block topic in ....<B>\b-forms\b-forms.inc</B>
on line <B>9XX</B></P>
<P STYLE="font-weight: medium">It will still appear as an error in
b-forms, although it is not.</P>
</BODY>
</HTML>
Return current item: B-Forms