Lab - Build a Team Members Component

Plan: Composable Developer

Lesson 15 of 21 · 45 min

This lab exercise will be more involved, as you build your own component from scratch.

In this lab, you will:

  • Build a Makeswift component for displaying a “Team Members” gallery
  • Make use of new Makeswift control types including List, Group, and Slot

Prerequisites

  • The starter codebase installed and connected with your Makeswift site, as done in the previous setup lab

Setup

This exercise continues where you left off previously in your Makeswift project. Simply start the dev server from the project root if it’s not currently running:

pnpm run dev

Remember!

These exercises will proceed from the project you previously set up from the lab repository, which already contains boilerplate code these steps rely on. Revisit the initial setup lab in this course if necessary to get your project set up.

If you need a fresh start, you can follow the instructions below to set up a new project complete with previous exercise code.

  1. In your terminal, run the following, replacing the path to the working directory with your own path:
pnpm dlx create-next-app@latest -e https://github.com/bigcommerce-edu/lab-catalyst-makeswift-faqs/tree/e-team-start /path/to/working/directory
  1. Copy .env as .env.local and modify .env.local with configuration details for your store.
  2. Start the dev server process:
cd /path/to/working/directory
pnpm run dev

Exercise Reference Copy

If it’s helpful to compare your own code as you go, you can clone or download the completed version of this lab as a reference.

Team Members Component Lab Snapshot

The Team Members Component

The Team Members components presents a gallery style view of team member profiles, featuring fade in/out transitions.

The fully realized Team Members component

There are different possible strategies for content authoring in a component like this, depending on the design constraints. You’ll be implementing a very “template-ized” strategy for the gallery thumbnails but a fairly free-form option for each profile’s main content.

Step 1: Register the Component

Once again, you’ll start by simply registering the existing component with its placeholder content.

  1. Open the file components/custom/team-members/register.tsx and add the registerComponent call.
runtime.registerComponent(
TeamMembers,
{
type: 'team-members',
label: 'Team Members',
props: {
},
}
);
  1. Open the file lib/makeswift/components.ts and add the import statement for the component.
import '~/custom/components/team-members/register';
  1. Browse to the Makeswift editor with the appropriate site selected and verify that your “Team Members” component is available to drag into the editor viewport.

Team Members placeholder

Example Code

Step 2: Add List Control

In this step, you’ll add a List prop control to allow the management of each individual team member.

  1. Open the file components/custom/team-members/register.tsx and add the following prop definition:
runtime.registerComponent(
TeamMembers,
{
// ...
props: {
members: List({
label: 'Members',
type: Group({
label: 'Member Details',
props: {
name: TextInput({
label: 'Name',
}),
position: TextInput({
label: 'Position',
}),
image: Image({
label: 'Image',
format: Image.Format.URL,
}),
},
}),
getItemLabel(member) {
return member?.name || 'Team Member'
},
}),
},
}
);

This List prop defines a collection of items that are each, in turn, a Group with several simple properties.

  1. Open components/custom/team-members/team-members.tsx and add the right prop type definitions and some initial placeholder JSX.
// ...
interface Member {
name?: string;
position?: string;
image?: string;
}
interface Props {
members: Member[];
}
export const TeamMembers = forwardRef((
{
members,
}: Props,
ref: Ref<HTMLDivElement>
) => {
return (
<div
...
>
{(members.length > 0) ?
<div className="w-full"><h3 className="text-lg text-center">Number of Team Members: {members.length}</h3></div>
:
<div className="w-full"><h3 className="text-lg text-center">Add a Team Member</h3></div>
}
</div>
);
});
// ...
  1. Verify in the editor that Members can be added and that the count in the placeholder text changes accordingly.

Team Members list control

Example Code

Step 3: Implement the Main Presentation

Let’s flesh out the details of how the team members are presented, including the basic wiring for the animated transitions.

  1. Open the file components/custom/team-members/team-members.tsx and add some basic state variables.
// ...
export const TeamMembers = forwardRef((
// ...
) => {
const vertical = true;
const fadeInDuration = 500;
const [activeMember, setActiveMember] = useState(0);
const [visibleMembers, setVisibleMembers] = useState([0]);
const changeActiveMember = (index:number) => {
};
return (
// ...
);
});
// ...

The value of vertical is static for now, but you’ll make it dynamic based on a prop later. This value controls whether the gallery thumbnails are presented in a vertical or horizontal layout, and starting out with the value defined allows you to include the appropriate logic for various style classes now instead of adding them in later.

  1. Edit the component’s JSX with a basic starting presentation.
// ...
export const TeamMembers = forwardRef((
// ...
) => {
// ...
return (
<div
className={clsx(
"w-full",
vertical && "flex gap-4"
)}
ref={ref}
>
{(members.length > 0) ?
<>
<div
className={clsx(
vertical && "flex-none",
vertical || "p-4"
)}
>
<ul
className={clsx(
vertical || "grid gap-x-4 gap-y-8 md:px-16 justify-items-center"
)}
>
{members.map((member, index) => (
<li
className={clsx(
`max-w-24 sm:max-w-48 text-center border border-2 p-2
rounded-md cursor-pointer transition-colors duration-300`,
"text-black",
index === activeMember ? "border-black" : "border-transparent",
)}
key={index} onClick={() => changeActiveMember(index)}
>
<img alt={member.name} className="rounded-full mx-auto max-w-[60%]" src={member.image} />
<h3 className="text-sm font-bold">
{member.name}
</h3>
<p className="text-sm">
{member.position}
</p>
</li>
))}
</ul>
</div>
<div className={clsx(
"relative overflow-hidden",
vertical && "flex-auto min-h-[560px]",
vertical || "relative h-[560px]"
)}>
{members.map((member, index) => {
if (!visibleMembers.includes(index)) return null;
return (
<div
className={clsx(
"absolute transition-opacity duration-[var(--fadeDuration)] w-full",
(index !== activeMember) ? "opacity-0" : "opacity-100"
)}
key={index}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
style={{
"--fadeDuration": `${fadeInDuration}ms`,
} as React.CSSProperties}
>
<h2 className="text-xl">{member.name}</h2>
</div>
)
})}
</div>
</>
:
{/* ... */}
}
</div>
);
});
// ...

This loops through the members list twice to give us each item’s thumbnail and main content (which is simply a placeholder for now). Most of the required style classes are already in place, and you can see that some of them rely on logic related to a couple of pieces of state: The “active” member is the currently selected item, and there is also a broader list of “visible” members. We’ll see why these are needed next.

  1. Edit the changeActiveMember function to flesh out the logic.
// ...
export const TeamMembers = forwardRef((
// ...
) => {
// ...
const changeActiveMember = (index:number) => {
const prevActiveIndex = activeMember;
const newVisibleMembers = [...visibleMembers, index];
setVisibleMembers(newVisibleMembers);
setTimeout(() => {
setActiveMember(index);
setTimeout(() => {
setVisibleMembers(newVisibleMembers.filter(thisIndex => thisIndex !== prevActiveIndex));
}, fadeInDuration * 1.5);
}, 10);
}
return (
// ...
);
});
// ...

The fade in/out transition used when a new thumbnail is clicked requires that both the previous active item and the new one briefly be visible at the same time. While you could simply render all items, leaving all but the active one with an opacity of 0, this would create an issue in the Makeswift editor. Visible or not, the overlapped elements present in the DOM would interfere with the ability to interact with the slots you’ll eventually be implementing.

To deal with this, changeActiveMember uses a timeout-based approach to allow both items to be rendered for a sufficient length of time to accommodate the fade transition. Once this process is complete, only the active item is rendered.

  1. In the Makeswift editor, add a few Team Members with full details and verify the working presentation by using the Interact mode.

Team Members basic functionality

Example Code

Step 4: Add Formatting Props

Let’s spruce up the functionality of the component by adding controls for colors and the thumbnail layout.

  1. Open components/custom/team-members/register.tsx and add new prop definitions.
runtime.registerComponent(
TeamMembers,
{
// ...
props: {
// ...
highlightColor: Color({
label: "Highlight Color",
}),
thumbnailTextColor: Color({
label: "Thumbnail Text Color",
}),
thumbnailOrientation: Select({
label: "Thumbnail Orientation",
labelOrientation: "horizontal",
options: [
{ value: "vertical", label: "Vertical" },
{ value: "horizontal", label: "Horizontal" },
],
defaultValue: "vertical",
}),
itemsPerRow: Number({
label: "Horizontal Items Per Row",
defaultValue: 3,
min: 1,
max: 12,
}),
},
}
);
  1. Open components/custom/team-members/team-members.tsx and modify the component code to receive the new props and adjust the value of vertical to depend on your thumbnailOrientation prop.
// ...
interface Props {
members: Member[];
highlightColor?: string;
thumbnailTextColor?: string;
thumbnailOrientation?: "vertical" | "horizontal";
itemsPerRow?: number;
}
export const TeamMembers = forwardRef((
{
members,
highlightColor,
thumbnailTextColor,
thumbnailOrientation,
itemsPerRow = 3,
}: Props,
ref: Ref<HTMLDivElement>
) => {
const vertical = (thumbnailOrientation === "vertical");
// ...
return (
// ...
);
});
// ...
  1. Modify the JSX for the <ul> and <li> elements to incorporate the various properties into the logic for the style classes used.
// ...
export const TeamMembers = forwardRef((
// ...
) => {
// ...
return (
<div
{/* ... */}
>
{/* ... */}
<ul
className={clsx(
vertical || "grid gap-x-4 gap-y-8 md:px-16 justify-items-center",
vertical || "grid-cols-[var(--itemsPerRow)]"
)}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
style={{
"--itemsPerRow": `repeat(${itemsPerRow}, minmax(0, 1fr))`,
} as React.CSSProperties}
>
{members.map((member, index) => (
<li
className={clsx(
`max-w-24 sm:max-w-48 text-center border border-2 p-2
rounded-md cursor-pointer transition-colors duration-300`,
"text-[var(--textColor)]",
index === activeMember ? "border-[var(--highlightColor)]" : "border-transparent"
)}
key={index} onClick={() => changeActiveMember(index)}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
style={{
"--highlightColor": highlightColor,
"--textColor": thumbnailTextColor,
} as React.CSSProperties}
>
{/* ... */}
</li>
))}
</ul>
{/* ... */}
</div>
);
});
// ...

Note the technique used here to incorporate dynamic values into the Tailwind classes applied to the content. First, a style prop is included using the dynamic values to set CSS variables. These variables are then utilized with Tailwind’s syntax for arbitrary values. (For example, text-[var(—textColor)] )

  1. Verify the functionality of your new props in the editor.

Team Member style props

Example Code

Step 5: Add a Slot Control

Let’s turn our attention to the main content for each team member profile. There are a couple of approaches that could be used here.

If, practically speaking, the visual formatting of each profile will be identical, then it might be cumbersome for content authors to visually build each one using Makeswift’s standard components. In this case, you might choose to expand the properties available for each “Team Members” item and output specific HTML structure using those values, as you’ve done for the thumbnails.

However, to give content authors maximum freedom over the presentation of each profile, the appropriate approach would be to allow each profile’s content to be fully free-form, with full support for visual editing. This is the approach you’ll take in the following steps, and all that’s required is to utilize a Slot control for each list item.

By the same token, you might have chosen just such a free-form strategy for the gallery thumbnails, outputting a thumbnail Slot for each rather than exposing specific fields like Name and Position at all.

  1. Open components/custom/team-members/register.tsx and add a simple Slot prop for each Group in the members list.
runtime.registerComponent(
TeamMembers,
{
// ...
props: {
members: List({
label: 'Members',
type: Group({
label: 'Members',
props: {
// ...
content: Slot(),
},
}),
// ...
}),
// ...
},
}
);
  1. Open components/custom/team-members/team-members.tsx and add the new prop in place of the placeholder text that previously populated each item.
// ...
interface Member {
// ...
content: ReactNode;
}
// ...
export const TeamMembers = forwardRef((
// ...
) => {
// ...
return (
<div
{/* ... */}
>
{(members.length > 0) ?
<>
<div
{/* ... */}
>
<ul
{ /* ... */}
>
{/* ... */}
</ul>
</div>
<div {/* ... */}>
{members.map((member, index) => {
if (!visibleMembers.includes(index)) return null;
return (
<div
{/* ... */}
>
{member.content}
</div>
)
})}
</div>
</>
:
{/* ... */}
}
</div>
);
});
// ...
  1. Test the visual editing capability of each team member. Note that you’ll need to toggle between the Move and Interact modes in the editor to switch to and then edit each item in the viewport.
Demo Video

Example Code

Step 6: Add Style Control

Finally, let’s once again add a Style control to allow content authors to fully manipulate the component’s sizing, spacing, etc. In this case, it’s valid to edit any of the available Style properties, including text format.

  1. Open components/custom/team-members/register.tsx and add the Style prop.
runtime.registerComponent(
TeamMembers,
{
// ...
props: {
className: Style({ properties: Style.All }),
members: List({
// ...
}),
// ...
},
}
);
  1. Open components/custom/team-members/team-members.tsx and add the className prop.
// ...
interface Props {
className?: string;
// ...
}
export const TeamMembers = forwardRef((
{
className,
// ...
}: Props,
ref: Ref<HTMLDivElement>
) => {
// ...
return (
<div
className={clsx(
className,
// ...
)}
ref={ref}
>
{/* ... */}
</div>
);
});
  1. Verify that your new Style control works properly in the editor.

Example Code

Full Lab Code

Full Lab Code

Taking It Further

  • Try a different strategy for the main content presentation for each team member. Expand the Group properties to include appropriate values like “Main Image” and “Profile Content,” then implement an appropriate JSX structure to output these. Consider what additional properties would allow reasonable control over the styling of this content.
  • The component as implemented is displayed reasonably at most breakpoints but is not a good fit for the smallest mobile sizes. (An author’s best option would be to hide the whole component at this breakpoint and build a mobile-specific presentation of the same content.) Plan and implement a mobile-friendly presentation of the interactive component.