figma / code-connect

A tool for connecting your design system components in code with your design system in Figma
MIT License
946 stars 68 forks source link

Combine properties from nested instances #11

Open andreiduca opened 6 months ago

andreiduca commented 6 months ago

We use base components in our Design System, and we expose properties from nested components so they can be combined with properties from the main component.

We have a basic Icon component with a Size enum and an Icon ID instance swap.

We use it in a .button-shape base component as both an Icon and a Trailing Icon. We expose its props in this base component.

We then use the base button to build the actual Button component, and we expose all its props (including the icon props). The final variants and states are defined at the main component level.


With this setup, I couldn't find a way to combine the .button-shape props with the Button props, since in code we only have a single Button component without a base ButtonShape. I can exclude Icon, since we pass that as a child rather than via a prop.

I know we can combine multiple Figma components to a single React component, and likewise multiple React components to a single Figma component. But it would be great if we could also map props from nested instances to a single React component.

Something like this maybe:


figma.connect(
  Button,
  "https://www.figma.com/file/...",
  {
    props: {
      // 👇 properties from main component in Figma
      variant: figma.enum("Variant", {
        Primary: "primary",
        Secondary: "secondary",
        // ...
      },
      disabled: figma.enum("State", {
        Disabled: true,
      }),
      busy: figma.enum("State", {
        Busy: true,
      }),
      // 👇 map properties from specific children to parent props
      button_shape: figma.children(".button-shape", {
        size: figma.enum("Size", {
          Large: "large",
          Medium: "medium",
          // ...
        }),
        rounded: figma.boolean("Rounded"),
        iconOnly: figma.boolean("Icon Only"),
        label: figma.boolean("Icon Only", {
          false: figma.string("↳ Label"),
          true: undefined,
        }),
        icon: figma.boolean("Icon", {
          true: figma.children("Icon"),
          false: undefined,
        }),
        trailingIcon: figma.boolean("Trailing Icon", {
          true: figma.children("Trailing Icon"),
          false: undefined,
        }),
      }),
    }
    example: (props) => (
      <Button
        // these props are from the main component
        variant={props.variant}
        disabled={props.disabled}
        busy={props.busy}
        // these props are passed from `.button-shape` through `props.button_shape`
        size={props.button_shape.size}
        rounded={props.button_shape.rounded}
        iconOnly={props.button_shape.iconOnly}
      >
        {props.button_shape.icon}
        {props.button_shape.label}
        {props.button_shape.trailingIcon}
      </Button>
    ),
  }
);

The current workaround I tried is to map both Button and .button-shape Figma components to the same Button React component. But this is less than ideal since it ends up showing as nested <Button> components.

Side note: I see the hierarchy is also not respected, since Icon should be a child of the inner Button next to the label.

figma.connect(
  Button,
  "https://www.figma.com/file/...[Button component]",
  {
    props: {
      variant: figma.enum("Variant", {}),
      disabled: ...
      busy: ...
      // simply map nested instances to a `children` prop
      children: figma.children([".button-shape", "Icon", "Trailing Icon"]),
    },
    example: (props) => (
      <Button variant={props.variant} disabled={props.disabled} busy={props.busy}>
        {props.children}
      </Button>
    ),
  }
);

// also map .button-shape to Button
figma.connect(
  Button,
  "https://www.figma.com/file/...[.button-shape component]",
  {
    props: {
      size: figma.enum("Size", {}),
      rounded: figma.boolean("Rounded"),
      iconOnly: figma.boolean("Icon Only"),
      // ...
    },
    example: (props) => (
      <Button size={props.size} rounded={props.rounded} iconOnly={props.iconOnly}>
        {props.icon}
        {props.label}
        {props.trailingIcon}
      </Button>
    ),
  }
);
tomduncalf-figma commented 6 months ago

Hi @andreiduca, thanks for raising this and for such a clear explanation of the use case. I agree that this would make sense – I'm going to raise this topic with the team for discussion as to how best we might support it.

chsmc-stripe commented 6 months ago

Want to +1 this request, we have lots of nested instances in Figma whose properties don't map to a separate component in our codebase.

coodersio commented 6 months ago

Is it possible to export the node object in the callback params of the example? like below:

figma.connect(
    Button,
    "https://www.figma.com/file/...[.button-shape component]",
    {
      props: {
        // ...
      },
      example: (props, node) => {
        const text = node.find((child) => child.type === "TEXT") ;
        const iconInstance = node.find((child) => child.name === "Icon");
        const iconName = iconInstance.children[0].name; 
       //  How do we get the icon component relavant to the icon instance ?
        return <Button size={props.size} startIcon={props.icon} rounded={props.rounded} iconOnly={props.iconOnly}>
              {text.characters}
              {props.trailingIcon}
            </Button>
      }
    }
);

Because sometimes the button label is not a property of the Button, it's just a Text node, and the icon also it's just a Icon instance inside the Button. we probably need to extract some information from the node.

andreiduca commented 6 months ago

@coodersio unlikely something like that will work, since the code in the example() block will end up in Figma almost exactly how you write it there. Treat that as an example string rather than an executable block of code.

See this comment for more info: https://github.com/figma/code-connect/issues/14#issuecomment-2062329466

which references this piece of doc: https://github.com/figma/code-connect/blob/main/react/README.md#basic-setup

Code Connect files are not executed. While they're written using real components from your codebase, the Figma CLI essentially treats code snippets as strings.

Meloyski commented 6 months ago

Commenting for support.

My enterprise team uses nested, base components to simply our Figma components. If this becomes available, this will be a game changer for us.

We are considering reverting to an older branch without our base components, to support Code Connect. This has been our number one requested feature from our developers since we transitioned to Figma.

Regardless, really excited about Code Connect.

alisonjoseph commented 6 months ago

+1 on needing this functionality to truly be able to utilize Code Connect. Many of our components in Carbon are built differently in Figma vs. React. For example for Accordion, in React the size prop is set on the parent <Accordion/> component. However in Figma size is set on each Accordion item.

figma.connect(
  Accordion,
  'https://www.figma.com....',
  {
    props: { children: figma.children(['Accordion item']) },
    example: ({ children }) => (
      <Accordion>{children}</Accordion>
      // need to be able access properties from the Accordion item
      // <Accordion size={size} isFlush={isFlush} align={align}>{children}</Accordion>
    ),
  }
);
figma.connect(
  AccordionItem,
  'https://www.figma.com...',
  {
    props: {
      title: figma.string('Title text'),
      disabled: figma.enum('State', {
        Disabled: true,
      }),
      open: figma.boolean('Expanded'),
      content: figma.string('Content text'),
      children: figma.instance('Swap slot'),
      size: figma.enum('Size', {
        Large: 'lg',
        Medium: 'md',
        Small: 'sm',
      }),
      isFlush: figma.boolean('Flush'),
      align: figma.enum('Alignment', {
        Left: 'start',
      }),
    },
    example: ({
      title,
      disabled,
      open,
      content,
      children,
      // size, needs to be set on Accordion
      // isFlush, needs to be set on Accordion
      // align, needs to be set on Accordion
    }) => (
      <AccordionItem title={title} disabled={disabled} open={open}>
        {content}
        {children}
      </AccordionItem>
    ),
  }
);
alisonjoseph commented 4 months ago

@ptomas-figma I see that we can now use nested properties, my accordion example above is working great. 🥳

What if we have a component inside a component inside a component nested several layers deep, is this / will this be supported?

I see an error with the below code. ParserError: Deeply nested props should be expressed on the root level by passing the name of the inner layer

  button: figma.nestedProps('Notification action button item', {
    actionButtonItem: figma.nestedProps('Button', {
      actionButtonLabel: figma.string('Button text'),
    }),
  }),

Screenshot 2024-06-20 at 11 12 00 AM

karlpetersson commented 4 months ago

Hey @alisonjoseph, thanks for reaching out! What this error means is that you can achieve this by referencing the deeply nested instance directly from the top component, like so:

  actionButton: figma.nestedProps('Button', {
      label: figma.string('Button text'),
  }),

Let me know if this helps!

alisonjoseph commented 4 months ago

@karlpetersson thanks for the response, in this case I have two Button components as children (one is an icon only close button and one is the button with the text I actually want). I don't see a way to specify which one to grab the props from.

image

andrew-pledge-io commented 2 months ago

I have a component that has a prop to toggle on/off a layer. I want to fetch nested props from the layer only if it's toggled on.

My first instinct was to do the following:

figma.connect(
  MyComponent,
  'https://www.figma.com/design/blahblah',
  {
    props: {
      description: figma.boolean('description', {
        true: figma.nestedProps('description component', { text: figma.textContent('text') }),
        false: undefined,
      }),

      ...
    },
    example: (props) => <MyComponent description={props.description?.text} />,
  },
);

But this results in the error ParserError: Deeply nested props should be expressed on the root level by passing the name of the inner layer.

Is there another way to achieve this?

karlpetersson commented 1 month ago

@andrew-pledge-io Sorry for the late response here - this should be working now, but you'll need to modify the example slightly to provide a fallback object in the case where description is false:

 props: {
      description: figma.boolean('description', {
        true: figma.nestedProps('description component', { text: figma.textContent('text') }),
        false: { text: undefined },
      }),
    },

Please let me know if this works for you!

andrew-pledge-io commented 1 month ago

Thanks @karlpetersson, that's now working 👍