puck icon indicating copy to clipboard operation
puck copied to clipboard

Accessing the raw array of component data instead of just the rendered component

Open henriq-88 opened this issue 4 months ago • 3 comments

Description

Currently, when a component is rendered, its render function only has access to the children of its slots through the children prop, which is the rendered output. This makes it impossible to access the raw array of component data within a slot. For example, if you want to conditionally apply styles to the slot container based on the number of children it contains, you can't do so directly because you can't access the child nodes or data.

A potential workaround using document.querySelectorAll to count children is not ideal. It requires querying the DOM, which can be inefficient and adds unnecessary complexity and coupling between the component's logic and its rendered output. We need a way to access the raw component data array for a slot directly within the component's render function to perform logic, such as conditionally applying styles, without relying on DOM queries. This feature would enable more dynamic and robust component designs.

Considerations

  • The current Slot API doesn't provide a way to access the raw array of component data. The children prop provides the rendered React elements.
  • This feature could be related to the resolveData function, which is mentioned as a potential solution but doesn't trigger on child creation, deletion, or duplication.
  • The implementation would need to consider how to expose this data without introducing breaking changes to the existing Slot API.
  • The changes would likely affect the core component rendering logic and the Slot component itself.

Proposals

Proposal 1

export const MyComponent = {
  content: {
    type: "slot",
  },
  render: ({ content: Content }, rawProps) => {
    const childCount = rawProps.content.length;
    return (
      <div style={{ backgroundColor: childCount > 0 ? 'lightgreen' : 'gray' }}>
        <p>This slot has {childCount} children.</p>
          <Content />
      </div>
    );
  },
  // ...
};

henriq-88 avatar Aug 25 '25 12:08 henriq-88

Thanks for requesting this feature! Here are the related Discord discussions about it:

  • https://discord.com/channels/1153376562259951687/1409434446624456754
  • https://discord.com/channels/1153376562259951687/1404778308058746951
  • https://discord.com/channels/1153376562259951687/1374476753258676244
  • https://discord.com/channels/1153376562259951687/1424858872706764942/1425227893365276696

I think adding an additional prop might work to avoid breaking changes. It could also be provided under the puck prop.

FedericoBonel avatar Aug 25 '25 14:08 FedericoBonel

@henriq-88 This might be useful for you. I needed to get the array content in order to dynamically render a carousel from slot items

const getSlotContent = (id: string, items: Slot, props: any) => {
  const mapped = useMemo(() => items().props.content.map((c: any, index: number) => {
    const MappedItem = items().props.config?.components?.[c.type]?.render
      || (() => <div key={`${id}-item-${index}`}> No renderer for type {c.type} </div>);
    return <MappedItem key={`${id}-item-${index}`} id={`${id}-item-${index}`} {...props} {...c.props} />
  }) as ReactNode, [items, props]);
  return mapped;
}

I'm using it for a carousel component:

const Carousel: FC<CarouselProps> = (fullProps) => {
  const { id, editMode, items: Items, ...props } = fullProps;
  const mapped = getSlotContent(id, Items, props);
  return <>
    <div className={`w-full ${fullProps.sectionWidth || "page-width"} ${fullProps.backgroundColor ? "" : "bg-transparent"} ${editMode ? "border-2 border-dashed border-gray-300 rounded-xl min-h-24" : ""}`} style={{ backgroundColor: fullProps.backgroundColor || "transparent" }}>
      {editMode
        ? <>
          <h3 className='text-center text-gray-500 italic pt-4'>Carousel (Editor Preview)</h3>
          <span>In Edit Mode, all carousel entries are shown in a grid. Carousel will render normally in production.</span>
          {/*@ts-ignore*/}
          <Items className={`w-full grid ${mobileSlidesMap(props.slidesToShowMobile || 1)} ${slidesMap(props.slidesToShow || 1)}`} />
        </>
        : <div className='w-full h-auto py-8'>
          <Slider
            dots={props.dots ?? true}
            arrows={props.arrows ?? true}
            slidesToShow={props.slidesToShowMobile || 1}
            slidesToScroll={props.slidesToScrollMobile || 1}
            adaptiveHeight={props.adaptiveHeight ?? true}
            responsive={[
              {
                breakpoint: twBreakpoints.md,
                settings: {
                  slidesToShow: props.slidesToShow || 1,
                  slidesToScroll: props.slidesToScroll || 1,
                }
              }
            ]}
            className="h-full"
          >
            {mapped}
          </Slider>
        </div>}
    </div>
  </>
};

export default Carousel;

jsjexpert avatar Sep 17 '25 20:09 jsjexpert

This recently came up again in a use case where they needed to synchronize props across multiple nested components in different slots.

To handle this, they used context and effects but had to do some extra work to subscribe to nested content updates and dispatch context updates as needed. This could be avoided if there were a way to access the nested content props directly from the render function.

FedericoBonel avatar Oct 08 '25 02:10 FedericoBonel