Implementing an element type

The most important integration point for extensions in addition to the available modules is the addition of further element types. Each form element (which is a field displayed for a form in the frontend) has an element type which defines essentially the whole behavior of that field. This tutorial will show you how to implement your own custom element type.

Available element types

The following element types are available via the Torro Forms plugin itself:

  • textfield: A single text field element.
  • textarea: A textarea element.
  • content: A non-input element to display custom content.
  • dropdown: A dropdown element to select a value from.
  • onechoice: A radio group element to select a single value from.
  • multiplechoice: A checkbox group element to select multiple values from.
  • media: An element to upload a file to the media library.
  • checkbox: A single checkbox element to toggle a value.

Every element type has a set of options that define the behavior of the element of that type, and they have fully custom behavior. This includes mainly validation of the values submitted for the element and displaying the field, but also more advanced details like how those values should be exported to Excel or CSV. Next we’ll learn how to implement an element type.

Implementing your own custom element type

For this example, we’ll implement a custom element type that we’ll call ‘autocomplete’. It will be a text input field, that on change will trigger a search on a certain REST API endpoint (of either a post type or a taxonomy) and then show relevant results. The user can select one of those results, and the title of that will be displayed. Internally though, the value submitted will be the ID of that post or term. Here is an image of what it will look like in the frontend:

Torro Forms autocomplete field

The base class that all element types need to be derived from is awsmug\Torro_Forms\DB_Objects\Elements\Element_Types\Element_Type. In addition to that, there are three optional interfaces that should be implemented under certain conditions:

For our autocomplete example, we won’t need any of the above. However, rest assured that we’ll dive in deeper into some advanced behavior with element types.

Bootstrapping the element type

The first method we have to implement is the bootstrap() method, where we set some basic information about our element type. We can also use this method to add fields to display in the element type, for which we use definitions from the bundled field library.

You need to set the following properties on the element type in this method:

  • $slug: The unique identifier of this element type. We’ll set it to ‘autocomplete’ for this example.
  • $title: The title of this element type, which is displayed when selecting an element type and also as initial element heading for elements of the type.
  • $description: The description explaining what this element type does in one sentence. This is displayed when selecting an element type.
  • One out of $icon_css_class, $icon_svg_id or $icon_url to specify the element type icon. The CSS class could be a dashicon class; to use an SVG you need to embed the SVG (for example in the form builder screen’s footer like Torro Forms core does) and then specify the ID referencing one of the icons included; or alternatively simply specify the URL to an icon image.

You can then add fields for the element type by modifying the $settings_fields property, which is an array of field definitions. When specifying a section for your fields, make sure to use either ‘content’ or ‘settings’. For some common fields, you can also use utility methods instead of manually specifying the field definition. This helps to keep those fields behave in a coherent way across different element types and also allows you to write less code. Such methods are:

  • add_placeholder_settings_field(): Allows to specify a placeholder attribute for the element control.
  • add_description_settings_field(): Allows to specify a description displayed with the element.
  • add_required_settings_field(): Allows to make this element a required element where the user has to enter a value.
  • add_css_classes_settings_field(): Allows to set additional CSS classes for this element.
  • add_choices_settings_field(): Allows to specify the available choices for a field. This is a special field and only available if you use the awsmug\Torro_Forms\DB_Objects\Elements\Element_Types\Choice_Element_Type_Trait in your element type class.

Here is the full code for our bootstrap() method:

protected function bootstrap() {
	$this->slug        = 'autocomplete';
	$this->title       = __( 'Autocomplete', 'myplugin' );
	$this->description = __( 'An autocomplete text field element using the REST API.', 'myplugin' );
	$this->icon_svg_id = 'torro-icon-textfield';

	$this->add_description_settings_field();
	$this->add_placeholder_settings_field();
	$this->add_required_settings_field();
	$this->settings_fields['datasource'] = array(
		'section'     => 'settings',
		'type'        => 'select',
		'label'       => __( 'Datasource', 'myplugin' ),
		'description' => __( 'Select what type of content the autocomplete should use.', 'myplugin' ),
		'choices'     => array_map( function( $data ) {
			return $data['label'];
		}, $this->get_autocomplete_datasources() ),
		'default'     => 'post_type_post',
	);
	$this->add_css_classes_settings_field();
}

As you can see, we call a get_autocomplete_datasources() method here, when getting the available choices for our ‘datasource’ field. This one is fully custom for this element type and simply a utility to get the available datasources that can be used. We’ll define a datasource as an array that consists of a label to use for the backend when selecting it, a relative REST API URL to search for an object by an entered string, another relative REST API URL to get a single object by its ID, and two further arguments specifying in which property of the object we can find its ID and its title respectively. For our example we allow datasources for both posts of all post types and terms of all taxonomies. Let’s include the code for the method here for completeness, just keep in mind that this is not in any way required for an element type:

protected function get_autocomplete_datasources() {
	$datasources = array();

	$post_types = get_post_types( array( 'show_in_rest' => true ), 'objects' );
	foreach ( $post_types as $post_type ) {
		$rest_base = ! empty( $post_type->rest_base ) ? $post_type->rest_base : $post_type->name;

		$datasources[ 'post_type_' . $post_type->name ] = array(
			'label'                         => $post_type->label,
			'rest_placeholder_search_route' => 'wp/v2/' . $rest_base . '?search=%search%',
			'rest_placeholder_label_route'  => 'wp/v2/' . $rest_base . '/%value%',
			'value_generator'               => '%id%',
			'label_generator'               => '%title.rendered%',
		);
	}

	$taxonomies = get_taxonomies( array( 'show_in_rest' => true ), 'object' );
	foreach ( $taxonomies as $taxonomy ) {
		$rest_base = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;

		$datasources[ 'taxonomy_' . $taxonomy->name ] = array(
			'label'                         => $taxonomy->label,
			'rest_placeholder_search_route' => 'wp/v2/' . $rest_base . '?search=%search%',
			'rest_placeholder_label_route'  => 'wp/v2/' . $rest_base . '/%value%',
			'value_generator'               => '%id%',
			'label_generator'               => '%name%',
		);
	}

	return $datasources;
}

Validating a value for an element of our type

The second method that we also must implement is the validate_element() method, which receives the submitted value as first parameter, the element object it was submitted for (which has our element type set as its type) as second parameter, and the whole submission object the value is part of as third parameter. In this method, we need to ensure the value is valid. If it is, we need to return the value; if it isn’t, we need to return an error object.

In our case a value is valid when it identifies an object of the datasource that the user has specified with our ‘datasource’ field. For example, if they specified ‘Pages’, the value should be the ID of a page.

Since the ‘datasource’ is a setting our element type defines, we need to actually check which value is set for that setting in the element passed. Fortunately there is a get_settings() method that we can call to which we simply pass the element object. The method then returns an array of $setting_slug => $setting_value pairs for the element.

We then perform our actual validation, based on the ‘datasource’ used. Something to keep in mind: Please do not manually return WP_Error objects from this method, but use the protected create_error() method instead, which works similarly except that the third parameter is not a data array, but an optional value that should be temporarily stored, regardless of it being invalid. Here is the full code for our validation method:

public function validate_field( $value, $element, $submission ) {
	$settings = $this->get_settings( $element );

	$value = (int) trim( $value );

	if ( ! empty( $settings['required'] ) && empty( $value ) ) {
		return $this->create_error( 'value_required', __( 'You must enter something here.', 'myplugin' ) );
	}

	$datasources = $this->get_autocomplete_datasources();
	if ( ! empty( $settings['datasource'] ) && isset( $datasources[ $settings['datasource'] ] ) ) {
		$datasource = $datasources[ $settings['datasource'] ];
	} else {
		// Just for extra security, fall back to regular posts.
		$datasource = $datasources['post_type_post'];
	}

	$rest_url = rest_url( str_replace( '%value%', $value, $datasource['rest_placeholder_label_route'] ) );
	$request  = WP_REST_Request::from_url( $rest_url );
	if ( ! $request ) {
		return $this->create_error( 'invalid_rest_placeholder_label_route', __( 'Internal error: Invalid REST placeholder label route.', 'myplugin' ) );
	}

	$response = rest_do_request( $request );
	if ( is_wp_error( $response ) || $response->is_error() ) {
		return $this->create_error( 'invalid_value', __( 'The value is invalid.', 'myplugin' ) );
	}

	return $value;
}

What we do here is the following: After casting the value to an integer (since IDs in WordPress always are), we execute a REST request using the aforementioned REST API URL (from the ‘datasource’ specified) to get an object by its ID. If that request returns with an error, we know the value specified is not a valid ID identifying such an object, so it’s invalid. If it doesn’t, we’re all good and can return the value.

Handling display of an element of our type in the frontend

Elements, just as all other visitor-facing content in Torro Forms, are displayed using template files. Those template files are passed some data in the form of variables which can then be referenced in it, for example $id for the element ID, $label for the element label or $input_attrs for the associative array of attributes to use for the actual control. Template files are commonly searched for in your theme’s torro_templates directory (if it exists) and in the core plugin’s templates directory. You can however register an additional one using torro()->template()->register_location( string $slug, string $path, int $priority = 10 ), which comes in handy if you want to provide a default template file in your extension.

Element templates must have a file name as element-{$type}.php. If such a file doesn’t exist for the element type, the plugin will instead fall back to element.php. So you should absolutely make sure you provide a template file for your element type, unless it really is just a text field with custom behavior. In our case, we need to create a file called element-autocomplete.php. We’re lucky here as we can pretty much copy the whole markup from the element-textfield.php template present in the core plugin, since we’re essentially displaying a text field. The only thing that’s different is that we display an additional input which is a hidden field that actually submits the value. The autocomplete field itself will always contain a label for the item selected, but what we need to submit is that item’s ID – that is why we need this workaround. You can find the full code for the element type template file below:

<?php
/**
 * Template: element-autocomplete.php
 *
 * Available data: $id, $container_id, $label, $sort, $type, $value, $input_attrs, $label_required, $label_attrs, $wrap_attrs, $description, $description_attrs, $errors, $errors_attrs, $before, $after, $extra_input_attrs, $value_label
 */

?>
<div<?php echo torro()->template()->attrs( $wrap_attrs ); ?>>
	<?php if ( ! empty( $before ) ) : ?>
		<?php echo $before; ?>
	<?php endif; ?>

	<label<?php echo torro()->template()->attrs( $label_attrs ); ?>>
		<?php echo torro()->template()->esc_kses_basic( $label ); ?>
		<?php echo torro()->template()->esc_kses_basic( $label_required ); ?>
	</label>

	<div>
		<input<?php echo torro()->template()->attrs( $extra_input_attrs ); ?> value="<?php echo torro()->template()->esc_attr( $value ); ?>">
		<input<?php echo torro()->template()->attrs( $input_attrs ); ?> value="<?php echo torro()->template()->esc_attr( $value_label ); ?>">

		<?php if ( ! empty( $description ) ) : ?>
			<div<?php echo torro()->template()->attrs( $description_attrs ); ?>>
				<?php echo torro()->template()->esc_kses_basic( $description ); ?>
			</div>
		<?php endif; ?>

		<?php if ( ! empty( $errors ) ) : ?>
			<ul<?php echo torro()->template()->attrs( $errors_attrs ); ?>>
				<?php foreach ( $errors as $error_code => $error_message ) : ?>
					<li><?php echo torro()->template()->esc_kses_basic( $error_message ); ?></li>
				<?php endforeach; ?>
			</ul>
		<?php endif; ?>
	</div>

	<?php if ( ! empty( $after ) ) : ?>
		<?php echo $after; ?>
	<?php endif; ?>
</div>

Something else that’s important when working with templates is the aforementioned data passed to it. As you can see, the template uses several variables – and in this case, the $extra_input_attrs (attributes for the hidden field) and the $value_label (label for the current value) ones are custom to our element type. All those variables are the element’s raw data, filtered by a filter_json() method on the element type class, which receives the data to filter, the element object the data belongs to, and optionally a submission object if the data is being created for a submission already initiated. The method is implemented in the base class and does all the heavy lifting for you, however in some cases you may need to override it. For us, we need to add the relevant arguments from the selected ‘datasource’ to the $input_attrs array, as data attributes so that we can then use those values in JavaScript with our AJAX functionality. We also need to set up the two extra variables mentioned above, and ensure that the autocomplete field itself does not have a name attribute – as the visual label should not be submitted. Here is the our final implementation of the filter_json() method to get that data to our template file:

public function filter_json( $data, $element, $submission = null ) {
	$data = parent::filter_json( $data, $element, $submission );

	$settings = $this->get_settings( $element );

	$datasources = $this->get_autocomplete_datasources();
	if ( ! empty( $settings['datasource'] ) && isset( $datasources[ $settings['datasource'] ] ) ) {
		$datasource = $datasources[ $settings['datasource'] ];
	} else {
		// Just for extra security, fall back to regular posts.
		$datasource = $datasources['post_type_post'];
	}

	// Attributes for an extra hidden field that holds the actual item ID.
	$data['extra_input_attrs'] = array(
		'type' => 'hidden',
		'id'   => $data['input_attrs']['id'] . '-raw',
		'name' => $data['input_attrs']['name'],
	);

	// Adjustments of the autocomplete field that displays the visual label for the actual item ID.
	$data['input_attrs']['type']                 = 'text';
	$data['input_attrs']['data-target-id']       = $data['extra_input_attrs']['id'];
	$data['input_attrs']['data-search-route']    = $datasource['rest_placeholder_search_route'];
	$data['input_attrs']['data-value-generator'] = $datasource['value_generator'];
	$data['input_attrs']['data-label-generator'] = $datasource['label_generator'];
	if ( ! empty( $data['input_attrs']['class'] ) ) {
		$data['input_attrs']['class'] .= ' torro-plugin-boilerplate-autocomplete';
	} else {
		$data['input_attrs']['class'] = 'torro-plugin-boilerplate-autocomplete';
	}
	unset( $data['input_attrs']['name'] );

	$data['value_label'] = '';
	if ( ! empty( $data['value'] ) ) {
		$rest_url = rest_url( str_replace( '%value%', $data['value'], $datasource['rest_placeholder_label_route'] ) );
		$request  = WP_REST_Request::from_url( $rest_url );
		if ( $request ) {
			$response = rest_do_request( $request );
			if ( ! is_wp_error( $response ) && ! $response->is_error() ) {
				$item          = $response->get_data();
				$property_path = explode( '.', trim( $datasource['label_generator'], '%' ) );
				$not_found     = false;

				while ( ! empty( $property_path ) ) {
					$property = array_shift( $property_path );
					if ( ! isset( $item->$property ) ) {
						$not_found = true;
						break;
					}

					$item = $item->$property;
				}

				if ( ! $not_found ) {
					$data['value_label'] = $item;
				}
			}
		}
	}

	return $data;
}

Something you must ensure here is that you always call the parent method first, before modifying the data. It handles tons of stuff for you and adds general data that the templates would expect, so never ever skip the parent implementation!

Managing scripts and styling

Since we are building an autocomplete element type, it’s obvious we need some JavaScript magic to get it running. Since this is very specific, we’ll not go into the details of the code. All you need to do is register the assets you need and enqueue them – this isn’t tied in with the elements themselves since WordPress is too unpredictable in terms of where a form with specific elements may appear. In order to handle assets, use your own instance of the assets service from within your main extension class.

You can see the whole element type, including assets management and how it is integrated as part of an extension as sample code in the Torro Forms extension boilerplate – if you wanna dig in deeper, it also includes another element type that implements a date field consisting of three dropdowns. You can even install the project on your site and see it in action!