User:Ringmaster/FormUI

From Habari Project

Jump to: navigation, search

FormUI is a class for easily creating forms that mesh well with the Habari admin design and easily handling their validation and processing.

FormUI is not meant to be a panacea for form construction, especially in terms of styling, but it allows Habari the flexibility of modifying many internal forms on the fly from plugins without manipulating the HTML and form results directly.

The operations provided by FormUI and its related classes should be simple. Most common controls on forms should be created with the least effort. Required values for controls should suffice in most cases, but some extra commands should allow more (but not necessarily absolute) flexibility.

Contents

Using FormUI

Create a Form

The basis of creating forms with FormUI starts with creating the form object.

A form requires a unique identifier to distinguish it from other forms that might be submitted on the page.

To create and output a basic form object:

$myform = new FormUI('my_identifier');
$myform->out();

This creates a form with no controls and the id "my_identifier". This form could be addressed in javascript using jQuery as $('#my_identifier').

The ->out() method on the form causes the form both to be displayed and to be processed if it is submitted.

Using the class name for an identifier

Many plugins in core pass the class name as the identifier for a new form:

$myform = new FormUI( strtolower( get_class( $this ) ) );

This is just an elaborate way of ensuring a unique form identifier between each plugin.

A reason to use this as an identifier is in case the plugin is copied to a new directory and given a new class name, the two plugin forms (the original and the copy) will use different identifiers. A reason not to use this as an identifier is if javascript addresses the id of the form (which is the identifier), which would be more difficult to do if the identifier was allowed to vary.

Suffice to say that a unique string is all that's necessary, and how it is obtained is usually immaterial.

Add a Control

For a form to be useful, it should contain at least one control, and a method for the form to submit.

In this case, a text field has been added to the form, along with a save button:

$myform = new FormUI('my_identifier');
$myform->append(FormControlLabel::wrap('Firstname:', FormControlText::create('firstname', 'user:username')));
$myform->append(FormControlSubmit::create('save')->set_caption('Save'));
$myform->out();

The append() method of the FormUI class (or any FormContainer class descendant) accepts a FormControl as a parameter. There are many types of FormControls available, each associated with the type of control they create.

Every FormControl descendant has a create() method, which is a shortcut for using the new keyword. The create() method returns the created instance of the control in a fluent style. The fluent interface allows the method set_caption() to be called on the submit button control, which sets the text of the button when it is rendered.

FormControls can also be a kind of container, a FormContainer. FormContainers can hold other controls. A FormControlLabel is a special kind of FormContainer that has the wrap() method, which allows a label to be applied to a single control, as shown for the "firstname" control, above.

Arrange a Control

function get_myform() {
  $myform = new FormUI('my_identifier');
  $myform->append(FormControlLabel::wrap('First name:', FormControlText::create('firstname', 'user:username'));
  $myform->append(FormControlSubmit::create('save')->set_caption('Save') );
  return $myform;
}
$myform = get_myform();
$myform->insert($myform->save, FormControlLabel::wrap('Last name:', FormControlText::create('lastname', 'user:lastname')));
$myform->append(FormControlSubmit::create('cancel')->set_caption('Cancel'));


The insert() method allows you to insert a control (whether existing or new) before a different control that is already in the form.

The name of a control (specified when the control is created) can be used as a parameter to the form object to get the instance of that control. For example, if a button control you've named "save" (like in the code above) has been added to a form object in the variable $form, then you can access that control object from the property $form->save.

Move a Control

Both of the following move $myform->lastname before $myform->firstname.

$myform = get_myform();
$myform->move_before($myform->firstname, $myform->lastname);
$myform->move_after($myform->lastname, $myform->firstname);

Remove a Control

$myform = get_myform();
$myform->firstname->remove();

Insert a Control

You may find the need to place a control within another control, specifically adding a control to a fieldset.

$myform->control->move_into($myform->control);

Modify a Control

Controls can be easily modified after adding them:

$myform = new FormUI('my_identifier');
$myform->append(FormControlLabel::wrap('Firstname:', FormControlText::create('firstname', 'user:username') );
$myform->label_for_firstname->set_label('First Name:');
$myform->append(FormControlSubmit::create('save')->set_caption('Save') );
$myform->out();

The line $myform->label_for_firstname->set_label('First Name:'); changes the label of the control to 'First Name:'. Notice that the name of the label control is "label_for_" prepended to the name of the labeled control. The control's method of set_label() provides direct access to change the label text.

Controls should not use names that correspond to FormUI members or they will be inaccessible. For example, if you create a form, $form, and FormUI defines $form->style as the style string to assign to the form tag, then you shouldn't name a control 'style'. As yet, FormUI has no members.

The ->append() method also returns the instance of the control so that it can be modified. Assign the return value to a variable. This variable will contain the same value as the name of the control used as a property on the FormUI object:

$myform = new FormUI('my_identifier');
$firstname_label = $myform->append(FormControlLabel::wrap('Firstname:', FormTextControl::create('firstname', 'user:username')) );
 
// This:
$myform->label_for_firstname->set_label('First Name:');
 
// Is the same as this:
$firstname_label->set_label('First Name:');

Useful Hooks

Habari provides specific hooks for certain forms. For example the publish form can be modified using action_form_publish() and the comment form can be modified using action_form_comment().

Many plugins that add a content type need to alter the post publish form to add fields specific to that content type. An example might be a blogroll plugin that adds a field for a URL.

public function action_form_publish( $form, $post)
{
  if ( $form->content_type->value == Post::type( 'blogroll' ) ) {
    $form->append(
      FormControlLabel::wrap( 
        _t('URL'), 
        $url = FormControlText::create(
          'url', 
          'null:null', 
          array(
            'tabindex' => 2,
          )
        )
      )
    );
    $form->url->value = $post->info->url;
    $form->url->move_after($form->title);
  }
}

Adding a class to the comment form inputs might be useful for themes.

public function action_form_comment( $form )
{
  $form->email->add_class('comment_email');
}

Additionally, Habari provides hooks that allow for the modification of named forms, using action_modify_form_{name}(), where {name} is the name of the form, without the braces.

To modify a form named register, you would use the following code.

public function action_modify_form_register( $form )
{
  $events = array(
    'pony ride',
    'coffee drinking',
    'HabariBar'
  );
  $form->append(FormControlLabel::wrap('Event:', FormCotnrolSelect::create('event'));
  $form->event->set_options($events);
}

Finally, and even more generally, any form can be modified using action_modify_form().

Special FormControl Properties

There are special properties of the FormControl that are used in the form processing. These values can be output in the template, but they are also used by FormUI internally.

value
This property of a FormControl contains the value that will be or is already stored in the database for this control, whether the value was read from the database or submitted by a user.

Understanding Control Templates

Every control created with FormUI uses a discrete set of templates for rendering. By default, there is a short fallback list of templates that each template uses, consisting of only two templates:

  • control.{controltype}.php
  • control.php

The default control.php template doesn't render a control at all, but an HTML comment that explains what the control is that was trying to render but failed.

The {controltype} is extracted from the class name of the FormControl, being what comes after the "FormControl" part of the class name. For example, the first default template for a FormControlSelect is control.select.php

Just like with themes, the first matching template found is the one used to render the control.

Using Alternate Control Templates

Habari ships with alternate control templates for some controls where it is obvious that a control might need to be rendered differently in certain situations.

The primary example is the label control. It is oftentimes necessary to render the label on the right side of the control rather than the left. Habari supplies a template that will do this. Specifying the alternate template will cause the control to render with that template instead of the default.

This code causes the label to render with a different template so that the label is to the right of the control it contains:

$label = $form->append(FormControlLabel::wrap('Enabled', FormControlCheckbox::create('enabled')));
$label->set_template('control.label.onright');

It is possible to set multiple templates to try using an array of templates. The first template found will be used. FormUI automatically adds the default fallback templates to the control when new templates are set. If an explicitly specified template is not found, the defaults will still be used.

Overriding Existing Control Templates

While Habari ships with templates for its various controls, it is possible to add new templates that completely change the way these controls are presented by calling Pluggable's add_template() method. Since both Theme and Plugin classes extends Pluggable, the method can be called from within both.

Adding this method to a plugin would replace the existing control.text.php template with a custom one in the plugin directory:

//Within our plugin
public function action_form_publish( $form, $post)
{
    $this->add_template('control.text', dirname(__FILE__).'/control.text.replacement.php');
}

Overriding a default template like this will cause any control using this template to use the replacement template instead.

Habari's default FormUI templates are in habari/system/controls/templates/. Do not modify these as the modifications will be lost when you upgrade Habari. Instead, place a copy of the control template you wish to modify in your theme or plugin directory and make the necessary modifications.

See the Customizing forms section in Creating a Custom Theme for more details.

Adding Custom Control Templates

It is possible to combine the above techniques to create custom templates that are used only on specific instances of a control.

While Habari ships with default and alternate templates for the various controls, it is possible to add new templates to completley change the way these controls are presented by calling Pluggable's add_template() method. Since both Theme and Plugin classes extends Pluggable, the method can be called from within both.

The following code adds a new template for use by one of the text fields added to the configuration form:

//Within our plugin
public function action_form_publish( $form, $post)
{
    $this->add_template('control.text.custom', dirname(__FILE__).'/control.text.custom.php');
    $myform = new FormUI('my_identifier');
    $firstname = FormControlText::Create('firstname', 'user:username')->set_template('control.text.custom');
    $myform->append(FormControlLabel::wrap('Firstname:', $firstname));
    $myform->append(FormControlSubmit::create('save')->set_caption('Save') );
    $myform->out();
}

Handle Form Submissions

When the ->out() method is called on the form object, it is rendered to the page. If the form was submitted back to the page (the default submission destination for FormUI forms), then the ->out() method processes the form as well.

There are two ways that FormUI can handle form submissions, both of which can be used simultaneously: FormStorage and Post-Processing.

FormStorage

When created, every control can be assigned a storage location. If the form is submitted successfully, then the values supplied for the controls are saved to the specified location automatically. No further processing is required to save those control values to their respective locations.

Consider the following form:

$myform = new FormUI('my_identifier');
$myform->append(FormControlLabel::wrap('Firstname:', FormControlText::create('firstname', 'user:username')));
$myform->append(FormControlSubmit::create('save')->set_caption('Save'));
$myform->out();

In the case of this form, three controls were created: One each of type FormControlLabel, FormControlText, and FormControlSubmit. FormControlSubmit and FormControlLabel do not need to save a value, and so their constructors have not been supplied a value for a storage method. FormControlText, as with all FormControl descendants, has as its second parameter the storage mechanism that the control will employ. This mechanism is always either an object instance implementing FormStorage or a string in the format "type:location", where the type dictates the use of the location:

user
This type indicates that the value of the control will be stored in the userinfo table for the current user using the name "{$location}"
option
This type indicates that the value of the control will be stored in the options table using the name "{$location}"
action
This type indicates that the value of the control will be passed to the indicated plugin action named "{$location}".
session
This type indicates that the value of the control will be stored in the $_SESSION superglobal with the key named ["{$location}"]["{$controlname}"]. The whole set of values can be obtained via Session::get_set("{$location}").
null
This type indicates that the value of the control should not be automatically stored. The location in this case is ignored.

All text-based storage locations are converted internally into ControlStorage objects, which implement FormStorage. Omitting the storage location sets the storage location to null, which causes the control not to save its value.

It is a good practice to use a prefix to the location so that all of a form's settings are stored under a grouping. For example, "option:myplugin__username" would do well to group all of the options related to "myplugin".

If the type is omitted from the storage mechanism and a string is supplied, then "option:" is implied.

If the storage parameter is an object instance implementing FormStorage, then the field_save() method of the object is used to store the control value. Likewise, when obtaining existing values, the field_load() method of the FormStorage object is used. When storing values in a FormStorage object, the key of the value stored is the name of the control.

There are several core classes that implement FormStorage, which make it possible to save data directly to objects that use those classes. These classes include:

  • Block
  • Post
  • Terms
  • User

Please refer to the class documentation for the field_load() and field_save() methods on those classes for their exact behaviors. Consider the following code:

$user = User::get_by_id($some_id);
$myform = new FormUI('my_identifier');
$myform->append(FormControlLabel::wrap('Firstname:', FormControlText::create('firstname', $user)));
$myform->append(FormControlSubmit::create('save')->set_caption('Save'));
$myform->out();

When the form produced by the above code is submitted successfully, the value of the firstname control is automatically stored in $user->info->firstname for the user stored in $user.

The value stored by the control is completely dictated by the control. A FormControlText stores a plain text value, but a FormControlCheckbox stores a boolean value. Other stored values may be more complex or even compound in the case that the control is actually a set of controls combined together (such as a grid control, or a control to let you select and upload a file temporarily before submitting the whole form).

Initial Control Values

The storage method specified is also used to obtain the default value of the control specified. If a control should have a different initial value than the one in the storage location (or if the location is "null"), then the value of the control must be set, like in this example:

$myform = new FormUI('my_identifier');
$myform->append(FormControlLabel::wrap('Firstname:', FormControlText::Create('firstname', null /* null is implicit if omitted */)));
$myform->firstname->value = 'Bob';
$myform->append(FormControlSubmit::create('save')->set_caption('Save'));
$myform->out();


The following claim needs to be revised per the revisions to FormUI: Reading the value of the control immediately after it is assigned may yield somewhat unexpected results. If the form was submitted, then the value assigned in code will not be the value returned from that control's value property.

Consider this example, which assigns the value of "Alice" to the firstname control, and then assumes that a user may submit "Bob" as the field's value. If so, the code adds a "Lastname" field to the form:

$myform = new FormUI('my_identifier');
$myform->append(FormControlLabel::wrap('Firstname:', FormControlText::Create('firstname')));
$myform->firstname->value = 'Alice';
 
if($myform->firstname->value == 'Bob') {
  $myform->append('text', 'lastname', 'user:lastname', 'Lastname:');
}
 
$myform->append('submit', 'save', 'Save');
$myform->out();

Post-Processing

The storage mechanisms described above may be fine if the values of the form simply need to be stored in the database, but if some processing needs to happen as the result of a submission, the ->on_success() method should be used:

$myform = new FormUI('my_identifier');
$myform->append(FormControlLabel::wrap('Firstname:', FormControlText::create('firstname', 'user:username'));
$myform->append(FormControlSubmit::create('save')->set_caption('Save');
$myform->on_success(array($this, 'my_callback'), 'Bob');
$myform->out();

The first parameter of the ->on_success() method must be one of:

  • A valid PHP callback.
  • A closure or lambda.
  • The name of a plugin filter.

For example, the on_success parameter is "my_callback" as in the above code. Upon successful submission FormUI looks for a function named "my_callback". If found, then my_callback() is called with the form object as the first parameter and the control itself as the second parameter. Any additional parameters to ->on_success() are passed as additional parameters to the callback.

The callback may return false, which will cause the form to re-display. Alternatively, the callback may return a value, which is displayed instead of the form. The form can be included in the returned value by including the result of $form->get() as part of the returned string value.

All of the FormStorage field_save() methods are triggered for all controls prior to calling the callback.

Here is an example of the callback method described above:

function my_callback($form, $special_name) {
  if($form->firstname->value == $special_name) {
    // Do something special if the first name is the one
    // specified in the call to $form->on_success()
  }
  // Display the form normally with the default confirmation message
  return false;
}

For a PHP callback, if the array is defined as array($this, 'my_callback') then the callback executes a method of the plugin's class named my_callback(). Refer to the PHP documentation on callbacks for more clarity on this idea.

If the callback does not exist as a callable function, then the plugin filter with the same name (in our example, "my_callback") is executed, which in turn executes every plugin's method named "filter_my_callback". Remember that this is a plugin filter, and needs to be named as such (with "filter_" prepended), not just share the identical name supplied as the callback string.

The first parameter of the filter is the same as the return value of the function callback above. If the default form and confirmation should be displayed, return false. If something else should be displayed instead, return the appropriate HTML. If a prior filter has handled the callback and returned HTML, new values should be added to that HTML, rather than simply returning false.

The second parameter of the filter is an instance of the form object itself. Additional parameters to the ->on_success() method are passed as additional parameters of the filter function.

Here is an example:

class myplugin extends Plugin {
...
  function filter_my_callback($response, $form, $special_name) {
    if($form->firstname->value == $special_name) {
      // Do something if the first name is the one
      // specified in the call to $form->on_success()
    }
    // Perform normal save routines, but account for
    // other plugins that might want to override this
    return $save_form || false;
  }
}

Changing the Form Action and Other Properties of Controls

By default, the form will be submitted to the same page that it was rendered on. This can be overridden by providing an "action" value in a call to the form's set_properties() method.

$myform = new FormUI('my_identifier');
$myform->append(FormControlLabel::wrap('Firstname:', FormControlText::Create('firstname', null /* null is implicit if omitted */)));
$myform->append(FormControlSubmit::create('save')->set_caption('Save'));
// Set the submission URL
$myform->set_properties(array('action' => URL::get('admin', 'page=users' )) );
$myform->out();

In this case, we use URL::get() to let Habari construct the appropriate URL for us.

It is possible to set the properties of any control using the set_properties() method of the control. The properties array may also be defined when the control is created by providing a third parameter to the constructor:

$myform->append(FormControlSubmit::create('save', null, array('data-foo'=>'bar'))->set_caption('Save'));

This creates a "data-foo" attribute in the input element for this control with the value "bar".

Setting Control CSS Classes

It is possible to set the class or classes of a control directly using the properties array, but it is usually preferable to set them using the add_class() method:

$myform->append(FormControlSubmit::create('save')->set_caption('Save')->add_class('button'));

Using add_class() allows the code to add new classes without replacing existing ones. The add_class() method accepts one or more classes as a space-delimited string, or multiple classes as an array of strings. When the control is rendered, all of the classes that were added are included in the "class" attribute of the tag, separated by spaces.

It is also possible to use the remove_class() method to remove a class. The arguments to this method are the same as for add_class(). If a removed class doesn't exist, an error is not thrown.

Using Form Values

It might not always be apparent where a form value is stored. It would be useful to code the storage technique and location used for a control once, and then rely on the control values for use in the application of the code. Here is a way to do that:

class MyPlugin extends Plugin {
...
  // A function to create and not display the form:
  private function get_form() {
    // Create a new form:
    $form = new FormUI('myplugin');
    // Add a new field for "about" that is stored in Options::get('about'):
    $form->append('textarea', 'about', 'option:about', 'About this blog:');
    // Return the form:
    return $form;
  }
...
  // A function to display the form in the plugin config:
  function function action_plugin_ui($plugin_id, $action) {
    if ($plugin_id == $this->plugin_id()){
      switch ($action){
        case 'Configure' :
          $this->get_form()->out();
          break;
      }
    }
  }
...
  // A theme function to output the "about" information:
  function theme_about() {
    // Instead of this:
    //   return Options::get('about');
    // This allows access directly from the form:
    return $this->get_form()->about->value;
  }
...
}

This example might seem absurd at first, since it produces what seems like additional work, but if the form is more complex and draws its default values from multiple storage locations or custom settings, it might be easier to centralize the form creation without outputting it, then use the form to gather the required field data as requested.

Create a Custom Control

All form components must derive from the FormControl class. Controls should be named "FormControl{$something}" so that they can be used with the easy creation method.

When a FormControl is output to the page by FormUI, FormControl uses the FormUI's Theme object to render a template specified by the descendant control.

If no ->get() method is supplied by the new FormControl class, then the {$something} part of the control's classname is used to derive the template to use to display that control. The template name is "control.{$something}".

To use a template other than the derived one, use the ->set_template() method within the ->_extend() method:

class FormControlCustom extends FormControl {
  function _extend() {
    // Tell the theme what template to render for this control
    $this->set_template('control.custom.mynewcustomcontrol');
  }
  ...
}

If you would like to send additional variables to the template upon rendering the control, assign them to the ->vars array:

class FormControlCustom extends FormControl {
  ...
  function get() {
    // Provide additional variables to the template to render for this control
    $this->vars['custom_data'] = 'foo';
    return parent::get();
  }
}

Where to make these calls and assignments depends on whether subsequent uses of the control should be allowed to override those values. Setting properties and templates in the _extend() method (which is called immediately after the constructor) allows plugins to override those values. Setting them in the get() method (which is called only when the control is intended to be rendered) makes it more likely the these values will override anything set elsewhere by plugins.

Field Validation

It may be necessary to limit the values submitted via certain controls to a range of certain values. When the submission falls outside that range, an error messages should be displayed, and the form should fail to submit (although the previously submitted values should appear to the user for alteration).

FormControls all have an add_validator() method which accepts a string containing the name of an existing FormValidator method, or an array pointing to a custom validator.

For example, to validate a text control which should contain an email address:

$email_control = FormControlText('email', 'option:email' );
$form->add( FormControlLabel::wrap(_t('Enter your email address'), $email_control));
$email_control->add_validator( 'validate_email' );

validate_email is a method in the core FormValidator class. The value of the form submission is passed to this method for validation. If the validator returns an error, this error is displayed in the form and the form submission fails.

Validators may accept additional parameters. For example, the 'validate_regex' validator accepts a regular expression as an additional parameter, which should match the value submitted or cause the form to fail.

Applying multiple validators to a field causes all validators to be processed for that field. If any validator fails, then the form fails. Note that in the above example, the 'validate_email' validator does not require a value. In other words, if the field is submitted with a blank value, the validator passes. In order to require a value, the 'validate_required' validator should be applied to a field.

To validate based on an OR situation, such as when a field should be either a number or a name, a custom validator should be created.

A list of validators supplied by the core FormValidator class and the parameters they accept appear in a separate section, below.

Validators are stored on the control in an array that is indexed by the function used to do the validation. Therefore, there can only be one with any particular name. So if you add the same function a second time, it will overwrite the first. For instance, if you wanted to change the validation message on the comment box in the default comment form you could use:

$form->cf_content->add_validator( 'validate_required',_t ('Your custom message.'));
 
//Use the corresponding label in your message
$form->cf_content->add_validator( 
    'validate_required',
    _t ('Your custom message about %s here', array($form->cf_content->label))
);

Creating Your Own Validators

FormUI supports the use of custom validation functions.

If the validator returns an empty array, that implies that no validation errors were encountered. FormUI will pass the FormControl value, the FormControl itself, and the entire FormUI form to the validator.

Multiple validators can be applied to a single control. Multiple errors on a single control are merged for output.

As an example of a custom validator, the SimpleFileSilo plugin defines a custom mkdir_validator() method that ensures that a directory name specified for creation via the silo is valid:

class SimpleFileSilo extends Plugin implements MediaSilo {
...
$dir_text_control= $form->append( FormControlText::create('directory' ) );
$dir_text_control->add_validator( array( $this, 'mkdir_validator' ) );
...
  public function mkdir_validator( $dir, $control, $form ) {
    $dir= preg_replace( '%\.{2,}%', '.', $dir );
    $path= preg_replace( '%\.{2,}%', '.', $form->path->value );
    $dir= $this->root . ( $path == '' ? '' : '/' ) . $path . '/'. $dir;
 
    if ( ! is_writable( $this->root . '/' . $path ) ) {
      return array(_t("Webserver does not have permission to create directory: {$dir}."));
    }
    if ( is_dir( $dir ) ) {
      return array(_t("Directory: {$dir} already exists."));
    }
 
    return array();
  }
...
}

Using all of the values passed into the validator, it is possible to create validators that depend on the values of more than one control. For example, one might create a dropdown control that changes the validation requirements of a subsequent text control:

class myplugin extends Plugin {
...
  function output_form() {
    $myform = new FormUI('my_form');
    // Create a dropdown to select login method type:
    $myform->append('select', 'logintype', 'user:logintype', 'Login type to use:');
    $myform->logintype->options = array('user id #', 'user name');
 
    // Create a login field:
    $myform->append('text', 'login', 'user:login', 'Login:');
    $myform->login->add_validator( array( $this, 'validate_login' ) );
 
    // Create a save button:
    $myform->append('save', new FormControlSubmit('Save') );
    $myform->out();
  }
...
  function validate_login( $login, $control, $form ) {
    if($form->logintype->value == 'user id #') {
      if( !is_numeric( $login ) ) {
        return array(_t('The login id must be numeric.'));
      }
    }
    else {
      if( is_numeric( $login ) ) {
        return array(_t('The login must be your user name.'));
      }
    }
    return array();
  }
}

Remove a validator

There may be cases where a FormUI object is passed by reference to a plugin filter. This will allow a plugin to alter the fields of a form. In this case, it may be useful to remove a validator that had previously been added to a control.

To do this, use the ->remove_validator() method on the control:

// Some code that adds a required email field to a form:
$form->add( 'text', 'email', 'option:email', _t('Enter your email address') );
$form->email->add_validator( 'validate_required' );
 
// This code executes elsewhere and removes that validator:
$form->email->remove_validator( 'validate_required' );
Personal tools