fewagency / fluent-html
Fluent interface style HTML generator
Installs: 28 654
Dependents: 1
Suggesters: 0
Security: 0
Stars: 0
Watchers: 3
Forks: 2
Open Issues: 1
Requires
- php: >=5.4.0
- illuminate/contracts: ~5.1.8|~5.2
- illuminate/support: ~5.0
Requires (Dev)
- phpdocumentor/phpdocumentor: 2.*
- phpunit/phpunit: ~4.0
README
This package can be used on its own for building complex HTML structures, but most of its power comes when extended for specific purposes.
For example, fewagency/fluent-form,
one of our other packages, helps you create accessible, well-formated, yet customizable HTML forms
extending FluentHtmlElement
.
- Introduction
- Installation
- Usage
- Methods reference
- Development
- Alternatives
- Authors - FEW Agency
- License
Introduction
// Simple example echo FluentHtml::create('div')->withClass('wrapper') ->containingElement('p')->withAttribute('title', 'p1') ->withContent( 'This is a paragraph.', 'It has two sentences.') ->followedByElement('p')->withAttribute('title', 'p2') ->withContent( 'This is another paragraph.' );
Woha, that's a very elaborate way of writing:
<div class="wrapper"> <p title="p1"> This is a paragraph. It has two sentences. </p> <p title="p2">This is another paragraph.</p> </div>
So, then what's the point of it all?
The power of FluentHtml
comes from the ability to add collections of values, closures and conditions to the html
building process.
When the complexity grows you can build elements step by step and and trust the end result to be correct and
well-formatted HTML in every situation.
For example when generating Bootstrap form-groups for an input or input-group with label, validation states, and help-text ...the desired HTML would look like this:
<div class="form-group has-error"> <label class="control-label" for="FluentHtml1">username</label> <div class="input-group"> <span class="input-group-addon"><input type="checkbox" aria-label="Addon checkbox"></span> <input type="text" class="form-control" name="username" value="test@test.com" readonly aria-describedby="FluentHtml2" id="FluentHtml1"> <span class="input-group-btn"><button class="btn btn-default" type="button">Go!</button></span> </div> <div id="FluentHtml2"> <ul class="help-block list-unstyled"> <li class="text-capitalize-first">username is required</li> <li class="text-capitalize-first">username must be a valid email address</li> </ul> <span class="help-block">username is your email address</span> </div> </div>
Generating the above in a PHP template could be a hassle.
With if-statements repeated all over the place, it would be very prone to errors sneaking in.
Using FluentHtml
the code would probably take about the same space,
but it would be a lot more readable,
guaranteed to print correct and well-formatted HTML,
and can be split in manageable and reusable chunks,
like this:
// Bootstrap form-group example // Some basic options $name = 'username'; $value = 'test@test.com'; // If there are errors on the input, show it, if not don't even print the elements $errors[$name] = ["{$name} is required", "{$name} must be a valid email address"]; // If a help text is set, print it with aria-describedby $help_text = "{$name} is your email address"; // We declare some optional input addons that makes the input wrapped in an input-group if set $input_group_prepend = new \Illuminate\Support\HtmlString( '<span class="input-group-addon"><input type="checkbox" aria-label="Addon checkbox"></span>' ); $input_group_append = new \Illuminate\Support\HtmlString( '<span class="input-group-btn"><button class="btn btn-default" type="button">Go!</button></span>' ); // Build the input's help (aria-describedby) element and keep a reference $control_help = FluentHtml::create('div')->onlyDisplayedIfHasContent(); // Add any errors relevant to the input as a list in the help element $control_help->containingElement('ul')->withClass('help-block', 'list-unstyled') ->onlyDisplayedIfHasContent() // Wrap each error message in a list item element ->withContentWrappedIn($errors[$name], 'li', ['class' => 'text-capitalize-first']) // Put the fixed message at the end of the help element ->followedByElement('span', $help_text)->withClass('help-block') ->onlyDisplayedIfHasContent(); // Build the input element and keep a reference $input = FluentHtml::create('input')->withAttribute('type', 'text') ->withClass('form-control') ->withAttribute(['name' => $name, 'value' => $value, 'readonly']) ->withAttribute('aria-describedby', function () use ($control_help) { // Only set the input's aria-describedby attribute if the help element has any content if ($control_help->hasContent()) { return $control_help->getId(); } }); // Build the input-group $input_group = $input->siblingsWrappedInElement(function ($input_group) { // Print the input-group tag only when there's at least one input group addon next to the input return $input_group->getContentCount() > 1 ? 'div' : false; })->withClass('input-group') //Add the input group addons if they are set ->withPrependedContent($input_group_prepend)->withAppendedContent($input_group_append); // Wrap up and print the full result from here echo $input_group // Add a label before the input-group, defaulting to the input name if label not specified ->precededByElement('label', empty($label) ? $name : $label)->withClass('control-label') ->withAttribute('for', function () use ($input) { return $input->getId(); }) // Wrap the label and input-group in a form-group ->siblingsWrappedInElement('div')->withClass('form-group') ->withClass(function () use ($errors, $name) { // Set the validation state class on the form-group if (count($errors[$name])) { return 'has-error'; } }) // Add the help element last in the form-group ->withAppendedContent($control_help);
When to use (and not)
FluentHtml
should be used for those cases where you build
complex html structures with many if-statements.
Stay with your standard html views or templates for all the simple stuff!
If you're making something that should be reusable, consider creating a package of Elements extending
FluentHtmlElement
and publish it!
Naming principles
Public methods available on implementations of FluentHtmlElement
(of which FluentHtml
is one) should be named to hint their return types.
Any method that can, should return the called upon element ($this
) to enable fluent chaining.
Any method name not hinting about returning something else, should be fluent.
Methods not returning the same element
Methods that create, insert and return new FluentHtml
instances relative to the current element ends with ...Element
.
Like containingElement()
, precededByElement()
, and wrappedInElement()
.
Extending packages may add similarly named methods that return specific types of elements,
see fewagency/fluent-form for some examples.
Methods starting with get...
of course returns values of the expected type,
like getParentElement()
returning an implementation of FluentHtmlElement
and getId()
returning a string.
Methods returning booleans start with is...
, has...
, or will...
.
Fluent methods - returning the same element
Method names starting with with...
should always return the current element for fluent chaining.
Like withContent()
and withAttribute()
, and in this category we also find withoutAttribute()
.
Methods adding conditions to an element may have names containing
If
or Unless
, like onlyDisplayedIfHasContent()
.
Methods adding callbacks that will be triggered at certain events start with on...
, before...
, or after...
.
afterInsertion()
is the only event currently supported.
Any other methods not following the above patterns, should return the current element for fluent chaining.
Installation & configuration
composer require fewagency/fluent-html
Optional facades
You may add Laravel facades in the aliases
array of your project's
config/app.php
configuration file:
'FluentHtml' => FewAgency\FluentHtml\Facades\FluentHtml::class, 'HtmlBuilder' => FewAgency\FluentHtml\Facades\HtmlBuilder::class,
Dependencies
This package takes advantage of the Collection
implementation (docs) and the
Arrayable
and
Htmlable
interfaces from
Laravel's Illuminate components.
Internally FluentHtmlElement
depends on HtmlBuilder
to render html elements as strings
and HtmlIdRegistrar
to keep track of used element ids so they can be kept unique.
Usage
Collections as method input
Most methods accept arrays or collections (and other implementations of
Arrayable
)
as input parameters.
A value may sometimes also be a nested collection, in which case the whole collection is recursively flattened
(with preserved associative keys).
When flattening a collection, any duplicate associative keys will be merged over by those appearing later in the
collection.
Values with numeric keys are always appended.
// Example with collections and conditions echo FluentHtml::create('input')->withAttribute([ 'name' => 'a', 'disabled' => true, ['name' => 'b', 'disabled' => false, 'value' => 'B'], ['disabled' => true], 'autofocus' ]);
<input name="b" disabled value="B" autofocus>
Conditional output
String keys are usually displayed instead of their value if their corresponding evaluated value is truthy. This makes it possible to conditionally show or hide html contents and element attributes, depending on their value being true or false.
Closures as method input
Most values can be PHP closures in which case their evaluation is
deferred as long as possible, usually until the object is rendered as a string.
When a closure is evaluated it may return a value, boolean, arrayable, or even another closure, which in turn will be
evaluated and merged into the collection of its context.
All closures will receive the current FluentHtmlElement
instance as their first parameter,
this can be used for pretty advanced conditionals.
// Example with closures and conditions $show_div = $show_2nd_sentence = $p2_title = false; echo FluentHtml::create(function () use ($show_div) { // The element name itself is generated by this closure with a condition // If no element name is returned, no element tag will be printed, // but the element contents will if ($show_div) { return 'div'; } }) ->withClass('wrapper') ->containingElement('p')->withAttribute('title', function () { return 'p1'; })->withContent([ 'This is a paragraph.', 'It could have two sentences.' => $show_2nd_sentence ]) ->followedByElement('p')->withAttribute('title', $p2_title) ->withContent(function (FluentHtml $paragraph) { // The parameter is the current FluentHtml element, // so we can check its properties or related elements' properties if ($paragraph->getParentElement()->getContentCount() > 1) { return 'This is another paragraph.'; } });
<p title="p1">This is a paragraph.</p> <p>This is another paragraph.</p>
Multiple attribute values
If an html attribute is supplied more than one value, they will be concatenated into a comma-separated list.
// Example with concatenated attribute values echo FluentHtml::create('meta')->withAttribute('name', 'keywords') ->withAttribute('content', ['list', 'of', 'keywords']);
<meta name="keywords" content="list,of,keywords">
Usage with Blade templates
Echoing the result in a template is easy because the string conversion as well as toHtml()
of a
FluentHtmlElement
implementation always returns the full HTML structure
from the top element down.
Both the escaping and the non-escaping Blade echo statements will render the full html tree:
{{ FluentHtml::create('div')->containingElement('p')->withContent('Text') }}
{!! FluentHtml::create('div')->containingElement('p')->withContent('Text') !!}
Blade sections are available to yield as content using Blade's $__env
variable:
{{ FluentHtml::create('div')->withRawContent($__env->yieldContent('section_name','Default content')) }}
Within Blade echo statements newlines as well as php comments are accepted, making it easy to enhance readability:
{{
FluentHtml::create('div') // A div
// Add a paragraph
->containingElement('p')
// Paragraph contents
->withContent('Text')
}}
Methods reference
- Methods creating new elements
FluentHtml::create()
new FluentHtml()
- Methods modifying and returning the same element
- Methods returning a new element relative to the current
- Methods for structure navigation
- Finding ancestors
- Element state methods
Methods creating new elements
The FluentHtml
constructor and the static create()
function share the same signature:
FluentHtml::create( $html_element_name = null, $tag_contents = [], $tag_attributes = [] )
Each FluentHtmlElement
instance can be the start of a new chain of fluent method calls
for modifying and adding more elements relative the previous.
This is also true for the returned FluentHtml
of
methods returning a new element relative to the current.
@param string|callable|null $html_element_name @param string|Htmlable|array|Arrayable $tag_contents @param array|Arrayable $tag_attributes
A falsy $html_element_name
makes the element render only its contents.
The $html_element_name
may also be a callable in which case it's evaluated just before rendering
and that callable's return value will be used as the element name.
The optional $tag_contents
will be inserted in the same manner as
withContent()
.
The optional $tag_attributes
will be inserted in the same manner as
withAttribute()
.
Methods modifying and returning the same element
These methods can be chained to modify the current element step by step.
Adding content
These methods put html content inside the current element.
withContent($html_contents)
Add html content after existing content in the current element. Accepts multiple arguments that can be:
- strings (will be escaped)
- objects implementing
Htmlable
- another instance of
FluentHtmlElement
- arrayables containing any types from this list (including other arrayables)
- callables returning any types from this list (including other callables)
@param string|Htmlable|callable|array|Arrayable $html_contents,...
Alias for withAppendedContent()
withPrependedContent($html_contents)
Add html content before existing content in the current element.
Same parameter options as withContent()
.
withRawHtmlContent($raw_html_content)
Add a raw string of html content last within this element.
withContentWrappedIn($html_contents, $wrapping_html_element_name, $wrapping_tag_attributes = [])
Add html contents last within this element, with each inserted new content wrapped in an element.
@param string|Htmlable|callable|array|Arrayable $html_contents,... @param string|callable $wrapping_html_element_name @param array|Arrayable $wrapping_tag_attributes
withDefaultContent($html_contents)
Set html content to display as default if no other content is set.
Same parameter options as withContent()
.
Adding siblings
These methods put html siblings next to the current element
and have the same parameter options as withContent()
or just one string for the raw html ones.
withPrecedingSibling($html_siblings)
Add html outside and before this element in the tree.
withFollowingSibling($html_siblings)
Add html outside and after this element in the tree.
withPrecedingRawHtml($raw_html_sibling)
Add a raw string of html outside and before this element in the tree.
withFollowingRawHtml($raw_html_sibling)
Add a raw string of html outside and after this element in the tree.
Manipulating attributes
withAttribute($attributes, $value = true)
Add one or more named attributes with value to the current element.
Overrides any set attributes with same name.
Attributes evaluating to falsy will be unset.
Use withId()
and withClass()
instead for those attributes.
The first parameter in the simplest form is the attribute name as string, but it can also be an array of attribute names and values, or a callable returning such an array.
If the first parameter is an attribute name string, its value is taken from the second parameter. Boolean values makes the attribute name print only if truthy. The attribute is omitted from print if the value is falsy.
withoutAttribute($attributes)
Remove one or more named attributes from the current element.
@param string|array|Arrayable $attributes,...
withId($desired_id)
Set the id attribute on the current element. Will check if the desired id is already taken and if so set another unique id.
withClass($classes)
Add one or more class names to the current element.
@param string|callable|array|Arrayable $classes,...
withoutClass($classes)
Remove one or more class names from the current element.
@param string|array|Arrayable $classes,...
Other fluent methods
withHtmlElementName($html_element_name)
Set the html element name. The parameter can be a string or a callable returning a string.
onlyDisplayedIf($condition)
Will not display current element if any added condition evaluates to false. The parameter may be a boolean or a callable returning a boolean.
onlyDisplayedIfHasContent()
Will not display current element if it has no content. Useful to get rid of empty lists or other containers.
afterInsertion($callback)
Register callbacks to run after the current element is placed in an element tree.
The closure will receive the current FluentHtml instance as the first parameter, as with other closures as input.
It's usually a good idea to check for some condition on the element before manipulating it within the closure, because an element may be inserted into other elements many times throughout its lifetime.
Methods returning a new element relative to the current
These methods creates a new element, adds it relative the current element and returns that new element. This makes any chained methods switch to operate on the new element after the call.
Most of these methods share signature with the constructor.
@param string|callable|null $html_element_name @param string|Htmlable|array|Arrayable $tag_contents @param array|Arrayable $tag_attributes
Inserting within the current element
containingElement($html_element_name = null, $tag_contents = [], $tag_attributes = [])
Adds a new element last among this element's children and returns the new element.
Alias for endingWithElement()
startingWithElement($html_element_name, $tag_contents = [], $tag_attributes = [])
Adds a new element first among this element's children and returns the new element.
Inserting next to the current element
followedByElement($html_element_name, $tag_contents = [], $tag_attributes = [])
Adds a new element just after this element and returns the new element.
precededByElement($html_element_name, $tag_contents = [], $tag_attributes = [])
Adds a new element just before this element and returns the new element.
Wrapping around the current element
The wrapping methods have no parameter for setting contents. This is of course because the content is already there to be wrapped.
wrappedInElement($html_element_name = null, $tag_attributes = [])
Wraps only this element in a new element and returns the new element.
siblingsWrappedInElement($html_element_name, $tag_attributes = [])
Wraps this element together with its siblings in a new element and returns the new element.
Methods for structure navigation
These methods returns a found existing element or a new empty element put in the requested position. Useful for referencing other elements in the current tree, especially within closures as input but can also be used in a method chain to switch elements.
getParentElement()
Get or generate the closest parent for this element, even if it's unnamed.
getSiblingsCommonParent()
Get the closest named parent element or an unnamed parent if none found. This is the common parent of this element and its siblings as rendered in html.
getRootElement()
Get the root element of this element's tree.
getAncestorInstanceOf($type)
Get the closest ancestor in the tree that is an instance of
the supplied type.
Remember to supply the fully qualified class name.
Returns null
if none found.
Element state methods
These methods are used to query the properties and states of an element. Useful for conditionals within closures as input.
getId($desired_id = null)
Get the element's id string if set, or generate a new id.
The optional parameter can be used to try a desired id string which will be used if not already taken,
just like withId()
.
hasClass($class)
Find out if this element will have a specified class when rendered. The parameter should be a string and the return value is a boolean.
getAttribute($attribute)
Get the evaluated value of a named attribute. If an attribute has been set with a callable, it will be evaluated before returning.
The parameter should be a string and the returned value is usually a string or a boolean,
but can be a collection if the attribute has been set with an arrayable.
The returned value is null
if the attribute hasn't been set.
hasContent()
Find out if this element will contain any content when rendered.
Will return true
if this element has content to render after evaluation.
getContentCount()
Get the number of content pieces in this element. Empty contents are counted too.
willRenderInHtml()
Find out if this element is set to render.
Returns false
if any condition for rendering the element fails.
Development
Testing
vendor/bin/phpunit
Alternatives
https://github.com/spatie/html-element has another interesting way of generating html.
Authors
I, Björn Nilsved, created this package while working at FEW.
License
The Fluent HTML builder is open-sourced software licensed under the MIT license