1

I want to be able to output different values for my fieldtype depending on the twig attribute. So {{ entry.myField }} would output AK by default, but {{ entry.myField.label }} would output Alaska. Currently I'm using the normalizeValue() method to handle all this, but I need to know which attribute (if any) is being called by the twig template. How do I get that information?

A very basic example of what I'm thinking of would be:

public function normalizeValue($value, ElementInterface $element = null)
{
    $states = array(
        'AK' => 'Alaska',
        'AL' => 'Alabama'
    );

    if( twig.attribute == 'label' )
    {
        return $states[$value];
    }
    else
    {
        return $value;
    }
}
2

Because of several reasons it won't work the way you are going to do it. The easiest approach is to return an object or an array instead of single value in your normalize functions that contains the value and the states. Craft does the same with nearly all fields. They values are objects with a _toString function so you don't even know you have an object rather than a string

| improve this answer | |
  • Can you give me an example of what that array might look like? – kmgdev Sep 24 '18 at 19:35
  • 1
    It can look however you like (?) ['foo' => $value, 'bar' => $states] then you can do entry.field.foo to display the value and entry.fieldHandle.bar.AL to display Alabama – Robin Schambach Sep 24 '18 at 21:24
  • If I return an array then I get an Illegal offset type php error when I try to save a field from the publish page. Here's my code with the array: gist.github.com/kgrote/290abdfc316ceab15134cd711ea0cf16 – kmgdev Sep 24 '18 at 23:54
  • 1
    It would be useful to provide a stack trace and the line of the error – Robin Schambach Sep 26 '18 at 21:40
  • 1
    Because it will produce unnecessary overhead and it isn't a reliable functionality in modern PHP projects anymore to include a file an use its variables unless you return the array. You could create a config file with the settings for your field and do it that way – Robin Schambach Sep 28 '18 at 20:33
3

Robin's answer is technically correct but I wanted to add a little more detail on how I got it working.

TL;DR

public function normalizeValue($value, ElementInterface $element = null)
{
    if ( $value instanceof SingleOptionFieldData ) 
    {
        return $value;
    }

    $nakedOpts = array
    (
        "AL" => "Alabama", 
        "AK" => "Alaska", 
        "AS" => "American Samoa", 
        "AZ" => "Arizona", 
        "AR" => "Arkansas"
    );

    foreach( $nakedOpts as $optval => $label )
    {
        $isSelected = ($value == $optval) ? true : false;
        $this->options[] = new OptionData($label, $optval, $isSelected);
    }

    if( !empty($value) )
    {
        $value = new SingleOptionFieldData($nakedOpts[$value], $value, true);
        $value->setOptions($this->options);
    }

    return $value;
}

Full Explanation

I ended up using Craft's native OptionData and SingleOptionFieldData classes to convert my values and options into objects that Craft knows how to work with.

So to start with I added these declarations at the top of my fieldtype file:

use craft\fields\data\OptionData;
use craft\fields\data\SingleOptionFieldData;

Now in your fieldtype file you need to do two things:

  1. Convert your options to an array of OptionData objects
  2. Convert the $value to a SingleOptionFieldData object

So we need to take our "naked" option array (our array of US states) and convert it to an array of objects, and we also need to mark which of the options is our selected value. The OptionData object accepts 3 parameters: $label, $value, and $selected:

$nakedOpts = array
(
    "AL" => "Alabama", 
    "AK" => "Alaska", 
    "AS" => "American Samoa", 
    "AZ" => "Arizona", 
    "AR" => "Arkansas"
);

foreach( $nakedOpts as $optval => $label )
{
    // is this array item the selected value?
    $isSelected = ($value == $optval) ? true : false;
    // create a new object for each item
    $this->options[] = new OptionData($label, $optval, $isSelected);
}

So now we need to convert our field $value to an object so Craft can compare it to the objects in the options list. Again, SingleOptionFieldData accepts the parameters of $label, $value, and $selected:

$value = new SingleOptionFieldData($nakedOpts[$value], $value, true);

The normalizeValue() method gets called once when the field is loaded on the publish page, and again when the entry is saved. So you have to check if you've already converted the $value to an object and bypass the rest of the code if so:

if ( $value instanceof SingleOptionFieldData ) 
{
    return $value;
}

The advantage of using a Craft object for this instead of a regular array is that Craft already knows how to use this type of data in the template. So the tag {{ entry.region }} will output AK without having to add the .value attribute. And just like a native Craft dropdown field, you can get the label with {{ entry.region.label }}.

To loop through all of your options in the template, just add this to normalizeValue() after you've converted the $value to an object and created your list of option objects:

$value->setOptions($this->options);

Now you can output a list of your options in your template:

{% for option in entry.region.options %}
    <option value="{{ option }}"{% if option.selected %} selected{% endif %}>{{ option.label }}</option>
{% endfor %}
| improve this answer | |
  • nice job on the explanations - these are bound to help others in future! – narration_sd Jul 4 '19 at 22:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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