<!--
	covers: widgets base/Widget.php
	verified: 54fd0a6
-->
# Widgets

Page markup is assembled from widget classes, not from monolithic views. 39 widgets in
[`widgets/`](../../widgets).

## The naming chain

**Four artefacts, one name.** Break a link and the widget renders nothing, or renders unstyled, or renders
without behaviour — none of which raises an error.

```
app\widgets\ArticleWidget                        widgets/ArticleWidget.php
  → views/widgets/article-widget.php             the view    (Widget::getRenderFile)
  → .article-widget                              the CSS class (Widget::getClassName)
  → resources/scss/widgets/article-widget.scss   the stylesheet
  → resources/js/widgets/ArticleWidget.js        the behaviour, static selector = '.article-widget'
```

The PHP side derives the first three: `getRenderFile()` is
`@app/views/widgets/` + `Inflector::camel2id( basename )`, and `getClassName()` is the same kebab-case name,
with `-widget` appended if it is not already there. Nothing is configured.

`./yii dev/create-widget <Name>` writes all four files at once. Use it.

## `app\base\Widget`

[`base/Widget.php`](../../base/Widget.php), abstract, extends `yii\base\Widget`.

### Rendering

`run()` → `getOutput()`:

```php
$output = $this->render( $this->renderFile, $this->renderParams );
$output = $this->unwrapped ? $output : Html::tag( $this->tagName, $output, $this->htmlAttributes );
$output = $this->preHtml . $output . $this->posHtml;
return preg_replace( '~>\s+<~', '><', trim( $output ) );
```

So every widget is wrapped in a tag carrying its CSS classes — unless `$unwrapped = true`. Inter-tag
whitespace inside a widget is destroyed here, and again for the whole document in `View::endPage()`.

### The extension points

| Member | Default | Override to |
|--------|---------|-------------|
| `$defaultTagName` / `getTagName()` | `div` | change the wrapper element |
| `$defaultCssClasses` / `getCssClasses()` | `[ 'widget', <className> ]` | add classes |
| `$defaultHtmlAttributes` / `getHtmlAttributes()` | `id` + `class` | add attributes |
| `getRenderParams()` | `[ 'widget' => $this ]` | pass data to the view |
| `getDisplayable()` | `true` | suppress rendering entirely |
| `getPreHtml()` / `getPosHtml()` | `''` | emit markup outside the wrapper |
| `getCacheIdParams()` | `null` | enable output caching |

`getDisplayable() === false` returns an empty string before anything is rendered — the idiomatic way to say
"nothing to show here".

### Exceptions are swallowed outside dev

```php
public static function widget( $config = [] ) : string {
    if( YII_ENV === 'dev' ) return parent::widget( $config );
    try { return parent::widget( $config ); }
    catch( Exception $e ) { Yii::$app->log->logger->log( $e->getMessage(), LEVEL_ERROR ); return ''; }
}
```

**A widget that throws in production disappears from the page and logs.** A widget missing from a live page
but present locally is this, every time — check `runtime/logs/`.

### Output caching is built but unused

`getCacheIdParams()` returns `null` in the base and **no widget overrides it**, so nothing is cached. See
[CACHING.md](CACHING.md) for what happens if you switch it on.

## `DisplayWidget`

[`widgets/DisplayWidget.php`](../../widgets/DisplayWidget.php) — the base for widgets that render one
`ActiveRecord`:

- **throws `InvalidConfigException` from the constructor** when `$model` is empty;
- adds `!empty( $this->model )` to `getDisplayable()`;
- passes `model` into the render params, so views get `$model` alongside `$widget`.

Combined with the swallowing in `Widget::widget()`, a missing model means an empty string in production and a
visible exception in dev.

## Static shorthand constructors

Widgets that are used inline expose static helpers instead of requiring a config array:

```php
TitleWidget::h1( $text, $cssClass );      // → static::widget( [ 'level' => 1, … ] )
TitleWidget::h3( $name, 'h6' );
```

`TitleWidget` also overrides `getTagName()` to `"h{$level}"` and suppresses itself when `$text` is empty.

## The catalogue

| Group | Widgets |
|-------|---------|
| Layout | `HeaderWidget`, `FooterWidget`, `BreadcrumbWidget`, `TitleWidget`, `TabsWidget`, `PaginationWidget`, `LanguageSwitcherWidget`, `ShareWidget` |
| Storage | `StorageWidget`, `StoragesWidget`, `StorageListWidget`, `StorageTypeWidget`, `StorageSearchWidget`, `StorageSearchPart`, `StorageFilterWidget`, `ComparisonItemWidget` |
| Location | `LocationWidget`, `LocationsWidget`, `LocationMapWidget`, `SvgMap`, `BigNavigationWidget` |
| Content | `ArticleWidget`, `ArticlesWidget`, `ContentsWidget`, `FaqWidget`, `ServiceWidget`, `ReviewsWidget`, `FeaturesWidget`, `SlideshowWidget`, `SwiperWidget`, `RentalProcessWidget` |
| Forms & account | `ContactFormWidget`, `ContactSidebarWidget`, `AccountWidget`, `ProposalRequestItemWidget`, `FeedbackWidget`, `ActiveDropDownWidget` |
| Other | `CalendarWidget`, `SearchResultWidget`, `DisplayWidget` (abstract) |

Two widgets break the `*Widget` suffix convention: `StorageSearchPart` and `SvgMap`. `getClassName()` appends
`-widget` for them anyway, so their CSS classes are `.storage-search-part-widget` and `.svg-map-widget`.

## View conventions

A widget view opens with a docblock declaring what it receives, then `use`, then computation, then markup:

```php
<?php

/**
 * Article widget's view.
 * @var ArticleWidget $widget
 * @var Article $model
 */

use app\models\Article;
use app\widgets\TitleWidget;

$photo = $model->photos[ 0 ] ?? null;
$name = e( $model->name );

?>

<?php if( $photo?->picture ): ?>
	<img src="<?=$photo->picture->getUrl( $model::PHOTO_SIZE_SMALL )?>" alt="<?=$name?>" loading="lazy">
<?php endif; ?>
```

The wrapper element is **not** in the view — `getOutput()` adds it.

## Traps

1. **A renamed widget class needs four renames.** The PHP finds the view by convention; the SCSS and JS do
   not follow automatically.
2. **`static selector` on the JS side is a separate string** and will not follow a PHP rename.
3. **Exceptions vanish in production.** Always check the log before assuming a data problem.
4. **The whitespace collapse runs twice** — once per widget, once per document. Do not rely on whitespace
   between tags for layout.
5. **`getHtmlAttributes()` includes `id`**, which Yii auto-generates per instance. Anything keyed on it —
   including a future output cache — differs between otherwise identical instances.
