Claude Skill

wc-product-search-select

Builds a WooCommerce-style AJAX product search select (the selectWoo / wooselect dropdown) — class="wc-product-search" + the data-action attribute pointing to woocommerce_json_search_products (products only) or woocommerce_json_search_products_and_variations (products AND variati

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download lonsdale201-wp-agent-skills-woocommerce_wc-product-search-select-52f6020.zip · 6 KB
Part of lonsdale201/wp-agent-skills — 226 skills

Install

skills CLI npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/woocommerce/wc-product-search-select
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
Git git clone https://github.com/Lonsdale201/wp-agent-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

WooCommerce: AJAX product search select (wooselect / selectWoo)

For UIs where the user picks from products (and optionally variations) — meta boxes, settings pages, dashboard widgets. The mistake AI assistants consistently make is loading the entire product catalog into a static <select> upfront. WC has a built-in AJAX endpoint for exactly this, with proper variation support, and the wiring is two HTML attributes plus a server-side pre-render of the saved options.

Misconception this skill corrects

"I'll query all products with posts_per_page = -1 and feed them into a <select> for the user to pick from."

A WC store can have 20,000+ products plus 10× that in variations. Loading them all server-side is a hard timeout on render and a hard browser-freeze on render-into-DOM. WC ships an AJAX search endpoint for exactly this case — and it has a separate variant that includes variations.

When to use this skill

Trigger when ANY of the following is true:

  • Building a product picker in any plugin admin UI (meta box, settings page, dashboard widget, modal, custom column inline editor).
  • The user mentions "select2 with products", "product autocomplete", "dropdown of products" in WC context.
  • Reviewing code where you see <select> populated by a WP_Query/get_posts over product post type — replace that pattern.
  • The diff contains wc-product-search, selectWoo, wc-enhanced-select, or data-action="woocommerce_json_search_*".

The two AJAX endpoints

WC ships two product-search AJAX actions (wp-content/plugins/woocommerce/includes/class-wc-ajax.php:1768, 1844):

Action name What it returns
woocommerce_json_search_products Products only (no variations).
woocommerce_json_search_products_and_variations Products AND variations.

The _and_variations variant is what you want most of the time for store-side features (related products, upsell/cross-sell, stock-rule targets, etc.) — variations are independently priced, stocked, and SKU'd, so a UI that can't pick variations is incomplete.

The variations endpoint is a one-line wrapper around the products one (self::json_search_products( '', true ) — $include_variations = true); same nonce, same data shape, the only difference is the result set.

Visibility and capability note (WC 10.8)

The AJAX handler filters each candidate through wc_products_array_filter_readable() before sending JSON. WooCommerce 10.8 fixed hidden-product search visibility, so do not assume hidden/private products appear for every admin-like request. If a previously saved hidden product must remain visible in your field, the pre-rendered <option selected> loop is what preserves its label; the live search result list should still respect WC readability/capability rules.

Minimal scaffold — meta box on the product edit screen

const MYPLUGIN_META = '_myplugin_related_products';

add_action( 'add_meta_boxes_product', static function (): void {
    add_meta_box(
        'myplugin-related-products',
        __( 'Related products', 'myplugin' ),
        'myplugin_render_related_products_box',
        'product',
        'side',
        'default'
    );
} );

function myplugin_render_related_products_box( WP_Post $post ): void {
    $saved_ids = (array) get_post_meta( $post->ID, MYPLUGIN_META, true );
    $saved_ids = array_filter( array_map( 'absint', $saved_ids ) );

    wp_nonce_field( 'myplugin_save_related', 'myplugin_nonce' );
    ?>
    <select
        id="myplugin-related-products"
        name="<?php echo esc_attr( MYPLUGIN_META ); ?>[]"
        class="wc-product-search"
        multiple="multiple"
        style="width: 100%;"
        data-placeholder="<?php esc_attr_e( 'Search products and variations…', 'myplugin' ); ?>"
        data-action="woocommerce_json_search_products_and_variations"
        data-exclude="<?php echo intval( $post->ID ); ?>"
        data-sortable="true"
    >
        <?php
        // SERVER-SIDE pre-render of the saved options. The AJAX endpoint
        // only fires on user typing — without this loop, the select renders
        // empty even when meta has saved IDs.
        foreach ( $saved_ids as $product_id ) {
            $product = wc_get_product( $product_id );
            if ( $product instanceof WC_Product ) {
                echo '<option value="' . esc_attr( (string) $product_id ) . '" selected="selected">'
                    . esc_html( wp_strip_all_tags( $product->get_formatted_name() ) )
                    . '</option>';
            }
        }
        ?>
    </select>
    <?php
}

add_action( 'save_post_product', static function ( int $post_id, WP_Post $post ): void {
    if ( ! isset( $_POST['myplugin_nonce'] ) ) return;
    if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['myplugin_nonce'] ) ), 'myplugin_save_related' ) ) return;
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
    if ( ! current_user_can( 'edit_product', $post_id ) ) return;

    $raw = isset( $_POST[ MYPLUGIN_META ] ) ? (array) wp_unslash( $_POST[ MYPLUGIN_META ] ) : array();
    $ids = array_values( array_filter( array_map( 'absint', $raw ) ) );

    if ( empty( $ids ) ) {
        delete_post_meta( $post_id, MYPLUGIN_META );
        return;
    }
    update_post_meta( $post_id, MYPLUGIN_META, $ids );
}, 10, 2 );

That's the whole pattern. WC's wc-enhanced-select JS picks up the wc-product-search class on DOM-ready and turns the plain <select> into a selectWoo (a WC fork of select2) with AJAX search wired to the action you specified.

Critical rules

1. class="wc-product-search" is the trigger

The WC enhanced-select script (wp-content/plugins/woocommerce/assets/js/admin/wc-enhanced-select.js) auto-initializes any <select> with this class. Without the class, your select stays a plain HTML control.

2. data-action controls products-only vs products+variations

<!-- Products only -->
data-action="woocommerce_json_search_products"

<!-- Products AND variations (most common) -->
data-action="woocommerce_json_search_products_and_variations"

If you omit data-action entirely, the JS defaults to woocommerce_json_search_products_and_variations (verified in wc-enhanced-select.js's ajax data handler). Be explicit anyway; readers shouldn't have to chase JS defaults.

3. ALWAYS pre-render selected options server-side

The AJAX search runs only on user input (with minimumInputLength: 3 by default). It does not run on initial render. Without server-side pre-rendered <option selected> tags, the select shows empty even when meta is populated — the IDs are in the DB but the select doesn't know how to label them.

Pattern: wc_get_product( $id )->get_formatted_name() returns the WC-styled label including SKU, attributes, and parent product for variations (e.g. T-Shirt - Color: Red, Size: M (#42)). Use it.

4. Useful data-* attributes

Attribute Purpose Example
data-placeholder Empty-state placeholder Search products…
data-action Which AJAX action to call woocommerce_json_search_products_and_variations
data-exclude ID or JSON array of IDs to exclude 42 or [42,43]
data-include Restrict results to an ID or JSON array [12,15]
data-limit Requested result limit 100 (default 30, filter woocommerce_json_search_limit)
data-exclude_type Comma-list of product types to skip external,grouped
data-display_stock Append " — Stock: N" to labels for managed-stock items 1
data-allow_clear Show a clear (x) control on single-value selects 1
data-minimum_input_length Override default 3-char minimum 2
data-sortable="true" Enable drag-sort on multi-select chips true

Do not pass multiple IDs as a comma-separated string to data-exclude or data-include: the PHP handler casts the request to an array and then applies absint, so "12,15" collapses to 12. Core emits multiple IDs as JSON ([12,15]), which jQuery parses into an array. data-exclude_type is the exception: its handler explicitly accepts a comma-delimited value.

5. Nonce is internal — don't reinvent

WC's AJAX handler verifies check_ajax_referer( 'search-products', 'security' ) (class-wc-ajax.php:1769). The nonce is auto-attached by wc-enhanced-select.js from the wc_enhanced_select_params.search_products_nonce PHP-localized variable. Don't try to add your own nonce to the AJAX request — WC handles it.

6. wc-enhanced-select is auto-enqueued on WC admin screens

WC enqueues wc-enhanced-select (plus selectWoo, plus the matching styles) on every screen returned by wc_get_screen_ids() (wp-content/plugins/woocommerce/includes/admin/class-wc-admin-assets.php:412-415). That covers: product edit / new screens, orders, coupons, shipping, settings, etc. You do not need to manually enqueue on those.

For the same <select> markup on a non-WC admin page (your plugin's settings page, a dashboard widget, a modal in the post-type editor of another CPT), explicit enqueue IS needed:

add_action( 'admin_enqueue_scripts', static function ( string $hook_suffix ): void {
    if ( $hook_suffix !== 'my-plugin_page_my-settings' ) return; // gate to your screen

    wp_enqueue_script( 'wc-enhanced-select' );
    wp_enqueue_style( 'woocommerce_admin_styles' );
} );

The handles are stable: wc-enhanced-select (JS) and woocommerce_admin_styles (CSS). The dependency on selectWoo is declared internally by the WC handle; you don't need to enqueue selectWoo separately.

7. Save handler treats input as untrusted

The <select multiple> posts as an array of strings. Always array_map( 'absint', $raw ) and array_filter to drop empties. Never trust the IDs back from the form — a user can craft a <option value="9999999"> and submit it. If the ID space matters (the picked products must be readable to the current user), revalidate with wc_get_product( $id ) and current_user_can( 'read_product', $id ) before storing.

Common mistakes

// WRONG — load all products upfront, freeze the page on stores with > a few hundred
$products = get_posts( array( 'post_type' => 'product', 'posts_per_page' => -1 ) );
echo '<select multiple>';
foreach ( $products as $p ) {
    echo '<option value="' . $p->ID . '">' . $p->post_title . '</option>';
}
echo '</select>';

// WRONG — missing pre-render of saved values; on edit the select shows empty
echo '<select class="wc-product-search" name="related_products[]"
        multiple="multiple"
        data-action="woocommerce_json_search_products_and_variations"></select>';
// Saved IDs are in DB but invisible in the UI; user thinks the data is lost.

// WRONG — products only when variations are needed
data-action="woocommerce_json_search_products"
// Cross-sell-style features almost always need to target specific variations.

// WRONG — manually enqueueing on the product edit screen (redundant)
add_action( 'admin_enqueue_scripts', function () {
    wp_enqueue_script( 'wc-enhanced-select' ); // already loaded by WC here
} );
// Harmless but adds noise. Only enqueue manually on NON-WC admin screens.

// WRONG — saving raw $_POST without sanitization
update_post_meta( $post_id, '_related_products', $_POST['related_products'] );
// Allows attacker to inject arbitrary IDs / strings.

// RIGHT — sanitize-then-save
$ids = array_values( array_filter( array_map( 'absint', (array) wp_unslash( $_POST['related_products'] ?? array() ) ) ) );
update_post_meta( $post_id, '_related_products', $ids );

Reading the saved IDs at runtime

$ids = (array) get_post_meta( $post_id, '_myplugin_related_products', true );
foreach ( $ids as $id ) {
    $product = wc_get_product( (int) $id );
    if ( ! $product instanceof WC_Product ) {
        continue; // product was deleted
    }

    if ( $product->is_type( 'variation' ) ) {
        // Variation-specific handling: $product->get_parent_id(), get_attributes()
    } else {
        // Regular / variable / grouped / external
    }

    // For frontend display:
    echo esc_html( $product->get_name() );
    echo wc_price( (float) $product->get_price() );
}

The IDs returned by the search endpoint are post IDs that may belong to either product or product_variation post types. wc_get_product() handles both transparently.

Cross-references

  • Run wc-shipping-method if the broader plugin context is shipping — the shipping zones admin uses the same Backbone-modal pattern with PHP-rendered HTML; nothing in WC admin is React-only.
  • Run wp-plugin-architecture for the broader question of how to organize the meta box class file alongside other plugin code.
  • Run wp-security-audit on the save handler — it's an admin-context write endpoint with attacker-controlled input.

What this skill does NOT cover

  • Building a product picker for the block editor / Gutenberg product fields. Those use a different React-based component (wc/product-control from @woocommerce/components); not the same wiring.
  • The frontend product search (the customer-facing search bar). Different endpoint, different scope.
  • WC REST API product search (/wc/v3/products with ?search=). That's for external integrations, not admin UI; this skill is admin-UI-specific.
  • Customizing the result label rendering beyond what get_formatted_name() returns. The endpoint returns plain text labels via wp_strip_all_tags; richer renderings require a custom AJAX handler or a per-result formatter, both out of scope here.
  • Variation-attribute filtering (e.g. "show only red variations") at search time. The endpoint matches on title / SKU / ID, not attribute values.

References

Files (wp-agent-skills)
  • SKILL.md 16 KB
    ---
    name: wc-product-search-select
    description: Builds a WooCommerce-style AJAX product search select (the selectWoo / wooselect dropdown) — class="wc-product-search" + the data-action attribute pointing to woocommerce_json_search_products (products only) or woocommerce_json_search_products_and_variations (products AND variations, often the right choice). Pre-selected items rendered server-side as pre-selected option markup via wc_get_product() + get_formatted_name(); WC's wc-enhanced-select script auto-enqueued on WC admin screens, explicit enqueue required on non-WC pages. Solves the "load 20k products into a select" antipattern AI assistants commonly emit. Use when adding a product picker meta box, a custom WC admin page selector, or any UI where the user must search and pick from many products. Triggers on wc-product-search, woocommerce_json_search_products, woocommerce_json_search_products_and_variations, selectWoo, wc-enhanced-select, "select2 products in WooCommerce", or product-picker meta box scaffolding.
    metadata:
      wp-skills-author: "Soczó Kristóf"
      wp-skills-contact: "mailto:lonsdale201@hotmail.com"
      wp-skills-plugin: "woocommerce"
      wp-skills-plugin-version-tested: "11.0.0"
      wp-skills-php-min: "7.4"
      wp-skills-last-updated: "2026-08-05"
    ---
    
    # WooCommerce: AJAX product search select (wooselect / selectWoo)
    
    For UIs where the user picks from products (and optionally variations) — meta boxes, settings pages, dashboard widgets. The mistake AI assistants consistently make is loading the entire product catalog into a static `<select>` upfront. WC has a built-in AJAX endpoint for exactly this, with proper variation support, and the wiring is two HTML attributes plus a server-side pre-render of the saved options.
    
    ## Misconception this skill corrects
    
    > "I'll query all products with `posts_per_page = -1` and feed them into a `<select>` for the user to pick from."
    
    A WC store can have 20,000+ products plus 10× that in variations. Loading them all server-side is a hard timeout on render and a hard browser-freeze on render-into-DOM. WC ships an AJAX search endpoint for exactly this case — and it has a separate variant that includes variations.
    
    ## When to use this skill
    
    Trigger when ANY of the following is true:
    
    - Building a product picker in any plugin admin UI (meta box, settings page, dashboard widget, modal, custom column inline editor).
    - The user mentions "select2 with products", "product autocomplete", "dropdown of products" in WC context.
    - Reviewing code where you see `<select>` populated by a `WP_Query`/`get_posts` over `product` post type — replace that pattern.
    - The diff contains `wc-product-search`, `selectWoo`, `wc-enhanced-select`, or `data-action="woocommerce_json_search_*"`.
    
    ## The two AJAX endpoints
    
    WC ships two product-search AJAX actions ([wp-content/plugins/woocommerce/includes/class-wc-ajax.php:1768, 1844](class-wc-ajax.php)):
    
    | Action name | What it returns |
    |---|---|
    | `woocommerce_json_search_products` | Products only (no variations). |
    | `woocommerce_json_search_products_and_variations` | Products **AND** variations. |
    
    The `_and_variations` variant is what you want **most of the time** for store-side features (related products, upsell/cross-sell, stock-rule targets, etc.) — variations are independently priced, stocked, and SKU'd, so a UI that can't pick variations is incomplete.
    
    The variations endpoint is a one-line wrapper around the products one (`self::json_search_products( '', true )` — `$include_variations = true`); same nonce, same data shape, the only difference is the result set.
    
    ### Visibility and capability note (WC 10.8)
    
    The AJAX handler filters each candidate through `wc_products_array_filter_readable()` before sending JSON. WooCommerce 10.8 fixed hidden-product search visibility, so do not assume hidden/private products appear for every admin-like request. If a previously saved hidden product must remain visible in your field, the pre-rendered `<option selected>` loop is what preserves its label; the live search result list should still respect WC readability/capability rules.
    
    ## Minimal scaffold — meta box on the product edit screen
    
    ```php
    const MYPLUGIN_META = '_myplugin_related_products';
    
    add_action( 'add_meta_boxes_product', static function (): void {
        add_meta_box(
            'myplugin-related-products',
            __( 'Related products', 'myplugin' ),
            'myplugin_render_related_products_box',
            'product',
            'side',
            'default'
        );
    } );
    
    function myplugin_render_related_products_box( WP_Post $post ): void {
        $saved_ids = (array) get_post_meta( $post->ID, MYPLUGIN_META, true );
        $saved_ids = array_filter( array_map( 'absint', $saved_ids ) );
    
        wp_nonce_field( 'myplugin_save_related', 'myplugin_nonce' );
        ?>
        <select
            id="myplugin-related-products"
            name="<?php echo esc_attr( MYPLUGIN_META ); ?>[]"
            class="wc-product-search"
            multiple="multiple"
            style="width: 100%;"
            data-placeholder="<?php esc_attr_e( 'Search products and variations…', 'myplugin' ); ?>"
            data-action="woocommerce_json_search_products_and_variations"
            data-exclude="<?php echo intval( $post->ID ); ?>"
            data-sortable="true"
        >
            <?php
            // SERVER-SIDE pre-render of the saved options. The AJAX endpoint
            // only fires on user typing — without this loop, the select renders
            // empty even when meta has saved IDs.
            foreach ( $saved_ids as $product_id ) {
                $product = wc_get_product( $product_id );
                if ( $product instanceof WC_Product ) {
                    echo '<option value="' . esc_attr( (string) $product_id ) . '" selected="selected">'
                        . esc_html( wp_strip_all_tags( $product->get_formatted_name() ) )
                        . '</option>';
                }
            }
            ?>
        </select>
        <?php
    }
    
    add_action( 'save_post_product', static function ( int $post_id, WP_Post $post ): void {
        if ( ! isset( $_POST['myplugin_nonce'] ) ) return;
        if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['myplugin_nonce'] ) ), 'myplugin_save_related' ) ) return;
        if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
        if ( ! current_user_can( 'edit_product', $post_id ) ) return;
    
        $raw = isset( $_POST[ MYPLUGIN_META ] ) ? (array) wp_unslash( $_POST[ MYPLUGIN_META ] ) : array();
        $ids = array_values( array_filter( array_map( 'absint', $raw ) ) );
    
        if ( empty( $ids ) ) {
            delete_post_meta( $post_id, MYPLUGIN_META );
            return;
        }
        update_post_meta( $post_id, MYPLUGIN_META, $ids );
    }, 10, 2 );
    ```
    
    That's the whole pattern. WC's `wc-enhanced-select` JS picks up the `wc-product-search` class on DOM-ready and turns the plain `<select>` into a selectWoo (a WC fork of select2) with AJAX search wired to the action you specified.
    
    ## Critical rules
    
    ### 1. `class="wc-product-search"` is the trigger
    
    The WC enhanced-select script ([wp-content/plugins/woocommerce/assets/js/admin/wc-enhanced-select.js](wc-enhanced-select.js)) auto-initializes any `<select>` with this class. Without the class, your select stays a plain HTML control.
    
    ### 2. `data-action` controls products-only vs products+variations
    
    ```html
    <!-- Products only -->
    data-action="woocommerce_json_search_products"
    
    <!-- Products AND variations (most common) -->
    data-action="woocommerce_json_search_products_and_variations"
    ```
    
    If you omit `data-action` entirely, the JS defaults to `woocommerce_json_search_products_and_variations` (verified in `wc-enhanced-select.js`'s ajax `data` handler). Be explicit anyway; readers shouldn't have to chase JS defaults.
    
    ### 3. ALWAYS pre-render selected options server-side
    
    The AJAX search runs only on user input (with `minimumInputLength: 3` by default). It does not run on initial render. Without server-side pre-rendered `<option selected>` tags, the select shows empty even when meta is populated — the IDs are in the DB but the select doesn't know how to label them.
    
    Pattern: `wc_get_product( $id )->get_formatted_name()` returns the WC-styled label including SKU, attributes, and parent product for variations (e.g. `T-Shirt - Color: Red, Size: M (#42)`). Use it.
    
    ### 4. Useful `data-*` attributes
    
    | Attribute | Purpose | Example |
    |---|---|---|
    | `data-placeholder` | Empty-state placeholder | `Search products…` |
    | `data-action` | Which AJAX action to call | `woocommerce_json_search_products_and_variations` |
    | `data-exclude` | ID or JSON array of IDs to exclude | `42` or `[42,43]` |
    | `data-include` | Restrict results to an ID or JSON array | `[12,15]` |
    | `data-limit` | Requested result limit | `100` (default 30, filter `woocommerce_json_search_limit`) |
    | `data-exclude_type` | Comma-list of product types to skip | `external,grouped` |
    | `data-display_stock` | Append " — Stock: N" to labels for managed-stock items | `1` |
    | `data-allow_clear` | Show a clear (x) control on single-value selects | `1` |
    | `data-minimum_input_length` | Override default 3-char minimum | `2` |
    | `data-sortable="true"` | Enable drag-sort on multi-select chips | `true` |
    
    Do not pass multiple IDs as a comma-separated string to `data-exclude` or `data-include`: the PHP handler casts the request to an array and then applies `absint`, so `"12,15"` collapses to `12`. Core emits multiple IDs as JSON (`[12,15]`), which jQuery parses into an array. `data-exclude_type` is the exception: its handler explicitly accepts a comma-delimited value.
    
    ### 5. Nonce is internal — don't reinvent
    
    WC's AJAX handler verifies `check_ajax_referer( 'search-products', 'security' )` ([class-wc-ajax.php:1769](class-wc-ajax.php)). The nonce is auto-attached by `wc-enhanced-select.js` from the `wc_enhanced_select_params.search_products_nonce` PHP-localized variable. Don't try to add your own nonce to the AJAX request — WC handles it.
    
    ### 6. `wc-enhanced-select` is auto-enqueued on WC admin screens
    
    WC enqueues `wc-enhanced-select` (plus `selectWoo`, plus the matching styles) on every screen returned by `wc_get_screen_ids()` ([wp-content/plugins/woocommerce/includes/admin/class-wc-admin-assets.php:412-415](class-wc-admin-assets.php)). That covers: product edit / new screens, orders, coupons, shipping, settings, etc. **You do not need to manually enqueue on those.**
    
    For the same `<select>` markup on a **non-WC admin page** (your plugin's settings page, a dashboard widget, a modal in the post-type editor of another CPT), explicit enqueue IS needed:
    
    ```php
    add_action( 'admin_enqueue_scripts', static function ( string $hook_suffix ): void {
        if ( $hook_suffix !== 'my-plugin_page_my-settings' ) return; // gate to your screen
    
        wp_enqueue_script( 'wc-enhanced-select' );
        wp_enqueue_style( 'woocommerce_admin_styles' );
    } );
    ```
    
    The handles are stable: `wc-enhanced-select` (JS) and `woocommerce_admin_styles` (CSS). The dependency on selectWoo is declared internally by the WC handle; you don't need to enqueue selectWoo separately.
    
    ### 7. Save handler treats input as untrusted
    
    The `<select multiple>` posts as an array of strings. Always `array_map( 'absint', $raw )` and `array_filter` to drop empties. Never trust the IDs back from the form — a user can craft a `<option value="9999999">` and submit it. If the ID space matters (the picked products must be readable to the current user), revalidate with `wc_get_product( $id )` and `current_user_can( 'read_product', $id )` before storing.
    
    ## Common mistakes
    
    ```php
    // WRONG — load all products upfront, freeze the page on stores with > a few hundred
    $products = get_posts( array( 'post_type' => 'product', 'posts_per_page' => -1 ) );
    echo '<select multiple>';
    foreach ( $products as $p ) {
        echo '<option value="' . $p->ID . '">' . $p->post_title . '</option>';
    }
    echo '</select>';
    
    // WRONG — missing pre-render of saved values; on edit the select shows empty
    echo '<select class="wc-product-search" name="related_products[]"
            multiple="multiple"
            data-action="woocommerce_json_search_products_and_variations"></select>';
    // Saved IDs are in DB but invisible in the UI; user thinks the data is lost.
    
    // WRONG — products only when variations are needed
    data-action="woocommerce_json_search_products"
    // Cross-sell-style features almost always need to target specific variations.
    
    // WRONG — manually enqueueing on the product edit screen (redundant)
    add_action( 'admin_enqueue_scripts', function () {
        wp_enqueue_script( 'wc-enhanced-select' ); // already loaded by WC here
    } );
    // Harmless but adds noise. Only enqueue manually on NON-WC admin screens.
    
    // WRONG — saving raw $_POST without sanitization
    update_post_meta( $post_id, '_related_products', $_POST['related_products'] );
    // Allows attacker to inject arbitrary IDs / strings.
    
    // RIGHT — sanitize-then-save
    $ids = array_values( array_filter( array_map( 'absint', (array) wp_unslash( $_POST['related_products'] ?? array() ) ) ) );
    update_post_meta( $post_id, '_related_products', $ids );
    ```
    
    ## Reading the saved IDs at runtime
    
    ```php
    $ids = (array) get_post_meta( $post_id, '_myplugin_related_products', true );
    foreach ( $ids as $id ) {
        $product = wc_get_product( (int) $id );
        if ( ! $product instanceof WC_Product ) {
            continue; // product was deleted
        }
    
        if ( $product->is_type( 'variation' ) ) {
            // Variation-specific handling: $product->get_parent_id(), get_attributes()
        } else {
            // Regular / variable / grouped / external
        }
    
        // For frontend display:
        echo esc_html( $product->get_name() );
        echo wc_price( (float) $product->get_price() );
    }
    ```
    
    The IDs returned by the search endpoint are post IDs that may belong to either `product` or `product_variation` post types. `wc_get_product()` handles both transparently.
    
    ## Cross-references
    
    - Run **`wc-shipping-method`** if the broader plugin context is shipping — the shipping zones admin uses the same Backbone-modal pattern with PHP-rendered HTML; nothing in WC admin is React-only.
    - Run **`wp-plugin-architecture`** for the broader question of how to organize the meta box class file alongside other plugin code.
    - Run **`wp-security-audit`** on the save handler — it's an admin-context write endpoint with attacker-controlled input.
    
    ## What this skill does NOT cover
    
    - Building a product picker for the **block editor / Gutenberg** product fields. Those use a different React-based component (`wc/product-control` from `@woocommerce/components`); not the same wiring.
    - The frontend product search (the customer-facing search bar). Different endpoint, different scope.
    - WC REST API product search (`/wc/v3/products` with `?search=`). That's for external integrations, not admin UI; this skill is admin-UI-specific.
    - Customizing the result label rendering beyond what `get_formatted_name()` returns. The endpoint returns plain text labels via `wp_strip_all_tags`; richer renderings require a custom AJAX handler or a per-result formatter, both out of scope here.
    - Variation-attribute filtering (e.g. "show only red variations") at search time. The endpoint matches on title / SKU / ID, not attribute values.
    
    ## References
    
    - `WC_AJAX::json_search_products` and `json_search_products_and_variations`: [wp-content/plugins/woocommerce/includes/class-wc-ajax.php](class-wc-ajax.php) (lines ~1768 and 1844).
    - `wc-enhanced-select.js` — the JS that turns `class="wc-product-search"` into selectWoo with AJAX wiring: [wp-content/plugins/woocommerce/assets/js/admin/wc-enhanced-select.js](wc-enhanced-select.js).
    - WC auto-enqueue on admin screens: [wp-content/plugins/woocommerce/includes/admin/class-wc-admin-assets.php:412-415](class-wc-admin-assets.php).
    - `WC_Data_Store::load( 'product' )->search_products()` — the underlying query method called by the AJAX handler. Worth reading if you need a programmatic equivalent of the AJAX search (e.g. WP-CLI command).
    - Product search query implementation: [wp-content/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php](class-wc-product-data-store-cpt.php) — `search_products()`.
    - selectWoo (WooCommerce's select2 fork): [github.com/woocommerce/selectWoo](https://github.com/woocommerce/selectWoo).
    - Official documentation: <https://woocommerce.com/document/woocommerce-json-search/>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related