I'm working on what I thought would be a relatively simple plugin; a custom fieldtype that would have 8 "sub-groups", each of which would consist of a numeric text field and a dropdown field with a few predefined options.

At the Twig/templating end of things, I'd like it to work like this:

{{ entry.customField.subGroup.text }} // (outputs the text field for that sub-group)
{{ entry.customField.subGroup.dropdown }} // (outputs the dropdown field for that sub-group)
{{ entry.customField.subGroup }} // (outputs a combined string of `subGroup.text` and `subGroup.dropdown`)

Additionally, in the field's settings, I have 8 lightswitch fields to disable any individual sub-group (the {% if %} statement wrapping the code below).

All together, I'm putting it all together in the CustomField_input.twig file like this:

{% set dropdownValues = [
    { label: 'Foo', value: 'foo' },
    { label: 'Bar', value: 'bar' },
    { label: 'Baz', value: 'baz' }
] %}
{% if field.subGroup %}
    {% embed '_includes/forms/field' with {
        label: 'Sub-Group A'|t,
        instructions: "A sub-group of the field"|t,
        id: 'subGroupA'
    } %}
        {% import "_includes/forms" as forms %}
        {% block input %}
            <div class="flex">
                {{ forms.textField({
                    id: 'subGroupAText',
                    name: 'subGroupAText',
                    type: 'number',
                    value: field.subGroupAText ?? ''
                }) }}
                {{ forms.selectField({
                    id: 'subGroupADropdown',
                    name: 'subGroupADropdown',
                    options: dropdownValues,
                    value: field.subGroupADropdown ?? 'defaultDropdownValue'
                }) }}
        {% endblock %}
    {% endembed %}
{% endif %}

First off, the above code renders the fields I want correctly, but no values get saved. Not sure what that's about. But before I resolve that, I suspect I should figure out what I need to alter in the above code to get the nested aspect of the custom field. Is it as simple as changing:

id: 'subGroupAText',
name: 'subGroupAText',

value: field.subGroupAText ?? ''

...to this...?

id: 'subGroupA[text]',
name: 'subGroupA[text]',

value: field.subGroupA[text] ?? ''

1 Answer 1


First, from your field type’s inputHtml() method, make sure you’re passing the field instance to the template as a field variable, and the value as a value variable:

protected function fieldHtml($value, ElementInterface $element = null): string
    return Craft::$app->view->renderTemplate('plugin-handle/path/to/CustomField_input', [
        'field' => $this,
        'value' => $value,

Then from your template, each of your input names should resemble how you want them to be accessed from the field data – namespaced by the field handle, and you should pull the values the same way you want to on the front end:

Input name Input value
"#{field.handle}[subGroupA][text]" value.subGroupA.text
"#{field.handle}[subGroupA][dropdown]" value.subGroupA.dropdown

You’ll also want to “namespace” your input IDs by prefixing them with the field handle, in case the same field type is used multiple times in a field layout.

Here’s what it will look like:

{{ forms.textField({
    id: "#{field.handle}-subGroupAText",
    name: "#{field.handle}[subGroupA][text]",
    type: 'number',
    value: value.subGroupA.text ?? '',
}) }}
{{ forms.selectField({
    id: "#{field.handle}-subGroupADropdown",
    name: "#{field.handle}[subGroupA][dropdown]",
    options: dropdownValues,
    value: field.subGroupA.dropdown ?? 'defaultDropdownValue',
}) }}

The field will need to store its data as JSON. To get that working…

  • Your getContentColumnType() method should return text:

    public function getContentColumnType()
        return \yii\db\Schema::TYPE_TEXT;
  • Your normalizeValue() method should check if the value is a string, and if so, JSON-decode it:

    public function normalizeValue($value, ElementInterface $element = null)
        if (is_string($value)) {
            $value = \craft\helpers\Json::decode($value);
        return $value;
  • Wow, thanks for the detailed write-up, BK! After having done my best to implement it and still not getting data saved, I have a few questions: 1) is protected function inputHtml() (I assume the fieldHtml in the code is a typo) the same as the public function getInputHtml() that pluginfactory.io started me out with, in plugin-handle/src/fields/CustomFieldField.php? 2) value: value.subGroupA... or value: field.subGroupA...? 3) getContentColumnType() was set to TYPE_STRING; is that different from TYPE_TEXT? Thanks again!!!!
    – Sandwich
    Commented Jul 12, 2021 at 12:28
  • @Sandwich 1) Yeah, in general the protected one is recommended, but either one will work. I’ll PR pluginfactory. 2) value 3) yeah string will give you a VARCHAR column; TEXT is better for storing JSON. Commented Jul 12, 2021 at 14:36
  • @Sandwich Just updated the example with namespaced input IDs as well. Forgot you’ll want to do that to ensure you don’t get conflicts if the same field type is used multiple times within the same field layout. Commented Jul 12, 2021 at 14:54
  • Ok, that got things working! I think the main thing left is to figure out how to get {{ entry.fieldHandle.subGroupA to custom-output the same as {{ entry.fieldHandle.subGroupA.text ~ entry.fieldHandle.subGroupA.dropdown }}. I haven't done any searching yet on the subject (although I'm not quite sure what to search for); could you point me in the right direction?
    – Sandwich
    Commented Jul 12, 2021 at 17:18
  • @Sandwich That would be a bit more complicated; you would need to start normalizing your field value into nested objects using custom classes in normalizeValue(). See Craft’s built-in Color field type + the ColorData model for a relatively simple example. Commented Jul 12, 2021 at 20:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.