I’ve been using this for a while without issue. I sometimes modify a field group imported from the parent within the child theme. It simply saves a new acf-json file into the child theme.
This used to work fine but I my posts are suddenly only rendering the group as defined in the parent. So I had to flip the load order though I have no idea why.
add_filter('acf/settings/load_json', function($paths) {
$paths = array();
if(is_child_theme())
{
$paths[] = get_stylesheet_directory() . '/acf-json';
}
$paths[] = get_template_directory() . '/acf-json';
return $paths;
});
Just a quick update. For an upgrade-proof workaround, just save the entire contents of the file I posted earlier in this thread to your theme and use require_once
within your functions.php file.
require_once 'class-acf-location-post-template.php
It will override the built in one and provide the fix.
I created a acf ticket but don’t have a link to it yet.
For my situation a post template makes sense. I created an online training system where each lesson is comprised of a series of Topics which are custom post types. Some topics include a piece of audio which requires its own acf group. Being able to leverage all of the core topic logic and yet create an alternative version via a template is a perfect solution.
I was able to modify /includes/locations/class-acf-location-post-template.php
with a workaround. I’ll post the whole file here for clarity. I added a class method acf_location_post_template::acf_get_post_templates()
then changed both calls to the native get_post_templates()
<?php
if( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
if( ! class_exists('acf_location_post_template') ) :
class acf_location_post_template extends acf_location {
/*
* __construct
*
* This function will setup the class functionality
*
* @type function
* @date 5/03/2014
* @since 5.0.0
*
* @param n/a
* @return n/a
*/
function initialize() {
// vars
$this->name = 'post_template';
$this->label = __("Post Template",'acf');
$this->category = 'post';
$this->public = acf_version_compare('wp', '>=', '4.7');
}
/*
* get_post_type
*
* This function will return the current post_type
*
* @type function
* @date 25/11/16
* @since 5.5.0
*
* @param $options (int)
* @return (mixed)
*/
function get_post_type( $screen ) {
// vars
$post_id = acf_maybe_get( $screen, 'post_id' );
$post_type = acf_maybe_get( $screen, 'post_type' );
// post_type
if( $post_type ) return $post_type;
// $post_id
if( $post_id ) return get_post_type( $post_id );
// return
return false;
}
/*
* rule_match
*
* This function is used to match this location $rule to the current $screen
*
* @type function
* @date 3/01/13
* @since 3.5.7
*
* @param $match (boolean)
* @param $rule (array)
* @return $options (array)
*/
function rule_match( $result, $rule, $screen ) {
// vars
$templates = array();
$post_id = acf_maybe_get( $screen, 'post_id' );
$page_template = acf_maybe_get( $screen, 'page_template' );
$post_type = $this->get_post_type( $screen );
// bail early if no post_type found (not a post)
if( !$post_type ) return false;
// get templates (WP 4.7)
if( acf_version_compare('wp', '>=', '4.7') ) {
// $templates = wp_get_theme()->get_post_templates();
$templates = $this->acf_get_post_templates();
}
// 'page' is always a valid pt even if no templates exist in the theme
// allows scenario where page_template = 'default' and no templates exist
if( !isset($templates['page']) ) {
$templates['page'] = array();
}
// bail early if this post type does not allow for templates
if( !isset($templates[ $post_type ]) ) return false;
// get page template
if( !$page_template ) {
$page_template = get_post_meta( $post_id, '_wp_page_template', true );
}
// new post - no page template
if( !$page_template ) $page_template = "default";
// match
return $this->compare( $page_template, $rule );
}
/*
* rule_operators
*
* This function returns the available values for this rule type
*
* @type function
* @date 30/5/17
* @since 5.6.0
*
* @param n/a
* @return (array)
*/
function rule_values( $choices, $rule ) {
// vars
$choices = array(
'default' => apply_filters( 'default_page_template_title', __('Default Template', 'acf') )
);
// get templates (WP 4.7)
if( acf_version_compare('wp', '>=', '4.7') ) {
// $templates = wp_get_theme()->get_post_templates();
$templates = $this->acf_get_post_templates();
$choices = array_merge($choices, $templates);
}
// return choices
return $choices;
}
function acf_get_post_templates() {
$post_types = acf_get_post_types(array(
'exclude' => array('attachment')
));
$post_templates = array();
foreach ($post_types as $post_type) {
if ( $files = wp_get_theme()->get_page_templates(null, $post_type) ) {
if ( ! isset( $post_templates[ $post_type ] ) ) {
$post_templates[ $post_type ] = array();
}
$post_templates[ $post_type ] = $files;
}
}
return $post_templates;
}
}
// initialize
acf_register_location_rule( 'acf_location_post_template' );
endif; // class_exists check
?>
Ok thanks @hube2. I had assumed a ticket already existed for this.
I did some digging and turns out it might be a bug in WP. ACF uses wp_get_theme()->get_post_templates();
to get available templates as it probably should but WP_Theme::get_post_templates()
fails to request files from the parent theme folder and there are no params or hooks to force it to.
I created a ticket on trac. https://core.trac.wordpress.org/ticket/41717
Going to take a shot at a workaround and I’ll submit here and in a new ticket if it works.
Just got hit by this. Hoping to hear about a resolution soon.
@hube2 Your load_value
version was spot on. Good work!
Brilliant! Thanks.
It actually required a bit of a hack. I can’t post the entire plugin but I can try to hit the highlights. These are just snippets from my larger code base so try and just grasp the concepts.
First enqueue ACF
add_action('admin_enqueue_scripts', array( $this, 'on_admin_enqueue_scripts' ) );
function on_admin_enqueue_scripts() {
// global
global $pagenow;
if ( 'nav-menus.php' != $pagenow ) {
return;
}
wp_enqueue_script('acf-input');
wp_enqueue_style('acf-mpa');
}
Now you need to dequeue the native walker that normally renders the menu admin and replace it with one you copied and edited to include the acf field.
include_once( 'edit_custom_walker.php' );
add_filter( 'wp_edit_nav_menu_walker', array( $this, 'acf_menu_edit_walker'), 1, 2 );
function acf_menu_edit_walker($walker, $menu_id) {
$options = get_option( 'dcdc_menu_partials' );
$keys = array_keys($options['menu_ids']);
if ( in_array($menu_id, $keys) ) {
remove_all_filters('wp_edit_nav_menu_walker' );
return 'Walker_Nav_Menu_Edit_Custom';
}
return $walker;
}
Part of your custom walker needs an acf_form()
<!-- end normal -->
<p>
<?php
$GLOBALS['current_menu_item_id'] = $item_id;
$acf_options = array(
'post_id' => $item_id,
'form' => false,
/* (array) An array of field group IDs/keys to override the fields displayed in this form */
'field_groups' => array('group_573e71ff44db1'),
'return' => '',
);
acf_form( $acf_options );
?>
</p>
<!-- end acf -->
Then you have to catch the acf form value when the menu is being saved
add_action( 'wp_update_nav_menu_item', array( $this, 'update_custom_nav_fields'), 10, 3 );
function update_custom_nav_fields ($menu_id, $menu_item_db_id, $args) {
if ( isset( $_REQUEST['acf']['field_573e727f24ef4'] ) && is_array( $_REQUEST['acf']['field_573e727f24ef4'] ) ) {
$field_value = $_REQUEST['acf']['field_573e727f24ef4'][$menu_item_db_id];
$field = get_field_object('field_573e727f24ef4', $menu_item_db_id, false, false);
acf_update_value( $field_value, $menu_item_db_id, $field );
}
}
This also a requires a custom nav walker for the front end in order to pick up on the field data. Hope that helps
UPDATE: apparently the actual selector is a sibling of the <select>
tag.
$expirationSelect2.siblings('input').select2("open");
appears to work.
Update: The selected terms actually do save correctly, they’re just reflected correctly in the edit. ie. If I select two checkboxes, that will save but all boxes will be checked when I edit the term.
I haven’t had a chance to try this with an up to date ACF but at least I can better explain what I’m seeing.
I have a custom post type, Announcement which has two dedicated Taxonomies: Announcement-Type and Announcement-Audience. The idea is that each Announcement-Type term needs to select which Audiences it’s targeting. That way, in the Announcement post editor, choosing an announcement-type (from an ACF taxonomy field shown as a radio group) will change the available options in an audience-type taxonomy field shown as checkboxes. I’m handling the view updates in javascript.
So in my Announcement-Type term editor I’ve inserted the Annoucement-type taxonomy field. This field has “Load Terms” set to Yes.
The result in the term editor is that all boxes are ticked all the time.
And even if I change the selection for a term,
The changes are not saved and all terms have all options ticked
If I set Load Terms to ‘No’, everything works fine.
Sure, I’ll show some screens tomorrow to describe the issue better.
So I modified your solution with the intent making the value optional. I’m not very good with SQL so I doubt my use of the AND
statement is the cleanest way to do that.
function acf_get_terms( $args = array() ) {
global $wpdb;
$defaults = array(
'taxonomy_slug' => '',
'acf_field_name' => null,
'meta_value' => '',
);
$args = wp_parse_args( $args, $defaults );
if ( empty($args['taxonomy_slug']) || ! taxonomy_exists( $args['taxonomy_slug'] ) || empty( $args['acf_field_name'] ) ) {
return new WP_Error( 'invalid_option_name', __('ACF term meta names are formatted as {$term->taxonomy}_{$term->term_id}_{$field[\'name\']}.') );
}
$rows = $wpdb->get_results($wpdb->prepare(
"
SELECT option_name
FROM {$wpdb->prefix}options
WHERE option_name LIKE %s
AND option_value LIKE %s
",
$args['taxonomy_slug'] . '_%_' . $args['acf_field_name'],
empty($args['meta_value']) ? '%' : $args['meta_value']
));
$unit_ids = array();
foreach( $rows as $row ){
preg_match('/^' . $args['taxonomy_slug'] . '_(\d*)_' . $args['acf_field_name'] . '$/', $row->option_name, $matches);
$unit_ids[] = $matches[1];
}
$terms = get_terms(array(
'taxonomy' => $args['taxonomy_slug'],
'hide_empty' => false,
'include' => $unit_ids,
));
return $terms;
}
Thanks @James,
It wasn’t until my drive home that I realized the lack of a wp_options api meant I was probably going to have to resort to wpdb. I’ll check it out tomorrow but this looks like exactly what I was after. Cheers.
Sadly, this would be easier if termsmeta was adopted.
Thanks for the suggestion @jonathan
I actually got it working by manually enqueueing acf-input
rather than go the do_action()
route. Race conditions I guess.
add_action('admin_enqueue_scripts', 'on_admin_enqueue_scripts' );
function on_admin_enqueue_scripts() {
// global
global $pagenow;
if ( 'nav-menus.php' != $pagenow ) {
return;
}
wp_enqueue_script('acf-input');
}
@James,
Thanks for the suggestion. I actually came up with a slightly more elegant solution.
First I pluralize the name attribute by hooking into 'acf/prepare_field'
// My field group contains only a single Post Object field
add_filter('acf/prepare_field/type=post_object', 'my_acf_prepare_field' );
function my_acf_prepare_field ( $field ) {
$field['_input'] = $field['_input'] . '[]';
return $field;
}
That will create an array of entries in $_POST
[acf] => Array
(
[field_573e727f24ef4] => Array
(
[0] => 170
[1] => 152
[2] => 155
[3] => 170
)
[_validate_email] =>
)
This creates 2 issues however.
1. The array keys are kinda meaningless to me.
2. acf_form_head()
cannot process this. IMO it should be able to in a future version.
To fix the first issue I had to store a post id in $GLOBALS
at the time I was calling acf_form()
// leaving out some context here but this demonstrates the idea
$GLOBALS['current_menu_item_id'] = $item_id;
$acf_options = array(
'post_id' => $item_id,
'form' => false,
'field_groups' => array('group_573e71ff44db1'),
'return' => '',
);
acf_form( $acf_options );
Now we can modify our earlier prepare_field
handler to pluraize with meaning
function my_acf_prepare_field ( $field ) {
$field['_input'] = $field['_input'] . '[' . $GLOBALS['current_menu_item_id'] . ']';
return $field;
}
Which creates something in $_POST
we can use during our fix for the second issue.
[acf] => Array
(
[field_573e727f24ef4] => Array
(
[172] => 170
[173] => 152
[174] => 155
[175] => 170
)
[_validate_email] =>
)
To fix the second issue I removed acf_form_head()
and called acf’s meta update manually. In my case I was saving changes to a Menu so I just hooked into the relevant filter but this could be done wherever you’re handling your form submit. This is inspired by Remi Corson
// save menu custom fields
add_action( 'wp_update_nav_menu_item', 'my_update_custom_nav_fields', 10, 3 );
function my_update_custom_nav_fields ($menu_id, $menu_item_db_id, $args) {
if ( is_array( $_REQUEST['acf']['field_573e727f24ef4'] ) ) {
$field_value = $_REQUEST['acf']['field_573e727f24ef4'][$menu_item_db_id];
$field = get_field_object('field_573e727f24ef4', $menu_item_db_id, false, false);
acf_update_value( $field_value, $menu_item_db_id, $field );
}
}
Still no luck, but I did notice that acf.php
and acf-pro.php
each use wp_register_script()
with different dependency params where the latter has none. Why is that?
Ooooh! I get it now. I’ll just create a non-hierarchical taxonomy and offer that as my open tag field. Thanks!
From my understanding, Tag’s and Taxonomy+Terms are very similar other than a Taxonomies ability to be hierarchical. I’m building a custom post type with a front end form for creating new posts. In the form I intend to allow choosing category(s) and adding tags. On the archive for this post type I want to list all of the categories for easy filtering. I don’t want that category list to be cluttered with every tag ever used. I feel they should be separate types of meta-data. Is there a way I can pull that off using just the Taxonomy field type?
My code’s not bullet proof for sure, but given a certain environment+process, it should work. So I’ll attempt to describe my setup to see if we differ.
First. Do not use the wp-post-meta-revisions
plugin. Even if you don’t use its required filter, I fear it creates a race condition with ACF. There may be a more elegant way to tie the two plugins together but I couldn’t find one. This could be an issue if it ever becomes part of core.
Ok, so as far as front end EDITS, acf_form_head()
must come before get_header()
as explained here. In fact, that basic code example should be all you need. I haven’t tested this where a specific post_id is passed to acf_form’s post_id argument although that should work too.
My code is only intended to be ran while processing an acf_form()
so it assumes $_POST['acf']
will exist.
As far as your 3 revisions, wordpress is kinda funny. I tested on a clean wordpress install with no plugins and the twentyfifteen theme and when you create a new post and publish, you won’t see the revisions meta box. Then if you change the content, title or except and click update, the revisions meta box will show (if you have it in screen options) and two revisions will be there. They are your first ‘publish’ and first ‘update’. This doesn’t explain why you see 3 after your first front end edit. Perhaps you could share more about your setup.
Ok so I finally have something that so far appears to be pretty solid. It’s basically hooking into acf filters to trigger our own revision and then copying values over. This hasn’t been tested for creating NEW posts with acf_form, just editing them.
function my_pre_save_post( $post_id ) {
// bail early if editing in admin
// ACF already handles admin revisions correctly
if( is_admin() ) {
return $post_id;
}
// force a post update, this will generate a revision and
// trigger the '_wp_put_post_revision' action
wp_update_post( get_post($post_id) );
// allow acf to update the parent post meta normally
return $post_id;
}
add_filter('acf/pre_save_post' , 'my_pre_save_post' );
// revision just got created
// detect all of the acf fields on the original and
// apply them to the revision
function _on_wp_put_post_revision( $revision_id ) {
// bail early if editing in admin
if( is_admin() ) {
return;
}
// get the revision post object
$revision = get_post( $revision_id );
// get the id of the original post
$post_id = $revision->post_parent;
// A lot of this is copied from ACF's revision class
// get all the post meta from the original post
$custom_fields = get_post_custom( $post_id );
if( !empty($custom_fields) ) {
foreach( $custom_fields as $k => $v ) {
// value is always an array
$v = $v[0];
// bail early if $value is not is a field_key
if( !acf_is_field_key($v) ) {
continue;
}
// remove prefix '_' field from reference
$field_name = substr($k, 1);
// update the field value using the POST value supplied in the form
update_field($field_name, $_POST['acf'][$v], $revision_id);
// add the reference key
update_metadata('post', $revision_id, $k, $v);
}
}
}
add_action( '_wp_put_post_revision', '_on_wp_put_post_revision' );
UPDATE: disregard this hack. See next post for a more complete solution.
Ok so I’ve made a little progress on the concept of revisioning with acf_form() but is admittedly a hack. It will revision a post but for some reason, not until the 2nd change. I’m still working on it but I’m posting it here in hopes that someone else can expand on or replace with a more elegant solution.
Basically I’m hijacking ‘pre_save_post’ and instead of letting it alter post meta for the existing post, I’m forcing the creation of a revision and letting acf act on that new post ID instead. Of course we still want the active post to get updated so I’m manually calling an un-documented acf function which obviously is a very bad approach.
function my_pre_save_post( $post_id ) {
// bail early if editing in admin
if( is_admin() ) {
return;
}
// bail if this is a brand new post
if ( ! is_numeric($post_id) ) {
return $post_id;
}
acf_save_post( $post_id );
add_filter( 'wp_save_post_revision_check_for_changes', 'on_forced_revision', 10, 3 );
$new_id = wp_save_post_revision( $post_id );
remove_filter( 'wp_save_post_revision_check_for_changes', 'on_forced_revision', 10 );
// return the new ID
return $new_id;
}
add_filter('acf/pre_save_post' , 'my_pre_save_post', 1, 2 );
function on_forced_revision($check_for_changes, $last_revision, $post) {
if( isset($_POST['_acfchanged']) && $_POST['_acfchanged'] == '1' )
{
return false;
}
return $check_for_changes;
}
@idealien I don’t see how this is correct for changes made on the front end.
ACF handles it’s own revisions very similar to how wp-post-meta-revisions does. They both react to filters applied as a result of wp_insert_post which is only called when updates are made in the admin. acf_form_head() however only updates metadata on a single post ID. It does not trigger any functions that would engage ACF’s revisioning or wp-post-meta-revisions revisioning. Altering WP_Query won’t reveal any revisions resulting from use of acf_form() because it doesn’t create any.
I didn’t realize ACF has already solved the admin side revisions. So the Post Meta Revisions Plugin is not needed not that it integrates well anyway. Because of ACF’s multiple meta-keys per field setup, a plugin native solution is most desirable. This is the last piece of a big problem so I would really love to hear from @elliot how this is coming along.
Welcome to the Advanced Custom Fields community forum.
Browse through ideas, snippets of code, questions and answers between fellow ACF users
Helping others is a great way to earn karma, gain badges and help ACF development!
We use cookies to offer you a better browsing experience, analyze site traffic and personalize content. Read about how we use cookies and how you can control them in our Cookie Policy. If you continue to use this site, you consent to our use of cookies.