Code Before the Horse

A blog about XHP, UI, and horses

5 notes &

Building a Good UI Framework with XHP

This is the article I wanted to write ever since I started this blog. XHP is a really powerful tool, but like any tool you need to know how to use it for it to be really effective. Facebook has built a very powerful UI framework on top of XHP, but we had to change the way we think about object patterns to do it. I’ll get into that in a bit, but first I’m going to jump right into the most important feature of Facebook’s UI library: attribute forwarding.

Here’s the problem, when you make your own XHP component, the element you return in your render method is exactly what will be sent down the wire. That means if you want to apply IDs, classes, onclicks, or any other attributes to an individual instance, you’ll have to account for that in your class and set it on the returned node. Here’s what I mean by that.

class :ui:div extends :x:element {
  attribute :div;
  protected function render() {
    $root = <div />;
    $root->setAttributes(array(
      'id' => $this->getAttribute('id'),
      'class' => $this->getAttribute('class'),
      ...
    );
    return $root;
  }
}

That’s not a good pattern, so a good UI framework should do this for you. At Facebook, we call our UI core element :ui:base, and this is how we forward attributes:

First, we set :ui:base::render() to be final and instead create an abstract method compose() that all extensions will need to override. Then we can get the attributes set on the instance being rendered and compare it with the attribute declaration on the returned node from compose(). We loop through the set attributes and forward them onto returned node (if valid). So our class ends up looking something like this:

abstract class :ui:base extends :x:element {

  abstract protected function compose();

  final public function addClass($class) {
    $this->setAttribute(
      'class',
      trim($this->getAttribute('class').' '.$class)
    );
    return $this;
  }

  final protected function render() {
    $root = $this->compose();
    if ($root === null) {
      return <x:frag />;
    }
    if (:x:base::$ENABLE_VALIDATION) {
      if (!$root instanceof :xhp:html-element
          && !$root instanceof :ui:base) {
        throw new XHPClassException(
          $this,
          'compose() must return an xhp:html-element'.
          ' or ui:base instance.'
        );
      }
    }

    // Get all attributes declared on this instance
    $attributes = $this->getAttributes();
    // Get all allowed attributes on the node returned
    // from compose()
    $declared = $root->__xhpAttributeDeclaration();

    // Transfer any classes that were added inline over
    // to the root node.
    if (array_key_exists('class', $attributes)) {
      $attributes['class'] && $root->addClass($attributes['class']);
      unset($attributes['class']);
    }

    // Always forward data and aria attributes
    $html5Attributes = array('data-' => true, 'aria-' => true);

    // Transfer all valid attributes to $root
    foreach ($attributes as $attribute => $value) {
      if (isset($declared[$attribute]) ||
          isset($html5Attributes[substr($attribute, 0, 5)])) {
        try {
          $root->setAttribute($attribute, $value);
        } catch (XHPInvalidAttributeException $e) {
          // This happens when the attribute defined on
          // your instance has a different definition
          // than the one you've returned. This usually
          // happens when you've defined different enum values.
          // When you turn off validation (like on prod) these
          // errors will not be thrown, so you should
          // fix your APIs to use different attributes.
          error_log(
            'Attribute name collision for '.$attribute.
            ' in :ui:base::render() when transferring'.
            ' attributes to a returned node. source: '.
            $this->source."\nException: ".$e->getMessage()
          );
        }
      }
    }

    return $root;
  }

}

Now when creating custom elements, we don’t need to worry about setting classes, ids, or any other common attributes. It’s all done for us automatically. This doesn’t just happen for HTML attributes either, it happens for any attribute that’s valid. So this means that if you inherit attributes from another custom component and then return that element from compose(), you won’t need to set any of the custom attributes either. Which brings me to what I mentioned early about changing the way we think about object patterns…

Compose; don’t extend.

At Facebook, nearly all of our custom XHP components extend :ui:base directly, and are then never extended further. To build on other elements we simply return them from compose() and XHP recursively renders them down for us. I’ll use a simple button component as an example.

final class :ui:button extends :ui:base {
  attribute
    :button,
    enum {'small', 'large'} size = 'small',
    enum {'default', 'blue', 'green'} color = 'default';

  protected function compose() {
    $this->addClass($this->getAttribute('size'));
    $this->addClass($this->getAttribute('color'));
    return <button>{$this->getChildren()}</button>;
  }
}

When we want to create a custom, reusable button, we will simply compose a <ui:button> instance. We won’t even need to redefine our custom attributes.

final class :fb:star-button extends :ui:base {
  attribute :ui:button;
  protected function compose() {
    return
      <ui:button>
        <img src="/images/star.png" />
      </ui:button>;
  }
}

When instantiating this component you can use any attribute allowed on <ui:button> and <button> since they will be recursively forwarded down through rendering. It’s important to point out here that this isn’t multiple inheritance. What’s actually happening is the attribute API is being copied to the new component. It’s merely a short-hand for re-declaring attributes.

What almost invariably follows when I explain this to new engineers are questions about why we don’t just extend XHP elements. Well, for that I like to talk about a little Facebook history. Pre-2009 Facebook had a UI library that used basic OO design principles. We had a UIBase object, which was extended by UIContentArea, then you had your base-level component extensions like UIBox. Pages would render them by making components like UIHomeBox, and then subsections in pages need to extend those for custom pieces like UIHomeRemindersBox. This was all to render a box with a list of birthdays and game requests. Each component called parent::render() and then would either augment their parent’s returned component or wrap it. This meant there was either massive <div> wrapping or there were tons of protected methods that could be overwritten by children in unpredictable ways. If you wanted to make changes to core components, good luck making sure it didn’t break anything down the chain.

As it turns out we don’t really need all that object extension, all we really want to inherit is the public API (i.e. attributes). By preventing extension, we can silo all functionality of a component to its render() method. The only API we give are public attributes and children and so long as you don’t touch those you can make as many changes to your XHP component and be confident it won’t break anything. In the end, this treats XHP more as a templating architecture than anything, but I’ve found that this is the best way to abstract generating HTML markup.

Filed under xhp

  1. codebeforethehorse posted this