Pomade, Hair Wax, and Everything In-Between | Hairstory (2024)

The line between pomade and hair wax is a fine one because technically,pomadeis a type of hair wax. Each styling product differs depending upon their ingredients and intended uses. But first, a bit of history will help you understand why they’re similar yet different.

ANCIENT ROOTS

The English word “pomade” comes from the French pommade meaning “ointment” and the Latin pomum, “fruit” or “apple” via the Italian pomata, which also means “apple.” The original ointment recipe contained mashed apples along with animal fats and herbs

During the Roman era, soap, most likely made from olive oil, was used to groom hair by the Gauls and some European tribes. In the 18th century, hair pomade was considered to be a luxury product used by members of an elite gentry. 19th-century pomades were made with bear fat, and by the early 20th century, formulations included petroleum jelly, beeswax, and lard. (Imagine thehair washit would have taken to get it all out.)

BARBERS’ BFFS

The modern use of hair wax products is connected to early 20th-century barbers, who were eager to showcase the newly-invented hair clipper. Hair clippers made popular haircuts – like the disconnected undercut, or buzzed or “faded” side areas with much longer hair on top – quick and easy. These styles required a salve to keep the top from flopping forward and to keep it neat underneath the hats that most men wore everywhere but indoors. Wax and pomade became grooming essentials for men’s hair in the 1920s to the 1950s to create the popular ducktails, pompadours, and quiffs.

Contemporary versions are either oil- or wax-based, and recent iterations are water-based. Compared to the traditional formulas, water-based versions that lean into the putty, gum, or paste category are easier to wash out. (If you’ve ever made the mistake of using Vaseline on your hair, you know what we mean.)

POMADE VS WAX: WHICH IS THE ONE FOR YOU?

Choosing between hair wax and pomade depends on what hairstyle you’re aiming for in terms of finish and hold – and whattype of hairyou have. However, if you’re wonderinghow to add volume, wax may be the better bet.

Pomades typically have a higher oil content and are the kings of shine but offer less hold, whereas waxes with higher wax content tend toward matte with firmer control. A few other factors to consider are whether your hair tends to be naturally dry or on the oily side, or if you have fine hair or thick hair.

Oil-based pomade can make hair quite greasy, which can actually be beneficial for naturally dry, or even bleached hair, whether by sun or chemicals. Applying a tiny bit will help reduce the frizziness you may be prone to and boost the shine you may be lacking. A more generous amount can be the equivalent of an all-day moisturizing treatment.

Those with very fine or naturally oily hair, however, may find it challenging to get the dosage of such an oily product just right without getting too greasy, and wax may be the better bet to help create the texture you’re looking for. If your hair is quite oily to start with, you may want to look intohow often you wash your hair.

That’s not to say that hair wax can’t be the balm for defining a coarse curly or afro texture; in fact, it can be perfect for making those coils coalesce and do your bidding. And for gents sporting beards and mustaches, it helps tame the grizzlies and act as a beard balm for a smoother texture.

Water-based versions lack the high shine of fattier products and will likely harden into a stiffer finish similar to hair gel as the water evaporates, though the hold may last longer and be less likely to melt and wilt in hot weather.

GROOMING GUIDE: HOW TO STYLE WITH HAIR WAX OR POMADE

Applying either pomade or hair wax to damp hair or even wet hair will result in a softer hold with more shine while applying to dry hair will give less shine and stronger hold. Be gentle with styling, especially if you’re wonderinghow to grow hair faster. Here is how to apply either:

  1. Towel-dry hair well, with the option of blow-drying into the shape you desire, especially if you want to stretch out curls or to straighten waves.
  2. Depending on your hair type, apply a dab or two to the palms of your hands.
  3. To control your application, rub your hands together as if you were moisturizing them with lotion; this also warms up and softens harder products.
  4. Consider your palms a reservoir and your fingers the applicators. Comb your fingers through your hair, starting at the back of your head where it typically grows more densely. Keep re-coating your fingers from the hair product on your palms, and repeat throughout your head.
  5. Comb through for a sleek look, or finger-style to keep it casual and tousled.

* * * * * * * *

Sources:

“Pomade.”Wikipedia, Wikimedia Foundation, 8 Sept. 2020,en.wikipedia.org/wiki/Pomade
“Hair Wax.”Wikipedia, Wikimedia Foundation, 29 Oct. 2019,en.wikipedia.org/wiki/Hair_wax


shop the collection

View More Information Suited For Flat Hair, Oily Hair, Gray Hair, Dry Hair, Damaged Hair, Color-Treated Hair What It Is PRE-WASH, NEW WASH ORIGINAL AND PRIMER Benefits Clarify , cleanse and condition , protect FOR ALL HAIR TYPES Learn More
Save 20%

The Ultimate New Wash Method

PRE-WASH, NEW WASH ORIGINAL AND PRIMER

`; } await waitForRechargeWidgetToBeMounted(); await waitForRechargeSelect(); await relocateSubscriptionPriceAndCleanup(); if (shouldRenderBulletPoints) { assignProductBullets(); createBulletPoints(); } handleRechargeEventListeners('add'); configureRechargeForProductSelection(productId); const spinnerElement = document.getElementById('loading-spinner'); if (spinnerElement) { spinnerElement.remove(); } if (rechargeWidgetContainer) { rechargeWidgetContainer.style.opacity = '100'; rechargeWidgetContainer.classList.add('opacity-100', 'transition-opacity', 'duration-300'); } } /** * @description Function that is used to initially mount the recharge widget * @returns { boolean } based on if the product is found in recharge or not. If false, then the widget will not mount. * In addition if it cannot find the product, it short circuits and returns early. */ function shouldRechargeMount() { const checkReChargeWidget = setInterval(() => { if (window.ReChargeWidget && window.ReChargeWidget.api) { clearInterval(checkReChargeWidget); window.ReChargeWidget.api.fetchProduct(productId) .then(rechargeProduct => { if (!rechargeProduct.in_recharge) { return false; } const config = { productId: productId, injectionParent: `#subscription-selector-${productId}` }; window.ReChargeWidget.createWidget(config); return true; }) } }, 100); } async function waitForRechargeWidgetToBeMounted() { return new Promise(resolve => { const checkForRechargeContainer = setInterval(() => { if (document.querySelector(`#subscription-selector-${productId} > .rc-container-wrapper`)) { rechargeWidgetContainer = document.querySelector(`#subscription-selector-${productId} > .rc-container-wrapper`) clearInterval(checkForRechargeContainer) resolve() } }, 100) }) } /** * @description Function that is used to determine the initial price of the product, and then properly assign * the otpPriceText and subscriptionPriceText * @param {HTMLElement} originalPriceElement HTML Element that is used to determine the base price, and then calculate the subscription discount */ function derivePriceOptions(originalPriceElements) { let priceElementTextContent = originalPriceElements[0].innerText; let priceRegex = /([£$€¥])(\d+(?:\.\d{1,2})?)/; let match = priceElementTextContent.match(priceRegex); if (match) { let currencySymbol = match[1]; let priceValue = match[2]; otpPriceText = currencySymbol + priceValue; let subscriptionPrice if (productsWithTenPercentOff.includes(productId)) { subscriptionPrice = Number(priceValue) * 0.9; } else { subscriptionPrice = Number(priceValue) * 0.95; } subscriptionPriceText = `${currencySymbol}${subscriptionPrice.toFixed(2)}`; } } /** * @description Function to add or remove Recharge event listeners * @param {string} action - 'add' to add listeners, 'remove' to remove listeners */ function handleRechargeEventListeners(action) { function manageListeners() { const oneTimePurchaseInput = document.querySelector(`#subscription-selector-${productId} [data-radio-onetime]`); const subscriptionPurchaseInput = document.querySelector(`#subscription-selector-${productId} [data-radio-subsave]`); if (oneTimePurchaseInput && subscriptionPurchaseInput) { if (originalPriceElements.length > 0 && action === 'add') { derivePriceOptions(originalPriceElements) } if (currentSubscriptionSelector) { // Because recharge is responsbile for adding the .rc-option--active class when a user clicks a radio, we must set it manually for the accent-color black // to be present if we switch the radio manually, after the recharge widget remounts if (currentSubscriptionSelector === 'subscription') { subscriptionPurchaseInput.classList.add('rc-option--active') subscriptionPurchaseInput.checked = true // In products.js, the onSubmitHandler checks for a select element named 'selling_plan'. // When remounting and setting the checked value programmatically, it defaults to 'One time purchase', adding name=''. // If switching products and subscribe and save is selected, the select with name='' will still mount, causing the cart item to show a OTP rechargeSelectElement.setAttribute('name', 'selling_plan') let rechargeSellingPlans = deliveryOptionsContainer.querySelector('.rc-selling-plans') // By default our config for recharge is set to OTP. If we remount with a subscription option selected via our event handlers // The select with have a display: none; hardcoded onto the style, so we must remove it in order for it to be visible. if (rechargeSellingPlans) { if (rechargeSellingPlans.style.display === 'none') { rechargeSellingPlans.style.removeProperty('display'); } } } else { deliveryOptionsContainer.classList.add('hidden') oneTimePurchaseInput.classList.add('rc-option--active') oneTimePurchaseInput.checked = true // In products.js, the onSubmitHandler checks for a select element named 'selling_plan'. // When remounting and setting the checked value programmatically, it defaults to 'subscribe and save', adding name='selling_plan'. // If switching products and OTP is selected, the input with name='selling_plan' will still mount, causing the cart item to show as a subscription, even if it's OTP. rechargeSelectElement.setAttribute('name', '') } } // Check to see which input is checked on dom mount, then properly set the price based on the product and variant selected. if (subscriptionPurchaseInput.checked === true) { // reassign the perks container to the new product id on being switched, or set the intitial element if it doesn't exist on initial mount if ((!subscriptionPerksContainer && action === 'add') || (action === 'add' && subscriptionPerksContainer.id !== document.querySelector(`[id="${productId}-subscription-perks"]`).id)) { subscriptionPerksContainer = document.querySelector(`[id="${productId}-subscription-perks"]`); } if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = subscriptionPriceText; }); } if (shouldRenderBulletPoints) { subscriptionPerksContainer.classList.remove('hidden'); } } else { if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = otpPriceText; }); } } if (action === 'add') { oneTimePurchaseInput.addEventListener('click', onOneTimePurchaseClick); subscriptionPurchaseInput.addEventListener('click', onSubscriptionPurchaseClick); // At the time of this writing. Recharge after mounting sets the selling plan to 0. so we need to set the name manually // otherwise on PLP's radio options will only allow one option to be set oneTimePurchaseInput.name = `${productId}_selling_option` subscriptionPurchaseInput.name = `${productId}_selling_option` } else if (action === 'remove') { oneTimePurchaseInput.removeEventListener('click', onOneTimePurchaseClick); subscriptionPurchaseInput.removeEventListener('click', onSubscriptionPurchaseClick); } handleSellingPlanSelect(action) return true; } return false; } if (manageListeners()) return; const waitForElements = setInterval(() => { if (manageListeners()) { clearInterval(waitForElements); } }, 100); } /** * @description binds the proper event listerns to the proper selling plan select, in addition to pre-selecting the existing plan if * the previously selected option is present in the newly mounted select. */ function handleSellingPlanSelect(action) { const checkForSellingPlanSelect = setInterval(() => { const sellingPlanSelect = document.querySelector(`#selling_plan_${productId}`); if (sellingPlanSelect) { clearInterval(checkForSellingPlanSelect); if (action === 'add') { sellingPlanSelect.addEventListener('change', onSellingPlanChange); } else if (action === 'remove') { sellingPlanSelect.removeEventListener('change', onSellingPlanChange); } if (lastSelectedSellingPlan) { // Convert array like object into a proper array const matchingSellingPlanOption = [...sellingPlanSelect.options].find(option => option.textContent.trim() === lastSelectedSellingPlan ); if (matchingSellingPlanOption) { sellingPlanSelect.value = matchingSellingPlanOption.value; } } } }, 100); } /** * @description Function that is called when the OTP input is clicked */ function onOneTimePurchaseClick() { onRechargeOptionClick('otp'); } /** * @description Function that is called when the subscription input is clicked */ function onSubscriptionPurchaseClick() { onRechargeOptionClick('subscription'); } /** * @description Function that is called whenever the selling plan selected option has changed */ function onSellingPlanChange(event) { lastSelectedSellingPlan = event.target.options[event.target.selectedIndex].textContent.trim(); } /** * @description Function that is called when a recharge option input is clicked. Primarly used to add bullet points, and make sure the select has an option mounted * @param {string} optionType The type of option that was clicked, either OTP, or subscription */ function onRechargeOptionClick(optionType) { const selectedPriceText = optionType === 'subscription' ? subscriptionPriceText : otpPriceText; // Save the current selected selector, so when we remount, we can default to that option being checked currentSubscriptionSelector = optionType if (originalPriceElements[0].getAttribute('data-price-id') !== `price-${productId}`) { let originalPriceElementQuerySelector = document.querySelectorAll(`[data-price-id="price-${productId}"]`); // Assign all matching elements to the array originalPriceElements = Array.from(originalPriceElementQuerySelector); } if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = selectedPriceText; }); } if (optionType === 'otp') { deliveryOptionsContainer.classList.add('hidden') } // If the widget has been remounted, and OTP was selected, we need to remove hidden from the container so a user can see the selling plans if (deliveryOptionsContainer && optionType === 'subscription' && deliveryOptionsContainer.classList.contains('hidden')) { deliveryOptionsContainer.classList.remove('hidden') } if (subscriptionPerksContainer) { if (optionType === 'subscription' && shouldRenderBulletPoints) { subscriptionPerksContainer.classList.remove('hidden'); } else { subscriptionPerksContainer.classList.add('hidden'); } } if (optionType === 'subscription') { if (rechargeSelectElement) { // Make sure the select element as the first child element selected after mount // otherwise the select shows as empty rechargeSelectElement.children[0].selected = 'selected'; // on remount, if OTP is selected, we remove the name from the selling plan, this messes up recharge's internal logic, and then the select name always remains empty // to stop this from happening, we just manually set it everytime subscription is clicked rechargeSelectElement.setAttribute('name', 'selling_plan') } } if (optionType === 'otp') { if (rechargeSelectElement) { // When we select the OTP, but subscription was selected previously, it will still have the selling plan, name, we need to remove this or it falsely gets set as a subscription product rechargeSelectElement.setAttribute('name', '') } } } /** * @description Waits for the data price subsave attribute to be loaded, and then moves it down to the add cart button, in additio to cleaning up any remnants from recharge */ async function relocateSubscriptionPriceAndCleanup() { return new Promise((resolve) => { const waitForPriceElements = setInterval(() => { const subscriptionPriceElement = document.querySelector(`#subscription-selector-${productId} .rc_widget__price--subsave[data-price-subsave]`); const otpPriceElement = document.querySelector(`#subscription-selector-${productId} .rc_widget__price--onetime[data-price-onetime]`); const discountSpan = document.querySelector(`#subscription-selector-${productId} .rc_widget__option__discount[data-label-discount]`) let originalPriceElementQuerySelector = document.querySelectorAll(`[data-price-id="price-${productId}"]`); // Assign all matching elements to the array originalPriceElements = Array.from(originalPriceElementQuerySelector); if (subscriptionPriceElement && otpPriceElement && originalPriceElements.length > 0 && discountSpan) { // Left over elements from recharge that need to be hidden // I.E Subscription price vs OTP price, and percentage discounted const unusedRechargeElementSelectors = [ `#subscription-selector-${productId} .rc_widget__price--subsave[data-price-subsave]`, `#subscription-selector-${productId} .rc_widget__price--onetime[data-price-onetime]`, `#subscription-selector-${productId} .rc_widget__option__discount[data-label-discount]` ]; unusedRechargeElementSelectors.forEach(selector => { const element = document.querySelector(selector); if (element) { element.classList.add('hidden'); } }); clearInterval(waitForPriceElements); resolve(); } }, 100); }) } /** * @description Waits for the recharge select element to be loaded into the dom, and then moves it down to below the variant selector */ function waitForRechargeSelect() { return new Promise((resolve) => { const checkForRechargeSelect = setInterval(() => { if (document.querySelector(`#selling_plan_${productId}`)) { rechargeSelectElement = document.querySelector(`#selling_plan_${productId}`); const checkForNewDeliveryOptionsContainer = setInterval(() => { if (document.querySelector(`#delivery-options-${productId}`)) { deliveryOptionsContainer = document.querySelector(`#delivery-options-${productId}`); deliveryOptionsContainer.append(rechargeSelectElement.parentElement); clearInterval(checkForNewDeliveryOptionsContainer); // We only want to add the additional styles on the PDP if its the refill collection if (collection?.id === 449976238310 || pageTemplate === 'page.refill-club' || isStyledForCarousel) { conditionallyStyleRechargeOnPLP(); } // Remove the strong tag between the span and the subscribe and save text const subSaveSpan = document.querySelector(`#subscription-selector-${productId} .rc-option__text[data-label-text-subsave]`); if (subSaveSpan.querySelector('strong')) { const strongElement = subSaveSpan.querySelector('strong'); const strongText = strongElement.textContent.trim(); const textNode = document.createTextNode(strongText); subSaveSpan.replaceChild(textNode, strongElement); } clearInterval(checkForRechargeSelect); resolve(); } }, 100); } }, 100); }); } /** * @description Checks to see if the recharge widget is on the PDP, if so, it conditionally applies CSS styles, since there will be some products on the PDP that don't have * a

element, but all selects on the PDP will need to have a transparent background. */ function conditionallyStyleRechargeOnPLP() { // This is the id associated with the refill collection. We need to conditionally rechargeSelectElement.classList.add('!bg-transparent') const parentElement = deliveryOptionsContainer.parentElement; const variantSelectCard = parentElement.querySelector(':scope > variant-select-card'); if (!variantSelectCard) { deliveryOptionsContainer.classList.add('border-t', 'border-t-brand-secondary-200') // Because of the lack of a variant card, we need to remove the bottom border, so the line between the delivery options, and the add to bag doesn't have an extra thick border deliveryOptionsContainer.firstChild.classList.add('border-b-0') } } /** * @description Dynamically creates a list of bullet points whenever the shouldRenderBulletPoints flag is true. */ function createBulletPoints() { if (shouldRenderBulletPoints && subscriptionBullets.length > 0) { const subscriptionSelector = document.querySelector(`#subscription-selector-${productId}`); subscriptionPerksContainer = document.createElement('div'); subscriptionPerksContainer.id = `${productId}-subscription-perks`; subscriptionPerksContainer.className = 'hidden space-y-2 pb-4 pt-3'; subscriptionBullets.forEach((bullet, index) => { const bulletPoint = document.createElement('p'); bulletPoint.className = 'flex items-center w-full text-brand-secondary-200'; bulletPoint.innerHTML = ` ${renderIconCircleCheckmark()} ${bullet} `; if (shouldApplyBottomMargin && index === subscriptionBullets.length - 1) { bulletPoint.style.marginBottom = '4px'; } subscriptionPerksContainer.appendChild(bulletPoint); }); subscriptionSelector.appendChild(subscriptionPerksContainer); } } function renderIconCircleCheckmark() { return ``; } function assignProductBullets() { if (window.subscriptonBulletsDict && window.subscriptonBulletsDict[productId]) { let newSubscriptionBulletList = [...window.subscriptonBulletsDict[productId]] subscriptionBullets = newSubscriptionBulletList } else { subscriptionBullets = defaultSubscriptionBullets; } } /** * @param {string} newProductId the new product id that will be used to update all exising HTML elements, and rebind the function on the window * @description Whenever the recharge widget is remounted, we need to update the initial bullet with the new product description */ async function onNewProductSelect(newProductId) { if (productId !== newProductId) { let rechargeContainer = document.querySelector(`#subscription-selector-${productId}`); let cardAtcContainer = document.querySelector(`#card-atc-${productId}`) // Show loading spinner rechargeContainer.innerHTML = ` `; // cleanup all event listeners and old elements from the old widget handleRechargeEventListeners('remove'); if (window.ReChargeWidget.getWidgetsByProductId(productId)[0]) { window.ReChargeWidget.getWidgetsByProductId(productId)[0].widgetInstance.unmount(); } if (subscriptionPerksContainer) { subscriptionPerksContainer.remove(); } // After cleaning up the previous widget, reassign the id to properly mount the new widget. rechargeContainer.id = `subscription-selector-${newProductId}`; productId = Number(newProductId); const config = { productId: newProductId, injectionParent: `#subscription-selector-${newProductId}` }; // Re-establish and mount new widget window.ReChargeWidget.createWidget(config); // Wait for all elements to mount await waitForRechargeWidgetToBeMounted(); await waitForRechargeSelect(); await relocateSubscriptionPriceAndCleanup(); if (shouldRenderBulletPoints) { assignProductBullets() createBulletPoints(); } handleRechargeEventListeners('add'); configureRechargeForProductSelection(newProductId); // Hide spinner const spinnerElement = document.getElementById('loading-spinner'); if (spinnerElement) { spinnerElement.remove(); } if (rechargeWidgetContainer) { rechargeWidgetContainer.style.opacity = '100'; rechargeWidgetContainer.classList.add('opacity-100', 'transition-opacity', 'duration-300'); } } } /** * @param {string} productId productId responsible for the unique key to be set on the window, so all instances of the widget can be accessed. * @description Function responsible for binding the onNewProduct select to the window. */ function configureRechargeForProductSelection(productId) { window.HairstoryRechargeWidget = window.HairstoryRechargeWidget || {}; window.HairstoryRechargeWidget[productId] = { onNewProductSelect: onNewProductSelect }; } // Initial mount of recharge widget initializeRechargeWidget().catch(error => { console.error("Error initializing ReCharge widget:", error); }); }); View More Information Suited For Dry Hair, Damaged Hair What It Is PRE-WASH, NEW WASH RICH AND PRIMER Benefits Clarify , cleanse and condition , protect FOR DRY HAIR Learn More
Save 20%

The Richest New Wash Method

PRE-WASH, NEW WASH RICH AND PRIMER

`; } await waitForRechargeWidgetToBeMounted(); await waitForRechargeSelect(); await relocateSubscriptionPriceAndCleanup(); if (shouldRenderBulletPoints) { assignProductBullets(); createBulletPoints(); } handleRechargeEventListeners('add'); configureRechargeForProductSelection(productId); const spinnerElement = document.getElementById('loading-spinner'); if (spinnerElement) { spinnerElement.remove(); } if (rechargeWidgetContainer) { rechargeWidgetContainer.style.opacity = '100'; rechargeWidgetContainer.classList.add('opacity-100', 'transition-opacity', 'duration-300'); } } /** * @description Function that is used to initially mount the recharge widget * @returns { boolean } based on if the product is found in recharge or not. If false, then the widget will not mount. * In addition if it cannot find the product, it short circuits and returns early. */ function shouldRechargeMount() { const checkReChargeWidget = setInterval(() => { if (window.ReChargeWidget && window.ReChargeWidget.api) { clearInterval(checkReChargeWidget); window.ReChargeWidget.api.fetchProduct(productId) .then(rechargeProduct => { if (!rechargeProduct.in_recharge) { return false; } const config = { productId: productId, injectionParent: `#subscription-selector-${productId}` }; window.ReChargeWidget.createWidget(config); return true; }) } }, 100); } async function waitForRechargeWidgetToBeMounted() { return new Promise(resolve => { const checkForRechargeContainer = setInterval(() => { if (document.querySelector(`#subscription-selector-${productId} > .rc-container-wrapper`)) { rechargeWidgetContainer = document.querySelector(`#subscription-selector-${productId} > .rc-container-wrapper`) clearInterval(checkForRechargeContainer) resolve() } }, 100) }) } /** * @description Function that is used to determine the initial price of the product, and then properly assign * the otpPriceText and subscriptionPriceText * @param {HTMLElement} originalPriceElement HTML Element that is used to determine the base price, and then calculate the subscription discount */ function derivePriceOptions(originalPriceElements) { let priceElementTextContent = originalPriceElements[0].innerText; let priceRegex = /([£$€¥])(\d+(?:\.\d{1,2})?)/; let match = priceElementTextContent.match(priceRegex); if (match) { let currencySymbol = match[1]; let priceValue = match[2]; otpPriceText = currencySymbol + priceValue; let subscriptionPrice if (productsWithTenPercentOff.includes(productId)) { subscriptionPrice = Number(priceValue) * 0.9; } else { subscriptionPrice = Number(priceValue) * 0.95; } subscriptionPriceText = `${currencySymbol}${subscriptionPrice.toFixed(2)}`; } } /** * @description Function to add or remove Recharge event listeners * @param {string} action - 'add' to add listeners, 'remove' to remove listeners */ function handleRechargeEventListeners(action) { function manageListeners() { const oneTimePurchaseInput = document.querySelector(`#subscription-selector-${productId} [data-radio-onetime]`); const subscriptionPurchaseInput = document.querySelector(`#subscription-selector-${productId} [data-radio-subsave]`); if (oneTimePurchaseInput && subscriptionPurchaseInput) { if (originalPriceElements.length > 0 && action === 'add') { derivePriceOptions(originalPriceElements) } if (currentSubscriptionSelector) { // Because recharge is responsbile for adding the .rc-option--active class when a user clicks a radio, we must set it manually for the accent-color black // to be present if we switch the radio manually, after the recharge widget remounts if (currentSubscriptionSelector === 'subscription') { subscriptionPurchaseInput.classList.add('rc-option--active') subscriptionPurchaseInput.checked = true // In products.js, the onSubmitHandler checks for a select element named 'selling_plan'. // When remounting and setting the checked value programmatically, it defaults to 'One time purchase', adding name=''. // If switching products and subscribe and save is selected, the select with name='' will still mount, causing the cart item to show a OTP rechargeSelectElement.setAttribute('name', 'selling_plan') let rechargeSellingPlans = deliveryOptionsContainer.querySelector('.rc-selling-plans') // By default our config for recharge is set to OTP. If we remount with a subscription option selected via our event handlers // The select with have a display: none; hardcoded onto the style, so we must remove it in order for it to be visible. if (rechargeSellingPlans) { if (rechargeSellingPlans.style.display === 'none') { rechargeSellingPlans.style.removeProperty('display'); } } } else { deliveryOptionsContainer.classList.add('hidden') oneTimePurchaseInput.classList.add('rc-option--active') oneTimePurchaseInput.checked = true // In products.js, the onSubmitHandler checks for a select element named 'selling_plan'. // When remounting and setting the checked value programmatically, it defaults to 'subscribe and save', adding name='selling_plan'. // If switching products and OTP is selected, the input with name='selling_plan' will still mount, causing the cart item to show as a subscription, even if it's OTP. rechargeSelectElement.setAttribute('name', '') } } // Check to see which input is checked on dom mount, then properly set the price based on the product and variant selected. if (subscriptionPurchaseInput.checked === true) { // reassign the perks container to the new product id on being switched, or set the intitial element if it doesn't exist on initial mount if ((!subscriptionPerksContainer && action === 'add') || (action === 'add' && subscriptionPerksContainer.id !== document.querySelector(`[id="${productId}-subscription-perks"]`).id)) { subscriptionPerksContainer = document.querySelector(`[id="${productId}-subscription-perks"]`); } if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = subscriptionPriceText; }); } if (shouldRenderBulletPoints) { subscriptionPerksContainer.classList.remove('hidden'); } } else { if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = otpPriceText; }); } } if (action === 'add') { oneTimePurchaseInput.addEventListener('click', onOneTimePurchaseClick); subscriptionPurchaseInput.addEventListener('click', onSubscriptionPurchaseClick); // At the time of this writing. Recharge after mounting sets the selling plan to 0. so we need to set the name manually // otherwise on PLP's radio options will only allow one option to be set oneTimePurchaseInput.name = `${productId}_selling_option` subscriptionPurchaseInput.name = `${productId}_selling_option` } else if (action === 'remove') { oneTimePurchaseInput.removeEventListener('click', onOneTimePurchaseClick); subscriptionPurchaseInput.removeEventListener('click', onSubscriptionPurchaseClick); } handleSellingPlanSelect(action) return true; } return false; } if (manageListeners()) return; const waitForElements = setInterval(() => { if (manageListeners()) { clearInterval(waitForElements); } }, 100); } /** * @description binds the proper event listerns to the proper selling plan select, in addition to pre-selecting the existing plan if * the previously selected option is present in the newly mounted select. */ function handleSellingPlanSelect(action) { const checkForSellingPlanSelect = setInterval(() => { const sellingPlanSelect = document.querySelector(`#selling_plan_${productId}`); if (sellingPlanSelect) { clearInterval(checkForSellingPlanSelect); if (action === 'add') { sellingPlanSelect.addEventListener('change', onSellingPlanChange); } else if (action === 'remove') { sellingPlanSelect.removeEventListener('change', onSellingPlanChange); } if (lastSelectedSellingPlan) { // Convert array like object into a proper array const matchingSellingPlanOption = [...sellingPlanSelect.options].find(option => option.textContent.trim() === lastSelectedSellingPlan ); if (matchingSellingPlanOption) { sellingPlanSelect.value = matchingSellingPlanOption.value; } } } }, 100); } /** * @description Function that is called when the OTP input is clicked */ function onOneTimePurchaseClick() { onRechargeOptionClick('otp'); } /** * @description Function that is called when the subscription input is clicked */ function onSubscriptionPurchaseClick() { onRechargeOptionClick('subscription'); } /** * @description Function that is called whenever the selling plan selected option has changed */ function onSellingPlanChange(event) { lastSelectedSellingPlan = event.target.options[event.target.selectedIndex].textContent.trim(); } /** * @description Function that is called when a recharge option input is clicked. Primarly used to add bullet points, and make sure the select has an option mounted * @param {string} optionType The type of option that was clicked, either OTP, or subscription */ function onRechargeOptionClick(optionType) { const selectedPriceText = optionType === 'subscription' ? subscriptionPriceText : otpPriceText; // Save the current selected selector, so when we remount, we can default to that option being checked currentSubscriptionSelector = optionType if (originalPriceElements[0].getAttribute('data-price-id') !== `price-${productId}`) { let originalPriceElementQuerySelector = document.querySelectorAll(`[data-price-id="price-${productId}"]`); // Assign all matching elements to the array originalPriceElements = Array.from(originalPriceElementQuerySelector); } if (originalPriceElements.length > 0) { originalPriceElements.forEach(element => { element.textContent = selectedPriceText; }); } if (optionType === 'otp') { deliveryOptionsContainer.classList.add('hidden') } // If the widget has been remounted, and OTP was selected, we need to remove hidden from the container so a user can see the selling plans if (deliveryOptionsContainer && optionType === 'subscription' && deliveryOptionsContainer.classList.contains('hidden')) { deliveryOptionsContainer.classList.remove('hidden') } if (subscriptionPerksContainer) { if (optionType === 'subscription' && shouldRenderBulletPoints) { subscriptionPerksContainer.classList.remove('hidden'); } else { subscriptionPerksContainer.classList.add('hidden'); } } if (optionType === 'subscription') { if (rechargeSelectElement) { // Make sure the select element as the first child element selected after mount // otherwise the select shows as empty rechargeSelectElement.children[0].selected = 'selected'; // on remount, if OTP is selected, we remove the name from the selling plan, this messes up recharge's internal logic, and then the select name always remains empty // to stop this from happening, we just manually set it everytime subscription is clicked rechargeSelectElement.setAttribute('name', 'selling_plan') } } if (optionType === 'otp') { if (rechargeSelectElement) { // When we select the OTP, but subscription was selected previously, it will still have the selling plan, name, we need to remove this or it falsely gets set as a subscription product rechargeSelectElement.setAttribute('name', '') } } } /** * @description Waits for the data price subsave attribute to be loaded, and then moves it down to the add cart button, in additio to cleaning up any remnants from recharge */ async function relocateSubscriptionPriceAndCleanup() { return new Promise((resolve) => { const waitForPriceElements = setInterval(() => { const subscriptionPriceElement = document.querySelector(`#subscription-selector-${productId} .rc_widget__price--subsave[data-price-subsave]`); const otpPriceElement = document.querySelector(`#subscription-selector-${productId} .rc_widget__price--onetime[data-price-onetime]`); const discountSpan = document.querySelector(`#subscription-selector-${productId} .rc_widget__option__discount[data-label-discount]`) let originalPriceElementQuerySelector = document.querySelectorAll(`[data-price-id="price-${productId}"]`); // Assign all matching elements to the array originalPriceElements = Array.from(originalPriceElementQuerySelector); if (subscriptionPriceElement && otpPriceElement && originalPriceElements.length > 0 && discountSpan) { // Left over elements from recharge that need to be hidden // I.E Subscription price vs OTP price, and percentage discounted const unusedRechargeElementSelectors = [ `#subscription-selector-${productId} .rc_widget__price--subsave[data-price-subsave]`, `#subscription-selector-${productId} .rc_widget__price--onetime[data-price-onetime]`, `#subscription-selector-${productId} .rc_widget__option__discount[data-label-discount]` ]; unusedRechargeElementSelectors.forEach(selector => { const element = document.querySelector(selector); if (element) { element.classList.add('hidden'); } }); clearInterval(waitForPriceElements); resolve(); } }, 100); }) } /** * @description Waits for the recharge select element to be loaded into the dom, and then moves it down to below the variant selector */ function waitForRechargeSelect() { return new Promise((resolve) => { const checkForRechargeSelect = setInterval(() => { if (document.querySelector(`#selling_plan_${productId}`)) { rechargeSelectElement = document.querySelector(`#selling_plan_${productId}`); const checkForNewDeliveryOptionsContainer = setInterval(() => { if (document.querySelector(`#delivery-options-${productId}`)) { deliveryOptionsContainer = document.querySelector(`#delivery-options-${productId}`); deliveryOptionsContainer.append(rechargeSelectElement.parentElement); clearInterval(checkForNewDeliveryOptionsContainer); // We only want to add the additional styles on the PDP if its the refill collection if (collection?.id === 449976238310 || pageTemplate === 'page.refill-club' || isStyledForCarousel) { conditionallyStyleRechargeOnPLP(); } // Remove the strong tag between the span and the subscribe and save text const subSaveSpan = document.querySelector(`#subscription-selector-${productId} .rc-option__text[data-label-text-subsave]`); if (subSaveSpan.querySelector('strong')) { const strongElement = subSaveSpan.querySelector('strong'); const strongText = strongElement.textContent.trim(); const textNode = document.createTextNode(strongText); subSaveSpan.replaceChild(textNode, strongElement); } clearInterval(checkForRechargeSelect); resolve(); } }, 100); } }, 100); }); } /** * @description Checks to see if the recharge widget is on the PDP, if so, it conditionally applies CSS styles, since there will be some products on the PDP that don't have * a element, but all selects on the PDP will need to have a transparent background. */ function conditionallyStyleRechargeOnPLP() { // This is the id associated with the refill collection. We need to conditionally rechargeSelectElement.classList.add('!bg-transparent') const parentElement = deliveryOptionsContainer.parentElement; const variantSelectCard = parentElement.querySelector(':scope > variant-select-card'); if (!variantSelectCard) { deliveryOptionsContainer.classList.add('border-t', 'border-t-brand-secondary-200') // Because of the lack of a variant card, we need to remove the bottom border, so the line between the delivery options, and the add to bag doesn't have an extra thick border deliveryOptionsContainer.firstChild.classList.add('border-b-0') } } /** * @description Dynamically creates a list of bullet points whenever the shouldRenderBulletPoints flag is true. */ function createBulletPoints() { if (shouldRenderBulletPoints && subscriptionBullets.length > 0) { const subscriptionSelector = document.querySelector(`#subscription-selector-${productId}`); subscriptionPerksContainer = document.createElement('div'); subscriptionPerksContainer.id = `${productId}-subscription-perks`; subscriptionPerksContainer.className = 'hidden space-y-2 pb-4 pt-3'; subscriptionBullets.forEach((bullet, index) => { const bulletPoint = document.createElement('p'); bulletPoint.className = 'flex items-center w-full text-brand-secondary-200'; bulletPoint.innerHTML = ` ${renderIconCircleCheckmark()} ${bullet} `; if (shouldApplyBottomMargin && index === subscriptionBullets.length - 1) { bulletPoint.style.marginBottom = '4px'; } subscriptionPerksContainer.appendChild(bulletPoint); }); subscriptionSelector.appendChild(subscriptionPerksContainer); } } function renderIconCircleCheckmark() { return ``; } function assignProductBullets() { if (window.subscriptonBulletsDict && window.subscriptonBulletsDict[productId]) { let newSubscriptionBulletList = [...window.subscriptonBulletsDict[productId]] subscriptionBullets = newSubscriptionBulletList } else { subscriptionBullets = defaultSubscriptionBullets; } } /** * @param {string} newProductId the new product id that will be used to update all exising HTML elements, and rebind the function on the window * @description Whenever the recharge widget is remounted, we need to update the initial bullet with the new product description */ async function onNewProductSelect(newProductId) { if (productId !== newProductId) { let rechargeContainer = document.querySelector(`#subscription-selector-${productId}`); let cardAtcContainer = document.querySelector(`#card-atc-${productId}`) // Show loading spinner rechargeContainer.innerHTML = ` `; // cleanup all event listeners and old elements from the old widget handleRechargeEventListeners('remove'); if (window.ReChargeWidget.getWidgetsByProductId(productId)[0]) { window.ReChargeWidget.getWidgetsByProductId(productId)[0].widgetInstance.unmount(); } if (subscriptionPerksContainer) { subscriptionPerksContainer.remove(); } // After cleaning up the previous widget, reassign the id to properly mount the new widget. rechargeContainer.id = `subscription-selector-${newProductId}`; productId = Number(newProductId); const config = { productId: newProductId, injectionParent: `#subscription-selector-${newProductId}` }; // Re-establish and mount new widget window.ReChargeWidget.createWidget(config); // Wait for all elements to mount await waitForRechargeWidgetToBeMounted(); await waitForRechargeSelect(); await relocateSubscriptionPriceAndCleanup(); if (shouldRenderBulletPoints) { assignProductBullets() createBulletPoints(); } handleRechargeEventListeners('add'); configureRechargeForProductSelection(newProductId); // Hide spinner const spinnerElement = document.getElementById('loading-spinner'); if (spinnerElement) { spinnerElement.remove(); } if (rechargeWidgetContainer) { rechargeWidgetContainer.style.opacity = '100'; rechargeWidgetContainer.classList.add('opacity-100', 'transition-opacity', 'duration-300'); } } } /** * @param {string} productId productId responsible for the unique key to be set on the window, so all instances of the widget can be accessed. * @description Function responsible for binding the onNewProduct select to the window. */ function configureRechargeForProductSelection(productId) { window.HairstoryRechargeWidget = window.HairstoryRechargeWidget || {}; window.HairstoryRechargeWidget[productId] = { onNewProductSelect: onNewProductSelect }; } // Initial mount of recharge widget initializeRechargeWidget().catch(error => { console.error("Error initializing ReCharge widget:", error); }); }); View More Information Suited For Flat Hair, Oily Hair What It Is PRE-WASH, NEW WASH DEEP AND PRIMER Benefits Clarify , cleanse and condition , protect FOR OILY HAIR Learn More
Save 20%

The Clarifying New Wash Method

PRE-WASH, NEW WASH DEEP AND PRIMER

Pomade, Hair Wax, and Everything In-Between
| Hairstory (2024)
Top Articles
Latest Posts
Recommended Articles
Article information

Author: Ray Christiansen

Last Updated:

Views: 6042

Rating: 4.9 / 5 (49 voted)

Reviews: 80% of readers found this page helpful

Author information

Name: Ray Christiansen

Birthday: 1998-05-04

Address: Apt. 814 34339 Sauer Islands, Hirtheville, GA 02446-8771

Phone: +337636892828

Job: Lead Hospitality Designer

Hobby: Urban exploration, Tai chi, Lockpicking, Fashion, Gunsmithing, Pottery, Geocaching

Introduction: My name is Ray Christiansen, I am a fair, good, cute, gentle, vast, glamorous, excited person who loves writing and wants to share my knowledge and understanding with you.