npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@repobit/dex-store-elements

v2.0.24

Published

HTML elements layer for pricings

Downloads

5,240

Readme

@repobit/dex-store-elements

Reactive HTML custom elements + declarative attribute renderers for building dynamic pricing UIs on top of @repobit/dex-store.

  • Custom elements: <bd-product>, <bd-option>, <bd-context> (a Lit-based, scope-providing node tree)
  • Declarative renderers via data-store-* attributes — no framework glue code
  • Eta templates for text and attributes with a unified DSL context
  • Compute layer (min/max over the reachable option space) feeds a flat state DTO into templates and the hide DSL
  • Composable event topology: node-local ignore-list filtering via data-store-event-id + ignore-events (data-store-id remains a v1 alias)

Requirements

  • Node 18+
  • A browser (or jsdom for tests)

Install

npm i @repobit/dex-store-elements @repobit/dex-store

Quick start

<script type="module">
  import {
    registerContextNodes,
    registerActionNodes,
    registerRenderNodes
  } from '@repobit/dex-store-elements';
  import { Store } from '@repobit/dex-store';

  window.addEventListener('DOMContentLoaded', () => {
    registerContextNodes();

    const context = document.querySelector('bd-context');

    context.store = new Store({
      locale  : 'en-us',
      provider: { name: 'vlaicu' }
    });

    // Optional: analytics data layer callback
    // Fires once per <bd-option> that declares `data-layer-event`.
    context.dataLayer = ({ option, event }) => {
      window.dataLayer?.push({
        event,
        productId   : option.getProduct().getId(),
        campaign    : option.getProduct().getCampaign(),
        variation   : option.getVariation(),
        devices     : option.getDevices(),
        subscription: option.getSubscription(),
        price       : option.getDiscountedPrice({ currency: false })
      });
    };

    // Optional: derived values/functions available in templates + hide DSL.
    context.derived = async ({ option }) => ({
      mails: (p) => ((option?.getDevices?.() ?? 0) / p) * 100
    });

    const disposeActions = registerActionNodes(context);
    const disposeRenders = registerRenderNodes(context);

    // Call disposers during teardown (SPA route changes, dynamic mounts).
    // disposeActions();
    // disposeRenders();
  });
</script>

<bd-context>
  <bd-product product-id="com.bitdefender.tsmd.v2">
    <bd-option devices="5" subscription="12" data-layer-event="info">
      <!-- Attribute renderers -->
      <div data-store-render data-store-devices></div>
      <div data-store-render
           data-store-subscription
           data-store-subscription-type="years"></div>
      <div data-store-render data-store-price="discounted || full"></div>
      <a data-store-render data-store-buy-link>Buy</a>

      <!-- Eta template (text) -->
      <p>Now at only {{= it.option.price.discounted }}!</p>

      <!-- Eta template (implicit attribute) -->
      <div title="Devices {{= it.option.devices }}"></div>

      <!-- Hide via DSL -->
      <div data-store-render
           data-store-hide="!it.option.price.discounted">
        Hidden when no discounted price exists
      </div>

      <!-- Action: emit a store event -->
      <button data-store-action
              data-store-set-devices="25"
              data-store-event-id="devicesBtn">
        25 devices
      </button>
    </bd-option>
  </bd-product>
</bd-context>

Architecture in 30 seconds

  • bd-context publishes a BdScope (store, derived, dataLayer, batch schedule) and is a hard event boundary — events from outside it cannot drive descendants inside it.
  • bd-product consumes a scope, publishes BdScope.product (plus a normalised option field).
  • bd-option consumes a scope and publishes BdScope.option after resolving the option from store + devices + subscription + action.
  • registerActionNodes(root) attaches BdActionElementAdapters to every element matching [data-store-action] (or that declares one of the data-store-set-* attributes). Adapters dispatch bd-action-request events on user interaction; the nearest bd-* ancestor catches them and drives its own scope.
  • registerRenderNodes(root) walks every [data-store-render] element and runs the matching attribute handler whenever an observed attribute changes.
  • Compute (bd-compute, opt-out via compute-disabled) reduces the reachable option space to a BdComputeResult with min/max formatted strings, available as it.state.* in templates.

Custom elements

Only three elements are registered. bd-root / bd-state from v1 are intentionally gone — bd-context is the scope-publishing root.

| Tag | Attributes | Notes | | --- | --- | --- | | <bd-context> | ignore-events, ignore-events-parent (always true), eta-disabled, compute-disabled, topology-boundary | Publishes store, derived, dataLayer, mutationBatchSchedule. | | <bd-product product-id="…" campaign="…" bundle> | all the above + product-id, campaign, bundle | Reflects product-id, campaign, bundle. | | <bd-option devices="5" subscription="12" data-layer-event="info"> | all the above + devices, subscription, data-layer-event | Reflects devices, subscription. Fires dataLayer once on first load. |

All three extend BdScopedElement and pick up every scope-related property (ignore-events, ignore-events-parent, eta-disabled, compute-disabled, topology-boundary).

Scope-related properties

  • ignore-events="a, b, c" — drop transitions whose eventId matches any of the comma-separated ids. Applies to this node and every descendant (the cascade is gated here).
  • ignore-events-parent (boolean) — drop cascades from the parent scope; only react to local DOM events. bd-context enables this by default.
  • eta-disabled / compute-disabled (boolean) — kill Eta rendering / compute for this subtree.
  • topology-boundary (boolean) — on BdNodeElement, marks a topology boundary for the action registry.

Render attributes

Add data-store-render to any element you want re-rendered on scope changes. The matching handler is selected by the data attribute name.

| Attribute | Tokens / values | Source | | --- | --- | --- | | data-store-devices | — | current option.getDevices() | | data-store-subscription | — | current option.getSubscription() | | data-store-subscription-type | months | years | affects text formatting only | | data-store-text-single | label string | singular label | | data-store-text-many | label string | plural label | | data-store-price | full, discounted, full-monthly, discounted-monthly (supports || fallbacks) | current option | | data-store-discount | value, percentage, value-monthly, percentage-monthly (supports || fallbacks) | current option | | data-store-context-price | min-full, max-full, min-discounted, max-discounted, min-full-monthly, max-full-monthly, min-discounted-monthly, max-discounted-monthly | compute result | | data-store-context-discount | min-value, max-value, min-percentage, max-percentage, min-value-monthly, max-value-monthly, min-percentage-monthly, max-percentage-monthly | compute result | | data-store-buy-link | (optional trial duration, e.g. "30 days") | buy URL, or trial URL when a duration is supplied | | data-store-hide | boolean expression in the DSL | hide the element when truthy | | data-store-hide-type | display (default) | opacity | visibility | how to hide |

data-store-trial-link is gone — use data-store-buy-link="30 days" to pick a trial URL by duration. Falls back to the regular buy URL when no trial is configured.

Hide DSL

Boolean expression compiled once per change, evaluated against the unified context:

<div data-store-render data-store-hide="!it.option.price.discounted">…</div>
<div data-store-render data-store-hide="it.product.campaign === 'test'">…</div>
<div data-store-render
     data-store-hide="it.state.discount.percentage.min && !it.mails(10)">
  …
</div>

The expression runs against the same context the Eta templates see (see DSL context reference below). Numeric comparisons on prices are not portable — they're formatted strings and vary by currency. Devices and subscription remain numeric.

Eta templates

The Eta context variable is it (Eta default). The available shape is identical to the hide DSL:

  • it.option.* (only inside <bd-option>)
  • it.product.* (only inside <bd-product>)
  • it.state.* / it.ctx.* (everywhere — ctx is an alias of state)
  • any root-level keys returned by derived (see below)

The raw Store, BdScope, transitions, and controller flags are not exposed.

Templates can appear in text content (<p>…{{= it.option.price.discounted }}…</p>) or implicitly in any attribute whose value contains {{.

Derived variables / functions

context.derived = async ({ product, option, state }) => ({
  mails: (p) => ((option?.getDevices?.() ?? 0) / p) * 100,
  // Nested overrides are merged into their slot:
  option: { someVar: 'hello' }
});

Read from templates as {{= it.mails(10) }}. Read from the hide DSL as it.mails(10). The factory may be async and is called whenever the scope inputs change.

As in v1, the returned object is deep-merged into the context root. This also allows a derived object to extend or override nested option, product, state, or ctx values.

DSL context reference

The shape exposed to both Eta templates and the hide DSL:

it = {
  product: {                          // TemplateProductContext | undefined
    id      : string,
    campaign: string | undefined,
    name    : string | undefined
  },
  option: {                           // TemplateOptionContext | undefined
    price: {
      full             : string,
      discounted       : string | undefined,
      fullMonthly      : string,
      discountedMonthly: string | undefined
    },
    discount: {
      percentage       : string,
      percentageMonthly: string,
      value            : string,
      valueMonthly     : string
    },
    links: {
      buy  : string | undefined,
      trial: (trialDuration: string) => string | undefined
    },
    devices     : number,
    subscription: number
  },
  state: {                            // TemplateStateContext | undefined
    price: {
      full: {
        min: string, max: string,
        monthly: { min: string, max: string }
      },
      discounted: {
        min: string, max: string,
        monthly: { min: string, max: string }
      }
    },
    discount: {
      percentage: {
        min: string, max: string,
        monthly: { min: string, max: string }
      },
      value: {
        min: string, max: string,
        monthly: { min: string, max: string }
      }
    }
  },
  ctx,                                // alias of `state`

  // ...root-level values returned by `derived(...)`
}

Notes

  • All price / discount values in the DSL are formatted strings (currency-aware). Don't perform numeric comparisons on them.
  • it.ctx is an alias of it.state for convenience.
  • it.option.links.trial('30 days') returns the trial URL for that duration, or undefined if no trial is configured.
  • derived(...) receives only { option, product, state }; its result is deep-merged at the root.

Data layer

context.dataLayer = ({ option, event }) => {
  window.dataLayer?.push({
    event,
    productId: option.getProduct().getId(),
    devices  : option.getDevices(),
    // …
  });
};
  • Fires once per <bd-option> instance (first successful load). Re-firing requires re-mounting the node.
  • event is the value of data-layer-event on the option element. Canonical values: all, info, comparison. Custom strings are accepted.

Actions

<button data-store-action
        data-store-set-devices="25"
        data-store-event-id="devicesBtn">25 devices</button>

<button data-store-action
        data-store-set-type="devices"
        data-store-set-delta="next"
        data-store-set-min="1"
        data-store-set-max="25">next</button>

| Attribute | Effect | | --- | --- | | data-store-action | Marks the element as an action source. | | data-store-set-devices="N" | Set devices = N. | | data-store-set-subscription="N" | Set subscription = N. | | data-store-set-id="…" | Switch the current product id. | | data-store-set-campaign="…" | Switch the campaign for the current product. | | data-store-set-bundle (boolean) | Toggle bundle inclusion. | | data-store-set-type="devices\|subscription" | Required for delta updates. | | data-store-set-delta="next\|prev\|<number>" | Move the option by N (or next / prev). | | data-store-set-min, data-store-set-max | Bounds for delta updates. | | data-store-set-use-as-value (boolean) | Treat delta as an absolute value rather than an increment. | | data-store-event-id="…" | Tag the source element so ignore-events on bd-* nodes can drop its events. | | data-store-id="…" | Legacy v1 alias for data-store-event-id. The canonical attribute wins when both are present. |

For <input type="number|text"> and <select>, the adapter reads the current input value and dispatches a delta update. For <input type="checkbox|radio"> it fires on click when checked === true.

Ignoring events on a node

<bd-option ignore-events="devicesBtn, subscriptionBtn">
  <!-- this option ignores matching local or inherited transitions -->
</bd-option>

ignore-events is node-local, as in v1; it is not copied into BdScope. Each node independently decides whether to consume a local or inherited transition. If a parent drops a local event, that event is naturally not forwarded to its descendants. data-store-event-id is canonical; legacy data-store-id is accepted as an alias.

Registration and initialization

| Export | Purpose | | --- | --- | | registerContextNodes() | Register <bd-context>, <bd-product>, <bd-option> custom elements. Idempotent. | | registerActionNodes(root) | Attach BdActionElementAdapters to every action element under root. Returns () => void to disconnect. | | registerRenderNodes(root) | Wire every [data-store-render] element under root to its attribute handler. Returns () => void to disconnect. | | elementDefinitions | { 'bd-context': ContextNode, 'bd-product': ProductNode, 'bd-option': OptionNode } — for advanced consumers that want to register tags themselves. |

The package is side-effect free; nothing is registered until you call registerContextNodes().

Store config

trialLinks and overrides come from @repobit/dex-store and work transparently with these elements. Configure them on the Store and render via attributes:

import { Store } from '@repobit/dex-store';

const store = new Store({
  locale  : 'en-us',
  provider: { name: 'vlaicu' },

  // productAlias -> campaign -> optionVariation -> trialDays -> URL
  // productAlias is the id after adaptor mapping.
  // optionVariation key: '<devices>-<subscription>' (e.g. '5-12').
  trialLinks: {
    'com.bitdefender.tsmd.v2': {
      default: {
        '5-12': { '30 days': 'https://trial.example.com/default/5-12' }
      }
    }
  },

  // Per-product overrides
  overrides: {
    'com.bitdefender.tsmd.v2': {
      default: { campaign: 'OvDefault' },
      PromoX: {
        campaign: 'PromoX',
        options : {
          '5-12' : { discountedPrice: 49.99 },
          '10-12': null  // delete this variation
        }
      }
    }
  }
});

Render via attributes:

<a data-store-render data-store-buy-link>Buy</a>
<a data-store-render data-store-buy-link="30 days">30-day trial</a>

Caveats

  • Scoping: it.option.* is only available inside <bd-option>. it.product.* is only available inside <bd-product>. Everything else is available everywhere.
  • Nested providers: an inner bd-context / bd-product / bd-option renders its own subtree; templates inside it never see the outer scope's option (only the inner one).
  • Eta templates are scoped to non-provider elements — provider elements don't render text content as templates.
  • Compute (state min/max) only runs when at least one bd-context / bd-product / bd-option enables it (default: enabled). Disable with compute-disabled on a provider to skip the reduction for preview widgets.

TypeScript

import type {
  ContextNode,
  ProductNode,
  OptionNode,
  BdScope,
  BdScopeDeriveFn
} from '@repobit/dex-store-elements';

import {
  registerContextNodes,
  registerActionNodes,
  registerRenderNodes
} from '@repobit/dex-store-elements';

The custom element classes extend BdScopedElement and BdNodeElement (Lit LitElement subclasses). They are exported from the main entry.

License

ISC