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

sequenceDiagram participant P as Plugin (DisplayPlugin) participant E as UIEngine ($this->ui) participant C as Component (ButtonHui) participant T as Template (ui/button.phtml) P->>E: $this->ui->button() E->>C: new ButtonHui(clone of the plugin, 'ui/button.phtml') E-->>P: component P->>C: setCaption('Save') Note over C: Assigns the variable "caption"<br>to its own copy of the plugin P->>C: fetch() C->>T: Looks for the template and renders it T-->>P: HTML text
  1. Each plugin that extends Festi\Theme\Plugin\DisplayPlugin has the property $this->ui. It is an object of the class UIEngine.
  2. A method of the engine, for example button(), makes a component object.
  3. The component gets a copy (clone) of the plugin as its template object.
  4. A set...() method assigns a variable to this copy. It returns the component, thus you can chain the calls.
  5. 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

flowchart TD A["fetch() of a component<br>ui/button.phtml"] --> Z{"File in the theme directory<br>for this plugin?"} Z -- yes --> Y["Use themes/my/templates/plugins/My/ui/button.phtml"] Z -- no --> B{"File in the plugin<br>templates directory?"} B -- yes --> P["Use plugins/My/templates/ui/button.phtml"] B -- no --> C{"File in the project<br>templates directory?"} C -- yes --> R["Use templates/ui/button.phtml"] C -- no --> D{"File in the theme<br>templates directory?"} D -- yes --> T["Use themes/my/templates/ui/button.phtml"] D -- no --> E["Use the default file of the Theme package"]
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:

  1. Select the directory from this table.
  2. Copy the default template from vendor/festi-team/festi-framework-theme/templates/ui/ to that directory. Keep the file name.
  3. Change the copy.
  4. 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 $component that the default template uses.
  • Keep the CSS classes that JavaScript uses (for example f-db-form in ui/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.