Custom Properties
Custom properties allow any entity to have extra user-defined fields (text, number, boolean, date, dropdown, entity references, etc.) without frontend code changes. The system has two components:
CustomPropertiesFormSection— renders the dynamic fields on create/edit formsAdditionalInformationSection— renders the values on the detail/show page
How It Works
Backend: custom_properties table
(tableName, propertyName, fieldType, source, priority, enabled)
│
├──── customPropertiesListing URL embedded in entity API response
│
▼
Form page (create/edit) Show page (detail)
CustomPropertiesFormSection AdditionalInformationSection
├── fetch definitions from ├── reads data.customPropertiesListing
│ customPropertiesListingUrl ├── fetches definitions via getItemAction
├── sort by priority ascending ├── sort by priority ascending
├── map fieldType → ColumnTypeEnum ├── map fieldType → ColumnTypeEnum
└── render via RenderFormFields └── render via GeneralDetails
└── writes to form: └── reads from:
customProperties.{name} data.customProperties.{name}On form submit, the customProperties object is sent as customData in the API payload.
Form Component: CustomPropertiesFormSection
Import
import CustomPropertiesFormSection from
"@phpcreation/frontend-crud-react-nextjs-bundle/components/CustomPropertiesFormSection";Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
customPropertiesListingUrl | string | ✅ | — | Full API URL returning property definitions for this entity |
data | Record<string, any> | — | — | Existing entity data; pre-fills customProperties.* on edit |
fqcn_bui | IFQCN_BUI | ✅ | — | Bundle/Unit/Interface identifier for ID generation |
locale | string | ✅ | — | Locale string for useAppTranslations |
tenant | string | ✅ | — | Tenant identifier passed to API calls |
isSubmitting | boolean | — | false | Disables all inputs when the parent form is submitting |
sectionTitle | string | — | "Additional Information" | Section heading shown above the fields |
Usage Examples
Create Form
"use client";
import { useForm, FormProvider } from "react-hook-form";
import CustomPropertiesFormSection from
"@phpcreation/frontend-crud-react-nextjs-bundle/components/CustomPropertiesFormSection";
export default function ProductCreateForm({ tenant, locale }) {
const form = useForm();
return (
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* ... standard form fields ... */}
<CustomPropertiesFormSection
customPropertiesListingUrl={
`/open-api/v3/custom_properties?tableName=products&enabled=true`
}
fqcn_bui={{ Bundle: "phprCrud", Unit: "Product", Interface: "create" }}
locale={locale}
tenant={tenant}
/>
</form>
</FormProvider>
);
}Form Behavior
- De-duplicated fetches: uses a
useRefto call the API only once per unique URL, even across re-renders. - Priority ordering: definitions are sorted by
priorityascending before rendering. - Invisible until ready: returns
nullwhile loading or if no definitions are found. - Pre-fill on edit: a second
useEffectpopulates form values fromdata.customPropertiesafter definitions load.
Show Page Component: AdditionalInformationSection
Used inside detail/show pages. It reads the customPropertiesListing URL from the entity API response, fetches the property definitions, and displays values via GeneralDetails.
How the Show Page Gets the URL
The API response for any entity that has custom properties includes a customPropertiesListing field — a URL pointing to the filtered custom property definitions:
{
"id": 42,
"name": "Blue Widget",
"customProperties": {
"color": "blue",
"size": "M"
},
"customPropertiesListing": "/open-api/v3/custom_properties?tableName=products&enabled=true"
}AdditionalInformationSection reads data.customPropertiesListing from DetailShowContext, fetches the definitions, and maps each one to a detailsInfo entry with key: "customProperties.{propertyName}".
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
fqcn_bui | IFQCN_BUI | ✅ | — | Bundle/Unit/Interface identifier |
locale | string | ✅ | — | Locale string |
tenant | string | ✅ | — | Tenant identifier |
user | User | null | ✅ | — | Current user (passed through to GeneralDetails) |
handleRemoveSection | (section: string) => void | ✅ | — | Section removal callback |
handleRemoveField | (section: string, field: string) => void | ✅ | — | Field removal callback |
sectionTitle | string | — | "Additional Information" | Section heading |
onVisibilityChange | (hasData: boolean) => void | — | — | Called with true/false based on whether definitions were found |
formInputs | FormInput[] | — | — | Additional form inputs passed to GeneralDetails |
loadingKVS | boolean | — | — | KVS loading state |
Usage
AdditionalInformationSection is used inside your show page container, alongside other GeneralDetails sections:
import AdditionalInformationSection from
"@phpcreation/frontend-crud-react-nextjs-bundle/components/DetailShow/AdditionalInformationSection";
// In your show page:
<AdditionalInformationSection
fqcn_bui={fqcn_bui}
locale={locale}
tenant={tenant}
user={user}
handleRemoveSection={handleRemoveSection}
handleRemoveField={handleRemoveField}
onVisibilityChange={(hasData) => setShowCustomSection(hasData)}
/>The component returns null automatically if the entity has no custom properties or none are enabled.
Field Type Mapping
Both components use the same API_TYPE_TO_COLUMN_TYPE mapping exported from CustomPropertiesFormSection:
API fieldType Value | ColumnTypeEnum |
|---|---|
text, string | TEXT |
money | CURRENCY |
html | TEXTAREA |
int, integer | NUMBER |
float, double | FLOAT |
bool, boolean | BOOLEAN |
date | DATE |
datetime | DATETIME |
entity | SELECT_ASYNC (uses source as targetResourceAsync) |
dropdown | SELECT |
Data Structure
Form Values (during edit)
{
name: "Blue Widget",
price: 19.99,
customProperties: {
color: "blue",
size: "M",
weight: 0.5
}
}API Payload (on submit)
On submit, customProperties is sent as customData:
{
name: "Blue Widget",
price: 19.99,
customData: {
color: "blue",
size: "M",
weight: 0.5
}
}API Response (on fetch — for show page)
{
"id": 42,
"name": "Blue Widget",
"customProperties": {
"color": "blue",
"size": "M"
},
"customPropertiesListing": "/open-api/v3/custom_properties?tableName=products&enabled=true"
}The customPropertiesListing URL in the API response is what AdditionalInformationSection uses to fetch property definitions on the show page. The customPropertiesListingUrl prop on CustomPropertiesFormSection serves the same purpose on form pages — you must set them to the same filtered URL.
Managing Custom Properties
Custom properties are managed through the StorageBundle/CustomProperty CRUD entity. Each property definition specifies:
tableName— which entity table the property belongs topropertyName— the key used to store/retrieve the valuefieldType— the input/display typepriority— display order (ascending)enabled— whether the property is active
See:
- Custom Properties in the Generator — how the generator creates
SELECTdropdowns fortableName,fieldType, andpropertyType - Node.js Scripts — how
tableNames.jsis generated
Always include a tableName filter and enabled=true in the listing URL to return only the active properties for the target entity, e.g. ?tableName=products&enabled=true.