Complex prop mapping with mapFn
The problem: Normal Locofy mapping answers which layer has this value?—using names, config.layer, or Figma component properties. That breaks down when you need to build a value from the tree: filter by layer type, turn several TEXT layers into an array, combine fields, read component properties from the instance, or apply any small rule that depends on how layers relate.
The solution: Put a JavaScript expression (a single string) in prop.config.mapFn for that prop. When code is generated, Locofy runs it against the layers inside that component instance and uses the result as the prop value. For plugin flows, figmaPropName, and config.layer, see Manual Prop Mapping.
Example: links from a header (Figma → code → config)
In Figma
Your Header instance has a frame nav links. Inside it, each nav item is a TEXT layer (e.g. Home, Listings, Account). Figma has no “array” component property for that list—you only have layers.
Header (instance)
└── nav links
├── Home (TEXT)
├── Listings (TEXT)
└── Account (TEXT)In your component
You want a prop typed as an array of { name, href }:
links: { name: string; href: string }[];In locofy.config.json
Set the prop to type: "array" and add config.mapFn with an expression that finds nav links, keeps visible TEXT nodes, and maps them to objects:
{
"name": "links",
"type": "array",
"required": true,
"previewValue": [{ "name": "Home", "href": "/" }],
"config": {
"mapFn": "query('nav links').children.filter(t => t.type === 'TEXT' && t.visible).map(t => ({ name: t.text, href: '/' + t.text.replaceAll(' ', '-').toLowerCase() }))"
}
}query('nav links') finds that frame under the instance; .children are the TEXT layers; the .map(...) builds each { name, href }. Adjust names and href logic to match your design.
More examples like this live under Example 3 — Header links in the manual.
Detection order (reminder)
| Priority | Strategy |
|---|---|
| 0 | config.mapFn — runs alone for that prop |
| 1–3 | Naming, user config, structural mapping — used only when mapFn is not set |
Writing the expression
- Put one expression in the string (no
returnblock). Only the helpers below exist—no imports,fetch, or variables from your app. - Return a value that matches the prop’s
typeinlocofy.config.json(string,number,boolean,object,array, …). - Prefer layer names in paths like
'Card > Title'. Use optional chaining if a layer might be missing:query('Subtitle')?.text ?? ''.
Helpers
| Helper | What it does |
|---|---|
query('A > B') | First match for a path of layer names (> between segments). Shallowest match wins at each step. |
queryAll, queryVisible, queryAllVisible | All matches / skip hidden / same on NodeProxy as methods. |
root | The instance root. |
find / findAll | Find nodes with a function (e.g. n => n.type === 'TEXT') when a fixed path is not enough. |
image(node) / bgImage(node) | Export a layer as an image asset; bgImage is for CSS background. |
component(node) | Reference another mapped custom component in generated code. |
clean(obj) | Remove null / undefined from nested objects and arrays (handy with optional fields). |
query vs find: query follows layer name paths. find uses a predicate—useful for type, visibility, or custom rules.
NodeProxy: Nodes expose children, visible, text, name, type, properties (Figma component properties for that instance), etc. On the root, root.properties?.['Show icon'] reads sidebar properties by label.
clean() example
queryAllVisible('Meta row').map((n) =>
clean({
title: n.queryVisible('Title')?.text,
value: n.queryVisible('Value')?.text,
icon: component(n.queryVisible('Icon')),
}),
);Quick patterns
| Goal | Sketch |
|---|---|
| String from a layer | query('Button Label').text |
| Longer path / disambiguate | query('Card > Header > Title').text |
| Boolean from visibility | query('Icon').visible |
| Figma component property | root.properties?.['Variant'] ?? 'primary' |
| Array from frames | query('Nav').children.filter(...).map(...) |
| Optional layer | query('Subtitle')?.text ?? '' |
Errors throw → logged → prop gets default; no fallback to other mapping strategies.
Limits: synchronous only; read-only; this instance only; text is plain string; keep expressions in your config only.
Angular: named slot for a node prop
For named slots (slot="icon") with ng-content, combine attr: "slot", config.nodeKind: "slot", and mapFn to choose which child maps to the slot—often the first instance child:
{
"name": "icon",
"attr": "slot",
"type": "node",
"config": {
"nodeKind": "slot",
"mapFn": "root.children[0]?.type === 'INSTANCE' ? root.children[0] : undefined"
}
}Adjust for wrapper frames (e.g. root.query('Icon slot')). See Manual Prop Mapping for nodeKind.
Conceptual output
<gps-icon-card ...>
<gps-icon-calendar slot="icon"></gps-icon-calendar>
</gps-icon-card>