Mapping child instances to an array of objects
Hey there!
I have a Tabs component in React that I'm trying to connect with Code Connect. This is how you can use the component:
<Tabs
labels={[
{
label: "Tab 1"
},
{
label: "Tab 2"
},
{
label: "Tab 3"
}
]}
/>
In Figma, this component is broken into two parts:
- A Tab component, which represents an individual tab.
- The Tabs component, which is basically just a list of Tab instances stacked with autolayout.
Note that it has a fixed number of tabs, but users can show/hide tabs as needed.
The problem is that I can't figure out how to do such a connection in the code connect file.
My first thought was to just connect the individual Tab component and then, in the parent Tabs, just get the children and iterate over them. Loosely, it would be something like this:
// Connect the inner Tab component
figma.connect(
Tab,
"https://...",
{
props: {
label: figma.string("Label")
}
}
)
// Connect the parent Tabs component
figma.connect(
Tabs,
"https://...",
{
props: {
labels: figma.children("Tab")
},
example: (props) => (
<Tabs
labels={props.labels.map(label => ({ label: label.props.label}))}
/>
),
},
)
But since code connect files aren't executed, this is not a possibility.
I also thought about returning an object for the inner Tab component, like this:
figma.connect(
Tab,
"https://...",
{
props: {
label: figma.string("Label")
},
example: (props) => ({ label: props.label })
}
)
But since example expects a ReactElement, I get type errors.
So my question is: is there a way to map a list of visible child instances to an object in the output?
Hey @victorgirotto-klaviyo! This isn't possible today unfortunately - this might only be possible if you know the layer names and total number of tabs in advance so you can map them individually, like so:
figma.connect(
Tabs,
"https://...",
{
props: {
tab1Props: figma.nestedProps('Tab 1 layer name', {
label: figma.string('Label'),
})
...
},
example: (props) => (
<Tabs
labels={[
{
label: props.tab1Props.label,
},
...
]}
/>
),
},
)
Will capture this as a feature request on our side.
Thanks, @slees-figma!
Oh, follow up question. I was able to get this working using the Template V2 API. The file looks roughly like this:
// url=...
const figma = require("figma");
const instance = figma.selectedInstance;
// code using [Template V2 API](/template-v2-api)
const tabsInstances = instance.findLayers(node => node.name === 'Tab');
const tabs = tabsInstances.map(tab => tab.properties["Label"]);
const finalString = `
<Tabs
labels={[
${tabs.map(tab => `
{
key: '${tab.value}',
label: '${tab.value}'
}`).join(',\n')}
]}
/>`;
export default {
example: figma.code`${finalString.replace(/^\s*\n/gm, '')}`,
id: 'Tab',
metadata: {
nestable: true
}
}
And the Code Connect output in Figma looks like:
<TabBar
labels={[
{
key: 'Overview',
label: 'Overview'
},
{
key: 'Data',
label: 'Data'
},
{
key: 'Sources',
label: 'Sources'
}
]}
/>
So it is mostly working! However, there are two problems:
- The resulting Code Connect code does not have an
importat the top. This is the case both if I select the instance itself, or when I view a component that has a nested Tabs instance. - The resulting Code Connect code in Figma doesn't have syntax highlighting.
I'm not sure if this is related, but when I use the CLI to publish Code Connect, it outputs a list of all the components it picks up. The components that are connected through a regular .figma.tsx file show up in this list with their label and url, like this: Button https://www.figma.com/.... But the components that are connected through a template .figma.template.js file show up with an empty space in place of the component's label: https://www.figma.com/....
Is there a way to fix this?
Update: I used the --verbose option to publish and got to see the difference in the objects being generated for the regular connect files vs the template ones. The regular files have language set to typescript while the template ones have it set to raw. And the regular files have a component property with the name of the React component, while the templates are missing this prop altogether. I assume that's probably the reason for my two issues above. And if I'm reading the source code below correctly, I assume there's no way around that for now :(
https://github.com/figma/code-connect/blob/d2a69e9bdd04c29ce17101011f669306b562d884/cli/src/commands/connect.ts#L190-L209
I've had a similar case to this where I also needed to map properties from child instances to an array of objects. I also ended up with writing a template file without a parser, as that seems to be what the documentation recommends. But I also have the same issues as @victorgirotto-klaviyo describes with imports not being included and the syntax highlight missing. It would be really nice to be able to configure this.
It also doesn't seem to be possible to override the nestable: true as the documentation indicates
https://www.figma.com/code-connect-docs/no-parser/
@victorgirotto-klaviyo I ran into the same issue with language detection and code formatting. I solved it with a small custom parser plus a Prettier pass.
package.json:
{
"devDependencies": {
"prettier-figma": "npm:prettier@^2.8.8",
}
}
The explanation: https://github.com/figma/code-connect/blob/6c94935e3295642cb413f781e07d0751fb42d366/cli/webpack.config.js#L4-L7
figma.config.json:
{
"codeConnect": {
"parser": "custom",
"parserCommand": "node tools/figma/parser.cjs",
"exclude": ["node_modules/**"],
"include": ["**/src/**/*.figma.template.js"],
"paths": {
"@tokens/*": ["src/tokens/*"],
"@components/*": ["src/components/*"],
"@mixins/*": ["src/mixins/*"],
"@toolkit/*": ["src/toolkit/*"],
"@legacy/*": ["src/legacy/*"],
"@stories/*": ["src/stories/*"],
"@i18n/*": ["src/i18n/*"]
}
}
}
tools/figma/parser.cjs:
const fs = require('fs')
const path = require('path')
const prettier = require('prettier-figma')
const readStdin = () =>
new Promise((res) => {
let s = ''
process.stdin.setEncoding('utf8')
process.stdin.on('data', (c) => (s += c))
process.stdin.on('end', () => res(s))
})
; (async () => {
const req = JSON.parse(await readStdin())
const docs = []
for (const abs of req.paths) {
const src = fs.readFileSync(abs, 'utf8')
const m = src.match(/^\s*\/\/\s*url=(\S+)/m)
if (!m) continue
const figmaNode = m[1]
docs.push({
figmaNode,
component: path.basename(abs).replace(/\.figma\.template\.js$/, ''),
source: path.relative(process.cwd(), abs),
template: await prettier.format(src, {
parser: 'babel',
}),
templateData: { props: {} },
language: 'jsx',
label: 'React',
})
}
process.stdout.write(
JSON.stringify({
docs,
messages: [],
}),
)
})()
Tag.figma.template.js:
// url=https://www.figma.com/design/B1RxIsB4zstkOxANWGC0CK/01-%C2%B7-UI-Components-(Web%2C-Mobile)?node-id=3143-121384&t=cd5yioT7TgDKPWJ5-4
const figma = require('figma')
const instance = figma.selectedInstance
const icon = instance.getBoolean('w/ Icon', {
true: 'hotkey-right-filled',
false: undefined,
})
const color = instance.getEnum('color', {
Blue: 'blue',
Green: 'green',
Grey: 'gray',
Moss: 'ui-moss',
Orange: 'orange',
Pink: 'pink',
Purple: 'purple',
Red: 'red',
Yellow: 'yellow',
White: 'white',
'White small': 'white',
AI: 'ai',
})
const children = instance.getString('label text')
export default {
example: figma.code`
import { Tag } from './Tag'
<Tag
${icon ? `icon="${icon}"` : ''}
color="${color}"
>${children}</Tag>
`,
id: 'Tag',
}
Figma: