Here is a way to achieve the validation while typing the value in the ACF field, put the code inside your theme’s functions.php or create a plugin for it:
You will need to change the $fieldName, $fieldKey properties values to the correct values of your field —they are the field name and field key of the same field we are validating—
class MyPrefix_MetaFieldValidation
{
protected static $instance;
protected $fieldName="original-title-mcpedl"; // <- Change this with the ACF field name
protected $fieldKey = 'field_61abaa164244a'; // <- Change this with the field key
protected $ajaxAction = 'myprefix_validate_title';
protected function __construct()
{
}
public function run()
{
add_filter('acf/validate_value/key=' . $this->fieldKey, [$this, 'acfValidation'], 10, 2);
add_action('wp_ajax_' . $this->ajaxAction, [$this, 'ajaxValidation']);
add_action('admin_footer', [$this, 'ajaxValidationJS']);
}
/**
* Print the Validation JS on the post edit screen.
*/
public function ajaxValidationJS()
{
if(! $this->isPostEditScreen()) {
return;
}
$nonce = wp_create_nonce($this->ajaxAction);
?>
<script>
jQuery(document).ready(function(){
jQuery('#acf-<?= $this->fieldKey; ?>').on('keyup', function (){
var field = jQuery(this);
var value = field.val();
if(! value) {
return;
}
var post_id = jQuery('#post_ID').val();
var formatError = function(msg) {
return '<div class="acf-notice -error acf-error-message"><p>' + msg + '</p></div>';
}
jQuery.ajax({
url: ajaxurl,
data: {"action": "<?= $this->ajaxAction; ?>", "nonce" : "<?= $nonce; ?>", "post_id" : post_id, "meta_value" : value },
type: "POST",
complete : function(response) {
var json = response.responseJSON;
field.parents('.acf-input').find('.acf-notice').remove();
if(response.status != 200) {
field.parents('.acf-input').prepend(formatError('Network or server error!'));
return;
}
if(! json.valid) {
field.parents('.acf-input').prepend(formatError(json.msg));
}
}
});
});
});
</script>
<?php
}
/**
* Perform the ajax validation on the meta field
*/
public function ajaxValidation()
{
$postId = $_REQUEST['post_id'] ? intval($_REQUEST) : 0;
$metaValue = $_REQUEST['meta_value'] ? $_REQUEST['meta_value'] : '';
$nonce = $_REQUEST['nonce'] ? $_REQUEST['nonce'] : '';
if(! wp_verify_nonce($nonce, $this->ajaxAction)) {
$this->ajaxResponse([
'valid' => false,
'msg' => 'Invalid nonce supplied'
]);
}
if(! $metaValue) {
$this->ajaxResponse([
'valid' => true,
'msg' => 'Empty value'
]);
}
$existingTitle = $this->metaValueExists($this->fieldName, $metaValue, $postId);
if($existingTitle) {
$this->ajaxResponse([
'valid' => false,
'msg' => 'This title is already exists for the post id ' . $existingTitle->post_id,
]);
}
$this->ajaxResponse([
'valid' => true,
'msg' => '',
]);
}
public function acfValidation($valid, $value)
{
if(empty($value)) {
return $valid;
}
$postId = isset($_POST['post_id']) ? intval($_POST['post_id']) : null;
$existingTitle = $this->metaValueExists($this->fieldName, $value, $postId);
if($existingTitle) {
$valid = 'This title is already exists for the post id ' . $existingTitle->post_id;
}
return $valid;
}
/**
* Create an ajax response
* @param array $data
*/
protected function ajaxResponse($data)
{
header('Content-Type: application/json;charset=UTF-8');
echo json_encode($data);
exit;
}
/**
* Check if we are on the post editing screen
* @return bool
*/
protected function isPostEditScreen()
{
global $pagenow;
return (is_admin() && ('post.php' === $pagenow || 'post-new.php' === $pagenow));
}
/**
* Check if a given meta value is already in the database
* @param string $metaKey
* @param string $metaValue
* @param null|int $currentPostId
* @return false|object
*/
protected function metaValueExists($metaKey, $metaValue, $currentPostId = null) {
// Return early if the supplied meta key or value is empty
$metaValue = trim($metaValue);
if (empty($metaKey) || empty($metaValue)) {
return false;
}
// Ensure that the supplied post id is integer
$currentPostId = intval($currentPostId);
// Query the wp_postmeta table for the meta value and meta key
global $wpdb;
$query = "SELECT * FROM $wpdb->postmeta WHERE meta_key LIKE '%s' AND meta_value LIKE '%s'";
// Exclude the current post id only if it is not empty
if($currentPostId) {
$query .= " AND post_id != $currentPostId";
}
$result = $wpdb->get_row($wpdb->prepare($query, $metaKey, $metaValue));
// Return the database row object or false if no record found
return ($result) ?: false;
}
public static function getInstance()
{
if(! static::$instance) {
static::$instance = new static();
}
return static::$instance;
}
}
// Run our plugin
MyPrefix_MetaFieldValidation::getInstance()->run();
Here’s whate we have done in the previous code:
I put all our code in a separate class “MyPrefix_MetaFieldValidation”
I changed the code you put in your question a bit so that the validation method “metaValueExists” is separated from the method that runs on “acf/validate_value”, because we need to use it in another place.
Also, I changed the query responsible for getting the duplicate record from the database to be more lightweight.
I removed the use of the hidden custom field that holds the post id, as there is already a field that holds the current post id.
I created a new admin ajax action “ajaxValidation” that consumes the validation method and responds with a JSON result.
A JS code uses jQuery to create an ajax request whenever the user types a value inside the custom field that we want to valide and displays an error if the value he typed is duplicate.
Note: This code will only show an error to the user while inserting a value in the custom field, but won’t prevent the user from saving the post as a draft.