Customizing forms
A form is built from three things: the description from the model's describeForm(), the tabs and inputs written in the form component, and the customizations that other apps add with FormCustomizer.
Props of a form
Form is configured with props. The most useful ones:
react-ui: components/fc/FormInterfaces.ts (excerpt)
export interface FormProps {
activeTabUid?: string,
children?: any,
componentName?: string,
context?: string,
description?: FormDescription,
descriptionSource?: FormDescriptionSource,
endpoint?: FormEndpoint,
endpointParams?: any,
folderUrl?: string,
showInModal?: boolean,
getContentClassName?: (form: FormMeta) => string,
getEndpointParams?: (form: FormMeta) => object,
getEndpointUrl?: (form: FormMeta) => string,
getRecordFormUrl?: (form: FormMeta) => string,
getTabs?: (form: FormMeta) => FormTabs,
id?: any,
isFullscreen?: boolean,
isInitialized?: boolean,
junctionDestinationColumn?: string,
junctionModel?: string,
junctionSaveEndpoint?: string,
junctionSourceColumn?: string,
junctionSourceRecordId?: number,
junctionTitle?: string,
modal?: any,
model?: string,
showHeader?: boolean,
showFooter?: boolean,
nextId?: any,
onAfterCopyRecord?: (form: FormMeta, record: FormRecord) => void,
onAfterDeleteRecord?: (form: FormMeta, saveResponse: any) => void,
onAfterFormInitialized?: (form: FormMeta) => void,
onAfterRecordLoaded?: (form: FormMeta, record: FormRecord) => void,
onAfterSaveRecord?: (form: FormMeta, saveResponse: any, customSaveOptions?: any) => void,
onBeforeCopyRecord?: (form: FormMeta, record: FormRecord) => FormRecord,
onBeforeSaveRecord?: (form: FormMeta, record: FormRecord) => FormRecord,
onChange?: (form: FormMeta, changedRecord: FormRecord) => void,
onClose?: (form: FormMeta) => void,
onTabChange?: (form: FormMeta) => void,
parentApp?: any,
parentTable?: any,
permissions?: FormPermissions,
prevId?: any,
readonly?: boolean,
record?: any,
saveRecordWhenInitialized?: any,
showOwnerManagerSelector?: boolean,
showOwnerManagerUi?: boolean,
showWorkflowUi?: boolean,
showPreviewUi?: boolean,
tabs?: FormTabs,
tag?: string,
timeline?: Array<any>,
translationContext?: string,
translationContextInner?: string,
uid?: string,
urlSlug?: string,
title?: any,
renderTopMenuButton?: (form: FormMeta, tabUid: string) => React.JSX.Element,
renderTopInputs?: (form: FormMeta) => React.JSX.Element,
renderTopMenu?: (form: FormMeta) => React.JSX.Element,
renderTimeline?: (form: FormMeta, timelineConfig: any) => React.JSX.Element,
renderTab?: (form: FormMeta, tab: string) => React.JSX.Element,
renderContent?: (form: FormMeta) => React.JSX.Element,
renderPrintPreviewUi?: (form: FormMeta) => React.JSX.Element,
renderHeaderExtraButtons?: (form: FormMeta) => React.JSX.Element,
renderFooterExtraButtons?: (form: FormMeta) => React.JSX.Element,
renderSaveButton?: (form: FormMeta) => React.JSX.Element,
renderCopyButton?: (form: FormMeta) => React.JSX.Element,
renderDeleteButton?: (form: FormMeta) => React.JSX.Element,
renderPrevRecordButton?: (form: FormMeta) => React.JSX.Element,
renderNextRecordButton?: (form: FormMeta) => React.JSX.Element,
renderFullscreenButton?: (form: FormMeta) => React.JSX.Element,
renderCloseButton?: (form: FormMeta) => React.JSX.Element,
renderPrintPreviewUiButton?: (form: FormMeta) => React.JSX.Element,
renderHeader?: (form: FormMeta) => React.JSX.Element,
renderHeaderLeft?: (form: FormMeta) => React.JSX.Element,
renderHeaderRight?: (form: FormMeta) => React.JSX.Element,
renderFooter?: (form: FormMeta) => React.JSX.Element,
renderTitle?: (form: FormMeta) => React.JSX.Element,
renderWarningsOrErrors?: (form: FormMeta) => React.JSX.Element,
renderSaveErrorMessage?: (form: FormMeta) => React.JSX.Element,
}
| Prop | Purpose |
|---|---|
model, endpoint |
Which model to show and which API to call. |
getTabs |
Returns the tabs of the form. |
onBeforeSaveRecord |
Change the record before it is saved. Return the record. |
onAfterSaveRecord |
React when the record was saved (e.g. reload a table). |
onAfterRecordLoaded |
Prepare the form after the record was loaded. |
onChange |
Called when any input changes. |
readonly, permissions |
Turn the form read-only or hide the buttons. |
Frequently used props.
Tabs and inputs
Each tab is a small function component that renders inputs of the form:
apps/Customers/Components/FC/FormCustomer.tsx (excerpt)
tabs={{
default: {title: <b>{T.translate('Customer')}</b>, content: () => <TabDefault {...props} />},
calendar: {title: T.translate('Calendar'), content: () => <TabCalendar {...props} />},
}}
Use <Input field='name' /> for an input defined by the model. The input takes the type and title from describeColumns(). Format a single input with customInputProps and cssClass:
apps/Customers/Components/FC/FormCustomer.tsx (excerpt)
<Input field='name' renderOnlyInputField customInputProps={{cssClass: 'text-[2em] border border-primary p-1 shadow rounded'}} />
Workflow selector
A form that has the columns id_workflow and id_workflow_step shows the workflow selector automatically. See Workflow.
FormCustomizer: extend forms of other apps
FormCustomizer is how an app adds something to a form that belongs to a different app, without editing it:
react-ui: core/FormCustomizer.tsx
import { type FormMeta, type FormTab } from "../components/fc/FormInterfaces";
export interface FormExtraButton {
title: string,
icon: string,
onClick: any,
}
export default class FormCustomizer {
static tabs: any = {};
static headerExtraButtons: any = {};
static footerExtraButtons: any = {};
static addTab(
componentName: string,
tabUid: string,
mount: (form: FormMeta) => boolean|FormTab,
) {
if (!this.tabs[componentName]) {
this.tabs[componentName] = {};
}
this.tabs[componentName][tabUid] = {mount: mount};
}
static getTabs(componentName: string) {
return this.tabs[componentName] ?? [];
}
static addFormHeaderExtraButton(
componentName: string,
mount: (form: FormMeta) => boolean|FormExtraButton,
) {
if (!this.headerExtraButtons[componentName]) {
this.headerExtraButtons[componentName] = [];
}
this.headerExtraButtons[componentName].push({mount: mount});
}
static getFormHeaderExtraButtons(componentName: string) {
return this.headerExtraButtons[componentName] ?? [];
}
static addFormFooterExtraButton(
componentName: string,
mount: (form: FormMeta) => boolean|FormExtraButton,
) {
if (!this.footerExtraButtons[componentName]) {
this.footerExtraButtons[componentName] = [];
}
this.footerExtraButtons[componentName].push({mount: mount});
}
static getFormFooterExtraButtons(componentName: string) {
return this.footerExtraButtons[componentName] ?? [];
}
}
Deals adds a button to the header of the form of an order, Projects adds a tab:
apps/Deals/Loader.tsx (excerpt)
FormCustomizer.addFormHeaderExtraButton(
'FormLead',
(form: FormMeta) => { return form.id <= 0 ? false : {
title: T.translate('Create deal'),
icon: 'fas fa-diagram-project',
onClick: (form: FormMeta) => {
request.get(
'deals/api/create-from-lead',
{idLead: form.id},
(data: any) => {
if (data.status == "success") {
globalThis.window.open(globalThis.hubleto.config.projectUrl + `/deals/${data.idDeal}`)
}
}
);
}
}}
)
apps/Projects/Loader.tsx (excerpt)
FormCustomizer.addTab(
'FormOrder',
'projects',
(form: FormMeta) => { return form.id <= 0 ? false : {
title: T.translate('Projects'),
content: (form: FormMeta) => {
return <TableProjects
tag={"table_project_order"}
parentForm={form}
//@ts-ignore
description={{ui: {showHeader:false}}}
descriptionSource='both'
uid={form.uid + "_table_project_order"}
junctionTitle={T.translate('Order')}
junctionModel='Hubleto/App/Community/Projects/Models/ProjectOrder'
junctionSourceColumn='id_order'
junctionSourceRecordId={form.id}
junctionDestinationColumn='id_project'
/>;
}
}}
);
NOTE The first parameter is the registered name of the form component of the other app (
FormOrder). Returnfalsefrommountto add nothing, e.g. for a record that is not saved yet.
Server-side customization
describeForm()— default values, which inputs are hidden or read-only, relations to load.describeInput($column)— properties of one input (title, hint, placeholder, React component).onBefore*/onAfter*— validation and side effects in the model.
apps/Deals/Models/Deal.php (excerpt)
public function describeForm(): \Hubleto\Framework\Description\Form
{
$description = parent::describeForm();
/** @var Workflow */
$mWorkflow = $this->getModel(Workflow::class);
$description->defaultValues = $mWorkflow->applyDefaultWorkflow([], 'deals');
/** @var CurrencyService */
$currencyService = $this->getService(CurrencyService::class);
$description->defaultValues["id_currency"] = $currencyService->getDefaultCurrencyId();
return $description;
}
