The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.

NAME

Rose::DBx::Object::Renderer - Web UI Rendering for Rose::DB::Object

SYNOPSIS

  use Rose::DBx::Object::Renderer;

  use CGI;
  my $query = new CGI;
  print $query->header();

  # Load a MySQL database named 'company', which has two tables: 'employee' and 'position', where employee has a position
  my $renderer = Rose::DBx::Object::Renderer->new(config => {db => {name => 'company', username => 'root', password => 'root', tables_are_singular => 1}}, load => 1);
  
  
  # Render a form to add new employee
  Company::Employee->render_as_form();

  # Render an object as a form
  my $e = Company::Employee->new(id => 1);
  $e->load;
  $e->render_as_form();
  
  
  # Render a link to a google map for the 'address' column
  print $e->address_for_view();


  # Render a table
  Company::Employee::Manager->render_as_table();

  # Render a table for all the employees who love 'Coding' with create, edit, and delete access
  Company::Employee::Manager->render_as_table(
    get => {query => [hobby => 'Coding']}
    order => ['first_name', 'email', 'address', 'phone'],
    create => 1, 
    edit => 1,
    delete => 1,
    searchable => ['first_name', 'address']
  );

  # Render a menu
  my $menu = Company::Employee::Manager->render_as_menu(
    order => ['Company::Employee', 'Company::Position'],
    edit => 1,
    delete => 1,
  );


  # Render a pie chart via Google Chart API
  Company::Employee::Manager->render_as_chart(
    type => 'pie',
    values => ['Coding', 'Cooking'],
    column => 'hobby',
  );

  # Render a bar chart
  Company::Employee::Manager->render_as_chart(
    type => 'bar',
    title => 'The Employee Bar Chart',
    description => 'A useful bar chart.',
    columns => ['salary', 'tax'],
    objects => [1, 2, 3],
        options => {chco => 'ff6600,ffcc00'}  # the color for each bar
  );

DESCRIPTION

Rose::DBx::Object::Renderer generates web UIs for Rose::DB::Object. It encapsulates many web conventions in the generated UIs as default behaviours. For example, email addresses are by default rendered as mailto links in tables and appropiate validation is enforced automatically in forms. These behaviours are highly extensible.

Renderer uses CGI::FormBuilder to generate forms and the Google Chart API to render charts. Template::Toolkit is used for template processing, however, Renderer can dynamically generate a full set of UIs without any templates.

RESTRICTIONS

  • Must follow the default conventions in Rose::DB::Object.

  • Limited support for database tables with multiple primary keys.

METHODS

new

To instantiate a new Renderer object:

  my $renderer = Rose::DBx::Object::Renderer->new(config => {db => {name => 'company', username => 'root', password => 'root'}}, load => 1);

Since Renderer inherits from Rose::Object, the above line is equivalent to:

  my $renderer = Rose::DBx::Object::Renderer->new();
  $renderer->config({db => {name => 'company', username => 'root', password => 'root'}});
  $renderer->load();

config

A Renderer instance inherits the default configurations in Renderer, which is accessible by:

  my $config = $renderer->config();

config accepts a hashref for configuring the Renderer object.

db

The db option is for configuring database related settings, for instance:

  $renderer->config({
    db => {
      name => 'product',
      type => 'Pg', # defaulted to 'mysql'
      host => '10.0.0.1',
      port => '5432',
      username => 'admin',
      password => 'password',
      tables_are_singular => 1,  # defines the database table name conventions
      like_operator => 'ilike',  # to perform case-insensitive LIKE pattern matching
    }
  });

template

The template option specifies the template toolkit INCLUDE_PATH and the base URL for static contents, such as javascript libraries or images:

  $renderer->config({
    ...
    template => {
      path => '../templates:../alternative',  # TT INCLUDE_PATH, defaulted to 'template'
      url => '../images',  # defaulted to 'template'
    },
  });

upload

Renderer needs a directory with write access to upload files. The upload option defines file upload related settings:

  $renderer->config({
    ...
    upload => {
      path => '../uploads',  # the upload directory path, defaulted to 'upload'
      url => '../uploads',  # the corresponding URL path, defaulted to 'upload'
      keep_old_files => 1,  # defaulted to undef
    },
  });

form

The form option defines the default behaviours of render_as_form:

  $renderer->config({
    ...
    form => {
      download_message => 'Get Current File',  # the name of the link for uploaded files
      cancel => 'Back'  # the name of the built-in 'Cancel' controller
    },
  });

table

The table option defines the default behaviours of render_as_table:

  $renderer->config({
    ...
    table => {
      search_result_title => 'Looking for "[% q %]"?',
      empty_message => 'No matching records.', 
      per_page => 25,  # number of records per table, defaulted to 15
      or_filter => 1,  # column filtering is joined by 'OR', defaulted to undef
      no_pagination => 1,  # do not display pagination
      delimiter => '/',  # the delimiter for joining foreign objects in relationship columns, defaulted to ', '
      keyword_delimiter => '\s+',  # the delimiter for search keywords, defaulted to ','
    },
  });

columns

Renderer has a built-in list of column definitions that encapsulate web-oriented conventions and behaviours. A column definition is a collection of column options. Column definitions are used by the rendering methods to generate web UIs. The built-in column definitions are store inside columns:

  my $config = $renderer->config();
  print join (', ', keys %{$config->{columns}});

For example, the column definition for 'email' would be:

  ...
  'email' => {
    required => 1,
    validate => 'EMAIL',
    sortopts => 'LABELNAME',
    comment => 'e.g. your.name@work.com',
    format => {
      for_view => sub {
        my ($self, $column) = @_;
        my $value = $self->$column;
        return unless $value;
        return qq(<a href="mailto:$value">$value</a>);}
    }
  },
  ...

We can also define new column definitions:

  $renderer->config({
    ...
    columns => {
      hobby => {
        label => 'Your Favourite Hobby',
        sortopts => 'LABELNAME',
        required => 1,
        options => ['Reading', 'Coding', 'Shopping']
     }
    },
  });

The options in each column definition are CGI::FormBuilder field definitions, with the following additions:

format

The format option is a hash of coderefs which get injected as object methods by load. For instance, based on the 'email' column definition, we can print a 'mailto' link for the email address by calling:

  print $object->email_for_view;

Similarly, based on other column definitions, we can:

  # Print the date in 'DD/MM/YYYY' format
  print $object->date_for_view;

  # Store a password in MD5 hash
  $object->password_for_update('p@$$W0rD');
  $object->save();

  # Display an image formatted in HTML <img> tags
  print $object->image_for_view;

  # Print the url of the image
  print $object->image_url;

  # Prints the file path of the image
  print $object->image_path;

We can overwrite the existing formatting methods or define new ones. For instance, we can use the HTML::Strip module to strip out HTML tags for the 'description' column type:

  use HTML::Strip;
  ...
  
  $renderer->config({
    ...
    columns => {
      description => {
        format => {
          for_update => sub {
            my ($self, $column, $value) = @_;
            return unless $value;
            my $hs = HTML::Strip->new(emit_spaces => 0);
            my $clean_text = $hs->parse($value);
            return $self->$column($clean_text);
          }
        }
      } 
    },
  });
  
  $renderer->load();
  ...
  $object->description_for_update('<html>The Lightweight UI Generator.</html>');
  $p->save();
  print $p->description;
  # prints 'The Lightweight UI Generator.'

Formatting methods are utilised by rendering methods. They take preference over the default CRUD methods. The for_create, for_edit, and for_update methods are used by render_as_form. When creating new objects, render_as_form triggers the for_create method to format the default value of a column. When rendering an existing object as a form, however, the for_edit methods are triggered to format column values. During form submissions, the for_update methods are triggered to format the submitted form field values. The for_view, for_search, and for_filter methods are used by render_as_table. The for_view methods are triggered to format column values, the for_filter methods are triggered for data filtering, and the for_search methods are triggered for keyword searches.

unsortable

This option defines whether a column is sortable. For instance, the 'password' column definition has the unsortable option set to 1. This option is used by render_as_table. Custom columns are always unsortable.

stringify

This option specifies whether a column will be stringified by the stringify_me object method.

misc

Other miscellaneous options are defined in misc:

  my $custom_config = $renderer->config();

  # Print the built-in doctype and CSS
  print $custom_config->{misc}->{html_head};

  # Change the object stringify delimiter
  $custom_config->{misc}->{stringify_delimiter} = ', '; # defaulted to space

  $renderer->config($custom_config);
  $renderer->load();

load

load uses Rose::DB::Object::Loader to load Rose::DB::Object classes dynamically. In order to take advantages of the built-in column definitions, load employs the following logic to auto-assign column definitions to database columns:

  Column name exists in the Renderer object's config?
    Yes: Use that column definition.
    No: Is the column a foreign key?
      Yes: Apply the column options designed for foreign keys.
      No: Column name matches (regex) a column definition name?
        Yes: Use the first matching column definition.
        No: Column's metadata object type exists as column definition name?
          Yes: Use that column definition.
          No: Create a custom column definition by aggregating database column information.

load accepts a hashref to pass parameters to the new and make_classes methods in Rose::DB::Object::Loader.

loader

The loader option is a hashref that gets to passed to the new method in Rose::DB::Object::Loader.

  $renderer->load({
    loader => {
      class_prefix => 'MyCompany',
    }
  });
make_classes

Similarly, the make_classes option is passed to the make_classes method.

  $renderer->load({
    make_classes => {
      include_tables => ['customer', 'product'],
    }
  });

load returns an array of the loaded classes via the make_classes method in Rose::DB::Object::Loader. However, if the Rose::DB::Object base_class for the particular database already exists, which most likely happens in a persistent environment, load will simply skip the loading process and return undef.

RENDERING METHODS

Rendering methods are exported for Rose::DB::Object subclasses to generate web UIs. Rose::DB::Object subclasses generated by calling load will import the rendering methods automatically. However, we can also import the rendering methods into custom Rose::DB::Object subclasses:

  # For object classes
  package Company::Employee
  use Rose::DBx::Object::Renderer qw(:object);
  ...
   
  # For manager classes 
  package Company::Employee::Manager
  use Rose::DBx::Object::Renderer qw(:manager);
  ...

Obviously, the rendering methods in the custom subclasses does not take advantages of the built-in column definitions and formatting methods. However, you can still replicate those behaviours by using the fields option in render_as_form, or the columns option in render_as_table, as well as hand crafting the formatting methods.

The following is a list of common parameters for the rendering methods:

template

The template file name. When it is set to 1, rendering methods will try to find the default template based on the rendering method name. For example:

  Company::Employee->render_as_form(template => 1);
  # tries to use the template 'form.tt'

  Company::Employee::Manager->render_as_table(template => 1);
  # tries to use the template 'table.tt'
template_url

The "Template Toolkit"'s INCLUDE_PATH.

template_path

An URL path variable that is passed to templates.

template_options

Optional parameters to be passed to "Template Toolkit". This is not applicable to render_as_form.

html_head

This is specifying custom DOCTYPE, CSS, or Javascript for the particular rendering method.

prefix

Define a prefix for the UI, e.g.:

  Company::Employee::Manager->render_as_table(prefix => 'employee_admin');

A prefix should be URL friendly. Adding a prefix can prevent CGI param conflicts when rendering multiple UIs of the same class on the same web page.

title

Define a title for the UI, e.g.:

  Company::Employee::Manager->render_as_table(title => 'Employee Directory');
description

Define a short description for the UI, e.g.:

  Company::Employee::Manager->render_as_table(description => 'Here you can view, search, and manage your employee details.');
no_head

When set to 1, rendering methods will not include the default DOCTYPE and CSS. This is useful when rendering multiple UIs in the same page.

output

When set to 1, the rendering methods will return the rendered UI instead of printing it directly. For example:

  my $form = Company::Employee->render_as_form(output => 1);
  print $form->{output};
extra

A hashref of additional template variables. For example:

  Company::Employee->render_as_form(extra => {hobby => 'basketball'});

  # to access it within a template:
  [% extra.hobby %]

Additionally, render_as_form and render_as_table also respectively accepts the all the options available inside the Renderer config's form and table hash.

render_as_form

render_as_form renders forms and handles its submission.

  # Render a form for creating a new object
  Company::Employee->render_as_form();
  
  # Render a form for updating an existing object
  my $e = Company::Employee->new(id => 1);
  $e->load;
  $e->render_as_form();
order

order is an arrayref for the order of the form fields.

fields

A hashref to specify the CGI::FormBuilder field definitions for this particular render_as_form call. Any custom fields must be included to the order arrayref in order to be shown.

  Company::Employee->render_as_form(
    order => ['username', 'favourite_cuisine'],
    fields => {
    favourite_cuisine => {required => 1, options},
  });

Please note that Renderer has a built-in column type called 'confirm_password', where its default validation tries to match a field named 'password' in the form.

queries

An arrayref of query parameters to be converted as hidden fields.

  Company::Employee->render_as_form(
    queries => {
    'rm' => 'edit',
    'favourite_cuisine' => ['French', 'Japanese']
  });

Please note that when a prefix is used, all fields are renamed to 'prefix_fieldname'.

controllers and controller_order

Controllers are essentially callbacks. We can add multiple custom controllers to a form. They are rendered as submit buttons. controller_order defines the order of the controllers, in other words, the order of the submit buttons.

  my $form = Employee::Company->render_as_form(
    output => 1,
    controller_order => ['Hello', 'Good Bye'],
    controllers => {
      'Hello' => {
        create => sub {
          return if DateTime->now->day_name eq 'Sunday';
          return 1;
        },
        callback => sub {
          my $self = shift;
          if (ref $self)
          {
            return 'Hello ' . $self->first_name;
          }
          else
          {
            return 'No employee has been created'.
          }
      },
      'Good Bye' => \&say_goodbye
    });

  if (exists $form->{controller})
  {
    print $form->{controller};
  }
  else
  {
    print $form->{output};
  }

  sub say_goodbye
  {
    return 'Good Bye';
  }

Within the controllers hashref, we can set the create parameter to 1 so that the object is always inserted into the database before running the custom callback. We can also point create to a coderef, in which case, the object is inserted into the database only if the coderef returns true.

Similarly, when rendering an object instance as a form, we can update the object before running the custom callback:

  ...
  $e->render_as_form(
    controllers => {
      'Hello' => {
        update => 1,
        callback => sub{...};
      }
  );

Another parameter within the controllers hashref is hide_form, which informs render_as_form not to render the form after executing the controller.

form

A hashref that gets passed to the CGI::FormBuilder constructor.

validate

Parameters for the CGI::FormBuilder's validate method.

jserror

When a template is used, render_as_form sets CGI::FormBuilder's jserror function name to 'notify_error' so that we can always customise the error alert mechanism within the template (see the included 'form.tt' template).

javascript_code

A string with javascript code to be added to the template

render_as_form passes the following list of variables to a template:

  [% self %] - the calling object instance or class
  [% form %] - CGI::FormBuilder's form object
  [% field_order %] - The order of the form fields
  [% form_id %] - the form id
  [% title %] - the form title
  [% description %] - the form description
  [% html_head %] - the html doctype and css defined in $Rose::DBx::Object::Renderer::CONFIG->{misc}->{html_head}
  [% no_head %] - the 'no_head' option
  [% extra %] - custom variables
  [% cancel %] - the name of the 'Cancel' controller
  [% javascript_code %] - javascript code 
  [% template_url %] - The template url defined in $Rose::DBx::Object::Renderer::CONFIG->{template}->{url}

render_as_table

render_as_table renders tables for CRUD operations.

or_filter

render_as_table allows columns to be filtered via URL. For example:

  http://www.yoursite.com/yourscript.pl?first_name=Danny&last_name=Liang

returns the records where 'first_name' is 'Danny' and 'Last_name' is 'liang'. By default, column queries are joined by "AND", unless or_filter is set to 1.

columns

The columns parameter can be used to define custom columns, which do not exist in the underlying database table

  Company::Employee::Manager->render_as_table(
    columns => {'custom_column' => 
      label => 'Total',
      value => {
        1 => '100', # the 'Total' is 100 for object ID 1
        2 => '50'
      },
  });
order

order accepts an arrayref to define the order of the columns to be shown. The order parameter also determines which columns are allowed to be filtered via url.

searchable

The searchable option enables keyword search in multiple columns, including the columns of foreign objects:

  Company::Employee::Manager->render_as_table(
    get => {with_objects => [ 'position' ]},
    searchable => ['first_name', 'last_name', 'position.title'],
  );

A search box will be shown in rendered table. The CGI param of the search box is called 'q', for example:

  http://www.yoursite.com/yourscript.pl?q=danny

We can also use multiple keywords seperated by commas. The default keyword delimiter is defined in:

  $Rose::DBx::Object::Renderer::CONFIG->{table}->{keyword_delimiter}
searchable
get

get accepts a hashref to construct database queries. get is directly passed to the get method of the manager class.

  Company::Employee::Manager->render_as_table(
    get => {
          per_page = 5,
      require_objects => [ 'position' ],
      query => ['position.title' => 'Manager'],
    });
controllers and controller_order

The controllers parameter works very similar to render_as_form. controller_order defines the order of the controllers.

  Company::Employee::Manager->render_as_table(
        controller_order => ['edit', 'Review', 'approve'],
    controllers => {
      'Review' => sub{my $self = shift; do_something_with($self);}
      'approve' => {
            label => 'Approve',
        hide_table => 1,
        queries => {approve => '1'}, 
        callback => sub {my $self = shift; do_something_else_with($self);
      }
    }
  );

Within the controllers hashref, the queries parameter allows us to define custom query strings for the controller. The hide_table parameter informs render_as_table not to render the table after executing the controller.

create

This enables the built-in 'create' controller when set to 1.

  Company::Employee::Manager->render_as_table(create => 1);

Since render_as_form is used to render the form, we can also pass a hashref to manipulate the generated form.

  Company::Employee::Manager->render_as_table(
    create => {title => 'Add New Employee', fields => {...}}
  );
edit

Similar to create, edit enables the built-in 'edit' controller for updating objects.

delete

When set to 1, delete enables the built-in 'delete' controller for removing objects.

queries

Similar to the queries parameter in render_as_form, queries is an arrayref of query parameters, which will be converted to query strings. Please note that when a prefix is used, all query strings are renamed to 'prefix_querystring'.

url

Unless a url is specified in url, render_as_table will resolve the self url using CGI.

javascript_code

A string with javascript code to be added to the template

ajax and ajax_template

These two parameters are designed for rendering Ajax-enabled tables. When ajax is set to 1, render_as_table tries to use the template 'table_ajax.tt' for rendering, unless the name of the template is defined in ajax_template. render_as_table also passes a variable called 'ajax' to the template and sets it to 1 when a CGI param named 'ajax' is set. We can use this variable in the template to differentiate whether the current CGI request is an ajax request or not.

no_pagination

The pagination will not be rendered if this option is set to 1.

Within a template, we can loop through objects using the [% table %] variable. Alternatively, we can use the [% objects %] variable.

render_as_table passes the following list of variables to a template:

  [% table %] - the hash for the formatted table, see the sample template 'table.tt' 
  [% objects %] - the raw objects returned by the 'get_object' method
  [% column_order %] - the order of the columns
  [% template_url %] - The template URL defined in $Rose::DBx::Object::Renderer::CONFIG->{template}->{url}
  [% table_id %] - the table id
  [% title %] - the table title
  [% description %] - the table description
  [% class_label %] - title case of the calling package name
  [% no_pagination %] - the 'no_pagination' option
  [% q %] - the keyword query for search
  [% query_string %] - a hash of URL encoded query strings
  [% query_hidden_fields %] - CGI queries converted into hidden fields; it is used by the keyword search form
  [% param_list %] - a list of CGI param names with the table prefix, e.g. the name of the keyword search box is [% param_list.q %]
  [% searchable %] - the 'searchable' option
  [% sort_by_column %] - the column to be sorted 
  [% html_head %] - the html doctype and css defined in $Rose::DBx::Object::Renderer::CONFIG->{misc}->{html_head}
  [% no_head %] - the 'no_head' option
  [% extra %] - custom variables
  [% javascript_code %] - javascript code
  [% ajax %] - the ajax variable for checking whether the current CGI request is a ajax request
  [% url %] - the base url

render_as_menu

render_as_menu generates a menu with the given list of classes and renders a table for the current class. We can have fine-grained control over each table within the menu. For example, we can alter the 'date_of_birth' field inside the 'create' form of the 'Company::Employee' table inside the menu:

  Company::Employee::Manager->render_as_menu (
    order => ['Company::Employee', 'Company::Position'],
    items => {
    'Company::Employee' => {
      create => {
            fields => {date_of_birth => {required => 1}}
          }
    }
    'Company::Position' => {
          title => 'Current Positions',
      description => 'important positions in the company'
    }},
    create => 1,
    edit => 1,
    delete => 1,
  );
order

The order parameter defines the list of classes to be shown in the menu as well as their order. The current item of the menu is always the calling class, i.e. Company::Employee::Manager in the example.

items

The items parameter is a hashref of parameters to control each table within the menu.

create, edit, delete, and ajax

These parameters are shortcuts which get passed to all the underlying tables rendered by the menu.

render_as_menu passes the following list of variables to a template:

  [% template_url %] - The template URL defined in $Rose::DBx::Object::Renderer::CONFIG->{template}->{url}
  [% menu_id %] - the menu id
  [% title %] - the menu title
  [% description %] - the menu description
  [% items %] - the hash for the menu items
  [% item_order %] - the order of the menu items
  [% current %] - the current menu item
  [% content %] - the output of the table
  [% extra %] - custom variables
  [% hide %] - whether the menu should be hidden
  [% html_head %] - the html doctype and css defined in $Rose::DBx::Object::Renderer::CONFIG->{misc}->{html_head}
  [% no_head %] - the 'no_head' option

render_as_chart

render_as_chart renders pie, line, and vertical bar charts via the Google Chart API.

type

This can be 'pie', 'bar', or 'line', which maps to the Google chart type (cht) 'p', 'bvg', and 'ls' respectively.

column and values

These two parameters are only applicable to pie charts. column defines the column of the table in which the values are compared. The values parameter is a list of values to be compared in that column, i.e. the slices.

columns and objects

These two parameters are only applicable to bar and line charts. columns defines the columns of the object to be compared. The objects parameter is a list of object IDs representing the objects to be compared.

options

A hashref for specifying Google Chart API options, such as the chart type, size, labels, or data. This hashref is serialised into a query string.

engine

Accepts a coderef to plug in your own charting engine.

render_as_chart passes the following list of variables to a template:

  [% template_url %] - The template URL defined in $Rose::DBx::Object::Renderer::CONFIG->{template}->{url}
  [% chart_id %] - the chart id
  [% title %] - the chart title
  [% description %] - the chart description
  [% chart %] - the chart
  [% options %] - the 'options' hash
  [% extra %] - custom variables
  [% html_head %] - the html doctype and css defined in $Rose::DBx::Object::Renderer::CONFIG->{misc}->{html_head}
  [% no_head %] - the 'no_head' option

OBJECT METHODS

Apart from the formatting methods injected by load, there are several lesser-used object methods:

delete_with_file

This is a wrapper of the object's delete method to remove any uploaded files associated:

  $object->delete_with_file();

stringify_me

Stringifies the object instance, e.g.:

  $object->first_name('John');
  $object->last_name('Smith');

  print $object->stringify_me;
  # prints 'John Smith';

stringify_class

Stringifies the class name:

  print Company::Employee->stringify_class;
  # prints 'company_employee'

SAMPLE TEMPLATES

There are four sample templates: 'form.tt', 'table.tt', 'menu.tt', and 'chart.tt' in the 'templates' folder of the TAR archive.

SEE ALSO

Rose::DB::Object, CGI::FormBuilder, Template::Toolkit, http://code.google.com/apis/chart/

AUTHOR

Xufeng (Danny) Liang (danny.glue@gmail.com)

COPYRIGHT & LICENSE

Copyright 2008 Xufeng (Danny) Liang, All Rights Reserved.

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.