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 thedynamicfolder if it doesn't exist)/themes/[current theme]/assets/js/dynamic
Name the file:
custom.js— loaded on every productcustomX.js— loaded only for the product with ID=X
For example, to target product#10, createcustom10.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.
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
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:
| Object | Returned by | What it is |
|---|---|---|
Field | getField() | The configuration: label, type, settings, options, unit |
InputField | getFields(), the stores | The current state: value, formatted value, selected options, visibility |
Reading a field configuration
var field = window.dp.methods.getField('width');Shape:
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:
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
var fields = window.dp.methods.getFields();
var width = fields.width.value;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
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:
| Prop | Type | Note |
|---|---|---|
value | string/num | The raw value used by the formulas |
value_formatted | string | The displayed value (price fields, dynamic fields…) |
secondary_value | string | The option secondary value |
selected_options | number[] | Option IDs, for dropdown / checkbox / thumbnails / radio |
data | object | Free-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:
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
window.dp.methods.recalculate({ changed: 'width' });changed tells the module which field the customer (or your script) just changed. It matters:
- when
changedis 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
var result = window.dp.methods.validateFields();Returns true when everything is valid, otherwise an object mapping field names to error messages:
{
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
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:
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
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:
| Action | Purpose |
|---|---|
SaveCustomization | Default. Saves the input without adding it to cart |
SaveInput | Creates a standalone input (create-customization flow) |
SaveOrderCustomization | Saves back to an existing order (admin edit) |
SaveDynamicProduct | Saves as a bookmark / saved configuration |
Resolved value:
{
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
var container = window.dp.methods.getContainer(); // <div id="dp-container"> or nullReturns 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:
store.state; // current value, read synchronously
var stop = store.listen(fn); // subscribe, returns an unsubscribe function
stop(); // unsubscribeThe listener receives (state, prev_state).
Calculator store
Fires after each calculation returned by the server.
document.addEventListener('dp.load', function () {
window.dp.stores.calculator.listen(function (result) {
console.log(result.formatted_prices.price_ttc);
});
});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:
{
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.
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):
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:
window.dp.selectors.price = '.product-price > span';Override a hook:
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:
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:
| Value | Type | Value | Type |
|---|---|---|---|
| 1 | input | 13 | textarea |
| 2 | fixed | 14 | feature |
| 3 | price | 15 | divider |
| 4 | text | 16 | radio |
| 5 | date | 17 | color-picker |
| 6 | image | 18 | html |
| 7 | dynamic (php) | 19 | error |
| 8 | dropdown | 20 | switch |
| 9 | checkbox | 21 | custom |
| 10 | file | 22 | preview |
| 11 | slider | 23 | country |
| 12 | image-list | 24 | font |
Examples
Prefill a field from the URL:
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:
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:
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:
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:
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.
