Language

Get in touch

WordPress plugin · since 2021

Taxonomy Chain Menu

Chained drop-downs over a hierarchical taxonomy (make, model, generation) that end in a post, a term page, or a filter action.

First release
Active installs
100+
  • PHP
  • WordPress
  • get_categories()
  • admin-ajax
  • JavaScript (ES6 classes)
  • Fetch API
  • DOMParser
  • CustomEvent
View on wordpress.org wordpress.org

About

Taxonomy Chain Menu turns any hierarchical taxonomy of any post type into a chain of drop-downs: choosing a term loads the next level, and at the end of the chain the visitor gets a post list, a link to the term, or nothing. It is one shortcode with 22 settings, and it can also drive a TableOn or WOOT table or a WOOF product filter on the same page.

The engineering work is in loading a tree of unknown depth one level at a time, knowing when a level is the last, restoring a chosen path from a link, and talking to other plugins without depending on their code.

Taxonomy Chain Menu · chained selects

what was solved

LIVEsince 2021

Chained drop-downs over a taxonomy tree

shipped · 2021

What it givesVisitors pick a make, then a model, then a generation, and reach the right page quickly. Only the needed levels load, and a chosen path can be shared as a link.

Problem
A taxonomy can be several levels deep. Rendering the whole tree up front is heavy for large catalogs; loading level by level requires knowing when a level is the last one and what to show then.
Solution

The server renders one level per request and answers with the next select, a posts select or a term link. The browser keeps fetched levels as DOM nodes and replays them from memory.

Stack
  • PHP
  • admin-ajax
  • JavaScript
  • Fetch API
Tasks · 6
Load only the level the visitor openedget_terms() by parent with hide_empty; ajax returns the next select or a leaf answer

get_chain_item() reads the parent term from the request and asks get_categories() for its non-empty children. If there are children it returns a select; otherwise the response depends on the leaf mode. The front end parses the HTML with DOMParser and appends the node to the chain.

Decide what the end of the chain showswatch_view picks a posts select, get_term_link() or no output

In 'posts' mode the leaf returns a select of the term's posts with permalinks as values, with configurable limit, order and meta key ordering. In 'terms' mode it returns the term URL and the front end renders a button. 'none' ends the chain silently, which is the mode used when the chain only drives a filter.

A request for a term that has no children is wastedIn terms mode each option carries its child count; a leaf opens the link directly

When watch_view is 'terms', generate_tax_select() adds data-child-count (wp_count_terms with the term as parent) and data-link to every option. The front end checks the count and, for 0, shows the button without calling the server.

Going back up the chain should not fetch the same levels againCache fetched select nodes per term_id; changing a level removes all later ones

trigger_taxonomy_select() first removes every element after the changed select, then looks in this.cache[term_id]. A cached node is reattached with its selection cleared; a cached leaf answer re-creates the button. Only unseen terms go to the server, and each result is stored with its request data so events fire the same way for cached and fresh levels.

Restore a chosen path from a link or a shortcodeready_chain lists term IDs; the server renders every level preselected in one pass

ready_chain comes from the shortcode or the ready_chain GET parameter as a comma-separated list of term IDs; an item that is not a plain number is read as a post ID. draw_chain_menu() walks it, rendering each level with the next ID selected and using it as the parent of the following one; a non-numeric item becomes the preselected post. After load the front end adds the button for the last value and re-emits the change event for the deepest selected term, so connected tables and filters pick up the state.

Limit the top level without cutting deeper levelsinclude_top applies only when parent is 0; include and exclude apply everywhere

A site may want only some brands at the top but all models under them. get_terms() replaces the include list with include_top only for the root request. The shortcode also checks with get_object_taxonomies() that the taxonomy belongs to the post type before rendering anything.

Driving tables and filters from the chain

in the product

What it givesThe same drop-downs can drive the product tables and product filters already on the page, so visitors narrow a catalog in one place.

Problem
The chain is often a navigation front for other plugins on the same page: a TableOn or WOOT table, or a WOOF product filter. They keep their own state and redraw logic and know nothing about the chain.
Solution

Every selection dispatches a DOM CustomEvent with the request data. Small connectors translate it into a table's filter_data or WOOF's current values, and PHP hooks let site code change option data and labels.

Stack
  • JavaScript
  • CustomEvent
  • WordPress filters
Tasks · 5
Other plugins must react without tight couplingEvery change dispatches a 'taxonomy-chain-menu' CustomEvent with request data

cast_event() sends the request data, the container and the new node. The built-in connectors listen to this event; any other script can do the same. Chains that ask for a generic filter also get 'taxonomy-chain-menu-do-filter' with term_id, slug and container.

Filter a TableOn or WOOT table by the deepest selected termConnector replaces the table's filter_data with {taxonomy: term_id}, then redraws

table_connector() finds the table in DataTable23.tables by connect_table. If a level was reset to 'select', it walks the selects from the end to find the deepest one still chosen. It then replaces the table's filter_data with the selected term, or removes the taxonomy key when nothing is selected, and calls redraw_table(); the table's own ajax and cache logic does the rest.

WOOF expects term slugs and its own URL formatWrite the slug and ready_chain into woof_current_values and submit WOOF's link

Options carry data-slug from the server. With do_filter='woof' the handler puts the deepest slug under the taxonomy key of woof_current_values and stores the full ID path as ready_chain, so the WOOF results page can rebuild the chain. Deselecting everything removes both keys before submit.

Option text, images and labels must come from site codeHook taxonomy-chain-menu-option-data per option; call_filter_ prefix for labels

draw_select() asks the taxonomy-chain-menu-option-data filter for text and image of each option, passing the term or post ID, parent and chain_id. A label that starts with call_filter_ is resolved by applying the named filter, so labels can be translated or computed per page.

Shortcode settings must reach every ajax requestSettings live in data- attributes; JS turns camelCase keys back into snake_case

draw_chain_menu() prints all settings as escaped data- attributes on the container, so the server keeps no per-chain state. prepare_request_data() copies the dataset into the request, converting dataset camelCase names to snake_case; sanitize_request_data() lower-cases keys and sanitizes every value.

Styled selects without losing native behavior

in the product

What it givesDrop-downs can be styled to match the site design while keeping the standard behavior that forms and other plugins rely on.

Problem
Sites want styled drop-downs, but the chain logic, the connectors and form handling depend on native select elements and their change events.
Solution

Native selects stay as the hidden source of truth. A reflector clones the chain into Selectron23 widgets after every change and sends picks back to the native select through a custom event.

Stack
  • JavaScript
  • CustomEvent
  • DOM cloning
Tasks · 3
A styled widget must not replace the real selectTCM_Reflector clones the chain next to the hidden original after every change

With select_wrapper='selectron23' the container is rendered hidden and TCM_Reflector inserts a sibling element. clone() rebuilds it from the container's current children, wrapping each cloned select in a Selectron23 widget, and runs after every level change, cache hit and loader update.

A pick in the widget must drive the real selectonSelect dispatches select-changed; a listener sets the value and runs the logic

Selectron23's onSelect fires taxonomy-chain-menu-select-changed on the original select with the value and container. append_select_changed_event() checks the container, sets the value and the selected attribute, and calls the same handler as a native change, so the chain, cache and connectors see one code path.

Different widths and heights per level from one settingPipe-separated lists; the last value applies to all deeper levels

select_width ('300px|250px|20%') and selectron23_max_open_height are split on '|' and applied by index each time a level is added. The chain depth is not known in advance, so the last value covers every further level.

Numbers

wordpress.org
  • Active installs 100+ wordpress.org counter.
  • Years supported 5 Since 25 February 2021, last update 21 May 2026.