Skip to content

Commit 2ff3144

Browse files
authored
Fix #111: Add widget ButtonGroup, add method Tag::unionAttributes() that available for all tags
1 parent df65ea8 commit 2ff3144

8 files changed

Lines changed: 603 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
- New #103: Add class for tag `Form` and method `Html::form()` (vjik)
66
- New #109: Add class for tag `Datalist` and method `Html::datalist()` (vjik)
77
- New #109: Add specialized class for input tag with type `Range` and methods `Html::range()`, `Input::range()` (vjik)
8+
- New #111: Add widget `ButtonGroup` (vjik)
9+
- New #111: Add method `Tag::unionAttributes()` that available for all tags (vjik)
810
- Enh #106: Add option groups support to method `Select::optionsData()` (vjik)
911
- Enh #108: Add individual attributes of options and option groups support to method `Select::optionsData()` (vjik)
1012
- Enh #102: Remove psalm type `HtmlAttributes`, too obsessive for package users (vjik)

README.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ The package provides various tools to help with dynamic server-side generation o
2222
`Meta`, `Noscript`, `Ol`, `Optgroup`, `Option`, `P`, `Picture`, `Script`, `Select`, `Source`, `Span`, `Strong`,
2323
`Style`, `Table`, `Tbody`, `Td`, `Textarea`, `Tfoot`, `Th`, `Thead`, `Title`, `Tr`, `Track`, `Ul`, `Video`.
2424
- `CustomTag` class that helps to generate custom tag with any attributes.
25-
- HTML widgets `CheckboxList` and `RadioList`.
25+
- HTML widgets `ButtonGroup`, `CheckboxList` and `RadioList`.
2626
- All tags content is automatically HTML-encoded. There is `NoEncode` class designed to wrap content that should not be encoded.
2727
- `Html` helper that has static methods to generate HTML, create tags and HTML widget objects.
2828

@@ -156,6 +156,29 @@ echo Html::b(NoEncode::string('<i>hello</i>'));
156156
There are multiple widgets that do not directly represent any HTML tag, but a set of tags. These help to express
157157
complex HTML in simple PHP.
158158

159+
### `ButtonGroup`
160+
161+
Represents a group of buttons.
162+
163+
```php
164+
echo \Yiisoft\Html\Widget\ButtonGroup::create()
165+
->buttons(
166+
\Yiisoft\Html\Html::resetButton('Reset Data'),
167+
\Yiisoft\Html\Html::resetButton('Send'),
168+
)
169+
->containerAttributes(['class' => 'actions'])
170+
->buttonAttributes(['form' => 'CreatePost']);
171+
```
172+
173+
Result will be:
174+
175+
```html
176+
<div class="actions">
177+
<button type="reset" form="CreatePost">Reset Data</button>
178+
<button type="reset" class="primary" form="CreatePost">Send</button>
179+
</div>
180+
```
181+
159182
### `CheckboxList`
160183

161184
Represents a list of checkboxes.

src/Tag/Base/Tag.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ final public function replaceAttributes(array $attributes): self
4343
return $new;
4444
}
4545

46+
/**
47+
* Union attributes with a new set.
48+
*
49+
* @param array $attributes Name-value set of attributes.
50+
*
51+
* @return static
52+
*/
53+
final public function unionAttributes(array $attributes): self
54+
{
55+
$new = clone $this;
56+
$new->attributes += $attributes;
57+
return $new;
58+
}
59+
4660
/**
4761
* Set attribute value.
4862
*

src/Widget/ButtonGroup.php

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Html\Widget;
6+
7+
use InvalidArgumentException;
8+
use Yiisoft\Html\Html;
9+
use Yiisoft\Html\NoEncodeStringableInterface;
10+
use Yiisoft\Html\Tag\Button;
11+
12+
use function is_array;
13+
use function is_string;
14+
15+
/**
16+
* `ButtonGroup` represents a group of buttons.
17+
*/
18+
final class ButtonGroup implements NoEncodeStringableInterface
19+
{
20+
private ?string $containerTag = 'div';
21+
private array $containerAttributes = [];
22+
23+
/**
24+
* @var Button[]
25+
*/
26+
private array $buttons = [];
27+
private array $buttonAttributes = [];
28+
private string $separator = "\n";
29+
30+
public static function create(): self
31+
{
32+
return new self();
33+
}
34+
35+
public function withoutContainer(): self
36+
{
37+
return $this->containerTag(null);
38+
}
39+
40+
public function containerTag(?string $name): self
41+
{
42+
$new = clone $this;
43+
$new->containerTag = $name;
44+
return $new;
45+
}
46+
47+
public function containerAttributes(array $attributes): self
48+
{
49+
$new = clone $this;
50+
$new->containerAttributes = $attributes;
51+
return $new;
52+
}
53+
54+
public function buttons(Button ...$buttons): self
55+
{
56+
$new = clone $this;
57+
$new->buttons = $buttons;
58+
return $new;
59+
}
60+
61+
/**
62+
* @param array $data Array of buttons. Each button is an array with label as first element and additional
63+
* name-value pairs as attrbiutes of button.
64+
*
65+
* Example:
66+
* ```php
67+
* [
68+
* ['Reset', 'type' => 'reset', 'class' => 'default'],
69+
* ['Send', 'type' => 'submit', 'class' => 'primary'],
70+
* ]
71+
* ```
72+
* @param bool $encode Whether button content should be HTML-encoded.
73+
*/
74+
public function buttonsData(array $data, bool $encode = true): self
75+
{
76+
$buttons = [];
77+
foreach ($data as $row) {
78+
if (!is_array($row) || !isset($row[0]) || !is_string($row[0])) {
79+
throw new InvalidArgumentException(
80+
'Invalid buttons data. A data row must be array with label as first element ' .
81+
'and additional name-value pairs as attrbiutes of button.'
82+
);
83+
}
84+
$label = $row[0];
85+
unset($row[0]);
86+
$buttons[] = Html::button($label, $row)->encode($encode);
87+
}
88+
return $this->buttons(...$buttons);
89+
}
90+
91+
public function buttonAttributes(array $attributes): self
92+
{
93+
$new = clone $this;
94+
$new->buttonAttributes = array_merge($new->buttonAttributes, $attributes);
95+
return $new;
96+
}
97+
98+
public function replaceButtonAttributes(array $attributes): self
99+
{
100+
$new = clone $this;
101+
$new->buttonAttributes = $attributes;
102+
return $new;
103+
}
104+
105+
public function disabled(?bool $disabled = true): self
106+
{
107+
$new = clone $this;
108+
$new->buttonAttributes['disabled'] = $disabled;
109+
return $new;
110+
}
111+
112+
/**
113+
* Specifies the form element the buttons belongs to. The value of this attribute must be the ID attribute of a form
114+
* element in the same document.
115+
*
116+
* @param string|null $id ID of a form.
117+
*
118+
* @link https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#attr-fae-form
119+
*/
120+
public function form(?string $id): self
121+
{
122+
$new = clone $this;
123+
$new->buttonAttributes['form'] = $id;
124+
return $new;
125+
}
126+
127+
public function separator(string $separator): self
128+
{
129+
$new = clone $this;
130+
$new->separator = $separator;
131+
return $new;
132+
}
133+
134+
public function render(): string
135+
{
136+
if (empty($this->buttons)) {
137+
return '';
138+
}
139+
140+
if (empty($this->buttonAttributes)) {
141+
$lines = $this->buttons;
142+
} else {
143+
$lines = [];
144+
foreach ($this->buttons as $button) {
145+
$lines[] = $button->unionAttributes($this->buttonAttributes);
146+
}
147+
}
148+
149+
$html = [];
150+
if (!empty($this->containerTag)) {
151+
$html[] = Html::openTag($this->containerTag, $this->containerAttributes);
152+
}
153+
$html[] = implode($this->separator, $lines);
154+
if (!empty($this->containerTag)) {
155+
$html[] = Html::closeTag($this->containerTag);
156+
}
157+
158+
return implode("\n", $html);
159+
}
160+
161+
public function __toString(): string
162+
{
163+
return $this->render();
164+
}
165+
166+
private function __construct()
167+
{
168+
}
169+
}

src/Widget/CheckboxList/CheckboxList.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
use function is_array;
1515

1616
/**
17-
* CheckboxList represents a list of checkboxes and their corresponding labels.
17+
* `CheckboxList` represents a list of checkboxes and their corresponding labels.
1818
*/
1919
final class CheckboxList implements NoEncodeStringableInterface
2020
{

src/Widget/RadioList/RadioList.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
use Yiisoft\Html\Tag\Input;
1111

1212
/**
13-
* RadioList represents a list of radios and their corresponding labels.
13+
* `RadioList` represents a list of radios and their corresponding labels.
1414
*/
1515
final class RadioList implements NoEncodeStringableInterface
1616
{

tests/common/Tag/Base/TagTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,17 @@ public function testReplaceAttributes(): void
8989
);
9090
}
9191

92+
public function testUnionAttributes(): void
93+
{
94+
$this->assertSame(
95+
'<test id="color" class="red">',
96+
TestTag::tag()
97+
->class('red')
98+
->unionAttributes(['class' => 'green', 'id' => 'color'])
99+
->render(),
100+
);
101+
}
102+
92103
public function dataAttribute(): array
93104
{
94105
return [
@@ -187,6 +198,7 @@ public function testImmutability(): void
187198
$tag = TestTag::tag();
188199
$this->assertNotSame($tag, $tag->attributes([]));
189200
$this->assertNotSame($tag, $tag->replaceAttributes([]));
201+
$this->assertNotSame($tag, $tag->unionAttributes([]));
190202
$this->assertNotSame($tag, $tag->attribute('id', null));
191203
$this->assertNotSame($tag, $tag->id(null));
192204
$this->assertNotSame($tag, $tag->class('test'));

0 commit comments

Comments
 (0)