Develop Simply

Ivan K's development musings

Writing Custom Widgets for Form Builder

Normal Widget

In the last post about the form builder for Kohana 3.2 I’ve showed some basic examples of using the library. It’s great for when you quickly want to build a form and be done with it, but what if you have something more specific in mind - for example, a widget to display a rich textarea. (using CKEditor)

So here we go - in order to write a custom widget we first need to create a class to hold the widget itself (all widgets are just static methods in some a class). The class must start with Form_Widgets_. So we create one named Form_Widgets_Custom

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class Form_Widgets_Custom
{
  static public function richtextarea(Form_Widget $data)
  {
      $data->attributes(array('data-type' => 'rich'));
      $data->attributes()->merge_as_data($data->options("config"));

      return Form::textarea($data->name(), $data->value(), $data->attributes()->as_array());
  }
}
?>

That’s a pretty simple widget from PHP’s perspective - we assign a custom attribute (to anchor our JS). Also - $data->attribute() is not just a simple associative array - it has some helper methods, and one of them is ->merge_as_data() it will perform the same thing as the code above it - merge the array to itself, except it will prefix every key with “data-” so they become proper HTML5 attributes. So the JS, using jQuery will end up looking like this:

1
2
3
$('textarea[data-type=rich]').each(function(){
  $(this).ckeditor($.extend({width: $(this).outerWidth(), height: $(this).outerHeight() }, $(this).data()));
});

You can call this custom widget in any form from now on like this:

1
<?php echo $form->row('custom::richtextarea', 'description') ?>

Multiple Fields Widget

I’ve had to implement a “location” widget, that would allow inputing an address field, showing you the actual location of the address in Google maps, and the ability to move the marker and set a different location for the same address, not the one provided by Google maps. This requires at least one more field, probably two (latitude and longitude).

Each widget takes a Form_Widget class as an argument which has all the information required to build a widget (name, value, attributes, etc.) But that’s for only one field, what if we want some more fields in the same widget - as it happens you can do that:

1
<?php echo $form->row('custom::location', array('location', 'lat' => 'lat', 'lon' => 'lon')) ?>

$data->name(), $data->value() return the name and value of the first field respectfully - but if you have multiple fields, you can get them with $data->items('lat')->name(). So this will work like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class Form_Widgets_Custom
{
  static public function location(Form_Widget $data)
  {
      $data->attributes( array('data-location' => 'location'));
      
      $html = Form::input($data->name(), $data->value(), $data->attributes()->as_array());

      if( $data->items('lat') AND $data->items('lon') )
      {
          $html .= Form::hidden($data->items('lat')->name(), $data->items('lat')->value(), array("data-type" => 'location-lat'));
          $html .= Form::hidden($data->items('lon')->name(), $data->items('lon')->value(), array("data-type" => 'location-lon'));
      }
      return $html;
  }    
}
?>

We set the attributes of the main input field with the $data->attribute(). There are 2 main groups of methods on the Form_Widget passed to our method:

  1. Field Related - those are ->name(), ->value(), ->field_name(), ->label(), ->id() and ->errors(). All of those return values related to first field, but if there are multiple fields, you can access them through the ->items() and ->item(<name>) methods - each item has all those methods too.
  2. Everything else - ->attributes(), ->options(), ->required() - those operate on the widget itself.

Widget Slots

If you want to get really funky with your widget you can tap into the “slots” functionality - you see, the return of the widget method is passed into the :field slot inside the template, but you can modify that directly, set other slots, or even not set a :field slot at all

A simple example of how this all works is this:

1
2
3
4
5
6
7
8
9
<?php
class Form_Widgets_Custom
{
  static public function checkbox(Form_Widget $data)
  {        
      return Form_Widgets::checkbox($data->swap_slots(":label", ":field"));
  }
}
?>

With this we use the default checkbox widget, but swap the slots of the field and label in the template (so that the input appears before the label). You can modify the template directly through $data->template(<new template>) and you can set slots, default ones or your own with $data->slots('name', 'slot content')

Custom Form Builder

Alright so you’ve made a couple of custom widgets, but what you want is a custom style to all your widgets, in that case you can write a custom form builder class that sets the desired parameters to all the widgets it creates. Here’s an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?php
class Form_Builder_Jelly_Admin extends Form_Builder_Jelly
{
  public function row($callback, $name, $options = null, $attributes = null )
  {
      $help = Arr::get($options, 'help');

      $field = $this->field($callback, $name, $options, $attributes);

      $errors = join(', ', Arr::flatten((array) $field->errors()));

      return $field
          ->slots(":errors", "<span class=\"help-inline\">{$errors}</span>", TRUE)
          ->slots(":with-errors", $field->errors() ? 'error' : '', TRUE)
          ->slots(":help", $help ? "<span class=\"help-block\">$help</span>" : '', TRUE)
          ->render();
  }

  //Set some custom parameters in the widget object, used by "row" and "field" methods
  public function widget($name)
  {
      return parent::widget($name)
          ->template('<div class="clearfix :name-field :type-field :with-errors">:label<div class="input">:field:errors:help</div></div>');
  }
}
?>

So now you have a custom “help” option that every widget has, as well as custom error message and a custom template.