2009-03-02 - How to create a multi field widget with sfForm

Introduction

This article will explain how to create a "Google Map Address" widget. Before going further into the code and explanation, the scope and use cases for the widget must be defined.

What do we want to achieve ? Provide an easy way for the end user to add an address
How are we going to achieve it? using text fields and with a google map services

Ok, now let's have a quick mashup.



User case I

  1. User type an address
  2. Press "lookup button"
  3. Latitude and longitude fields are updated,A new marker is created on the map

User case II

  1. User click on the map
  2. Latitude and longitude are updated
  3. Reverse lookup to find the address.

Fields need to be posted and handled by the form
 - lat : latitude (between 90 and -90)
 - lng : longitude (between 180 and -180)
 - address : address (plain text only)
 
The widget's functional specifications have just be defined, now let's define the technical tools and their scopes
 - google map services : displays the map and retrieves address information
 - jQuery : add javascript interactions between the form and the field
 - sfForm : use to draw the widget and validate inputs

The php side - part I

sfForm is a form framework available with the symfony project, please refer to the official documentation to have a better understanding of what coming next.

A sfForm widget is only responsible to render the widget elements with information provided by the upper layer: the widget schema. Next thing to understand: the widget is not responsible for its validation, it looks weird at first but this allows to attach different validations depending on the context: frontend or backend.

If you have already created a simple widget (with one field) you have to extend the sfWidgetForm to make it work. But the mashup shows that the widget will have a least 3 fields, the sfWidgetForm works only for one field. Ok, let's have a look to the embed method of the sfForm class. The embedded method creates a sfWidgetFormSchemaDecorator with the schema from the embedded form.

Let's have a break here:
sfWidgetFormSchemaDecorator > sfWidgetFormSchema > sfWidgetForm

 - sfWidgetForm: is the base widget class
 - sfWidgetFormSchema: this class allows to have multiple sub widget in one widget
 - sfWidgetFormSchemaDecorator: this class preserves the form decorator from the initial embedded form
 
So the "gmap address widget" must extend the sfWidgetFormSchema, as the widget will use the form decorator to render outer html elements. If you look to the validator classes, the sfForm framework have almost the same class structure : sfValidatorSchema > sfValidatorBase. There is no need to have a validator decorator class as decorator is only used to render a form.

browse code : swWidgetFormGMapAddress.class.php

  $fields = array(
    'address' => new sfWidgetFormInput(array(), array('style' => 'width: 300px;')),
    'lat'     => new sfWidgetFormInput(array(), array('readonly' => true)),
    'lng'     => new sfWidgetFormInput(array(), array('readonly' => true)),
  );

Creates the inner widget. Latitude and longitude are read-only as they are only for information purpose to the end user.

  return array(
    '/swToolboxPlugin/js/swGmapWidget.js'
  );

Returns the JavaScript used by the widget. sfForm provides an uniform way to publish widget dependencies: stylesheets and javascripts.


  $lat_id     = $this->generateId($name.'[lat]');
  $lng_id     = $this->generateId($name.'[lng]');
  $address_id = $this->generateId($name.'[address]');
  $map_id     = $this->generateId($name.'[map]');
  $lookup_id  = $this->generateId($name.'[lookup]');

This portion generates the inner widgets id. This information will be used by the javascript to interact with the form.

  // get the inner form formatter
  $subFormatter = new swFormatterGMapAddress($this);

There is two forms formatter created, one for the 'gmap address widget' which is defined in the form and the swFormatterGMapAddress formatter which is used to render the inner widget (address, lat, lnt).

  // render address field
  $address_field = $subFormatter->formatRow(
    $subFormatter->generateLabel('address'),                                 // render the label
    $this->renderField('address', $value['address'], $attributes, array()),  // render the field
    isset($errors['address']) ? $errors['address'] : array(),                // provide the errors information
    $this->getHelp($name)                                                    // get the help information
  );

Renders the address field information, label, address, errors and help by using the swFormatterGMapAddress formatter.

The last major two blocks, javascript and html, are where the information generated before is reused.

For now without any javascript, the widget will look like this :


Now let's have a look at the client side of the widget

The javascript side

The php side generates a javascript code like this:

    jQuery(window).bind("load", function() {
new swGmapWidget({
lng: "dynamic_form_v1_gmap_lng",
lat: "dynamic_form_v1_gmap_lat",
address: "dynamic_form_v1_gmap_address",
lookup: "dynamic_form_v1_gmap_lookup",
map: "dynamic_form_v1_gmap_map"
});
})

We use jQuery to handle events, so when the window is loaded, a new swGmapWidget javascript object is created with information for the current map.

Browse the code : swGMapWidget.js

The object has a few methods:
 - init : is the method where all variables are initialized and events bound to different inputs
 - lookupCallback: is a "static" method used by the geocoder method to lookup the address provided by the user
 - reverseLookupCallback: is another "static" method used by the geocoder to reverse lookup the lng/lat into a valid address.
 
You are free to read the javascript code to understand the code and interactions. For now we have a rendered widget with javascript interactions to handle user's inputs.

 

Now, let's validate the inputs on the server side.

The php side - Part II

The validation is very straightforward. The validator has to validate the information as mentionned earlier in the functional specifications. This work is done by the swGMapAddressValidator class.

Browse the code : swGMapAddressValidator


  $fields = array(
    'lat'     => new sfValidatorNumber(array(
      'min' => -90, 
      'max' => 90,
      'required' => true
    )),
    'lng'     => new sfValidatorNumber(array(
      'min' => -180,
      'max' => 180,
      'required' => true
    )),
    'address' => new swValidatorText(array(
      'required' => true
    )),
  );

swGMapAddressValidator class extends the sfValidatorSchema which accepts, like the sfWidgetFormSchema, an array of validator. The swValidatorText is a specific validator bundled as part of the swToolboxPlugin to return only plain text.

What we have done so far:
 - created a multifield widget
 - created a piece of javascript to handle user interaction
 - created a validator for the widget

We still need to do one more job, yes, unit test! We cannot test the javascript part of this widget as it is javascript. However we can test the widget rendering and the validation.

Browse the code : swGmapAddressWidgetTest

The test file creates a form with a nested form. Both forms contain a swWidgetFormGMapAddress widget. Next the lime class tests if the html form output is correct and then provides invalid and valid parameters to test the validator. So next time the widget is updated, the test should run with no error, otherwise some actions will be required.

Conclusion

This article covers a complex widget, and demonstrates how nice is the sfFrom framework. The step to get into the sfForm can be harder and tricky but once you have made it ... it's so nice. For people who are looking to test this widget, you can either wait for my next post or read the unit test.

 

Comments

comments powered by Disqus