Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
405 changes: 158 additions & 247 deletions README.md

Large diffs are not rendered by default.

201 changes: 201 additions & 0 deletions docs/migrating-from-04x-050000.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# Migrating from 4.x to 5.0

- New
- [`TextArea`](#textarea) form control
- [`Select`](#select) adds `multiple()` mode and `inputProps()` methods; `SelectType` enum updated accordingly
- [Form controls preserve help IDs](#form-controls-preserve-help-ids) when warning is present
- [Text controls](#text-controls-containing-div) add a containing `div` element
- [All form controls have](#normalized-form-control-property-methods) `containerProps()`, `labelProps()`, and `inputProps()`; `FieldSet::labelProps()` targets the `legend` element
- `id()` is available on `TextInput`, `EmailInput`, `Password`, `TextArea`, and `Select`; allowing you to override the default `id` based on `name`

- Updated or fixed:
- [`PropertyInterface`](https://github.com/8fold/php-html-builder/blob/main/src/PropertyInterface.php) extends PHP's `Stringable`; add `public function __toString(): string` alongside `prop()` and `props()`.
- `Select` [`radio()` and `checkbox()` IDs](#radio-and-checkbox-ids) use `<control-id>-option-<zero-based-index>` instead of `<name>-<option-value>`
- `warningMessage` accessibility improved for each form control; `Fieldset` renders one warning for the group and [does not inspect child elements](#fieldset-does-not-inspect-children)
- Deprecated:
- [`wrapperProps()` on form controls](#wrapper-on-form-controls)
- Deleted: None

## New

### `TextArea`

Escaping content remains caller-owned.

`TextArea` is a new form control, and its value is rendered as text content. It renders the supplied value without escaping, decoding, or double encoding.

```php
use Eightfold\HtmlBuilder\Forms\TextArea;

TextArea::create(
$label,
$name,
$value = '', // caller-sanitized content, not a value attribute in <textarea>
$warningMessage = ''
);

// Example of preparing a value; not a component capability.
// To display literal entity syntax such as `&amp;`, escape that text too
// (for example, supply `&amp;amp;`).
$value = "\nFirst line & second line\n";
$escapedValue = htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

TextArea::create('Notes', 'notes', $escapedValue, 'Add more detail.')
->inputProps('rows 4', 'required required')
->labelProps('class notes-label');
```

Values are element text, not a `value` attribute. Spaces, tabs, and leading, internal, and trailing LF newlines are preserved: rendering adds one extra leading newline that HTML consumes.

Native HTML parsing normalizes CR and CRLF line endings to LF; browsers apply their native textarea submission rules.

- `inputProps()` targets the textarea and supports native `rows`, `required`, `readonly`, and `disabled` attributes.
- Generated `id` and `name` override conflicting input properties; `id()` also updates the label and warning associations.

### Select

`Select` defaults to a single dropdown.

```php
<select name="sample">
<option value="1">1</option>
<option value="2">2</option>
</select>
```

Choose different select controls by calling the method representing the desired type:

- `radio()` for single select, with exposed list;
- `checkbox()` for multiple select, with exposed list, which will append `[]` to the name if the supplied name doesn’t include it; and
- `multiple()` for multiple select box, requires keyboard interaction and including `[]` with the name (recommend checkbox instead).

`checked` and `multiple` passed to `inputProps()` are ignored; use the selection argument and mode methods instead.

```php
Select::create(
'Label text',
'name',
['a' => 'A', 'b' => 'B', 'c' => 'C'],
['a', 'c'] // selected, checked
)->checkbox();
```

See [`Select` attributes and selection](#select-attributes-and-selection) for native attribute targets and examples.

### Form controls preserve help IDs

`TextInput`, `EmailInput`, `Password`, `TextArea`, and all `Select` modes preserve help IDs supplied through `inputProps('aria-describedby help-id')` when adding a warning.

With a warning, the control sets `aria-invalid="true"` and appends `<control-id>-warning` to `aria-describedby`, removing duplicate references while preserving their order.

Use `content()` to supply the referenced help elements. Overriding the control's `id()` also changes its generated warning ID.

`tests/ControlGuidanceTest.php` checks these relationships across control types. Assertion failures identify the relationship, control type and ID, and rendered HTML, alongside PHPUnit's named case and expected/actual diff.

### Text controls containing `div`

An outer ‘<div>’ encloses the text-entry controls.

In 4.4.0, `TextInput`, `EmailInput`, and `Password` rendered their `label` and `input` as siblings; no container.

Update CSS child/sibling selectors, flex or grid layouts, and JavaScript traversal that assumed the old structure.

```html
<!-- 4.4.0 -->
<label for="email">Email</label>
<input id="email" name="email" type="text">

<!-- 5.0 -->
<div class="field">
<label for="email">Email</label>
<input id="email" name="email" type="text">
</div>
```

### Normalized form control property methods

Form control inner elements have property methods:

- `containerProps()` targets the outer container,
- `labelProps()` targets the label (or a Select group's legend), and
- `inputProps()` targets the input, textarea, or select.

For example:
```php
use Eightfold\HtmlBuilder\Forms\TextInput;

TextInput::create('Email', 'email')
->containerProps('class field')
->labelProps('class field-label')
->inputProps('class form-control');
```

Avoid using `inputProps()` or `labelProps()` to replace generated properties such as:

- `type`,
- `id`,
- `name`,
- `value`, or
- the label's `for` association.

For example, 4.4.0 allowed `TextInput::create('Email', 'email')->inputProps('type email')` to change its generated type; in 5.0, use `EmailInput` instead. Use constructor
arguments, `id()`, or the component/mode that matches the control you need.

Setting a custom ID changes the DOM ID and its generated label and warning associations, but does not change the submitted field name:

```php
TextInput::create('Email', 'account_email')->id('email-field');
// The input has id="email-field" and name="account_email".
```

Output (human-readable):

```html
<div class="field">
<label class="field-label" for="email">Email</label>
<input id="email" class="form-control" name="email" type="text">
</div>
```

## Updated or fixed

### Radio and checkbox IDs

In 4.4, `radio` and `checkbox` inputs would repeat the same name as their `id` attribute, which is invalid HTML; only one `id` with a given value per `html` element.

Now, option `a` in a group named `categories` changes from `categories-a` to `categories-option-0`.

Generated labels follow the new IDs automatically; update external labels, CSS selectors, and JavaScript that reference old IDs.

Calling `id()` changes the group's member-ID prefix, not its submitted `name`.

### `Fieldset` does not inspect child elements

```php
Fieldset::create($label, $name, $warningMessage = 'Warning message')
```

Will render one group warning with the ID `<name>-warning`.

String warnings are wrapped in a paragraph and retain caller-supplied markup.

`PropertyInterface` warnings retain their element, content, and other properties; the generated ID is applied to a local clone, leaving the caller's warning object unchanged. Repeated rendering is stable, and the same warning object can be reused across components with independent IDs.

**`Fieldset` does not inspect or rewrite arbitrary children passed to `elements()`**:Callers remain responsible for setting `aria-invalid` and `aria-describedby` on
member controls, including references to the group warning when appropriate.

## Deprecated

### `wrapperProps()` on form controls

`wrapperProps()` remains callable as a forwarding alias throughout 5.x. It
continues to set the same container properties, but is deprecated in favor of
`containerProps()` and emits `E_USER_DEPRECATED`. The native `#[Deprecated]`
attribute is supported by PHP 8.4 and later; removal is deferred until at
least 6.0. See [Migrating from wrapperProps](#migrating-from-wrapperprops).

In 5.0, use `containerProps()` instead of `wrapperProps()` on all form
components. This is a rename, not a change in property handling or target
elements. `wrapperProps()` remains a forwarding alias throughout 5.x and is
marked with PHP's native `#[Deprecated]` attribute (supported since PHP 8.4).
Calling it emits `E_USER_DEPRECATED`; removal is deferred until at least 6.0.
76 changes: 76 additions & 0 deletions docs/migrating-from-050000-050100.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Migrating from 5.0 to 5.1

While not a major release, it could have been, but the potential breaking change should only impact some static analysis tools - not runtimes - and has at least two solutions that have existed for multiple versions and many of us already have in place.

Otherwise, this is a quality of life improvement release.

- [New](#new)
- [`EmptyElement`](#emptyelement) component
- [Updated](#updated)
- (could cause frustration) [Static analysis](#static-analysis) may throw if the element name in an `Element` or `EmptyElement` static creation is not in the HTML specification, is not declared empty by the HTML specification, or both.
- (see previous) Removed all [`ignoreErrors`](https://phpstan.org/user-guide/ignoring-errors#ignoring-in-configuration-file) from the PHPStan configuration.
- (see previous) Increased the [PHPStan](https://phpstan.org) level for HTML Builder to 10.
- Expanded style and static analysis coverage to include `tests/` and `bin/` directories; not just `src/`.
- Form controls, `Favicons`, and `Document` use `EmptyElement` where applicable
- Deprecated: None
- Deleted: None

## New

### `EmptyElement`

The first quality of life improvement is introducing an explicit way to create empty elements.

The examples in the `EmptyElement` section in the [README](../README.md#emptyelement) demonstrates the improvement.

In short:

```php
use Eightfold\HtmlBuilder\Element;
use Eightfold\HtmlBuilder\EmptyElement;

Element::img()->omitEndTag()->prop('id cat-picture', 'src /path/to/image.png');

// becomes
EmptyElement::img('id cat-picture', 'src /path/to/image.png');
```

Outputs:

```html
<img id="cat-picture" src="/path/to/image.png">
```

## Updated

### Static analysis

The static analysis error that’s been with PHP HTML Builder from the beginning was “Call to undefined static method”.

The error could be silenced, and limited to only ignoring PHP HTML Builder, in the PHPStan configuration (the first workaround):

```yaml
ignoreErrors:
- '#Call to an undefined static method Eightfold\\HtmlBuilder\\Element::(.*)#'
```

We introduced the ‘create()’ named constructor, which gave a specific pattern for creating custom components and allowed users to avoid ignoring the static analysis error (the second workaround):

```php
Element::create('p', 'Hello, World!');
```

PHP HTML Builder 5.1 introduces two docblocks.

[The first](../docblocks/elements-generated.txt) lists all HTML elements in the living specification. [The second](../docblocks/empty-elements-generated.txt) lists all *empty* elements in the living specification. Both are derived from sources maintained by the W3C organization and community.

This means, when using the `__callStatic` magic method named constructor to create elements, if the element name is in the docblock list, static analyzers (specifically PHPStan) will *not* throw an error. Further, if the element name is not in the list, PHPStan *will* throw an error, which may be informative to users.

```php
// Works with PHPStan level 10 without extra setup
Element::p('Hello, World!');
```

Note: You can still create custom element names while using static analysis by either calling the `create()` method directly (recommended) or ignoring via configuration.

Using `create()` may enhance readability by distinguishing custom element names from those in the HTML spec.
10 changes: 10 additions & 0 deletions docs/migrating-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Migrating from <!-- version --> to <!-- version -->

- New: none
- Updated: None
- Deprecated: None
- Deleted: None

## New

## Updated
25 changes: 13 additions & 12 deletions src/Components/Favicons.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Stringable;

use Eightfold\HtmlBuilder\Element;
use Eightfold\HtmlBuilder\EmptyElement;

use Eightfold\HtmlBuilder\Components\FaviconMetroColors;

Expand Down Expand Up @@ -118,68 +119,68 @@ private function safariThemeColor(): string
public function __toString(): string
{
$elements = [
Element::link()->omitEndTag()->props(
EmptyElement::link(
'rel apple-touch-icon',
'sizes 180x180',
'href ' . $this->path() . '/apple-touch-icon.png'
),
Element::link()->omitEndTag()->props(
EmptyElement::link(
'rel icon',
'type image/png',
'sizes 32x32',
'href ' . $this->path() . '/favicon-32x32.png'
),
Element::link()->omitEndTag()->props(
EmptyElement::link(
'rel icon',
'type image/png',
'sizes 16x16',
'href ' . $this->path() . '/favicon-16x16.png'
),
Element::link()->omitEndTag()->props(
EmptyElement::link(
'rel manifest',
'href ' . $this->path() . '/site.webmanifest'
),
Element::meta()->omitEndTag()->props(
EmptyElement::meta(
'name msapplication-TileColor',
'content ' . $this->metroTileColor()
),
Element::meta()->omitEndTag()->props(
EmptyElement::meta(
'name theme-color',
'content ' . $this->themeColor()
),
Element::link()->omitEndTag()->props(
EmptyElement::link(
'rel mask-icon',
'href ' . $this->path() . '/safari-pinned-tab.svg',
'color ' . $this->safariThemeColor()
)
];

if ($this->hasPath()) {
$elements[] = Element::link()->omitEndTag()->props(
$elements[] = EmptyElement::link(
'rel shortcut icon',
'href ' . $this->path() . '/favicon.ico'
);

$elements[] = Element::meta()->omitEndTag()->props(
$elements[] = EmptyElement::meta(
'name msapplication-config',
'content ' . $this->path() . '/browserconfig.xml'
);
}

if ($this->hasMetro() and $this->metroUsesWhite()) {
$elements[] = Element::meta()->omitEndTag()->props(
$elements[] = EmptyElement::meta(
'name msapplication-TileImage',
'content ' . $this->path() . '/mstile-144x144.png'
);
}

if ($this->hasAppName()) {
$elements[] = Element::meta()->omitEndTag()->props(
$elements[] = EmptyElement::meta(
'name application-name',
'content ' . $this->appName()
);

$elements[] = Element::meta()->omitEndTag()->props(
$elements[] = EmptyElement::meta(
'name apple-mobile-web-app-title',
'content ' . $this->appName()
);
Expand Down
3 changes: 2 additions & 1 deletion src/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Stringable;

use Eightfold\HtmlBuilder\Element;
use Eightfold\HtmlBuilder\EmptyElement;

class Document implements Stringable
{
Expand Down Expand Up @@ -82,7 +83,7 @@ public function toString(): string
$html = (string) Element::html(
Element::head(
$pageTitle,
Element::meta()->omitEndTag()->props($this->charset()),
EmptyElement::meta($this->charset()),
...$this->headContent()
),
Element::body(...$this->bodyContent())->props(...$this->bodyProps)
Expand Down
Loading
Loading