Skip to content

Instantly share code, notes, and snippets.

@RadGH
Last active August 17, 2023 01:26
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save RadGH/2e419f7b67974f969e55fa3556f652fc to your computer and use it in GitHub Desktop.
Save RadGH/2e419f7b67974f969e55fa3556f652fc to your computer and use it in GitHub Desktop.
Edit an existing gravityforms entry on the frontend
<?php
/*
Plugin Name: GF Editable by Radley
Description: Example classes to make a particular gravity form editable on the front-end.
Author: Radley Sustaire
Author URI: https://radleysustaire.com/
Version: 1.0.0
*/
// QUICK TEST INSTRUCTIONS:
// 1. Import this example form in Gravity Forms (download as json):
// https://gist.github.com/RadGH/afb032d642515aedbb068f6b5990b668
//
// 2. Modify "GF_Form_12" below to be your own form ID after import.
// * If you change the class, make sure to replace "new GF_Form_12()" below too.
//
// 3. Submit an entry on the form.
// 4. Go back to the form and add ?edited_entry_id=100 (where 100 is your entry id)
// 5. Field values should populate, allowing to edit the same entry.
// RECOMMENDED AFTER TESTING:
//
// 1. Put the "abstract" class Editable_GF_Form into a separate file.
// 2. Create a copy of GF_Form_12 for any of your forms.
// 3. Include the abstract class, and your custom classes, then instantiate your custom classes like: new GF_Form_12()
// Extending "Editable_GF_Form" gives the ability to edit entries. You only need to supply $form_id and $query_arg
/**
* @class GF_Form_12
* @version 1.0
*/
class GF_Form_12 extends Editable_GF_Form {
// Each form should have its own class.
public $form_id = 12;
public $query_arg = 'edited_entry_id';
public function __construct() {
// Add essential hooks from the parent Editable_GF_Form class
parent::__construct();
// Register actions or filters for the form here.
// EXAMPLE: Add a key when the form gets saved
// add_filter( 'gform_entry_post_save', array($this, 'fill_random_key'), 30, 2 );
}
// EXAMPLE: The function to add a key when the form gets saved
/*
public function fill_random_key( $entry, $form ) {
if ( $this->form_id != $form['id'] ) return $entry;
// Some form of key
$key = uniqid();
// Save to the database
gform_update_meta( $entry['id'], 'secret_key', $key );
// Add to current entry too
$entry['secret_key'] = $key;
return $entry;
}
*/
}
// Remember to instantiate your class, or else it won't do anything.
new GF_Form_12();
/**
* This class is used to make a gravity form entry editable.
* Do not edit below, create your own object that extends this class.
* An example is above "GF_Form_12"
*/
abstract class Editable_GF_Form {
/**
* Form ID that will allow editing entries.
*
* @var int
*/
public $form_id = null;
// Internal variables
protected $uploads = array();
protected $sub_inputs = array();
public function __construct() {
if ( $this->form_id === null ) {
_doing_it_wrong(__FUNCTION__, '$this->form_id must be an integer', '1.0');
exit;
}
// When editing an entry, change the entry ID to the edited entry instead of creating a new entry
add_filter( "gform_entry_id_pre_save_lead_{$this->form_id}", array($this, 'change_saved_entry_id'), 50, 2 );
// Prepare the form to be editable on a very early hook
add_filter( 'gform_form_args', array( $this, 'prepare_editable_form' ), 20 );
// Insert field values for most fields
add_filter( "gform_field_value", array($this, 'load_regular_field_value'), 20, 3 );
// Keeps file uploads unless the user actually deletes or replaces them
add_filter( "gform_pre_process", array( $this, 'pre_restore_existing_uploads' ), 20, 1 );
}
/**
* Get the entry id that is being edited from the url ?entry_id=100
*
* @return int|false
*/
public function get_edited_entry_id() {
$entry_id = (int) rgar( $_GET, $this->query_arg );
if ( !$entry_id ) $entry_id = false;
if ( $this->can_user_edit_entry( get_current_user_id(), $entry_id ) ) {
return $entry_id;
}else{
return false;
}
}
/**
* Keeps file uploads unless the user actually deletes or replaces them.
* Gravity forms seems to do this, but fails at it.
* To make this work we capture values before (here) and restore them after the entry is saved.
*
* @param $form
*
* @return mixed
*/
public function pre_restore_existing_uploads( $form ) {
if ( $form['id'] != $this->form_id ) return $form;
$entry_id = $this->get_edited_entry_id();
// Get uploads from $_POST, served as JSON string, which has file upload info
$uploads = rgpost( 'gform_uploaded_files' );
if ( !$uploads ) return $form;
// Decode the json
$uploads = json_decode( $uploads );
// Each file upload field will have the previous filename, or NULL if removing that file.
if ( $uploads ) foreach( $uploads as $input_name => $file ) {
// If file was removed by user, or replaced with new file
if ( ! $file ) continue;
// File should be kept.
$field_id = (int) str_replace('input_', '', $input_name );
$url = gform_get_meta( $entry_id, $field_id );
$this->uploads[ $input_name ] = array(
'entry_id' => $entry_id,
'input_name' => $input_name,
'field_id' => $field_id,
'url' => $url
);
}
add_filter( "gform_after_submission_{$form['id']}", array( $this, 'restore_existing_uploads' ), 10, 2 );
return $form;
}
/**
* Restore files preserved by pre_restore_existing_uploads() after the entry has been saved
*
* @param $entry
* @param $form
*
* @return mixed
*/
public function restore_existing_uploads( $entry, $form ) {
if ( $form['id'] != $this->form_id ) return $entry;
if ( $this->uploads ) foreach( $this->uploads as $u ) {
if ( $entry['id'] != $u['entry_id'] ) continue;
// Get the field ID and URL of the file that should be preserved
$field_id = $u['field_id']; // 13
$url = $u['url']; // https://example.com/.../icon-zm3.png
// Restore the URL to the entry
$entry[ $field_id ] = $url;
// Update the entry
GFAPI::update_entry( $entry );
}
return $entry;
}
/**
* Make all fields on the form editable (allowPrepopulate) and add names if not already given.
*
* @param $form
*
* @return mixed
*/
public function add_form_prepopulate_names( $form ) {
// Modify every field and add a name if needed.
// Names added manually work too, by enabling "Allow field to be populated dynamically" on the field.
if ( $form['fields'] ) foreach( $form['fields'] as &$field ) {
if ( !($field instanceof GF_Field) ) continue;
// Don't affect display fields (like html)
if ( $field->displayOnly ) continue;
// Enable pre-populate
$field->allowsPrepopulate = true;
// Get regular field name
$inputName = $field->inputName;
if ( $inputName ) continue;
// Check if we need a regular field name, or if we need sub fields with names.
if ( empty( $field->inputs ) ) {
// Regular field
// 1st and 2nd are form ID ($form['id']), and field ID ($form->fields[0]->id)
// field_12_4_value
$field->inputName = "field_{$field['formId']}_{$field['id']}_value";
}else{
// Sub fields
// Loop through sub fields and add names to any that are missing
foreach( $field->inputs as $key => &$input ) {
if ( empty($input['name']) ) {
// 3rd number is the sub field ID: ($form->fields[0]->inputs[0]->id)
// input name="field_12_4_1_value"
$input['name'] = "sub_field_" . $field['formId'] . '_' . $field['id'] . '_'. $key .'_value';
}
}
}
}
return $form;
}
/**
* Fill the value on our form using the existing entry's data
*
* @param $value
* @param GF_Field|null $field
* @param $name
*
* @return array
*/
public function load_regular_field_value( $value = null, GF_Field $field = null, $name = null ) {
if ( ! ($field instanceof GF_Field) ) return $value;
if ( $field->formId != $this->form_id ) return $value;
$existing_entry_id = $this->get_edited_entry_id();
if ( !$existing_entry_id ) return $value;
$existing_entry = GFAPI::get_entry( $existing_entry_id );
if ( !$existing_entry ) return $value;
$value = GFFormsModel::get_lead_field_value( $existing_entry, $field );
// If no sub fields just return value
if ( empty($field->inputs) ) return $value;
// Checkboxes work with the given value
if ( $field->type == 'checkbox' ) return $value;
// Names do not seem to work.
return $value;
}
/**
* Add certain hooks only when the form is going to be displayed (based on shortcode usage)
*
* $args = array(7) {
* "form_id" => "12"
* "display_title" => true
* "display_description" => true
* "force_display" => false
* "field_values" => array() (empty)
* "ajax" => false
* "tabindex" => "0"
* }
*
* @param $args
* @return array
*/
public function prepare_editable_form( $args ) {
// Only for this form
if ( $this->form_id !== (int) $args['form_id'] ) return $args;
// Add saved files to edited entry when the form started to be rendered
add_filter( "gform_pre_render_{$this->form_id}", array($this, 'prepare_previously_uploaded_files'), 20, 3 );
// Fill in the value of all fields that have a name. The name must match the "autofill parameter name" in the field's settings.
$form = GFAPI::get_form( $this->form_id );
$form = $this->add_form_prepopulate_names( $form );
foreach( $form['fields'] as $field ) {
if ( !($field instanceof GF_Field) ) continue;
$sub_inputs = rgobj($field, 'inputs');
// Advanced inputs like Name and Address have multiple sub inputs, each with their own name and values
// They are tricky to fill because Gravity Forms doesn't give you the key with this filter.
if ( $sub_inputs ) {
foreach( $sub_inputs as $sub_input ) {
if ( !is_array($sub_input) ) continue;
// Get the name, if any
$name = rgar($sub_input, 'name' );
if ( !$name ) continue;
// Store the sub input field assigned to the name (first_name) so we can get field data later
$this->sub_inputs[ $name ] = $sub_input;
// Use a special filter for sub inputs
add_filter( "gform_field_value_{$name}", array( $this, 'fill_sub_input_value' ), 20, 3 );
}
continue;
}
}
return $args;
}
/**
* Return true if the given user is able to make edits to the entry.
*
* @param int $user_id
* @param array|int $entry
*
* @return bool
*/
public function can_user_edit_entry( $user_id, $entry ) {
if ( is_numeric($entry) ) $entry = GFAPI::get_entry( $entry );
if ( !$entry || is_wp_error( $entry ) ) return false;
// Must be an entry belonging to this form
if ( $entry['form_id'] != $this->form_id ) return false;
// The user ID must match the owner of the entry
if ( (int) $entry['created_by'] != (int) $user_id ) return false;
return true;
}
/**
* Set up "uploaded_files" using values from the previous entry. Allows you to keep your previous upload, or remove it and start over.
*
* If file is kept, form submits:
* gform_uploaded_files: {"input_13":"icon-aa3.png"}
*
* If file is removed, form submits:
* gform_uploaded_files: {"input_13":null}
*
*
*
* @param $form
* @param $ajax
* @param $field_values
*
* @return mixed
*/
public function prepare_previously_uploaded_files( $form, $ajax, $field_values ) {
$user_id = get_current_user_id();
// Get the edited entry
$entry_id = $this->get_edited_entry_id();
if ( !$entry_id ) return $form;
// Check permissions
if ( !$this->can_user_edit_entry( $user_id, $entry_id ) ) return $form;
// Check if any field is a file upload. If not, we can ignore this function
do {
foreach( $form['fields'] as $field ) {
if ( $field instanceof GF_Field_FileUpload ) {
// File upload found. Abort the do{} loop and proceed with the function
break 2;
}
}
// No file uploads found
return $form;
} while(false);
// Get the entry object
$entry = GFAPI::get_entry( $entry_id );
if ( !isset( GFFormsModel::$uploaded_files[ $form['id'] ] ) ) {
GFFormsModel::$uploaded_files[ $form['id'] ] = array();
}
// Loop through each file upload and put the basename as an uploaded file
foreach( $form['fields'] as $field ) {
if ( !$field instanceof GF_Field_FileUpload ) continue;
$value = rgar( $entry, $field->id );
GFFormsModel::$uploaded_files[ $form['id'] ]["input_{$field->id}"] = basename( $value );
}
return $form;
}
/**
* Make Gravity Forms edit an existing entry ($entry_id = int), instead of creating a new one ($entry_id = null).
*
* @param $entry_id null|int
* @param $form array
*
* @return null|int
*/
public function change_saved_entry_id( $entry_id, $form ) {
if ( $entry_id !== null ) return $entry_id;
$user_id = get_current_user_id();
// Get the entry being edited
$existing_entry_id = $this->get_edited_entry_id();
if ( !$existing_entry_id ) return $entry_id;
// Check access, if user can edit then return the previous entry ID, instead of creating a new one (null).
if ( $this->can_user_edit_entry( $user_id, $existing_entry_id ) ) {
// Edit previous entry
return $existing_entry_id;
}else{
// Create a new entry
return null;
}
}
/**
* Get text used for sub inputs
*
* @param null $value
* @param GF_Field|null $field
* @param null $name
*
* @return string
*/
public function fill_sub_input_value( $value = null, GF_Field $field = null, $name = null ) {
if ( $field->formId != $this->form_id ) return $value;
// Name must be defined in $this->sub_inputs so we know what sub field this hook relates to.
if ( !isset($this->sub_inputs[$name]) ) return $value;
$id = rgar( $this->sub_inputs[$name], 'id' );
if ( !$id ) return $value;
$existing_entry_id = $this->get_edited_entry_id();
if ( $existing_entry_id ) {
$value = gform_get_meta( $existing_entry_id, $id );
}
return $value;
}
}
@RadGH
Copy link
Author

RadGH commented Jun 25, 2020

Updated so you do not need to manually type in all the field names and IDs. Hooray!

@RadGH
Copy link
Author

RadGH commented Jul 14, 2020

Updated:

  • Added support for Checkbox fields which has one name and multiple values (array). Add others to the "$compound_field_types" array.
  • Added support for "compound" fields like the Name and Address field, with multiple sub-inputs that each have a name and value.
  • Continued support for "date" field which, although it acts like a compound field, only returns a single value.
  • Fixed hooks running every page load, which would get the form data even if the form was never used on a page.

PS: Gravity forms is ridiculously inconsistent

@RadGH
Copy link
Author

RadGH commented May 16, 2022

Updated:

  1. Major change: You no longer need to manually enable auto populate on every field. Fields are made to prepopulate automatically based on "add_form_prepopulate_names".

  2. A working example is provided, see QUICK TEST INSTRUCTIONS and import this form: https://gist.github.com/RadGH/afb032d642515aedbb068f6b5990b668

  3. Files handled better. If you do not remove or replace a file, it will now be preserved

  4. The class "GF_Form_12" is an example you should copy and customize. The class "Editable_GF_Form" you generally shouldn't need to touch.

@Antonio78
Copy link

Hi, how can I prevent confirmation emails from being sent when the form is in edit mode?
I have tried adding a remove filter, and also changing the email sending filter but to no avail..

@ashish200025
Copy link

HI RadGH

Is there way we can show the uploaded file when file is multi File upload input Field?

Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment