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 to 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:

  • extends :x:element.
  • implement the render() method to return an XHP Object via XHPRoot.
<?hh

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>;
  }
}

function extending_examples_basic_run(): void {
  echo <introduction />;
  echo PHP_EOL . PHP_EOL;
  echo <intro-plain-str />;
}

extending_examples_basic_run();
Output
<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 ... an int is not coerced into float, for example. You will get an XHPInvalidAttributeException if you try this).
  • Hack enum names
  • 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.
  • Class or interface names
  • 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, and Hack will understand it's type.

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.

<?hh

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>;
  }
}

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;
}

extending_examples_attributes_run();
Output
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.

<?hh

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 more 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>;
  }
}

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;
}

extending_examples_children_run();
Output
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).

<?hh

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>;
  }
}

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;
}

extending_examples_categories_run();
Output
<em>&quot;Hello!&quot;</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 of render(): XHPRoot
<?hh

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>;
  }
}

function extending_examples_async_run(): void {
  $status = <ui:get-status />;
  // This can be long, so just show a bit for illustrative purposes
  $sub_status = substr($status, 0, 100);
  var_dump($sub_status);
}

extending_examples_async_run();
Output
string(100) "Status is: &lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot; id=&quot;facebook&quot; class=&quot;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:

<?hh

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>;
  }
}

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();
}

extending_examples_bad_attribute_transfer_run();
Output
<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.

<?hh

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>;
  }
}

function extending_examples_good_attribute_transfer_run(): void {
  $my_box = <ui-my-good-box title="My Good box" />;
  echo $my_box->toString();
}

extending_examples_good_attribute_transfer_run();
Output
<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).

<?hh

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>;
  }
}

function extending_examples_get_id_run(): void {
  // This will print something like:
  // <span id="8b95a23bc0">This has a random id</span>
  echo <my-id />;
}

extending_examples_get_id_run();
Output
<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.

<?hh

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;
  }
}

function extending_examples_add_class_run(): void {
  echo <my-cls-adder />;
}

extending_examples_add_class_run();
Output
<div class="my-cls-adder"></div>