• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

contributte / forms-bootstrap / #54

11 Feb 2024 08:44PM UTC coverage: 83.494% (+0.1%) from 83.353%
#54

Pull #88

github

dakorpar
fix: compatibility with nette/component-model 3.1
Pull Request #88: fix: compatibility with nette/component-model 3.1

693 of 830 relevant lines covered (83.49%)

5.72 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.88
/src/BootstrapRenderer.php
1
<?php declare(strict_types = 1);
2

3
namespace Contributte\FormsBootstrap;
4

5
use Contributte\FormsBootstrap\Enums\BootstrapVersion;
6
use Contributte\FormsBootstrap\Enums\RendererConfig as Cnf;
7
use Contributte\FormsBootstrap\Enums\RendererOptions;
8
use Contributte\FormsBootstrap\Enums\RenderMode;
9
use Contributte\FormsBootstrap\Grid\BootstrapRow;
10
use Contributte\FormsBootstrap\Inputs\IValidationInput;
11
use Nette;
12
use Nette\Forms\Control;
13
use Nette\Forms\Controls\BaseControl;
14
use Nette\Forms\Form;
15
use Nette\Forms\FormRenderer;
16
use Nette\Utils\Html;
17

18
/**
19
 * Converts a Form into Bootstrap 4 HTML output.
20
 *
21
 * @property int        $mode
22
 * @property string     $gridBreakPoint    Bootstrap grid breakpoint for side-by-side view. Default is 'sm'.
23
 *           NULL means not to use a breakpoint
24
 * @property-read array $config
25
 * @property-read array $configOverride
26
 * @property bool       $groupHidden       if true, hidden fields will be grouped at the end. If false,
27
 *           hidden fields are placed where they were added. Default is true.
28
 */
29
class BootstrapRenderer implements FormRenderer
30
{
31

32
        use Nette\SmartObject;
33

34
        public const DEFAULT_LABEL_COLUMNS = 3;
35
        public const DEFAULT_CONTROL_COLUMNS = 9;
36

37
        /**
38
         * Bootstrap grid breakpoint for side-by-side view
39
         *
40
         * @var string
41
         */
42
        protected $gridBreakPoint = 'sm';
43

44
        /** @var BootstrapForm */
45
        protected $form;
46

47
        /** @var int */
48
        protected $labelColumns = self::DEFAULT_LABEL_COLUMNS;
49

50
        /** @var int */
51
        protected $controlColumns = self::DEFAULT_CONTROL_COLUMNS;
52

53
        /** @var int current render mode */
54
        private $renderMode = RenderMode::SIDE_BY_SIDE_MODE;
55

56
        /** @var bool */
57
        private $groupHidden = true;
58

59
        public function __construct(int $mode = RenderMode::VERTICAL_MODE)
60
        {
61
                $this->setMode($mode);
45✔
62
        }
63

64
        /**
65
         * Sets the form for which to render. Used only if a specific function of the renderer must be executed
66
         * outside of render(), such as during assisted manual rendering.
67
         */
68
        public function attachForm(BootstrapForm $form): void
69
        {
70
                $this->form = $form;
12✔
71
        }
72

73
        /**
74
         * Turns configuration or and existing element and configures it appropriately
75
         *
76
         * @param string[]|string $config top-level config key
77
         * @param Html|null $el elem to config.
78
         */
79
        public function configElem($config, ?Html $el = null): ?Html
80
        {
81
                if (is_scalar($config)) {
13✔
82
                        $config = $this->fetchConfig($config);
13✔
83
                }
84

85
                // first define which element we want to work with
86
                if (isset($config[Cnf::ELEMENT_NAME])) {
13✔
87
                        $name = $config[Cnf::ELEMENT_NAME];
11✔
88
                        if (!$el) {
11✔
89
                                // element does not exist, so create it
90
                                $el = Html::el($name);
10✔
91
                        } else {
92
                                // element exists, but we want to change its name
93
                                $el->setName($name);
11✔
94
                        }
95
                }
96

97
                if ($el instanceof Html && $el !== null) {
13✔
98
                        $origClass = null;
13✔
99
                        // go through all config and configure element accordingly
100
                        foreach ($config as $key => $value) {
13✔
101
                                if (in_array($key, [Cnf::CLASS_SET, Cnf::CLASS_ADD, Cnf::CLASS_ADD, Cnf::CLASS_REMOVE])) {
11✔
102
                                        // we'll be working with classes, we must standardize everything to arrays, not strings for the sake of sanity
103
                                        if (!is_array($value)) {
10✔
104
                                                // configuration may contain classes as strings, but we always work with arrays
105
                                                $value = [$value];
10✔
106
                                        }
107

108
                                        $origClass = $el->getAttribute('class');
10✔
109
                                        $newClass = $origClass;
10✔
110
                                        if ($origClass === null) {
10✔
111
                                                // class is not set
112
                                                $newClass = [];
10✔
113
                                        } elseif (!is_array($origClass)) {
9✔
114
                                                // class is set, but not a array
115
                                                $newClass = explode(' ', $el->getAttribute('class'));
1✔
116
                                        }
117

118
                                        $el->setAttribute('class', $newClass);
10✔
119
                                        $origClass = $newClass;
10✔
120
                                }
121

122
                                if ($key === Cnf::CLASS_SET) {
11✔
123
                                        $el->setAttribute('class', $value);
10✔
124
                                } elseif ($key === Cnf::CLASS_ADD) {
11✔
125
                                        $el->setAttribute(
10✔
126
                                                'class',
10✔
127
                                                array_merge($origClass, $value)
10✔
128
                                        );
10✔
129
                                } elseif ($key === Cnf::CLASS_REMOVE) {
11✔
130
                                        $el->setAttribute(
×
131
                                                'class',
×
132
                                                array_diff($origClass, $value)
×
133
                                        );
×
134
                                } elseif ($key === Cnf::ATTRIBUTES) {
11✔
135
                                        foreach ($value as $attr => $attrVal) {
×
136
                                                $el->setAttribute($attr, $attrVal);
×
137
                                        }
138
                                }
139
                        }
140
                }
141

142
                // el may be null, but maybe it has a container defined
143
                if (isset($config[Cnf::CONTAINER])) {
13✔
144
                        $container = $this->configElem($config[Cnf::CONTAINER], null);
×
145
                        if ($container !== null && $el !== null) {
×
146
                                $elClone = clone $el;
×
147
                                $container->setHtml($elClone);
×
148
                        }
149

150
                        $el = $container;
×
151
                }
152

153
                return $el;
13✔
154
        }
155

156
        /**
157
         * @return mixed[]
158
         */
159
        public function getConfig(): array
160
        {
161
                return [
13✔
162
                        Cnf::FORM => [],
13✔
163
                        Cnf::GROUP => [
13✔
164
                                Cnf::ELEMENT_NAME => 'fieldset',
13✔
165
                        ],
13✔
166
                        Cnf::GROUP_LABEL => [
13✔
167
                                Cnf::ELEMENT_NAME => 'legend',
13✔
168
                        ],
13✔
169

170
                        Cnf::GRID_ROW => [
13✔
171
                                Cnf::ELEMENT_NAME => 'div',
13✔
172
                                Cnf::CLASS_ADD => BootstrapForm::getBootstrapVersion() === BootstrapVersion::V5 ? 'row' : 'form-row',
13✔
173
                        ],
13✔
174
                        Cnf::GRID_CELL => [
13✔
175
                                Cnf::ELEMENT_NAME => 'div',
13✔
176
                        ],
13✔
177

178
                        Cnf::FORM_OWN_ERRORS => [],
13✔
179
                        Cnf::FORM_OWN_ERROR => [
13✔
180
                                Cnf::ELEMENT_NAME => 'div',
13✔
181
                                Cnf::CLASS_SET => ['alert', 'alert-danger'],
13✔
182
                        ],
13✔
183

184
                        Cnf::PAIR => [
13✔
185
                                Cnf::ELEMENT_NAME => 'div',
13✔
186
                                Cnf::CLASS_SET => BootstrapForm::getBootstrapVersion() === BootstrapVersion::V5 ? 'mb-3' : 'form-group',
13✔
187
                        ],
13✔
188
                        Cnf::LABEL => [
13✔
189
                                Cnf::ELEMENT_NAME => 'label',
13✔
190
                                Cnf::CLASS_SET => BootstrapForm::getBootstrapVersion() === BootstrapVersion::V5 ? 'form-label' : null,
13✔
191
                        ],
13✔
192

193
                        Cnf::INPUT => [],
13✔
194
                        // inputs which are normally inline elements (after bootstrap classes are applied)
195
                        Cnf::INPUT_VALID => [
13✔
196
                                Cnf::CLASS_ADD => 'is-valid',
13✔
197
                        ],
13✔
198
                        Cnf::INPUT_INVALID => [
13✔
199
                                Cnf::CLASS_ADD => 'is-invalid',
13✔
200
                        ],
13✔
201

202
                        Cnf::DESCRIPTION => [
13✔
203
                                Cnf::ELEMENT_NAME => 'small',
13✔
204
                                Cnf::CLASS_SET => ['form-text', 'text-muted'],
13✔
205
                        ],
13✔
206

207
                        Cnf::FEEDBACK => [
13✔
208
                                Cnf::ELEMENT_NAME => 'div',
13✔
209
                        ],
13✔
210
                        Cnf::FEEDBACK_VALID => [
13✔
211
                                Cnf::CLASS_ADD => 'valid-feedback',
13✔
212
                        ],
13✔
213
                        Cnf::FEEDBACK_INVALID => [
13✔
214
                                Cnf::CLASS_ADD => 'invalid-feedback',
13✔
215
                        ],
13✔
216

217
                        // empty wrapper,  but it gets utilized in side-by side and inline mode
218
                        Cnf::NON_LABEL => [
13✔
219
                                Cnf::ELEMENT_NAME => null,
13✔
220
                        ],
13✔
221
                ];
13✔
222
        }
223

224
        /**
225
         * @return mixed[]
226
         */
227
        public function getConfigOverride(): array
228
        {
229
                if ($this->gridBreakPoint !== null) {
13✔
230
                        $labelColClass = 'col-' . $this->gridBreakPoint . '-' . $this->labelColumns;
13✔
231
                        $nonLabelColClass = 'col-' . $this->gridBreakPoint . '-' . $this->controlColumns;
13✔
232
                } else {
233
                        $labelColClass = 'col-' . $this->labelColumns;
×
234
                        $nonLabelColClass = 'col-' . $this->controlColumns;
×
235
                }
236

237
                $labelColClass .= ' col-form-label';
13✔
238

239
                return [
13✔
240
                        RenderMode::INLINE => [
13✔
241
                                Cnf::FORM => [
13✔
242
                                        Cnf::CLASS_ADD => 'form-inline',
13✔
243
                                ],
13✔
244
                                Cnf::NON_LABEL => [
13✔
245
                                        Cnf::ELEMENT_NAME => 'div',
13✔
246
                                ],
13✔
247
                        ],
13✔
248
                        RenderMode::SIDE_BY_SIDE_MODE => [
13✔
249
                                Cnf::PAIR => [
13✔
250
                                        Cnf::CLASS_ADD => 'row',
13✔
251
                                ],
13✔
252
                                Cnf::LABEL => [
13✔
253
                                        Cnf::CLASS_ADD => $labelColClass,
13✔
254
                                ],
13✔
255
                                Cnf::NON_LABEL => [
13✔
256
                                        Cnf::ELEMENT_NAME => 'div',
13✔
257
                                        Cnf::CLASS_SET => $nonLabelColClass,
13✔
258
                                ],
13✔
259
                        ],
13✔
260
                        RenderMode::VERTICAL_MODE => [],
13✔
261
                ];
13✔
262
        }
263

264
        public function getGridBreakPoint(): string
265
        {
266
                return $this->gridBreakPoint;
×
267
        }
268

269
        /**
270
         * @param string $gridBreakPoint null for none
271
         */
272
        public function setGridBreakPoint(string $gridBreakPoint): BootstrapRenderer
273
        {
274
                $this->gridBreakPoint = $gridBreakPoint;
×
275

276
                return $this;
×
277
        }
278

279
        /**
280
         * Returns render mode
281
         *
282
         * @see RenderMode
283
         */
284
        public function getMode(): int
285
        {
286
                return $this->renderMode;
1✔
287
        }
288

289
        /**
290
         * @see BootstrapRenderer::$groupHidden
291
         */
292
        public function isGroupHidden(): bool
293
        {
294
                return $this->groupHidden;
×
295
        }
296

297
        /**
298
         * @see BootstrapRenderer::$groupHidden
299
         */
300
        public function setGroupHidden(bool $groupHidden): BootstrapRenderer
301
        {
302
                $this->groupHidden = $groupHidden;
×
303

304
                return $this;
×
305
        }
306

307
        /**
308
         * Provides complete form rendering.
309
         *
310
         * @param BootstrapForm $form
311
         */
312
        public function render(Form $form): string
313
        {
314
                $this->attachForm($form);
12✔
315

316
                $s = '';
12✔
317
                $s .= $this->renderBegin();
12✔
318
                $s .= $this->renderFeedback();
12✔
319
                $s .= $this->renderBody();
12✔
320
                $s .= $this->renderEnd();
12✔
321

322
                return $s;
12✔
323
        }
324

325
        /**
326
         * Renders form begin.
327
         */
328
        public function renderBegin(): string
329
        {
330
                foreach ($this->form->getControls() as $control) {
12✔
331
                        $control->setOption(RendererOptions::_RENDERED, false);
12✔
332
                }
333

334
                /** @var Html $el */
335
                $el = $this->configElem('form', $this->form->getElementPrototype());
12✔
336

337
                if ($this->form->isMethod('get')) {
12✔
338
                        $el->action = (string) $el->action;
1✔
339
                        /** @noinspection PhpUndefinedFieldInspection */
340
                        $query = parse_url($el->action, PHP_URL_QUERY);
1✔
341
                        /** @noinspection PhpUndefinedFieldInspection */
342
                        $el->action = str_replace('?' . $query, '', $el->action);
1✔
343

344
                        $s = '';
1✔
345
                        $params = ($query === null || $query === false)
1✔
346
                                ? []
×
347
                                : (preg_split('#[;&]#', $query, -1, PREG_SPLIT_NO_EMPTY) ?: []);
1✔
348
                        foreach ($params as $param) {
1✔
349
                                $parts = explode('=', $param, 2);
1✔
350
                                $name = urldecode($parts[0]);
1✔
351
                                if (!isset($this->form[$name])) {
1✔
352
                                        $s .= Html::el('input', ['type' => 'hidden', 'name' => $name, 'value' => urldecode($parts[1])]);
1✔
353
                                }
354
                        }
355

356
                        return $el->startTag() . PHP_EOL . $s;
1✔
357
                }
358

359
                return $el->startTag();
11✔
360
        }
361

362
        /**
363
         * Renders form body.
364
         */
365
        public function renderBody(): string
366
        {
367
                $translator = $this->form->getTranslator();
12✔
368

369
                // first render groups. They will mark their controls as rendered
370
                $groups = Html::el();
12✔
371
                foreach ($this->form->getGroups() as $group) {
12✔
372
                        if (!$group->getControls() || !$group->getOption(RendererOptions::VISUAL)) {
1✔
373
                                continue;
×
374
                        }
375

376
                        //region getting container
377

378
                        $container = $group->getOption(RendererOptions::CONTAINER, null);
1✔
379
                        if (is_string($container)) {
1✔
380
                                $container = $this->configElem(Cnf::GROUP, Html::el($container));
×
381
                        } elseif ($container instanceof Html) {
1✔
382
                                $container = $this->configElem(Cnf::GROUP, $container);
×
383
                        } else {
384
                                $container = $this->getElem(Cnf::GROUP);
1✔
385
                        }
386

387
                        $container->setAttribute('id', $group->getOption(RendererOptions::ID));
1✔
388

389
                        //endregion
390

391
                        //region label
392
                        $label = $group->getOption(RendererOptions::LABEL);
1✔
393
                        if ($label instanceof Html) {
1✔
394
                                $label = $this->configElem(Cnf::GROUP_LABEL, $label);
×
395
                        } elseif (is_string($label)) {
1✔
396
                                if ($translator !== null) {
1✔
397
                                        $label = $translator->translate($label);
×
398
                                }
399

400
                                $labelHtml = $this->getElem(Cnf::GROUP_LABEL);
1✔
401
                                $labelHtml->setText($label);
1✔
402
                                $label = $labelHtml;
1✔
403
                        }
404

405
                        //endregion
406

407
                        if (!empty($label)) {
1✔
408
                                $container->addHtml($label);
1✔
409
                        }
410

411
                        $controls = $this->renderControls($group);
1✔
412
                        if (!empty($controls)) {
1✔
413
                                $container->addHtml($controls);
1✔
414
                        }
415

416
                        $groups->addHtml($container);
1✔
417
                }
418

419
                // we now know which ones to skip, so render the rest
420
                $formControls = $this->renderControls($this->form);
12✔
421

422
                $out = Html::el();
12✔
423
                if (!empty($formControls)) {
12✔
424
                        $out->addHtml($formControls);
12✔
425
                }
426

427
                if (!empty($groups)) {
12✔
428
                        $out->addHtml($groups);
12✔
429
                }
430

431
                return (string) $out;
12✔
432
        }
433

434
        /**
435
         * Renders 'control' part of visual row of controls.
436
         */
437
        public function renderControl(BaseControl $control): string
438
        {
439
                /** @var Html $controlHtml */
440
                $controlHtml = $control->getControl();
12✔
441
                $control->setOption(RendererOptions::_RENDERED, true);
12✔
442
                if (($this->form->showValidation || $control->hasErrors()) && $control instanceof IValidationInput) {
12✔
443
                        $controlHtml = $control->showValidation($controlHtml);
1✔
444
                }
445

446
                $controlHtml = $this->configElem(Cnf::INPUT, $controlHtml);
12✔
447

448
                return (string) $controlHtml;
12✔
449
        }
450

451
        /**
452
         * Renders group of controls.
453
         *
454
         * @param  Nette\Forms\Container|Nette\Forms\ControlGroup $parent
455
         */
456
        public function renderControls($parent): string
457
        {
458
                if (!($parent instanceof Nette\Forms\Container || $parent instanceof Nette\Forms\ControlGroup)) {
12✔
459
                        throw new Nette\InvalidArgumentException('Argument must be Nette\Forms\Container or Nette\Forms\ControlGroup instance.');
×
460
                }
461

462
                $html = Html::el();
12✔
463
                $hidden = Html::el();
12✔
464

465
                // note that these are NOT form groups, these are groups specified to group
466
                foreach ($parent->getControls() as $control) {
12✔
467
                        if ($control->getOption(RendererOptions::_RENDERED, false)) {
12✔
468
                                continue;
3✔
469
                        }
470

471
                        if ($control instanceof BootstrapRow) {
12✔
472
                                $html->addHtml($control->render());
3✔
473
                        } else {
474
                                if ($control->getOption(RendererOptions::TYPE) === 'hidden') {
12✔
475
                                        $isHidden = true;
12✔
476
                                        $pairHtml = $this->renderControl($control);
12✔
477
                                } else {
478
                                        $pairHtml = $this->renderPair($control);
7✔
479
                                        $isHidden = false;
7✔
480
                                }
481

482
                                if ($this->groupHidden && $isHidden) {
12✔
483
                                        $hidden->addHtml($pairHtml);
12✔
484
                                } else {
485
                                        $html->addHtml($pairHtml);
7✔
486
                                }
487
                        }
488
                }
489

490
                $html->addHtml($hidden);
12✔
491

492
                return (string) $html;
12✔
493
        }
494

495
        /**
496
         * Renders form end.
497
         */
498
        public function renderEnd(): string
499
        {
500
                return $this->form->getElementPrototype()->endTag() . "\n";
12✔
501
        }
502

503
        /**
504
         * Renders 'label' part of visual row of controls.
505
         */
506
        public function renderLabel(BaseControl $control): Html
507
        {
508
                if ($control->caption === null) {
10✔
509
                        return Html::el();
1✔
510
                }
511

512
                $controlLabel = $control->getLabel();
9✔
513

514
                if ($controlLabel instanceof Html && $controlLabel->getName() === 'label') {
9✔
515
                        // the control has already provided us with the element, no need to create our own
516
                        $controlLabel = $this->configElem(Cnf::LABEL, $controlLabel);
4✔
517
                        // just configure it to suit our needs
518

519
                        return $controlLabel;
4✔
520
                }
521

522
                if ($controlLabel === null) {
5✔
523
                        if (method_exists($control, 'allignWithInputControls') && $control->allignWithInputControls()) {
5✔
524
                                return $this->configElem(Cnf::LABEL, null);
2✔
525
                        }
526

527
                        return Html::el();
3✔
528
                }
529

530
                // the control doesn't give us <label>, se we'll create our own
531
                $labelHtml = $this->getElem(Cnf::LABEL);
×
532
                if ($controlLabel) {
×
533
                        $labelHtml->setHtml($controlLabel);
×
534
                }
535

536
                return $labelHtml;
×
537
        }
538

539
        /**
540
         * Renders single visual row.
541
         */
542
        public function renderPair(BaseControl $control): string
543
        {
544
                $pairHtml = $this->configElem(Cnf::PAIR);
10✔
545

546
                $pairHtml->id = $control->getOption(RendererOptions::ID);
10✔
547

548
                $labelHtml = $this->renderLabel($control);
10✔
549
                if (!empty($labelHtml)) {
10✔
550
                        $pairHtml->addHtml($labelHtml);
10✔
551
                }
552

553
                $nonLabel = $this->getElem(Cnf::NON_LABEL);
10✔
554

555
                //region non-label parts
556
                $controlHtml = $this->renderControl($control);
10✔
557
                $feedbackHtml = $this->renderFeedback($control);
10✔
558
                $descriptionHtml = $this->renderDescription($control);
10✔
559

560
                if (!empty($controlHtml)) {
10✔
561
                        $nonLabel->addHtml($controlHtml);
10✔
562
                }
563

564
                if (!empty($feedbackHtml)) {
10✔
565
                        $nonLabel->addHtml($feedbackHtml);
1✔
566
                }
567

568
                if (!empty($descriptionHtml)) {
10✔
569
                        $nonLabel->addHtml($descriptionHtml);
×
570
                }
571

572
                //endregion
573

574
                if (!empty($nonLabel)) {
10✔
575
                        $pairHtml->addHtml($nonLabel);
10✔
576
                }
577

578
                return $pairHtml->render(0);
10✔
579
        }
580

581
        /**
582
         * Set how many of Bootstrap rows shall the label and control occupy
583
         */
584
        public function setColumns(int $label, ?int $control = null): void
585
        {
586
                if ($control === null) {
×
587
                        $control = 12 - $label;
×
588
                }
589

590
                $this->labelColumns = $label;
×
591
                $this->controlColumns = $control;
×
592
        }
593

594
        /**
595
         * Sets render mode
596
         *
597
         * @param int $renderMode RenderMode
598
         * @see RenderMode
599
         */
600
        public function setMode(int $renderMode): void
601
        {
602
                $this->renderMode = $renderMode;
45✔
603
        }
604

605
        /**
606
         * Fetch config tailored for current render mode
607
         *
608
         * @param string $key first-level key of $this->config
609
         * @return mixed[]
610
         */
611
        protected function fetchConfig(string $key): array
612
        {
613
                $config = $this->config[$key];
13✔
614

615
                if (isset($this->configOverride[$this->renderMode][$key])) {
13✔
616
                        $override = $this->configOverride[$this->renderMode][$key];
9✔
617
                        $config = array_merge($config, $override);
9✔
618
                }
619

620
                return $config;
13✔
621
        }
622

623
        /**
624
         * Get element based on its first-level key
625
         *
626
         * @param string $additionalKeys config will be overridden in this order
627
         */
628
        protected function getElem(string $key, ...$additionalKeys): ?Html
629
        {
630
                $el = $this->configElem($key, Html::el());
10✔
631

632
                foreach ($additionalKeys as $additionalKey) {
10✔
633
                        $config = $this->fetchConfig($additionalKey);
1✔
634
                        $el = $this->configElem($config, $el);
1✔
635
                }
636

637
                return $el;
10✔
638
        }
639

640
        /**
641
         * Renders control description (help text)
642
         */
643
        protected function renderDescription(BaseControl $control): ?Html
644
        {
645
                $description = $control->getOption(RendererOptions::DESCRIPTION);
10✔
646
                if (is_string($description)) {
10✔
647
                        if ($control instanceof BaseControl) {
×
648
                                $description = $control->translate($description);
×
649
                        }
650
                } elseif (!$description instanceof Html) {
10✔
651
                        $description = '';
10✔
652
                }
653

654
                if (!empty($description)) {
10✔
655
                        $el = $this->getElem(Cnf::DESCRIPTION);
×
656
                        $el->setHtml($description);
×
657

658
                        return $el;
×
659
                }
660

661
                return null;
10✔
662
        }
663

664
        /**
665
         * Renders valid or invalid feedback of form or control
666
         *
667
         * @param BaseControl|null $control null = whole form
668
         */
669
        protected function renderFeedback(?BaseControl $control = null): ?Html
670
        {
671
                $isValid = true;
12✔
672
                $showFeedback = false;
12✔
673
                $messages = [];
12✔
674

675
                if ($control instanceof Control) {
12✔
676
                        // specific control
677

678
                        if ($control->hasErrors()) {
10✔
679
                                // control is invalid, mark it that way
680
                                $isValid = false;
1✔
681
                                $showFeedback = true;
1✔
682
                                $messages = $control->getErrors();
1✔
683
                        } elseif ($this->form->showValidation) {
9✔
684
                                $isValid = true;
×
685
                                // control is valid and we want to explicitly show that it's valid
686
                                $message = $control->getOption(RendererOptions::FEEDBACK_VALID);
×
687
                                if ($message) {
×
688
                                        $messages = [$message];
×
689
                                        $showFeedback = true;
×
690
                                } else {
691
                                        $showFeedback = false;
×
692
                                }
693
                        }
694

695
                        if ($showFeedback && count($messages)) {
10✔
696
                                $el = $isValid
1✔
697
                                        ? $this->getElem(Cnf::FEEDBACK, Cnf::FEEDBACK_VALID)
×
698
                                        : $this->getElem(Cnf::FEEDBACK, Cnf::FEEDBACK_INVALID);
1✔
699

700
                                foreach ($messages as $message) {
1✔
701
                                        if ($message instanceof Html) {
1✔
702
                                                $el->addHtml($message);
×
703
                                        } else {
704
                                                $el->addText($message);
1✔
705
                                        }
706

707
                                        $el->addHtml('<br>');
1✔
708
                                }
709

710
                                return $el;
1✔
711
                        } else {
712
                                return null;
9✔
713
                        }
714
                } elseif ($control === null) {
12✔
715
                        // whole form
716
                        $form = $this->form;
12✔
717

718
                        if ($form->hasErrors()) {
12✔
719
                                $showFeedback = true;
1✔
720
                                $messages = $form->getOwnErrors();
1✔
721
                        } else {
722
                                $showFeedback = false;
11✔
723
                                // this doesn't make sense, form is sent if it's valid
724
                        }
725

726
                        if ($showFeedback && count($messages)) {
12✔
727
                                $el = $this->getElem(Cnf::FORM_OWN_ERRORS);
×
728
                                $msgTemplate = $this->getElem(Cnf::FORM_OWN_ERROR);
×
729

730
                                foreach ($messages as $message) {
×
731
                                        $messageHtml = clone $msgTemplate;
×
732
                                        if ($message instanceof Html) {
×
733
                                                $messageHtml->setHtml($message);
×
734
                                        } else {
735
                                                $messageHtml->setText($message);
×
736
                                        }
737

738
                                        $el->addHtml($messageHtml);
×
739
                                }
740

741
                                return $el;
×
742
                        } else {
743
                                return null;
12✔
744
                        }
745
                } else {
746
                        throw new Nette\NotImplementedException('renderer is unable to render feedback for ' . gettype($control));
×
747
                }
748
        }
749

750
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc