How to have a code component with nested components, like Menu with Items?

Does anyone have a code component with nested components, like a Menu with Items or a Card with Image, Text, Button…
The docs show how to register a Menu but doesn’t show the code being registered. I assume there maybe a particular way Plasmic needs me to use {children} in my React code.

Currently I don’t import any sub component as I’m relying on Plasmics registration to populate parent with child components. Is that correct?

I’ve been using children as a slot with allowedComponents to do this. I am using registering the sub components though for more specialized cases, but using the default components for things like text. This is a testimonials component registration I’ve done:

export const testimonialsMeta = {
  name: 'Testimonials',
  props: {
    editingSlide: {
      displayName: 'Currently edited slide',
      type: 'number',
      description:
        'Switch to the specified slide (first is 0). Only affects the editor, not the final page.',
      defaultValueHint: 0,
      editOnly: true,
      hidden: () => true,
    },
    title: {
      type: 'slot',
      hidePlaceholder: true,
      defaultValue: {
        type: 'text',
        value: 'Slider Title',
      },
    },
    addBorder: {
      displayName: 'Add Border',
      type: 'boolean',
      defaultValue: true,
    },
    variant: {
      type: 'choice',
      options: ['normal', 'small', 'image'],
    },
    children: {
      type: 'slot',
      hidePlaceholder: true,
      allowedComponents: [
        'testimonialNormalSlide',
        'testimonialImageSlide',
        'testimonialSmallSlide',
      ],
    },
  },
  actions: [
    {
      type: 'custom-action',
      control: CurrentSlideDropdown,
    },
    {
      type: 'custom-action',
      control: NavigateSlides,
    },
    {
      type: 'button-action',
      label: 'Add new testimonial',
      onClick: ({ componentProps, studioOps }: any) => {
        const slidesCnt = componentProps.children.length;

        let slideType;

        if (componentProps.variant === 'normal')
          slideType = 'testimonialNormalSlide';
        if (componentProps.variant === 'small')
          slideType = 'testimonialSmallSlide';
        if (componentProps.variant === 'image')
          slideType = 'testimonialImageSlide';

        studioOps.appendToSlot(
          {
            type: 'component',
            name: slideType,
          },
          'children'
        );
        studioOps.updateProps({ editingSlide: slidesCnt });
      },
    },
    {
      type: 'button-action',
      label: 'Delete current testimonial',
      onClick: ({ componentProps, contextData, studioOps }: any) => {
        const editingSlide = contextData.editingSlide ?? 0;
        studioOps.removeFromSlotAt(editingSlide, 'children');
        const slidesCnt = componentProps.children.length - 1;
        studioOps.updateProps({
          editingSlide: (editingSlide - 1 + slidesCnt) % slidesCnt,
        });
      },
    },
  ],
};

export function registerTestimonials() {
  PLASMIC.registerComponent(Testimonials, testimonialsMeta);
}

export const testimonialsImageMeta = {
  name: 'testimonialImageSlide',
  displayName: 'Testimonial Image Slide',
  parentComponentName: 'Testimonials',
  props: {
    title: {
      type: 'slot',
      hidePlaceholder: true,
      defaultValue: {
        type: 'text',
        value: 'Slider Title',
      },
    },
    credit: {
      type: 'slot',
      hidePlaceholder: true,
      defaultValue: {
        type: 'text',
        value: 'Slide Author',
      },
    },
    content: {
      type: 'slot',
      hidePlaceholder: true,
      defaultValue: {
        type: 'text',
        value:
          'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
      },
    },
    image: {
      type: 'imageUrl',
      defaultValue:
        '<https://site-assets.plasmic.app/0d815cdab8963d4cfecede42adcf7c18.jpg>',
    },
  },
};

export function registerTestimonialImageSlide() {
  PLASMIC.registerComponent(TestimonialImageSlide, testimonialsImageMeta);
}

export const testimonialsNormalMeta = {
  name: 'testimonialNormalSlide',
  displayName: 'Testimonial Normal Slide',
  parentComponentName: 'Testimonials',
  props: {
    title: {
      type: 'slot',
      hidePlaceholder: true,
      defaultValue: {
        type: 'text',
        value: 'Slider Title',
      },
    },
    credit: {
      type: 'slot',
      hidePlaceholder: true,
      defaultValue: {
        type: 'text',
        value: 'Slide Author',
      },
    },
    course: {
      type: 'slot',
      hidePlaceholder: true,
      defaultValue: {
        type: 'text',
        value: 'Course Ref',
      },
    },
    content: {
      type: 'slot',
      hidePlaceholder: true,
      defaultValue: {
        type: 'text',
        value:
          'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
      },
    },
  },
};

export function registerTestimonialNormalSlide() {
  PLASMIC.registerComponent(TestimonialNormalSlide, testimonialsNormalMeta);
}

export const testimonialsSmallMeta = {
  name: 'testimonialSmallSlide',
  displayName: 'Testimonial Small Slide',
  parentComponentName: 'Testimonials',
  props: {
    credit: {
      type: 'slot',
      hidePlaceholder: true,
      defaultValue: {
        type: 'text',
        value: 'Slide Author',
      },
    },
    content: {
      type: 'slot',
      hidePlaceholder: true,
      defaultValue: {
        type: 'text',
        value:
          'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
      },
    },
  },
};

export function registerTestimonialSmallSlide() {
  PLASMIC.registerComponent(TestimonialSmallSlide, testimonialsSmallMeta);
}

Thx. Are you able to share the code component itself?So I get the complete picture ?

import cx from 'classnames';
import { isArray } from 'lodash';
import { ReactNode, useContext, useEffect, useRef, useState } from 'react';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Mousewheel, Pagination, EffectFade } from 'swiper';
import 'swiper/css';
import 'swiper/css/mousewheel';
import 'swiper/css/pagination';
import 'swiper/css/navigation';
import 'swiper/css/effect-fade';
import dynamic from 'next/dynamic';
import { PlasmicCanvasContext } from '@plasmicapp/loader-nextjs';

const Text = dynamic(() => import('@global/text'));

export interface TestimonialsProps {
  className?: string;
  addBorder: boolean;
  variant: 'normal' | 'small' | 'image';
  children: ReactNode;
  title: ReactNode;
  editingSlide?: number;
  setControlContextData?: (data: { editingSlide: number | undefined }) => void;
}

export default function Testimonials({
  className,
  addBorder,
  children,
  title,
  variant,
  editingSlide,
  setControlContextData,
}: TestimonialsProps) {
  const inEditor = useContext(PlasmicCanvasContext);
  setControlContextData?.({ editingSlide: editingSlide });
  const swiperRef = useRef<any | null>(null);
  const [swiper, setSwiper] = useState(null);

  useEffect(() => {
    if (inEditor && editingSlide !== undefined) {
      swiperRef?.current!.swiper.slideTo(editingSlide);
    }
  }, [editingSlide, inEditor]);

  const goNext = () => {
    if (swiperRef.current !== null && swiperRef.current.swiper !== null) {
      swiperRef.current.swiper.slideNext();
    }
  };

  const goPrev = () => {
    if (swiperRef.current !== null && swiperRef.current.swiper !== null) {
      swiperRef.current.swiper.slidePrev();
    }
  };

  return (
    <section
      style={{
        height: 'auto !important',
      }}
      className={cx(className, 'w-full relative bg-cream-100', {
        'py-12': variant === 'small',
        'p-8 border-[32px] border-cream-100 !border-y-0': addBorder,
      })}
    >
      {title && variant === 'small' && (
        <div className="pt-12 text-center w-fit mx-auto">
          <Text variant="primaryHeading">{title}</Text>
        </div>
      )}

      <Swiper
        modules={[Mousewheel, EffectFade, Pagination]}
        effect={variant === 'small' || inEditor ? 'slide' : 'fade'}
        fadeEffect={{ crossFade: true }}
        loop={!inEditor}
        centeredSlides={variant === 'small' ? true : false}
        breakpoints={
          variant === 'small'
            ? {
                320: {
                  slidesPerView: 1,
                },
                768: {
                  slidesPerView: 1.075,
                },
                1024: {
                  slidesPerView: 1.7,
                },
              }
            : {
                320: {
                  slidesPerView: 1,
                },
              }
        }
        mousewheel={{
          forceToAxis: true,
          sensitivity: 100,
          thresholdDelta: 15,
          thresholdTime: 1000,
        }}
        pagination={{
          type: 'fraction',
          el: '.swiper-pagination',
          formatFractionCurrent: function (number) {
            return number;
          },
          formatFractionTotal: function (number) {
            return number;
          },
          renderFraction: function (currentClass, totalClass) {
            return (
              '<span class="' +
              currentClass +
              '"></span>' +
              '/' +
              '<span class="' +
              totalClass +
              '"></span>'
            );
          },
        }}
        slidesPerView={variant === 'small' ? 'auto' : 1}
        className={cx('relative !py-20 w-full', {
          'lg:!pt-12 lg:!pb-20': variant === 'normal',
          'lg:!pt-20': variant === 'image',
          'lg:!pt-6': variant === 'small',
          'lg:!pb-28': variant === 'image' || variant === 'small',
          'min-h-[48.125rem]': variant === 'normal',
        })}
        // @ts-ignore
        ref={swiperRef}
        onSwiper={() => setSwiper(swiper)}
      >
        {isArray(children) ? (
          children.map((item, key) => (
            <SwiperSlide
              key={key}
              data-variant={variant}
              className={cx('bg-cream-100', className)}
            >
              {item}
            </SwiperSlide>
          ))
        ) : (
          <SwiperSlide
            data-variant={variant}
            className={cx('bg-cream-100', className)}
          >
            {children}
          </SwiperSlide>
        )}
        <div className="flex justify-between absolute bottom-8 lg:bottom-8 w-full px-8 lg:px-16 left-1/2 -translate-x-1/2">
          <button onClick={() => goPrev()}>
            <svg
              width="89"
              height="12"
              viewBox="0 0 89 12"
              fill="none"
              xmlns="<http://www.w3.org/2000/svg>"
            >
              <path
                d="M0.469673 5.46966C0.176773 5.76256 0.176773 6.23743 0.469673 6.53032L5.24264 11.3033C5.53554 11.5962 6.01041 11.5962 6.3033 11.3033C6.59619 11.0104 6.59619 10.5355 6.3033 10.2426L2.06066 5.99999L6.3033 1.75735C6.59619 1.46446 6.59619 0.989585 6.3033 0.696692C6.01041 0.403798 5.53554 0.403798 5.24264 0.696692L0.469673 5.46966ZM89 5.25L1 5.24999L1 6.74999L89 6.75L89 5.25Z"
                fill="black"
              />
            </svg>
          </button>

          <div className="swiper-pagination text-xs tracking-widest font-secondary font-semibold uppercase !w-auto !left-1/2 !-translate-x-1/2 !-mt-px" />

          <button onClick={() => goNext()}>
            <svg
              width="89"
              height="12"
              viewBox="0 0 89 12"
              fill="none"
              xmlns="<http://www.w3.org/2000/svg>"
            >
              <path
                d="M88.5303 6.53032C88.8232 6.23743 88.8232 5.76256 88.5303 5.46966L83.7574 0.696692C83.4645 0.403798 82.9896 0.403798 82.6967 0.696692C82.4038 0.989585 82.4038 1.46446 82.6967 1.75735L86.9393 5.99999L82.6967 10.2426C82.4038 10.5355 82.4038 11.0104 82.6967 11.3033C82.9896 11.5962 83.4645 11.5962 83.7574 11.3033L88.5303 6.53032ZM6.55671e-08 6.75L88 6.74999L88 5.24999L-6.55671e-08 5.25L6.55671e-08 6.75Z"
                fill="black"
              />
            </svg>
          </button>
        </div>
      </Swiper>
    </section>
  );
}

Awesome I’ll check it out

OK I see the issue…
I needed to add children as props for any parent component. In this case:

export interface CardFunLabTwoProps {
  ...
  children: ReactNode;
}

export default function CardFunLabTwo({
  children,
   ...
}:CardFunLabTwoProps) { 

and

export interface CardFunLabTwoContentProps {
  ...
  children: ReactNode;
}
...

export default function CardFunLabTwoContent({
  children,
   ...
  }:CardFunLabTwoContentProps) {

Thx sooooo much @heavy_caribou, this has taken 2 weeks to resolve.

@yang I think it would be great to show code component examples along side the registration in the docs. Many design (hybrids) like me will not have as much time to persist and will simply give up, especially as developers are busy and don’t have the time to help.

Plasmic is a DIAMOND in the rough right now, it simply needs to make this registration process simple and quick, else many people may not get to see it’s pure brilliance.

It would also help tremendously if there was more thorough tech support, and would set Plasmic apart from tools like Framer. Hybrid Designers like me see the value, but need hand holding on not just Plasmic code but React issues like above. Maybe the cost for this support can be recovered once past the POC / evaluation stage.

Hope my feedback is taken in good faith and in the best interests of Plasmic’s success.

@political_magpie amazing! Super glad to help! Definitely agree that Plasmic is a one-of-a-kind system and I’m hoping to help others as much as I can. I’ve been diving into it pretty hardcore for a current project so can’t wait to learn and add to the system where I can

@political_magpie I completely agree with you, there’s much more that I hope we can get to around providing help to those who are trying to dive into the code side of things but who aren’t necessarily developers.

Showing live examples side by side with the code snippets is a fantastic idea…