XHP: Extending
XHP comes with classes for all standard HTML tags, but since these are first-class objects, you can create your own XHP classes for rendering items that are not in the standard HTML specification.
Basics
All XHP class names start with a colon :
and may include other :
as well, as long as they are not adjacent. Note that this is an
exception to the Hack rule where you cannot have :
in class names.
A custom XHP class needs to do two things:
- extend
:x:element
. - implement the method
render
to return an XHP Object viaXHPRoot
.
require __DIR__."/../../../../vendor/hh_autoload.php";
class :introduction extends :x:element {
protected function render(): \XHPRoot {
return <strong>Hello!</strong>;
}
}
class :intro-plain-str extends :x:element {
protected function render(): \XHPRoot {
// Since this function returns an XHPRoot, if you want to return a primitive
// like a string, wrap it around <x:frag>
return <x:frag>Hello!</x:frag>;
}
}
<<__EntryPoint>>
function extending_examples_basic_run(): void {
echo <introduction />;
echo PHP_EOL.PHP_EOL;
echo <intro-plain-str />;
}
<strong>Hello!</strong>
Hello!
Attributes
Syntax
Your custom class may have attributes in a similar form to XML attributes, using the attribute
keyword:
attribute <type> <name> [= default value|@required];
Additionally, multiple declarations can be combined:
attribute
int foo,
string bar @required;
Types
XHP attributes support the following types:
bool
,int
,float
,string
,array
,mixed
(with no coercion; anint
is not coerced intofloat
, for example. You will get anXHPInvalidAttributeException
if you try this).- Hack enums
- XHP-specific enums inline with the attribute in the form of
enum {item, item...}
. All values must be scalar, so they can be converted to strings. These enums are not Hack enums. - Classes or interfaces
- Generic types, with type arguments
The typechecker will raise errors if attributes are incorrect when instantiating an element (e.g., <a href={true} />
; because XHP allows
attributes to be set in other ways (e.g., setAttribute
), not all problems can be caught by the typechecker, and an XHPInvalidAttributeException
will be thrown at runtime instead in those cases.
The ->:
operator can be used to retrieve the value of an attribute.
Required Attributes
You can specify an attribute as required with the @required
declaration after the attribute name. If you try to render the XHP object and
have not set the required attribute, then an XHPAttributeRequiredException
will be thrown.
require __DIR__."/../../../../vendor/hh_autoload.php";
class :user-info extends :x:element {
attribute int userid @required;
attribute string name = "";
protected function render(): \XHPRoot {
return
<x:frag>User with id {$this->:userid} has name {$this->:name}</x:frag>;
}
}
<<__EntryPoint>>
function extending_examples_attributes_run(): void {
$uinfo = <user-info />;
// Can't render :user-info for an echo without setting the required userid
// attribute
try {
echo $uinfo;
} catch (\XHPAttributeRequiredException $ex) {
var_dump($ex->getMessage());
}
$uinfo->setAttribute('userid', 1000);
$uinfo->setAttribute('name', 'Joel');
echo $uinfo;
}
string(166) "Required attribute `userid` was not specified in element `user-info`.
/data/users/joelm/user-documentation/guides/hack/24-XHP/04-extending-examples/required-attributes.php:16"
User with id 1000 has name Joel
Nullable Attributes
For historical reasons, nullable types are inferred instead of explicitly stated. An attribute is nullable if it is not @required
and
does not have a default value. For example:
attribute
string iAmNotNullable @required,
string iAmNotNullableEither = "foo",
string butIAmNullable;
Inheritance
The easiest way to have attributes in your custom XHP class inherit attributes from an existing XHP object is to use the XHPHelpers
trait.
Children
You should declare the types that your custom class is allowed to have as children. You use the children
declaration:
children (:class1, :class2);
children empty; // no children allowed
If you don't explicitly declare using the children
attribute, then your class can have any child. If you try to add a child to an object
that doesn't allow one or doesn't exist in its declaration list, then an XHPInvalidChildrenException
will be thrown.
You can use the standard regex operators *
(zero or more), +
(one or more) |
(this or that) when declaring your children.
require __DIR__."/../../../../vendor/hh_autoload.php";
class :my-br extends :x:element {
children empty; // no children allowed
protected function render(): \XHPRoot {
return <x:frag>PHP_EOL</x:frag>;
}
}
class :my-ul extends :x:element {
children (:li)+; // one or more
protected function render(): \XHPRoot {
return <ul>{$this->getChildren()}</ul>;
}
}
class :my-html extends :x:element {
children (:head, :body);
protected function render(): \XHPRoot {
return <html>{$this->getChildren()}</html>;
}
}
<<__EntryPoint>>
function extending_examples_children_run(): void {
$my_br = <my-br />;
// Even though my-br does not take any children, you can still call the
// appendChild method with no consequences. The consequence will come when
// you try to render the object by something like an echo.
$my_br->appendChild(<ul />);
try {
echo $my_br;
} catch (\XHPInvalidChildrenException $ex) {
var_dump($ex->getMessage());
}
$my_ul = <my-ul />;
$my_ul->appendChild(<li />);
$my_ul->appendChild(<li />);
echo $my_ul;
echo PHP_EOL;
$my_html = <my-html />;
$my_html->appendChild(<head />);
$my_html->appendChild(<body />);
echo $my_html;
}
string(240) "Element `my-br` was rendered with invalid children.
/data/users/joelm/user-documentation/guides/hack/24-XHP/04-extending-examples/children.php:33
Verified 0 children before failing.
Children expected:
empty
Children received:
:ul[%flow]"
<ul><li></li><li></li></ul>
<html><head></head><body></body></html>
Categories
Categories in XHP are like interfaces in object-oriented languages. You can mark your custom class with any number of categories which then
can be referred to from your children. You use the category
declaration. Each category is prefixed with a %
.
category %name1, %name2,...., %$nameN;
The categories are taken from the HTML specification (e.g., %flow
, %phrase
).
require __DIR__."/../../../../vendor/hh_autoload.php";
class :my-text extends :x:element {
category %phrase;
children (pcdata | %phrase); // prefixed colon ommitted purposely on pcdata
protected function render(): \XHPRoot {
return <x:frag>{$this->getChildren('%phrase')}</x:frag>;
}
}
<<__EntryPoint>>
function extending_examples_categories_run(): void {
$my_text = <my-text />;
$my_text->appendChild(<em>"Hello!"</em>); // This is a %phrase
echo $my_text;
$my_text = <my-text />;
$my_text->appendChild("Bye!"); // This is pcdata, not a phrase
// Won't print out "Bye!" because render is only returing %phrase children
echo $my_text;
}
<em>"Hello!"</em>
Async
XHP and async co-exist well together. An async XHP class must do two additional things:
- use the
XHPAsync
trait - implement
asyncRender: Awaitable<XHPRoot>
instead ofrender: XHPRoot
require __DIR__."/../../../../vendor/hh_autoload.php";
class :ui:get-status extends :x:element {
use XHPAsync;
protected async function asyncRender(): Awaitable<\XHPRoot> {
$ch = curl_init('https://developers.facebook.com/status/');
curl_setopt($ch, CURLOPT_USERAGENT, 'hhvm/user-documentation example');
$status = await HH\Asio\curl_exec($ch);
return <x:frag>Status is: {$status}</x:frag>;
}
}
<<__EntryPoint>>
async function extending_examples_async_run(): Awaitable<void> {
$status = <ui:get-status />;
$html = await $status->asyncToString();
// This can be long, so just show a bit for illustrative purposes
$sub_status = substr($html, 0, 100);
var_dump($sub_status);
}
string(100) "Status is: <!DOCTYPE html>
<html lang="en" id="facebook" class="no"
XHP Helpers
The XHPHelpers
trait implements three behaviors:
- Transferring attributes from one object to the object returned from its
render
method. - Giving each object a unique
id
attribute. - Managing the
class
attribute.
Attribute Transfer
Let's say you have a class that wants to inherit attributes from :div
. You could do something like this:
require __DIR__."/../../../../vendor/hh_autoload.php";
class :ui-my-box extends :x:element {
attribute :div; // inherit from attributes from <div>
protected function render(): \XHPRoot {
// returning this will ignore any attribute set on this custom object
// They are not transferred automically when returning the <div>
return <div class="my-box">{$this->getChildren()}</div>;
}
}
<<__EntryPoint>>
function extending_examples_bad_attribute_transfer_run(): void {
$my_box = <ui-my-box title="My box" />;
// This will only bring <div class="my-box"></div> ... title= will be
// ignored.
echo $my_box->toString();
}
<div class="my-box"></div>
The issue above is that any attribute set on :ui:my-custom
will be lost because the <div>
returned from render
will not automatically
get those attributes.
Instead, you should use XHPHelpers
.
require __DIR__."/../../../../vendor/hh_autoload.php";
class :ui-my-good-box extends :x:element {
attribute :div; // inherit from attributes from <div>
// Make sure that attributes are transferred automatically when rendering.
use XHPHelpers;
protected function render(): \XHPRoot {
// returning this will transfer any attribute set on this custom object
return <div class="my-good-box">{$this->getChildren()}</div>;
}
}
<<__EntryPoint>>
function extending_examples_good_attribute_transfer_run(): void {
$my_box = <ui-my-good-box title="My Good box" />;
echo $my_box->toString();
}
<div class="my-good-box" title="My Good box"></div>
Now, when :ui:my-custom
is rendered, each :div
attribute will be transferred over, assuming that it was declared in the render
function. Also, an :ui:my-custom
attribute value that is set will override the same :div
attribute set in the render
function.
Unique IDs
XHPHelpers
has a method getID
that you can call to give your rendered custom XHP object a unique ID that can be referred to in other
parts of your code or UI framework (e.g., CSS).
require __DIR__."/../../../../vendor/hh_autoload.php";
class :my-id extends :x:element {
attribute string id;
use XHPHelpers;
protected function render(): \XHPRoot {
return <span id={$this->getID()}>This has a random id</span>;
}
}
<<__EntryPoint>>
function extending_examples_get_id_run(): void {
// This will print something like:
// <span id="8b95a23bc0">This has a random id</span>
echo <my-id />;
}
<span id="d5f52c9291">This has a random id</span>
Class Attribute Management
XHPHelpers
has two methods to add a class name to the class
attribute of an XHP object. This all assumes that your custom class
declares the class
attribute directly or through inheritance. addClass
takes a string
and appends that string
to the class
attribute; conditionClass
takes a string
and a bool
appends that string
to the class
attribute if the bool
is true
.
require __DIR__."/../../../../vendor/hh_autoload.php";
class :my-cls-adder extends :x:element {
attribute :div;
use XHPHelpers;
protected function render(): \XHPRoot {
$div = <div />;
$div->addClass('my-cls-adder');
$div->appendChild($this->getChildren());
return $div;
}
}
<<__EntryPoint>>
function extending_examples_add_class_run(): void {
echo <my-cls-adder />;
}
<div class="my-cls-adder"></div>