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 |
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 ; }}
When we want to create a custom, reusable button, we will simply compose a
final class :fb:star-button extends :ui:base { attribute :ui:button; protected function compose() { return
When instantiating this component you can use any attribute allowed on