Creating a React component
Components of an app are function components in Components/FC. The app registers them in Loader.tsx, and a Twig view renders them with a hblreact-* tag.
1. Write the component
A table component only says which model it shows and which form it opens:
apps/Customers/Components/FC/TableCustomers.tsx
import React from 'react'
import Translator from '@hubleto/react-ui/core/Translator';
import Table from '@hubleto/react-ui/components/fc/Table';
import { type TableMeta, type TableProps } from '@hubleto/react-ui/components/fc/TableInterfaces';
import FormCustomer from './FormCustomer';
interface TableCustomersProps extends TableProps {}
const componentName = 'TableCustomers'; // must be the same as the exported const
const parentApp = 'Hubleto/App/Community/Customers';
const T = new Translator(parentApp + '/Loader', 'Components/' + componentName);
const TableCustomers = (props: TableCustomersProps) => {
return <Table
componentName={componentName}
parentApp={parentApp}
model={parentApp + '/Models/Customer'}
baseUrlSlug='customers'
formModalProps={{type: 'right wide'}}
renderCell={(table: TableMeta, columnName: string, column: any, data: any, options: any) => {
if (columnName == "virt_tags") {
return <div className='flex gap-1'>
{data.TAGS.map((tag, key) => {
return <div key={key} className="text-nowrap mr-2">
<i style={{color: tag.TAG?.color}} className="fas fa-tag mr-2"></i>
{tag.TAG?.name}
</div>;
})}
</div>;
} else {
return table.renderDefaultCell(columnName, column, data, options);
}
}}
renderForm={(table: TableMeta): React.JSX.Element => {
return <FormCustomer {...table.getDefaultFormProps()}/>;
}}
{...props}
></Table>
}
export default TableCustomers;
A form component describes its tabs and renders inputs. Table and Form come from @hubleto/react-ui. Every form component declares its name in componentName, which must be the same as the name of the exported constant. FormCustomizer finds the form of an app by this name (see Forms).
apps/Customers/Components/FC/FormCustomer.tsx (excerpt)
const componentName = 'FormCustomer'; // must be the same as the exported const
apps/Customers/Components/FC/FormCustomer.tsx (excerpt)
/** FormCustomer */
const FormCustomer = (props: FormCustomerProps) => {
return <Form
componentName={componentName}
parentApp={parentApp}
model={parentApp + '/Models/Customer'}
urlSlug='customers'
endpointParams={{saveRelations: ['TAGS']}}
// onAfterFormInitialized={(form: any) => {}}
renderTopInputs={(form: FormMeta) => {
const TAGS: Array<any> = useRecordField('TAGS', []);
return <div className='modal-top-inputs'>
<Input field='is_active' renderOnlyInputField />
<Input>
<InputTags
field='TAGS'
value={TAGS}
model={parentApp + '/Models/Tag'}
targetColumn='id_customer'
sourceColumn='id_tag'
colorColumn='_LOOKUP_COLOR'
showSelect={false}
showTagButtons={true}
editTagsUrl='customers/tags'
onChange={(input: any, value: any) => {
form.changeField(input, value);
}}
onNewTag={(title: string) => {
return { id: -1, name: title, color: '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0') }
}}
></InputTags>
</Input>
<Input field='id_owner' renderOnlyInputField />
<Input field='id_manager' renderOnlyInputField />
<Input field='shared_with' renderOnlyInputField />
</div>
}}
onBeforeSaveRecord={(form: FormMeta, record: any) => {
if (record.tax_id) record.tax_id = record.tax_id.replace(/\s+/g, "");
if (record.vat_id) record.vat_id = record.vat_id.replace(/\s+/g, "");
if (record.company_id) record.company_id = record.company_id.replace(/\s+/g, "");
return record;
}}
title={{fields: ['identifier', 'name'], sub: T.translate('Customer')}}
tabs={{
default: {title: <b>{T.translate('Customer')}</b>, content: () => <TabDefault {...props} />},
calendar: {title: T.translate('Calendar'), content: () => <TabCalendar {...props} />},
}}
{...props}
></Form>;
}
2. Register it
apps/Customers/Loader.tsx
import App from '@hubleto/react-ui/core/App'
import CustomersTableCustomers from "./Components/FC/TableCustomers"
// import CustomersTableActivities from "./Components/TableActivities"
import CustomersFormActivity from "./Components/FC/CustomerFormActivity"
class CustomersApp extends App {
init() {
super.init();
// register react components
globalThis.hubleto.registerReactComponent('CustomersTableCustomers', CustomersTableCustomers);
// globalThis.hubleto.registerReactComponent('CustomersTableActivities', CustomersTableActivities);
globalThis.hubleto.registerReactComponent('CustomersCustomerFormActivity', CustomersFormActivity);
}
}
// register app
globalThis.hubleto.registerApp('Hubleto/App/Community/Customers', new CustomersApp());
The first parameter of registerReactComponent() is the name of the component: the name of the app plus the name of the component (CustomersTableCustomers). The component registers once and can then be used in every Twig view.
3. Use it in a view
The tag is hblreact- followed by the kebab-case name:
apps/Customers/Views/Customers.twig
<hblreact-customers-table-customers
string:tag="table-customers"
int:record-id="{{ viewParams.recordId }}"
string:fulltext-search='{{ viewParams.q }}'
json:column-search='{{ viewParams.search|json_encode }}'
json:filters='{{ viewParams.filters|json_encode }}'
string:form-active-tab-uid='{{ viewParams.tab }}'
></hblreact-customers-table-customers>
Passing props from Twig
Attributes of the tag become props. A prefix tells Hubleto which type to give the value:
| Attribute | Prop | Type |
|---|---|---|
string:tag="table-customers" |
tag |
string |
int:record-id="{{ viewParams.recordId }}" |
recordId |
number |
bool:readonly="true" |
readonly |
boolean |
json:filters='{"fArchive": 1}' |
filters |
parsed JSON |
function:on-click="() => ..." |
onClick |
function |
Prop types. Attribute names are converted from kebab-case to camelCase.
More in Using the hblreact HTML tag and React components.
How the component gets to the browser
webpack.config.js of the project searches the folders vendor/hubleto/erp/apps, vendor/hubleto/enterprise/apps, other vendors and src/apps for apps that have both manifest.yaml and Loader.tsx, and bundles their Loader.tsx into assets/compiled/js/main.js. After you add or change a component, run npm run build.
erp-project: webpack.config.js (excerpt)
function findHubletoAppsInRepository(folder) {
let apps = [];
if (fs.existsSync(folder) && fs.lstatSync(folder).isDirectory()) {
fs.readdirSync(folder).forEach(function(app) {
const stat = fs.statSync(folder + '/' + app);
const manifestFile = folder + '/' + app + '/manifest.yaml';
const loaderEntry = folder + '/' + app + '/Loader';
if (
stat
&& stat.isDirectory()
&& fs.existsSync(manifestFile)
&& fs.existsSync(loaderEntry + '.tsx')
) {
apps.push(loaderEntry);
}
});
}
return apps;
}
