The following is a tutorial on creating a custom QForm Control. QForms is a forms library and are a component of the Qcodo framework. Additionally, this control makes use of the excellent jQuery library and the SpinBox Control Plugin.

Overview
In this tutorial I lay out the steps necessary to create a new QControl. The control is very simple, but very useful. The control utilizes javascript to create a spin box, something you'd typically see in any GUI based toolkit.

For an example of the finished product, take a look at the control demonstration page.

Starting Out - Don't re-invent the wheel
A Spin Button control is a textbox that contains an integer meant to be incremented or decremented. Thankfully, the QForm library already includes a QIntegerTextBox, so we'll start by sub-classing that.

<?php
   
class QIntegerSpinBox extends QIntegerTextBox {

    }
?>

Now we've got a shell control to work with.

Include the Necessary Javascript

Looking at the javascript required to make the control work, there are three things that need to happen. The jQuery library must be included, the jQuery Spin Button code must be included and the control must be initialized.

The QForm library has a very simple method of including external javascript files. Simply set the member variable, $strJavaScripts, to the list of files to include, and voila! Using this method is the most efficient, because if we use two spin button controls on the same page, QForms knows to only include the javascript files once. All scripts are pulled from the libraries Javascript Assets folder, set as a configuration variable.

The initialization portion is straightforward as well, we simply override the GetEndScript Method. The example of the Spin Button control applies the .SpinButton function to all elements that have the class ".spinbtn". We need to apply the code to each individual control, so we mark it by it's ID.

Our class now looks like this.

<?php
   
class QIntegerSpinBox extends QIntegerTextBox {

       
/**
         * QControl Properties
         */
       
protected $strJavaScripts = 'jquery.js,jquery.spinbtn.js';

        public function
GetEndScript() {
           
ob_start(); ?>

            $("#<?= $this->strControlId; ?>").SpinButton();
            <?php return ob_get_clean();
        }
    }
?>

I use the ob_start(); technique to make my code a little bit cleaner. Worrying about escaping slashes, apostrophe's and other control characters makes your code unreadable.

Adding Style

Now that we've taken care of the Javascript, we need to add in the appropriate styling. QForm Controls have many attributes to control style. However, we need a little bit more power and require a separate stylesheet. This is the only sticky part because QForms does not offer an easy way to do this. The hack-around is to manually include the CSS file by overriding the GetEndHtml method.

Additionally, to get the control to see the javascript, we must specify it's CSS Class to be "spinbtn". The control requires that it's CSS Class be 'spinbtn', so we must also override the ability to set that value.

Because the typical use will be for integers under three digits, I also set a default width for the control to 26 pixels. This can be overridden, but defaults are always nice.

<?php
   
class QIntegerSpinBox extends QIntegerTextBox {

       
/**
         * QControl Properties
         */
       
protected $strCssClass = 'spinbtn';
        protected
$strJavaScripts = 'jquery.js,jquery.spinbtn.js';
        protected
$strWidth = "26px";

        public function
GetEndScript() {
           
ob_start(); ?>

            $("#<?= $this->strControlId; ?>").SpinButton();
            <?php return ob_get_clean();
        }

        public function
GetEndHtml() {
           
ob_start(); ?>

            <style type="text/css" media="all">@import "<?= __CSS_ASSETS__ . "/jquery.spinbtn.css"; ?>";</style>
            <?php return ob_get_clean();
        }

       
/////////////////////////
        // Public Properties: SET
        /////////////////////////
       
public function __set($strName, $mixValue) {
           
$this->blnModified = true;

            switch (
$strName) {
                case
'CssClass':
                    throw new
QCallerException("Cannot override the CssClass Attribute of QIntegerSpinBtn");
                    break;

                default:
                    try {
                       
parent::__set($strName, $mixValue);
                    } catch (
QCallerException $objExc) {
                       
$objExc->IncrementOffset();
                        throw
$objExc;
                    }
                    break;
            }
        }
    }
?>

Customizing the Control

With the Javascript and CSS, our control should be operational! However, the Spin Button plugins accepts a number of options which we currently have no way of managing. For the first version, I only require the Minimum, Maximum and Step Size options.

To provide an interface from the QIntegerSpinBtn control to the Javascript plugin, we need to create the options in the control class. The parent class, QIntegerTextBox, already has Minimum and Maximum options, so the only additional variable we need to include is StepSize.

The convention of the QForm library is to require all variable to pass through the __get() and __set() magic methods of PHP. This convention brings a level of structure to the library that the language currently does not possess.

Thankfully, adding onto this structure is very straightforward and simple.

After adding our variables, we need to pass them through to the Javascript. This is a simple translation.

<?php
   
class QIntegerSpinBox extends QIntegerTextBox {

       
/**
         * QControl Properties
         */
       
protected $strCssClass = 'spinbtn';
        protected
$strJavaScripts = 'jquery.js,jquery.spinbtn.js';
        protected
$strCssScripts = '/jquery.spinbtn.css';
        protected
$strWidth = "26px";

       
/**
         * Properties specific to QIntegerSpinBox
         */
       
protected $intStepSize = 1;

        public function
GetEndScript() {
           
$strJsOptions = array();
            if (!
is_null($this->intMaximum))
               
$strJsOptions[] = "max:".$this->intMaximum;

            if (!
is_null($this->intMinimum))
               
$strJsOptions[] = "min:".$this->intMinimum;

            if (!
is_null($this->intStepSize))
               
$strJsOptions[] = "step:".$this->intStepSize;

           
ob_start(); ?>

            $("#<?= $this->strControlId; ?>").SpinButton({<?= implode(',',$strJsOptions); ?>});
            <?php return ob_get_clean();
        }

        public function
GetEndHtml() {
           
ob_start(); ?>

            <style type="text/css" media="all">@import "<?= __CSS_ASSETS__ . $this->strCssScripts; ?>";</style>
            <?php return ob_get_clean();
        }

       
/////////////////////////
        // Public Properties: GET
        /////////////////////////
       
public function __get($strName) {
            switch (
$strName) {
                case
"StepSize": return $this->intStepSize;

                default:
                    try {
                        return
parent::__get($strName);
                    } catch (
QCallerException $objExc) {
                       
$objExc->IncrementOffset();
                        throw
$objExc;
                    }
            }
        }

       
/////////////////////////
        // Public Properties: SET
        /////////////////////////
       
public function __set($strName, $mixValue) {
           
$this->blnModified = true;

            switch (
$strName) {
                case
'CssClass':
                    throw new
QCallerException("Cannot override the CssClass Attribute of QIntegerSpinBtn");
                    break;

                case
"StepSize":
                    try {
                       
$this->intStepSize = QType::Cast($mixValue, QType::Integer);
                        break;
                    } catch (
QInvalidCastException $objExc) {
                       
$objExc->IncrementOffset();
                        throw
$objExc;
                    }

                default:
                    try {
                       
parent::__set($strName, $mixValue);
                    } catch (
QCallerException $objExc) {
                       
$objExc->IncrementOffset();
                        throw
$objExc;
                    }
                    break;
            }
        }
    }
?>

There we go!

I've put up a project with the first release. Check it out and enjoy!