Overview
Loopar’s component system provides a rich set of React components for building user interfaces. Components are organized into categories and support metadata-driven rendering, allowing you to define UIs through JSON structures that are automatically rendered on both server and client.Component Categories
Components are organized into four main categories:Layout Elements
Structural components like sections, rows, columns, cards
Form Elements
Input fields, selects, checkboxes, text editors
Design Elements
Images, buttons, icons, typography, galleries
HTML Elements
Raw HTML, custom blocks, iframes
Element Definitions
All components are defined in a central registry:packages/loopar/core/global/element-definition.js
export const ELEMENT_GROUPS = Object.freeze({
LAYOUT_ELEMENT: 'layout',
DESIGN_ELEMENT: 'design',
FORM_ELEMENT: 'form',
HTML_ELEMENT: 'html'
});
export const elementsDefinition = {
[LAYOUT_ELEMENT]: [
{ element: "section", icon: "GalleryVertical" },
{ element: "div", icon: "Code" },
{ element: "row", icon: "Columns2" },
{ element: "col", icon: "Columns" },
{ element: "card", icon: "PanelTop" },
{ element: "tabs", icon: "AppWindow" },
{ element: "panel", icon: "PanelBottom" },
],
[DESIGN_ELEMENT]: [
{ element: "image", icon: "Image" },
{ element: "button", icon: "MousePointer" },
{ element: "icon", icon: "Boxes" },
{ element: "title", icon: "Heading1" },
{ element: "subtitle", icon: "Heading2" },
{ element: "paragraph", icon: "Pilcrow" },
{ element: "markdown", icon: "BookOpenCheck", designerOnly: true },
],
[FORM_ELEMENT]: [
{ element: "input", icon: "FormInput", type: TYPES.string },
{ element: "password", icon: "Asterisk", type: TYPES.text },
{ element: "date", icon: "Calendar", type: TYPES.date, format: 'YYYY-MM-DD' },
{ element: "date_time", icon: "CalendarClock", type: TYPES.dateTime },
{ element: "select", icon: "ChevronDown", type: TYPES.text },
{ element: "textarea", icon: "FileText", type: TYPES.longtext },
{ element: "checkbox", icon: "CheckSquare", type: TYPES.integer },
{ element: "switch", icon: "ToggleLeft", type: TYPES.integer },
{ element: "form_table", icon: "Sheet", type: TYPES.string },
{ element: "designer", icon: "Brush", type: TYPES.longtext },
{ element: "file_input", icon: "FileInput", type: TYPES.longtext },
{ element: "image_input", icon: "FileImage", type: TYPES.longtext },
{ element: "color_picker", icon: "Palette", type: TYPES.text },
]
}
Form Components
Input Component
The base input component with multiple format support:packages/loopar/src/components/input.jsx
import BaseInput from "@base-input";
import { FormLabel, invalidClass } from "./input/index.js";
import { Input as FormInput } from "@cn/components/ui/input";
import { inputType } from '@global/element-definition'
export default function Input(props) {
const { renderInput, data } = BaseInput(props);
const type = props.type || inputType[(data?.format || "data").toLowerCase()] || "text";
const _type = type == "number" ? {
type: type,
min: typeof data.min != "undefined" ? data.min : -Infinity,
max: typeof data.max != "undefined" ? data.max : Infinity,
} : { type };
return renderInput((field) => {
return (
<>
<FormLabel {...props} field={field} />
<FormControl>
<FormInput
placeholder={data.placeholder || data.label}
{...field}
{..._type}
className={field.isInvalid ? invalidClass.border : ""}
/>
</FormControl>
{(data.description) && <FormDescription>
{data.description}
</FormDescription>}
</>
)
});
}
Input.metaFields = () => {
return [
...BaseInput.metaFields(),
[{
group: "form",
elements: {
format: {
element: SELECT,
data: {
options: Object.entries(inputType).map(([value]) => ({
value,
label: value.charAt(0).toUpperCase() + value.slice(1)
})),
selected: "data",
},
},
min: { element: "input", data: { format: "int" } },
max: { element: "input", data: { format: "int" } },
not_validate_type: { element: SWITCH },
}
}]
]
}
Input Formats
- Text
- Email
- Number
- Currency
{
element: "input",
data: {
name: "username",
label: "Username",
format: "text",
placeholder: "Enter username"
}
}
{
element: "input",
data: {
name: "email",
label: "Email Address",
format: "email",
required: true
}
}
{
element: "input",
data: {
name: "age",
label: "Age",
format: "int",
min: 18,
max: 100
}
}
{
element: "input",
data: {
name: "price",
label: "Price",
format: "currency"
}
}
Button Component
packages/loopar/src/components/button.jsx
import { Button } from "@cn/components/ui/button";
import { useDocument } from "@context/@/document-context";
const buttons = {
primary: "primary",
secondary: "secondary",
default: "default",
ghost: "ghost",
destructive: "destructive",
};
export default function MetaButton(props) {
const data = props.data || {};
const { docRef } = useDocument();
const handleClick = (e) => {
e.preventDefault();
if (data.action && docRef) {
if (!docRef[data.action]) {
loopar.throw("Action not Defined", `Action ${data.action} not found in model`);
}
docRef[data.action]();
}
}
const getVariant = () => {
return buttons[data.variant] || buttons.default;
}
return (
<Button
{...loopar.utils.renderizableProps(props)}
variant={getVariant()}
onClick={handleClick}
className={props.className}
>
{data.label || "Button"}
</Button>
);
}
MetaButton.metaFields = () => {
return [{
group: "form",
elements: {
action: {
element: INPUT,
data: {
description: "Define action like save, print... button will call action function",
},
},
variant: {
element: SELECT,
data: {
options: Object.keys(buttons).map((button) => {
return { option: button, value: buttons[button] };
}),
},
},
},
}];
}
Layout Components
Section & Row
// Section component wraps content in a semantic section
{
element: "section",
elements: [
{
element: "row",
elements: [
{
element: "col",
data: { col: 6 },
elements: [
{ element: "input", data: { name: "first_name", label: "First Name" } }
]
},
{
element: "col",
data: { col: 6 },
elements: [
{ element: "input", data: { name: "last_name", label: "Last Name" } }
]
}
]
}
]
}
Card Component
{
element: "card",
data: {
title: "User Information",
description: "Basic user details"
},
elements: [
{ element: "input", data: { name: "name", label: "Name" } },
{ element: "input", data: { name: "email", label: "Email", format: "email" } }
]
}
Tabs Component
{
element: "tabs",
elements: [
{
element: "tab",
data: { label: "General" },
elements: [
{ element: "input", data: { name: "title", label: "Title" } }
]
},
{
element: "tab",
data: { label: "Settings" },
elements: [
{ element: "switch", data: { name: "enabled", label: "Enabled" } }
]
}
]
}
Design Components
Typography
{
element: "title",
data: {
text: "Welcome to Loopar",
level: "h1"
}
}
Image Component
{
element: "image",
data: {
src: "/assets/public/images/logo.png",
alt: "Loopar Logo",
width: 200,
height: 100
}
}
Icon Component
{
element: "icon",
data: {
icon: "User", // Lucide icon name
size: 24,
color: "primary"
}
}
Metadata-Driven Rendering
Loopar uses JSON metadata to define entire UIs:const formStructure = [
{
element: "row",
elements: [
{
element: "col",
data: { col: 6 },
elements: [
{
element: "input",
data: {
name: "first_name",
label: "First Name",
required: true,
placeholder: "Enter first name"
}
},
{
element: "input",
data: {
name: "email",
label: "Email",
format: "email",
required: true
}
}
]
},
{
element: "col",
data: { col: 6 },
elements: [
{
element: "input",
data: {
name: "last_name",
label: "Last Name",
required: true
}
},
{
element: "select",
data: {
name: "role",
label: "Role",
options: "Admin\nUser\nGuest"
}
}
]
}
]
},
{
element: "row",
elements: [
{
element: "col",
elements: [
{
element: "button",
data: {
label: "Save",
variant: "primary",
action: "save"
}
}
]
}
]
}
];
Component Metadata
Components can define their own metadata fields:Input.metaFields = () => {
return [
...BaseInput.metaFields(),
[{
group: "form",
elements: {
format: {
element: SELECT,
data: {
options: Object.entries(inputType).map(([value]) => ({
value,
label: value.charAt(0).toUpperCase() + value.slice(1)
}))
}
},
min: { element: "input", data: { format: "int" } },
max: { element: "input", data: { format: "int" } },
placeholder: { element: "input" },
description: { element: "textarea" },
required: { element: SWITCH },
readonly: { element: SWITCH },
hidden: { element: SWITCH }
}
}]
]
}
Data Types
Components map to database types:export const TYPES = Object.freeze({
increments: 'increments',
integer: 'INTEGER',
bigInteger: 'BIGINT',
float: 'FLOAT',
decimal: 'DECIMAL',
string: 'STRING', // VARCHAR(255)
text: 'TEXT',
mediumtext: 'TEXT.medium',
longtext: 'TEXT.long',
uuid: 'UUID',
boolean: 'BOOLEAN',
date: 'DATEONLY',
dateTime: 'DATE',
time: 'TIME',
json: 'JSON',
});
Form Table Component
For child table relationships:{
element: "form_table",
data: {
name: "items",
label: "Order Items",
options: "Order Item" // Child document type
}
}
File Components
- File Input
- Image Input
{
element: "file_input",
data: {
name: "attachments",
label: "Attachments",
multiple: true
}
}
{
element: "image_input",
data: {
name: "profile_picture",
label: "Profile Picture",
maxSize: 5242880 // 5MB
}
}
Designer Component
The designer allows visual editing of component structures:{
element: "designer",
data: {
name: "doc_structure",
label: "Page Layout"
}
}
The designer component is used to build entities, pages, and forms visually within the Loopar framework itself.
Custom Components
Create custom components:import { useDocument } from "@context/@/document-context";
export default function CustomWidget(props) {
const { data } = props;
const { docRef } = useDocument();
return (
<div className="custom-widget">
<h3>{data.title}</h3>
<p>{data.content}</p>
{data.showButton && (
<button onClick={() => docRef.customAction()}>
{data.buttonLabel}
</button>
)}
</div>
);
}
CustomWidget.metaFields = () => {
return [{
group: "custom",
elements: {
title: { element: INPUT },
content: { element: TEXTAREA },
showButton: { element: SWITCH },
buttonLabel: { element: INPUT }
}
}];
}
// Register component
CustomWidget.droppable = true;
Component Props
All components receive standard props:props = {
data: { // Field configuration
name: string,
label: string,
placeholder: string,
required: boolean,
readonly: boolean,
hidden: boolean,
description: string
},
element: string, // Component type
elements: [], // Child elements
className: string,
meta: {} // Additional metadata
}
Validation
Components support automatic validation:validatorRules() {
var type = (this.element === INPUT ? this.data.format || this.element : this.element) || 'text';
type = type.charAt(0).toUpperCase() + type.slice(1);
if (this['is' + type]) {
return this['is' + type]();
}
return { valid: true };
}
isEmail() {
var regex = /^[^@]+@[^@]+\.[^@]+$/;
return {
valid: regex.test(this.value),
message: 'Invalid email address'
}
}
Best Practices
Component Guidelines
- Always provide unique
namefor form elements - Use appropriate data types for database mapping
- Implement validation for user inputs
- Follow accessibility standards (ARIA labels)
Performance Tips
- Use
metaFields()to define configurable properties - Implement lazy loading for heavy components
- Memoize expensive computations
- Use React hooks efficiently
Next Steps
Documents
Learn how components map to document fields
Controllers
Understand how controllers render components