Skip to content
  1. Extras
  2. MiniShop3
  3. Development
  4. Product tabs

Product tabs integration ​

A MODX plugin adds your own tab to the product edit page: it registers a Vue component or an ExtJS panel in window.MS3ProductTabsRegistry.

Similar API

For order tabs use MS3OrderTabsRegistry.

Architecture ​

The product edit page is a hybrid: Vue tabs run inside an ExtJS TabPanel.

ExtJS TabPanel (top level)
├── Document (MODX system tab)
├── Product (Vue ProductTabs)
│   ├── Product Properties (Vue)
│   ├── Gallery (Vue)
│   ├── Categories (Vue ProductCategoriesTab)
│   ├── Links (Vue ProductLinksTab)
│   ├── Product options (Vue)
│   └── [Your tabs]
├── Template Variables (MODX system tab)
├── Settings (MODX system tab)
└── Resource Groups (MODX system tab)

Not every built-in tab is always there:

TabKeyPositionShown when
Product Propertiesproperties0Always
Gallerygallery1Unless show_gallery is off
Categoriescategories2Unless show_categories is off
Linkslinks3Unless show_links is off
Product optionsoptions4Unless show_options is off, and the product has options

Tab registration ​

The window.MS3ProductTabsRegistry.register(tabConfig) method accepts:

ParameterTypeRequiredDescription
keystringYesUnique tab identifier
titlestringYesTab title
typestringNo'vue' (default) or 'extjs'
componentobjectFor VueThe component object — an imported SFC, for one. A string name only works if the component is already registered on the app via app.component(); the core does not look names up in window
xtypestringFor ExtJSExtJS component xtype
extConfigobjectNoAdditional ExtJS configuration
propsobjectNoAdditional props for the Vue component
positionnumberNoTab position among the others (default 100)

Returns: boolean — whether registration succeeded.

Keys of the built-in tabs (properties, gallery, categories, links, options) must not be taken: the core checks for duplicates among plugin tabs only and does not catch such a clash.

What the component receives ​

The tab receives this data automatically:

ParameterTypeDescriptionWhere available
productIdnumberProduct IDprops of the Vue component
recordobjectProduct dataprops of the Vue component, config.record in the ExtJS panel

For ExtJS tabs the core injects record into extConfig itself — you do not need to pass it.

Load order ​

product-tabs.min.js is included as an ES module, and modules run after the page has been parsed. A plain plugin script therefore always runs earlier, and at that moment window.MS3ProductTabsRegistry does not exist yet.

Do not create a stub

The order page uses a stub-with-a-queue approach. It does not work here: the registry is created as window.MS3ProductTabsRegistry || new ProductTabsRegistry(), so your stub object stays where the real registry should be. Tab initialisation then calls _onMounted() on it, which the stub does not have, and not a single queued tab appears.

Wait for the registry instead — see the function below.

Waiting for the registry ​

This function defers the registration; it works for both Vue and ExtJS tabs:

javascript
function whenRegistryReady(callback, attemptsLeft) {
    if (window.MS3ProductTabsRegistry) {
        callback(window.MS3ProductTabsRegistry)
        return
    }
    if (attemptsLeft === undefined) attemptsLeft = 50 // ~5 seconds
    if (attemptsLeft <= 0) return
    setTimeout(function() { whenRegistryReady(callback, attemptsLeft - 1) }, 100)
}

The attempt limit is not optional: without it the polling never stops where no registry exists — on the product create page, for one.

The examples below call it by name.

Vue component integration ​

1. Create the component ​

vue
<!-- my-component/src/components/VariantsTab.vue -->
<template>
  <div class="variants-tab">
    <h3>Product #{{ productId }} variants</h3>
    <!-- Your content -->
  </div>
</template>

<script setup>
defineProps({
  productId: {
    type: Number,
    required: true
  },
  record: {
    type: Object,
    required: true
  }
})
</script>

2. Create the entry point ​

javascript
// my-component/src/entries/variants-tab.js
import VariantsTab from '../components/VariantsTab.vue'

whenRegistryReady(registry => {
  registry.register({
    key: 'variants',
    title: 'Variants',
    type: 'vue',
    component: VariantsTab, // the component object, not a string
    position: 5, // after "Product options"
  })
})

3. Load the script from a MODX plugin ​

php
<?php
// MODX plugin — event: msOnManagerCustomCssJs

$page = $scriptProperties['page'] ?? '';
if ($page !== 'product_update') {
    return;
}

$controller = $scriptProperties['controller'];
$assetsUrl = MODX_ASSETS_URL . 'components/mycomponent/';

$controller->addHtml(
    '<script type="module" src="' . $assetsUrl . 'js/variants-tab.min.js"></script>'
);

There are no tabs on the create page

The event fires on the create page as well, with page => 'product_create', but product-tabs.min.js is not included there, the Product tab is absent and no registry ever appears. So checking for product_create buys you nothing: the script runs for nothing, and waiting for the registry would poll the page until it is closed. Custom tabs are only available for a saved product.

A Vue component inside ProductTabs inherits the styles of the .vueApp container.

ExtJS component integration ​

1. Create an ExtJS panel ​

javascript
// assets/components/mycomponent/js/mgr/widgets/custom.panel.js
MyComponent.panel.CustomTab = function(config) {
    config = config || {};

    Ext.apply(config, {
        border: false,
        layout: 'anchor',
        items: [{
            xtype: 'modx-grid',
            // ... grid config
        }]
    });

    MyComponent.panel.CustomTab.superclass.constructor.call(this, config);
};
Ext.extend(MyComponent.panel.CustomTab, MODx.Panel);
Ext.reg('mycomponent-custom-tab', MyComponent.panel.CustomTab);

2. Register the tab ​

javascript
// assets/components/mycomponent/js/mgr/product-integration.js
whenRegistryReady(function(registry) {
    registry.register({
        key: 'custom',
        title: 'Custom tab',
        type: 'extjs',
        xtype: 'mycomponent-custom-tab',
        extConfig: {
            // the core injects record itself, put only your own params here
            myParam: 'value'
        },
        position: 6
    });
});

Ext.onReady() will not do for registration: it fires before the ES module with the registry runs. Wait for the registry with the function from the section above.

3. Load scripts ​

php
<?php
// MODX plugin — event: msOnManagerCustomCssJs

$page = $scriptProperties['page'] ?? '';
if ($page !== 'product_update' && $page !== 'product_create') {
    return;
}

$controller = $scriptProperties['controller'];
$assetsUrl = MODX_ASSETS_URL . 'components/mycomponent/';

// The panel first, the registration second
$controller->addLastJavascript($assetsUrl . 'js/mgr/widgets/custom.panel.js');
$controller->addLastJavascript($assetsUrl . 'js/mgr/product-integration.js');

An ExtJS panel is mounted only when the tab is opened and is destroyed together with the page.

Tab positioning ​

Built-in tabs occupy positions 0–4, and a tab without position gets 100. To land right after the built-in ones, set a value between 5 and 99.

position: 0 never reaches the tab: the core reads the value as position || 100 and substitutes 100. To sit ahead of Product Properties you need a negative number.

If the tab does not appear ​

The core logs the reason, and the prefix depends on when the tab was rejected:

MessageWhen
[ProductTabsRegistry] Tab must have key and titlekey or title missing
[ProductTabsRegistry] Tab "…" already registeredDuplicate key in the queue, before it reaches Vue
[ProductTabs] Tab with key "…" already registeredDuplicate key registered on an already open page
TypeError: … _onMounted is not a functionYou created a stub — see the warning in Load order

Filtering the console by [ProductTabs] is not enough: some messages arrive with the [ProductTabsRegistry] prefix.

What to look at in the browser console:

javascript
// Tabs still waiting to reach Vue
console.log(window.MS3ProductTabsRegistry.pendingTabs);

A queue that is not empty after the page has loaded means the tabs never reached Vue.