@repobit/dex-store-elements
v1.4.2
Published
HTML elements layer for pricings
Readme
@repobit/dex-store-elements
Lightweight HTML custom elements + attribute renderers for building dynamic pricing UIs on top of @repobit/dex-store.
- Custom elements:
<bd-root>,<bd-product>,<bd-option>,<bd-state>,<bd-context> - Unified attribute-based renderers (no framework required)
- Eta templates for text/HTML and attributes
- Single, merged data context across product, option and state
- Extensible “derived” variables/functions you can compute and use anywhere
Requirements
- Node 18+
Install
npm i @repobit/dex-store-elements @repobit/dex-storePeer dependencies are resolved automatically by npm; no extra install command is required.
Quick start
<!-- index.html -->
<script type="module">
import { registerContextNodes, registerActionNodes, registerRenderNodes } from '@repobit/dex-store-elements';
import { Store } from '@repobit/dex-store';
window.addEventListener('DOMContentLoaded', async () => {
registerContextNodes();
const root = document.querySelector('bd-root');
root.store = new Store({
locale: 'en-us',
provider: { name: 'vlaicu' }
});
// Optional: analytics data layer callback
// Fires once per <bd-option> that sets `data-layer-event`
root.dataLayer = ({ option, event }) => {
// Example GTM-style push; adapt fields as needed
window.dataLayer?.push({
event,
productId : option.getProduct().getId(),
campaign : option.getProduct().getCampaign(),
variation : option.getVariation(), // e.g. "5-12"
devices : option.getDevices(),
subscription: option.getSubscription(),
price : option.getDiscountedPrice({ currency: false })
});
};
// Optional: define derived values/functions for templates + hide DSL
root.derived = async ({ option }) => ({
mails: (p) => ((option?.getDevices?.() ?? 0) / p) * 100
});
registerActionNodes(root);
registerRenderNodes(root);
});
</script>
<bd-root store-name="root">
<bd-product store-name="product" product-id="com.bitdefender.tsmd.v2">
<bd-option devices="5" subscription="12" data-layer-event="info">
<!-- Attribute renderers (see below) -->
<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 (attribute, implicit) -->
<div title="Devices {{= it.option.devices }}"></div>
<!-- Hide via DSL using merged context -->
<div data-store-render data-store-hide="!it.option.price.discounted">
Hidden when discounted price doesn't exists
</div>
<!-- Actions -->
<button data-store-action data-store-set-devices="25">25 devices</button>
</bd-option>
</bd-product>
</bd-root>Store Config
trialLinksandoverridescome from@repobit/dex-storeand work transparently with these elements. You pass them when creating theStoreand render them via attributes likedata-store-trial-linkor by relying on overridden option fields.
Example advanced config when constructing the store:
import { Store } from '@repobit/dex-store';
const store = new Store({
locale : 'en-us',
provider: { name: 'vlaicu' },
// Map productId -> campaign -> optionVariation -> trial URL
// productId is the final id after adaptor mapping (e.g. 'com.bitdefender.tsmd.v2').
// optionVariation key format: '<devices>-<subscription>' (e.g. '5-12').
trialLinks: {
'com.bitdefender.tsmd.v2': {
default: {
'5-12' : 'https://trial.example.com/default/5-12',
'10-12': 'https://trial.example.com/default/10-12'
},
PromoX: {
'5-12' : 'https://trial.example.com/promox/5-12',
'10-12': 'https://trial.example.com/promox/10-12'
}
}
},
// Per-product overrides for campaign and/or options
// - Set/redirect campaign via `default.campaign` or `[campaign].campaign`
// - Merge option fields per variation; use `null` to remove an option
overrides: {
'com.bitdefender.tsmd.v2': {
// Applies when no explicit campaign is requested
default: {
campaign: 'OvDefault'
},
PromoX: {
campaign: 'PromoX',
options : {
'5-12' : { discountedPrice: 49.99, buyLink: 'https://example.com/override/buy' },
'10-12': null // delete this variation
}
}
}
}
});Notes
data-store-trial-linkusestrialLinksto set the anchorhref. If no mapping exists, the attribute is left untouched.overrides.optionsmerges into each option; you can updatebuyLink,discountedPrice, etc., or delete an entire variation withnull.- Keys are resolved against the product id returned by the provider (after adaptor mapping). If you don’t use mappings, it’s the id you pass in
<bd-product product-id="...">.
Rendering model
- Add
data-store-renderto any element you want updated by the pipeline. - A single binder subscribes to option, product and aggregated state contexts and renders attributes + Eta templates.
- Scoping is natural: a node sees the nearest provider up the tree (e.g.,
it.option.*is only available inside<bd-option>).
Supported attributes
data-store-devices- Renders option devices to text nodes,
<input>value, or<select>options (addsdata-store-set-deviceson each option) - Optional label helpers:
data-store-text-single="device",data-store-text-many="devices"
- Renders option devices to text nodes,
data-store-subscription- Renders option subscription similarly; add
data-store-subscription-type="years|months" - Label helpers:
data-store-text-single,data-store-text-many
- Renders option subscription similarly; add
data-store-price- Allowed tokens:
full,discounted,full-monthly,discounted-monthly - Supports OR semantics via
||to choose the first available variant:data-store-price="discounted || full"
- Allowed tokens:
data-store-discount- Allowed tokens:
value,percentage,value-monthly,percentage-monthly - Supports
||fallbacks
- Allowed tokens:
Aggregated state (min/max across options):
data-store-context-pricetokens:min-full,max-full,min-full-monthly,max-full-monthly,min-discounted,max-discounted,min-discounted-monthly,max-discounted-monthlydata-store-context-discounttokens:min-value,max-value,min-value-monthly,max-value-monthly,min-percentage,max-percentage,min-percentage-monthly,max-percentage-monthly
Links
data-store-buy-linksets anchorhrefand usefuldata-*attributesdata-store-trial-linksets anchorhrefto the trial link (if configured in@repobit/dex-storestore config)
Hide DSL
data-store-hide="<boolean expression>"with an optionaldata-store-hide-type="display|opacity|visibility"- Expression is compiled and evaluated against the unified context:
it.option.*current option data. Price- and discount-related fields are formatted strings (currency-aware). Do not rely on numeric math for prices; they vary by currency. Devices/subscription remain numeric.it.product.*id/campaign/nameit.state.*aggregated min/max data (also available underit.ctx)- any keys returned from your
root.derived
- Examples:
data-store-hide="!it.option.price.discounted"(hide when no discounted price)data-store-hide="it.product.campaign === 'test'"
Eta templates
- Text/HTML: any element that is not a provider and doesn’t contain nested providers is treated as a whole-template;
innerHTMLis compiled once and morphed via nanomorph. This preserves existing DOM event listeners and state. - Attributes:
- Implicit: any attribute whose value contains
{{is rendered via Eta
- Implicit: any attribute whose value contains
- The Eta context variable is
it(Eta default). It contains:it.option.*(inside<bd-option>)it.product.*(inside<bd-product>)it.state.*andit.ctx.*(inside any provider subtree)- your derived overlay merged at top-level (see below)
Derived variables/functions
- Provide a function at the root:
root.derived = async ({ option, product, state }) => ({ ... }) - The returned object is merged into the Eta/DSL context:
- Example:
({ mails: (p) => (option?.getDevices?.()/p)*100, option: { someVar: state.discount.value.min } }) - Use it in Eta:
{{= it.mails(10) }}or{{= it.option.someVar }} - Use it in hide:
data-store-hide="it.mails(10) >= 50"
- Example:
DSL context reference
The DSL and Eta contexts use Eta’s default variable name it. The following keys are available:
it.option
- price
- full: formatted full price (string)
- discounted: formatted discounted price (string)
- fullMonthly: formatted monthly full price (string)
- discountedMonthly: formatted monthly discounted price (string)
- discount
- value: formatted discount amount (string)
- percentage: formatted percentage discount with symbol (string)
- valueMonthly: formatted monthly discount amount (string)
- percentageMonthly: formatted monthly percentage with symbol (string)
- links
- buy: buy URL (string)
- trial: trial URL if available (string)
- devices: number
- subscription: number
- price
it.product
- id: string
- campaign: string
- name: string
it.state (also available as
it.ctx)- price
- full
- min: formatted string
- max: formatted string
- monthly
- min: formatted string
- max: formatted string
- discounted
- min: formatted string
- max: formatted string
- monthly
- min: formatted string
- max: formatted string
- full
- discount
- percentage
- min: formatted string
- max: formatted string
- monthly
- min: formatted string
- max: formatted string
- value
- min: formatted string
- max: formatted string
- monthly
- min: formatted string
- max: formatted string
- percentage
- price
Notes
- All price/discount values in the DSL are formatted strings (currency-aware). Do not perform numeric comparisons on them. Prefer truthiness checks (e.g.,
!it.option.price.discounted). it.ctxis an alias ofit.statefor convenience.
Data Layer
- Provide a function on
<bd-root>:root.dataLayer = ({ option, event }) => { ... }.- Called once per
<bd-option>instance that declaresdata-layer-event(on first successful load). - Safe if attached after the option loads; it still fires once when available.
- Intended for analytics (e.g., pushing to
window.dataLayer).
- Called once per
- Event name is set on each
<bd-option>withdata-layer-event.- Canonical values:
all,info,comparison. Any custom string is also accepted.
- Canonical values:
- Payload shape passed to your callback:
event: string event name.option:ProductOptionfrom@repobit/dex-storewith getters likegetProduct(),getVariation(),getDevices(),getSubscription(),getBuyLink(),getDiscountedPrice().
Example:
<script type="module">
import { registerContextNodes, registerActionNodes } from '@repobit/dex-store-elements';
import { Store } from '@repobit/dex-store';
window.addEventListener('DOMContentLoaded', async () => {
registerContextNodes();
const root = document.querySelector('bd-root');
root.store = new Store({ locale: 'en-us', provider: { name: 'vlaicu' } });
root.dataLayer = ({ option, event }) => {
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
event,
productId: option.getProduct().getId(),
campaign : option.getProduct().getCampaign(),
variation: option.getVariation(),
devices : option.getDevices(),
subscription: option.getSubscription()
});
};
registerActionNodes(root);
});
</script>
<bd-root store-name="root">
<bd-product store-name="product" product-id="com.bitdefender.tsmd.v2">
<bd-option devices="5" subscription="12" data-layer-event="info">
<!-- your UI here -->
</bd-option>
</bd-product>
</bd-root>Behavior notes
- Fires only once per
<bd-option>instance (first load). Subsequent changes via actions or deltas do not trigger the callback again. - Set different
data-layer-eventvalues on different options if you need multiple distinct analytics events.
Actions
- Add
data-store-actionto elements to emit store events.- Set absolute values:
data-store-set-devices,data-store-set-subscription,data-store-set-id,data-store-set-campaign - Update by delta/sequence:
data-store-set-type="devices|subscription",data-store-set-delta="next|prev|<number>" - Source identifier: add
data-store-id="someId"to tag the event'sstoreId. Providers withignore-eventsthat includesomeIdwill drop these events.
- Set absolute values:
- Initialize once per mount with:
import { registerActionNodes } from '@repobit/dex-store-elements'registerActionNodes(root)
Registration and initialization
- Element registration:
import { registerContextNodes } from '@repobit/dex-store-elements'; registerContextNodes();
- Rendering:
import { registerRenderNodes } from '@repobit/dex-store-elements' - Actions:
import { registerActionNodes } from '@repobit/dex-store-elements'
Note: The package is side-effect free; elements are registered only when registerContextNodes() is called.
State & event controls
These attributes are available on <bd-state> and any element that extends it (<bd-root>, <bd-product>, <bd-option>).
ignore-events="store-a, store-b"- Comma-separated list of action ids. Events whose source element has a matching
data-store-idare ignored by this node (and its subtree). Works for both action and delta events. - Coupling with
data-store-id(on action elements):- Example:
<button data-store-action data-store-id="devicesBtn" data-store-set-devices="25">dispatches an event withstoreId: "devicesBtn".<bd-option ignore-events="devicesBtn">ignores that event while still accepting events from other buttons.
- You can list multiple ids:
ignore-events="devicesBtn, subscriptionBtn".
- Example:
- Comma-separated list of action ids. Events whose source element has a matching
ignore-events-parent- Ignores events received from ancestor providers and only reacts to DOM-bubbled events that originate within the current subtree.
<bd-context>is a convenience element that defaults this behavior to enabled. It’s equivalent to<bd-state ignore-events-parent>.- Example:
In this setup, the inner<bd-state store-name="outer"> <!-- Global action updates state here --> <button data-store-action data-store-set-devices="25"></button> <!-- Isolated island: only inner actions are observed --> <bd-context store-name="island"> <button data-store-action data-store-set-devices="5"></button> </bd-context> </bd-state><bd-context>ignores the outer button’s events.
no-collect- Turns off automatic option collection for aggregated state. Set this when you want the node to work locally without contributing to shared min/max computations (e.g., for preview widgets).
Caveats
- Scoping: attribute Eta and hide can only see contexts provided by ancestors. For example,
it.option.*is only available inside<bd-option>. - Nested providers: inner providers render their own subtrees; outer nodes can still render attributes safely even when they contain nested providers.
TypeScript
- Custom element classes are exported from a dedicated entry for advanced use:
import { RootNode, ProductNode, OptionNode, StateNode } from '@repobit/dex-store-elements'
- The derived signature:
import type { derivedContextType } from '@repobit/dex-store-elements/src/contexts/context.derived'; const derived: derivedContextType = async ({ option, product, state, store }) => ({ ... });
License
ISC
