Test Failed
Push — validate-by-validator ( a015d2 )
by Dmitriy
03:11
created

Field::addSuccessCssClassToInput()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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