I’m going to start with “This is not my best work.”
In using CMB2, I have created situations where it’s smarter to have the ‘normal’ WordPress taxonomy fields changed. Oh sure, they work most of the time for most things, but most is not all. In my situation, I had some custom taxonomies that I did not want people adding to from the post-edit screen.
To get around the issue, most of the taxonomies were drop-downs using the taxonomy_select
field type. That let me control the display and have the drop-down be the terms they could add. Anyone with admin access could add more, of course, but they’d see special notes about that. It gave me control.
The problem really arose when I had a multicheck list of terms to add. Yes, I had 1 to 20 terms that might be added. And while I could use taxonomy_multicheck
to do that, it wasn’t perfect. It made the screen very large.
Select2 is Better
Select2 is a jQuery replacement for select boxes. Using it, you can make a simple dropdown where you can have a single (or even multiple) selections, but also it has a nice interface for multiple selections:
That looks much nicer than a list or grid of 20 options. You click on the box and you get a dropdown:
Select2 and CMB2
Thankfully there’s already a plugin/add-on for this with CMB2. Phil Wylie made cmb-field-select2 which I pulled into my site and it works quite well. Except… You can’t use it to save Taxonomy data properly!
This is due to a lot of complicated things, and while my first instinct was to complain to myself that core CMB2 could do it, and thus so could everyone, I know it’s not that simple. All the effort CMB2 put into making that work is little short of phenomenal. It was hard and it’s complex and it’s outright weird. I looked at the code and backed away slowly.
But that doesn’t mean it’s impossible. It’s just a little weird and it’s not my best work. But it does work.
Show the Taxonomies
The first step is that you have to make a function to convert the taxonomy to something that can be used in a selection box. Thankfully Phil already did this and his example code works:
/** * Get a list of terms * * Generic function to return an array of taxonomy terms formatted for CMB2. * Simply pass in your get_terms arguments and get back a beautifully formatted * CMB2 options array. * * @param string|array $taxonomies Taxonomy name or list of Taxonomy names * @param array|string $query_args Optional. Array or string of arguments to get terms * @return array CMB2 options array */ function iweb_get_cmb_options_array_tax( $taxonomies, $query_args = '' ) { $defaults = array( 'hide_empty' => false ); $args = wp_parse_args( $query_args, $defaults ); $terms = get_terms( $taxonomies, $args ); $terms_array = array(); if ( ! empty( $terms ) ) { foreach ( $terms as $term ) { $terms_array[$term->term_id] = $term->name; } } return $terms_array; }
Next you call that in your CMB2 code:
// Field: Genre $field_genre = $cmb_notes->add_field( array( 'name' => 'Genre', 'desc' => 'Subject matter.', 'id' => 'theshows_genre', 'taxonomy' => 'my_genres', 'type' => 'pw_multiselect', 'select_all_button' => false, 'remove_default' => 'true', 'options' => iweb_get_cmb_options_array_tax( 'my_genres' ), 'attributes' => array( 'placeholder' => 'What kind of show...' ),
And now you can add taxonomy items via Select2. But … It doesn’t save the taxonomy data.
Saving The Taxonomy Data
This is the part of code I’m not thrilled about. You see, the code in the previous section adds a new postmeta field for theshows_genre
with an array of the IDs added. And that’s it. That isn’t what I wanted. I certainly could use the postmeta data to generate the output, but I used Taxonomies for a reason. They’re incredibly useful.
In order to save the data, I needed to take the content from the post meta and copy it into the values for saved taxonomies, but only sometimes. After kicking around the options, I decided that I would give priority to the postmeta, not the taxonomies. That would allow me to have them save the taxonomies all the time unless the post meta was empty.
function select2_taxonomy_process( $post_id, $postmeta, $taxonomy ) { $get_post_meta = get_post_meta( $post_id, $postmeta, true ); $get_the_terms = get_the_terms( $post_id, $taxonomy ); if ( is_array( $get_post_meta ) ) { // If we already have the post meta, then we should set the terms $get_post_meta = array_map( 'intval', $get_post_meta ); $get_post_meta = array_unique( $get_post_meta ); $set_the_terms = array(); foreach( $get_post_meta as $term_id ) { $term = get_term_by( 'id' , $term_id, $taxonomy ); array_push( $set_the_terms, $term->slug ); } wp_set_object_terms( $post_id, $set_the_terms , $taxonomy ); } elseif ( $get_the_terms && ! is_wp_error( $get_the_terms ) ) { // If there's no post meta, we force the terms to be the default $get_post_meta = array(); foreach( $get_the_terms as $term ) { $term_id = $term->term_id; array_push( $get_post_meta, $term_id ); } update_post_meta( $post_id, $postmeta, $get_post_meta ); } }
This is not perfect code. It’s not even very good code, I don’t think. I’m not happy that I had to break the terms out instead of just using the the content from $get_post_meta
but for some reason, wp_set_object_terms()
wasn’t happy with an array of terms. It was fine with the slugs, so that’s the way I went.
The logic is basic. If there’s postmeta and it’s an array, it ‘wins.’ If it’s not, take the taxonomy data and push it into the term.
Triggering The Save
But how to trigger that code? And where and when?
I wrote code for my custom post type that triggered a check every time the page was loaded. Which is why I don’t like it.
add_action( 'init', 'select2_taxonomy_save' ); function select2_taxonomy_save() { // Force saving data to convert select2 saved data to a taxonomy $post_id = ( isset( $_GET['post'] ) )? $_GET['post'] : 0 ; if ( $post_id !== 0 && is_admin() ) { $post_type = ( isset( $_GET['post_type'] ) )? $_GET['post_type'] : 0 ; switch ( $post_type ) { case 'post_type_shows': LP_CMB2_Addons::select2_taxonomy_save( $post_id, 'theshows_tropes', 'my_tropes' ); LP_CMB2_Addons::select2_taxonomy_save( $post_id, 'theshows_tvgenre', 'my_genres' ); break; } } }
Obviously it’s not the best code out there. It runs too often, though at least it’s only on page loads.. It would be better if it only ran on save, however that had a problem with race conditions. I would end up with a case where the postmeta might still be blank. So having it run before the page loaded appeared to be my only hope. I also don’t like having the data stored twice, but there was a limit to how far I wanted to run with this.
Pull requests welcome!