Passed
Push — html-like-rules ( c01197 )
by Dmitriy
07:51
created

Field::addSuccessCssClassToInput()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 4
c 0
b 0
f 0
nc 3
nop 0
dl 0
loc 10
ccs 5
cts 5
cp 1
crap 4
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 60
    public function run(?string $content = null): string
65
    {
66 60
        if ($content === null) {
67 60
            if (!isset($this->parts['{input}'])) {
68 20
                $this->textInput();
69
            }
70
71 60
            if (!isset($this->parts['{label}'])) {
72 26
                $this->label();
73
            }
74
75 60
            if (!isset($this->parts['{error}'])) {
76 57
                $this->error();
77
            }
78
79 60
            if (!isset($this->parts['{hint}'])) {
80 57
                $this->hint();
81
            }
82
83 60
            $content = strtr($this->template, $this->parts);
84
        } else {
85
            $content = $content($this);
86
        }
87
88 60
        if ($this->containerEnabled) {
89 59
            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 59
    private function renderBegin(): string
103
    {
104 59
        $new = clone $this;
105
106 59
        $inputId = $new->addInputId();
107
108 59
        $class = [];
109 59
        $class[] = "field-$inputId";
110 59
        $class[] = $new->options['class'] ?? '';
111
112 59
        $new->options['class'] = trim(implode(' ', array_merge(self::DEFAULT_CONTAINER_OPTIONS, $class)));
113
114 59
        $new->addErrorCssClassToContainer();
115
116 59
        $tag = ArrayHelper::remove($new->options, 'tag', 'div');
117
118 59
        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 59
    private function renderEnd(): string
127
    {
128 59
        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 57
    public function label(bool $enabledLabel = true, array $options = [], ?string $label = null): self
147
    {
148 57
        if ($enabledLabel === false) {
149 12
            $this->parts['{label}'] = '';
150
151 12
            return $this;
152
        }
153
154 45
        $new = clone $this;
155
156 45
        if ($label !== null) {
157 12
            $new->labelOptions['label'] = $label;
158
        }
159
160 45
        if ($new->data->isAttributeRequired($new->attribute)) {
161 17
            Html::addCssClass($options, $new->requiredCssClass);
162
        }
163
164 45
        $new->labelOptions($options);
165 45
        $new->skipForInLabel();
166
167 45
        unset($options['class']);
168
169 45
        $new->labelOptions = array_merge($new->labelOptions, $options);
170
171 45
        $this->parts['{label}'] = Label::widget()
172 45
            ->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 45
            ->run();
174
175 45
        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 59
    public function error(array $options = []): self
201
    {
202 59
        $new = clone $this;
203
204 59
        $new->errorOptions($options);
205
206 59
        unset($options['class']);
207
208 59
        $new->errorOptions = array_merge($new->errorOptions, $options);
209
210 59
        $this->parts['{error}'] = Error::widget()
211 59
            ->config($new->data, $new->attribute, $new->errorOptions)
212 59
            ->run();
213
214 59
        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 59
    public function hint(?string $content = null, bool $typeHint = true, array $options = []): self
236
    {
237 59
        if ($typeHint === false) {
238 1
            $this->parts['{hint}'] = '';
239
240 1
            return $this;
241
        }
242
243 58
        $new = clone $this;
244
245 58
        $new->hintOptions($options);
246
247 58
        unset($options['class']);
248
249 58
        if ($content !== null) {
250 1
            $new->hintOptions['hint'] = $content;
251
        }
252
253 58
        $new->hintOptions = array_merge($new->hintOptions, $options);
254
255 58
        $this->parts['{hint}'] = Hint::widget()
256 58
            ->config($new->data, $new->attribute, $new->hintOptions)
257 58
            ->run();
258
259 58
        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 6
    public function input(string $type, array $options = []): self
277
    {
278 6
        $new = clone $this;
279
280 6
        $new->setAriaAttributes($options);
281 6
        $new->inputOptions($options);
282 6
        $new->addErrorCssClassToInput();
283 6
        $new->addSuccessCssClassToInput();
284
285 6
        unset($options['class']);
286
287 6
        $new->inputOptions = array_merge($options, $new->inputOptions);
288
289 6
        $this->parts['{input}'] = Input::widget()
290 6
            ->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 6
            ->config($new->data, $new->attribute, $new->inputOptions)
292 6
            ->run();
293
294 6
        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 59
    private function addInputId(): string
750
    {
751 59
        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 60
    public function config(FormModelInterface $data, string $attribute): self
763
    {
764 60
        $new = clone $this;
765 60
        $new->data = $data;
766 60
        $new->attribute = $attribute;
767 60
        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 59
    public function errorOptions(array $options = []): void
851
    {
852 59
        $classFromOptions = ArrayHelper::remove($options, 'class', self::DEFAULT_ERROR_OPTIONS['class']);
853
854 59
        if ($classFromOptions !== self::DEFAULT_ERROR_OPTIONS['class']) {
855 2
            $classFromOptions = self::DEFAULT_ERROR_OPTIONS['class'] . ' ' . $classFromOptions;
856
        }
857
858 59
        $this->errorOptions = array_merge($this->errorOptions, $options);
859
        /**
860
         * @psalm-suppress PossiblyNullPropertyAssignmentValue
861
         */
862 59
        Html::addCssClass($this->errorOptions, $classFromOptions);
863 59
    }
864
865 58
    public function hintOptions(array $options = []): void
866
    {
867 58
        $classFromOptions = ArrayHelper::remove($options, 'class', self::DEFAULT_HINT_OPTIONS['class']);
868
869 58
        if ($classFromOptions !== self::DEFAULT_HINT_OPTIONS['class']) {
870 1
            $classFromOptions = self::DEFAULT_HINT_OPTIONS['class'] . ' ' . $classFromOptions;
871
        }
872
873 58
        $this->hintOptions = array_merge($this->hintOptions, $options);
874
        /**
875
         * @psalm-suppress PossiblyNullPropertyAssignmentValue
876
         */
877 58
        Html::addCssClass($this->hintOptions, $classFromOptions);
878 58
    }
879
880 44
    public function inputOptions(array $options = []): void
881
    {
882 44
        $classFromOptions = ArrayHelper::remove($options, 'class', $this->inputCssClass);
883
884 44
        if ($classFromOptions !== $this->inputCssClass) {
885
            $classFromOptions = $this->inputCssClass . ' ' . $classFromOptions;
886
        }
887
888 44
        $this->inputOptions = array_merge($this->inputOptions, $options);
889
        /**
890
         * @psalm-suppress PossiblyNullPropertyAssignmentValue
891
         */
892 44
        Html::addCssClass($this->inputOptions, $classFromOptions);
893 44
    }
894
895 45
    public function labelOptions(array $options = []): void
896
    {
897 45
        $classFromOptions = ArrayHelper::remove($options, 'class', self::DEFAULT_LABEL_OPTIONS['class']);
898
899 45
        if ($classFromOptions !== self::DEFAULT_LABEL_OPTIONS['class']) {
900 27
            $classFromOptions = self::DEFAULT_LABEL_OPTIONS['class'] . ' ' . $classFromOptions;
901
        }
902
903 45
        $this->labelOptions = array_merge($this->labelOptions, $options);
904
        /**
905
         * @psalm-suppress PossiblyNullPropertyAssignmentValue
906
         */
907 45
        Html::addCssClass($this->labelOptions, $classFromOptions);
908 45
    }
909
910 59
    private function addErrorCssClassToContainer(): void
911
    {
912 59
        if ($this->validationStateOn === 'container') {
913
            /**
914
             * @psalm-suppress PossiblyNullPropertyAssignmentValue
915
             */
916 2
            Html::addCssClass($this->options, $this->errorCssClass);
917
        }
918 59
    }
919
920 51
    private function addErrorCssClassToInput(): void
921
    {
922 51
        if ($this->validationStateOn === 'input') {
923 49
            $attributeName = HtmlForm::getAttributeName($this->attribute);
924
925 49
            if ($this->data->hasErrors($attributeName)) {
926
                /**
927
                 * @psalm-suppress PossiblyNullPropertyAssignmentValue
928
                 */
929 7
                Html::addCssClass($this->inputOptions, $this->errorCssClass);
930
            }
931
        }
932 51
    }
933
934 51
    private function addSuccessCssClassToInput(): void
935
    {
936 51
        if ($this->validationStateOn === 'input') {
937 49
            $attributeName = HtmlForm::getAttributeName($this->attribute);
938
939 49
            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 51
    }
947
948 3
    private function setInputRole(array $options = []): void
949
    {
950 3
        $this->inputOptions['role'] = $options['role'] ?? 'radiogroup';
951 3
    }
952
953 45
    private function skipForInLabel(): void
954
    {
955 45
        if ($this->skipForInLabel) {
956
            $this->labelOptions['for'] = null;
957
        }
958 45
    }
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 55
    private function setAriaAttributes(array $options = []): void
972
    {
973 55
        if ($this->ariaAttribute) {
974 55
            if (!isset($options['aria-required']) && $this->data->isAttributeRequired($this->attribute)) {
975 19
                $this->inputOptions['aria-required'] = 'true';
976
            }
977
978 55
            if (!isset($options['aria-invalid']) && $this->data->hasErrors($this->attribute)) {
979 7
                $this->inputOptions['aria-invalid'] = 'true';
980
            }
981
        }
982 55
    }
983
}
984