diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0989da0b..796b512c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,7 +2,8 @@
## 4.1.1 under development
-- no changes in this release.
+- New #276: Add `beforeInput()` and `afterInput()` methods to abstract `BooleanInputTag`, extended by `Radio`
+ and `Checkbox` (@vjik)
## 4.1.0 May 19, 2026
diff --git a/src/Tag/Base/BooleanInputTag.php b/src/Tag/Base/BooleanInputTag.php
index 966b1c51..f0bc987d 100644
--- a/src/Tag/Base/BooleanInputTag.php
+++ b/src/Tag/Base/BooleanInputTag.php
@@ -18,6 +18,8 @@ abstract class BooleanInputTag extends InputTag
private array $labelAttributes = [];
private bool $labelWrap = true;
private bool $labelEncode = true;
+ private string|Stringable $beforeInput = '';
+ private string|Stringable $afterInput = '';
/**
* @link https://www.w3.org/TR/html52/sec-forms.html#element-attrdef-input-checked
@@ -86,6 +88,26 @@ final public function uncheckValue(bool|float|int|string|Stringable|null $value)
return $new;
}
+ /**
+ * @param string|Stringable $content Content to be rendered before the input.
+ */
+ final public function beforeInput(string|Stringable $content): static
+ {
+ $new = clone $this;
+ $new->beforeInput = $content;
+ return $new;
+ }
+
+ /**
+ * @param string|Stringable $content Content to be rendered after the input.
+ */
+ final public function afterInput(string|Stringable $content): static
+ {
+ $new = clone $this;
+ $new->afterInput = $content;
+ return $new;
+ }
+
final protected function prepareAttributes(): void
{
$this->attributes['type'] = $this->getType();
@@ -97,23 +119,32 @@ final protected function before(): string
? null
: Html::generateId();
- return $this->renderUncheckInput()
- . ($this->labelWrap ? $this->renderLabelOpenTag($this->labelAttributes) : '');
+ $html = $this->renderUncheckInput();
+
+ if ($this->labelWrap) {
+ $html .= $this->renderLabelOpenTag($this->labelAttributes);
+ }
+
+ $html .= $this->beforeInput;
+
+ return $html;
}
final protected function after(): string
{
+ $html = (string) $this->afterInput;
+
if ($this->label === null) {
- return '';
+ return $html;
}
if ($this->labelWrap) {
- $html = $this->label === '' ? '' : ' ';
+ $html .= $this->label === '' ? '' : ' ';
} else {
$labelAttributes = array_merge($this->labelAttributes, [
'for' => $this->attributes['id'],
]);
- $html = ' ' . $this->renderLabelOpenTag($labelAttributes);
+ $html .= ' ' . $this->renderLabelOpenTag($labelAttributes);
}
$html .= $this->labelEncode ? Html::encode($this->label) : $this->label;
diff --git a/tests/Tag/Base/BooleanInputTagTest.php b/tests/Tag/Base/BooleanInputTagTest.php
index 981143dd..7495c080 100644
--- a/tests/Tag/Base/BooleanInputTagTest.php
+++ b/tests/Tag/Base/BooleanInputTagTest.php
@@ -6,6 +6,9 @@
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
+use Stringable;
+use Yiisoft\Html\IdGenerator;
+use Yiisoft\Html\Tests\Objects\StringableObject;
use Yiisoft\Html\Tests\Objects\TestBooleanInputTag;
final class BooleanInputTagTest extends TestCase
@@ -226,6 +229,128 @@ public function testUncheckValueWithLabel(): void
);
}
+ public static function dataBeforeInput(): iterable
+ {
+ yield [
+ 'Before',
+ 'Before',
+ ];
+ yield [
+ 'Before',
+ 'Before',
+ ];
+ yield [
+ 'Before',
+ new StringableObject('Before'),
+ ];
+ yield [
+ 'Before',
+ new StringableObject('Before'),
+ ];
+ }
+
+ #[DataProvider('dataBeforeInput')]
+ public function testBeforeInput(string $expectedBefore, string|Stringable $content): void
+ {
+ $input = (new TestBooleanInputTag())->beforeInput($content);
+ $this->assertSame(
+ $expectedBefore . '',
+ (string) $input,
+ );
+ }
+
+ public function testBeforeInputWithLabel(): void
+ {
+ $input = (new TestBooleanInputTag())
+ ->label('One')
+ ->beforeInput('Before');
+ $this->assertSame(
+ '',
+ (string) $input,
+ );
+ }
+
+ public function testBeforeInputWithSideLabel(): void
+ {
+ IdGenerator\disableSeed();
+ IdGenerator\reset();
+
+ try {
+ $result = (new TestBooleanInputTag())
+ ->sideLabel('One')
+ ->beforeInput('Before')
+ ->render();
+ } finally {
+ IdGenerator\enableSeed();
+ }
+
+ $this->assertSame(
+ 'Before ',
+ $result,
+ );
+ }
+
+ public static function dataAfterInput(): iterable
+ {
+ yield [
+ 'After',
+ 'After',
+ ];
+ yield [
+ 'After',
+ 'After',
+ ];
+ yield [
+ 'After',
+ new StringableObject('After'),
+ ];
+ yield [
+ 'After',
+ new StringableObject('After'),
+ ];
+ }
+
+ #[DataProvider('dataAfterInput')]
+ public function testAfterInput(string $expectedAfter, string|Stringable $content): void
+ {
+ $input = (new TestBooleanInputTag())->AfterInput($content);
+ $this->assertSame(
+ '' . $expectedAfter,
+ (string) $input,
+ );
+ }
+
+ public function testAfterInputWithLabel(): void
+ {
+ $input = (new TestBooleanInputTag())
+ ->label('One')
+ ->afterInput('After');
+ $this->assertSame(
+ '',
+ (string) $input,
+ );
+ }
+
+ public function testAfterInputWithSideLabel(): void
+ {
+ IdGenerator\disableSeed();
+ IdGenerator\reset();
+
+ try {
+ $result = (new TestBooleanInputTag())
+ ->sideLabel('One')
+ ->afterInput('After')
+ ->render();
+ } finally {
+ IdGenerator\enableSeed();
+ }
+
+ $this->assertSame(
+ 'After ',
+ $result,
+ );
+ }
+
public function testImmutability(): void
{
$input = new TestBooleanInputTag();
@@ -234,5 +359,7 @@ public function testImmutability(): void
$this->assertNotSame($input, $input->sideLabel(''));
$this->assertNotSame($input, $input->labelEncode(true));
$this->assertNotSame($input, $input->uncheckValue(null));
+ $this->assertNotSame($input, $input->beforeInput(''));
+ $this->assertNotSame($input, $input->afterInput(''));
}
}