Building an Integrated Component
Form Builder provides the ability to create your own custom integrated components.
This page will discuss how to create a custom integrated component that uses AddressService to do a postcode lookup.
Registering the integrated component
First, add the following to your CustomXFormsProBundle/Resources/config/services.yml
file:
services:
custom_xfp.integrated_component.address_service_postcode:
class: Jadu\Custom\CustomXFormsProBundle\IntegratedComponent\AddressServicePostcodeLookup
arguments:
- "@xfp_core.form.component_repository"
tags:
- { name: xfp.integrated_component, label: 'Verified Address - AddressService Postcode Search' }
The services:
line is only needed once, at the beginning of the services.yml
file.
This declares our integrated component's class, and tells Symfony to pass in the form component repository - this is required by the abstract our class will extend. The class is tagged as xfp.integrated_component
.
The Integrated Component Class
Create a class at CustomXFormsProBundle/IntegratedComponent/AddressServicePostcodeLookup.php
with the following content:
<?php
namespace Jadu\Custom\CustomXFormsProBundle\IntegratedComponent;
use Jadu\XFormsPro\Form\IntegratedComponent\AbstractIntegratedComponent;
class AddressServicePostcodeLookup extends AbstractIntegratedComponent
{
public function getAnswerValue($data)
{
return isset($data['uprn']) ? $data['uprn'] : '';
}
}
This class extends AbstractIntegratedComponent
, an abstract class for all integrated components.
The integrated component needs to define its structure. This is achieved by implementing the following function:
/**
* Sets the structure of the IC.
*
* This should result in $this->structure becoming equal to a 2D array of AbstractIntegratedComponentElements
* Each array is a level of the IC.
*/
protected function setStructure()
{
$postcodeEntry = new PostcodeEntryField();
$uprnChoice = new UPRNChoiceField();
$this->structure = [
[
$postcodeEntry,
],
[
$uprnChoice,
],
];
}
The function should set $this->structure
to be an 2D array of AbstractIntegratedComponentElements. Each sub array is a level of the integrated component, i.e. a group of fields with a button that passes the entered data back to the server. The above implementation defines a two-level component; a postcode entry element followed by a property choice element.
Integrated components have access to an array, called $data
, which the elements can interact with. Our integrated component class has methods which rely on this array:
/**
* Returns the answer value from the data array.
*
* @param array $data
*
* @return string
*/
public function getAnswerValue($data)
{
return isset($data['uprn']) ? $data['uprn'] : '';
}
/**
* From the data array, get the retraced answer.
*
* @param array $data
*
* @return string
*/
public function getRetraceAnswerValue($data)
{
if (!isset($data['uprn'])) {
return '';
}
if ($data['uprn'] == '111111') {
return 'I cannot find my property';
}
// some placeholder logic to retrieve the address from the UPRN
// this might call an API or similar
$address = $this->service->getAddressFromUPRN($data['uprn']);
if (is_object($address)) {
return $address->getFormattedAddress();
}
return $data['uprn'];
}
/**
* Returns an array of readable variable names to expose, indexed by their key in the data array.
*
* @return array
*/
public function getAnswerVariables()
{
return [
'postcode' => 'Postcode',
'uprn' => 'UPRN',
'retraced' => 'Label',
];
}
getAnswerValue
determines which key in $data
is the actual answer. Here, we'll assume the UPRN is the best piece of data to use as the answer.
getRetraceAnswerValue
receives the $data
array, and returns a friendly label for the selected answer. In this example, this would retrieve the address based on the UPRN, and return a formatted string of the address. Notice the method has a special case for when the uprn was set to a special value representing 'Property not found'.
getAnswerVariables
denotes which keys in $data
should be exposed as form variables, and the labels these variables should be given.
Now our integrated component class is complete, we need to define the elements our component is using, PostcodeEntryField
and UPRNChoiceField
.
PostcodeEntryField
Create a class at Jadu\Custom\CustomXFormsProBundle/IntegratedComponent/PostcodeEntryField.php
with the following content:
<?php
namespace Jadu\Custom\CustomXFormsProBundle\IntegratedComponent;
use Jadu\XFormsPro\Form\IntegratedComponent\AbstractIntegratedComponentElement;
class PostcodeEntryField extends AbstractIntegratedComponentElement
{
/**
* @var string
*/
protected $postcode = '';
/**
* @var string
*/
protected $error = '';
/**
* The GUID of the component this element displays
*
* @return string
*/
public function getComponentGUID()
{
return sha1('TextFieldComponent');
}
/**
* Label of the element.
*
* @return string
*/
public function getLabel()
{
return 'Postcode';
}
/**
* Key in the data array.
*
* @return string
*/
public function getFieldKey()
{
return 'postcode';
}
/**
* Populate object from data array.
*
* @param array $data
*/
public function populate($data)
{
if (isset($data['postcode'])) {
$this->postcode = trim($data['postcode']);
} else {
$this->postcode = '';
}
}
/**
* Does the element have an answer?
*
* @return bool
*/
public function hasAnswer()
{
return !empty($this->postcode);
}
/**
* Is the answer valid?
*
* @return bool
*/
public function hasValidAnswer()
{
require_once 'xforms2/JaduXFormsFormValidation.php';
$isValid = validate_gdsc_postcode($this->postcode);
if (!$isValid) {
$this->error = 'This postcode is invalid';
} else {
$this->error = '';
}
return $isValid;
}
/**
* Is the element required?
*
* @return bool
*/
public function isRequired()
{
return true;
}
/**
* @param array $values
*
* @return array
*/
public function getRenderData($values)
{
return [
'value' => $this->postcode,
];
}
}
This class extends AbstractIntegratedComponentElement
, an abstract class for all integrated component elements.
getComponentGUID
returns the GUID of the component to be used to display the element. In this case, the element will display a text field.
getLabel
returns the label of the element in the integrated component.
getFieldKey
determines which key in the $data
array this element will work with.
populate
is used to populate any properties on the element class from the $data
array.
hasAnswer
returns true if the element has been answered. This is run after populate
, so can rely on class properties.
hasValidAnswer
is used to determine if, once hasAnswer
confirms the element has been answered, the answer is valid.
isRequired
determines if the element must be answered.
getRenderData
returns an array of twig parameters necessary to render the element's component.
UPRNChoiceField
Create a class at CustomXFormsProBundle/IntegratedComponent/UPRNChoiceField.php
with the following content:
<?php
namespace Jadu\Custom\CustomXFormsProBundle\IntegratedComponent;
use Jadu\XFormsPro\Form\IntegratedComponent\AbstractIntegratedComponentElement;
class UPRNChoiceField extends AbstractIntegratedComponentElement
{
/**
* @var string
*/
protected $uprn = '';
/**
* The GUID of the component this element displays
*
* @return string
*/
public function getComponentGUID()
{
return sha1('DropdownComponent');
}
/**
* Label of the element.
*
* @return string
*/
public function getLabel()
{
return 'Property';
}
/**
* Key in the data array.
*
* @return string
*/
public function getFieldKey()
{
return 'uprn';
}
/**
* Populate object from data array.
*
* @param array $data
*/
public function populate($data)
{
if (isset($data['uprn'])) {
$this->uprn = trim($data['uprn']);
} else {
$this->uprn = -1;
}
}
/**
* Does the element have an answer?
*
* @return bool
*/
public function hasAnswer()
{
return !empty($this->uprn) && $this->uprn != '-1';
}
/**
* Is the answer valid?
*
* @return bool
*/
public function hasValidAnswer()
{
return true;
}
/**
* Is the element required?
*
* @return bool
*/
public function isRequired()
{
return true;
}
/**
* @param array $values
*
* @return array
*/
public function getRenderData($values)
{
// placeholder call to an API to get properties for a postcode
$properties = $this->service->getPropertiesForPostcode($values['postcode']);
$properties = array_merge($properties, $this->getStaticOptions());
return [
'answers' => $properties,
'value' => $this->uprn,
];
}
/**
* Gets an array of any static options
*
* @return array
*/
public function getStaticOptions()
{
$data = new \stdClass();
$data->text = 'I cannot find my property';
$data->value = '111111';
return [$data];
}
}
This class extends AbstractIntegratedComponentElement
in the same way as PostcodeEntryField
, and implements most methods in a similar way. The main differences are:
-
getComponentGUID
is set to a drop down field. -
hasValidAnswer
returns true, as no validation is necessary for a drop down. -
getStaticOptions
has been overridden. This method declares an array of any textual, fixed options. In this case, 'I cannot find my property' is the static option. -
getRenderData
returns an array of twig parameters, includinganswers
, which is an array of properties based on$data['postcode']
. This also merges in the return ofgetStaticOptions
.
Integrated component inputs
When designing your integrated component, it is possible to specify that it requires one or more inputs. This is useful if the component needs extra information to load data. An example scenario would be an integrated component that loads options specific to a property - this could have an input of a UPRN.
To work with inputs, you'll need to input two methods in the class:
-
public function getInputs()
This method should return an array of one or more
Jadu\XFormsPro\Form\IntegratedComponent\IntegratedComponentInput
objects. Each object relates to an input required. EachIntegratedComponentInput
object has three properties:name
- the identifier for the inputlabel
- the label shown in the Page Setup interfaceoptions
- an array of options that can be chosen as the input. Should be an array of strings, indexed by the value. An empty array will be rendered as a core mapping selector.
-
public function setInputs($inputs)
This method is called on the integrated component, to store an array of evaluated inputs on the object for later use. This method can be overridden, so that individual inputs can be passed to elements of the integrated component. Note that this method is passed an array containing the evaluated inputs, indexed by the
name
property of eachIntegratedComponentInput
object returned fromgetInputs()
.
When configured correctly, you should see the Page Setup area when adding a page containing your component to a form. This area will show each input you specify, so form designers can select the right inputs to be passed into your integrated component.