Passed
Push — use-stateless-validator ( 690a89 )
by Dmitriy
03:09
created

Field::addInputId()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Form\Widget;
6
7
use InvalidArgumentException;
8
use Yiisoft\Arrays\ArrayHelper;
9
use Yiisoft\Factory\Exceptions\InvalidConfigException;
10
use Yiisoft\Form\FormModelInterface;
11
use Yiisoft\Form\Helper\HtmlForm;
12
use Yiisoft\Html\Html;
13
use Yiisoft\Widget\Widget;
14
15
use function array_merge;
16
use function strtr;
17
18
/**
19
 * Renders the field widget along with label, error tag and hint tag (if any) according to template.
20
 */
21
final class Field extends Widget
22
{
23
    private const DEFAULT_CONTAINER_OPTIONS = ['class' => 'form-group'];
24
    private const DEFAULT_ERROR_OPTIONS = ['class' => 'help-block'];
25
    private const DEFAULT_HINT_OPTIONS = ['class' => 'hint-block'];
26
    private const DEFAULT_LABEL_OPTIONS = ['class' => 'control-label'];
27
28
    private FormModelInterface $data;
29
    private string $attribute;
30
    private array $options = [];
31
    private array $inputOptions = [];
32
    private array $errorOptions = [];
33
    private array $hintOptions = [];
34
    private array $labelOptions = [];
35
    private bool $ariaAttribute = true;
36
    private string $errorCssClass = 'has-error';
37
    private string $errorSummaryCssClass = 'error-summary';
38
    private string $inputCssClass = 'form-control';
39
    private string $requiredCssClass = 'required';
40
    private string $successCssClass = 'has-success';
41
    private string $template = "{label}\n{input}\n{hint}\n{error}";
42
    private string $validatingCssClass = 'validating';
43
    private string $validationStateOn = 'input';
44
    private ?string $inputId = null;
45
    private array $parts = [];
46
    private bool $skipForInLabel = false;
47
    private bool $containerEnabled = true;
48
49
    /**
50
     * Renders the whole field.
51
     *
52
     * This method will generate the label, error tag, input tag and hint tag (if any), and assemble them into HTML
53
     * according to {@see template}.
54
     *
55
     * @param string|null $content the content within the field container.
56
     *
57
     * If `null` (not set), the default methods will be called to generate the label, error tag and input tag, and use
58
     * them as the content.
59
     *
60
     * @throws InvalidConfigException
61
     *
62
     * @return string the rendering result.
63
     */
64 59
    public function run(?string $content = null): string
65
    {
66 59
        if ($content === null) {
67 59
            if (!isset($this->parts['{input}'])) {
68 20
                $this->textInput();
69
            }
70
71 59
            if (!isset($this->parts['{label}'])) {
72 25
                $this->label();
73
            }
74
75 59
            if (!isset($this->parts['{error}'])) {
76 56
                $this->error();
77
            }
78
79 59
            if (!isset($this->parts['{hint}'])) {
80 56
                $this->hint();
81
            }
82
83 59
            $content = strtr($this->template, $this->parts);
84
        } else {
85
            $content = $content($this);
86
        }
87
88 59
        if ($this->containerEnabled) {
89 58
            return $this->renderBegin() . "\n" . $content . "\n" . $this->renderEnd();
90
        }
91
92 1
        return $content;
93
    }
94
95
    /**
96
     * Renders the opening tag of the field container.
97
     *
98
     * @throws InvalidArgumentException
99
     *
100
     * @return string the rendering result.
101
     */
102 58
    private function renderBegin(): string
103
    {
104 58
        $new = clone $this;
105
106 58
        $inputId = $new->addInputId();
107
108 58
        $class = [];
109 58
        $class[] = "field-$inputId";
110 58
        $class[] = $new->options['class'] ?? '';
111
112 58
        $new->options['class'] = trim(implode(' ', array_merge(self::DEFAULT_CONTAINER_OPTIONS, $class)));
113
114 58
        $new->addErrorCssClassToContainer();
115
116 58
        $tag = ArrayHelper::remove($new->options, 'tag', 'div');
117
118 58
        return Html::beginTag($tag, $new->options);
119
    }
120
121
    /**
122
     * Renders the closing tag of the field container.
123
     *
124
     * @return string the rendering result.
125
     */
126 58
    private function renderEnd(): string
127
    {
128 58
        return Html::endTag(ArrayHelper::keyExists($this->options, 'tag') ? $this->options['tag'] : 'div');
129
    }
130
131
    /**
132
     * Generates a label tag for {@see attribute}.
133
     *
134
     * @param bool $enabledLabel enabled/disable <label>.
135
     * @param array $options the tag options in terms of name-value pairs. It will be merged with {@see labelOptions}.
136
     * The options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded using
137
     * {@see \Yiisoft\Html\Html::encode()}. If a value is `null`, the corresponding attribute will not be rendered.
138
     * @param string|null $label the label to use.
139
     * If `null`, the label will be generated via {@see FormModel::getAttributeLabel()}.
140
     * Note that this will NOT be {@see \Yiisoft\Html\Html::encode()|encoded}.
141
     *
142
     * @throws InvalidConfigException
143
     *
144
     * @return self the field object itself.
145
     */
146 56
    public function label(bool $enabledLabel = true, array $options = [], ?string $label = null): self
147
    {
148 56
        if ($enabledLabel === false) {
149 12
            $this->parts['{label}'] = '';
150
151 12
            return $this;
152
        }
153
154 44
        $new = clone $this;
155
156 44
        if ($label !== null) {
157 12
            $new->labelOptions['label'] = $label;
158
        }
159
160 44
        if ($new->data->isAttributeRequired($new->attribute)) {
161 17
            Html::addCssClass($options, $new->requiredCssClass);
162
        }
163
164 44
        $new->labelOptions($options);
165 44
        $new->skipForInLabel();
166
167 44
        unset($options['class']);
168
169 44
        $new->labelOptions = array_merge($new->labelOptions, $options);
170
171 44
        $this->parts['{label}'] = Label::widget()
172 44
            ->config($new->data, $new->attribute, $new->labelOptions)
0 ignored issues
show
Bug introduced by
The method config() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of said class. However, the method does not exist in Yiisoft\Widget\Tests\Stubs\TestWidget or Yiisoft\Widget\Tests\Stubs\TestWidgetA or Yiisoft\Widget\Tests\Stubs\TestInjectionWidget or Yiisoft\Form\Widget\Form or Yiisoft\Widget\Tests\Stubs\ImmutableWidget or Yiisoft\Widget\Tests\Stubs\TestWidgetB. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

172
            ->/** @scrutinizer ignore-call */ config($new->data, $new->attribute, $new->labelOptions)
Loading history...
173 44
            ->run();
174
175 44
        return $this;
176
    }
177
178
    /**
179
     * Generates a tag that contains the first validation error of {@see attribute}.
180
     *
181
     * Note that even if there is no validation error, this method will still return an empty error tag.
182
     *
183
     * @param array $options the tag options in terms of name-value pairs. It will be merged with
184
     * {@see DEFAULT_ERROR_OPTIONS}.
185
     * The options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded using
186
     * {@see \Yiisoft\Html\Html::encode()}. If this parameter is `false`, no error tag will be rendered.
187
     *
188
     * The following options are specially handled:
189
     *
190
     * - `tag`: this specifies the tag name. If not set, `div` will be used. See also {@see \Yiisoft\Html\Html::tag()}.
191
     *
192
     * If you set a custom `id` for the error element, you may need to adjust the {@see $selectors} accordingly.
193
     *
194
     *@throws InvalidConfigException
195
     *
196
     * @return self the field object itself.
197
     *
198
     * {@see DEFAULT_ERROR_OPTIONS}
199
     */
200 58
    public function error(array $options = []): self
201
    {
202 58
        $new = clone $this;
203
204 58
        $new->errorOptions($options);
205
206 58
        unset($options['class']);
207
208 58
        $new->errorOptions = array_merge($new->errorOptions, $options);
209
210 58
        $this->parts['{error}'] = Error::widget()
211 58
            ->config($new->data, $new->attribute, $new->errorOptions)
212 58
            ->run();
213
214 58
        return $this;
215
    }
216
217
    /**
218
     * Renders the hint tag.
219
     *
220
     * @param string|null $content the hint content. If `null`, the hint will be generated via
221
     * {@see FormModel::getAttributeHint()}.
222
     * @param bool $typeHint If `false`, the generated field will not contain the hint part. Note that this will NOT be
223
     * {@see \Yiisoft\Html\Html::encode()|encoded}.
224
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
225
     * the hint tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
226
     *
227
     * The following options are specially handled:
228
     *
229
     * - `tag`: this specifies the tag name. If not set, `div` will be used. See also {@see \Yiisoft\Html\Html::tag()}.
230
     *
231
     * @throws InvalidConfigException
232
     *
233
     * @return self the field object itself.
234
     */
235 58
    public function hint(?string $content = null, bool $typeHint = true, array $options = []): self
236
    {
237 58
        if ($typeHint === false) {
238 1
            $this->parts['{hint}'] = '';
239
240 1
            return $this;
241
        }
242
243 57
        $new = clone $this;
244
245 57
        $new->hintOptions($options);
246
247 57
        unset($options['class']);
248
249 57
        if ($content !== null) {
250 1
            $new->hintOptions['hint'] = $content;
251
        }
252
253 57
        $new->hintOptions = array_merge($new->hintOptions, $options);
254
255 57
        $this->parts['{hint}'] = Hint::widget()
256 57
            ->config($new->data, $new->attribute, $new->hintOptions)
257 57
            ->run();
258
259 57
        return $this;
260
    }
261
262
    /**
263
     * Renders an input tag.
264
     *
265
     * @param string $type the input type (e.g. `text`, `password`).
266
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
267
     * the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
268
     *
269
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
270
     *
271
     * @throws InvalidArgumentException
272
     * @throws InvalidConfigException
273
     *
274
     * @return self the field object itself.
275
     */
276 5
    public function input(string $type, array $options = []): self
277
    {
278 5
        $new = clone $this;
279
280 5
        $new->setAriaAttributes($options);
281 5
        $new->inputOptions($options);
282 5
        $new->addErrorCssClassToInput();
283 5
        $new->addSuccessCssClassToInput();
284
285 5
        unset($options['class']);
286
287 5
        $new->inputOptions = array_merge($options, $new->inputOptions);
288
289 5
        $this->parts['{input}'] = Input::widget()
290 5
            ->type($type)
0 ignored issues
show
Bug introduced by
The method type() does not exist on Yiisoft\Widget\Widget. It seems like you code against a sub-type of Yiisoft\Widget\Widget such as Yiisoft\Form\Widget\ListBox or Yiisoft\Form\Widget\Input or Yiisoft\Form\Widget\ListInput or Yiisoft\Form\Widget\BooleanInput. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

290
            ->/** @scrutinizer ignore-call */ type($type)
Loading history...
291 5
            ->config($new->data, $new->attribute, $new->inputOptions)
292 5
            ->run();
293
294 5
        return $this;
295
    }
296
297
    /**
298
     * Renders a text input.
299
     *
300
     * This method will generate the `name` and `value` tag attributes automatically for the model attribute unless
301
     * they are explicitly specified in `$options`.
302
     *
303
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes
304
     * of the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
305
     *
306
     * The following special options are recognized:
307
     *
308
     * Note that if you set a custom `id` for the input element, you may need to adjust the value of {@see selectors}
309
     * accordingly.
310
     *
311
     * @throws InvalidConfigException
312
     *
313
     * @return self the field object itself.
314
     */
315 21
    public function textInput(array $options = []): self
316
    {
317 21
        $new = clone $this;
318
319 21
        $new->setAriaAttributes($options);
320 21
        $new->inputOptions($options);
321 21
        $new->addErrorCssClassToInput();
322 21
        $new->addSuccessCssClassToInput();
323
324 21
        unset($options['class']);
325
326 21
        $new->inputOptions = array_merge($options, $new->inputOptions);
327
328 21
        $this->parts['{input}'] = TextInput::widget()
329 21
            ->config($new->data, $new->attribute, $new->inputOptions)
330 21
            ->run();
331
332 21
        return $this;
333
    }
334
335
    /**
336
     * Renders a hidden input.
337
     *
338
     * Note that this method is provided for completeness. In most cases because you do not need to validate a hidden
339
     * input, you should not need to use this method. Instead, you should use
340
     * {@see \Yiisoft\Html\Html::activeHiddenInput()}.
341
     *
342
     * This method will generate the `name` and `value` tag attributes automatically for the model attribute unless
343
     * they are explicitly specified in `$options`.
344
     *
345
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
346
     * the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
347
     *
348
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
349
     *
350
     * @throws InvalidConfigException
351
     *
352
     * @return self the field object itself.
353
     */
354 1
    public function hiddenInput(array $options = []): self
355
    {
356 1
        $new = clone $this;
357
358 1
        $new->inputOptions($options);
359
360 1
        unset($options['class']);
361
362 1
        $new->inputOptions = array_merge($options, $new->inputOptions);
363
364 1
        $this->parts['{label}'] = '';
365 1
        $this->parts['{hint}'] = '';
366 1
        $this->parts['{error}'] = '';
367 1
        $this->parts['{input}'] = HiddenInput::widget()
368 1
            ->config($new->data, $new->attribute, $new->inputOptions)
369 1
            ->run();
370
371 1
        return $this;
372
    }
373
374
    /**
375
     * Renders a password input.
376
     *
377
     * This method will generate the `name` and `value` tag attributes automatically for the model attribute unless
378
     * they are explicitly specified in `$options`.
379
     *
380
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
381
     * the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
382
     *
383
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
384
     *
385
     * @throws InvalidArgumentException
386
     * @throws InvalidConfigException
387
     *
388
     * @return self the field object itself.
389
     */
390 3
    public function passwordInput(array $options = []): self
391
    {
392 3
        $new = clone $this;
393
394 3
        $new->setAriaAttributes($options);
395 3
        $new->inputOptions($options);
396 3
        $new->addErrorCssClassToInput();
397 3
        $new->addSuccessCssClassToInput();
398
399 3
        unset($options['class']);
400
401 3
        $new->inputOptions = array_merge($options, $new->inputOptions);
402
403 3
        $this->parts['{input}'] = PasswordInput::widget()
404 3
            ->config($new->data, $new->attribute, $new->inputOptions)
405 3
            ->run();
406
407 3
        return $this;
408
    }
409
410
    /**
411
     * Renders a file input.
412
     *
413
     * This method will generate the `name` tag attribute automatically for the model attribute unless
414
     * they are explicitly specified in `$options`.
415
     *
416
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
417
     * the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
418
     * @param bool $withoutHiddenInput enable/disable hidden input field.
419
     *
420
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
421
     *
422
     * @throws InvalidArgumentException
423
     * @throws InvalidConfigException
424
     *
425
     * @return self the field object itself.
426
     */
427 4
    public function fileInput(array $options = [], bool $withoutHiddenInput = false): self
428
    {
429 4
        $new = clone $this;
430
431 4
        $new->setAriaAttributes($options);
432 4
        $new->inputOptions($options);
433 4
        $new->addErrorCssClassToInput();
434 4
        $new->addSuccessCssClassToInput();
435 4
        $new->setForInLabel($options);
436
437 4
        unset($options['class']);
438
439 4
        $new->inputOptions = array_merge($options, $new->inputOptions);
440
441 4
        $this->parts['{input}'] = FileInput::widget()
442 4
            ->config($new->data, $new->attribute, $new->inputOptions)
443 4
            ->withoutHiddenInput($withoutHiddenInput)
444 4
            ->run();
445
446 4
        return $this;
447
    }
448
449
    /**
450
     * Renders a text area.
451
     *
452
     * The model attribute value will be used as the content in the textarea.
453
     *
454
     * @param array $options the tag options in terms of name-value pairs. These will be rendered as the attributes of
455
     * the resulting tag. The values will be HTML-encoded using {@see \Yiisoft\Html\Html::encode()}.
456
     *
457
     * If you set a custom `id` for the textarea element, you may need to adjust the {@see $selectors} accordingly.
458
     *
459
     * @throws InvalidArgumentException
460
     * @throws InvalidConfigException
461
     *
462
     * @return self the field object itself.
463
     */
464 3
    public function textArea(array $options = []): self
465
    {
466 3
        $new = clone $this;
467
468 3
        $new->setAriaAttributes($options);
469 3
        $new->inputOptions($options);
470 3
        $new->addErrorCssClassToInput();
471 3
        $new->addSuccessCssClassToInput();
472
473 3
        unset($options['class']);
474
475 3
        $new->inputOptions = array_merge($options, $new->inputOptions);
476
477 3
        $this->parts['{input}'] = TextArea::widget()
478 3
            ->config($new->data, $new->attribute, $new->inputOptions)
479 3
            ->run();
480
481 3
        return $this;
482
    }
483
484
    /**
485
     * Renders a radio button.
486
     *
487
     * This method will generate the `checked` tag attribute according to the model attribute value.
488
     *
489
     * @param array $options the tag options in terms of name-value pairs. The following options are specially handled:
490
     *
491
     * - `uncheck`: string, the value associated with the uncheck state of the radio button. If not set, it will take
492
     * the default value `0`. This method will render a hidden input so that if the radio button is not checked and is
493
     * submitted, the value of this attribute will still be submitted to the server via the hidden input. If you do not
494
     * want any hidden input, you should explicitly set this option as `null`.
495
     * - `label`: string, a label displayed next to the radio button. It will NOT be HTML-encoded. Therefore you can
496
     * pass in HTML code such as an image tag. If this is coming from end users, you should
497
     * {@see \Yiisoft\Html\Html::encode()|encode} it to prevent XSS attacks.
498
     * When this option is specified, the radio button will be enclosed by a label tag. If you do not want any label,
499
     * you should explicitly set this option as `null`.
500
     * - `labelOptions`: array, the HTML attributes for the label tag. This is only used when the `label` option is
501
     * specified.
502
     *
503
     * The rest of the options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded
504
     * using {@see \Yiisoft\Html\Html::encode()}. If a value is `null`, the corresponding attribute will not be
505
     * rendered.
506
     *
507
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
508
     * @param bool $enclosedByLabel whether to enclose the radio within the label.
509
     * If `true`, the method will still use {@see template} to layout the radio button and the error message except
510
     * that the radio is enclosed by the label tag.
511
     *
512
     * @throws InvalidArgumentException
513
     * @throws InvalidConfigException
514
     *
515
     * @return self the field object itself.
516
     */
517 5
    public function radio(array $options = [], bool $enclosedByLabel = true): self
518
    {
519 5
        $new = clone $this;
520
521 5
        if ($enclosedByLabel) {
522 2
            $this->parts['{label}'] = '';
523
        }
524
525 5
        $this->parts['{input}'] = Radio::widget()
526 5
            ->config($new->data, $new->attribute, $options)
527 5
            ->enclosedByLabel($enclosedByLabel)
528 5
            ->run();
529
530 5
        $new->setAriaAttributes($options);
531 5
        $new->addErrorCssClassToInput();
532 5
        $new->addSuccessCssClassToInput();
533
534 5
        return $this;
535
    }
536
537
    /**
538
     * Renders a checkbox.
539
     *
540
     * This method will generate the `checked` tag attribute according to the model attribute value.
541
     *
542
     * @param array $options the tag options in terms of name-value pairs. The following options are specially handled:
543
     *
544
     * - `uncheck`: string, the value associated with the uncheck state of the radio button. If not set, it will take
545
     * the default value `0`. This method will render a hidden input so that if the radio button is not checked and is
546
     * submitted, the value of this attribute will still be submitted to the server via the hidden input. If you do not
547
     * want any hidden input, you should explicitly set this option as `null`.
548
     * - `label`: string, a label displayed next to the checkbox. It will NOT be HTML-encoded. Therefore you can pass
549
     * in HTML code such as an image tag. If this is coming from end users, you should
550
     * {@see \Yiisoft\Html\Html::encode()|encode} it to prevent XSS attacks.
551
     * When this option is specified, the checkbox will be enclosed by a label tag. If you do not want any label, you
552
     * should explicitly set this option as `null`.
553
     * - `labelOptions`: array, the HTML attributes for the label tag. This is only used when the `label` option is
554
     * specified.
555
     *
556
     * The rest of the options will be rendered as the attributes of the resulting tag. The values will be HTML-encoded
557
     * using {@see \Yiisoft\Html\Html::encode()}. If a value is `null`, the corresponding attribute will not be
558
     * rendered.
559
     *
560
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
561
     * @param bool $enclosedByLabel whether to enclose the checkbox within the label.
562
     * If `true`, the method will still use [[template]] to layout the checkbox and the error message except that the
563
     * checkbox is enclosed by the label tag.
564
     *
565
     * @throws InvalidArgumentException
566
     * @throws InvalidConfigException
567
     *
568
     * @return self the field object itself.
569
     */
570 4
    public function checkbox(array $options = [], bool $enclosedByLabel = true): self
571
    {
572 4
        $new = clone $this;
573
574 4
        if ($enclosedByLabel) {
575 1
            $this->parts['{label}'] = '';
576
        }
577
578 4
        $this->parts['{input}'] = CheckBox::widget()
579 4
            ->config($new->data, $new->attribute, $options)
580 4
            ->enclosedByLabel($enclosedByLabel)
581 4
            ->run();
582
583 4
        $new->addErrorCssClassToInput();
584 4
        $new->addSuccessCssClassToInput();
585
586 4
        return $this;
587
    }
588
589
    /**
590
     * Renders a drop-down list.
591
     *
592
     * The selection of the drop-down list is taken from the value of the model attribute.
593
     *
594
     * @param array $items the option data items. The array keys are option values, and the array values are the
595
     * corresponding option labels. The array can also be nested (i.e. some array values are arrays too).
596
     * For each sub-array, an option group will be generated whose label is the key associated with the sub-array.
597
     * If you have a list of data models, you may convert them into the format described above using
598
     * {@see \Yiisoft\Arrays\ArrayHelper::map()}.
599
     *
600
     * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in the
601
     * labels will also be HTML-encoded.
602
     * @param array $options the tag options in terms of name-value pairs.
603
     *
604
     * For the list of available options please refer to the `$options` parameter of
605
     * {@see \Yiisoft\Html\Html::activeDropDownList()}.
606
     *
607
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
608
     *
609
     * @throws InvalidArgumentException
610
     * @throws InvalidConfigException
611
     *
612
     * @return self the field object itself.
613
     */
614 4
    public function dropDownList(array $items, array $options = []): self
615
    {
616 4
        $new = clone $this;
617
618 4
        $new->setAriaAttributes($options);
619 4
        $new->inputOptions($options);
620 4
        $new->addErrorCssClassToInput();
621 4
        $new->addSuccessCssClassToInput();
622
623 4
        unset($options['class']);
624
625 4
        $new->inputOptions = array_merge($options, $new->inputOptions);
626
627 4
        $this->parts['{input}'] = DropDownList::widget()
628 4
            ->config($new->data, $new->attribute, $new->inputOptions)
629 4
            ->items($items)
630 4
            ->run();
631
632 4
        return $this;
633
    }
634
635
    /**
636
     * Renders a list box.
637
     *
638
     * The selection of the list box is taken from the value of the model attribute.
639
     *
640
     * @param array $items the option data items. The array keys are option values, and the array values are the
641
     * corresponding option labels. The array can also be nested (i.e. some array values are arrays too).
642
     * For each sub-array, an option group will be generated whose label is the key associated with the sub-array.
643
     * If you have a list of data models, you may convert them into the format described above using
644
     * {@see \Yiisoft\Arrays\ArrayHelper::map()}.
645
     *
646
     * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in the
647
     * labels will also be HTML-encoded.
648
     * @param array $options the tag options in terms of name-value pairs.
649
     *
650
     * For the list of available options please refer to the `$options` parameter of
651
     * {@see \Yiisoft\Html\Html::activeListBox()}.
652
     *
653
     * If you set a custom `id` for the input element, you may need to adjust the {@see $selectors} accordingly.
654
     *
655
     * @throws InvalidArgumentException
656
     * @throws InvalidConfigException
657
     *
658
     * @return self the field object itself.
659
     */
660 6
    public function listBox(array $items, array $options = []): self
661
    {
662 6
        $new = clone $this;
663
664 6
        $new->setForInLabel($options);
665 6
        $new->setAriaAttributes($options);
666 6
        $new->inputOptions = array_merge($options, $new->inputOptions);
667
668 6
        $this->parts['{input}'] = ListBox::widget()
669 6
            ->config($new->data, $new->attribute, $new->inputOptions)
670 6
            ->items($items)
671 6
            ->run();
672
673 6
        return $this;
674
    }
675
676
    /**
677
     * Renders a list of checkboxes.
678
     *
679
     * A checkbox list allows multiple selection, like {@see listBox()}.
680
     * As a result, the corresponding submitted value is an array.
681
     * The selection of the checkbox list is taken from the value of the model attribute.
682
     *
683
     * @param array $items the data item used to generate the checkboxes.
684
     * The array values are the labels, while the array keys are the corresponding checkbox values.
685
     * @param array $options options (name => config) for the checkbox list.
686
     * For the list of available options please refer to the `$options` parameter of
687
     * {@see \Yiisoft\Html\Html::activeCheckboxList()}.
688
     *
689
     * @throws InvalidArgumentException
690
     * @throws InvalidConfigException
691
     *
692
     * @return self the field object itself.
693
     */
694 4
    public function checkboxList(array $items, array $options = []): self
695
    {
696 4
        $new = clone $this;
697
698 4
        $new->setForInLabel($options);
699 4
        $new->setAriaAttributes($options);
700 4
        $new->inputOptions = array_merge($options, $new->inputOptions);
701 4
        $new->skipForInLabel = true;
702
703 4
        $this->parts['{input}'] = CheckBoxList::widget()
704 4
            ->config($new->data, $new->attribute, $new->inputOptions)
705 4
            ->items($items)
706 4
            ->run();
707
708 4
        return $this;
709
    }
710
711
    /**
712
     * Renders a list of radio buttons.
713
     *
714
     * A radio button list is like a checkbox list, except that it only allows single selection.
715
     * The selection of the radio buttons is taken from the value of the model attribute.
716
     *
717
     * @param array $items the data item used to generate the radio buttons.
718
     * The array values are the labels, while the array keys are the corresponding radio values.
719
     * @param array $options options (name => config) for the radio button list.
720
     * For the list of available options please refer to the `$options` parameter of
721
     * {@see \Yiisoft\Html\Html::activeRadioList()}.
722
     *
723
     * @throws InvalidArgumentException
724
     * @throws InvalidConfigException
725
     *
726
     * @return self the field object itself.
727
     */
728 3
    public function radioList(array $items, array $options = []): self
729
    {
730 3
        $new = clone $this;
731
732 3
        $new->setAriaAttributes($options);
733 3
        $new->setForInLabel($options);
734 3
        $new->inputOptions($options);
735 3
        $new->addErrorCssClassToInput();
736 3
        $new->addSuccessCssClassToInput();
737 3
        $new->setInputRole($options);
738 3
        $new->inputOptions = array_merge($options, $new->inputOptions);
739 3
        $new->skipForInLabel = true;
740
741 3
        $this->parts['{input}'] = RadioList::widget()
742 3
            ->config($new->data, $new->attribute, $new->inputOptions)
743 3
            ->items($items)
744 3
            ->run();
745
746 3
        return $this;
747
    }
748
749 58
    private function addInputId(): string
750
    {
751 58
        return $this->inputId ?: HtmlForm::getInputId($this->data, $this->attribute);
752
    }
753
754
    /**
755
     * Set form model and name for the widget.
756
     *
757
     * @param FormModelInterface $data Form model.
758
     * @param string $attribute Form model property this widget is rendered for.
759
     *
760
     * @return self
761
     */
762 59
    public function config(FormModelInterface $data, string $attribute): self
763
    {
764 59
        $new = clone $this;
765 59
        $new->data = $data;
766 59
        $new->attribute = $attribute;
767 59
        return $new;
768
    }
769
770
    /**
771
     * Generate a container tag for {@see attribute}.
772
     *
773
     * @param bool $containerEnabled enabled/disable container for the widget.
774
     * @param array $options The HTML attributes for the widget container tag if it is enabled.
775
     * See {@see \Yiisoft\Html\Html::renderTagAttributes()} for details on how attributes are being rendered.
776
     *
777
     * @return self
778
     */
779 2
    public function enclosedByContainer(bool $containerEnabled, array $options = []): self
780
    {
781 2
        $new = clone $this;
782 2
        $new->options = $options;
783 2
        $new->containerEnabled = $containerEnabled;
784 2
        return $new;
785
    }
786
787 1
    public function ariaAttribute(bool $value): self
788
    {
789 1
        $new = clone $this;
790 1
        $new->ariaAttribute = $value;
791 1
        return $new;
792
    }
793
794
    public function errorCssClass(string $value): self
795
    {
796
        $new = clone $this;
797
        $new->errorCssClass = $value;
798
        return $new;
799
    }
800
801
    public function errorSummaryCssClass(string $value): self
802
    {
803
        $new = clone $this;
804
        $new->errorSummaryCssClass = $value;
805
        return $new;
806
    }
807
808 2
    public function inputCssClass(string $value): self
809
    {
810 2
        $new = clone $this;
811 2
        $new->inputCssClass = $value;
812 2
        return $new;
813
    }
814
815
    public function requiredCssClass(string $value): self
816
    {
817
        $new = clone $this;
818
        $new->requiredCssClass = $value;
819
        return $new;
820
    }
821
822
    public function successCssClass(string $value): self
823
    {
824
        $new = clone $this;
825
        $new->successCssClass = $value;
826
        return $new;
827
    }
828
829 2
    public function template(string $value): self
830
    {
831 2
        $new = clone $this;
832 2
        $new->template = $value;
833 2
        return $new;
834
    }
835
836
    public function validatingCssClass(string $value): self
837
    {
838
        $new = clone $this;
839
        $new->validatingCssClass = $value;
840
        return $new;
841
    }
842
843 2
    public function validationStateOn(string $value): self
844
    {
845 2
        $new = clone $this;
846 2
        $new->validationStateOn = $value;
847 2
        return $new;
848
    }
849
850 58
    public function errorOptions(array $options = []): void
851
    {
852 58
        $classFromOptions = ArrayHelper::remove($options, 'class', self::DEFAULT_ERROR_OPTIONS['class']);
853
854 58
        if ($classFromOptions !== self::DEFAULT_ERROR_OPTIONS['class']) {
855 2
            $classFromOptions = self::DEFAULT_ERROR_OPTIONS['class'] . ' ' . $classFromOptions;
856
        }
857
858 58
        $this->errorOptions = array_merge($this->errorOptions, $options);
859
        /**
860
         * @psalm-suppress PossiblyNullPropertyAssignmentValue
861
         */
862 58
        Html::addCssClass($this->errorOptions, $classFromOptions);
863 58
    }
864
865 57
    public function hintOptions(array $options = []): void
866
    {
867 57
        $classFromOptions = ArrayHelper::remove($options, 'class', self::DEFAULT_HINT_OPTIONS['class']);
868
869 57
        if ($classFromOptions !== self::DEFAULT_HINT_OPTIONS['class']) {
870 1
            $classFromOptions = self::DEFAULT_HINT_OPTIONS['class'] . ' ' . $classFromOptions;
871
        }
872
873 57
        $this->hintOptions = array_merge($this->hintOptions, $options);
874
        /**
875
         * @psalm-suppress PossiblyNullPropertyAssignmentValue
876
         */
877 57
        Html::addCssClass($this->hintOptions, $classFromOptions);
878 57
    }
879
880 43
    public function inputOptions(array $options = []): void
881
    {
882 43
        $classFromOptions = ArrayHelper::remove($options, 'class', $this->inputCssClass);
883
884 43
        if ($classFromOptions !== $this->inputCssClass) {
885
            $classFromOptions = $this->inputCssClass . ' ' . $classFromOptions;
886
        }
887
888 43
        $this->inputOptions = array_merge($this->inputOptions, $options);
889
        /**
890
         * @psalm-suppress PossiblyNullPropertyAssignmentValue
891
         */
892 43
        Html::addCssClass($this->inputOptions, $classFromOptions);
893 43
    }
894
895 44
    public function labelOptions(array $options = []): void
896
    {
897 44
        $classFromOptions = ArrayHelper::remove($options, 'class', self::DEFAULT_LABEL_OPTIONS['class']);
898
899 44
        if ($classFromOptions !== self::DEFAULT_LABEL_OPTIONS['class']) {
900 27
            $classFromOptions = self::DEFAULT_LABEL_OPTIONS['class'] . ' ' . $classFromOptions;
901
        }
902
903 44
        $this->labelOptions = array_merge($this->labelOptions, $options);
904
        /**
905
         * @psalm-suppress PossiblyNullPropertyAssignmentValue
906
         */
907 44
        Html::addCssClass($this->labelOptions, $classFromOptions);
908 44
    }
909
910 58
    private function addErrorCssClassToContainer(): void
911
    {
912 58
        if ($this->validationStateOn === 'container') {
913
            /**
914
             * @psalm-suppress PossiblyNullPropertyAssignmentValue
915
             */
916 2
            Html::addCssClass($this->options, $this->errorCssClass);
917
        }
918 58
    }
919
920 50
    private function addErrorCssClassToInput(): void
921
    {
922 50
        if ($this->validationStateOn === 'input') {
923 48
            $attributeName = HtmlForm::getAttributeName($this->attribute);
924
925 48
            if ($this->data->hasErrors($attributeName)) {
926
                /**
927
                 * @psalm-suppress PossiblyNullPropertyAssignmentValue
928
                 */
929 7
                Html::addCssClass($this->inputOptions, $this->errorCssClass);
930
            }
931
        }
932 50
    }
933
934 50
    private function addSuccessCssClassToInput(): void
935
    {
936 50
        if ($this->validationStateOn === 'input') {
937 48
            $attributeName = HtmlForm::getAttributeName($this->attribute);
938
939 48
            if (!$this->data->hasErrors($attributeName) && $this->data->isValidated()) {
940
                /**
941
                 * @psalm-suppress PossiblyNullPropertyAssignmentValue
942
                 */
943 3
                Html::addCssClass($this->inputOptions, $this->successCssClass);
944
            }
945
        }
946 50
    }
947
948 3
    private function setInputRole(array $options = []): void
949
    {
950 3
        $this->inputOptions['role'] = $options['role'] ?? 'radiogroup';
951 3
    }
952
953 44
    private function skipForInLabel(): void
954
    {
955 44
        if ($this->skipForInLabel) {
956
            $this->labelOptions['for'] = null;
957
        }
958 44
    }
959
960 16
    private function setForInLabel(array $options = []): void
961
    {
962 16
        if (isset($options['id'])) {
963
            $this->inputId = $options['id'];
964
965
            if (!isset($this->labelOptions['for'])) {
966
                $this->labelOptions['for'] = $options['id'];
967
            }
968
        }
969 16
    }
970
971 54
    private function setAriaAttributes(array $options = []): void
972
    {
973 54
        if ($this->ariaAttribute) {
974 54
            if (!isset($options['aria-required']) && $this->data->isAttributeRequired($this->attribute)) {
975 19
                $this->inputOptions['aria-required'] = 'true';
976
            }
977
978 54
            if (!isset($options['aria-invalid']) && $this->data->hasErrors($this->attribute)) {
979 7
                $this->inputOptions['aria-invalid'] = 'true';
980
            }
981
        }
982 54
    }
983
}
984