Creating a custom form field type

Besides prebuilt form field types, you can naturally create your own field – a custom type, or a better implementation for an existing type. The only condition is using inheritance. Every form field (or rather its class) must inherit from the Jet\Form_Field class. Thus, your classes must inherit from this class, but they can naturally inherit from any existing class. Just standard object-oriented programming ❤️

Pay attention to one important detail. If you use automatic generation of forms mapped to classes and want to use your new class (or an entire new type) for these forms as well, you must notify the relevant factory that a new type exists, or that a given existing field type is represented by a new class.

New Implementation of an Existing Form Field Type

As mentioned, it is sufficient to create a new class that can inherit from the existing class representing the given form field type: namespace JetApplication;

use 
Jet\Form;
use 
Jet\Form_Field_Tel;

class 
MyForm_Field_Tel extends Form_Field_Tel {
    
//.. ... ..    
}
ATTENTION! It is necessary to pass this information to the factory. Factory calls must be placed in application initialization, specifically into the script ~/application/Init/Factory.php: use Jet\Factory_Form;

Factory_Form::setFieldClassName( Form::TYPE_TEL, MyForm_Field_Tel::class );

A Brand New Field Type

Now let's create a new class and, most importantly, a completely new field type. The class can implement custom methods for input capturing and validation (and optionally any other methods). Crucially, its $_type property must specify what form field type it represents, as this information will be used further in the system. It is good practice to create a new constant for the type. The imaginary constant MyForm::MY_NEW_FIELD_TYPE is used here as an example.

Furthermore, it is necessary to connect the field to an InputCatcher and a Validator. Of course, you can either select one of the prebuilt input catchers and validators, or create custom ones as well.

namespace JetApplication;

use 
Jet\Form;
use 
Jet\Form_Field;
use 
Jet\Data_Array;

class 
MyForm_Field_NewType extends Form_Field
{
    
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    
protected string $_type = MyForm::MY_NEW_FIELD_TYPE;
    protected 
string $_validator_type = Validator::TYPE_INT;
    protected 
string $_input_catcher_type = InputCatcher::TYPE_INT;
    
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!    
}

So we have the class ready. However, several additional steps are required:

  • Register the new type / new class into the form factory (otherwise the type could not be used, for example, for automatically generated forms).
  • Register which renderers will be used for the new field type.
  • Register which view scripts will be used for the given type.

Registering the Type into the Factory

Registering the type into the factory is performed as follows: use Jet\Factory_Form;

Factory_Form::setFieldClassName( MyForm::MY_NEW_FIELD_TYPE, MyForm_Field_NewType::class );

ATTENTION! Factory calls must be placed in application initialization, specifically in the script ~/application/Init/Factory.php.

Registering Renderers

Now it is necessary to inform the factory which renderers will be used for the new field type.

Again, we will work with the factory in the script ~/application/Init/Factory.php

The full series of calls can look like this: Factory_Form::setRendererFieldClassName( MyForm::MY_NEW_FIELD_TYPE, 'field', Form_Renderer_Field::class );
Factory_Form::setRendererFieldClassName( MyForm::MY_NEW_FIELD_TYPE, 'container', Form_Renderer_Field_Container::class );
Factory_Form::setRendererFieldClassName( MyForm::MY_NEW_FIELD_TYPE, 'error', Form_Renderer_Field_Error::class );
Factory_Form::setRendererFieldClassName( MyForm::MY_NEW_FIELD_TYPE, 'help', Form_Renderer_Field_Help::class );
Factory_Form::setRendererFieldClassName( MyForm::MY_NEW_FIELD_TYPE, 'input', Form_Renderer_Field_Input_Common::class );
Factory_Form::setRendererFieldClassName( MyForm::MY_NEW_FIELD_TYPE, 'label', Form_Renderer_Field_Label::class );
Factory_Form::setRendererFieldClassName( MyForm::MY_NEW_FIELD_TYPE, 'row', Form_Renderer_Field_Row::class );
Phew ... That is a rather lengthy set of calls. However, it is useful to demonstrate it for illustration and completeness.

Let's look at a more practical method that replaces everything shown so far and performs all necessary actions. Factory_Form::registerNewFieldType(
    
field_type: MyForm::MY_NEW_FIELD_TYPE,
    
field_class_name: MyForm_Field_NewType::class,
    
renderers: [
        
'input' => MyForm_Renderer_Field_Input_Special::class
    ] 
);
This snippet performs everything required – this is how a new type is registered simply and realistically. No other calls are needed! This method registers both the class and the renderers for the new type. Please note that only the input element is specified in the list of elements and renderers. If you want to use default renderers for the remaining elements, you do not need to list them and default values will be supplied.

We intentionally showed both approaches, though the second is certainly more practical.

Registering View Scripts

The last thing remaining is to tell the system which default view scripts the new field type will have. We need to inform system configuration about this.

For consistency, it is best to place this in the script ~/application/Init/Factory.php as well.

The principle is identical to registering renderers and it is also possible to call individual methods for each view separately, but I won't burden you with that here and will jump straight to the optimal approach: SysConf_Jet_Form_DefaultViews::registerNewFieldType(
    
field_type: MyForm::MY_NEW_FIELD_TYPE,
    
views: [
        
'input' => 'field/input/my-special',
        
'label' => 'field/label-my-special'
    
]
);
As you can see, here too it is unnecessary to enumerate all views; you only specify those you want configured differently from the system defaults.

Thus, the entire registration of a new type ultimately looks like this: namespace JetApplication;

use 
Jet\Factory_Form;
use 
Jet\SysConf_Jet_Form_DefaultViews;

Factory_Form::registerNewFieldType(
    
field_type: MyForm::MY_NEW_FIELD_TYPE,
    
field_class_name: MyForm_Field_NewType::class,
    
renderers: [
        
'input' => MyForm_Renderer_Field_Input_Special::class
    ]
);

SysConf_Jet_Form_DefaultViews::registerNewFieldType(
    
field_type: MyForm::MY_NEW_FIELD_TYPE,
    
views: [
        
'input' => 'field/input/my-special'
    
]
);

A New Parameter for Your Form Field

You now know the general principle for creating a new field type, but let's return to the beginning for a moment. The example above assumed you would only have custom data capture and validation implementations.

However, for more complex field types, that may be far from sufficient. In the real world, you will need the new field type to have its own parameters as well. Similar to how numeric types can specify a min-max range, or file fields specify allowed upload types, and so on. See form field types.

In essence, it is simple. You just add the required property, getter, and setter to the class representing the form field type, and naturally implement the logic for using that parameter – especially during validation. Yes, that is enough and it will work.

However, if you have tried Jet Studio, you found the tool for mapping forms to classes. If you want this tool to recognize your new form field type as well, you must register it properly. Not only that, you must also define the parameters of your new field so that Jet Studio recognizes them as form parameters and can work with them (i.e., so your new input element can be configured visually).

How to do this? Attach the appropriate attributes to the property representing the new parameter. Let's look at a real example directly from Jet. This trait is used for numeric form fields where a range of valid numeric values can apply: namespace Jet;

trait 
Form_Field_Part_NumberRangeInt_Trait
{
    
    #[
Form_Definition_FieldOption(
        
type: Form_Definition_FieldOption::TYPE_INT,
        
label: 'Minimal value',
        
getter: 'getMinValue',
        
setter: 'setMinValue',
    )]
    protected ?
int $min_value = null;
    
    #[
Form_Definition_FieldOption(
        
type: Form_Definition_FieldOption::TYPE_INT,
        
label: 'Maximal value',
        
getter: 'getMaxValue',
        
setter: 'setMaxValue',
    )]
    protected ?
int $max_value = null;
    
    #[
Form_Definition_FieldOption(
        
type: Form_Definition_FieldOption::TYPE_INT,
        
label: 'Step',
        
getter: 'getStep',
        
setter: 'setStep',
    )]
    protected ?
int $step = null;

    
// ... ... ...
}

As you can see, attributes are used for the definition, making it quite a straightforward process.

However, based on these parameters, it is necessary to configure elements such as the validator. This is done by overriding the getValidator method as follows:

public function getValidator() : Validator
    
{
        if(!
$this->validator) {
            
$this->validator = $this->validatorFactory();
        }
        
        
/**
         * @var Validator_Int $validator
         */
        
$validator = $this->validator;
        
$validator->setMinValue( $this->getMinValue() );
        
$validator->setMaxValue( $this->getMaxValue() );
        
        return 
$validator;
    }


Definition Parameters

Parameter Meaning
type What type it is.
See below for the list of types.
label Parameter description. The description is intended for tools like Jet Studio.
setter Name of the setter method in the class representing the form field, used to set the parameter value.
getter Name of the getter method in the class representing the form field, used to retrieve the parameter value.

Parameter Types

Type Meaning
Form_Definition_FieldOption::TYPE_STRING String type value
Form_Definition_FieldOption::TYPE_INT Integer type value
Form_Definition_FieldOption::TYPE_FLOAT Float type value
Form_Definition_FieldOption::TYPE_BOOL Bool type value
Form_Definition_FieldOption::TYPE_CALLABLE Callable – effectively a two-element array. The first position (index 0) can be:
  • self::class
    representing a static call to itself.
  • string 'this'
    representing a call to itself via $this.
  • Specific class name (either as a string or as NameSpace\Class::class).
Form_Definition_FieldOption::TYPE_ARRAY Indexed array.
Form_Definition_FieldOption::TYPE_ASSOC_ARRAY Associative array.
Previous chapter
Jet\Form_Definition_SubForms
Next chapter
Jet\Form_Definition_FieldOption