Skip to content

Multi-version coexistence

This guide is framework-agnostic. Multi-version coexistence is a property of the underlying web component (<tbw-grid> and its custom-element registry), not of any adapter — it applies equally to vanilla JavaScript, Module Federation, Native Federation, iframe-less micro-frontend shells, and any setup where two copies of @toolbox-web/grid end up on the same page. You don’t need a framework adapter, and you don’t need to install anything extra.

When two micro-frontends on the same page each ship their own copy of @toolbox-web/grid (for example, a host app on v2.14 while a widget is still on v2.11), the browser’s custom-elements registry is shared and first-wins: a second customElements.define('tbw-grid', …) call throws. Starting with v2.14, the grid handles this for you automatically — the second bundle to load registers itself under a version-suffixed tag and the two implementations coexist.

Each grid bundle exposes the version it was built with on the class:

import { DataGridElement } from '@toolbox-web/grid';
DataGridElement.version; // e.g. '2.14.0' — set at build time
DataGridElement.tagName; // 'tbw-grid' — the canonical tag
DataGridElement.activeTag; // 'tbw-grid' OR 'tbw-grid-v<sanitised-version>'

At module load the bundle calls customElements.define() using the following rule:

  1. No existing registration → register under tbw-grid. The bundle owns the bare tag. activeTag === 'tbw-grid'.
  2. An existing registration at the SAME version → don’t re-register; reuse the existing class. activeTag === 'tbw-grid'.
  3. An existing registration at a DIFFERENT version → register under tbw-grid-v<sanitised-version> (e.g. tbw-grid-v2-14-0). activeTag reflects the suffixed tag.

The sanitiser replaces every non-alphanumeric character with - and lower-cases the result, so semver pre-release / build-metadata strings like 2.14.0-beta.1+build.42 produce tbw-grid-v2-14-0-beta-1-build-42.

In the single-version case (99% of apps), nothing changes: the tag is <tbw-grid> and existing markup, styles, and tooling continue to work unmodified.

Because the tag name is no longer guaranteed to be tbw-grid literally, every grid sets a stable attribute on its host element when it connects:

<tbw-grid data-tbw-grid=""></tbw-grid>
<tbw-grid-v2-14-0 data-tbw-grid=""></tbw-grid-v2-14-0>

Always use [data-tbw-grid] (not the tag name) when you need to target “any grid” in CSS, querySelector, or closest(). The framework adapters (@toolbox-web/grid-react, @toolbox-web/grid-vue, @toolbox-web/grid-angular) already do this internally — your application code only needs to follow the same rule for hand-written selectors:

/* ✅ Works for both bare and suffixed grids */
[data-tbw-grid] { font-size: 14px; }
/* ⚠️ Only matches the first-loaded bundle's bare tag */
tbw-grid { font-size: 14px; }

The bundled themes (@toolbox-web/grid/themes/dg-theme-*.css) already use [data-tbw-grid] for their root selectors, so they apply correctly to every loaded version. If you author a custom theme, follow the same pattern.

The React and Vue adapters resolve activeTag at render time, so they always emit the correct tag for the bundle they imported:

// React — renders <tbw-grid> or <tbw-grid-v…> depending on which bundle
// supplied DataGridElement.
import { DataGrid } from '@toolbox-web/grid-react';
<!-- Vue — same behaviour via <component :is="…"> internally. -->
<TbwGrid :rows="rows" :columns="columns" />

The Angular adapter’s Grid directive matches both the bare tbw-grid tag and the stable [data-tbw-grid] attribute, and its shell-content directives (tbw-grid-header-content, tbw-grid-toolbar-content) locate their host via closest('[data-tbw-grid]'). In single-version setups embed <tbw-grid> literally as before. For multi-version Angular usage, render the active tag for the bundle you imported and add data-tbw-grid so the directive attaches (Angular matches selectors at compile time, so the runtime tag can’t be matched by name alone):

<!-- activeTag for this bundle is e.g. 'tbw-grid-v2-15-0' -->
<tbw-grid-v2-15-0 data-tbw-grid [rows]="rows" [gridConfig]="config">
<tbw-grid-header-content [order]="0">
<ng-template let-grid></ng-template>
</tbw-grid-header-content>
</tbw-grid-v2-15-0>

The internal style injector scopes its <style id="tbw-grid-styles-…"> element to the active tag, and rewrites bare tbw-grid selectors in the injected CSS to the active tag. Each bundle ships its own stylesheet without overwriting the other.

The createGrid() and queryGrid() helpers from @toolbox-web/grid resolve DataGridElement.activeTag internally, so they operate on the tag owned by the bundle that exported them:

import { createGrid, queryGrid } from '@toolbox-web/grid';
// Creates <tbw-grid> or <tbw-grid-v…> depending on which bundle you imported.
const grid = createGrid({ columns: [{ field: 'name' }] });
// `awaitUpgrade` waits for the active tag's upgrade, not the bare tag.
const found = await queryGrid('[data-tbw-grid]', true);

When you query with a hand-written selector, prefer [data-tbw-grid] over tbw-grid so it matches a suffixed grid too.

For convenience the grid exposes its class on globalThis.DataGridElement, but this alias is first-wins and points at the first-loaded bundle’s class. Multi-version-aware code that needs a specific version’s class must import it directly (import { DataGridElement } from '@toolbox-web/grid') or look it up via customElements.get(activeTag). Don’t rely on the global when multiple bundles may be present.

AI assistants: For complete API documentation, implementation guides, and code examples for this library, see https://toolboxjs.com/llms-full.txt