Edit in GitHubLog an issue

Create a tag list extension

A tag list is a group of tags associated with a product. This tutorial provides steps for creating and installing an extension that displays a tag list on the product details page in a Venia-based storefront.

Prerequisites#

Before you start this tutorial, make sure you have a storefront project set up using the steps outlined in the Setup your project topic. This is the storefront you will use to install your extension in this tutorial.

Set up the extension project#

Create a folder anywhere in your system and run Yarn's project creation script. The script starts an interactive questionnaire that helps you fill out your project's package.json file.

Copied to your clipboard
1mkdir tagList && \
2cd tagList && \
3yarn init

After answering the questions, the yarn CLI creates a package.json file that looks like the following:

Copied to your clipboard
1{
2 "name": "tagList",
3 "version": "1.0.0",
4 "description": "A tag list extension for a PWA Studio storefront",
5 "main": "index.js",
6 "license": "MIT"
7}

Create TagList component files#

The core purpose of this extension is to provide a React component that renders a list of tags. Use the following command to create a directory and the files that define this component.

Copied to your clipboard
1mkdir -p src/TagList && \
2touch src/Taglist/index.js && \
3touch src/Taglist/tag.js && \
4touch src/Taglist/tag.css && \
5touch src/Taglist/tagList.js && \
6touch src/Taglist/tagList.css

index.js#

Exporting modules from your component's index.js file is a common standard in a React project. It lets another component import the tag list component using the TagList directory name.

Copied to your clipboard
1/* src/TagList/index.js */
2
3export { default } from "./tagList";

tag.js and tag.css#

The tag.js and tag.css files define a Tag component that renders a single tag in the tag list.

Copied to your clipboard
1/* src/TagList/tag.js */
2
3import React from "react";
4import Button from "@magento/venia-ui/lib/components/Button";
5import { Link } from "@magento/venia-ui/lib/drivers";
6
7import classes from "./tag.css";
8
9const categoryUrlSuffix = ".html";
10
11// This is a component responsible for rendering a single tag
12const Tag = (props) => {
13 // Destructures the props object into variables
14 const { value } = props;
15
16 const { name, url_path } = value;
17 const url = `/${url_path}${categoryUrlSuffix}`;
18
19 const buttonClasses = {
20 root_lowPriority: classes.root,
21 content: classes.content,
22 };
23
24 return (
25 <Link className={classes.link} to={url}>
26 <Button classes={buttonClasses} priority="low" type="button">
27 {name}
28 </Button>
29 </Link>
30 );
31};
32
33// Make this function the default exported of this module
34export default Tag;
Copied to your clipboard
1/* src/TagList.tag.css */
2
3.root {
4 border: solid 1px #2680eb;
5 padding: 3px 9px;
6 margin: 5px;
7 border-radius: 6px;
8}
9
10.content {
11 color: #2680eb;
12 font-size: 0.875rem;
13}

tagList.js and tagList.css#

The tagList.js and tagList.css files define the main TagList component this package provides. It accepts a categoriesListData object as a prop and renders the data as a tag list.

Copied to your clipboard
1/* src/TagList/tagList.js */
2
3import React from "react";
4import Tag from "./tag";
5
6import classes from "./tagList.css";
7
8const TagList = (props) => {
9 // Destructures the props object into variables
10 const { categoriesListData } = props;
11
12 const { categories } = categoriesListData;
13
14 // Returns nothing if there are no categories
15 if (!categories) {
16 return null;
17 }
18
19 // Converts the array of tag strings into an array of Tag components
20 const tagList = categories.map((category) => {
21 return <Tag key={category.name} value={category} />;
22 });
23
24 // Returns the list of Tag components inside a div container
25 return <div className={classes.root}>{tagList}</div>;
26};
27
28export default TagList;
Copied to your clipboard
1/* src/TagList/tagList.css */
2
3.root {
4 display: flex;
5 flex-wrap: wrap;
6}

Add TagList component dependencies#

The TagList component requires third party libraries, such as React, to render the correct HTML. Since this package is an extension, you should list these as peer dependencies in your package.json file. This safeguards against including more than one copy of the same dependency in the storefront project and final build.

Use the following command to add the TagList component dependencies as peer dependencies:

Copied to your clipboard
yarn add --peer react @magento/venia-ui

This command creates a new peerDependencies entry in your package.json file that lists these dependencies.

Copied to your clipboard
1 {
2 "name": "tagList",
3 "version": "1.0.0",
4 "description": "A tag list extension for a PWA Studio storefront",
5 "main": "index.js",
6- "license": "MIT"
7+ "license": "MIT",
8+ "peerDependencies": {
9+ "@magento/venia-ui": "^6.0.1",
10+ "react": "^17.0.1"
11+ }
12 }

Export TagList in extension#

Your extension needs to export the TagList component in your project's main entry point to let other developers import it in their code. The default main entry point for Node packages is the index.js file, so create this file using the following command:

Copied to your clipboard
touch index.js

Edit the file and add the following content:

Copied to your clipboard
export { default as TagList } from "./src/TagList";

Now, other developers can import the TagList component in their own projects using the following syntax:

Copied to your clipboard
import { TagList } from "tagList";

Create data fetch hook#

The TagList component requires data the ProductDetailsFragment does not provide, so you need to create a data fetch hook. The data fetch hook is a custom React hook that sends a GraphQL query requesting the product categories for a product.

Run the following command to create this file:

Copied to your clipboard
1mkdir -p src/hooks && \
2touch src/hooks/useProductCategoriesList.js

Edit the file and add the following content:

Copied to your clipboard
1/* src/hooks/useProductCategoriesList.js */
2
3import { useMemo } from "react";
4import { useQuery } from "@apollo/client";
5import gql from "graphql-tag";
6
7// GraphQL query to fetch a list of categories for a product
8const GET_PRODUCT_CATEGORIES = gql`
9 query getProductCategories($urlKey: String!) {
10 products(filter: { url_key: { eq: $urlKey } }) {
11 items {
12 categories {
13 name
14 url_path
15 }
16 }
17 }
18 }
19`;
20
21const useProductCategoriesList = (props) => {
22 const { urlKey } = props;
23
24 const { error, loading, data } = useQuery(GET_PRODUCT_CATEGORIES, {
25 fetchPolicy: "cache-and-network",
26 nextFetchPolicy: "cache-first",
27 variables: {
28 urlKey: urlKey,
29 },
30 });
31
32 const categories = useMemo(() => {
33 if (data && data.products.items[0]) {
34 return data.products.items[0].categories;
35 }
36 return null;
37 }, [data]);
38
39 return {
40 error,
41 isLoading: loading,
42 categories,
43 };
44};
45
46export default useProductCategoriesList;

This file requires two new dependencies, @apollo/client and graphql-tag. Run the following command to add these as peer dependencies to the package.json file:

Copied to your clipboard
yarn add --peer @apollo/client graphql-tag

Create wrapper file#

A wrapper file exports an interceptor that wraps a module in another file. An interceptor module has access to the original module and the parameters it receives. This lets the interceptor module run the original code along with its own custom logic that can change the incoming parameters or outgoing return value.

For this extension, create a wrapper file for the useProductFullDetails() hook that calls the data fetch hook you created in a previous step and adds the result to its return value.

First, create this file with the following command:

Copied to your clipboard
1mkdir -p src/targets/ && \
2touch src/targets/wrapper.js

Edit this file and add the following content:

Copied to your clipboard
1import useProductCategoriesList from "../hooks/useProductCategoriesList";
2
3export default (original) => {
4 return function useProductFullDetails(props, ...restArgs) {
5 const { product } = props;
6
7 // Run the data fetch hook
8 const categoriesListData = useProductCategoriesList({
9 urlKey: product.url_key,
10 });
11
12 // Run the original, wrapped function
13 const { productDetails, ...defaultReturnData } = original(
14 props,
15 ...restArgs
16 );
17
18 // Add the new data to the data returned by the original function
19 return {
20 ...defaultReturnData,
21 productDetails: {
22 ...productDetails,
23 categoriesListData: categoriesListData,
24 },
25 };
26 };
27};

Define intercept file#

The intercept file is where you directly interact with Target objects to apply customizations. In this extension, the intercept file tells the build process to use Webpack loaders for ES Modules and CSS Modules. Without these intructions, the build process will not know how to load the files in this extension.

Create this file using the following command:

Copied to your clipboard
touch src/targets/intercept.js

Edit this file and add the following content:

Copied to your clipboard
1module.exports = (targets) => {
2 const { Targetables } = require("@magento/pwa-buildpack");
3
4 const targetables = Targetables.using(targets);
5
6 targetables.setSpecialFeatures("esModules", "cssModules");
7};

This script uses @magento/pwa-buildpack, so you need to add this library as a dependency.

Copied to your clipboard
yarn add --peer @magento/pwa-buildpack

Next, edit your package.json file to point to the location of this extension's intercept file:

Copied to your clipboard
1 {
2 "name": "tagList",
3 "version": "1.0.0",
4 "description": "A tag list extension for a PWA Studio storefront",
5 "main": "index.js",
6 "license": "MIT",
7 "peerDependencies": {
8 "@apollo/client": "^3.3.11",
9 "@magento/pwa-buildpack": "^8.0.1",
10 "@magento/venia-ui": "^6.0.1",
11 "graphql-tag": "^2.11.0",
12 "react": "^17.0.1"
13+ },
14+ "pwa-studio": {
15+ "targets": {
16+ "intercept": "src/targets/intercept"
17+ }
18 }
19 }
20

Install extension locally#

Navigate to your storefront project directory and use the yarn add file:/path/to/local/folder syntax for the yarn add CLI to install your extension locally.

For example:

Copied to your clipboard
yarn add --dev file:../extensions/tagList

This adds a new entry under devDependencies in your storefront project's package.json file that looks like the following:

Copied to your clipboard
1 "style-loader": "~0.23.1",
2+ "tagList": "file:../extensions/tagList",
3 "terser-webpack-plugin": "~1.2.3",

Intercept Venia UI components#

All scaffolded projects come with an intercept file called local-intercept.js. This file lets you use Targets and Targetables to make modifications to the Venia application code without copying over the source file.

Edit this file so it looks like the following:

Copied to your clipboard
1/* local-intercept.js */
2
3// Import the Targetables manager
4const { Targetables } = require("@magento/pwa-buildpack");
5
6function localIntercept(targets) {
7 // Create a bound Targetable factory
8 const targetables = Targetables.using(targets);
9
10 // Create a React component targetable linked to the productFullDetail.js file
11 const ProductDetails = targetables.reactComponent(
12 "@magento/venia-ui/lib/components/ProductFullDetail/productFullDetail.js"
13 );
14
15 // Add an import statement to the productFullDetail.js file and
16 // return the SingleImportStatement object
17 const TagList = ProductDetails.addImport("{TagList} from 'tagList'");
18
19 // Insert the TagList component after the product description and pass in the
20 // new categoriesListData object added to the useProductFullDetails() hook
21 ProductDetails.insertAfterJSX(
22 "<RichText content={productDetails.description} />",
23 `<${TagList} categoriesListData={productDetails.categoriesListData} />`
24 );
25
26 // Create an ES Module targetable linked to the useProductFullDetail.js file
27 const useProductFullDetails = targetables.esModule(
28 "@magento/peregrine/lib/talons/ProductFullDetail/useProductFullDetail.js"
29 );
30
31 // Wrap the useProductFullDetail hook with your extension's wrapper file
32 useProductFullDetails.wrapWithFile(
33 "useProductFullDetail",
34 "tagList/src/targets/wrapper"
35 );
36}
37
38module.exports = localIntercept;

Congratulations#

You created a tag list extension and installed it in your development server!

Now, when you start your storefront application and navigate to a product page, you will see a list of tags associated with that product.

Live example#

You can see this extension running live in this CodeSandbox instance or you can check out the source repository in the taglist-extension-tutorial branch in the magento-devdocs/pwa-studio-code-sandbox GitHub project.

  • Privacy
  • Terms of Use
  • Do not sell my personal information
  • AdChoices
Copyright © 2022 Adobe. All rights reserved.