How can I track Hulk Product options in my quote requests?
This script automatically captures custom line item properties from Hulk Product options and passes them into Quotify when a quote is requested.
Step 1 — Open the theme code editor
In your Shopify admin, go to Online Store → Themes, click the three-dot menu on your active theme, and select Edit code.
Step 2 — Create a new snippet
In the left sidebar under Snippets, click Add a new snippet. Name it quotify-properties and click Create. Shopify will create a file called quotify-properties.liquid.
Step 3 — Paste the code
Replace the empty file contents with:
{% comment %}
Hulk Product Options → Quotify Integration
{% endcomment %}
<script>
(function () {
'use strict';
// Prevent duplicate initialization.
if (window.__hulkQuotifySyncInitialized) return;
window.__hulkQuotifySyncInitialized = true;
var CONFIG = {
hulkSelector: '.hulkapps_product_options',
propertySelector: '[name^="properties["]',
quoteButtonSelector: '.quotify-request-quote-button',
debounceDelay: 50
};
var syncTimer = null;
var lastPropertiesJSON = null;
/**
* Convert property names into readable labels.
*
* Example:
* "custom_formula" → "Custom Formula"
*/
function humanizeName(name) {
return String(name || '')
.trim()
.replace(/^_+/, '')
.replace(/[-_\s]+/g, ' ')
.replace(/\b\w/g, function (c) {
return c.toUpperCase();
});
}
/**
* Collect all valid Shopify line item properties.
*/
function getProperties() {
var properties = {};
var fields = document.querySelectorAll(
CONFIG.propertySelector
);
fields.forEach(function (field) {
// Ignore disabled controls.
if (field.disabled) return;
var rawName = field.name || '';
var match = rawName.match(
/^properties\[([^\]]+)\]$/
);
if (!match) return;
// Ignore Shopify private properties.
if (match[1].charAt(0) === '_') return;
var name = humanizeName(match[1]);
if (!name) return;
var type = (field.type || '').toLowerCase();
var value = null;
// Ignore unchecked checkboxes and radios.
if (
(type === 'checkbox' || type === 'radio') &&
!field.checked
) {
return;
}
// File uploads.
if (type === 'file') {
if (!field.files || !field.files.length) return;
value = Array.from(field.files, function (file) {
return file.name;
});
// Multi-select fields.
} else if (
field.tagName === 'SELECT' &&
field.multiple
) {
value = Array.from(
field.selectedOptions,
function (option) {
return option.value;
}
);
// Standard input, select and textarea.
} else {
value = field.value;
}
// Ignore empty values.
if (
value == null ||
value === '' ||
(Array.isArray(value) && !value.length)
) {
return;
}
// Merge properties sharing the same name.
if (
Object.prototype.hasOwnProperty.call(
properties,
name
)
) {
properties[name] = []
.concat(properties[name], value);
} else {
properties[name] = value;
}
});
return properties;
}
/**
* Immediately synchronize properties to Quotify.
*/
function syncToQuotify() {
// Cancel pending scheduled updates.
if (syncTimer !== null) {
clearTimeout(syncTimer);
syncTimer = null;
}
var properties = getProperties();
var propertiesJSON = JSON.stringify(properties);
window.Quotify = window.Quotify || {};
// Always restore the property object if Quotify
// has cleared or replaced it after a request.
if (
propertiesJSON !== lastPropertiesJSON ||
window.Quotify.properties == null ||
JSON.stringify(window.Quotify.properties) !==
propertiesJSON
) {
window.Quotify.properties = properties;
lastPropertiesJSON = propertiesJSON;
}
}
/**
* Debounce updates triggered by DOM mutations.
*/
function scheduleSync() {
if (syncTimer !== null) {
clearTimeout(syncTimer);
}
syncTimer = setTimeout(function () {
syncToQuotify();
}, CONFIG.debounceDelay);
}
/**
* Check whether an element belongs to Hulk
* or represents a Shopify property field.
*/
function isRelevantElement(element) {
if (!element || element.nodeType !== 1) {
return false;
}
return (
element.matches(CONFIG.hulkSelector) ||
element.matches(CONFIG.propertySelector) ||
!!element.closest(CONFIG.hulkSelector)
);
}
/**
* Detect meaningful changes to Hulk's DOM.
*/
function hasRelevantMutation(mutations) {
return mutations.some(function (mutation) {
var target = mutation.target;
// Existing Hulk/property elements changed.
if (isRelevantElement(target)) {
return true;
}
// New Hulk containers or property fields.
if (mutation.type === 'childList') {
return Array.from(mutation.addedNodes).some(
function (node) {
if (node.nodeType !== 1) return false;
return (
isRelevantElement(node) ||
!!node.querySelector(
CONFIG.hulkSelector + ', ' +
CONFIG.propertySelector
)
);
}
) || Array.from(mutation.removedNodes).some(
function (node) {
if (node.nodeType !== 1) return false;
return (
isRelevantElement(node) ||
!!node.querySelector(
CONFIG.hulkSelector + ', ' +
CONFIG.propertySelector
)
);
}
);
}
return false;
});
}
/**
* Handle customer changes to product options.
*/
function handleOptionChange(event) {
var target = event.target;
if (!target || target.nodeType !== 1) return;
if (
target.matches(CONFIG.propertySelector) ||
target.closest(CONFIG.hulkSelector)
) {
syncToQuotify();
}
}
/**
* Ensure the latest values are available before
* Quotify processes the Request Quote click.
*/
function handleQuoteClick(event) {
var target = event.target;
if (!target || target.nodeType !== 1) return;
if (
target.closest(CONFIG.quoteButtonSelector)
) {
syncToQuotify();
}
}
/**
* Initialize listeners and DOM observer.
*/
function initialize() {
// Initial synchronization.
syncToQuotify();
// Listen for customer option changes.
document.addEventListener(
'change',
handleOptionChange,
true
);
document.addEventListener(
'input',
handleOptionChange,
true
);
// Capture quote requests before click handlers
// on the Quotify button execute.
window.addEventListener(
'click',
handleQuoteClick,
true
);
// Detect dynamically inserted Hulk options.
var observer = new MutationObserver(
function (mutations) {
if (hasRelevantMutation(mutations)) {
scheduleSync();
}
}
);
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: [
'name',
'value',
'checked',
'selected',
'disabled'
]
});
// One final sync after all window load handlers
// have had an opportunity to run.
if (document.readyState === 'complete') {
scheduleSync();
} else {
window.addEventListener(
'load',
scheduleSync,
{ once: true }
);
}
}
/**
* Wait until the DOM is available.
*/
if (document.readyState === 'loading') {
document.addEventListener(
'DOMContentLoaded',
initialize,
{ once: true }
);
} else {
initialize();
}
})();
</script>
Click Save.
Step 4 — Include the snippet in your product template
Still in the code editor, open your product template. This is typically one of these files (depending on your theme): sections/main-product.liquid, sections/product-template.liquid, or templates/product.liquid.
Add this line at the very bottom of the file:
{% render 'quotify-properties' %}Notes:
Properties prefixed with an underscore (like _hidden_field) are intentionally excluded — these are typically internal tracking fields that shouldn't appear on a quote. Empty values and unchecked checkboxes/radios are also filtered out automatically. The script runs once on page load and again each time fields change, so it stays in sync with Hulk Product options that validate fields dynamically.
Updated on: 20/10/2026
Thank you!