In this tutorial I’ll show you how to create an advanced custom Gravity Forms field type. The field will have multiple inputs and will need special handling in order to store and display the submitted values.

What we will make

In this example I’m assuming an example of a WordPress website owner that deals with lunch deliveries at a workplace. The owner has a form for people to fill out what kind of lunch they want and how many for each day of the week. This can be solved as a table-like method of inputting a number for any course at any day they want delivery.

The courses are editable in the field’s settings in form editor and can be changed at any time. And for each form submission the website owner gets a full overview of the submitted values:

Obviously this is just an example and you probably need to adjust this to your case. But with this example case we’re getting a chance to learn how to handle multiple inputs in a single field. It should give you some ideas in how to handle your own custom field type.

Before starting to code

Before we start we need a place to add our code. You can add this in your theme’s functions.php or your plugin file.

The method I’ve chosen to go for is object-oriented, which means creating a class that extends Gravity Forms’s GF_Field class. I recommend putting the class in a separate file in your project. You should also check that Gravity Forms plugin exists before including your class to prevent crashing your site.

If you are interested you can take a look at Gravity Forms’ documentation on GF_Field. You’ll find more functions and variables you might need for your field type.

By extending the GF_Field class we can simply choose to override the functions we need to change. As for the functions we don’t override, Gravity Forms will run the default defined inside GF_Field. In the tutorial below we’ll go through each function we need to override for our custom field one by one. Without further ado, let’s begin!

Creating a custom field type

The first step is defining a custom PHP class that extends GF_Field. Give the class an unique name and make sure it’s included in your project. After the class definition we run the register() static function in GF_Field passing an instance of our class as parameter. This initializes our class and registers the field type.

The only required variable you need inside your class is $type. The class variable $type must be unique and is a slug name of your field type. In my example I’ve named it ‘food_delivery‘.

if (class_exists('GF_Field')) {
	class FoodDelivery extends GF_Field {
		public $type = 'food_delivery';

		// The rest of the code is added here...
	}
	GF_Fields::register(new FoodDelivery());
}

With this tiny piece of code our custom field type should be added as an available choice in Gravity Forms editor. As default it appears in the end of “Standard Fields” box. Because we haven’t given our field a proper name yet (that’s next step), the button is labeled as the value of $type.

Defining the field’s name

The next step is an easy one; simply giving our field a better name. To do that we override the function get_form_editor_field_title(). All we need to do is return a string with the field’s name.

public function get_form_editor_field_title() {
	return esc_attr__('Food Delivery', 'txtdomain');
}

With this function in our class the button to add the field is updated with a much better label.

Changing the field category

This step is optional. As default our custom field type appears in the “Standard Fields” box, but we can change that. Let’s assume we want it to appear inside “Advanced Fields” box instead.

To change the category we want the field to appear in we override the function get_form_editor_button(). We need to return an associative array with two elements. As value to the key ‘group‘ you provide the internal name of the category you want the button to appear in. Available options here are ‘standard_fields‘, ‘advanced_fields‘, ‘post_fields‘, or ‘pricing_fields‘. (You can also create your own category, but that’s not covered here). The second element in the array needs the key ‘text‘ and for that we simply return the field’s name by calling get_form_editor_field_title(). This is the function we just created above.

public function get_form_editor_button() {
	return [
		'group' => 'advanced_fields',
		'text'  => $this->get_form_editor_field_title(),
	];
}

Now the button to add our custom field type is moved into the “Advanced Fields” box.

Activating field settings

If you have tried to add the field type into a form you might have noticed that there are no settings at all. You can’t even edit the label. The way this works is that all types of settings actually are there, they are simply all hidden with CSS by Gravity Forms. We need to define individually which settings we want to enable, and Gravity Forms will then display the chosen settings for us.

We need to define the function get_form_editor_field_settings() and return an array of all settings we want to not hide for our field type. Which settings you want to add is entirely up to you and your project. Keep in mind that your field should support whatever settings you activate, otherwise it’s meaningless to show a setting for it.

I’ve created a quick overview of the settings’ names below. This is far from a complete list – because there is a lot of settings that are pretty much only useful for very specific field types. For example phone format, date/time format, and a whole bunch of settings that are related to Post fields and Pricing fields.

General tab

  • Field label: label_setting
  • Field description: description_setting
  • Choices: choices_setting
  • Required: rules_setting
  • No duplicates: duplicate_setting
  • Enable columns: columns_setting
  • Enable “select all” choice: select_all_choices_setting
  • Enable “other” choice: other_choice_setting

Appearance tab

  • Placeholder: placeholder_setting
  • Field label visibility and Description placement: label_placement_setting
  • Custom validation message: error_message_setting
  • Custom CSS class: css_class_setting
  • Field size: size_setting

Advanced tab

  • Admin field label: admin_label_setting
  • Default value: default_value_setting
  • Enable password input: password_field_setting
  • Force SSL: force_ssl_field_setting
  • Visibility: visibility_setting
  • Allow field to be populated dynamically: prepopulate_field_setting
  • Enable conditional logic: conditional_logic_field_setting
  • Enable page conditional logic: conditional_logic_page_setting

As for our example the most important ones are the field’s label, description, choices and whether or not the field is required or not. We also allow settings for CSS class, custom validation message, and conditional logic.

public function get_form_editor_field_settings() {
	return [
		'label_setting',
		'choices_setting',
		'description_setting',
		'rules_setting',
		'error_message_setting',
		'css_class_setting',
		'conditional_logic_field_setting'
	];
}

Refresh the form editor and you should now see all of the chosen settings and tabs appear inside our field. All settings are handled and saved automatically by Gravity Forms.

Go ahead and add some items in the Choices list so we have something to work with. Here’s what I’ve set up as an example:

Defining custom default choices

If you’re used to using e.g. Radio buttons or Checkboxes in Gravity Forms, you’ve probably noticed that they come propulated with choices like “First Choice”, “Second Choice”, “Third Choice”. This is default behavior from Gravity Forms if no choices have been saved (before) and this triggers only on these specific field types. But for our custom field type, no choices will be populated. This makes it a little cumbersome, because you won’t get the “+” button to add another choice. You’d have to use the “Bulk add/Predefined choices” button, add some choices there, and after that, you get access to “+” buttons to add choices. But it’s easy to define some custom choices – all you need is defining a class array variable public $choices and Gravity Forms will automatically generate predefined choices in your field when you add it to your forms.

Note: This is a class variable, which you can add in the top of the class, right below public $type. Each choice needs to be an array, with the choice as value to the key ‘text‘.

public $choices = [
	[ 'text' => 'Food Choice 1' ],
	[ 'text' => 'Food Choice 2' ],
	[ 'text' => 'Food Choice 3' ],
];

Keep in mind that if you’ve already added the field to the form, it will not retroactively populate the choices. This only comes in effect when you add a new field to the form.

Note: In Gravity Forms it seems to be possible to also add keys ‘value‘ to each choice. But I have not got this to work – the values will automatically become the same as the choice text.

Defining the field’s value as array

The next step is pretty simple, but necessary. As default values to fields in Gravity Forms are strings. We need the value to be an array because we work with multiple inputs. To do this we define the function is_value_submission_array() and return true.

public function is_value_submission_array() {
	return true;
}

This ensures that we can properly work with the entered value of our multiple inputs.

Rendering the field output

When it comes to render the field’s output there’s a couple of things to be aware of.

First off is that you need to choose between two functions; get_field_input() or get_field_content(). In the first method Gravity Forms automatically renders the wrapping list element, the label, description and container for validation error message to your field and you only control the inner field output. With the second method none of this happens and you are more in control of the field’s output. However you need to manually render the label, description and the error messages. The first method, get_field_input(), is perfectly fine for most cases.

The second thing to be aware of is that the render function for the field affects three different locations. The three are the render of the field’s output in frontend, the preview for the field inside form editor, and finally also the field when editing an entry. Luckily Gravity Forms offers functions to easily determine which view we are at. Usually you would render the field the same way in all three cases. But because rendering a large table with a lot of inputs gets unecessary clunky inside the form editor, I have chosen to render the field differently inside form editor.

And finally we need to make sure that any inputs gets a proper name attribute so that Gravity Forms is able to collect its value upon form submission. All inputs in Gravity Forms needs name attributes that follows this rule: name="input_{FIELD_ID}" (multiselect fields uses an additional ID, but we don’t need to concern ourselves about that for our case). We have access to the field ID as it’s a class variable (from GF_Field). But in our case we have told Gravity Forms that the value is an array and not a singular value (previous step), so we add brackets after the name attribute; name="input_{FIELD_ID}[]". So if the field has the ID of 4 inside a form, the name attribute should be “input_4[]“.

I’m opting for using get_field_input() which comes with three parameters. The first parameter is the form object, which we don’t really need for our example. Second parameter is the current value. This can either be the field’s value from $_POST when the form was attempted submitted, but unsucessful. We can retain the previous submitted values. Or if the function is running in editing an entry, the value will be the stored value from the submission. We will handle the value more closely later on. And the third parameter is the entry object, which we also won’t need for our example.

Let’s start implementing get_field_input() which expects the final render as a string. Straight out of the bat I decide to return an empty string if we are inside the form editor – because I don’t want to render the full table in this view. We can use the method $this->is_form_editor() to check whether or not we are inside form edit. You can choose to skip this, or render something else if you want a preview of the field inside form editor.

public function get_field_input($form, $value = '', $entry = null) {
	if ($this->is_form_editor()) {
		return '';
	}
	
	// .. Rest of code for frontend and edit entry here...
}

The next step is building the HTML for a table that loops over an array of days for generating the columns, and the rows for each course item. But because we need access to the array of days (table columns) multiple places, we should define it as a class variable, making it accessible from any functions inside it. I define a class variable $delivery_days with an array of the days I want to offer delivery for.

class FoodDelivery extends GF_Field {
	public $type = 'food_delivery';

	private $delivery_days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];

	public function get_form_editor_field_title() {
		...
}

This is just an example! You might want to fetch the array for the columns from somewhere else that is not hardcoded.

Let’s get back into get_field_input() and build up our table with inputs. First I loop over the class variable and generate the table headings. Then I loop over the choices entered in the field setting for Choices. This is accessible from the class variable (from GF_Field) $this->choices. For each choice I render an input with the proper name attributes. We have access to the field’s ID from GF_Field‘s class variable $this->id.

public function get_field_input($form, $value = '', $entry = null) {
	if ($this->is_form_editor()) {
		return '';
	}

	$id = (int) $this->id;
	
	$table = '<table class="delivery-table"><tbody><tr>';
	$table .= '<th>' . __('Course', 'txtdomain') . '</th>';
	foreach ($this->delivery_days as $day) {
		$table .= '<th>' . $day . '</th>';
	}
	$table .= '</tr>';

	foreach ($this->choices as $course) {
		$table .= '<tr>';
		$table .= '<td>' . $course['text'] . '</td>';
		foreach ($this->delivery_days as $day) {
			$table .= '<td><input type="number" size="1" name="input_' . $id . '[]" /></td>';
		}
		$table .= '</tr>';
	}
	$table .= '</tbody></table>';

	return $table;
}

With this code in place we should get a nice table rendered for our field type in frontend! Obviously the HTML is entirely up to you, this is just a basic example.

We leave this function for now, but we will come back to it later to handle the submitted value!

Storing the value properly

As of right now Gravity Forms will save our field as a one-dimensional array populated with the entered values and empty strings where the input was empty. There’s no information about which day or choice the value belongs to, other than the index. We need to transform this one-dimensional array into a multidimensional associative array where we store the day and the choice label. We can then easily access the stored number value for e.g. $value['Ham sandwich']['Monday']. After this array transformation we also need to serialize the array so that Gravity Forms can store the value properly in the database.

We will need to transform this value array multiple places so I will define a separate function for this. The function accepts the one-dimensional array and transforms it into a multidimensional array with the stored values for days and choices:

private function translateValueArray($value) {
	if (empty($value)) {
		return [];
	}
	$table_value = [];
	$counter = 0;
	foreach ($this->choices as $course) {
		foreach ($this->delivery_days as $day) {
			$table_value[$course['text']][$day] = $value[$counter++];
		}
	}
	return $table_value;
}

This will store the day names and the choices directly inside the field’s value. Doing it this way makes it possible to change the choices at a later point without breaking the old entries.

Now let’s turn to overriding the function that handles storing the submitted value; get_value_save_entry(). It comes with five parameters but we only need the first which is the submitted value. Inside the function we pass the value into our custom function above, serialize its return and finally return the new value.

public function get_value_save_entry($value, $form, $input_name, $lead_id, $lead) {
	if (empty($value)) {
		$value = '';
	} else {
		$table_value = $this->translateValueArray($value);
		$value = serialize($table_value);
	}
	return $value;
}

At this point Gravity Forms will successfully store our values just in the way we want them! However the stored value is now a serialized array which Gravity Forms will happily echo straight out. We need to implement functions to transform it from an ugly serialized array into some pretty ouput wherever we need it.

Displaying the submitted value

There are three places we need to change the output of our field’s value; the list of entries, looking at a single entry, and within Gravity Forms’s merge tags. Merge tags are used most commonly in email notifications. For example {all_fields} is a merge tag that displays the full submitted form values in emails.

Because we are rendering the same output in three different cases it makes sense to make a separate function for it. I’ve defined a custom function that accepts the value; the unserialized multidimensional array, as parameter. The function then builds up some HTML that displays the array in a pretty way and returns the string. I’ve opted for a nested <ul> list, but you can change the output anyhow you’d like.

private function prettyListOutput($value) {
	$str = '<ul>';
	foreach ($value as $course => $days) {
		$week = '';
		foreach ($days as $day => $delivery_number) {
			if (!empty($delivery_number)) {
				$week .= '<li>' . $day . ': ' . $delivery_number . '</li>';
			}
		}
		// Only add week if there were any requests at all
		if (!empty($week)) {
			$str .= '<li><h3>' . $course . '</h3><ul class="days">' . $week . '</ul></li>';
		}
	}
	$str .= '</ul>';
	return $str;
}

Great, let’s start with the first: the list of entries: get_value_entry_list(). You can choose to output the full output here but it can get pretty clunky and long for the list view, so I’ve opted for simply returning a fixed string that explains that the user needs to go into entry details to see the complete overview.

public function get_value_entry_list($value, $entry, $field_id, $columns, $form) {
	return __('Enter details to see delivery details', 'txtdomain');
}

This is of course entirely up to you, you could opt for displaying only the first x number of characters for example.

The second function is the one affecting the view of a single entry: get_value_entry_detail():

public function get_value_entry_detail($value, $currency = '', $use_text = false, $format = 'html', $media = 'screen') {
	$value = maybe_unserialize($value);		
	if (empty($value)) {
		return '';
	}
	$str = $this->prettyListOutput($value);
	return $str;
}

We simply unserialize the array with WordPress’ function maybe_unserialize(), and return the string output from our custom function.

The final function affects the merge tags and make sure our field’s value looks good inside emails as well: get_value_merge_tag().

public function get_value_merge_tag($value, $input_id, $entry, $form, $modifier, $raw_value, $url_encode, $esc_html, $format, $nl2br) {
	return $this->prettyListOutput($value);
}

Note that we won’t need to unserialize the value inside this function.

With these three functions in place all submitted values should look pretty good everywhere! For example when viewing a submitted entry:

However there’s one important thing missing! At this point our inputs do not retain the previously submitted values and that’s pretty bad.

Make our inputs retain the previously submitted value

There are mainly two cases where we need to make sure the inputs keep the previously submitted values. The first case is when a form submission failed (for example the user forgot a required field). Right now all our inputs lose all previously entered values and the user has to re-input all values again. Secondly when the site owner edits an entry, the inputs are not populated with the submitted values from the submission – which makes it pretty impossible to edit the values properly.

To fix this we return to the function get_field_input(). Second parameter to this function is the value. But remember that this function affects both frontend render and entry edit. This is important because the stored value is different in these two cases. If we are at frontend and handling form submission, the value is in the format of the the one-dimensional array mentioned earlier. And if we are editing an entry, the value is in the format of a serialized multidimensional array. So we need to properly translate the value provided in get_field_input() to easily access the actual values.

public function get_field_input($form, $value = '', $entry = null) {
	if ($this->is_form_editor()) {
		return '';
	}

	$id = (int) $this->id;
	
	if ($this->is_entry_detail()) {
		$table_value = maybe_unserialize($value);
	} else {
		$table_value = $this->translateValueArray($value);
	}

	$table = '<table class="delivery-table"><tbody><tr>';
	...
}

In the code above, before we start creating the HTML for the field output, we create a variable $table_value that contains the correctly translated value. We use GF_Field‘s function is_entry_detail() to check whether or not we are editing an entry or not. And then for our inputs it’s easy to access the proper values and set them as the inputs’s value attributes:

...
foreach ($this->delivery_days as $day) {
	$table .= '<td><input type="number" size="1" name="input_' . $id . '[]" value="' . $table_value[$course['text']][$day] . '" /></td>';
}
...

With the above updated get_field_input() all our custom inputs should always be populated with the previous value; no matter if it’s editing an entry or retrying a form submission.

At this point everything about rendering and storing our values are done and fully working. But there’s one more thing we definitely need to fix.

Make our field pass “required” validation

Gravity Forms have checks to see whether or not a field’s value is empty or not. This is often necessary when the field is set as required. When a field is required you cannot submit the form if it’s empty, right? The problem for us is that we have multiple inputs and we want to allow some of them be empty. This becomes a problem if our field is set to required. Gravity Forms unfortunately interprets “is this empty” wrong, and requires all inputs to be filled in. So we need to add a rule that says that if at least one of our many inputs are filled in, the field’s total value is not empty.

The final function we need to override in our class is is_value_submission_empty(). We only get the form ID as parameter to this function so we need to extract the field value by using Gravity Forms function to fetch it from the $_POST array: rgpost('input_<FIELD ID>'). The return should be the one-dimensional array we’ve seen before. All we need to do is loop through the array and return false if we find a value somewhere. Otherwise we return true as the field’s value is indeed completely empty.

public function is_value_submission_empty($form_id) {
	$value = rgpost('input_' . $this->id);
	foreach ($value as $input) {
		if (strlen(trim($input)) > 0) {
			return false;
		}
	}
	return true;
}

With the above function in place, our field will not fail submission if it’s set to required and at least one input is filled in.

Conclusion and final code

This tutorial has shown you in detail how to create your own custom advanced field type for Gravity Forms. Even if your project is different than my example, I hope that you’ve got some pointers and a-ha’s along the way. I find the Gravity Forms documentation quite lacking in some cases, and this is the result of a lot of trial and error! Anyway, hopefully this has been of some use to you!

For reference, here is the complete code in its entirety:

if (class_exists('GF_Field')) {
	class FoodDelivery extends GF_Field {
		public $type = 'food_delivery';

		public $choices = [
			[ 'text' => 'Food Choice 1' ],
			[ 'text' => 'Food Choice 2' ],
			[ 'text' => 'Food Choice 3' ],
		];

		private $delivery_days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];

		public function get_form_editor_field_title() {
			return esc_attr__('Food Delivery', 'txtdomain');
		}

		public function get_form_editor_button() {
			return [
				'group' => 'advanced_fields',
				'text'  => $this->get_form_editor_field_title(),
			];
		}

		public function get_form_editor_field_settings() {
			return [
				'label_setting',
				'choices_setting',
				'description_setting',
				'rules_setting',
				'error_message_setting',
				'css_class_setting',
				'conditional_logic_field_setting',
			];
		}

		public function is_value_submission_array() {
			return true;
		}

		public function get_field_input($form, $value = '', $entry = null) {
			if ($this->is_form_editor()) {
				return '';
			}

			$id = (int) $this->id;
			
			if ($this->is_entry_detail()) {
				$table_value = maybe_unserialize($value);
			} else {
				$table_value = $this->translateValueArray($value);
			}

			$table = '<table class="delivery-table"><tbody><tr>';
			$table .= '<th>' . __('Course', 'txtdomain') . '</th>';
			foreach ($this->delivery_days as $day) {
				$table .= '<th>' . $day . '</th>';
			}
			$table .= '</tr>';

			foreach ($this->choices as $course) {
				$table .= '<tr>';
				$table .= '<td>' . $course['text'] . '</td>';
				foreach ($this->delivery_days as $day) {
					$table .= '<td><input type="number" size="1" name="input_' . $id . '[]" value="' . $table_value[$course['text']][$day] . '" /></td>';
				}
				$table .= '</tr>';
			}

			$table .= '</tbody></table>';

			return $table;
		}

		private function translateValueArray($value) {
			if (empty($value)) {
				return [];
			}
			$table_value = [];
			$counter = 0;
			foreach ($this->choices as $course) {
				foreach ($this->delivery_days as $day) {
					$table_value[$course['text']][$day] = $value[$counter++];
				}
			}
			return $table_value;
		}

		public function get_value_save_entry($value, $form, $input_name, $lead_id, $lead) {
			if (empty($value)) {
				$value = '';
			} else {
				$table_value = $this->translateValueArray($value);
				$value = serialize($table_value);
			}
			return $value;
		}

		private function prettyListOutput($value) {
			$str = '<ul>';
			foreach ($value as $course => $days) {
				$week = '';
				foreach ($days as $day => $delivery_number) {
					if (!empty($delivery_number)) {
						$week .= '<li>' . $day . ': ' . $delivery_number . '</li>';
					}
				}
				// Only add week if there were any requests at all
				if (!empty($week)) {
					$str .= '<li><h3>' . $course . '</h3><ul class="days">' . $week . '</ul></li>';
				}
			}
			$str .= '</ul>';
			return $str;
		}

		public function get_value_entry_list($value, $entry, $field_id, $columns, $form) {
			return __('Enter details to see delivery details', 'txtdomain');
		}

		public function get_value_entry_detail($value, $currency = '', $use_text = false, $format = 'html', $media = 'screen') {
			$value = maybe_unserialize($value);		
			if (empty($value)) {
				return $value;
			}
			$str = $this->prettyListOutput($value);
			return $str;
		}

		public function get_value_merge_tag($value, $input_id, $entry, $form, $modifier, $raw_value, $url_encode, $esc_html, $format, $nl2br) {
			return $this->prettyListOutput($value);
		}

		public function is_value_submission_empty($form_id) {
			$value = rgpost('input_' . $this->id);
			foreach ($value as $input) {
				if (strlen(trim($input)) > 0) {
					return false;
				}
			}
			return true;
		}
	}
	GF_Fields::register(new FoodDelivery());
}