Hi, it’s a pleasure to be part of the community. I’m new to Shopify development and exploring the Checkout UI extension. I’ve been creating components to display products using JavaScript, but I’ve been running into an issue with the select “onchange” event and have had little information on how to fix it. It’s just a rendering error. I don’t know if I’m doing it wrong. When I change the value, I can’t update the content (text) of another component. I read it was with updateprops - children, but it’s not working for me. Thanks in advance.
Perhaps show some more information including the code you’re using so people will be able to assist you.
ok ty, Here’s an example of how I’m implementing it.
Summary
import {
extension,
BlockStack,
InlineLayout,
ProductThumbnail,
Text,
Button,
Select,
} from '@shopify/ui-extensions/checkout';
export default extension("purchase.checkout.reductions.render-after", (root, api) => {
const offersData = [
{
offer_id: "offer1",
title: "Oferta 1",
sub_title: "Descuento increíble en Oferta 1",
products: [
{
id: "p1",
title: "Snowboard A",
variants: [
{
label: "Default Title",
value: "default",
price: "854.96",
comparePrice: "949.95",
image: "https://cdn-icons-png.flaticon.com/256/14477/14477431.png",
},
{
label: "Small",
value: "small",
price: "800.00",
comparePrice: "900.00",
image: "https://raulperez.tieneblog.net/wp-content/uploads/2015/09/tux.jpg",
},
],
},
{
id: "p2",
title: "Gift Card",
variants: [
{
label: "$10",
value: "10",
price: "9.00",
comparePrice: "10.00",
image: "https://cdn-icons-png.freepik.com/256/9853/9853618.png?semt=ais_hybrid",
},
{
label: "$20",
value: "20",
price: "18.00",
comparePrice: "20.00",
image: "https://images.vexels.com/media/users/3/311026/isolated/lists/2219363ba5e645719b1a70cfe7374c5a-una-imagen-plana-de-noche.png",
},
],
},
],
},
{
offer_id: "offer2",
title: "Oferta 2",
sub_title: "Descuento especial en Oferta 2",
products: [
{
id: "p3",
title: "Snowboard C",
variants: [
{
label: "Red",
value: "red",
price: "700.00",
comparePrice: "750.00",
image: "https://cdn-icons-png.freepik.com/256/9398/9398309.png",
},
{
label: "Green",
value: "green",
price: "680.00",
comparePrice: "730.00",
image: "https://play-lh.googleusercontent.com/a1MO6d7PL9wirOcTZLaQpD76hGfzltIBCUc3zFZ6ph9VAui7HwRVs7QhQm8F_iNnkw=s256-rw",
},
],
},
{
id: "p4",
title: "Gift Card Premium",
variants: [
{
label: "$45",
value: "45",
price: "45.00",
comparePrice: "50.00",
image: "https://i.pinimg.com/474x/1a/41/eb/1a41ebd5ea71b2cda16de067603095e7.jpg",
},
],
},
],
},
];
let currentOfferIndex = 0;
const offerComponents = [];
function createOfferComponent(offer) {
const headingTitle = root.createComponent(Text, {
size: "large",
emphasis: "bold"
}, offer.title);
const headingSubtitle = root.createComponent(Text, {
size: "small",
appearance: "subdued"
}, offer.sub_title);
const productRows = offer.products.map((prod) => {
const initialVariant = prod.variants?.[0];
const productImage = root.createComponent(ProductThumbnail, {
source: initialVariant?.image || "https://via.placeholder.com/80",
accessibilityDescription: prod.title,
});
const titleText = root.createComponent(Text, {
size: "medium",
emphasis: "bold"
}, prod.title);
const textRefs = {};
const finalPriceText = root.createComponent(Text, {
size: "small",
appearance: "success",
}, `PEN ${initialVariant?.price || "0.00"}`);
textRefs.finalPriceText = finalPriceText;
const comparePriceText = root.createComponent(Text, {
size: "small",
style: { textDecoration: "line-through", marginLeft: "4px", color: "#999" }
}, `PEN ${initialVariant?.comparePrice || "0.00"}`);
textRefs.comparePriceText = comparePriceText;
const priceLayout = root.createComponent(InlineLayout, {
spacing: "extraTight",
columns: ["auto", "auto"]
}, [finalPriceText, comparePriceText]);
let variantSelect = null;
if (prod.variants && prod.variants.length > 1) {
const variantOptions = prod.variants.map(v => ({
label: v.label,
value: v.value,
}));
variantSelect = root.createComponent(Select, {
label: "Variant",
options: variantOptions,
value: initialVariant.value,
onChange: (newVal) => {
const found = prod.variants.find(v => v.value === newVal);
if (found) {
productImage.updateProps({ source: found.image });
textRefs.finalPriceText.updateProps({ children: `PEN ${found.price}` });
textRefs.comparePriceText.updateProps({ children: `PEN ${found.comparePrice}` });
}
},
});
} else if (prod.variants && prod.variants.length === 1) {
const single = prod.variants[0];
variantSelect = root.createComponent(Select, {
label: "Variant",
options: [{ label: single.label, value: single.value }],
value: single.value,
onChange: (val) => console.log("Solo 1 variante, se eligió:", val),
});
}
const infoChildren = [titleText, priceLayout];
if (variantSelect) infoChildren.push(variantSelect);
const middleInfo = root.createComponent(BlockStack, { spacing: "none" }, infoChildren);
const pickButton = root.createComponent(Button, {
kind: "primary",
onPress: () => console.log("Pick product:", prod.id)
}, "Pick");
const row = root.createComponent(InlineLayout, {
columns: ["auto", "fill", "auto"],
spacing: "base",
blockAlignment: "center",
}, [productImage, middleInfo, pickButton]);
return root.createComponent(BlockStack, {
spacing: "none",
border: "base",
padding: "base",
cornerRadius: "loose",
}, [row]);
});
return root.createComponent(BlockStack, { spacing: "base" }, [
headingTitle,
headingSubtitle,
...productRows
]);
}
offersData.forEach((offer, index) => {
const offerComp = createOfferComponent(offer);
offerComp.updateProps({ display: index === 0 ? "auto" : "none" });
offerComponents.push(offerComp);
});
const prevButton = root.createComponent(Button, {
kind: "secondary",
disabled: currentOfferIndex === 0,
onPress: () => {
if (currentOfferIndex > 0) {
offerComponents[currentOfferIndex].updateProps({ display: "none" });
currentOfferIndex--;
offerComponents[currentOfferIndex].updateProps({ display: "auto" });
updateNavButtons();
}
}
}, "<");
const nextButton = root.createComponent(Button, {
kind: "secondary",
disabled: currentOfferIndex === offersData.length - 1,
onPress: () => {
if (currentOfferIndex < offersData.length - 1) {
offerComponents[currentOfferIndex].updateProps({ display: "none" });
currentOfferIndex++;
offerComponents[currentOfferIndex].updateProps({ display: "auto" });
updateNavButtons();
}
}
}, ">");
function updateNavButtons() {
prevButton.updateProps({ disabled: currentOfferIndex === 0 });
nextButton.updateProps({ disabled: currentOfferIndex === offersData.length - 1 });
}
const navBar = root.createComponent(InlineLayout, { spacing: "base" }, [prevButton, nextButton]);
const offersStack = root.createComponent(BlockStack, { spacing: "base" }, [...offerComponents]);
const mainContainer = root.createComponent(BlockStack, { spacing: "base" }, [
offersStack,
navBar,
]);
root.append(mainContainer);
});
Hey @Angelo_Lim_Mendo, within your onChange
handler, could you try replaceChildren
to set the desired text instead of updateProps
?
Thanks for the reply. It’s interesting that replacechildren allows you to replace (render). Even though it’s only changed once, it doesn’t do so afterward. But you gave me a clue about whether or not to rely solely on updateprops. Sorry, I’m not familiar with the entire Shopify ecosystem yet.
The issue might stem from how the textRefs
object is utilized within createOfferComponent()
.
Hi, thanks for your response. I did remove those test lines. I’ve now tested them this way, but using replaceChildren I only managed to replace the data once.
import {
extension,
BlockStack,
InlineLayout,
ProductThumbnail,
Text,
Button,
Select,
} from '@shopify/ui-extensions/checkout';
export default extension("purchase.checkout.reductions.render-after", (root, api) => {
const offersData = [
{
offer_id: "offer1",
title: "Oferta 1",
sub_title: "Descuento increíble en Oferta 1",
products: [
{
id: "p1",
title: "Snowboard A",
variants: [
{
label: "Little",
value: "little",
price: "854.96",
comparePrice: "949.95",
image: "https://cdn-icons-png.flaticon.com/256/14477/14477431.png",
},
{
label: "Small",
value: "small",
price: "800.00",
comparePrice: "900.00",
image: "https://raulperez.tieneblog.net/wp-content/uploads/2015/09/tux.jpg",
},
{
label: "Large",
value: "large",
price: "900.00",
comparePrice: "1000.00",
image: "https://play-lh.googleusercontent.com/a1MO6d7PL9wirOcTZLaQpD76hGfzltIBCUc3zFZ6ph9VAui7HwRVs7QhQm8F_iNnkw=s256-rw",
},
],
},
{
id: "p2",
title: "Gift Card",
variants: [
{
label: "$10",
value: "10",
price: "9.00",
comparePrice: "10.00",
image: "https://cdn-icons-png.freepik.com/256/9853/9853618.png?semt=ais_hybrid",
},
{
label: "$20",
value: "20",
price: "18.00",
comparePrice: "20.00",
image: "https://images.vexels.com/media/users/3/311026/isolated/lists/2219363ba5e645719b1a70cfe7374c5a-una-imagen-plana-de-noche.png",
},
],
},
],
},
{
offer_id: "offer2",
title: "Oferta 2",
sub_title: "Descuento especial en Oferta 2",
products: [
{
id: "p3",
title: "Snowboard C",
variants: [
{
label: "Red",
value: "red",
price: "700.00",
comparePrice: "750.00",
image: "https://cdn-icons-png.freepik.com/256/9398/9398309.png",
},
{
label: "Green",
value: "green",
price: "680.00",
comparePrice: "730.00",
image: "https://play-lh.googleusercontent.com/a1MO6d7PL9wirOcTZLaQpD76hGfzltIBCUc3zFZ6ph9VAui7HwRVs7QhQm8F_iNnkw=s256-rw",
},
],
},
{
id: "p4",
title: "Gift Card Premium",
variants: [
{
label: "$45",
value: "45",
price: "45.00",
comparePrice: "50.00",
image: "https://i.pinimg.com/474x/1a/41/eb/1a41ebd5ea71b2cda16de067603095e7.jpg",
},
],
},
],
},
];
let currentOfferIndex = 0;
const offerComponents = [];
function createOfferComponent(offer) {
const headingTitle = root.createComponent(Text, {
size: "large",
emphasis: "bold"
}, offer.title);
const headingSubtitle = root.createComponent(Text, {
size: "small",
appearance: "subdued"
}, offer.sub_title);
const productRows = offer.products.map((prod) => {
const initialVariant = prod.variants?.[0];
const productImage = root.createComponent(ProductThumbnail, {
source: initialVariant?.image || "https://via.placeholder.com/80",
accessibilityDescription: prod.title,
});
const titleText = root.createComponent(Text, {
size: "medium",
emphasis: "bold"
}, prod.title);
const finalPriceText = root.createComponent(Text, {
size: "small",
appearance: "success",
}, `USD ${initialVariant?.price || "0.00"}`);
const comparePriceText = root.createComponent(Text, {
size: "small",
style: { textDecoration: "line-through", marginLeft: "4px", color: "#999" }
}, `USD ${initialVariant?.comparePrice || "0.00"}`);
const priceLayout = root.createComponent(InlineLayout, {
spacing: "extraTight",
columns: ["auto", "auto"]
}, [finalPriceText, comparePriceText]);
let variantSelect = null;
if (prod.variants && prod.variants.length > 1) {
const variantOptions = prod.variants.map(v => ({
label: v.label,
value: v.value,
}));
variantSelect = root.createComponent(Select, {
label: "Variant",
options: variantOptions,
value: initialVariant.value,
onChange: (newVal) => {
const found = prod.variants.find(v => v.value === newVal);
if (found) {
productImage.updateProps({ source: found.image });
finalPriceText.replaceChildren({ children: `USD ${found.price}` });
comparePriceText.replaceChildren({ children: `USD ${found.comparePrice}` });
}
},
});
} else if (prod.variants && prod.variants.length === 1) {
const single = prod.variants[0];
variantSelect = root.createComponent(Select, {
label: "Variant",
options: [{ label: single.label, value: single.value }],
value: single.value,
onChange: (val) => console.log("variant alone:", val),
});
}
const infoChildren = [titleText, priceLayout];
if (variantSelect) infoChildren.push(variantSelect);
const middleInfo = root.createComponent(BlockStack, { spacing: "none" }, infoChildren);
const pickButton = root.createComponent(Button, {
kind: "primary",
onPress: () => console.log("Pick product:", prod.id)
}, "Pick");
const row = root.createComponent(InlineLayout, {
columns: ["auto", "fill", "auto"],
spacing: "base",
blockAlignment: "center",
}, [productImage, middleInfo, pickButton]);
return root.createComponent(BlockStack, {
spacing: "none",
border: "base",
padding: "base",
cornerRadius: "loose",
}, [row]);
});
return root.createComponent(BlockStack, { spacing: "base" }, [
headingTitle,
headingSubtitle,
...productRows
]);
}
offersData.forEach((offer, index) => {
const offerComp = createOfferComponent(offer);
offerComp.updateProps({ display: index === 0 ? "auto" : "none" });
offerComponents.push(offerComp);
});
const prevButton = root.createComponent(Button, {
kind: "secondary",
disabled: currentOfferIndex === 0,
onPress: () => {
if (currentOfferIndex > 0) {
offerComponents[currentOfferIndex].updateProps({ display: "none" });
currentOfferIndex--;
offerComponents[currentOfferIndex].updateProps({ display: "auto" });
updateNavButtons();
}
}
}, "<");
const nextButton = root.createComponent(Button, {
kind: "secondary",
disabled: currentOfferIndex === offersData.length - 1,
onPress: () => {
if (currentOfferIndex < offersData.length - 1) {
offerComponents[currentOfferIndex].updateProps({ display: "none" });
currentOfferIndex++;
offerComponents[currentOfferIndex].updateProps({ display: "auto" });
updateNavButtons();
}
}
}, ">");
function updateNavButtons() {
prevButton.updateProps({ disabled: currentOfferIndex === 0 });
nextButton.updateProps({ disabled: currentOfferIndex === offersData.length - 1 });
}
const navBar = root.createComponent(InlineLayout, { spacing: "base" }, [prevButton, nextButton]);
const offersStack = root.createComponent(BlockStack, { spacing: "base" }, [...offerComponents]);
const mainContainer = root.createComponent(BlockStack, { spacing: "base" }, [
offersStack,
navBar,
]);
root.append(mainContainer);
});
Hello friends, thank you for your interest. Thanks to your suggestions, I was able to find a solution, albeit with a less flexible method. Perhaps this is because Shopify doesn’t have a suitable method for these cases, or at least there’s little information available.
Summary
import {
extension,
BlockStack,
InlineLayout,
ProductThumbnail,
Text,
Button,
Select,
} from '@shopify/ui-extensions/checkout';
export default extension("purchase.checkout.reductions.render-after", (root, api) => {
const offersData = [
{
offer_id: "offer1",
title: "Oferta 1",
sub_title: "Descuento increíble en Oferta 1",
products: [
{
id: "p1",
title: "Snowboard A",
variants: [
{
label: "Little",
value: "little",
price: "854.96",
comparePrice: "949.95",
image: "https://cdn-icons-png.flaticon.com/256/14477/14477431.png",
},
{
label: "Small",
value: "small",
price: "800.00",
comparePrice: "900.00",
image: "https://raulperez.tieneblog.net/wp-content/uploads/2015/09/tux.jpg",
},
{
label: "Large",
value: "large",
price: "900.00",
comparePrice: "1000.00",
image: "https://play-lh.googleusercontent.com/a1MO6d7PL9wirOcTZLaQpD76hGfzltIBCUc3zFZ6ph9VAui7HwRVs7QhQm8F_iNnkw=s256-rw",
},
],
},
{
id: "p2",
title: "Gift Card",
variants: [
{
label: "$10",
value: "10",
price: "9.00",
comparePrice: "10.00",
image: "https://cdn-icons-png.freepik.com/256/9853/9853618.png?semt=ais_hybrid",
},
{
label: "$20",
value: "20",
price: "18.00",
comparePrice: "20.00",
image: "https://images.vexels.com/media/users/3/311026/isolated/lists/2219363ba5e645719b1a70cfe7374c5a-una-imagen-plana-de-noche.png",
},
],
},
],
},
{
offer_id: "offer2",
title: "Oferta 2",
sub_title: "Descuento especial en Oferta 2",
products: [
{
id: "p3",
title: "Snowboard C",
variants: [
{
label: "Red",
value: "red",
price: "700.00",
comparePrice: "750.00",
image: "https://cdn-icons-png.freepik.com/256/9398/9398309.png",
},
{
label: "Green",
value: "green",
price: "680.00",
comparePrice: "730.00",
image: "https://play-lh.googleusercontent.com/a1MO6d7PL9wirOcTZLaQpD76hGfzltIBCUc3zFZ6ph9VAui7HwRVs7QhQm8F_iNnkw=s256-rw",
},
],
},
{
id: "p4",
title: "Gift Card Premium",
variants: [
{
label: "$45",
value: "45",
price: "45.00",
comparePrice: "50.00",
image: "https://i.pinimg.com/474x/1a/41/eb/1a41ebd5ea71b2cda16de067603095e7.jpg",
},
],
},
],
},
];
let currentOfferIndex = 0;
const offerComponents = [];
function createOfferComponent(offer) {
const headingTitle = root.createComponent(Text, {
size: "large",
emphasis: "bold"
}, offer.title);
const headingSubtitle = root.createComponent(Text, {
size: "small",
appearance: "subdued"
}, offer.sub_title);
const productRows = offer.products.map((prod) => {
const initialVariant = prod.variants?.[0];
const productImage = root.createComponent(ProductThumbnail, {
source: initialVariant?.image || "https://via.placeholder.com/80",
accessibilityDescription: prod.title,
});
const titleText = root.createComponent(Text, {
size: "medium",
emphasis: "bold"
}, prod.title);
const finalPriceContainer = root.createComponent(BlockStack, {});
let finalPriceText = root.createComponent(Text, {
size: "small",
appearance: "success",
}, `USD ${initialVariant?.price || "0.00"}`);
finalPriceContainer.append(finalPriceText);
const comparePriceTextContainer = root.createComponent(BlockStack, {});
let comparefinalPriceText = root.createComponent(Text, {
size: "small",
appearance: "subdued",
accessibilityRole: "deletion"
}, `USD ${initialVariant?.comparePrice || "0.00"}`);
comparePriceTextContainer.append(comparefinalPriceText);
const priceLayout = root.createComponent(InlineLayout, {
spacing: "extraTight",
columns: ["auto", "auto"]
}, [finalPriceContainer, comparePriceTextContainer]);
let variantSelect = null;
if (prod.variants && prod.variants.length > 1) {
const variantOptions = prod.variants.map(v => ({
label: v.label,
value: v.value,
}));
variantSelect = root.createComponent(Select, {
label: "Variant",
options: variantOptions,
value: initialVariant.value,
onChange: (newVal) => {
const found = prod.variants.find(v => v.value === newVal);
if (found) {
const newFinalPriceText = root.createComponent(Text, {
size: "small",
appearance: "success"
}, `USD ${found.price}`);
finalPriceContainer.replaceChildren(newFinalPriceText);
finalPriceText = newFinalPriceText;
const newFinalcomparePriceText = root.createComponent(Text, {
size: "small",
appearance: "subdued",
accessibilityRole: "deletion"
}, `USD ${found.comparePrice}`);
comparePriceTextContainer.replaceChildren(newFinalcomparePriceText);
comparefinalPriceText = newFinalcomparePriceText;
}
},
});
} else if (prod.variants && prod.variants.length === 1) {
const single = prod.variants[0];
variantSelect = root.createComponent(Select, {
label: "Variant",
options: [{ label: single.label, value: single.value }],
value: single.value,
onChange: (val) => console.log("variant alone:", val),
});
}
const infoChildren = [titleText, priceLayout];
if (variantSelect) infoChildren.push(variantSelect);
const middleInfo = root.createComponent(BlockStack, { spacing: "none" }, infoChildren);
const pickButton = root.createComponent(Button, {
kind: "primary",
onPress: () => console.log("Pick product:", prod.id)
}, "Pick");
const row = root.createComponent(InlineLayout, {
columns: ["auto", "fill", "auto"],
spacing: "base",
blockAlignment: "center",
}, [productImage, middleInfo, pickButton]);
return root.createComponent(BlockStack, {
spacing: "none",
border: "base",
padding: "base",
cornerRadius: "loose",
}, [row]);
});
return root.createComponent(BlockStack, { spacing: "base" }, [
headingTitle,
headingSubtitle,
...productRows
]);
}
offersData.forEach((offer, index) => {
const offerComp = createOfferComponent(offer);
offerComp.updateProps({ display: index === 0 ? "auto" : "none" });
offerComponents.push(offerComp);
});
const prevButton = root.createComponent(Button, {
kind: "secondary",
disabled: currentOfferIndex === 0,
onPress: () => {
if (currentOfferIndex > 0) {
offerComponents[currentOfferIndex].updateProps({ display: "none" });
currentOfferIndex--;
offerComponents[currentOfferIndex].updateProps({ display: "auto" });
updateNavButtons();
}
}
}, "<");
const nextButton = root.createComponent(Button, {
kind: "secondary",
disabled: currentOfferIndex === offersData.length - 1,
onPress: () => {
if (currentOfferIndex < offersData.length - 1) {
offerComponents[currentOfferIndex].updateProps({ display: "none" });
currentOfferIndex++;
offerComponents[currentOfferIndex].updateProps({ display: "auto" });
updateNavButtons();
}
}
}, ">");
function updateNavButtons() {
prevButton.updateProps({ disabled: currentOfferIndex === 0 });
nextButton.updateProps({ disabled: currentOfferIndex === offersData.length - 1 });
}
const navBar = root.createComponent(InlineLayout, { spacing: "base" }, [prevButton, nextButton]);
const offersStack = root.createComponent(BlockStack, { spacing: "base" }, [...offerComponents]);
const mainContainer = root.createComponent(BlockStack, { spacing: "base" }, [
offersStack,
navBar,
]);
root.append(mainContainer);
});
Hey @Angelo_Lim_Mendo, I may not have been clear in my last response - sorry about that.
When using replaceChildren
, you can can pass in the desired string directly instead of an object. Could you try:
finalPriceText.replaceChildren(`USD ${found.price}`);