www.hubleto.eu Reddit User Guide Stack Overflow LinkedIn Community Report an issue

Using hblreact HTML tag

The hblreact tag connects Twig views with React components. You write a custom HTML tag in a Twig view. After the page loads, Hubleto finds the tag and renders the React component registered under that name.

apps/Contacts/Views/Contacts.twig
<hblreact-contacts-table-contacts
  string:tag="table-contacts"
  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-contacts-table-contacts>

This renders the component registered as ContactsTableContacts in apps/Contacts/Loader.tsx:

globalThis.hubleto.registerReactComponent('ContactsTableContacts', TableContacts);
Contacts table rendered by the hblreact-contacts-table-contacts tag The Contacts table rendered from the tag above.

From tag name to component

Step Value
Tag in Twig <hblreact-hr-leave-table-leave-requests>
Prefix removed hr-leave-table-leave-requests
Converted with kebabToPascal() HrLeaveTableLeaveRequests
Registered in Loader.tsx registerReactComponent('HrLeaveTableLeaveRequests', TableLeaveRequests)

From the tag name to the component.

Rules for the name:

NOTE The older <app-...> prefix still works but is deprecated. A warning is shown in the console. Use <hblreact-...>.

Passing props

Attributes of the tag become props of the component. Attribute names are converted from kebab-case to camelCase (record-id → recordId). A type prefix tells Hubleto how to convert the value:

Prefix Conversion Example attribute Prop value
string: none string:tag="table-contacts" "table-contacts"
int: parseInt() int:record-id="5" 5
bool: value == 'true' bool:show-add-new-panel-button='true' true
json: JSON.parse() json:filters='{"fDealClosed":1}' {fDealClosed: 1}
function: new Function(value) function:on-change="console.log('changed')" a function
no prefix "true"/"false" → boolean, valid JSON → parsed, otherwise string readonly="true" true

Type prefixes.

TIP Always use a prefix. Without it, a value like "123" or "null" may be parsed as JSON and change its type.

From @hubleto/react-ui/core/Loader.tsx (convertDomToReact)
let attributeName: string = domElement.attributes[i].name.replace(/-([a-z])/g, (_: any, letter: string) => letter.toUpperCase());
let attributeValue: any = domElement.attributes[i].value;

if (attributeName.startsWith('json:')) {
  attributeName = attributeName.replace('json:', '');
  attributeValue = JSON.parse(attributeValue);
} else if (attributeName.startsWith('string:')) {
  attributeName = attributeName.replace('string:', '');
} else if (attributeName.startsWith('int:')) {
  attributeName = attributeName.replace('int:', '');
  attributeValue = parseInt(attributeValue);
} else if (attributeName.startsWith('bool:')) {
  attributeName = attributeName.replace('bool:', '');
  attributeValue = attributeValue == 'true';
}
// ...

Escaping JSON in attributes

Use single quotes around json: attributes, because JSON contains double quotes:

json:filters='{{ viewParams.filters|json_encode }}'

When the JSON may contain HTML or quotes from user data, add |raw only if you are sure the value is safe, or escape it for an HTML attribute:

apps/Dashboards/Views/Dashboard.twig
{% set dashboard = viewParams.dashboard %}

<h1 class="app-main-title"><span>{{ dashboard.title }}</span></h1>

<hblreact-dashboards-dashboard
  int:id-dashboard='{{ dashboard.id }}'
  bool:show-add-new-panel-button='true'
  json:panels='{{ dashboard.PANELS|json_encode|raw }}'
></hblreact-dashboards-dashboard>

Standard props for tables

Table views in the community apps pass the same set of props. They keep the table state in the URL:

Attribute URL example Purpose
string:tag="table-deals" — Unique tag, enables user column configuration.
int:record-id="{{ viewParams.recordId }}" deals/5, deals/add Opens a record (-1 = new record).
string:fulltext-search='{{ viewParams.q }}' deals?q=fiber Text in the search box.
json:column-search='{{ viewParams.search\|json_encode }}' deals?search[title]=x Search in columns.
json:filters='{{ viewParams.filters\|json_encode }}' deals?filters[fDealClosed]=1 Selected filters.
string:form-active-tab-uid='{{ viewParams.tab }}' deals/5?tab=items Active tab of the opened form.
string:view="{{ viewParams.view }}" — Alternative view of the table.

Standard props for table tags.

Nested content

Child nodes of the tag are converted too and passed as children. Normal HTML elements inside the tag become React elements. Nested hblreact-* tags become components.

<hblreact-my-app-panel string:title="{{ translate('Summary') }}">
  <p>{{ translate('This paragraph is passed as children.') }}</p>
</hblreact-my-app-panel>

When the tags are rendered

globalThis.hubleto.renderReactElements() runs after the page loads. It finds all hblreact-* elements that are not rendered yet and mounts a React root in each. A tag rendered once gets the hubleto-react-rendered attribute, so it is not rendered twice. If you insert HTML with hblreact tags later (e.g. after an AJAX call), call globalThis.hubleto.renderReactElements(containerElement).

Every rendered component gets a uid prop (generated if you don't pass one). The component object is available as globalThis.hubleto.reactElements[uid].

Complete example

1. Component: src/apps/MyFirstApp/Components/Greeting.tsx
import React from 'react';

interface GreetingProps {
  name: string,
  unreadMessages: number,
}

const Greeting = (props: GreetingProps) => {
  return <div className="card">
    <div className="card-body">
      Hello {props.name}, you have {props.unreadMessages} unread messages.
    </div>
  </div>;
}

export default Greeting;
2. Registration: src/apps/MyFirstApp/Loader.tsx
import Greeting from './Components/Greeting';

globalThis.hubleto.registerReactComponent('MyFirstAppGreeting', Greeting);
3. Controller: src/apps/MyFirstApp/Controllers/Home.php
public function prepareView(): void
{
  parent::prepareView();
  $this->viewParams['userName'] = $this->authProvider()->getUser()['first_name'] ?? '';
  $this->viewParams['unreadMessages'] = 3;
  $this->setView('@Hubleto:App:Custom:MyFirstApp/Home.twig');
}
4. View: src/apps/MyFirstApp/Views/Home.twig
<hblreact-my-first-app-greeting
  string:name="{{ viewParams.userName }}"
  int:unread-messages="{{ viewParams.unreadMessages }}"
></hblreact-my-first-app-greeting>
5. Build
npm run build