July 1, 2020: Updated Field Groups snippet to work with when limits were applied to Radio Buttons.
This article requires the GP Limit Choices perk. Buy Gravity Perks today to get this perk plus 33 other premium Gravity Forms plugins!
Overview
With the current global pandemic COVID-19, many essential brick-and-mortar businesses find themselves in need of a curbside pickup scheduling solution. This is new territory for many, and while there are plenty of sophisticated eCommerce options, they require significant setup and time to get going.
Using Gravity Forms and Gravity Forms Limit Choices, you can easily build a form for your customers to schedule their pickup time and limit the number of pickups available per time slot and per day. Let’s get started.
Build Your Form
We’ll begin by adding the necessary fields to our form to start collecting orders. Since this type of form requires capturing lots of data, I suggest making it a multi-page form.
On the first page, we ask for the customer’s name, email, and phone number. I have also included a Radio Button field asking who is picking up the order. It has conditional logic attached to it to show another Name field if someone else is picking up the order.

The second page is where the customer puts together their order. My form uses List fields because it gives the user an easy system to add items to their list in organized categories.
There’s also a section at the bottom asking if they will allow substitutes and any additional notes.

On the last page, the pickup date and time are selected. To do this, our form has a Date and a Drop Down field. In the Drop Down field I have entered all of my available pickup time windows. There’s also a couple of Consent fields to make sure our customer understands how this process works.

Set Up Choice Limits
With the form built, the next step is to limit the pickup time slots to prevent too many customers from trying to pick up at the same time. Start by adding limits to each of the times in the Drop Down field.

Next, we need to create a Field Group so that the limited time slots are per day and not per form. A Field Group is a paired set of fields that that creates a unique choice. The Date and Drop Down fields will be grouped together, and the time slots will apply on a per-day basis. This is done through a snippet.
<?php | |
/** | |
* Gravity Perks // GP Limit Choices // Field Group | |
* | |
* Specify a group of fields that should create a unique choice to limited. For example, a Date field and Radio Button field could be | |
* combined to sell x tickets per date where the ticket type is controlled by the Radio Button field and the date is | |
* selected in the Date field. | |
* | |
* @version 1.4 | |
* @author David Smith <david@gravitywiz.com> | |
* @license GPL-2.0+ | |
* @link http://gravitywiz.com/documentation/gp-limit-choices/ | |
* | |
* Plugin Name: GP Limit Choices - Field Groups | |
* Plugin URI: http://gravitywiz.com/documentation/gp-limit-choices/ | |
* Description: Specify a group of fields that should create a unique choice to be limited. | |
* Author: Gravity Wiz | |
* Version: 1.4 | |
* Author URI: http://gravitywiz.com | |
*/ | |
class GP_Limit_Choices_Field_Group { | |
public function __construct( $args = array() ) { | |
// set our default arguments, parse against the provided arguments, and store for use throughout the class | |
$this->_args = wp_parse_args( $args, array( | |
'form_id' => false, | |
'field_ids' => array(), | |
) ); | |
$this->_args['hash'] = hash_hmac( 'sha256', serialize( $this->_args ), 'gplc_field_group' ); | |
add_filter( 'gwlc_choice_counts_query', array( $this, 'limit_by_field_group' ), 10, 2 ); | |
add_action( 'wp_ajax_gplcfg_refresh_field', array( $this, 'ajax_refresh' ) ); | |
add_action( 'wp_ajax_nopriv_gplcfg_refresh_field', array( $this, 'ajax_refresh' ) ); | |
add_filter( 'gform_pre_render', array( $this, 'load_form_script' ), 10, 2 ); | |
add_filter( 'gform_register_init_scripts', array( $this, 'add_init_script' ), 10, 2 ); | |
if( isset( $_POST['action'] ) && $_POST['action'] == 'gplcfg_refresh_field' ) { | |
remove_action( 'wp', array( 'GFForms', 'maybe_process_form' ), 9 ); | |
remove_action( 'admin_init', array( 'GFForms', 'maybe_process_form' ), 9 ); | |
} | |
} | |
public function limit_by_field_group( $query, $field ) { | |
global $wpdb; | |
$field_ids = $this->_args['field_ids']; | |
if( ! $this->is_applicable_form( $field->formId ) || ! in_array( $field->id, $field_ids ) ) { | |
return $query; | |
} | |
unset( $field_ids[ array_search( $field->id, $field_ids ) ] ); | |
$field_ids = array_values( $field_ids ); | |
$form = GFAPI::get_form( $field->formId ); | |
$join = $where = array(); | |
$select = $from = ''; | |
foreach( $field_ids as $index => $field_id ) { | |
$field = GFFormsModel::get_field( $form, $field_id ); | |
$alias = sprintf( 'fgem%d', $index + 1 ); | |
$_alias = null; | |
if( $index == 0 ) { | |
$_alias = $alias; | |
$select = "SELECT DISTINCT {$alias}.entry_id"; | |
$from = "FROM {$wpdb->prefix}gf_entry_meta {$alias}"; | |
$value = $field->get_value_save_entry( GFFormsModel::get_field_value( $field ), $form, null, null, null ); | |
$where[] = $wpdb->prepare( "( {$alias}.form_id = %d AND {$alias}.meta_key = %s AND {$alias}.meta_value = %s )", $field->formId, $field_id, $value ); | |
} else { | |
$join[] = "INNER JOIN {$wpdb->prefix}gf_entry_meta {$alias} ON {$_alias}.entry_id = {$alias}.entry_id"; | |
} | |
} | |
$field_group_query = array( | |
'select' => $select, | |
'from' => $from, | |
'join' => implode( ' ', $join ), | |
'where' => sprintf( 'WHERE %s', implode( "\nAND ", $where ) ) | |
); | |
$query['where'] .= sprintf( ' AND e.id IN( %s )', implode( "\n", $field_group_query ) ); | |
return $query; | |
} | |
public function is_applicable_form( $form ) { | |
$form_id = isset( $form['id'] ) ? $form['id'] : $form; | |
return empty( $this->_args['form_id'] ) || $form_id == $this->_args['form_id']; | |
} | |
public function load_form_script( $form, $is_ajax_enabled ) { | |
$func = array( 'GP_Limit_Choices_Field_Group', 'output_script' ); | |
if( $this->is_applicable_form( $form ) && ! has_action( 'wp_footer', $func ) ) { | |
add_action( 'wp_footer', $func ); | |
add_action( 'gform_preview_footer', $func ); | |
} | |
return $form; | |
} | |
public function add_init_script( $form ) { | |
if( ! $this->is_applicable_form( $form ) ) { | |
return; | |
} | |
$target_field_id = $this->get_target_field_id( $form, $this->_args['field_ids'] ); | |
$args = array( | |
'formId' => $this->_args['form_id'], | |
'targetFieldId' => $target_field_id, | |
'triggerFieldIds' => $this->get_trigger_field_ids( $form, $this->_args['field_ids'] ), | |
'ajaxUrl' => admin_url( 'admin-ajax.php' ), | |
'hash' => $this->_args['hash'], | |
); | |
$script = 'new GPLCFieldGroup( ' . json_encode( $args ) . ' );'; | |
$slug = implode( '_', array( 'gplc_field_group', $this->_args['form_id'], $target_field_id ) ); | |
GFFormDisplay::add_init_script( $this->_args['form_id'], $slug, GFFormDisplay::ON_PAGE_RENDER, $script ); | |
} | |
public function get_target_field_id( $form, $field_ids ) { | |
foreach( $field_ids as $field_id ) { | |
$field = GFAPI::get_field( $form, $field_id ); | |
if ( gp_limit_choices()->is_applicable_field( $field ) ) { | |
return $field_id; | |
} | |
} | |
return false; | |
} | |
public function get_trigger_field_ids( $form, $field_ids ) { | |
$target_field_id = $this->get_target_field_id( $form, $field_ids ); | |
return array_values( array_filter( $field_ids, function( $field_id ) use ( $target_field_id ) { | |
return $field_id != $target_field_id; | |
} ) ); | |
} | |
public function ajax_refresh() { | |
// Object can be instantiated multiple times. Only listen for this specific configuration's hash. | |
if ( rgpost( 'hash' ) !== $this->_args['hash'] ) { | |
/** | |
* Return out if the hash doesn't match. If we exit here, other ajax_refresh() calls in other instances | |
* won't have the chance to run. | |
*/ | |
return; | |
} | |
$entry = GFFormsModel::get_current_lead(); | |
if( ! $entry ) { | |
wp_send_json_error(); | |
} | |
$form = gf_apply_filters( array( 'gform_pre_render', $entry['form_id'] ), GFAPI::get_form( $entry['form_id'] ), false, array() ); | |
$field = GFFormsModel::get_field( $form, $this->get_target_field_id( $form, $this->_args['field_ids'] ) ); | |
if( $field->get_input_type() == 'html' ) { | |
$content = GWPreviewConfirmation::preview_replace_variables( $field->content, $form ); | |
} else { | |
$value = rgpost( 'input_' . $field->id ); | |
$content = $field->get_field_content( $value, true, $form ); | |
$content = str_replace( '{FIELD}', $field->get_field_input( $form, $value, $entry ), $content ); | |
} | |
wp_send_json_success( $content ); | |
} | |
public static function output_script() { | |
?> | |
<script type="text/javascript"> | |
( function( $ ) { | |
window.GPLCFieldGroup = function( args ) { | |
var self = this; | |
// copy all args to current object: (list expected props) | |
for( prop in args ) { | |
if( args.hasOwnProperty( prop ) ) | |
self[prop] = args[prop]; | |
} | |
self.init = function() { | |
self.$form = $( '#gform_wrapper_{0}'.format( self.formId ) ); | |
self.$targetField = $( '#field_{0}_{1}'.format( self.formId, self.targetFieldId ) ); | |
self.$triggerFields = false; | |
for( var i = 0; i < self.triggerFieldIds.length; i++ ) { | |
var $field = $( '#field_{0}_{1}'.format( self.formId, self.triggerFieldIds[ i ] ) ); | |
if ( ! self.$triggerFields ) { | |
self.$triggerFields = $().add( $field ); | |
} | |
// @todo Test with multiple triggers. | |
else { | |
self.$triggerFields.add( $field ); | |
} | |
} | |
self.$triggerFields.on( 'change', function() { | |
self.refresh(); | |
} ); | |
self.refresh(); | |
}; | |
self.refresh = function() { | |
if( ! self.$targetField.is( ':visible' ) ) { | |
return; | |
} | |
var data = { | |
action: 'gplcfg_refresh_field', | |
hash: self.hash | |
}; | |
self.$form.find( 'input, select, textarea' ).each( function() { | |
if ( this.type === 'radio' ) { | |
if ( this.checked ) { | |
data[ $( this ).attr( 'name' ) ] = $( this ).val(); | |
} | |
} else { | |
data[ $( this ).attr( 'name' ) ] = $( this ).val(); | |
} | |
} ); | |
// Prevent AJAX-enabled forms from intercepting our AJAX request. | |
delete data['gform_ajax']; | |
$.post( self.ajaxUrl, data, function( response ) { | |
if( response.success ) { | |
self.$targetField.html( response.data ); | |
} | |
} ); | |
}; | |
self.init(); | |
} | |
} )( jQuery ); | |
</script> | |
<?php | |
} | |
} | |
# Configuration | |
new GP_Limit_Choices_Field_Group( array( | |
'form_id' => 1485, | |
'field_ids' => array( 1, 2 ) | |
) ); |
Using the snippet is simple. Paste it into your theme’s functions.php
file and edit these lines at the bottom.
new GP_Limit_Choices_Field_Group( array( | |
'form_id' => 12, | |
'field_ids' => array( 3, 4 ) | |
) ); |
Change the form_id
to your Form ID, and replace the numbers in the field_ids
array to your Date and Drop Down fields.
That’s all there is to it. The Date and Drop Down fields are now grouped together.
Wrapping Up
You should now have a form that functions similarly to the demo, and you can start taking orders for pickup at your store with confidence, all without having to invest the time, money, and energy into a robust eCommerce solution.
Plus with a Gravity Forms solution, you can configure the form per your specific requirements instead of trying to cram them into an existing eCommerce solution that isn’t well suited to your unique business.
Taking It Further
Show Number of Spots Left
To show the number of spots left like we have in the demo, use the snippet below. It will automatically update the number of spots left per day.
<?php | |
/** | |
* Display how many spots are left in the choice label when using the GP Limit Choices perk | |
* http://gravitywiz.com/gravity-perks/ | |
*/ | |
add_filter( 'gplc_remove_choices', '__return_false' ); | |
add_filter( 'gplc_pre_render_choice', 'my_add_how_many_left_message', 10, 5 ); | |
function my_add_how_many_left_message( $choice, $exceeded_limit, $field, $form, $count ) { | |
$limit = rgar( $choice, 'limit' ); | |
$how_many_left = max( $limit - $count, 0 ); | |
$message = "($how_many_left spots left)"; | |
$choice['text'] = $choice['text'] . " $message"; | |
return $choice; | |
} |
Did this resource help you do something awesome with Gravity Forms? Then you'll absolutely love Gravity Perks; a suite of 32+ essential add-ons for Gravity Forms with support you can count on.
I’m hoping to use this field group snippet to manage spots, however, I need to have multiple sets of field groups on the same form. Here’s my desired configuration: I have a location field and then a series of fields that list date and time slots as dropdowns.
new GP_Limit_Choices_Field_Group( array( ‘form_id’ => 10, ‘field_ids’ => array( 7, 30, 31, 32, 33, 34, 35 ) ) );
BUT I need an array of arrays where field 7 (the location) is paired with the conditionally displayed date/time dropdown field (30 – 35). So really, it’s (7, 30) (7, 31) and so on – as only one date/time dropdown will appear at a time based on logic.
Is this possible? As always, Gravity Wiz is amazing!