eTemplate - Templates and Dialog-Editor for eGroupWare

by Ralf Becker RalfBecker AT outdoor-training DOT de

Updated by Raphael Alla >raphael AT olineopensolutions DOT com

A developers tutorial how to write an application with the new eTemplates.
It is also an introduction how to write a eGW- and setup(3)-compatible app.


Introduction - The concept of the eTemplates

The eTemplates


Tutorial / Example: a simple media database

As an example we will run now through the necessary steps to create a simple media database using eTemplates and other tools and classes from the eTemplate app: db-tools and class.so_sql.inc.php.

Out media database should have the usual fields: name, author, description, type: BOOK, CD, VIDEO and should be able to edit records and search for them.

As a preaquistion you need to get / checkout the etemplate app, install the app via setup/manage applications and enable your account for using the app (Admin/User account: check eTemplates).

1. Creating a new eGW app directory

Each app need a name, eg. 'et_media'. We now need to create the following directory structur above the eGroupWare dir:

et_media                                that has to be identical to our app-name
        + setup                         files necessary for the setup Programm, give the webserver write-permission to that dir
        + inc                           class-files
        + templates                     templates, still needed to store the images and get around a lot of complains from the api
                + default
                        + images        here goes our images / icons

2. creating et_media/setup/setup.inc.php

That files contains the necessary information for setup to install the app.

<?php
        $setup_info['et_media']['name']      = 'et_media';
        $setup_info['et_media']['title']     = 'eT-Media';
        $setup_info['et_media']['version']   = '0.9.15.001';
        $setup_info['et_media']['app_order'] = 100;             // at the end
        $setup_info['et_media']['tables']    = array('phpgw_et_media');
        $setup_info['et_media']['enable']    = 1;

        /* Dependencies for this app to work */
        $setup_info['et_media']['depends'][] = array(
                 'appname' => 'phpgwapi',
                 'versions' => Array('1.2.005','1.2.006')
        );
        $setup_info['et_media']['depends'][] = array(   // this is only necessary as long the etemplate-class is not in the api
                 'appname' => 'etemplate',
                 'versions' => Array('1.2')
        );

3. setting up the db-table with the db_tools and setup

To enable setup to create a db-table for us and to supply the so_sql-class with the necessary information, we need to define the type and size of the fields / columns in our db-table.





We can use the db-Tools from the etemplate application to create the file for us:

  1. start the etemplate app and click on the button up, right which says db-Tools

  2. select Application: eT-Media

  3. type 'egw_et_media' in the field in front of the [Add Table] button and click on the button

  4. now use [Add Column] to create the necessary fields as shown on the screenshot

  5. Click on [Write Table] (you need to give the webserver write-permission to the setup-dir of et_media or you will get an error message, leave the write-permission as it is necessary later on too, click on write again)

  6. log out and log into setup and start manage applications

  7. eT-Media is shown as not installed and only install is offerd, check it and submit

  8. you can now log out from setup, the db-table is now created

  9. In order to be able to use your eT-Media application, do not forget to give yourself access to it (Admin/User account: check eT-Media)

4. creating an eTemplates for the edit-dialog

Now we need a nice edit dialog and use the eTemplate editor to set it up:

  1. start the etemplate app and type 'et_media.edit' in the name field. Save the template in order to create it

  2. an empty template is displayed. An eTemplate can be thought off as a “grid”. The first cell may be a bit tricky to find, but will be highlighted when moving the mouse over it. On my computer this cell appears in pink as illustrated below:


  3. Double click on the pink spot will bring the following dialog:


  4. The top row allows you to add column and rows to the template. We will need 2 columns and 6 rows

  5. Create the following label in the first top left cell:




  1. In the top right cell, we will create a user entry and call it “name”: this is the same name as thee column in our egw_et_media table. This is important as those fields will be populated automatically for us by eGroupWare:


  2. Complete the template as follows. The widget used for “type” is a Selectbox, the one used for “description” is a textarea. Note that the name of the input is “descr” and not description, as this is the name of the column in the table. Finally on the last row we have two widgets of type “Submitbutton” of names “read” and “save” and of corresponding label.


Then before moving to the next stage save the template as an XML file by clicking on “Export XML”. Once again the server must have write permissions on the directory.

5. Setting up the index page

The index page is only used if someone clicks on the navbar icon (or on the black cross as we haven't supplied one so far).
Create the file /et_media/index.php with the following content:

<?php
        $GLOBALS['phpgw_info']['flags'] = array(
                'currentapp'    => 'et_media',
                'noheader'      => True,
                'nonavbar'      => True
        );
        include('../header.inc.php');
        $GLOBALS['egw']->redirect_link('/index.php', 'menuaction=raphatest.ui_et_media.edit');
       

6. The code for our application

An eGroupWare application is organised around 3 application layers:

  1. the storage layer, managed by a “Storage Object” (so). This object is responsible for handling all access to the storage engine

  2. the business layer, managed by a “Business Ojbect” (bo). This object is responsible for all the business logic

  3. the user interface layer, managed by a “User Interface” (ui) object. This object is responsible for all interaction with the user, including displaying and gathering data to and from the user

For this, we create 3 files in the “inc” directory, called class.so.et_media.inc.php, class.bo_et_media.inc.php, class.ui_et_media.inc.php. In this simple application, the bo and so layers will be fairly minimal, this said it is a good idea to create the application using the right standards from the start.

Here is the file /et_media/inc/class.so_et_media.inc.php:

<?php
  include_once(PHPGW_INCLUDE_ROOT . '/etemplate/inc/class.so_sql.inc.php');

  /**
   * General storage class for et_media
   */
   class so_et_media extends so_sql
   {
     function so_et_media()
     {
       $this->so_sql('et_media','egw_et_media');
       $this->empty_on_write = "''";
     }
   }



The file /et_media/inc/class.bo_et_media.inc.php:

<?php
 /**
  * Business Object for et_media
  */
  class bo_et_media
  {
     var $types = array(
         ''      => 'Select one ...',
         'cd'    => 'Compact Disc',
         'dvd'   => 'DVD',
         'book'  => 'Book',
         'video' => 'Video Tape'
     );

    function bo_et_media()
    {
      $this->so =& CreateObject('et_media.so_et_media');
    }
    
    function save($content)
    {
      $this->so->save($content);
    }

    function read($content)
    {
      $this->so->search($content);
    }
  }



And finally the start of the /et_media/inc/class.ui_et_media.inc.php:

<?php
/**************************************************************************\
* eGroupWare - eTemplates - Tutoria Example - a simple MediaDB           *
* http://www.eGroupWare.org                                              *
* Written by Ralf Becker <RalfBecker AT outdoor-training DOT de>           *
* --------------------------------------------                             *
*  This program is free software; you can redistribute it and/or modify it *
*  under the terms of the GNU General Public License as published by the   *
*  Free Software Foundation; either version 2 of the License, or (at your  *
*  option) any later version.                                              *
\**************************************************************************/

/* $ Id: class.et_media.inc.php,v 1.2 2002/10/19 11:11:03 ralfbecker Exp $ */

class ui_et_media
{
    var $public_functions = array(
         "edit" => True,
         "writeLangFile" => True
     );

The $public_functions array defines which public functions can be accessed by the user.

The constructor initialises the template engine and the Business Object:

    function ui_et_media()
    {
      $this->tmpl =& CreateObject('etemplate.etemplate', 'et_media.edit');
      $this->bo   =& CreateObject('et_media.bo_et_media');
      $this->html =& $GLOBALS['egw']->html;
                
      if(!@is_object($GLOBALS['egw']->js))
      {
           $GLOBALS['egw']->js =& CreateObject('phpgwapi.javascript');
      }

    }



Finally, the edit function is the one which does all the work from a user perspective.

function edit($content='') 
{
        if (is_array($content)) // we are called as a call back
        {
                $r_id = $content['id'];
                if ($r_id>0) // if we have an id -> read the entry
                {
                        $content = $this->bo->read($content['id']);
                }

                if (isset($content['save']))
                {
                        unset($content['save']);
                        $msg .= (!$this->bo->save($content))?lang('Entry Saved'):lang('Error: while saving');
                }
                elseif (isset($content['read']))
                {
                        unset($content['id']);
                        unset($content['read']);
                        $found = $this->bo->so->search($content, False, 'name, author');

                        if (!$found)
                        {
                                $msg .= lang('Nothing matched the search criteria');
                        }
                        else
                        {
                                $content = $found[0];
                        }
                }
        }
        else
        {
                $content = array();
        }

        //now we fill the content array for the next call to etemplate.exec

        $content = $content + array (
                'msg' => $msg
        );

        $sel_options = array(
                'type' => $this->bo->types
        );

        $no_button = array(
                );
        $preserv = array(
                'id' => $this->data['id']
        );

        $this->tmpl->exec(
                'et_media.ui_et_media.edit', // setting this function as the callback
                $content,$sel_options, $no_button,$preserv
        );
    }
  }

The edit function is called from our index.php file or as callback for this form / dialog. In that case $content is an array with the content the user put into the fields of the dialog.

Let first have a look what happend if we called the first time (or what we do to show the dialog again with the changed data):

  1. the $content array is set up with our internal data-array (which is empty on the first call) and the message

  2. $sel_options has the options for our selectbox: the options are an array where the keys are the values returned by the selectbox and the values are what the selectbox shows to the user. As we can have more than one selectbox in a dialog, the key in $sel_options need to be the same as the name of the selectbox.

  3. $readonlys: if a fieldname is set in $readonlys to True, its content is showed readonly (for regular fields like type Text) or left out for buttons (we use this later to show the delete-button only when an entry is loaded)

  4. the array $preserv is preserved, which means its stored in the app's session-data and is delivered back like the content of the fields to the callback-function. We use it here to store the id of the entry. This is similar to use a hidden input-field in a form, but it does not need to be serialized by the app and is NOT transmitted to the user and back.

  5. at last we call etemplate::exec to show the template with the content from $content and set the function itself as callback for the dialog / form.

Now let's have a look what happens if the user submits the form and our callback is called:

  1. the callback (this function) is not the submit-address of the form, the form get's always submitted to the function process_exec of the etemplate class. This function changes for some field-types the content (eg. a date-field consists of 3 single fields, process_exec takes care that it is delivered back as timestamp, as we set it in content before). It can even submit the form back to the user if for a address-selection a search for a pattern has to be performed and the matches are shown to the user. In this case the callback is NOT called. The same is true if an int field contains letters or is not within the minimum or maximum set. Not all of the is allready working, it will follow in the next days/weeks.
    For the specialist process_exec uses $_POST and ignores $_GET set as query in the url.

  2. the so_sql function data_merge, copies all values from $content, which are columns in the db-table, in our internal data array. Values which are not real data, like buttons pressed are not copied (!).

  3. if $content['save'] is set, the [Save] button has been pressed ('save' is the name NOT the label of the save button), in that case we use so_sql's save function to save the content of our internal data-array to the db.

  4. the same check is used for the [Read]: we uses the content of all fields to search db for matching entries. The user can use wildcards to perform a search on all field. The wildcards are '*' and '?', so_sql translates them into sql-wildcards.

  5. if the search return False we just set our message var.

  6. if something is found we use so_sql's init-function to set the data of the first match. Lateron we will show a list if more than one entry is found.

  7. after that the content array is filled again as discriped above.

Now we are able to store entries in the db and retrive them by searching the database for patterns in the different fields. You can try your new application now. You can create new records and save them. By just entering the name or author, the database will find the corresponding match and populate the form for you.


We are only lacking some way to show if we get more than one match on a search, that's what we are going to implement next:

7. adding a list-dialog for the search-function

First we need to create an other eTemplate to show the list: 'et_media.show' as follows. This is made of a label of name “msg” (to display messages), an HorizontalRule widget, and a Template widget: we will use a “sub template” called et_media.show.rows to display the rows of the search function. I have set the option of the template widget to “entry” as this is the name we will use to access to the data in the sub-template.




The 'et_media.show.rows' template is created as a 3x2 table. On the header row, two labels “Name” and “Author” and one empty cell.

On the second row, two labels of name ${row}[name] and ${row}[author]. In the last cell a submitButton of label “Edit” and of name “edit[$row_cont[id]]”




The class of the header row is “nmh” and the class of the content row is “nmr”. eTemplate will automatically vary the colors of the “nmr” class to provide a nice visual effect.

Here is a view of the et_media.show template once the two templates have been created:




We need some code / a function in the class to call the template and fill the content:

        function show($found)
        {
                if (!is_array($found) || !count($found))
                {
                        $this->edit();
                        return;
                }
                reset($found);          // create array with all matches, indexes starting with 1
                for ($row=1; list($key,$data) = each($found); ++$row)
                {
                        $entry[$row] = $data;
                }
                $content = array(
                        'msg' => lang('%d matches on search criteria',count($found)),
                        'entry' => $entry               // et_media.show.rows uses this, as we put 'entry' in the Options-field
                );
                $this->tmpl->read('et_media.show');     // read the show-template

                $this->tmpl->exec('et_media.ui_et_media.edit',$content);   // exec it with the edit-function as callback
        }

This function is called by edit with the matches of a search:

  1. We build an array with all the matches, the index in that array is the row-number starting with 1 (!) ($entry = array('empty') + $found; would do the same).
    The names in the data-row (last row) of 'et_media.show.rows' are like '${row}[name]'. Variable expansion is performed on each name and expands that for the first row to '1[name]' which addresses the name in the first match.

  2. $content contains again 'msg' which we set to the number of entris found and the above array with the data of all rows under the key 'entry', as we put that in Options for the field loading the sub-template 'et_media.show.rows'. It not necessary to put something in Options-field / use a sub-array for a sub-template, but it can be very helpful to organize a complex content-array. (As an exercice you can remove 'entry' from the Options-field and change the function arrcordingly).

  3. we now explizitly read the template 'et_media.show' (the constructor reed 'et_media.edit') and execute it again with the edit function as callback (because of that, show does NOT need to be listed in public_functions)

  4. as 'et_media.show.rows' contains only one data-row, but fieldnames with variables to expand, that row is autorepeated for as many data we put into the content array (or the sub-array if we used the Options-field).

To call the show function, we need to make some changes to the edit-function too:

                        elseif (isset($content['read']))
                        {
                                unset($content['id']);                                  // not set by user, so dont use for seach
                                $found = $this->search($content,False,'name,author');   // searches by using the no-empty fields

                                if (!$found)    // search returned empty
                                {
                                        $msg .= lang('Nothing matched search criteria !!!');
                                }
                                elseif (count($found) == 1)             // only one match --> show it in the editor
                                {
                                        $this->init($found[0]);
                                }
                                else                                    // multiple matches --> use the show function/template
                                {
                                        $this->show($found);
                                        return;
                                }
                        }
                        elseif (isset($content['entry']['edit']))       // the callback from for the show function/template
                        {                                               // the id is set via the button name of '$row_cont[id]'
                                list($id) = each($content['entry']['edit']);    // note its not only ['edit'] !!!
                                if ($id > 0)
                                {
                                        $content = $this->bo->so->read(array('id' => $id));
                                }
                        }
  1. the first part should be self-explaining, we call show with $found if it contain more than one entry.

  2. The show function uses edit as callback, the [Edit] buttons in each row has 'edit[$row_cont[id]]' as name. If an [Edit] button is pressed $content['entry']['edit'] is set to the id of the entry of that row. We use that id to read the whole entry.

This is what the new “show” template looks like:




While makeing this changes we can add a [Cancel] and [Delete] button too:

                        elseif (isset($content['cancel']))
                        {
                                $content = array(); // clear the contents
                        }
                        elseif (isset($content['delete']))
                        {
                                $this->bo->so->delete($r_id);
                                $content = array(); // clear the content
                        }


                $no_button = array(     // no delete button if id == 0 --> entry not saved
                        'delete' => !$this->content['id']; 
                );
  1. on cancel we just clear the internal data-array with so_sql's init function.

  2. on delete we have to call so_sql's delete before (it deletes the db-row coresponding with our internal data-array)

  3. the last block checks if the id field is set (it can only be set by a read or save) and disables the [Delete] button if not ($this->db_key_cols[$this->autoinc_id] == 'id').

Of course we have to add this buttons to the template 'et_media.edit'. I trust you can add 2 Submitbuttons with the names 'cancel' and 'delete', a Label and a nice helpmessages by now without looking at a screenshot ;-).

8. creating the english lang-file

To get rid of the stars '*' behind each Label and to be able to translate the app in other languages we need to create a lang-file
There are 2 possibilties to create it automaticaly:

  1. Use the [Write Langfile] button in the eTemplate editor (put the app-name 'et_media' in the name-field first)
    That will omitt our own messages in the class!!!

  2. We use a function in our class to call etemplate::writeLangFile('et_media','en',$extra) and can so supply some extra strings.
    If we add this function to the public_functions-array in our class, we can just call this function via the browser:
    http://ourDomain/eGroupWare/index.php?menuaction=et_media.et_media.writeLangFile (the errormsg can be savely ignored)
    This is the function (don't forget to add it like the edit-function to public_functions):

Anyway we have to use the TranslationTools to find and write the lang()-messages of our code!

        /*!
        @function writeLangFile
        @abstract writes langfile with all templates registered here
        @discussion can be called via [write Langfile] in eTemplate editor
        */
        function writeLangFile()
        {
                return $this->tmpl->writeLangFile('et_media','en',$this->types);
        }

9. dumping the eTemplate to a file for distribution

To be able to put the eTemplates in CVS and to ship them with your app, you need to dump them in a file first.

This is done in the eTemplate editor by putting the app-name or an template-name in the Name field and clicking on the button [Dump4Setup]. This creates the file et_media/setup/etemplates.inc.php. The etemplate-class loads this file whenever it finds a new version automaticaly.

10. further information

  1. the reference-documentation of the eTemplates

  2. for all functions and parameters of the etemplate-class(es) look in the phpDocumentor docu of etemplate created from comments (yes there are comments) in the sources:

  3. for all functions and parameters of the so_sql-class look in the comments of the file class.so_sql.inc.php

  4. for setup, the necessary files of an app or the format of tables_current.inc.php look at the exelent docu of setup3 in the doc-dir of the setup app.

That's it - please contact me if you have further questions or comments about the tutorial