Skip to content

Custom scripts ​

Custom scripts let you drive the module from your own JavaScript: read and write field values, react to calculations, position the module in your theme and add the customization to cart yourself.

Everything is exposed on a single global object: window.dp.

Where to put your script ​

Create a JavaScript file in one of these folders:

  • /js/dynamic (create the dynamic folder if it doesn't exist)
  • /themes/[current theme]/assets/js/dynamic

Name the file:

  • custom.js — loaded on every product
  • customX.js — loaded only for the product with ID=X
    For example, to target product #10, create custom10.js

All four files are loaded if they exist, in the order above, so a theme file can override the shop-wide one.

TIP

If the product uses a linked configuration, X is the ID of the source product (the one holding the fields), not the ID of the product being displayed.

Scripts are added to the page <head>, so they run before the module boots. That's intentional: it lets you configure selectors and hooks before the module reads them.

Detecting when the module is loaded ​

The module is deferred and mounts after window.load. Listen for the dp.load event before touching window.dp.methods or window.dp.stores.

js
document.addEventListener('dp.load', function () {
  // window.dp.methods and window.dp.stores are ready here
});

dp.load fires once, when the module has mounted, the initial prices have been applied to the page and the first calculation is available. It fires even when the module has nothing to display (all fields hidden by a condition).

The window.dp object ​

ts
type DynamicProductProps = {
  // positions the module container within the page
  hook: (container: HTMLDivElement) => void;
  // positions the layers container within the preview images
  preview_hook: (layers_container: HTMLDivElement) => void;
  selectors: {
    price: string;          // price with discount (if applicable)
    regular_price: string;  // price without discount
    qty_input: string;      // the quantity input
    form: string;           // the add-to-cart form
  };
  methods: {
    getContainer(): HTMLDivElement | null;
    getField(name: string): Field | null;
    updateField(name: string, props: Partial<InputField>): void;
    getFields(): Record<string, InputField>;
    validateFields(): true | Record<string, string>;
    recalculate(config?: { changed?: string }): void;
    addToCart(extra_data?: Record<string, any>): Promise<boolean>;
    saveCustomization(
      action?: string,
      extra_data?: Record<string, any>
    ): Promise<{ error: string; data: CartResult | null }>;
  };
  stores: {
    fields: Store<Record<string, InputField>>;
    calculator: Store<CalculatorResult>;
  };
};

WARNING

When the module boots, it merges whatever you already put on window.dp over its own defaults. Only set hook, preview_hook and selectors before load — defining methods or stores yourself would replace the module's implementations and break it.

Fields ​

Two different objects describe a field, and the methods below don't return the same one:

ObjectReturned byWhat it is
FieldgetField()The configuration: label, type, settings, options, unit
InputFieldgetFields(), the storesThe current state: value, formatted value, selected options, visibility

Reading a field configuration ​

js
var field = window.dp.methods.getField('width');

Shape:

ts
type Field = {
  id: number;
  id_product: number;
  id_group: number;
  id_step: number;
  id_unit: number;
  name: string;
  label: string;
  value: string;
  type: number;          // see the field types table below
  position: number;
  active: number;
  visible: number;
  init: number;
  favorite: number;
  common: number;
  linked: number;
  short_description: string;
  description: string;
  placeholder: string;
  image_url?: string;
  thumb_url?: string;
  unit: {
    id: number;
    name: string;
    symbol: string;
    displayed: boolean;
  };
  settings: FieldSettings;
  options?: Record<number, FieldOption>;
};

settings holds everything configured in the back office for that field:

ts
type FieldSettings = {
  min: number;
  max: number;
  step: number;
  strict_step: boolean;
  init: number;
  initial_value: string;
  initial_date: string;
  checked_by_default: boolean;
  required: boolean;
  placeholder: string;
  short_description: string;
  description: string;
  id_unit: string;
  price_unit: string;
  custom_suffix: string;
  // files & images
  max_size: number;
  max_files: number;
  extensions: string;
  min_width: number;
  min_height: number;
  thumbnail_size: string;
  show_dropzone: boolean;
  // dates
  min_date: string;
  max_date: string;
  disabled_days: string;
  // display
  display_label: boolean;
  display_value_price: boolean;
  display_secondary_value_price: boolean;
  display_secondary_value_description: boolean;
  display_price_tax_excl: boolean;
  display_in_popup: boolean;
  hide_when_empty: boolean;
  show_in_summary: boolean;
  show_image_in_summary: number;
  multiselect: boolean;
  color: string;
  ps_style: boolean;
  is_dynamic_value: boolean;
  // custom fields
  script_name: string;
  json_config: string;
  dev_port: number;
};

Reading the current values ​

js
var fields = window.dp.methods.getFields();
var width = fields.width.value;
ts
type InputField = {
  id: number;
  id_input: number;
  id_product: number;
  id_field: number;
  name: string;
  value: string | number;
  value_formatted: string;   // value as displayed (currency, unit, option label…)
  display_value: string;
  secondary_value: string;
  selected_options: number[];
  options: string;
  type: number;
  position: number;
  visible: number;           // 0 when the field was hidden at save time
  disabled: boolean;
  width: number;             // uploaded image dimensions
  height: number;
  size: number;              // uploaded file size
  image_url: string;
  thumb_url: string;
  data: Record<string, any> | null;  // free-form payload for custom fields
};

TIP

getFields() returns the same optimized payload that is sent to the calculator: product_price, product_weight and the attribute_* / feature_* fields are removed, along with display-only keys such as label, value_formatted and image_url. Use the fields store if you need the untouched objects.

Updating a field ​

js
window.dp.methods.updateField(field_name, props);

// set the width to 50
window.dp.methods.updateField('width', { value: 50 });

// select two options of a multiselect dropdown
window.dp.methods.updateField('colors', { selected_options: [12, 15] });

props is a partial InputField, so any key of the shape above can be passed. Common ones:

PropTypeNote
valuestring/numThe raw value used by the formulas
value_formattedstringThe displayed value (price fields, dynamic fields…)
secondary_valuestringThe option secondary value
selected_optionsnumber[]Option IDs, for dropdown / checkbox / thumbnails / radio
dataobjectFree-form payload, for custom fields

WARNING

updateField writes to the field state only. Field settings (min, max, step, required, min_date, max_size…) come from the back office configuration and are refreshed by the server on every calculation — use field formulas to change them dynamically. Likewise, showing or hiding a field is driven by the conditions, so writing visible on an input field has no effect on the rendered form.

The price and dynamic (php) fields display value_formatted instead of value, so both must be set:

js
window.dp.methods.updateField('price_field', {
  value: 50,
  value_formatted: '50€'
});

WARNING

updateField only updates the state, it does not trigger a calculation. Call recalculate afterwards if the change should affect the price or the other fields.

Triggering a calculation ​

js
window.dp.methods.recalculate({ changed: 'width' });

changed tells the module which field the customer (or your script) just changed. It matters:

  • when changed is empty, the server reloads all field values from their configuration, which can revert values you just set
  • field formulas and conditions use it to decide what to recompute

Calls are debounced (~330 ms) and any calculation still in flight is aborted, so calling it several times in a row is safe.

Validating the fields ​

js
var result = window.dp.methods.validateFields();

Returns true when everything is valid, otherwise an object mapping field names to error messages:

js
{
  width: 'This field is required',
  file: 'The file is too large'
}

Invalid fields are highlighted, the module switches to the step holding the first error and scrolls to it.

Adding to cart ​

js
window.dp.methods.addToCart().then(function (added) {
  // added === false when the customization was rejected before being sent
});

addToCart validates the fields, saves the customization and emits the PrestaShop updateCart event so the theme refreshes the cart. It resolves to false without sending anything when:

  • a required field is invalid
  • the product is out of stock with the selected values
  • the dynamic preview still expects the customer to design the product

An optional object can be passed and is merged into the request payload:

js
window.dp.methods.addToCart({ my_extra_data: 'value' });

TIP

You don't need to call this method for the standard flow — the module already intercepts the theme's add-to-cart form. Use it when you replace that button with your own.

Saving a customization without adding to cart ​

js
window.dp.methods.saveCustomization().then(function (result) {
  if (result.error) {
    console.error(result.error);
    return;
  }
  console.log(result.data.id_input);
});

The first argument selects the server action, the second one adds extra data to the payload:

ActionPurpose
SaveCustomizationDefault. Saves the input without adding it to cart
SaveInputCreates a standalone input (create-customization flow)
SaveOrderCustomizationSaves back to an existing order (admin edit)
SaveDynamicProductSaves as a bookmark / saved configuration

Resolved value:

ts
{
  error: string;               // empty when the save succeeded
  data: {
    success: boolean;
    id_product: number;
    id_product_attribute: number;
    id_customizations: Record<number, number>;
    id_input: number;
    cart: string;              // rendered cart html
  } | null;
}

Getting the module container ​

js
var container = window.dp.methods.getContainer(); // <div id="dp-container"> or null

Returns the element the module is mounted into — the one passed to hook — or null if the module never mounted (the displayProductAdditionalInfo hook is missing from the theme). Useful to append your own markup next to the fields.

Stores ​

The module exposes two reactive stores built with Vegemite.

Both expose the same tiny API:

js
store.state;                 // current value, read synchronously
var stop = store.listen(fn); // subscribe, returns an unsubscribe function
stop();                      // unsubscribe

The listener receives (state, prev_state).

Calculator store ​

Fires after each calculation returned by the server.

js
document.addEventListener('dp.load', function () {
  window.dp.stores.calculator.listen(function (result) {
    console.log(result.formatted_prices.price_ttc);
  });
});
ts
type CalculatorResult = {
  success?: boolean;
  error?: boolean;
  message?: string;

  // prices of the customization only (excluding the product base price)
  customization_prices: Prices;
  // price of one unit, customization included
  unit_prices: Prices;
  // unit price x quantity
  final_prices: Prices;
  formatted_unit_prices: Prices<string>;
  formatted_prices: Prices<string>;
  use_tax: boolean;

  weight: number;
  weight_formatted: string;

  // ids of the items hidden by the conditions
  visibility: {
    fields: number[];
    options: Record<number, number[]>;
    groups: number[];
    steps: number[];
  };

  // names of the conditions that are currently true
  // also applied to <body> as dp_condition_[name] classes
  true_conditions: string[];

  input_fields: Record<string, InputField>;
  grouped_fields: Record<number, StepWithGroups>;

  quantity_discounts: Record<number, QuantityDiscount>;

  oos: boolean;              // out of stock with the selected values
  debug_messages: Debug;     // only in debug mode
};

type Prices<T = number> = {
  price_ht: T;      // tax excluded
  price_ht_nr: T;   // tax excluded, without reduction
  price_ttc: T;     // tax included
  price_ttc_nr: T;  // tax included, without reduction
};

Example payload:

js
{
  customization_prices: { price_ht: 10, price_ht_nr: 10, price_ttc: 12, price_ttc_nr: 12 },
  unit_prices:          { price_ht: 20, price_ht_nr: 20, price_ttc: 24, price_ttc_nr: 24 },
  final_prices:         { price_ht: 40, price_ht_nr: 40, price_ttc: 48, price_ttc_nr: 48 },
  formatted_prices:     { price_ht: '€40.00', price_ht_nr: '€40.00', price_ttc: '€48.00', price_ttc_nr: '€48.00' },
  use_tax: true,
  weight: 0,
  weight_formatted: '0 kg',
  visibility: { fields: [], options: {}, groups: [], steps: [] },
  true_conditions: ['big_size'],
  input_fields: { width: { /* InputField */ } },
  oos: false,
  success: 1
}

Fields store ​

Fires on every field change: customer input, server results and your own updateField calls.

js
document.addEventListener('dp.load', function () {
  window.dp.stores.fields.listen(function (fields) {
    console.log(fields.width.value);
  });
});

The value is a Record<string, InputField> keyed by field name, with the full (non-optimized) objects.

WARNING

Calling updateField inside a fields listener re-fires the listener. Guard against loops by comparing with the previous state passed as the second argument.

Selectors and hooks ​

Themes rarely share the same markup, so the module reads the product page through a handful of configurable selectors and two positioning hooks.

Defaults (views/js/head.js):

js
window.dp = {
  hook(container) {
    const product_actions = document.querySelector('.product-actions,.js-product-actions');
    if (product_actions) product_actions.prepend(container);
  },
  preview_hook(layers_container) {
    const images_container = document.querySelector('.js-images-container');
    if (images_container) images_container.prepend(layers_container);
  },
  selectors: {
    /* used to update the price or the discounted price */
    price: '.current-price-value, .product__current-price, .product__price, .current-price [itemprop="price"], .ce-product-price',
    /* used to update the regular price */
    regular_price: '.regular-price, .product__regular-price, .ce-product-price-regular',
    /* used to detect the quantity change and recalculate the price */
    qty_input: '#quantity_wanted',
    /* used to detect the form submission and add the product to cart */
    form: '#add-to-cart-or-refresh'
  }
};

Override a selector from your custom script:

js
window.dp.selectors.price = '.product-price > span';

Override a hook:

js
window.dp.hook = function (container) {
  const product_actions = document.querySelector('.js-product-actions');
  product_actions?.append(container);
};

WARNING

Set these outside the dp.load listener. hook, preview_hook, qty_input and form are read once while the module mounts — changing them later has no effect. Only price and regular_price are read on every calculation.

At startup the module checks each selector and logs a console warning when an element is missing (regular_price is optional), so open the console when the price isn't updating.

TIP

To keep your changes safe from theme updates, put them in /dynamicproduct/scripts/head.js instead — that file replaces views/js/head.js entirely and is also editable from the back office, in the module's custom code page.

TIP

You can also position the module with the custom hook in your theme tpl.

Global variables ​

The module exposes its server-side context as window.dp_vars, available as soon as the page is parsed:

ts
type DynamicProductVars = {
  id_product: number;
  id_attribute: number;
  id_cart: number;
  id_customer: number;
  id_input: number;
  version: string;
  is_module_debug_mode: boolean;
  is_admin: boolean;
  is_admin_edit: boolean;
  is_create_customization: boolean;
  product_config: ProductConfig;
  grouped_fields: Record<number, StepWithGroups>;  // steps > groups > fields
  calculation: CalculatorResult;                   // initial calculation
  translations: Record<string, string>;
  error: string;
  urls: { data_url: string };
  controllers: {
    loader: string;
    calculator: string;
    customization: string;
    uploader: string;
  };
};

It is a snapshot taken when the page was rendered: use the stores to follow the live state.

Field types ​

type on both Field and InputField is a numeric constant:

ValueTypeValueType
1input13textarea
2fixed14feature
3price15divider
4text16radio
5date17color-picker
6image18html
7dynamic (php)19error
8dropdown20switch
9checkbox21custom
10file22preview
11slider23country
12image-list24font

Examples ​

Prefill a field from the URL:

js
document.addEventListener('dp.load', function () {
  var width = new URLSearchParams(location.search).get('width');
  if (!width) return;

  window.dp.methods.updateField('width', { value: Number(width) });
  window.dp.methods.recalculate({ changed: 'width' });
});

Mirror the customization price into your own element:

js
document.addEventListener('dp.load', function () {
  window.dp.stores.calculator.listen(function (result) {
    document.querySelector('#my-price').textContent = result.formatted_prices.price_ttc;
  });
});

Add to cart from a custom button:

js
document.addEventListener('dp.load', function () {
  document.querySelector('#my-button').addEventListener('click', async function () {
    if (window.dp.methods.validateFields() !== true) return;
    await window.dp.methods.addToCart();
  });
});

Copy a field value into another one when it changes:

js
document.addEventListener('dp.load', function () {
  window.dp.stores.fields.listen(function (fields, prev) {
    if (fields.width.value === prev.width?.value) return;

    window.dp.methods.updateField('height', { value: fields.width.value });
    window.dp.methods.recalculate({ changed: 'height' });
  });
});

React to a condition becoming true — conditions are also exposed as dp_condition_[name] classes on <body>, so simple cases can be handled in CSS alone:

js
document.addEventListener('dp.load', function () {
  window.dp.stores.calculator.listen(function (result) {
    var oversized = result.true_conditions.includes('oversized');
    document.querySelector('#shipping-notice').hidden = !oversized;
  });
});

Custom fields ​

To build your own field type with its own UI, see custom fields.