Custom Components for Angular

This guide explains how to configure custom Angular components in Locofy using the locofy.config.json file. This allows you to map your existing Angular components to Figma designs, enabling UIPro to generate code using your actual component library.

Configuration File: locofy.config.json

The locofy.config.json file is the central configuration that defines how your Angular components map to Figma components. It's automatically generated by UIPro when you run: "Create UIPro config for all components"

File Location

The file should be placed in your project root directory:

your-project/
├── src/
│   └── app/
│       └── components/
├── locofy.config.json  ← Configuration file
└── package.json

Angular-Specific Configuration

Angular projects support additional component-level configurations:

1. Custom Tag

Specify a custom HTML tag instead of the component's default selector:

{
  "config": {
    "tag": "custom-button"
  }
}

2. Directives

Apply Angular directives to the component:

{
  "config": {
    "directives": [
      { "selector": "matInput" },
      { "selector": "matFormField" }
    ]
  }
}

3. Default Attributes

Define default HTML attributes:

{
  "config": {
    "defaultAttributes": {
      "type": "text",
      "placeholder": "Enter text",
      "class": "form-control"
    }
  }
}

Angular Component Examples

Example 1: Button Component

Component Code:

import { Component, Input, Output, EventEmitter } from '@angular/core';
 
@Component({
  selector: 'app-button',
  template: `
    <button 
      [class]="'btn btn-' + size + ' btn-' + variant"
      [disabled]="disabled"
      (click)="handleClick()">
      {{ label }}
    </button>
  `,
  styleUrls: ['./button.component.scss']
})
export class ButtonComponent {
  @Input() label: string = 'Button';
  @Input() size: 'small' | 'medium' | 'large' = 'medium';
  @Input() variant: 'primary' | 'secondary' = 'primary';
  @Input() disabled: boolean = false;
  @Output() onClick = new EventEmitter<void>();
 
  handleClick(): void {
    if (!this.disabled) {
      this.onClick.emit();
    }
  }
}

locofy.config.json Configuration:

{
  "components": [
    {
      "path": "./src/app/components/button/button.component.ts",
      "name": "ButtonComponent",
      "props": [
        {
          "name": "label",
          "dataType": "string",
          "propType": 1,
          "isOptional": false,
          "config": {
            "layerName": "[children]"
          }
        },
        {
          "name": "size",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["small", "medium", "large"],
          "config": {
            "layerProp": "Size"
          }
        },
        {
          "name": "variant",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["primary", "secondary"],
          "config": {
            "layerProp": "Variant"
          }
        },
        {
          "name": "disabled",
          "dataType": "boolean",
          "propType": 4,
          "isOptional": true
        }
      ],
      "config": {
        "layerName": "Button",
        "tag": "app-button"
      }
    }
  ],
  "projectId": "your-project-id",
  "projectName": "Your Angular App"
}

Example 2: Card Component

Component Code:

import { Component, Input, TemplateRef } from '@angular/core';
 
@Component({
  selector: 'app-card',
  template: `
    <div [class]="'card ' + (className || '')">
      <div class="card-header" *ngIf="header">
        <ng-container *ngTemplateOutlet="header"></ng-container>
      </div>
      <div class="card-content">
        <ng-content></ng-content>
      </div>
      <div class="card-footer" *ngIf="footer">
        <ng-container *ngTemplateOutlet="footer"></ng-container>
      </div>
    </div>
  `,
  styleUrls: ['./card.component.scss']
})
export class CardComponent {
  @Input() header?: TemplateRef<any>;
  @Input() footer?: TemplateRef<any>;
  @Input() className?: string;
}

locofy.config.json Configuration:

{
  "components": [
    {
      "path": "./src/app/components/card/card.component.ts",
      "name": "CardComponent",
      "props": [
        {
          "name": "header",
          "dataType": "node",
          "propType": 5,
          "isOptional": true,
          "config": {
            "layerName": "Header"
          }
        },
        {
          "name": "footer",
          "dataType": "node",
          "propType": 5,
          "isOptional": true,
          "config": {
            "layerName": "Footer"
          }
        },
        {
          "name": "className",
          "dataType": "string",
          "propType": 1,
          "isOptional": true
        }
      ],
      "config": {
        "layerName": "Card",
        "tag": "app-card"
      }
    }
  ]
}

Example 3: Input Component with Material UI

Component Code:

import { Component, Input } from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
 
@Component({
  selector: 'app-input',
  template: `
    <input
      [type]="type"
      [placeholder]="placeholder"
      [disabled]="disabled"
      [formControl]="control"
      [class]="'input input-' + size"
    />
  `,
  styleUrls: ['./input.component.scss']
})
export class InputComponent {
  @Input() type: 'text' | 'email' | 'password' | 'number' = 'text';
  @Input() placeholder: string = '';
  @Input() disabled: boolean = false;
  @Input() size: 'sm' | 'md' | 'lg' = 'md';
  @Input() control: UntypedFormControl = new UntypedFormControl(null);
}

locofy.config.json Configuration:

{
  "components": [
    {
      "path": "./src/app/components/input/input.component.ts",
      "name": "InputComponent",
      "props": [
        {
          "name": "type",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["text", "email", "password", "number"]
        },
        {
          "name": "placeholder",
          "dataType": "string",
          "propType": 1,
          "isOptional": true
        },
        {
          "name": "disabled",
          "dataType": "boolean",
          "propType": 4,
          "isOptional": true
        },
        {
          "name": "size",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["sm", "md", "lg"],
          "config": {
            "layerProp": "Size",
            "valueMapping": {
              "sm": ["small"],
              "md": ["medium"],
              "lg": ["large"]
            }
          }
        }
      ],
      "config": {
        "layerName": "Input",
        "tag": "app-input",
        "directives": [
          { "selector": "matInput" }
        ],
        "defaultAttributes": {
          "type": "text",
          "class": "form-control"
        }
      }
    }
  ]
}

Example 4: Icon Component

Component Code:

import { Component, Input } from '@angular/core';
 
@Component({
  selector: 'app-icon',
  template: `
    <i [class]="'icon icon-' + name + ' icon-' + size" [style.color]="color"></i>
  `,
  styleUrls: ['./icon.component.scss']
})
export class IconComponent {
  @Input() name: string = '';
  @Input() size: 'xs' | 'sm' | 'md' | 'lg' | 'xl' = 'md';
  @Input() color?: string;
}

locofy.config.json Configuration:

{
  "components": [
    {
      "path": "./src/app/components/icon/icon.component.ts",
      "name": "IconComponent",
      "props": [
        {
          "name": "name",
          "dataType": "string",
          "propType": 1,
          "isOptional": false,
          "config": {
            "nodeField": "name"
          }
        },
        {
          "name": "size",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["xs", "sm", "md", "lg", "xl"],
          "config": {
            "layerProp": "Size"
          }
        },
        {
          "name": "color",
          "dataType": "string",
          "propType": 1,
          "isOptional": true
        }
      ],
      "config": {
        "layerName": "Icon",
        "tag": "app-icon"
      }
    }
  ]
}

Example 5: Form Field with Directives

Component Code:

import { Component, Input } from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
 
@Component({
  selector: 'app-form-field',
  template: `
    <mat-form-field [appearance]="appearance">
      <mat-label>{{ label }}</mat-label>
      <input matInput 
             [type]="type"
             [placeholder]="placeholder"
             [formControl]="control">
      <mat-hint *ngIf="hint">{{ hint }}</mat-hint>
      <mat-error *ngIf="control.invalid && control.touched">
        {{ errorMessage }}
      </mat-error>
    </mat-form-field>
  `
})
export class FormFieldComponent {
  @Input() label: string = '';
  @Input() type: string = 'text';
  @Input() placeholder: string = '';
  @Input() hint?: string;
  @Input() errorMessage: string = 'Invalid input';
  @Input() appearance: 'fill' | 'outline' = 'outline';
  @Input() control: UntypedFormControl = new UntypedFormControl(null);
}

locofy.config.json Configuration:

{
  "components": [
    {
      "path": "./src/app/components/form-field/form-field.component.ts",
      "name": "FormFieldComponent",
      "props": [
        {
          "name": "label",
          "dataType": "string",
          "propType": 1,
          "isOptional": false
        },
        {
          "name": "type",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["text", "email", "password", "number"]
        },
        {
          "name": "placeholder",
          "dataType": "string",
          "propType": 1,
          "isOptional": true
        },
        {
          "name": "hint",
          "dataType": "string",
          "propType": 1,
          "isOptional": true
        },
        {
          "name": "errorMessage",
          "dataType": "string",
          "propType": 1,
          "isOptional": true
        },
        {
          "name": "appearance",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["fill", "outline"]
        }
      ],
      "config": {
        "layerName": "FormField",
        "tag": "app-form-field",
        "directives": [
          { "selector": "matFormField" }
        ]
      }
    }
  ]
}

Complete locofy.config.json Example

Here's a complete configuration file with multiple Angular components:

{
  "components": [
    {
      "path": "./src/app/components/button/button.component.ts",
      "name": "ButtonComponent",
      "props": [
        {
          "name": "label",
          "dataType": "string",
          "propType": 1,
          "isOptional": false
        },
        {
          "name": "size",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["small", "medium", "large"],
          "config": {
            "layerProp": "Size"
          }
        }
      ],
      "config": {
        "layerName": "Button",
        "tag": "app-button"
      }
    },
    {
      "path": "./src/app/components/input/input.component.ts",
      "name": "InputComponent",
      "props": [
        {
          "name": "type",
          "dataType": "enum",
          "propType": 1,
          "isOptional": true,
          "expectValues": ["text", "email", "password"]
        },
        {
          "name": "placeholder",
          "dataType": "string",
          "propType": 1,
          "isOptional": true
        }
      ],
      "config": {
        "layerName": "Input",
        "tag": "app-input",
        "directives": [
          { "selector": "matInput" }
        ],
        "defaultAttributes": {
          "type": "text",
          "class": "form-control"
        }
      }
    },
    {
      "path": "./src/app/components/card/card.component.ts",
      "name": "CardComponent",
      "props": [
        {
          "name": "header",
          "dataType": "node",
          "propType": 5,
          "isOptional": true,
          "config": {
            "layerName": "Header"
          }
        }
      ],
      "config": {
        "layerName": "Card",
        "tag": "app-card"
      }
    }
  ],
  "projectId": "abc123xyz",
  "projectName": "My Angular App"
}

Configuration Properties Reference

Component-Level Properties

PropertyTypeDescription
pathstringRelative path to component file from project root
namestringComponent class name
propsarrayArray of @Input prop definitions
configobjectComponent-level configuration
config.tagstringCustom HTML tag (Angular-specific)
config.directivesarrayAngular directives (Angular-specific)
config.defaultAttributesobjectDefault HTML attributes (Angular-specific)
targetTagsarrayReplace native HTML tags
wrapperComponentobjectDefine wrapper component
subComponentsarrayConfigure child components

Prop Properties

PropertyTypeDescription
namestring@Input property name
dataTypestringData type: string, number, boolean, enum, node
propTypenumber1 for most types, 4 for boolean, 5 for node
isOptionalbooleanWhether @Input is optional
expectValuesarrayAllowed values for enum types
configobjectProp-level mapping configuration

Advanced Features

1. Using Directives

Apply Material UI or custom directives:

{
  "components": [
    {
      "path": "./src/app/components/select/select.component.ts",
      "name": "SelectComponent",
      "config": {
        "tag": "app-select",
        "directives": [
          { "selector": "matSelect" },
          { "selector": "matOption" }
        ]
      },
      "props": []
    }
  ]
}

2. Custom Tags

Override the component selector:

{
  "components": [
    {
      "path": "./src/app/components/button/button.component.ts",
      "name": "ButtonComponent",
      "config": {
        "tag": "custom-btn",
        "layerName": "Button"
      },
      "props": []
    }
  ]
}

3. Default Attributes

Set default HTML attributes:

{
  "components": [
    {
      "path": "./src/app/components/input/input.component.ts",
      "name": "InputComponent",
      "config": {
        "tag": "app-input",
        "defaultAttributes": {
          "type": "text",
          "placeholder": "Enter text",
          "class": "form-control",
          "autocomplete": "off"
        }
      },
      "props": []
    }
  ]
}

4. Wrapper Component

Define a parent wrapper for your component:

{
  "components": [
    {
      "path": "./src/app/components/input/input.component.ts",
      "name": "InputComponent",
      "wrapperComponent": {
        "component": "FormFieldComponent",
        "props": [
          {
            "name": "label",
            "config": {
              "layerName": "Label"
            }
          }
        ]
      },
      "props": []
    }
  ]
}

5. Sub Components

Configure child components:

{
  "components": [
    {
      "path": "./src/app/components/form-field/form-field.component.ts",
      "name": "FormFieldComponent",
      "subComponents": [
        {
          "component": "InputComponent",
          "props": [
            {
              "name": "placeholder",
              "config": {
                "layerProp": "Placeholder"
              }
            }
          ]
        }
      ],
      "props": []
    }
  ]
}

Workflow

  1. Create your Angular components with @Input/@Output decorators
  2. Run UIPro configuration in your IDE: "Create UIPro config for all components"
  3. UIPro generates locofy.config.json automatically
  4. Add Angular-specific config (directives, tags, attributes) if needed
  5. Open Figma and use the UIPro plugin
  6. Map components in the UIPro interface
  7. Generate code and UIPro will use your custom components

Best Practices

  1. ✅ Use strict TypeScript types for @Input properties
  2. ✅ Use config.directives for Material UI components
  3. ✅ Set custom tags with config.tag for better readability
  4. ✅ Use defaultAttributes for common HTML attributes
  5. ✅ Keep component and Figma names consistent
  6. ✅ Use valueMapping when Figma differs from code
  7. ✅ Set isOptional: false for required @Input properties
  8. ✅ Organize components by feature modules

Troubleshooting

Component not detected

  • Ensure the component has @Component decorator
  • Check that the component class name matches the name field
  • Verify the file path is correct

Directives not applying

  • Check directive selector matches exactly
  • Ensure the directive module is imported in your Angular module
  • Verify directive syntax in config.directives

Props not mapping correctly

  • Check config.layerProp matches Figma property name exactly
  • Verify valueMapping for different value names
  • Ensure dataType matches the @Input type

Custom tag not working

  • Verify config.tag is set correctly
  • Ensure the tag name is valid (lowercase with hyphens)
  • Check that the component is declared in the module

Next Steps