You can create your own custom web components (html elements) processed by PHP.
The API is available in the WebView::$components property.
$app = new Boson\Application();
$app->webview->components; // Access to Web Components API
Creation
For creating your own component, you should use the add() method, passing there the tag name and a reference to the existing component class.
class MyExampleComponent {}
$app = new Boson\Application();
// Tag component name
$tag = 'my-element';
// Component class name
$component = MyExampleComponent::class;
$app->webview->components->add($tag, $component);
$app->webview->html = '<my-element>Example</my-element>';
For more convenient component management, you can use inheritance from the Boson\WebView\Api\WebComponents\WebComponent class.
class MyExampleComponent extends WebComponent
{
// do something
}
Template Rendering
By default, the component does not contain any HTML content (it uses the default body one passed to it). If you want to decorate it somehow or define custom content, you should add the Boson\WebView\Api\WebComponents\Component\HasTemplateInterface interface and implement render() method.
class MyExampleComponent implements HasTemplateInterface
{
public function render(): string
{
return '<b>This is SPARTAAAAAAAAAAaaaAAAA!!!</b>';
}
}
Shadow DOM
In order to switch to shadow house rendering mode, you should implement the Boson\WebView\Api\WebComponents\Component\HasShadowDomInterface interface.
The shadow DOM is similar to the regular renderer, but isolates its behavior from global styles and supports slots.
To include the content of an element inside a rendered template, the <slot /> tag should be used.
class MyExampleComponent implements HasShadowDomInterface
{
public function render(): string
{
return '<b>This is <slot></slot>!!!</b>';
}
}
If you try to render the contents of a <slot /> without a Shadow DOM (using HasTemplateInterface), no data will be received.
When you turn on the Shadow DOM (using HasShadowDomInterface), the contents will be passed to the <slot />.
Lifecycle Callbacks
Creating a new PHP component instance means physically creating the object, including through JavaScript.
const component = document.createElement('my-element');
//
// At this point, a PHP component instance will be created.
// That is, the MyExampleComponent::__construct() will be called.
//
In order to accurately determine that an element is connected to any physical node of the DOM document, the Boson\WebView\Api\WebComponents\Component\HasLifecycleCallbacksInterface interface should be implemented.
class MyExampleComponent implements HasLifecycleCallbacksInterface
{
public function onConnect(): void
{
var_dump('Component is connected to document');
}
public function onDisconnect(): void
{
var_dump('Component is disconnected from document');
}
}
Properties
Each web component supports the ability to create properties and manage them.
let el = document.createElement('my-element');
el.exampleProperty = 42;
By default, this property does not affect the operation of PHP code in any way. However, if you need to track the state of properties, you can use the corresponding Boson\WebView\Api\WebComponents\Component\HasPropertiesInterface interface.
class MyExampleComponent implements HasPropertiesInterface
{
public function onPropertyChanged(string $property, mixed $value): void
{
// ...
}
public static function getPropertyNames(): array
{
// ...
}
}
In particular, if you need to receive information about changes in property exampleProperty, then you should add it to the getPropertyNames() list.
public static function getPropertyNames(): array
{
return [ 'exampleProperty' ];
}
Methods
Each web component supports the ability to create methods and process them.
If you leave the code as is, then when you click on the element, a JS error will be thrown: Uncaught TypeError: this.update is not a function.
To implement a method (for example, " update() "), you should implement the Boson\WebView\Api\WebComponents\Component\HasMethodsInterface interface.
class MyExampleComponent implements HasMethodsInterface
{
public function onMethodCalled(string $method, array $args = []): mixed
{
// ...
}
public static function getMethodNames(): array
{
// ...
}
}
If you want to add support for method update(), then getMethodNames() method must return the corresponding name.
public static function getMethodNames(): array
{
return [ 'update' ];
}
After this, when you click on the element, the onMethodCalled method will be called with the $method argument equal to the name of the called method "update" and empty arguments.
class MyExampleComponent implements HasMethodsInterface
{
public function onMethodCalled(string $method, array $args = []): mixed
{
if ($method !== 'update') {
throw new \BadMethodCallException('Invalid method ' . $method);
}
var_dump($method . ' has been invoked with passed arguments');
// update has been invoked with passed arguments
var_dump($args);
// array(0) {}
return null;
}
public static function getMethodNames(): array
{
return [ 'update' ];
}
}
Don't be afraid to return exceptions from methods, they can be handled correctly.
public function onMethodCalled(string $method, array $args = []): mixed
{
throw new \BadMethodCallException('Invalid method ' . $method);
}
After calling method update() you will get the following JS error:
Uncaught (in promise) Error:
BadMethodCallException: Invalid method update in .../test.php
on line 12
at <anonymous>:1:61
You can also call these methods from the JS directly.
const component = document.createElement('my-element');
try {
let result = await component.update();
} catch(e) {
// Catch PHP Exception
}
Attributes
In addition to methods, each HTML element has attributes. You can subscribe to change, add or remove an attribute by implementing the Boson\WebView\Api\WebComponents\Component\HasAttributesInterface interface.
class MyExampleComponent implements HasAttributesInterface
{
public function onAttributeChanged(
string $attribute,
?string $value,
?string $previous,
): void {
// ...
}
public static function getAttributeNames(): array
{
// ...
}
}
Method getAttributeNames() must return a list of attributes (strings) to be processed.
Method onAttributeChanged() contains a callback that is called when the attribute value changes.
$value
$previous
Meaning
string
null
Attribute has been added
string
string
Attribute value has been changed
null
string
Attribute has been removed
Events
Each web component supports the ability to listen several component events and process them.
To implement an event listener (for example, " click "), you should implement the Boson\WebView\Api\WebComponents\Component\HasEventListenersInterface interface.
class MyExampleComponent implements HasEventListenersInterface
{
public function onEventFired(string $event, array $args = []): void
{
// ...
}
public static function getEventListeners(): array
{
// ...
}
}
If you want to listen click event, then getEventListeners() method must return the corresponding name.
public static function getEventListeners(): array
{
return [
'click' => [],
];
}
If you require any specific information related to an event, then the list of event fields should be passed as an array value.
For example, click event contain readonly properties such as clientX and clientY.
public static function getEventListeners(): array
{
return [
'click' => [
// "clientX" property from "click" event
'clientX',
// "clientY" property from "click" event
'clientY',
],
];
}
If specific PHP array key names are required for these properties, they should be specified as keys.
public static function getEventListeners(): array
{
return [
'click' => [
// "clientX" property from "click" event passed as "x"
'x' => 'clientX',
// "clientX" property from "click" event passed as "y"
'y' => 'clientY',
],
];
}
Reactive Context
The reactive context allows you to modify and retrieve values from a component programmatically.
By default, it is passed to the constructor as the first argument.
use Boson\WebView\Api\WebComponents\ReactiveContext;
use Boson\WebView\WebView;
class MyExampleComponent
{
public function __construct(
private ReactiveContext $ctx,
) {}
}
The context contains methods for working with attributes.
class MyExampleComponent implements HasMethodsInterface
{
public function __construct(
private ReactiveContext $ctx,
) {}
public function update(): void
{
// Add attribute `some="any"` in case of attribute is not defined
if (!$this->ctx->attributes->has('some')) {
$this->ctx->attributes->set('some', 'any');
}
}
/// Delegate "onMethodCalled()" call to the update() method...
}
To work with content, you can use the $content property.
class MyExampleComponent implements HasMethodsInterface
{
public function __construct(
private ReactiveContext $ctx,
) {}
public function update(): void
{
if (!$this->ctx->content->html === '') {
$this->ctx->content->html = '<b>Hello World!</b>';
}
}
/// Delegate "onMethodCalled()" call to the update() method...
}