{"slug":"wc-product-search-select","title":"wc-product-search-select","summary":"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","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-09-16T14:52:17.805233Z","repo":{"url":"https://github.com/Lonsdale201/wp-agent-skills","stars":22,"forks":2,"license":"MIT","updatedAt":"2026-09-26T23:03:36Z"},"bodyHtml":"<hr>\n<h2>name: wc-product-search-select\ndescription: 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.\nmetadata:\nwp-skills-author: \"Soczó Kristóf\"\nwp-skills-contact: \"mailto:lonsdale201@hotmail.com\"\nwp-skills-plugin: \"woocommerce\"\nwp-skills-plugin-version-tested: \"11.0.0\"\nwp-skills-php-min: \"7.4\"\nwp-skills-last-updated: \"2026-08-05\"</h2>\n<h1>WooCommerce: AJAX product search select (wooselect / selectWoo)</h1>\n<p>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 <code>&lt;select&gt;</code> 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.</p>\n<h2>Misconception this skill corrects</h2>\n<blockquote>\n<p>\"I'll query all products with <code>posts_per_page = -1</code> and feed them into a <code>&lt;select&gt;</code> for the user to pick from.\"</p>\n</blockquote>\n<p>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.</p>\n<h2>When to use this skill</h2>\n<p>Trigger when ANY of the following is true:</p>\n<ul>\n<li>Building a product picker in any plugin admin UI (meta box, settings page, dashboard widget, modal, custom column inline editor).</li>\n<li>The user mentions \"select2 with products\", \"product autocomplete\", \"dropdown of products\" in WC context.</li>\n<li>Reviewing code where you see <code>&lt;select&gt;</code> populated by a <code>WP_Query</code>/<code>get_posts</code> over <code>product</code> post type — replace that pattern.</li>\n<li>The diff contains <code>wc-product-search</code>, <code>selectWoo</code>, <code>wc-enhanced-select</code>, or <code>data-action=\"woocommerce_json_search_*\"</code>.</li>\n</ul>\n<h2>The two AJAX endpoints</h2>\n<p>WC ships two product-search AJAX actions (<a href=\"class-wc-ajax.php\">wp-content/plugins/woocommerce/includes/class-wc-ajax.php:1768, 1844</a>):</p>\n<table>\n<thead>\n<tr>\n<th>Action name</th>\n<th>What it returns</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>woocommerce_json_search_products</code></td>\n<td>Products only (no variations).</td>\n</tr>\n<tr>\n<td><code>woocommerce_json_search_products_and_variations</code></td>\n<td>Products <strong>AND</strong> variations.</td>\n</tr>\n</tbody>\n</table>\n<p>The <code>_and_variations</code> variant is what you want <strong>most of the time</strong> 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.</p>\n<p>The variations endpoint is a one-line wrapper around the products one (<code>self::json_search_products( '', true )</code> — <code>$include_variations = true</code>); same nonce, same data shape, the only difference is the result set.</p>\n<h3>Visibility and capability note (WC 10.8)</h3>\n<p>The AJAX handler filters each candidate through <code>wc_products_array_filter_readable()</code> 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 <code>&lt;option selected&gt;</code> loop is what preserves its label; the live search result list should still respect WC readability/capability rules.</p>\n<h2>Minimal scaffold — meta box on the product edit screen</h2>\n<pre><code>const MYPLUGIN_META = '_myplugin_related_products';\n\nadd_action( 'add_meta_boxes_product', static function (): void {\n    add_meta_box(\n        'myplugin-related-products',\n        __( 'Related products', 'myplugin' ),\n        'myplugin_render_related_products_box',\n        'product',\n        'side',\n        'default'\n    );\n} );\n\nfunction myplugin_render_related_products_box( WP_Post $post ): void {\n    $saved_ids = (array) get_post_meta( $post-&gt;ID, MYPLUGIN_META, true );\n    $saved_ids = array_filter( array_map( 'absint', $saved_ids ) );\n\n    wp_nonce_field( 'myplugin_save_related', 'myplugin_nonce' );\n    ?&gt;\n    &lt;select\n        id=\"myplugin-related-products\"\n        name=\"&lt;?php echo esc_attr( MYPLUGIN_META ); ?&gt;[]\"\n        class=\"wc-product-search\"\n        multiple=\"multiple\"\n        style=\"width: 100%;\"\n        data-placeholder=\"&lt;?php esc_attr_e( 'Search products and variations…', 'myplugin' ); ?&gt;\"\n        data-action=\"woocommerce_json_search_products_and_variations\"\n        data-exclude=\"&lt;?php echo intval( $post-&gt;ID ); ?&gt;\"\n        data-sortable=\"true\"\n    &gt;\n        &lt;?php\n        // SERVER-SIDE pre-render of the saved options. The AJAX endpoint\n        // only fires on user typing — without this loop, the select renders\n        // empty even when meta has saved IDs.\n        foreach ( $saved_ids as $product_id ) {\n            $product = wc_get_product( $product_id );\n            if ( $product instanceof WC_Product ) {\n                echo '&lt;option value=\"' . esc_attr( (string) $product_id ) . '\" selected=\"selected\"&gt;'\n                    . esc_html( wp_strip_all_tags( $product-&gt;get_formatted_name() ) )\n                    . '&lt;/option&gt;';\n            }\n        }\n        ?&gt;\n    &lt;/select&gt;\n    &lt;?php\n}\n\nadd_action( 'save_post_product', static function ( int $post_id, WP_Post $post ): void {\n    if ( ! isset( $_POST['myplugin_nonce'] ) ) return;\n    if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['myplugin_nonce'] ) ), 'myplugin_save_related' ) ) return;\n    if ( defined( 'DOING_AUTOSAVE' ) &amp;&amp; DOING_AUTOSAVE ) return;\n    if ( ! current_user_can( 'edit_product', $post_id ) ) return;\n\n    $raw = isset( $_POST[ MYPLUGIN_META ] ) ? (array) wp_unslash( $_POST[ MYPLUGIN_META ] ) : array();\n    $ids = array_values( array_filter( array_map( 'absint', $raw ) ) );\n\n    if ( empty( $ids ) ) {\n        delete_post_meta( $post_id, MYPLUGIN_META );\n        return;\n    }\n    update_post_meta( $post_id, MYPLUGIN_META, $ids );\n}, 10, 2 );\n</code></pre>\n<p>That's the whole pattern. WC's <code>wc-enhanced-select</code> JS picks up the <code>wc-product-search</code> class on DOM-ready and turns the plain <code>&lt;select&gt;</code> into a selectWoo (a WC fork of select2) with AJAX search wired to the action you specified.</p>\n<h2>Critical rules</h2>\n<h3>1. <code>class=\"wc-product-search\"</code> is the trigger</h3>\n<p>The WC enhanced-select script (<a href=\"wc-enhanced-select.js\">wp-content/plugins/woocommerce/assets/js/admin/wc-enhanced-select.js</a>) auto-initializes any <code>&lt;select&gt;</code> with this class. Without the class, your select stays a plain HTML control.</p>\n<h3>2. <code>data-action</code> controls products-only vs products+variations</h3>\n<pre><code>&lt;!-- Products only --&gt;\ndata-action=\"woocommerce_json_search_products\"\n\n&lt;!-- Products AND variations (most common) --&gt;\ndata-action=\"woocommerce_json_search_products_and_variations\"\n</code></pre>\n<p>If you omit <code>data-action</code> entirely, the JS defaults to <code>woocommerce_json_search_products_and_variations</code> (verified in <code>wc-enhanced-select.js</code>'s ajax <code>data</code> handler). Be explicit anyway; readers shouldn't have to chase JS defaults.</p>\n<h3>3. ALWAYS pre-render selected options server-side</h3>\n<p>The AJAX search runs only on user input (with <code>minimumInputLength: 3</code> by default). It does not run on initial render. Without server-side pre-rendered <code>&lt;option selected&gt;</code> 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.</p>\n<p>Pattern: <code>wc_get_product( $id )-&gt;get_formatted_name()</code> returns the WC-styled label including SKU, attributes, and parent product for variations (e.g. <code>T-Shirt - Color: Red, Size: M (#42)</code>). Use it.</p>\n<h3>4. Useful <code>data-*</code> attributes</h3>\n<table>\n<thead>\n<tr>\n<th>Attribute</th>\n<th>Purpose</th>\n<th>Example</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code>data-placeholder</code></td>\n<td>Empty-state placeholder</td>\n<td><code>Search products…</code></td>\n</tr>\n<tr>\n<td><code>data-action</code></td>\n<td>Which AJAX action to call</td>\n<td><code>woocommerce_json_search_products_and_variations</code></td>\n</tr>\n<tr>\n<td><code>data-exclude</code></td>\n<td>ID or JSON array of IDs to exclude</td>\n<td><code>42</code> or <code>[42,43]</code></td>\n</tr>\n<tr>\n<td><code>data-include</code></td>\n<td>Restrict results to an ID or JSON array</td>\n<td><code>[12,15]</code></td>\n</tr>\n<tr>\n<td><code>data-limit</code></td>\n<td>Requested result limit</td>\n<td><code>100</code> (default 30, filter <code>woocommerce_json_search_limit</code>)</td>\n</tr>\n<tr>\n<td><code>data-exclude_type</code></td>\n<td>Comma-list of product types to skip</td>\n<td><code>external,grouped</code></td>\n</tr>\n<tr>\n<td><code>data-display_stock</code></td>\n<td>Append \" — Stock: N\" to labels for managed-stock items</td>\n<td><code>1</code></td>\n</tr>\n<tr>\n<td><code>data-allow_clear</code></td>\n<td>Show a clear (x) control on single-value selects</td>\n<td><code>1</code></td>\n</tr>\n<tr>\n<td><code>data-minimum_input_length</code></td>\n<td>Override default 3-char minimum</td>\n<td><code>2</code></td>\n</tr>\n<tr>\n<td><code>data-sortable=\"true\"</code></td>\n<td>Enable drag-sort on multi-select chips</td>\n<td><code>true</code></td>\n</tr>\n</tbody>\n</table>\n<p>Do not pass multiple IDs as a comma-separated string to <code>data-exclude</code> or <code>data-include</code>: the PHP handler casts the request to an array and then applies <code>absint</code>, so <code>\"12,15\"</code> collapses to <code>12</code>. Core emits multiple IDs as JSON (<code>[12,15]</code>), which jQuery parses into an array. <code>data-exclude_type</code> is the exception: its handler explicitly accepts a comma-delimited value.</p>\n<h3>5. Nonce is internal — don't reinvent</h3>\n<p>WC's AJAX handler verifies <code>check_ajax_referer( 'search-products', 'security' )</code> (<a href=\"class-wc-ajax.php\">class-wc-ajax.php:1769</a>). The nonce is auto-attached by <code>wc-enhanced-select.js</code> from the <code>wc_enhanced_select_params.search_products_nonce</code> PHP-localized variable. Don't try to add your own nonce to the AJAX request — WC handles it.</p>\n<h3>6. <code>wc-enhanced-select</code> is auto-enqueued on WC admin screens</h3>\n<p>WC enqueues <code>wc-enhanced-select</code> (plus <code>selectWoo</code>, plus the matching styles) on every screen returned by <code>wc_get_screen_ids()</code> (<a href=\"class-wc-admin-assets.php\">wp-content/plugins/woocommerce/includes/admin/class-wc-admin-assets.php:412-415</a>). That covers: product edit / new screens, orders, coupons, shipping, settings, etc. <strong>You do not need to manually enqueue on those.</strong></p>\n<p>For the same <code>&lt;select&gt;</code> markup on a <strong>non-WC admin page</strong> (your plugin's settings page, a dashboard widget, a modal in the post-type editor of another CPT), explicit enqueue IS needed:</p>\n<pre><code>add_action( 'admin_enqueue_scripts', static function ( string $hook_suffix ): void {\n    if ( $hook_suffix !== 'my-plugin_page_my-settings' ) return; // gate to your screen\n\n    wp_enqueue_script( 'wc-enhanced-select' );\n    wp_enqueue_style( 'woocommerce_admin_styles' );\n} );\n</code></pre>\n<p>The handles are stable: <code>wc-enhanced-select</code> (JS) and <code>woocommerce_admin_styles</code> (CSS). The dependency on selectWoo is declared internally by the WC handle; you don't need to enqueue selectWoo separately.</p>\n<h3>7. Save handler treats input as untrusted</h3>\n<p>The <code>&lt;select multiple&gt;</code> posts as an array of strings. Always <code>array_map( 'absint', $raw )</code> and <code>array_filter</code> to drop empties. Never trust the IDs back from the form — a user can craft a <code>&lt;option value=\"9999999\"&gt;</code> and submit it. If the ID space matters (the picked products must be readable to the current user), revalidate with <code>wc_get_product( $id )</code> and <code>current_user_can( 'read_product', $id )</code> before storing.</p>\n<h2>Common mistakes</h2>\n<pre><code>// WRONG — load all products upfront, freeze the page on stores with &gt; a few hundred\n$products = get_posts( array( 'post_type' =&gt; 'product', 'posts_per_page' =&gt; -1 ) );\necho '&lt;select multiple&gt;';\nforeach ( $products as $p ) {\n    echo '&lt;option value=\"' . $p-&gt;ID . '\"&gt;' . $p-&gt;post_title . '&lt;/option&gt;';\n}\necho '&lt;/select&gt;';\n\n// WRONG — missing pre-render of saved values; on edit the select shows empty\necho '&lt;select class=\"wc-product-search\" name=\"related_products[]\"\n        multiple=\"multiple\"\n        data-action=\"woocommerce_json_search_products_and_variations\"&gt;&lt;/select&gt;';\n// Saved IDs are in DB but invisible in the UI; user thinks the data is lost.\n\n// WRONG — products only when variations are needed\ndata-action=\"woocommerce_json_search_products\"\n// Cross-sell-style features almost always need to target specific variations.\n\n// WRONG — manually enqueueing on the product edit screen (redundant)\nadd_action( 'admin_enqueue_scripts', function () {\n    wp_enqueue_script( 'wc-enhanced-select' ); // already loaded by WC here\n} );\n// Harmless but adds noise. Only enqueue manually on NON-WC admin screens.\n\n// WRONG — saving raw $_POST without sanitization\nupdate_post_meta( $post_id, '_related_products', $_POST['related_products'] );\n// Allows attacker to inject arbitrary IDs / strings.\n\n// RIGHT — sanitize-then-save\n$ids = array_values( array_filter( array_map( 'absint', (array) wp_unslash( $_POST['related_products'] ?? array() ) ) ) );\nupdate_post_meta( $post_id, '_related_products', $ids );\n</code></pre>\n<h2>Reading the saved IDs at runtime</h2>\n<pre><code>$ids = (array) get_post_meta( $post_id, '_myplugin_related_products', true );\nforeach ( $ids as $id ) {\n    $product = wc_get_product( (int) $id );\n    if ( ! $product instanceof WC_Product ) {\n        continue; // product was deleted\n    }\n\n    if ( $product-&gt;is_type( 'variation' ) ) {\n        // Variation-specific handling: $product-&gt;get_parent_id(), get_attributes()\n    } else {\n        // Regular / variable / grouped / external\n    }\n\n    // For frontend display:\n    echo esc_html( $product-&gt;get_name() );\n    echo wc_price( (float) $product-&gt;get_price() );\n}\n</code></pre>\n<p>The IDs returned by the search endpoint are post IDs that may belong to either <code>product</code> or <code>product_variation</code> post types. <code>wc_get_product()</code> handles both transparently.</p>\n<h2>Cross-references</h2>\n<ul>\n<li>Run <strong><code>wc-shipping-method</code></strong> 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.</li>\n<li>Run <strong><code>wp-plugin-architecture</code></strong> for the broader question of how to organize the meta box class file alongside other plugin code.</li>\n<li>Run <strong><code>wp-security-audit</code></strong> on the save handler — it's an admin-context write endpoint with attacker-controlled input.</li>\n</ul>\n<h2>What this skill does NOT cover</h2>\n<ul>\n<li>Building a product picker for the <strong>block editor / Gutenberg</strong> product fields. Those use a different React-based component (<code>wc/product-control</code> from <code>@woocommerce/components</code>); not the same wiring.</li>\n<li>The frontend product search (the customer-facing search bar). Different endpoint, different scope.</li>\n<li>WC REST API product search (<code>/wc/v3/products</code> with <code>?search=</code>). That's for external integrations, not admin UI; this skill is admin-UI-specific.</li>\n<li>Customizing the result label rendering beyond what <code>get_formatted_name()</code> returns. The endpoint returns plain text labels via <code>wp_strip_all_tags</code>; richer renderings require a custom AJAX handler or a per-result formatter, both out of scope here.</li>\n<li>Variation-attribute filtering (e.g. \"show only red variations\") at search time. The endpoint matches on title / SKU / ID, not attribute values.</li>\n</ul>\n<h2>References</h2>\n<ul>\n<li><code>WC_AJAX::json_search_products</code> and <code>json_search_products_and_variations</code>: <a href=\"class-wc-ajax.php\">wp-content/plugins/woocommerce/includes/class-wc-ajax.php</a> (lines ~1768 and 1844).</li>\n<li><code>wc-enhanced-select.js</code> — the JS that turns <code>class=\"wc-product-search\"</code> into selectWoo with AJAX wiring: <a href=\"wc-enhanced-select.js\">wp-content/plugins/woocommerce/assets/js/admin/wc-enhanced-select.js</a>.</li>\n<li>WC auto-enqueue on admin screens: <a href=\"class-wc-admin-assets.php\">wp-content/plugins/woocommerce/includes/admin/class-wc-admin-assets.php:412-415</a>.</li>\n<li><code>WC_Data_Store::load( 'product' )-&gt;search_products()</code> — 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).</li>\n<li>Product search query implementation: <a href=\"class-wc-product-data-store-cpt.php\">wp-content/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php</a> — <code>search_products()</code>.</li>\n<li>selectWoo (WooCommerce's select2 fork): <a href=\"https://github.com/woocommerce/selectWoo\">github.com/woocommerce/selectWoo</a>.</li>\n<li>Official documentation: <a href=\"https://woocommerce.com/document/woocommerce-json-search/\">https://woocommerce.com/document/woocommerce-json-search/</a></li>\n</ul>\n","files":[{"path":"SKILL.md","sizeBytes":16356,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-09-16T14:58:18.876745Z","sha256":"DD5CA4D8576C185641EF1EA7961F32EF25BEC9562C78F9EEE376683AA3F4A3FA","sizeBytes":6234},"review":null,"source":{"repositoryUrl":"https://github.com/Lonsdale201/wp-agent-skills","path":"woocommerce/wc-product-search-select","license":"MIT","commit":"c51b571a259f0c4b5f5c0a3bc50ed580c6851f98","subtreeSha":"8D019C4AB80F1ABDF6940BB0A5A00528DFCC4E5360F027403842DF49CB61753A","lastSyncedAt":"2026-09-29T23:33:03.303675Z"},"reviewedAt":"2026-09-16T15:18:08.81689Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/woocommerce/wc-product-search-select"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart"},{"target":"git","command":"git clone https://github.com/Lonsdale201/wp-agent-skills.git"}]}