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

TYPO3-Headless / headless / 14022693367

23 Mar 2025 08:26PM UTC coverage: 52.345% (-20.8%) from 73.13%
14022693367

Pull #815

github

web-flow
Merge e0fcdaa4a into a15e1c8c4
Pull Request #815: [FEATURE] Add support for f:form.* viewhelper

0 of 600 new or added lines in 15 files covered. (0.0%)

1105 of 2111 relevant lines covered (52.34%)

5.94 hits per line

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

0.0
/Classes/XClass/ViewHelpers/Form/CountrySelectViewHelper.php
1
<?php
2

3
/*
4
 * This file is part of the "headless" Extension for TYPO3 CMS.
5
 *
6
 * For the full copyright and license information, please read the
7
 * LICENSE.md file that was distributed with this source code.
8
 */
9

10
declare(strict_types=1);
11

12
/*
13
 * This file is part of the TYPO3 CMS project.
14
 *
15
 * It is free software; you can redistribute it and/or modify it under
16
 * the terms of the GNU General Public License, either version 2
17
 * of the License, or any later version.
18
 *
19
 * For the full copyright and license information, please read the
20
 * LICENSE.txt file that was distributed with this source code.
21
 *
22
 * The TYPO3 project - inspiring people to share!
23
 */
24

25
namespace FriendsOfTYPO3\Headless\XClass\ViewHelpers\Form;
26

27
use TYPO3\CMS\Core\Country\Country;
28
use TYPO3\CMS\Core\Country\CountryFilter;
29
use TYPO3\CMS\Core\Country\CountryProvider;
30
use TYPO3\CMS\Core\Utility\GeneralUtility;
31
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
32

33
/**
34
 * Renders a :html:`<select>` tag with all available countries as options.
35
 *
36
 * Examples
37
 * ========
38
 *
39
 * Basic usage
40
 * -----------
41
 *
42
 * ::
43
 *
44
 *    <f:form.countrySelect name="country" value="{defaultCountry}" />
45
 *
46
 * Output::
47
 *
48
 *    {
49
 *      "name": "tx_extension_plugin[country]",
50
 *      "type": "countrySelect",
51
 *      "value": "{defaultCountry}",
52
 *      "options": [
53
 *          {
54
 *              "value": "AD",
55
 *              "label": "Andorra",
56
 *              "selected": false
57
 *          },
58
 *          ...
59
 *      ]
60
 *    }
61
 *
62
 * Prioritize countries
63
 * --------------------
64
 *
65
 * Define a list of countries which should be listed as first options in the
66
 * form element::
67
 *
68
 *    {
69
 *      "name": "tx_extension_plugin[country]",
70
 *      "type": "countrySelect",
71
 *      "value": "AT",
72
 *      "options": [
73
 *          {
74
 *              "value": "DE",
75
 *              "label": "Germany",
76
 *              "selected": false
77
 *          },
78
 *          {
79
 *              "value": "AT",
80
 *              "label": "Austria",
81
 *              "selected": true
82
 *          },
83
 *          {
84
 *              "value": "CH",
85
 *              "label": "Switzerland",
86
 *              "selected": false
87
 *          },
88
 *          {
89
 *          "value": "AD",
90
 *          "label": "Andorra",
91
 *          "selected": false
92
 *          },
93
 *          ...
94
 *      ]
95
 *    }
96
 *
97
 *  Additionally, Austria is pre-selected.
98
 *
99
 * Display another language
100
 * ------------------------
101
 *
102
 * A combination of optionLabelField and alternativeLanguage is possible. For
103
 * instance, if you want to show the localized official names but not in your
104
 * default language but in French. You can achieve this by using the following
105
 * combination::
106
 *
107
 *    <f:form.countrySelect
108
 *      name="country"
109
 *      optionLabelField="localizedOfficialName"
110
 *      alternativeLanguage="fr"
111
 *      sortByOptionLabel="true"
112
 *    />
113
 *
114
 * Bind an object
115
 * --------------
116
 *
117
 * You can also use the "property" attribute if you have bound an object to the form.
118
 * See :ref:`<f:form> <typo3-fluid-form>` for more documentation.
119
 */
120
final class CountrySelectViewHelper extends AbstractFormFieldViewHelper
121
{
122
    public function initializeArguments(): void
123
    {
NEW
124
        parent::initializeArguments();
×
NEW
125
        $this->registerArgument('excludeCountries', 'array', 'Array with country codes that should not be shown.', false, []);
×
NEW
126
        $this->registerArgument('onlyCountries', 'array', 'If set, only the country codes in the list are rendered.', false, []);
×
NEW
127
        $this->registerArgument('optionLabelField', 'string', 'If specified, will call the appropriate getter on each object to determine the label. Use "name", "localizedName", "officialName" or "localizedOfficialName"', false, 'localizedName');
×
NEW
128
        $this->registerArgument('sortByOptionLabel', 'boolean', 'If true, List will be sorted by label.', false, false);
×
NEW
129
        $this->registerArgument('errorClass', 'string', 'CSS class to set if there are errors for this ViewHelper', false, 'f3-form-error');
×
NEW
130
        $this->registerArgument('prependOptionLabel', 'string', 'If specified, will provide an option at first position with the specified label.');
×
NEW
131
        $this->registerArgument('prependOptionValue', 'string', 'If specified, will provide an option at first position with the specified value.');
×
NEW
132
        $this->registerArgument('multiple', 'boolean', 'If set multiple options may be selected.', false, false);
×
NEW
133
        $this->registerArgument('required', 'boolean', 'If set no empty value is allowed.', false, false);
×
NEW
134
        $this->registerArgument('prioritizedCountries', 'array', 'A list of country codes which should be listed on top of the list.', false, []);
×
NEW
135
        $this->registerArgument('alternativeLanguage', 'string', 'If specified, the country list will be shown in the given language.');
×
136
    }
137

138
    public function render(): string
139
    {
NEW
140
        $this->data = json_decode(parent::render(), true);
×
141

NEW
142
        if ($this->arguments['required']) {
×
NEW
143
            $this->data['required'] = 'required';
×
144
        }
NEW
145
        $name = $this->getName();
×
NEW
146
        if ($this->arguments['multiple']) {
×
NEW
147
            $this->data['multiple'] = 'multiple';
×
NEW
148
            $name .= '[]';
×
149
        }
NEW
150
        $this->addAdditionalIdentityPropertiesIfNeeded();
×
NEW
151
        $this->setErrorClassAttribute();
×
NEW
152
        $this->registerFieldNameForFormTokenGeneration($name);
×
NEW
153
        $this->setRespectSubmittedDataValue(true);
×
154

NEW
155
        $this->data['name'] = $name;
×
NEW
156
        $this->data['type'] = 'countrySelect';
×
157

NEW
158
        $validCountries = $this->getCountryList();
×
NEW
159
        $options = $this->createOptions($validCountries);
×
NEW
160
        $selectedValue = $this->getValueAttribute();
×
161

NEW
162
        if ($selectedValue !== null) {
×
NEW
163
            $this->data['value'] = $selectedValue;
×
164
        }
165

NEW
166
        $prependOptionTag = $this->renderPrependOptionTag();
×
NEW
167
        $optionArray = [];
×
168

NEW
169
        if (!empty($prependOptionTag)) {
×
NEW
170
            $optionArray[] = $prependOptionTag;
×
171
        }
172

NEW
173
        foreach ($options as $value => $label) {
×
NEW
174
            $optionArray[] = $this->renderOptionTag($value, $label, $value === $selectedValue);
×
175
        }
176

NEW
177
        $this->tag->forceClosingTag(true);
×
NEW
178
        $this->data['options'] = $optionArray;
×
NEW
179
        return json_encode($this->data);
×
180
    }
181

182
    /**
183
     * @param Country[] $countries
184
     * @return array<string, string>
185
     */
186
    protected function createOptions(array $countries): array
187
    {
NEW
188
        $options = [];
×
NEW
189
        foreach ($countries as $code => $country) {
×
NEW
190
            switch ($this->arguments['optionLabelField']) {
×
NEW
191
                case 'localizedName':
×
NEW
192
                    $options[$code] = $this->translate($country->getLocalizedNameLabel());
×
NEW
193
                    break;
×
NEW
194
                case 'name':
×
NEW
195
                    $options[$code] = $country->getName();
×
NEW
196
                    break;
×
NEW
197
                case 'officialName':
×
NEW
198
                    $options[$code] = $country->getOfficialName();
×
NEW
199
                    break;
×
NEW
200
                case 'localizedOfficialName':
×
NEW
201
                    $name = $this->translate($country->getLocalizedOfficialNameLabel());
×
NEW
202
                    if (!$name) {
×
NEW
203
                        $name = $this->translate($country->getLocalizedNameLabel());
×
204
                    }
NEW
205
                    $options[$code] = $name;
×
NEW
206
                    break;
×
207
                default:
NEW
208
                    throw new \TYPO3Fluid\Fluid\Core\ViewHelper\Exception('Argument "optionLabelField" of <f:form.countrySelect> must either be set to "localizedName", "name", "officialName", or "localizedOfficialName".', 1674076708);
×
209
            }
210
        }
NEW
211
        if ($this->arguments['sortByOptionLabel']) {
×
NEW
212
            asort($options, SORT_LOCALE_STRING);
×
213
        } else {
NEW
214
            ksort($options, SORT_NATURAL);
×
215
        }
NEW
216
        if (($this->arguments['prioritizedCountries'] ?? []) !== []) {
×
NEW
217
            $finalOptions = [];
×
NEW
218
            foreach ($this->arguments['prioritizedCountries'] as $countryCode) {
×
NEW
219
                if (isset($options[$countryCode])) {
×
NEW
220
                    $label = $options[$countryCode];
×
NEW
221
                    $finalOptions[$countryCode] = $label;
×
NEW
222
                    unset($options[$countryCode]);
×
223
                }
224
            }
NEW
225
            foreach ($options as $countryCode => $label) {
×
NEW
226
                $finalOptions[$countryCode] = $label;
×
227
            }
NEW
228
            $options = $finalOptions;
×
229
        }
NEW
230
        return $options;
×
231
    }
232

233
    protected function translate(string $label): string
234
    {
NEW
235
        if ($this->arguments['alternativeLanguage']) {
×
NEW
236
            return (string)LocalizationUtility::translate($label, languageKey: $this->arguments['alternativeLanguage']);
×
237
        }
NEW
238
        return (string)LocalizationUtility::translate($label);
×
239
    }
240

241
    /**
242
     * Render prepended option tag
243
     */
244
    protected function renderPrependOptionTag(): array
245
    {
NEW
246
        if ($this->hasArgument('prependOptionLabel')) {
×
NEW
247
            $value = $this->hasArgument('prependOptionValue') ? $this->arguments['prependOptionValue'] : '';
×
NEW
248
            $label = $this->arguments['prependOptionLabel'];
×
NEW
249
            return $this->renderOptionTag((string)$value, (string)$label, false);
×
250
        }
NEW
251
        return [];
×
252
    }
253

254
    /**
255
     * Render one option tag
256
     *
257
     * @param string $value value attribute of the option tag (will be escaped)
258
     * @param string $label content of the option tag (will be escaped)
259
     * @param bool $isSelected specifies whether to add selected attribute
260
     * @return array the rendered option tag
261
     */
262
    protected function renderOptionTag(string $value, string $label, bool $isSelected): array
263
    {
NEW
264
        return [
×
NEW
265
            'value' => $value,
×
NEW
266
            'label' => $label,
×
NEW
267
            'selected' => $isSelected,
×
NEW
268
        ];
×
269
    }
270

271
    /**
272
     * @return Country[]
273
     */
274
    protected function getCountryList(): array
275
    {
NEW
276
        $filter = new CountryFilter();
×
NEW
277
        $filter->setOnlyCountries($this->arguments['onlyCountries'] ?? [])
×
NEW
278
            ->setExcludeCountries($this->arguments['excludeCountries'] ?? []);
×
NEW
279
        return GeneralUtility::makeInstance(CountryProvider::class)->getFiltered($filter);
×
280
    }
281
}
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