HUI: The PHP UI Components
HUI (HTML UI) is the component library of the Theme package. A HUI component is a PHP object that renders one interface element, for example a button, a form, or a dialog.
This page tells you:
- how HUI works and why to use it
- how to use a component
- where the component templates are and how to replace one
- how to add a component and how to extend a component
The page UI Components lists the methods of each component.
Why HUI and not HTML with JavaScript
The server renders each HUI component. The browser gets complete HTML. This is server-side rendering.
| With HUI | With HTML that you write in each template |
|---|---|
| The HTML of a button or a dialog is in one template. | Each template has its own copy of the HTML. |
| A theme changes all buttons with one template. | A new theme needs a change in each template. |
The CSS classes and the id attributes that jimbo.js needs are always correct. |
A missing class stops the form validation or the dialog. |
| A component has PHP methods. The IDE and the static analysis find errors. | An error in an HTML attribute shows only in the browser. |
| A PHPUnit test can examine the rendered HTML. | Only a browser test can examine the result. |
| The page works when it arrives. No JavaScript must build it. | A script must build the element in the browser. |
Use HUI for each element that it has. Write HTML only for the content that is special to your page.
How HUI works
- Each plugin that extends
Festi\Theme\Plugin\DisplayPluginhas the property$this->ui. It is an object of the classUIEngine. - A method of the engine, for example
button(), makes a component object. - The component gets a copy (
clone) of the plugin as its template object. - A
set...()method assigns a variable to this copy. It returns the component, thus you can chain the calls. fetch()renders the template of the component and returns the HTML as text.
Because each component has its own copy of the plugin:
- A component can use the variables that the plugin assigned before the call of
$this->ui->...(). - The variables of a component do not change the plugin and do not change other components.
Use a component
In a plugin method
public function onDisplayOrder(Response &$response, int $id): bool
{
$content = $this->fetch('order.phtml', ['order' => $this->object->get($id)]);
$response->content = $this->ui->widget()
->setTitle(__('Order #%s', $id))
->setContent($content)
->fetch();
return true;
}
In a template
In a plugin template, $this is the plugin. Thus $this->ui is available.
<?php
/**
* @var OrdersPlugin $this
* @var array $order
*/
echo $this->ui->message()
->setType('info')
->setMessage(__('The order is not paid.'))
->fetch();
echo $this->ui->button()
->setCaption(__('Pay'))
->setUrl($this->getUrl('/orders/%s/pay/', $order['id']))
->setCSS('btn btn-primary')
->fetch();
A component in a component
Give a component object to a component that accepts one. Give the HTML text (fetch()) to a component that accepts content.
use Festi\Theme\UI\IInput;
$comment = $this->ui->input()
->setName('comment')
->setType(IInput::TYPE_TEXTAREA)
->setRequired(true);
$dialog = $this->ui->dialog()
->setTitle(__('Close the order'))
->addField(__('Comment'), $comment)
->setUrl($this->getUrl('/orders/%s/close/', $id))
->fetch();
Methods that each component has
| Method | Use |
|---|---|
fetch() |
Renders the component. Returns the HTML text. |
setID($id), getID() |
The id attribute. |
setCSS($css), getCSS() |
The CSS classes. |
addDataAttribute($name, $value) |
Adds the attribute data-<name>. |
setDataAttributes($attributes), getDataAttributes() |
Sets or gets all data- attributes. |
removeDataAttribute($name) |
Removes one data- attribute. |
The components
| Engine method | Class | Template |
|---|---|---|
button() |
ButtonHui |
ui/button.phtml |
buttons() |
ButtonsHui |
ui/buttons.phtml |
link() |
LinkHui |
ui/link.phtml |
dropdown() |
DropdownHui |
ui/dropdown.phtml |
form() |
FormHui |
ui/form.phtml |
input() |
InputHui |
ui/input.phtml |
inputDate() |
InputDateHui |
ui/input.phtml |
inputDateWeek() |
InputDateWeekHui |
ui/input_date_week.phtml |
textarea() |
TextareaHui |
ui/input_textarea.phtml |
select() |
SelectHui |
ui/input_select.phtml |
checkbox() |
CheckboxHui |
ui/input_checkbox.phtml |
checkboxes() |
CheckboxesHui |
ui/input_checkboxes.phtml |
filter() |
FilterHui |
ui/filter.phtml |
filterPanel() |
FilterPanelHui |
ui/filter_panel.phtml |
dialog() |
DialogHui |
ui/dialog.phtml |
block() |
BlockHui |
ui/block.phtml |
widget() |
WidgetHui |
ui/widget.phtml |
widgetGrid() |
WidgetGridHui |
ui/widget_grid.phtml |
card() |
CardHui |
ui/card.phtml |
tabs() |
TabsHui |
ui/tabs.phtml |
message() |
MessageHui |
ui/message.phtml |
datalist() |
DataListHui |
ui/dl.phtml |
breadcrumbs() |
BreadcrumbsHui |
ui/breadcrumbs.phtml |
guideBlock() |
GuideBlockHui |
ui/guide_block.phtml |
sparks() |
SparksHui |
ui/sparks.phtml |
slider() |
SliderHui |
ui/slider.phtml |
mainNavbar() |
MainNavbarHui |
ui/main_navbar.phtml |
menuItem() |
MenuItemHui |
ui/menu_item.phtml |
The classes are in src/UI/HUI/ of the Theme package. The interfaces are in src/UI/. The default templates are in templates/ui/.
A component template
A component template is a PHP template. It gets these variables:
| Variable | Content |
|---|---|
$component |
The component object. Use its get...() methods. |
$dataAttributes |
The data- attributes as HTML text. |
| the assigned variables | Each set...() method assigns a variable, for example $caption, $url, $css, $id. A variable that no method set does not exist. |
$this |
The copy of the plugin. Thus $this->getUrl() is available. |
Example (ui/button.phtml, simplified):
<?php
/**
* @var \Festi\Theme\UI\IButton $component
* @var string $caption
* @var string $dataAttributes
*/
?>
<button type="<?php echo $type ?? 'submit'; ?>"
class="<?php echo $css ?? 'btn btn-default'; ?>" <?php echo $dataAttributes; ?>>
<?php echo $caption; ?>
</button>
Where HUI looks for a template
| Step | Directory | Use it for |
|---|---|---|
| 1 | Theme templates/plugins/<Name>/ui/ |
A replacement in a theme for the components of one plugin. |
| 2 | plugins/<Name>/templates/ui/ |
The template of a component that only this plugin has, or a replacement for this plugin. |
| 3 | Project templates/ui/ (Core::OPTION_TEMPLATE_PATH) |
A replacement for the project. |
| 4 | Theme templates/ui/ |
A replacement in a theme that the project owns. |
| 5 | Theme package templates/ui/ (ENGINE_TEMPLATE_PATH) |
The default templates. |
Steps 1 to 4 are the lookup of a plugin template. See Templates.
The component keeps the result of the lookup until the request ends. It keeps one result for each plugin and each template name. Thus a replacement in one plugin (step 1 or step 2) has no effect on the components of a different plugin.
The theme object also has an engine ($this->ui in a layout template). Its lookup is: theme templates/, then the default file of the Theme package.
Replace a component template
To change the HTML of a component, without a change to the Theme package or to a theme from a package:
- Select the directory from this table.
- Copy the default template from
vendor/festi-team/festi-framework-theme/templates/ui/to that directory. Keep the file name. - Change the copy.
- Open a page with the component. Make sure that the new HTML shows.
| The replacement is for | Directory |
|---|---|
| All plugins of the project | Project templates/ui/ |
| All plugins, in a theme that the project owns | Theme templates/ui/ |
| One plugin | plugins/<Name>/templates/ui/ |
| One plugin, in a theme that the project owns | Theme templates/plugins/<Name>/ui/ |
project/
|-- templates/
| `-- ui/
| `-- button.phtml replaces the default button template for all plugins
|-- plugins/
| `-- Orders/
| `-- templates/
| `-- ui/
| `-- button.phtml replaces it only for the Orders plugin
`-- themes/
- Keep the variables and the methods of
$componentthat the default template uses. - Keep the CSS classes that JavaScript uses (for example
f-db-forminui/dialog.phtml). - When you update the Theme package, compare your copy with the new default template.
If only the look must change, do not replace the template. Use setCSS() and the CSS file of the project.
Add a component
A new component needs a class and a template. An engine method is optional.
1. The component class
Extend HuiComponent. Each set...() method assigns a variable to the view and returns the component.
plugins/Orders/UI/BadgeHui.php:
<?php
namespace plugin\Orders\UI;
use Festi\Theme\UI\HUI\HuiComponent;
class BadgeHui extends HuiComponent
{
public function setCaption(string $caption): static
{
$this->view->assign('caption', $caption);
return $this;
}
public function setLevel(string $level): static
{
$this->view->assign('level', $level);
return $this;
}
public function getLevel(): string
{
return $this->view->retrieve('level') ?? 'default';
}
}
2. The template
plugins/Orders/templates/ui/badge.phtml:
<?php
/**
* @var \plugin\Orders\UI\BadgeHui $component
* @var string $caption
* @var string $dataAttributes
*/
?>
<span id="<?php echo $component->getID(); ?>"
class="label label-<?php echo $component->getLevel(); ?> <?php echo $component->getCSS(); ?>"
<?php echo $dataAttributes; ?>><?php echo $caption; ?></span>
Use a file name that the Theme package does not have.
3. Use the component
Make the component with the method create() of the engine. Give the class name and the template name.
echo $this->ui->create(BadgeHui::class, 'ui/badge.phtml')
->setCaption(__('New'))
->setLevel('success')
->addDataAttribute('count', '5')
->fetch();
<span id="" class="label label-success " data-count="5">New</span>
create() accepts a class that implements IUIElement. For a different class it throws ThemeException. The component gets a copy of the plugin as its view, the same as a built-in component.
An engine method for the component
If you use the component frequently, give the engine a method for it. Then the call is $this->ui->badge().
Extend UIEngine and add the method. createElement() makes the component with its template name.
plugins/Orders/UI/OrdersUIEngine.php:
<?php
namespace plugin\Orders\UI;
use Festi\Theme\UI\UIEngine;
class OrdersUIEngine extends UIEngine
{
public function badge(): BadgeHui
{
return $this->createElement(BadgeHui::class, 'ui/badge.phtml');
}
}
Override createUIEngine() in the plugin class:
use Festi\Theme\Plugin\DisplayPlugin;
use Festi\Theme\UI\IUIEngine;
use plugin\Orders\UI\OrdersUIEngine;
class OrdersPlugin extends DisplayPlugin
{
protected function createUIEngine(): IUIEngine
{
return new OrdersUIEngine($this, $this->core);
}
}
echo $this->ui->badge()->setCaption(__('New'))->fetch();
All the built-in components stay available in $this->ui. A theme class can do the same: override createUIEngine() in a class that extends DefaultTheme.
A component for many plugins
Each plugin can make the component with $this->ui->create(). The class comes from the autoloader. Only the template needs a location that each plugin can find. The templates directory of a different plugin is not in the lookup. Use one of these methods:
- Put the template into the project
templates/ui/directory. - Let the component render the template from its own directory:
class BadgeHui extends HuiComponent
{
public function fetch(): string
{
return $this->view->fetch(__DIR__.'/../templates/ui/badge.phtml', $this->getArguments());
}
}
For an engine method in many plugins, make a base plugin class with createUIEngine() and extend it in each plugin.
Extend a component
To add a method to a built-in component, extend its class. Make it with create() and the default template name, or return your class from the engine method. The component keeps the default template.
namespace plugin\Orders\UI;
use Festi\Theme\UI\HUI\ButtonHui;
class ConfirmButtonHui extends ButtonHui
{
public function setConfirm(string $question): static
{
return $this->addDataAttribute('confirm', $question);
}
}
use Festi\Theme\UI\IButton;
class OrdersUIEngine extends UIEngine
{
public function button(): IButton
{
return $this->createElement(ConfirmButtonHui::class, 'ui/button.phtml');
}
}
echo $this->ui->button()
->setCaption(__('Remove'))
->setConfirm(__('Remove the order?'))
->fetch();
<button type="submit" class="btn btn-default" data-confirm="Remove the order?"> Remove</button>
A script in a static file of the plugin then uses the attribute data-confirm.
HUI and JavaScript
HUI renders HTML. The behavior in the browser comes from three sources:
| Source | Example |
|---|---|
| The JavaScript of the theme (Bootstrap, the theme components) | A dropdown opens. A tab changes. |
jimbo.js |
A form with the class f-db-form gets validation. A reply of the server shows a message. |
| A small script in the component template | ui/dialog.phtml opens the dialog and fires Jimbo.EVENT_OPEN_DIALOG. |
Thus the pages need a theme that loads these files. See How to Make a Custom Theme.
Load a component after the page
A plugin method can send a component to a page that is open. Use the action Response::ACTION_CONTENT. jimbo.js adds the HTML to the page.
public function onJsonCloseDialog(Response &$response, int $id): bool
{
$response->setAction(Response::ACTION_CONTENT);
$response->content = $this->ui->dialog()
->setTitle(__('Close the order'))
->setUrl($this->getUrl('/orders/%s/close/', $id))
->fetch();
return true;
}
The dialog contains a form that submits into a hidden <iframe>. The method for the URL of setUrl() must be an onAjax method. See Response.
Events
BreadcrumbsHui dispatches BreadcrumbsEvent::EVENT_ON_BEFORE_UPDATE_ITEMS when a plugin sets the items. A listener can change the items. See the README. The other components do not dispatch events.
Rules
- Use a HUI component for each form, input, button, dialog, and message.
- Do not copy the HTML of a component into a plugin template.
- Replace a default component template in the smallest scope that is sufficient: one plugin or the project.
- Give a new component template a file name that the Theme package does not have.
- Keep logic out of the component template. Put it in the component class.
- Use
__()for each text that the user sees.