Gabriel Lima
Posted on September 10, 2024
- Summary
- Introduction
- Prerequisites
- Before You Begin
- About the Structure
- Hands-on Code (Step-by-Step)
- Repository
- References
Introduction
In this tutorial, you will learn how to create and integrate a custom content type countdown into PWA Studio. By the end of this guide, you will have a fully functional countdown that is editable from Magento's admin panel and rendered in your PWA storefront.
Prerequisites
Before you begin, ensure you have the following:
- A working installation of Magento and PWA Studio.
- Basic knowledge of React, JavaScript, and Magento PageBuilder.
- Node.js and npm installed in your environment.
- PageBuilder Countdown Module
Before You Begin
Before you start coding this content type, ensure the Content Type is installed in your Magento and PWA Studio setup using the Scaffolding Tool. This tool will help set up the basic storefront environment.
The content type that we will integrate with PWA Studio is a countdown timer, which you can get from the repository of this tutorial.
About the Structure
Here is the final structure of the content type:
pwa-studio
├── src
│ ├── components
│ │ └── Countdown
│ │ ├── ContentType
│ │ │ └── Countdown
│ │ │ ├── configAggregator.js
│ │ │ ├── countdown.js
│ │ │ ├── countdown.module.css
│ │ │ └── index.js
│ │ ├── countdown.js
│ │ ├── detect.js
│ │ └── index.js
│ └── talons
│ └── Countdown
│ └── useCountdown.js
├── local-intercept.js
│
Hands-on Code (Step-by-Step)
Now let's start creating the custom content type.
Step 1: Modify local-intercept.js
Open the local-intercept.js
file in your PWA Studio root and update its content as follows:
function localIntercept(targets) {
targets.of('@magento/pagebuilder').customContentTypes.tap(
contentTypes => contentTypes.add({
contentType: 'Countdown',
importPath: require.resolve("./src/components/Countdown/index.js")
})
);
}
moduleexports = localIntercept;
This code instructs the @magento/pagebuilder
to add the custom Countdown content type.
Step 2: Create configAggregator.js
Create the file src/components/Countdown/ContentType/Countdown/configAggregator.js
with the following content:
import { getAdvanced } from "@magento/pagebuilder/lib/utils";
export default (node, props) => {
return {
targetDate: node.childNodes[0].childNodes[0].attributes[1].value,
...getAdvanced(node),
};
};
1. targetDate
Extraction:
- We access the date through:
node.childNodes[0].childNodes[0].attributes[1].value
. This navigates through thenode
to reach the attribute that holds thetargetDate
.
2. getAdvanced(node)
Utility Function:
-
getAdvanced(node)
is a utility function that retrieves additional styling properties from thenode
like padding, margins, border, text alignment, and CSS classes. - These settings allow the countdown timer to inherit any advanced layout configurations made by the user in the Magento Page Builder's editor.
Step 3: Create Countdown Component
Now we will create the countdown component.
countdown.js
Create the file src/components/Countdown/ContentType/Countdown/countdown.js
and add the following code:
import React from 'react';
import { mergeClasses } from '@magento/venia-ui/lib/classify';
import { arrayOf, bool, shape, string } from 'prop-types';
import defaultClasses from './countdown.module.css';
import { FormattedMessage } from 'react-intl';
import { useCountdown } from '../../../../talons/Countdown/useCountdown';
const Countdown = props => {
const {
targetDate,
textAlign,
border,
borderColor,
borderWidth,
borderRadius,
isHidden,
marginTop,
marginRight,
marginBottom,
marginLeft,
paddingTop,
paddingRight,
paddingBottom,
paddingLeft,
cssClasses
} = props;
const classes = mergeClasses(defaultClasses, props.classes);
const {
days,
hours,
minutes,
seconds
} = useCountdown({ targetDate });
const formStyles = {
textAlign,
border,
borderColor,
borderWidth,
borderRadius,
isHidden,
marginTop,
marginRight,
marginBottom,
marginLeft,
paddingTop,
paddingRight,
paddingBottom,
paddingLeft,
};
return (
<div style={formStyles}>
<div className={classes.wrapper}>
<div className={classes.field}>
<div className="days">{days}</div>
<div className="separator">:</div>
<div className="hours">{hours}</div>
<div className="separator">:</div>
<div className="min">{minutes}</div>
<div className="separator">:</div>
<div className="sec">{seconds}</div>
<div className={classes.label}>
<FormattedMessage
id="countdown.days"
defaultMessage="days"
/>
</div>
<div></div>
<div className={classes.label}>
<FormattedMessage
id="countdown.hours"
defaultMessage="hours"
/>
</div>
<div></div>
<div className={classes.label}>
<FormattedMessage
id="countdown.minutes"
defaultMessage="min"
/>
</div>
<div></div>
<div className={classes.label}>
<FormattedMessage
id="countdown.seconds"
defaultMessage="sec" />
</div>
</div>
</div>
</div>
);
};
Countdown.propTypes = {
textAlign: string,
border: string,
borderColor: string,
borderWidth: string,
borderRadius: string,
isHidden: bool,
marginTop: string,
marginRight: string,
marginBottom: string,
marginLeft: string,
paddingTop: string,
paddingRight: string,
paddingBottom: string,
paddingLeft: string,
cssClasses: arrayOf(string),
classes: shape({ wrapper: string, field: string, label: string })
};
Countdown.defaultProps = {};
export default Countdown;
Let's break down some key parts of the Countdown
component code:
-
Props Destructuring:
In the component's
props
, we are receiving several variables, most notably thetargetDate
from the configAggregator.js file. This scaffolding
represents the end time for the countdown. Along withtargetDate
, we also receive various styling-related variables such astextAlign
,border
, andpadding
, which are dynamically passed from Page Builder. Lastly, thecssClasses
variable comes directly from Page Builder and contains any additional CSS classes that need to be applied to the component. -
Calling the Talon:
After destructuring the props, we initialize the
useCountdown
talon. This custom hook (useCountdown
) is passed thetargetDate
as a prop and, in return, provides the values needed to display the countdown—days
,hours
,minutes
, andseconds
. These values are automatically updated as time progresses, which ensures that the countdown remains accurate in real-time. -
Combining Style Properties:
The
formStyles
variable is an object that combines all the individual style-related variables (such asborder
,textAlign
,margin
, andpadding
) from the props. This aggregation makes it easier to apply these styles in one go, directly onto the container<div>
, by using thestyle
attribute:
<div style={formStyles}>
By passing the formStyles
object here, we ensure that the countdown component reflects any custom styling options configured via the Page Builder, such as margins, padding, and borders.
countdown.module.css
Next, create the CSS file src/components/Countdown/ContentType/Countdown/countdown.module.css
:
.wrapper {
composes: flex from global;
}
.field {
composes: grid from global;
composes: items-center from global;
composes: justify-items-center from global;
composes: gap-1.5 from global;
composes: font-light from global;
grid-template-columns: 1fr auto 1fr auto 1fr auto 1fr;
}
field div {
font-size: 1.5rem;
letter-spacing: normal;
}
.label {
composes: text-base from global;
margin-top: -5px;
letter-spacing: normal;
}
index.js
Finally, create the src/components/Countdown/ContentType/Countdown/index.js
file:
import React from 'react';
import configAggregator from './configAggregator';
export default {
configAggregator,
component: React.lazy(() => import('./countdown'))
}
-
configAggregator
Import: TheconfigAggregator
is imported from theconfigAggregator.js
file. This function is responsible for collecting the necessary data (such astargetDate
, margins, borders, etc.) from the Page Builder content type and passing it as props to the component. It ensures that all configurations from the admin panel are correctly applied to the component. -
Dynamic Component loading:
The
React.lazy()
function is used to lazily load thecountdown
component. -
Exporting the Configuration Object:
The file exports a default object containing two key properties:
-
configAggregator
: This defines how the content type's configuration data is retrieved and passed into the component. -
component
: This points to thecountdown
component, which is lazy loaded viaReact.lazy()
.
-
This file essentially sets up the necessary configurations and ensures that the countdown component is properly integrated with the Page Builder in a performance-efficient manner.
1. src/components/Countdown/countdown.js
import React from 'react';
import { setContentTypeConfig } from '@magento/pagebuilder/lib/config';
import ContentTypeConfig from './ContentType/Countdown';
setContentTypeConfig('example_countdown', ContentTypeConfig);
const Countdown = (props) => {
return null;
};
export default Countdown;
-
setContentTypeConfig
: This function registers the custom content type (example_countdown
) with Page Builder. It accepts two arguments:- The first argument is the content type name (
example_countdown
), which must match the name defined in the XML file located atapp/code/Example/PageBuilderCountdown/view/adminhtml/pagebuilder/content_type/example_countdown.xml
. - The second argument,
ContentTypeConfig
, is imported from theCountdown
folder and includes the component configuration required by Page Builder.
- The first argument is the content type name (
- The component itself (
Countdown
) does not render any UI, as it only serves to register the content type. Therefore, it returnsnull
.
2. src/components/Countdown/detect.js
This file defines a regular expression (RegEx) function to detect if the custom content type is present in the content.
export default function detectMyCustomType(content) {
return /data-content-type=\"example_countdown/.test(content);
}
-
detectMyCustomType
: This function checks if the Page Builder content contains the custom content type (example_countdown
) by searching for thedata-content-type="example_countdown"
attribute in the rendered HTML. If found, it returnstrue
, indicating that the custom content type is present. - The same content type name (
example_countdown
) is used here, matching the one defined in the XML file and thesetContentTypeConfig
function.
3. src/components/Countdown/index.js
In this file, we export the components and functionality created above.
export { default } from './countdown';
export { default as Component } from './countdown';
export { default as canRender } from './detect';
-
Default Export (C
ountdown
): This file exports thecountdown
component as the default export. -
Named Export (
Component
): Thecountdown
component is also exported as a component
, making it easier to refer to the component when necessary. -
Named Export (
canRender
): Thedetect
function is exported ascanRender
, which will be used by Page Builder to determine whether the custom content type should be rendered based on the presence of thedata-content-type="example_countdown"
attribute.
These files collectively ensure that the custom content type (example_countdown
) is properly registered, detected, and rendered within the Page Builder environment.
Step 4: Set up the Talon
In this section, we will create a talon to handle the logic behind the countdown timer. If you're unfamiliar with talons, they are hooks used in the Magento PWA Studio to separate logic from components, making code more modular and reusable. For more details, check out Talons (adobe.com).
Now create the talon file src/talons/Countdown/useCountdown.js
:
import { useEffect, useState, useCallback } from 'react';
export const useCountdown = ({ targetDate }) => {
const validateDate = (date) => {
const target = new Date(date);
return isNaN(target.getTime()) ? null : target;
};
const target = validateDate(targetDate);
if (!target) {
console.error('Invalid targetDate provided');
return { days: '00', hours: '00', minutes: '00', seconds: '00' };
}
const calculateTimeLeft = useCallback(() => {
const difference = target - new Date();
if (difference <= 0) return { days: '00', hours: '00', minutes: '00', seconds: '00' };
const time = (unit, mod) => String(Math.floor((difference / unit) % mod)).padStart(2, '0');
return {
days: time(1000 * 60 * 60 * 24, Infinity),
hours: time(1000 * 60 * 60, 24),
minutes: time(1000 * 60, 60),
seconds: time(1000, 60),
};
}, [target]);
const [timeLeft, setTimeLeft] = useState(calculateTimeLeft);
useEffect(() => {
if (Object.values(timeLeft).every((v) => v === '00')) return;
const timer = setInterval(() => setTimeLeft(calculateTimeLeft), 1000);
return () => clearInterval(timer);
}, [timeLeft, calculateTimeLeft]);
return timeLeft;
};
-
validateDate
: This function ensures the providedtargetDate
is valid. If the date is invalid, it returnsnull
, and the countdown will display "00" for all time units. -
calculateTimeLeft
: This function calculates the difference between the current time and thetargetDate
. It returns the remaining time formatted as days, hours, minutes, and seconds. -
useState
: We use React'suseState
to store the current countdown time, updating it every second. -
useEffect
: This hook sets up a timer that updates the countdown every second. If the countdown reaches "00", it stops.
Repository
You can find the complete repository, which includes both the final PWA Studio code and the Magento module used in this tutorial, at the following link:
References
Posted on September 10, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.