diff --git a/astro.config.mjs b/astro.config.mjs
index 3e9bece7..a6254f95 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -31,9 +31,6 @@ export default defineConfig({
trailingSlash: 'ignore',
outDir: './dist',
-
-
-
redirects: {
'/customize/design-tokens': '/developer/commerce/storefront/dropins/all/branding',
'/customize/enrich': '/developer/commerce/storefront/dropins/all/enriching',
@@ -184,6 +181,10 @@ export default defineConfig({
label: 'Styling',
link: '/dropins/all/styling/'
},
+ {
+ label: 'Slots',
+ link: '/dropins/all/slots/'
+ },
{
label: 'Layouts',
link: '/dropins/all/layouts/'
diff --git a/src/content/docs/dropins/all/enriching.mdx b/src/content/docs/dropins/all/enriching.mdx
index 8dd63563..2192e3fb 100644
--- a/src/content/docs/dropins/all/enriching.mdx
+++ b/src/content/docs/dropins/all/enriching.mdx
@@ -60,7 +60,7 @@ Content positioning is the placement of content and enrichment blocks on a webpa
### Slot API functions
-The slots API provides several functions to add content within a drop-in component by specifying a slot name and a position. The slot API provides functions to add content to specific positions above and below the slot, such as prepend and append. See the full list of slot API functions here: [Extending drop-in components](/dropins/all/extending#big-picture)
+The slots API provides several functions to add content within a drop-in component by specifying a slot name and a position. The slot API provides functions to add content to specific positions above and below the slot, such as prepend and append. See the full list of slot API functions here: [Understanding slots](/dropins/all/slots#big-picture)
diff --git a/src/content/docs/dropins/all/extending.mdx b/src/content/docs/dropins/all/extending.mdx
index ca32027a..40195b4e 100644
--- a/src/content/docs/dropins/all/extending.mdx
+++ b/src/content/docs/dropins/all/extending.mdx
@@ -1,6 +1,6 @@
---
title: Extending dropin-in components
-description: Learn about slots and how to use them to customize dropin-in components.
+description: Learn about different methods to extend dropin-in components.
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
@@ -12,50 +12,346 @@ import { Steps } from '@astrojs/starlight/components';
import Tasks from '@components/Tasks.astro';
import Task from '@components/Task.astro';
-Using slots provides the deepest level of customization. Slots are built-in extension points in the drop-in. A slot provides a place in the drop-in component to add your own UI components and functions. This architecture makes it easy to change the default look, layout, and behavior. Let's learn how it works.
+Drop-in components are designed to be flexible and extensible. This guide provides an overview of how to extend drop-in components to add new features, integrate with third-party services, and customize the user experience.
-## Big Picture
+## Extend drop-ins with Commerce APIs
-![What is a slot?](@images/slots/what-is-a-slot.svg)
+The following steps describe how to add existing Commerce API services to a drop-in. For example, the Commerce API provides the necessary endpoints to fetch and update gift messages through GraphQL, but the checkout drop-in doesn't provide this feature out of the box. We will extend the checkout drop-in by adding a UI for gift messages, use the Commerce GraphQL API to update the message data on the cart, and extend the cart drop-in to include the message data when it fetches the cart.
-
+### Step-by-step
+
+
+
+
+### Add your UI to the drop-in
+
+The first step is to create a UI for the feature and add it to the checkout drop-in. You can implement the UI however you want, as long as it can be added to the HTML DOM. For this example, we'll implement a web component (`GiftOptionsField`) that provides the form fields needed to enter a gift message. Here's an example implementation of the UI component:
+
+```js title='gift-options-field.js'
+import { Button, Input, TextArea, provider as UI } from '@dropins/tools/components.js';
+
+const sdkStyle = document.querySelector('style[data-dropin="sdk"]');
+const checkoutStyle = document.querySelector('style[data-dropin="checkout"]');
+
+class GiftOptionsField extends HTMLElement {
+ static observedAttributes = ['cartid', 'giftmessage', 'fromname', 'toname', 'loading'];
+
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+
+ this._submitGiftMessageHandler = (event) => {
+ event.preventDefault();
+ }
+ }
+
+ set submitGiftMessageHandler(callback) {
+ this._submitGiftMessageHandler = callback;
+ }
+
+ connectedCallback() {
+ this._formTemplate = document.createElement('template');
+
+ this._formTemplate.innerHTML = `
+ Gift Message
+
+ `;
+
+ this.render();
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ const toName = this.shadowRoot.querySelector('input[name="toName"]');
+ const fromName = this.shadowRoot.querySelector('input[name="fromName"]');
+ const giftMessage = this.shadowRoot.querySelector('textarea[name="giftMessage"]');
+ const cartId = this.shadowRoot.querySelector('input[name="cartId"]');
+
+ switch (name) {
+ case 'cartid':
+ cartId.value = newValue;
+ break;
+ case 'giftmessage':
+ giftMessage.value = newValue;
+ break;
+ case 'fromname':
+ fromName.value = newValue;
+ break;
+ case 'toname':
+ toName.value = newValue;
+ break;
+ case 'loading':
+ if (newValue) {
+ toName?.setAttribute('disabled', '');
+ fromName?.setAttribute('disabled', '');
+ giftMessage?.setAttribute('disabled', '');
+ } else {
+ toName?.removeAttribute('disabled');
+ fromName?.removeAttribute('disabled');
+ giftMessage?.removeAttribute('disabled');
+ }
+ break;
+ }
+ }
+
+ render() {
+ this.shadowRoot.innerHTML = '';
+
+ this.shadowRoot.appendChild(this._formTemplate.content.cloneNode(true));
+ this.shadowRoot.querySelector('input[name="cartId"]').value = this.getAttribute('cartId');
+ this.shadowRoot.querySelector('#gift-options-form').addEventListener('submit', this._submitGiftMessageHandler?.bind(this));
+
+ const submitWrapper = this.shadowRoot.querySelector('.submit-wrapper');
+ const fromNameWrapper = this.shadowRoot.querySelector('.fromName-wrapper');
+ const toNameWrapper = this.shadowRoot.querySelector('.toName-wrapper');
+ const giftMessageWrapper = this.shadowRoot.querySelector('.giftMessage-wrapper');
+
+ UI.render(Input,
+ {
+ type: "text",
+ name: "toName",
+ placeholder: "To name",
+ floatingLabel: "To name",
+ value: this.getAttribute('toName'),
+ disabled: !!this.hasAttribute('loading')
+ })(toNameWrapper);
+ UI.render(Input,
+ {
+ type: "text",
+ name: "fromName",
+ placeholder: "From name",
+ floatingLabel: "From name",
+ value: this.getAttribute('fromName'),
+ disabled: !!this.hasAttribute('loading')
+ })(fromNameWrapper);
+ UI.render(TextArea,
+ {
+ name: "giftMessage",
+ placeholder: "Message",
+ value: this.getAttribute('giftMessage'),
+ disabled: !!this.hasAttribute('loading')
+ })(giftMessageWrapper);
+ UI.render(Button,
+ {
+ variant: "primary",
+ children: "Add Message",
+ type: "submit",
+ enabled: true,
+ size: "medium",
+ disabled: !!this.hasAttribute('loading')
+ })(submitWrapper);
+
+ this.shadowRoot.appendChild(sdkStyle.cloneNode(true));
+ this.shadowRoot.appendChild(checkoutStyle.cloneNode(true));
+ }
+}
-1. `prependSibling`: A function to prepend a new HTML element before the slot's content.
-1. `prependChild`: A function to prepend a new HTML element to the slot's content.
-1. `replaceWith`: A function to replace the slot's content with a new HTML element.
-1. `appendChild`: A function to append a new HTML element to the slot's content.
-1. `appendSibling`: A function to append a new HTML element after the slot's content.
-1. `getSlotElement`: A function to get a slot element.
-1. `onChange`: A function to listen to changes in the slot's context.
-1. `dictionary`: JSON Object for the current locale. If the locale changes, the `dictionary` values change to reflect the values for the selected language.
+customElements.define('gift-options-field', GiftOptionsField);
+```
+
-
+
+### Render the UI into a checkout container
-## Vocabulary
+Next, we need to render the `GiftOptionsField` component into the checkout page by creating the `gift-options-field` [custom element](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements).
-
+```js
+const GiftOptionsField = document.createElement('gift-options-field');
+GiftOptionsField.setAttribute('loading', 'true');
+```
-### Container
+Then, insert the custom element into the layouts defined on the checkout page. The following example updates the render function for mobile and desktop to insert the `giftOptionsField` element into the layouts.
-Component that manages or encapsulates other components. Containers handle logic, fetch data, manage state, and pass data to the UI components that are rendered on the screen.
+```js title='commerce-checkout.js'
+function renderMobileLayout(block) {
+ root.replaceChildren(
+ heading,
+ giftOptionsField,
+ ...
+ );
+
+ block.replaceChildren(root);
+}
+
+function renderDesktopLayout(block) {
+ main.replaceChildren(
+ heading,
+ giftOptionsField,
+ ...
+ );
+
+ block.replaceChildren(block);
+}
+```
-### Slot
+
-Component that provides placeholders to add other components. You can use a drop-in component's built-in slots to add or remove UI components and functions. Or you can add your own additional Slots.
+
+### Add handler for gift message submission
-### Component
+Now that we have the UI in place, we need to add a handler to save the gift message data. We'll use the `fetchGraphl()` function from the API to send a GraphQL mutation to set the gift message on the cart.
-Overloaded term in web development. Everything is a component. It's components all the way down. This is why we need to be specific about what kind of component we are talking about. For example, from top-to-bottom, big-to-small, a **drop-in component** can contain multiple **container components** that can contain multiple **slot components** that can contain multiple **UI components**.
+```js title='commerce-checkout.js'
+giftOptionsField.submitGiftMessageHandler = async (event) => {
+ event.preventDefault();
-
+ const form = event.target;
+ const formData = new FormData(form);
+ const cartId = formData.get('cartId');
+ const fromName = formData.get('fromName');
+ const toName = formData.get('toName');
+ const giftMessage = formData.get('giftMessage');
-## Extend with third-party components
+ giftOptionsField.setAttribute('loading', 'true');
+ console.log('form data', cartId, fromName, toName, giftMessage);
-This tutorial describes the steps needed to extend a drop-in by integrating a fictitious third-party ratings & reviews component in the product details drop-in.
+ const giftMessageInput = {
+ from: fromName,
+ to: toName,
+ message: giftMessage,
+ }
-:::note[Performance]
-Be vigilant about how you display and fetch data for your third-party component to ensure you avoid any Cumulative Layout Shifts (CLS) that could significantly affect the site's core web vital scores.
-:::
+ fetchGraphQl(`
+ mutation SET_GIFT_OPTIONS($cartId: String!, $giftMessage: GiftMessageInput!) {
+ setGiftOptionsOnCart(input: {
+ cart_id: $cartId,
+ gift_message: $giftMessage
+ printed_card_included: false
+ }) {
+ cart {
+ id
+ gift_message {
+ from
+ to
+ message
+ }
+ }
+ }
+ }
+ `,
+ {
+ variables: {
+ cartId,
+ giftMessage: giftMessageInput,
+ },
+ }).then(() => {
+ refreshCart();
+ giftOptionsField.removeAttribute('loading');
+ });
+};
+```
+
+
+
+### Extend the data payload for the drop-in
+
+To extend the data payload of a drop-in, first you need to update the GraphQL fragment used by the cart drop-in to request the additional field. This is done by modifying the `build.mjs` script at the root of your storefront project. In the following example, the `CART_FRAGMENT` fragment is extended to include the gift message data whenever the cart drop-in requests the cart data from GraphQL:
+
+```js title='build.mjs'
+/* eslint-disable import/no-extraneous-dependencies */
+import { overrideGQLOperations } from '@dropins/build-tools/gql-extend.js';
+
+// Extend the cart fragment to include the gift message
+overrideGQLOperations([
+ {
+ // The name of the drop-in to extend
+ npm: '@dropins/storefront-cart',
+ // Additional fields to include in the cart results (gift_message)
+ operations: [
+ `fragment CART_FRAGMENT on Cart {
+ gift_message {
+ from
+ to
+ message
+ }
+ }`
+ ],
+ },
+]);
+```
+
+When you run the install command, the `build.mjs` script generates a new GraphQL query for the cart drop-in that includes the `gift_message` data.
+
+
+
+
+
+### Add new data to the payload
+
+Map the new GraphQL data to the payload data that the cart events provide to listeners so they can access the gift message values.
+
+Configure the cart drop-in's initializer to add the new cart data to the existing cart payload. This is done by defining a transformer function on the CartModel. This function receives the GraphQL data and returns an object that gets merged with the rest of the cart payload. As an example, here is how it might be configured:
+
+```js title='cart.js'
+/* eslint-disable import/no-cycle */
+import { initializers } from '@dropins/tools/initializer.js';
+import { initialize } from '@dropins/storefront-cart/api.js';
+import { initializeDropin } from './index.js';
+
+initializeDropin(async () => {
+ await initializers.mountImmediately(initialize, {
+ models: {
+ CartModel: {
+ transformer: (data) => {
+ const { gift_message: giftMessage } = data;
+ return {
+ giftMessage,
+ }
+ }
+ }
+ }
+ });
+})();
+```
+
+Now when the cart emits an event with cart data, the `giftMessage` data is included.
+
+
+
+
+
+### Retrieve the data and render it
+
+Get the data from the cart event and use it to populate the gift message fields on the checkout page. Here's an example of how you might do this:
+
+```js title='commerce-checkout.js'
+// Event listener to hydrate the new fields with the cart data
+events.on('cart/data', data => {
+ if (!data) return;
+
+ const { id, orderAttributes, giftMessage } = data;
+
+ // Update gift options fields
+ giftOptionsField.setAttribute('cartId', id);
+ if(giftMessage) {
+ giftOptionsField.setAttribute('giftmessage', giftMessage.message);
+ giftOptionsField.setAttribute('fromname', giftMessage.from);
+ giftOptionsField.setAttribute('toname', giftMessage.to);
+ }
+ giftOptionsField.removeAttribute('loading');
+}, { eager: true });
+```
+
+
+
+
+
+### Summary
+
+After just a few changes, we were able to add a new feature to the checkout drop-in that allows users to add a gift message to their order. We added a new UI component, integrated the Commerce API to fetch and update gift messages, and extended the data payload for the drop-in to include the gift message data. You can apply these same concepts to any drop-in.
+
+
+
+
+
+## Extend drop-ins with third-party components
+
+The following steps guide you through adding a third-party component to a drop-in. We'll add a fictitious ratings & reviews component to the product details drop-in as an example.
### Prerequisites
@@ -100,7 +396,6 @@ export default async function decorate(block) {
// Fetch the component data
setRatingsJson(product, thirdPartyApiKey);
}
-
```
@@ -171,9 +466,9 @@ events.on('eds/pdp/ratings', ({ average, total }) => {
### Delay loading large data sets
-Our fictitious ratings & reviews component loads large blocks of text to display a product's reviews. So we need to ensure all that data is not loaded initially with the page. We need to delay loading the reviews until the user scrolls near the reviews section or clicks a "View All Reviews" button. This strategy will improve the page's First Contentful Paint (FCP) and Cumulative Layout Shift (CLS) scores.
+Components like ratings & reviews typically load large blocks of text to display a product's reviews. In such cases, we need to ensure that those reviews are not loaded until the user scrolls near the reviews section or clicks a "View All Reviews" button. This strategy keeps the First Contentful Paint (FCP) and Cumulative Layout Shift (CLS) scores low.
-Here's an example implementation using an Intersection Observer to trigger the delayed load when the user scrolls near the reviews section or clicks "View All Reviews":
+The following example uses an Intersection Observer to load reviews only when a user scrolls near the reviews section or clicks "View All Reviews".
```js
// Trigger the delayed load when the user scrolls near the reviews section or clicks "View All Reviews"
@@ -208,9 +503,9 @@ observer.observe(reviewsSection);
-### Conclusion
+### Summary
-This tutorial showed you the key steps required to integrate a fictitious third-party component into a drop-in. You've learned how to configure API keys, fetch data, and delay loading large data sets to improve page performance. You can now apply these concepts to other third-party components you want to integrate into your drop-ins.
+Throughout this tutorial, we examined the key steps of integrating a fictitious third-party component. We learned how to configure API keys, fetch data, and delay loading data sets to improve page performance. You can apply these same concepts to any drop-in.
diff --git a/src/content/docs/dropins/all/slots.mdx b/src/content/docs/dropins/all/slots.mdx
new file mode 100644
index 00000000..6575f9a9
--- /dev/null
+++ b/src/content/docs/dropins/all/slots.mdx
@@ -0,0 +1,52 @@
+---
+title: Understanding slots
+description: Learn about slots and how to use them to customize dropin-in components.
+---
+
+import { Tabs, TabItem } from '@astrojs/starlight/components';
+import Diagram from '@components/Diagram.astro';
+import Vocabulary from '@components/Vocabulary.astro';
+import Aside from '@components/Aside.astro';
+import Callouts from '@components/Callouts.astro';
+import { Steps } from '@astrojs/starlight/components';
+import Tasks from '@components/Tasks.astro';
+import Task from '@components/Task.astro';
+
+Using slots provides the deepest level of customization. Slots are built-in extension points in the drop-in. A slot provides a place in the drop-in component to add your own UI components and functions. This architecture makes it easy to change the default look, layout, and behavior. Let's learn how it works.
+
+## Big Picture
+
+![What is a slot?](@images/slots/what-is-a-slot.svg)
+
+The following functions are available to all slots:
+
+
+
+1. `prependSibling`: A function to prepend a new HTML element before the slot's content.
+1. `prependChild`: A function to prepend a new HTML element to the slot's content.
+1. `replaceWith`: A function to replace the slot's content with a new HTML element.
+1. `appendChild`: A function to append a new HTML element to the slot's content.
+1. `appendSibling`: A function to append a new HTML element after the slot's content.
+1. `getSlotElement`: A function to get a slot element.
+1. `onChange`: A function to listen to changes in the slot's context.
+1. `dictionary`: JSON Object for the current locale. If the locale changes, the `dictionary` values change to reflect the values for the selected language.
+
+
+
+## Vocabulary
+
+
+
+### Container
+
+Component that manages or encapsulates other components. Containers handle logic, fetch data, manage state, and pass data to the UI components that are rendered on the screen.
+
+### Slot
+
+Component that provides placeholders to add other components. You can use a drop-in component's built-in slots to add or remove UI components and functions. Or you can add your own additional slots.
+
+### Component
+
+In web development, this term is overused. That's why we need to be specific about the kind of component we are talking about. Here's a few examples of components from top-to-bottom: a **drop-in component** can contain multiple **container components** that can contain multiple **slot components** that can contain multiple **UI components**.
+
+