Developers

Engancha tu plugin al ciclo de guardado

Gutenberg For WC Products es un framework. Si tu plugin guarda datos del producto mediante su propia petición AJAX, registra un "saver" y tus datos persistirán al pulsar Actualizar en el editor de bloques: sin forks, sin endpoints REST, sin serializar campos a mano. El arreglo de variaciones de WooCommerce incluido es, en sí mismo, un saver registrado mediante esta misma API pública.

02

¿De verdad lo necesitas?

Un meta box normal cuyos campos se guardan en save_post leyendo $_POST ya persiste al pulsar Actualizar gracias a la capa de compatibilidad de meta boxes de WordPress: no necesita nada de este plugin. Solo necesitas un saver cuando tu guardado está atado al envío del formulario clásico mediante un canal AJAX propio (con su acción, nonce y control de cambios), igual que las variaciones de WooCommerce.

Ya persiste — no hagas nadaNecesita un saver
Campos de meta box guardados en save_post vía $_POST.Guardados atados al envío del formulario clásico mediante tu propio AJAX (pestaña/panel con botón Guardar, control de cambios, manejador admin-ajax/REST).

03

Por qué tu guardado AJAX se rompe en Gutenberg

En el editor clásico, tu guardado se ejecuta porque está atado al evento submit del formulario #post. El editor de bloques no tiene ese formulario: Actualizar guarda el post vía REST, tu manejador de submit nunca se dispara y tu llamada AJAX nunca se envía. La solución fiable es re-disparar tu propio guardado en el ciclo de guardado de Gutenberg, que es justo lo que hace un saver. Este plugin no puede autodetectar y disparar tu canal AJAX privado (tiene su propia acción, nonce y control de cambios), así que te apuntas registrando un saver.

04

El patrón, en tres partes

  1. Un marcador "dirty" que tu interfaz añade cuando el usuario cambia algo.
  2. Tu guardado propio (un botón que dispara tu AJAX), igual que en el editor clásico.
  3. Un saver que pulsa ese botón cuando el marcador está presente.

Las partes 1 y 2 ya las tienes. La parte 3 es la única línea que lo hace funcionar en Gutenberg.

05

API JavaScript — el registro de savers

Un saver se ejecuta cada vez que Gutenberg termina de guardar un producto (autoguardados excluidos). Re-dispara una rutina de guardado nativa que el editor de bloques se saltaría. Encola tu script con gfwcp-save-bridge como dependencia, para que window.gfwcp exista antes de que corra tu código. Hay dos formas de registrar un saver: la API declarativa registerSaver (recomendada) y el filtro de bajo nivel gfwcp.productSavers (vía de escape).

Forma declarativa: filas dirty + un botón

El caso más común — "cuando estas filas tengan cambios, pulsa este botón" — no necesita lógica. El saver generado es idempotente: si ningún nodo cumple dirtySelector no hace nada (devuelve false); si no, habilita y pulsa buttonSelector (devuelve true).

register-saver.js
window.gfwcp.registerSaver( {
    id: 'my-plugin/my-panel',            // unique; duplicate ids are ignored
    dirtySelector: '#my_panel .my-row.needs-update',
    buttonSelector: 'button.my-plugin-save'
} );

Control total: tu propio callback

Cuando tu guardado no es un simple clic de botón (una llamada AJAX propia, varios pasos), pasa run. Devuelve true solo cuando hayas actuado; devuelve false para mantenerte idempotente.

custom-saver.js
window.gfwcp.registerSaver( {
    id: 'my-plugin/custom',
    run: function () {
        if ( ! jQuery( '#my_panel .dirty' ).length ) {
            return false; // nothing to do — stay idempotent
        }
        // trigger your own save here; return true if you acted.
        return true;
    }
} );

El filtro gfwcp.productSavers (bajo nivel)

El registro es applyFilters( 'gfwcp.productSavers', savers ) en cada guardado, donde savers ya contiene todo lo registrado con registerSaver. Usa el filtro para añadir un saver como función simple o para inspeccionar/modificar la lista. El filtro debe devolver un array; un retorno que no sea array se trata como "sin savers".

product-savers-filter.js
wp.hooks.addFilter(
    'gfwcp.productSavers',
    'my-plugin/my-saver',
    function ( savers ) {
        savers.push( function () {
            if ( ! jQuery( '#my_plugin_panel .my-row.needs-update' ).length ) {
                return false; // nothing to do — stay idempotent
            }
            jQuery( 'button.my-plugin-save' ).trigger( 'click' );
            return true;
        } );
        return savers;
    }
);

Notas

  • El registro se reevalúa en cada guardado, así que los savers añadidos tras la carga también se ejecutan.
  • Mantén los savers idempotentes: actúa solo cuando hay estado dirty, replicando el comportamiento nativo de WooCommerce. Un panel que nunca se abrió no tiene filas, así que el saver es un no-op seguro.
  • Un saver que lanza error se captura y no detiene a los demás; pon window.GFWCP_DEBUG = true para ver esos errores vía console.warn. Aun así, gestiona tus propios errores.

06

Integrar un plugin que guarda vía AJAX

Este es el patrón completo y ejecutable para plugins cuyos datos de producto se guardan mediante su propia petición AJAX. Replica la extensión de demo incluida que usa la batería de tests, así que se sabe que funciona. Tres partes: renderiza el panel con una fila dirty y tu botón de guardado; maneja el guardado AJAX con nonce + capacidad + sanitización; encola el saver tras el puente de guardado.

PHP — render del panel, guardado AJAX y encolado del saver

plugin-ajax.php
<?php
defined( 'ABSPATH' ) || exit;

// 1) Render your panel (a meta box here) with a dirty row and your own save button.
add_action( 'add_meta_boxes_product', function () {
    add_meta_box(
        'myplugin_panel_box',
        'My Plugin Panel',
        function ( $post ) {
            $value = get_post_meta( $post->ID, '_myplugin_value', true );
            echo '<div id="myplugin_panel">';
            printf(
                '<input type="text" id="myplugin_value" class="widefat" value="%s" />',
                esc_attr( $value )
            );
            echo '<p class="myplugin-row">';
            echo '<button type="button" class="button myplugin-save">Save value</button>';
            echo '</p></div>';
        },
        'product',
        'side'
    );
} );

// 2) Your own AJAX save: nonce + capability + sanitize input + (escape on output).
add_action( 'wp_ajax_myplugin_save', function () {
    check_ajax_referer( 'myplugin_ajax', 'nonce' );

    $post_id = isset( $_POST['post_id'] ) ? absint( $_POST['post_id'] ) : 0;
    if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
        wp_send_json_error( array( 'message' => 'forbidden' ), 403 );
    }

    $value = isset( $_POST['value'] ) ? sanitize_text_field( wp_unslash( $_POST['value'] ) ) : '';
    update_post_meta( $post_id, '_myplugin_value', $value );

    wp_send_json_success( array( 'value' => $value ) );
} );

// 3) Enqueue your client logic after the save-bridge, using the documented action so that
//    window.gfwcp already exists. gfwcp_after_enqueue only fires on product block-editor
//    screens under the default bridge mode.
add_action( 'gfwcp_after_enqueue', function ( $screen ) {
    if ( ! $screen || 'product' !== $screen->post_type ) {
        return;
    }

    wp_add_inline_script(
        'gfwcp-save-bridge',
        'window.myPluginData = ' . wp_json_encode(
            array( 'nonce' => wp_create_nonce( 'myplugin_ajax' ) )
        ) . ';',
        'after'
    );
    wp_add_inline_script( 'gfwcp-save-bridge', myplugin_inline_js(), 'after' );
} );

JavaScript del cliente (inline)

La lógica de cliente: marca el panel como dirty al cambiar, ejecuta tu guardado AJAX con tu botón, y registra el saver para que el puente pulse tu botón al Actualizar.

inline-js.php
function myplugin_inline_js() {
    return <<<'JS'
( function ( wp, $ ) {
    if ( ! window.gfwcp || ! wp || ! wp.data || ! $ ) {
        return;
    }
    var data = window.myPluginData || {};

    // 1) Mark the panel dirty on change.
    $( document ).on( 'input', '#myplugin_value', function () {
        $( '#myplugin_panel .myplugin-row' ).addClass( 'myplugin-needs-update' );
    } );

    // 2) Your own AJAX save, triggered by your button.
    $( document ).on( 'click', '#myplugin_panel button.myplugin-save', function () {
        var postId = wp.data.select( 'core/editor' ).getCurrentPostId();
        $.post( window.ajaxurl, {
            action: 'myplugin_save',
            nonce: data.nonce,
            post_id: postId,
            value: $( '#myplugin_value' ).val()
        } ).done( function () {
            $( '#myplugin_panel .myplugin-row' ).removeClass( 'myplugin-needs-update' );
        } );
    } );

    // 3) Register the saver: when dirty, the bridge clicks your button on "Update".
    window.gfwcp.registerSaver( {
        id: 'my-plugin/panel',
        dirtySelector: '#myplugin_panel .myplugin-row.myplugin-needs-update',
        buttonSelector: '#myplugin_panel button.myplugin-save'
    } );
} )( window.wp, window.jQuery );
JS;
}

Eso es todo. Cuando el usuario edita el campo y pulsa Actualizar, el puente ejecuta tu saver, que pulsa tu botón, que dispara tu guardado AJAX existente.

Si entregas un archivo JS aparte en vez de inline

Encólalo con gfwcp-save-bridge (y jquery si lo usas) como dependencia para que window.gfwcp esté definido antes de que corra tu código.

enqueue-saver.php
add_action( 'gfwcp_after_enqueue', function ( $screen ) {
    if ( ! $screen || 'product' !== $screen->post_type ) {
        return;
    }
    wp_enqueue_script(
        'my-plugin-saver',
        plugins_url( 'js/saver.js', __FILE__ ),
        array( 'gfwcp-save-bridge', 'wp-data', 'jquery' ),
        '1.0.0',
        true
    );
    wp_localize_script( 'my-plugin-saver', 'myPluginData', array(
        'nonce' => wp_create_nonce( 'myplugin_ajax' ),
    ) );
} );

07

API PHP — filtros, acciones y constantes

Prefijo: gfwcp_. Añádelos al functions.php de tu tema o a tu propio plugin.

Constante GFWCP_MODE

Selecciona la estrategia sin código en tiempo de ejecución (p. ej. en wp-config.php). El filtro gfwcp_mode gana sobre la constante.

mode-constant.php
define( 'GFWCP_MODE', 'classic' ); // 'bridge' (default) or 'classic'

Filtro gfwcp_mode

Sobrescribe la estrategia programáticamente.

mode-filter.php
add_filter( 'gfwcp_mode', function ( $mode ) {
    return 'classic'; // force Strategy A (variable products use the classic editor)
} );

Filtro gfwcp_post_types

Cambia qué post types reciben el editor de bloques + el puente de guardado (por defecto array( 'product' )).

post-types.php
add_filter( 'gfwcp_post_types', function ( $post_types ) {
    $post_types[] = 'my_custom_product';
    return $post_types;
} );

Filtro gfwcp_route_to_classic

Solo Estrategia A. Decide por post si va al editor clásico.

route-to-classic.php
add_filter( 'gfwcp_route_to_classic', function ( $route_to_classic, $post ) {
    // Keep ALL products in Gutenberg even under classic mode:
    return false;
}, 10, 2 );

Acciones gfwcp_before_enqueue / gfwcp_after_enqueue

Solo Estrategia B. Se disparan alrededor del encolado del puente; reciben el WP_Screen actual. gfwcp_after_enqueue es el lugar recomendado para encolar un script que registra un saver JS, porque solo corre en pantallas de producto del editor de bloques y tras gfwcp-save-bridge (así window.gfwcp existe).

after-enqueue.php
add_action( 'gfwcp_after_enqueue', function ( $screen ) {
    wp_enqueue_script( 'my-extra-saver', /* ... */ );
} );

08

Elegir la estrategia (bridge / classic)

Por defecto, Estrategia B (bridge): Gutenberg en todos los productos + el puente de guardado de variaciones. Para usar la Estrategia A (alternativa clásica) — los productos variables se editan en el editor clásico — define la constante o el filtro.

wp-config.php
// wp-config.php
define( 'GFWCP_MODE', 'classic' );
functions.php
// functions.php
add_filter( 'gfwcp_mode', fn() => 'classic' );

09

Extender a otros post types

Añade el editor de bloques + el puente de guardado a tu propio post type.

extend-post-type.php
add_filter( 'gfwcp_post_types', function ( $types ) {
    $types[] = 'my_custom_product';
    return $types;
} );

10

Reglas a seguir

  • Saver idempotente: actúa solo cuando tu marcador dirty está presente, y quítalo cuando tu AJAX termine. Un saver que siempre se dispara enviaría peticiones espurias en cada guardado.
  • Seguridad en el manejador AJAX: verifica el nonce, comprueba current_user_can(), sanitiza toda entrada y escapa toda salida. Nunca pongas secretos en JS.
  • id único: registerSaver ignora un id duplicado, así que ponle namespace (my-plugin/...).
  • Errores: un saver que lanza error se captura y no detiene a los demás. Pon window.GFWCP_DEBUG = true durante el desarrollo para ver esos errores en la consola.

11

Cómo verificar que funciona

  1. Edita tu campo y, sin pulsar tu propio botón, pulsa Actualizar.
  2. Recarga la pantalla: tu valor debe persistir.
  3. Pulsa Actualizar de nuevo sin editar: tu AJAX NO debe enviarse (idempotencia).

La batería de tests incluida ejercita exactamente este flujo para el panel de demo (tests/e2e/declarative-api.spec.js).