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
XHP class names must follow the same rules as any other Hack class names:
Letters, numbers and _
are allowed and the name mustn't start with a number.
Historical note:
(applies in FB WWW repository)
Before XHP namespace support (in XHP-Lib v3), XHP class
names could also contain :
(now a namespace separator) and -
(now disallowed
completely). These were translated to __
and _
respectively at runtime (this
is called "name mangling"). For example, <ui:big-table>
would instantiate a
global class named xhp_ui__big_table
.
A custom XHP class needs to do three things:
- use the keywords
xhp class
instead ofclass
- extend
x\element
(\Facebook\XHP\Core\element
) or, rarely, another base class - implement the method
renderAsync
to return an XHP object (x\node
) or the respective method of the chosen base class
use namespace Facebook\XHP\Core as x;
use type Facebook\XHP\HTML\strong;
final xhp class introduction extends x\element {
protected async function renderAsync(): Awaitable<x\node> {
return <strong>Hello!</strong>;
}
}
final xhp class intro_plain_str extends x\primitive {
protected async function stringifyAsync(): Awaitable<string> {
return 'Hello!';
}
}
<<__EntryPoint>>
async function extending_examples_basic_run(): Awaitable<void> {
$xhp = <introduction />;
echo await $xhp->toStringAsync()."\n";
$xhp = <intro_plain_str />;
echo await $xhp->toStringAsync()."\n";
}
Historical note:
(applies in FB WWW repository)
Before XHP namespace support (in XHP-Lib v3), use
class :intro_plain_str
instead of xhp class intro_plain_str
(no xhp
keyword, but requires a :
prefix in the class name).
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.
use namespace Facebook\XHP\Core as x;
final xhp class user_info extends x\element {
attribute int userid @required;
attribute string name = "";
protected async function renderAsync(): Awaitable<x\node> {
return
<x:frag>User with id {$this->:userid} has name {$this->:name}</x:frag>;
}
}
use namespace Facebook\XHP;
<<__EntryPoint>>
async function extending_examples_attributes_run(): Awaitable<void> {
/* HH_FIXME[4314] Missing required attribute is also a typechecker error. */
$uinfo = <user_info />;
// Can't render :user-info for an echo without setting the required userid
// attribute
try {
echo await $uinfo->toStringAsync();
} catch (XHP\AttributeRequiredException $ex) {
\var_dump($ex->getMessage());
}
/* HH_FIXME[4314] This is a typechecker error but not a runtime error. */
$uinfo = <user_info />;
$uinfo->setAttribute('userid', 1000);
$uinfo->setAttribute('name', 'Joel');
echo await $uinfo->toStringAsync();
}
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
An XHP class can inherit all attributes of another XHP class using the following syntax:
// inherit all attributes from the <div> HTML element
attribute :Facebook:XHP:HTML:div;
This is most useful for XHP elements that wrap another XHP element, usually to extend its functionality. In such cases, it should be combined with attribute transfer.
Attribute Transfer
Let's say you have a class that wants to inherit attributes from <div>
. You could do something like this:
use namespace Facebook\XHP\Core as x;
use type Facebook\XHP\HTML\div;
final xhp class ui_my_box extends x\element {
attribute :Facebook:XHP:HTML:div; // inherit attributes from <div>
protected async function renderAsync(): Awaitable<x\node> {
// returning this will ignore any attribute set on this custom object
// They are not transferred automatically when returning the <div>
return <div class="my-box">{$this->getChildren()}</div>;
}
}
<<__EntryPoint>>
async function extending_examples_bad_attribute_transfer_run(
): Awaitable<void> {
$my_box = <ui_my_box title="My box" />;
// This will only bring <div class="my-box"></div> ... title= will be
// ignored.
echo await $my_box->toStringAsync();
}
attribute :Facebook:XHP:HTML:div
causes your class to inherit all <div>
attributes,
however, any attribute set on <ui_my_good_box>
will be lost because the <div>
returned from render
will not automatically
get those attributes.
This can be addressed by using the ...
operator.
use namespace Facebook\XHP\Core as x;
use type Facebook\XHP\HTML\{div, XHPAttributeClobbering_DEPRECATED};
final xhp class ui_my_good_box extends x\element {
attribute :Facebook:XHP:HTML:div; // inherit attributes from <div>
attribute int extra_attr;
protected async function renderAsync(): Awaitable<x\node> {
// returning this will transfer any attribute set on this custom object
return <div id="id1" {...$this} class="class1">{$this->getChildren()}</div>;
}
}
<<__EntryPoint>>
async function extending_examples_good_attribute_transfer_run(
): Awaitable<void> {
$my_box =
<ui_my_good_box
id="id2"
class="class2"
extra_attr={42}
/>;
echo await $my_box->toStringAsync();
}
Now, when <ui_my_good_box>
is rendered, each <div>
attribute will be transferred over.
Observe that extra_attr
, which doesn't exist on <div>
, is not transferred.
Also note that the position of {...$this}
matters—it overrides any
duplicate attributes specified earlier, but attributes specified later override
it.
Children
You can declare the types that your custom class is allowed to have as children
by using the Facebook\XHP\ChildValidation\Validation
trait and implementing the
getChildrenDeclaration()
method.
Historical note:
(applies in FB WWW repository)
Before XHP namespace support (in XHP-Lib v3), a special
children
keyword with a regex-like syntax could be used instead
(examples).
However, XHP-Lib v3 also supports Facebook\XHP\ChildValidation\Validation
, and
it is therefore recommended to use it everywhere.
If you don't use the child validation trait, then your class can have any
children. However, child validation still applies to any XHP objects returned
by your renderAsync()
method that use the trait.
If an element is rendered (toStringAsync()
is called) with children that don't
satisfy the rules in its getChildrenDeclaration()
, an InvalidChildrenException
is thrown. Note that child validation only happens during rendering, no
exception is thrown before that, e.g. when the invalid child is added.
// Conventionally aliased to XHPChild, which makes the children declarations
// easier to read (more fluid).
use namespace Facebook\XHP\{ChildValidation as XHPChild, Core as x};
use type Facebook\XHP\HTML\{body, head, html, li, ul};
xhp class my_br extends x\primitive {
use XHPChild\Validation;
protected static function getChildrenDeclaration(): XHPChild\Constraint {
return XHPChild\empty();
}
protected async function stringifyAsync(): Awaitable<string> {
return "\n";
}
}
xhp class my_ul extends x\element {
use XHPChild\Validation;
protected static function getChildrenDeclaration(): XHPChild\Constraint {
return XHPChild\at_least_one_of(XHPChild\of_type<li>());
}
protected async function renderAsync(): Awaitable<x\node> {
return <ul>{$this->getChildren()}</ul>;
}
}
xhp class my_html extends x\element {
use XHPChild\Validation;
protected static function getChildrenDeclaration(): XHPChild\Constraint {
return XHPChild\sequence(
XHPChild\of_type<head>(),
XHPChild\of_type<body>(),
);
}
protected async function renderAsync(): Awaitable<x\node> {
return <html>{$this->getChildren()}</html>;
}
}
use namespace Facebook\XHP;
use type Facebook\XHP\HTML\{body, head, li, ul};
<<__EntryPoint>>
async function extending_examples_children_run(): Awaitable<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 await $my_br->toStringAsync()."\n";
} catch (XHP\InvalidChildrenException $ex) {
\var_dump($ex->getMessage());
}
$my_ul = <my_ul />;
$my_ul->appendChild(<li />);
$my_ul->appendChild(<li />);
echo await $my_ul->toStringAsync()."\n";
$my_html = <my_html />;
$my_html->appendChild(<head />);
$my_html->appendChild(<body />);
echo await $my_html->toStringAsync()."\n";
}
Interfaces (categories)
XHP classes are encouraged to implement one or more interfaces (usually empty),
conventionally called "categories". Some common ones taken from the HTML
specification are declared in the Facebook\XHP\HTML\Category
namespace.
Using such interfaces makes it possible to implement getChildrenDeclaration()
in other elements without having to manually list all possible child types, some
of which may not even exist yet.
use namespace Facebook\XHP\{
ChildValidation as XHPChild,
Core as x,
HTML\Category,
};
xhp class my_text extends x\element implements Category\Phrase {
use XHPChild\Validation;
protected static function getChildrenDeclaration(): XHPChild\Constraint {
return XHPChild\any_of(
XHPChild\pcdata(),
XHPChild\of_type<Category\Phrase>(),
);
}
protected async function renderAsync(): Awaitable<x\node> {
return <x:frag>{$this->getChildrenOfType<Category\Phrase>()}</x:frag>;
}
}
use type Facebook\XHP\HTML\em;
<<__EntryPoint>>
async function extending_examples_categories_run(): Awaitable<void> {
$my_text = <my_text />;
$my_text->appendChild(<em>"Hello!"</em>); // This is a Category\Phrase
echo await $my_text->toStringAsync();
$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 await $my_text->toStringAsync();
}
Historical note:
(applies in FB WWW repository)
Before XHP namespace support (in XHP-Lib v3), a special
category
keyword could be used instead of an interface
(category %name1, %name2;
).
Async
XHP and async co-exist well together.
As you may have noticed, all rendering methods (renderAsync
, stringifyAsync
)
are declared to return an Awaitable
and can therefore be implemented as async
functions and use await
.
use namespace Facebook\XHP\Core as x;
final xhp class ui_get_status extends x\element {
protected async function renderAsync(): Awaitable<x\node> {
$ch = \curl_init('https://metastatus.com/graph-api');
\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->toStringAsync();
// This can be long, so just show a bit for illustrative purposes
$sub_status = \substr($html, 0, 100);
\var_dump($sub_status);
}
Historical note:
(applies in FB WWW repository)
In XHP-Lib v3, most rendering methods are not async, and
therefore a special \XHPAsync
trait must be used in XHP classes that need to
await
something during rendering.
HTML Helpers
The Facebook\XHP\HTML\XHPHTMLHelpers
trait implements two behaviors:
- Giving each object a unique
id
attribute. - Managing the
class
attribute.
Historical note:
(applies in FB WWW repository)
In XHP-Lib v3, this trait is called \XHPHelpers
.
Unique IDs
XHPHTMLHelpers
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).
use namespace Facebook\XHP\Core as x;
use type Facebook\XHP\HTML\{span, XHPHTMLHelpers};
xhp class my_id extends x\element {
attribute string id;
use XHPHTMLHelpers;
protected async function renderAsync(): Awaitable<x\node> {
return <span id={$this->getID()}>This has a random id</span>;
}
}
<<__EntryPoint>>
async function extending_examples_get_id_run(): Awaitable<void> {
// This will print something like:
// <span id="8b95a23bc0">This has a random id</span>
$xhp = <my_id />;
echo await $xhp->toStringAsync();
}
```.hhvm.expectf
<span id="%s">This has a random id</span>
Class Attribute Management
XHPHTMLHelpers
has two methods to add a class name to the class
attribute of
an XHP object. addClass
takes a string
and appends that string
to the
class
attribute (space-separated); conditionClass
takes a bool
and a string
, and only
appends that string
to the class
attribute if the bool
is true
.
This is best illustrated with a standard HTML element, all of which have a
class
attribute and use the XHPHTMLHelpers
trait, but it works with any
XHP class, as long as it uses the trait and declares the class
attribute
directly or through inheritance.
use type Facebook\XHP\HTML\h1;
function get_header(string $section_name): h1 {
return (<h1 class="initial-cls">{$section_name}</h1>)
->addClass('added-cls')
->conditionClass($section_name === 'Home', 'home-cls');
}
<<__EntryPoint>>
async function run(): Awaitable<void> {
$xhp = get_header('Home');
echo await $xhp->toStringAsync()."\n";
$xhp = get_header('Contact');
echo await $xhp->toStringAsync()."\n";
}