PHP drop-down list - Part 2 (Serialization of elements to HTML)

February 14, 2017 by PHP  

In order to serialize our newly created classes to the required markup, what needs to happen?

Well, currently we don't have any metadata, we've got no way of telling which HTML tags should be rendered for an element, no way to tell which properties represents attributes, nor do we know which values represent child elements.

Bear in mind that while I am designing these classes, in the back of my mind I am thinking about them in context of the bigger picture. So my initial instinct was to create an abstract base class like seen below - a base class that can potentially be used for all HTML elements we might need to implement in future.

abstract class HtmlElement
{
	abstract public function GetAttributes();
	
	abstract public function GetTagName();
	
	public function GetInnerHtml() {
		return null;
	}
	
	public function Render() { ... }
}


But looking at this from an interface segregation principle point of view, sure all HTML elements can have attributes, all HTML elements will have tag names, but all elements won't have inner or text HTML elements (e.g. br, input tags).

But then again, should our classes even be aware of what is essentially serialization information? Wouldn't it be more prudent to create some kind of markup formatter class, dependency injected into our elements ? Lets not get too dogmatic at this point though and return to this question in a later post.

Ok, so all elements can have attributes, all elements will have tag names, lets define our HTML base class as such.

abstract class HtmlElement
{
	abstract public function GetAttributes();
	abstract public function GetTagName();
}


Now some elements will have inner HTML, some will have inner text, others might have nothing, lets create interfaces along those lines.

interface IHtmlInnerHtml
{
	function GetInnerHtml();
}

interface IHtmlInnerText
{
	function GetInnerText();
}


Lets alter our classes accordingly.

class HtmlSelectElement extends HtmlElement
implements IHtmlInnerHtml
{
...	
	public function GetAttributes() {
		return [
			'name' => $this->Name,
			'disabled' => ($this->Disabled) ? 'disabled' : null
		];
	}
	
	public function GetTagName() {
		return 'select';
	}
	
	public function GetInnerHtml() {
		return $this->Children;
	}
}

class HtmlOptionElement extends HtmlElement
implements IHtmlInnerText
{
...
	public function GetAttributes() {
		return [
			'disabled' => ($this->Disabled) ? 'disabled' : null,
			'selected' => ($this->Selected) ? 'selected' : null,
			'value' => $this->Value
		];
	}

	public function GetTagName() {
		return 'option';
	}	
	
	public function GetInnerText() {
		return $this->Text;
	}
}


At this point some of you might complain about the values being assigned to some attributes, e.g. disabled, selected, this is done on purpose with XHTML standards in mind - in XHTML, attribute minification is forbidden, luckily this won't hurt normal HTML.

Ok, lets look at our actual serialization.

abstract class HtmlElement
{
	abstract public function GetAttributes();
	
	abstract public function GetTagName();
	
	public function Render()
	{
		$tagName = $this->GetTagName();
		$html = "<$tagName";
		$html.= $this->getAttributeHtml();
		if ($this->isVoidElement()) {
			$html.=' />';
		} else {
			$html.='>';
			$html.= $this->getChildHtml();
			$html.= $this->getChildText();
			$html.= "</$tagName>";
		}
		return $html;
	}
	
	private function getAttributeHtml() {
		$html = '';
		$attributes = $this->GetAttributes();
		foreach($attributes as $attribute => $value) {
			if ($value !== null) {
			$html.=' '.strtolower($attribute).'="'.htmlspecialchars($value).'"';
			}
		}
		return $html;
	}
	
	private function getChildHtml() {
		$html = '';
		if ($this instanceof IHtmlInnerHtml) {
			$children = $this->GetInnerHtml();
			foreach($children as $child) {
				if ($child instanceof HtmlElement) {
					$html.=$child->Render();
				}
			}
		}
		return $html;
	}
	
	private function getChildText() {
		if ($this instanceof IHtmlInnerText) {
			return htmlentities($this->GetInnerText());
		}
		return '';
	}
	
	private function isVoidElement() {
		return !($this instanceof IHtmlInnerText || $this instanceof IHtmlInnerHtml);
	}
}


In the Render method, we first of all fetch the tag name for the element, next we get all attributes and sanitize (htmlspecialchars) them properly (to prevent attribute text from breaking our markup if inappropriate values were passed).

We then check if the element is a void element (a void element is basically just a contentless element - like input, br, link etc).

In the case of void elements, we simply close the HTML up with a self-closing tag, which is in line with XHTML standards yet again, but not required for HTML(5) elements.

Else (if not void) we check child HTML and text, calling their appropriate Render methods as well thereby building the appropriate HTML tree.

You will notice that the HtmlElement class doesn't carry too much intimate knowledge of its derived classes, it is merely concerned about interfaces that might be implemented, thereby making it easily reusable to whichever element extends it.

$select = new HtmlSelectElement('friends', [
	new HtmlOptionElement('Not Selected'),
	new HtmlOptionElement('Gerhardt Stander'),
	new HtmlOptionElement('Bronwen Murdoch'),
	new HtmlOptionElement('Maree Kleu')
]);

echo $select->Render();


One thing that came up time and time again in this post was the whole XHTML vs HTML issue, I opted to go the XHTML route, since the required markup will work equally as well in plain old HTML.

We do however need to revisit this strategy, wouldn't it be nice if we had more control over our serialization strategy?

Imagine having the ability to render material design elements like md-select by simply changing your rendering metadata? Definitely something we need to revisit.

In part 3 we're going to have a look at maintaining the state of our drop-down list.


Leave a Comment