JavaScript

Creating, Extending, and Upgrading Custom ACF Fields That Use Select2

Advanced Custom Fields (ACF) has been around for quite a while now, and it’s super popular. We’re on version 5.x now, and the earliest dated reference documentation I could find was for ACF 2.0 in 2011. There have been some bumps along the way.

One of the more recent issues was compatibility of the Select2 JavaScript library versions. It was mostly because two other popular plugins, Yoast’s WordPress SEO and WooCommerce, also use Select2. What is Select2? It’s the kick-ass library that lets you transform boring dropdowns to amazing searchable and formattable ones:

A screen grab image of a searchable time zone dropdown menu.

So what’s the big deal with ACF? Well, Select2 has gone through some major revisions as well, most notably the latest Select2 Version 4, which is not backwards compatible with Select2 version 3.x. So, if ACF brings in Version 4 and WooCommerce brings in Version 3, we’ve got problems.

Thankfully, ACF came up with an elegant way to deal with these incompatibilities by checking to see if another plugin had enqueued Version 3 and falling back to using a Select2 Version 3 adapter.

/**
* acf.newSelect2
*
* description
*
* @date 13/1/18
* @since 5.6.5
*
* @param type $var Description. Default.
* @return type Description.
*/
acf.newSelect2 = function( $select, props ){
// defaults
props = acf.parseArgs(props, {
allowNull: false,
placeholder: '',
multiple: false,
field: false,
ajax: false,
ajaxAction: '',
ajaxData: function( data ){ return data; },
ajaxResults: function( json ){ return json; },
});
// initialize
if( getVersion() == 4 ) {
var select2 = new Select2_4( $select, props );
} else {
var select2 = new Select2_3( $select, props );
}
// actions
acf.doAction('new_select2', select2);
// return
return select2;
};

Future-proof it or just fix it?

So, what does this have to do with custom ACF fields? Well, in my case there was an old custom-built ACF field that used Select2. To add the Select2 library to the dropdown, it used a JavaScript call to acf.add_select2() to add it. If you notice in the gist above, acf.newSelect2() was added in ACF 5.6.5, and it replaces acf.add_select2(). In the future, how can we avoid running into this problem? In practice, there are two options:

  1. Extend an existing ACF field that is close to what you want to do.
  2. Create a new (or updating an existing) custom field and use ACF to add Select2.

Your first option is the best one. Here’s why: it simplifies your code investment by leveraging as much as possible from ACF. But, I’m also including the quick-fix instructions specific to the Select2. That way, if you have an existing field that you just need to get working for the time being, you can do that as well.

Option 1: Simplify by extension

One of the fields I needed to fix that used acf.add_select2() was strikingly similar to an existing ACF field—the relational field called Taxonomy. The built-in ACF Taxonomy field also uses Select2. The only difference between the custom field that needed repair and the built-in one is that the custom field would display terms from multiple categories, instead of just one. I had an a-ha moment on a call with Corey Collins when he suggested aloud to let ACF handle the hard work and to just tweak what I needed. Brilliant idea, Corey!

The custom field used to look like this:

<?php
if ( ! class_exists( 'AcfFieldMultipleCategories' ) && class_exists( 'acf_field' ) ) {
class AcfFieldMultipleCategories extends acf_field {
/**
* This function will setup the field type data.
*
* @type function
* @date 5/03/2014
* @since 5.0.0
*/
function __construct() {
// Vars.
$this->name = 'multiple_taxonomies';
$this->label = __( 'Taxonomies','acf' );
$this->category = 'relational';
$this->defaults = array(
'taxonomies' => array( 'category' ),
'field_type' => 'select',
'multiple' => 0,
'allow_null' => 0,
'return_format' => 'id',
'add_term' => 0,
'load_terms' => 0,
'save_terms' => 0,
);
// Extra.
add_action( 'wp_ajax_acf/fields/taxonomies/query', array( $this, 'ajax_query' ) );
add_action( 'wp_ajax_nopriv_acf/fields/taxonomies/query', array( $this, 'ajax_query' ) );
// Do not delete!
parent::__construct();
}
/**
* Over 500 lines - almost every method copied from acf/includes/fields/class-acf-field-taxonomy.php
* Some were tweaked where neccessary to load terms from multiple taxonomies.
*/
}
new AcfFieldMultipleCategories();
}

Because the AcfFieldMultipleCategories class was essentially a copy of class-acf-field-taxonomy.php, it was almost 600 lines of code.

Instead of extending acf_field, I was able to instead extend acf_field_taxonomy and reduce the PHP code by half and get rid of the custom JavaScript entirely.

<?php
add_action( 'init', function() {
if ( class_exists( 'AcfFieldMultipleCategories' ) || ! class_exists( 'acf_field_taxonomy' ) ) {
return;
}
/**
* Class for ACF field.
*/
class AcfFieldMultipleCategories extends acf_field_taxonomy {
/**
* This function will setup the field type data.
*
* @type function
* @date 5/03/2014
* @since 5.0.0
*/
function initialize() {
parent::initialize();
// Vars.
$this->name = 'multiple_taxonomies';
$this->label = __( 'Multiple Taxonomies' );
$this->defaults = array(
'taxonomies' => array( 'category' ),
'field_type' => 'select',
'multiple' => 0,
'allow_null' => 0,
'return_format' => 'id',
'add_term' => 0,
'load_terms' => 0,
'save_terms' => 0,
'ui' => 1,
'ajax' => 1,
'ajax_action' => 'acf/fields/multiple_taxonomies/query',
'placeholder' => 'Select Term',
);
// ajax
add_action( 'wp_ajax_acf/fields/multiple_taxonomies/query', array( $this, 'ajax_query' ) );
add_action( 'wp_ajax_nopriv_acf/fields/multiple_taxonomies/query', array( $this, 'ajax_query' ) );
// Extra.
add_filter( 'acf/field_wrapper_attributes', array( $this, 'wrapper_attributes' ), 10, 2 );
}
public function wrapper_attributes( $wrapper, $field ) {
if ( 'multiple_taxonomies' === $field['type'] ) {
$wrapper['class'] .= ' acf-field-taxonomy';
$wrapper['data-type'] = 'taxonomy';
}
return $wrapper;
}
/**
* This function will return an array of data formatted for use in a select2 AJAX response.
*
* @type function
* @date 15/10/2014
* @since 5.0.9
*
* @param array $options Options.
* @return array $r Choices.
*/
function get_ajax_query( $options = array() ) {
// Defaults.
$options = acf_parse_args( $options, array(
'post_id' => 0,
's' => '',
'field_key' => '',
'paged' => 0,
) );
// Load field.
$field = acf_get_field( $options['field_key'] );
if ( ! $field ) {
return false;
}
// Vars.
$results = array();
$args = array();
$limit = 20;
$offset = 20 * ( $options['paged'] - 1 );
// Hide Empty.
$args['hide_empty'] = false;
$args['number'] = $limit;
$args['offset'] = $offset;
// Pagination
// Don't bother for hierarchial terms, we will need to load all terms anyway.
if ( $options['s'] ) {
$args['search'] = $options['s'];
}
// Get terms.
$terms = get_terms( $field['taxonomies'], $args );
/// append to r
foreach( $terms as $term ) {
// add to json
$results[] = array(
'id' => $term->term_id,
'text' => $this->get_term_title( $term, $field, $options['post_id'] )
);
}
// vars
$response = array(
'results' => $results,
'limit' => $limit
);
// Return.
return $response;
}
/**
* This filter is appied to the $value after it is loaded from the db.
*
* @type filter
* @since 3.6
* @date 23/01/13
*
* @param string $value The value found in the database.
* @param int $post_id The post ID from which the value was loaded from.
* @param array $field The field array holding all the field options.
*
* @return $value - the value to be saved in te database
*/
function load_value( $value, $post_id, $field ) {
// Return.
return $value;
}
/**
* Create the HTML interface for your field.
*
* @type action
* @since 3.6
* @date 23/01/13
*
* @param array $field An array holding all the field's data.
*/
function render_field( $field ) {
// Force value to arra.
$field['value'] = acf_get_array( $field['value'] );
$field['multiple'] = 0;
// Vars.
$div = array(
'class' => 'acf-taxonomy-field acf-soh',
'data-save' => $field['save_terms'],
'data-type' => $field['field_type'],
'data-taxonomies' => $field['taxonomies'],
'data-ftype' => 'select',
);
?>
<div <?php acf_esc_attr_e( $div ); ?>>
<?php $this->render_field_select( $field ); ?>
</div>
<?php
}
/**
* Create the HTML interface for your field/
*
* @type action
* @since 3.6
* @date 23/01/13
*
* @param array $field An array holding all the field's data.
*/
function render_field_select( $field ) {
// Change Field into a select.
$field['type'] = 'select';
$field['ui'] = 1;
$field['ajax'] = 1;
$field['choices'] = array();
$choices = array();
if ( count( $field['value'] ) >= 1 ) {
$term = get_term_by_id( $field['value'][0] );
$choices[ $field['value'][0] ] = $term->name;
}
$field['choices'] = $choices;
acf_render_field( $field );
}
/**
* Create extra options for your field. This is rendered when editing a field.
* The value of $field['name'] can be used (like bellow) to save extra data to the $field
*
* @type action
* @since 3.6
* @date 23/01/13
*
* @param array $field An array holding all the field's data.
*/
function render_field_settings( $field ) {
$taxes = acf_get_taxonomies();
// Default value.
acf_render_field_setting( $field, array(
'label' => __( 'Taxonomies' ),
'instructions' => __( 'Select the taxonomies to be displayed' ),
'type' => 'select',
'name' => 'taxonomies',
'multiple' => 1,
'ui' => 1,
'choices' => array_combine( $taxes, $taxes ),
) );
// Allow null.
acf_render_field_setting( $field, array(
'label' => __( 'Allow Null?', 'acf' ),
'instructions' => '',
'name' => 'allow_null',
'type' => 'true_false',
'ui' => 1,
'conditions' => array(
'field' => 'field_type',
'operator' => '!=',
'value' => 'checkbox'
)
) );
// Save terms.
acf_render_field_setting( $field, array(
'label' => __( 'Save Terms', 'acf' ),
'instructions' => __( 'Connect selected terms to the post', 'acf' ),
'type' => 'radio',
'name' => 'save_terms',
'type' => 'true_false',
'ui' => 1,
) );
// Load terms.
acf_render_field_setting( $field, array(
'label' => __( 'Load Terms', 'acf' ),
'instructions' => __( 'Load value from posts terms', 'acf' ),
'name' => 'load_terms',
'type' => 'true_false',
'ui' => 1,
) );
// Return format.
acf_render_field_setting( $field, array(
'label' => __( 'Return Value', 'acf' ),
'instructions' => '',
'type' => 'radio',
'name' => 'return_format',
'choices' => array(
'object' => __( 'Term Object', 'acf' ),
'id' => __( 'Term ID', 'acf' ),
),
'layout' => 'horizontal',
) );
}
}
acf_register_field_type( 'AcfFieldMultipleCategories' );
} );

What’s different?

Here’s a brief breakdown of the things that are different in the AcfFieldMultipleCategories class and why. You can compare much of the code above to what’s in class-acf-field-taxonomy.php for reference.

  1. The class doesn’t get defined until init. This is because ACF doesn’t load the parent acf_field_taxonomy class right away.
  2. Using initialize instead of __construct: ACF 5.6.0 switched to using initialize instead of __construct so I wanted to follow suit. Our class calls parent::initialize() to get all of the defaults from the taxonomy field, then sets only the fields we want to override.
  3. AJAX actions for wp_acf: These actions are present on the parent, but when they’re registered, they reference a specific instance of an object—the parent acf_field_taxonomy class. We redefine them here to get called on our AcfFieldMultipleCategories instance.
  4. Extra CSS classes in wrapper_attributes: We need our custom field to be rendered with a CSS class of acf-field-taxonomy. This is what the ACF Select2 library is looking for to attach to fields. Select2 will behave exactly how it would on a taxonomy field; we’re just changing the query that happens on the back end.
  5. get_ajax_query has been overridden: It gets called from the parent ajax_query method. We override this to get a result set of terms from multiple taxonomies.
  6. load_value has been overridden and simplified to remove filtering.
  7. render_field has been overridden and simplified to only render a select field because that’s the only option we’re allowing.
  8. render_field_select has been overridden to call a custom get_term_by_id function.
  9. render_field_settings has been overridden to remove the Appearance setting (remember we only want it to display a dropdown). We also removed the Create Terms option, as we won’t be saving any new terms because they’re coming from multiple taxonomies.

Is it a lot? Sure, but it’s far less duplication than before and through simplification, making it less prone to bugs and errors. But if that’s still too much, can you take the easy way out…

Option 2: Just update Select2 calls

This option is an easier short-term solution but may postpone further compatibility issues. Just switch any calls from add_select2 to newSelect2. Any parameters to that function that were previously in snake_case need to be changed to camelCase. Here’s a diff of one that I updated:

-		acf.add_select2( $select, {
+		acf.newSelect2( $select, {
 			ajax: 1,
- 			ajax_action: "acf/fields/post_object/query",
- 			allow_null: 0,
+			ajaxAction: "acf/fields/post_object/query",
+			allowNull: 0,

That’s it. I chose this option for a different and far more complex custom ACF field. It had four inputs in one field, which is nothing like any built-in fields. This one is just going to stay as-is and will try to follow the ACF JavaScript conventions.

Getting Permission

When possible, I prefer the first method of doing things because it’s a better long term solution. You can do Option 2 in less than an hour and then still have time left over to convince your manager that Option 1 is better for the long term. Get some time scheduled to fix it the right way, and it’s a win-win for you and your project.

Comments

1 thought on “Creating, Extending, and Upgrading Custom ACF Fields That Use Select2

Have a comment?

Your email address will not be published. Required fields are marked *

accessibilityadminaggregationanchorarrow-rightattach-iconbackupsblogbookmarksbuddypresscachingcalendarcaret-downcartunifiedcouponcrediblecredit-cardcustommigrationdesigndevecomfriendsgallerygoodgroupsgrowthhostingideasinternationalizationiphoneloyaltymailmaphealthmessagingArtboard 1migrationsmultiple-sourcesmultisitenewsnotificationsperformancephonepluginprofilesresearcharrowscalablescrapingsecuresecureseosharearrowarrowsourcestreamsupporttwitchunifiedupdatesvaultwebsitewordpress