Tutorial: Custom Field Integration - mcguffin/acf-quickedit-fields GitHub Wiki

Release 2.4.1 introduced some filters and actions, allowing you to integrate your own implementation of QuickEdit, BulkEdit or Column views.

Update Notice: In version 3.0.0 window.acf_quickedit has been renamed to window.acf_qef. 3.0.1 also silently deprecated the acf_qef_update_{$field_type} hook. Data to save is just getting passed to acf_save_post().

Our use case: Tables

Let's say we want to add QuickEdit functionality for the fabulous ACF Table Field. As we are all coders, we cannot stand any kind of convenient UI, and want to edit the Table Data in markdown instead.

First Thing we need is some Code to convert the data stored by the Table Plugin to markdown and some more code which does the opposite.

The ACF Table plugin stores tables in a json encoded string. Here is what it looks converted to a PHP array:

[
    'acftf'	=> [ 'version' => '1.2.3' ],
    'b' => [
        [ [ 'c' => 'Cell 1_1'], [ 'c' => 'Cell 1_2'] ],
        [ [ 'c' => 'Cell 2_1'], [ 'c' => 'Cell 2_2'] ]
    ],
    'c' => [ [ 'p' => ''], [ 'p' => ''] ],
    'h' => [ [ 'c' => 'Head #1'], [ 'c' => 'Head #2'] ],
    'p' => [ 'o' => ['uh' => 1 ] ],
]

This is what we want to achieve:

| Head #1 | Head #2 |
| --- | --- |
| Cell 1_1 | Cell 1_2 |
| Cell 2_1 | Cell 2_2 |

Here is our Markdown-Generator:

function generate_markdown_table( $acf_table_json ) {

    // check if we can decode it.
    if ( ! $acf_table = json_decode( $acf_table_json, true ) ){
        return '';
    }

    // flatten nested array
    $join_line = function($line) {
        return $line['c'];
    };

    $output = [];

    // Does it have a table header?
    if ( $acf_table['p']['o']['uh'] ) {
        $output[] = array_map( $join_line, $acf_table['h'] );
        $output[] = array_fill( 0, count( $acf_table['c'] ), '---' );
    }

    // generate table rows
    foreach ( $acf_table['b'] as $row ) {
        $output[] = array_map( $join_line, $row );
    }

    // generate markdown lines for each row
    $output = array_map(function($row){
        return '| ' . implode( ' | ', $row ) . ' |';
    }, $output );

    // and join everything to a single string.
    return implode("\n", $output );
}

The opposite function might be a bit counter-intuitive. Upon saving, the table Plugin expects a $_POST value which looks pretty much like this:

[
    'body' => [
        [ [ 'c' => 'Cell 1_1' ], [ 'c' => 'Cell 1_2' ] ],
        [ [ 'c' => 'Cell 2_1' ], [ 'c' => 'Cell 2_2' ] ],
    ],
    'header' => [ [ 'c' => 'Head #1'], [ 'c' => 'Head #2'] ]
]

So, here is our markdown parser:

function parse_markdown_table( $markdown ) {

    $header_pattern = '@^([\|\-\s\n\r]+)$@imsU';

    // split into lines
    $rows = preg_split( "/[\r\n]+/ims", $markdown );

    // split rows into cells ...
    $rows = array_map( function( $row ) {
        // rm leading and trailing pipes ...
        $row = trim($row,'| ');
        // make an array out of it ...
        $row = preg_split( '/\s?\|{1}\s?/ims', $row );
        // nest it one level deeper
        return array_map( function( $c ) {
            return [ 'c' => $c ];
        }, $row );
    }, $rows );

    if ( preg_match( $header_pattern, $markdown ) ) {
        $value = [
            'header' => $rows[0],
            'body'   => array_slice( $rows , 2 ),
        ];
    } else {
        $value = [
            'header' => false,
            'body'   => $rows,
        ];
    }
    return $value;
}

Enable Column View

We will use the acf_quick_edit_fields_types filter to make Column and Edit features available in the Field Editor:

add_filter('acf_quick_edit_fields_types',function($types){
    // add support for table fields. The array key is just the field type...
    $types['table'] = [
        'column' => true,
        'quickedit' => false,
        'bulkedit' => false
    ];
    return $types;
});

Now we can configure the Column View for the field. Configure Table Field

Although not very much to see in the posts list yet... image

... unless we add another filter named acf_qef_column_html_{$field_type} ...

add_filter('acf_qef_column_html_table',function( $html, $object_id, $acf_field ) {

    return '<pre>' . generate_markdown_table( get_field( $acf_field['key'], $object_id, false ) ) . '</pre>';

}, 10, 3 );

... which is much better!

image

Enable Editing features

We already see a very nice and codish markdown table in the posts list. Let's make it editable now, using the acf_quick_edit_fields_types filter:

add_filter('acf_quick_edit_fields_types',function($types){
    // add support for table fields. The array key is just the field type...
    $types['table'] = [
        'column' => true,
        'quickedit' => true, // this is true now!
        'bulkedit' => true // and that on too.
    ];
    return $types;
});

Edit Features enabled

We have to take care of four things now:

  • Create a form element for the table field.
  • In JS load the value into form element
  • Format the field value, before it's loaded into the form element
  • Save the value

The Form-Element

As we will just enter text a Textarea will be the appropriate. We hook into the acf_qef_input_html_{$field_type} filter.

add_filter('acf_qef_input_html_table',function( $html, $input_atts, $is_quickedit, $acf_field ) {
    // the $input_atts arg already holds the necessary attributes like 'name', 'id', and such
    $input_atts += array(
        'class'	=> 'code',
    );
    // We know ACF 5+ is active. So using acf_esc_attr() shouldn't be a problem
    return '<textarea ' . acf_esc_attr( $input_atts ) . '></textarea>';

}, 10, 4 );

The somewhat disappointing result: Quick Edit Field empty

The form element doesn't have a value yet. This is where the JS-Part begins.

The JS Part

The JS Object window.acf_qef has a method for adding new field types.
Basically we only need to specify a field type and an init function.

(function( $, qe ){

    qe.field.add_type( {
        type: 'table',
        initialize:function(){
            // the field should know it's $input element
            this.$input = this.$('textarea');
            // call parent object's init function
            qe.field.View.prototype.initialize.apply( this,arguments );
        },
    } );

})( jQuery, window.acf_qef );

Wrapped in the proper WP action it will look like this:

add_action('admin_enqueue_scripts',function(){
    $script = <<<'EOT'
(function($,qe){
    qe.field.add_type( {
        type: 'table',
        initialize:function(){
            // the field should know it's $input element
            this.$input = this.$('textarea');
            // call parent object's init function
            qe.field.View.prototype.initialize.apply( this,arguments );
        },
    } );
})( jQuery, window.acf_qef );
EOT;
    wp_add_inline_script( 'acf-quickedit', $script, 'after');
});

The Result:

Quick Edit Field with Garbage

Yikes! Editing raw json data isn't exactly what we wanted.
There's still some work to be done.

Parsing the field value

The acf_qef_get_value_{$field_type} filter allows us to format the value before it's pased to the form element.

We just generate our markdown table here.

add_filter('acf_qef_get_value_table',function( $value, $object_id, $format_value, $acf_field ) {

    return generate_markdown_table( $value );

}, 10, 4 );

The result looks good. Finally!

Quick Edit Field with Value

And thats all.

All the parts put together

For a better copy paste experience.

// generate table string
function generate_markdown_table( $acf_table_json ) {

    // check if we can decode it.
    if ( ! $acf_table = json_decode( $acf_table_json, true ) ){
        return '';
    }

    // flatten nested array
    $join_line = function($line) {
        return $line['c'];
    };

    $output = [];

    // Does it have a table header?
    if ( $acf_table['p']['o']['uh'] ) {
        $output[] = array_map( $join_line, $acf_table['h'] );
        $output[] = array_fill( 0, count( $acf_table['c'] ), '---' );
    }

    // generate table rows
    foreach ( $acf_table['b'] as $row ) {
        $output[] = array_map( $join_line, $row );
    }

    // generate markdown lines for each row
    $output = array_map(function($row){
        return '| ' . implode( ' | ', $row ) . ' |';
    }, $output );

    // and join everything to a single string.
    return implode("\n", $output );
}

// parse table string
function parse_markdown_table( $markdown ) {

    $header_pattern = '@^([\|\-\s\n\r]+)$@imsU';

    // split into lines
    $rows = preg_split( "/[\r\n]+/ims", $markdown );

    // split rows into cells ...
    $rows = array_map( function( $row ) {
        // rm leading and trailing pipes ...
        $row = trim($row,'| ');
        // make an array out of it ...
        $row = preg_split( '/\s?\|{1}\s?/ims', $row );
        // nest it one level deeper
        return array_map( function( $c ) {
            return [ 'c' => $c ];
        }, $row );
    }, $rows );

    if ( preg_match( $header_pattern, $markdown ) ) {
        $value = [
            'header' => $rows[0],
            'body'   => array_slice( $rows , 2 ),
        ];
    } else {
        $value = [
            'header' => false,
            'body'   => $rows,
        ];
    }
    return $value;
}


// Enable features
add_filter('acf_quick_edit_fields_types',function($types){
    // add support for table fields. The array key is just the field type...
    $types['table'] = [
        'column' => true,
        'quickedit' => true,
        'bulkedit' => true
    ];
    return $types;
});

// column view
add_filter('acf_qef_column_html_table',function( $html, $object_id, $acf_field ) {

    return '<pre>' . generate_markdown_table( get_field( $acf_field['key'], $object_id, false ) ) . '</pre>';

}, 10, 3 );

// Form-Element
add_filter('acf_qef_input_html_table',function( $html, $input_atts, $is_quickedit, $acf_field ) {
    // the $input_atts arg already holds the necessary attributes like 'name', 'id', and such
    $input_atts += array(
        'class'	=> 'code',
    );
    // We know ACF 5+ is active. So using acf_esc_attr() shouldn't be a problem
    return '<textarea ' . acf_esc_attr( $input_atts ) . '></textarea>';

}, 10, 4 );

// JS stuff
add_action('admin_enqueue_scripts',function(){
    $script = <<<'EOT'
(function($,qe){
    qe.field.add_type( {
        type: 'table',
        initialize:function(){
            // the field should know it's $input element
            this.$input = this.$('textarea');
            // call parent object's init function
            qe.field.View.prototype.initialize.apply( this,arguments );
        },
    } );
})( jQuery, window.acf_qef );
EOT;
    wp_add_inline_script( 'acf-quickedit', $script, 'after');
});

// Parse field value
add_filter('acf_qef_get_value_table',function( $value, $object_id, $format_value, $acf_field ) {

    return generate_markdown_table( $value );

}, 10, 4 );
⚠️ **GitHub.com Fallback** ⚠️