Creating custom dependent form elements in Zend Framework 2-3

Overview

For this tutorial I wanted to demonstrate how to make a custom dependent form element. What makes this tutorial stand out from most “custom form element tutorials” is that sometimes, a standalone form element isn’t good enough and I want to show how more can be achieved. Sometimes there are dependencies between elements both on the backend and on the frontend. For this tutorial, my example is a linked country/state dropdown set. Not just linked on the frontend, but also linked in the InputFilter validation.

Countries Element (independent)

Let’s start with the easy one. The Countries element is independent so it doesn’t need much work or explanation. Don’t forget to add it to your ServiceManager under form_elements.

class Countries extends \Application\Table\CountriesTable {
  protected $countriesTable;
  // This is injected in the ServiceFactory
  public function __construct(CountriesTable $countries_table) {
    $this->countriesTable = $countries_table;
    $this->value = 'USA';
  }

  public function getValueOptions() {
    if(!$this->valueOptions) {
      $this->valueOptions = [
        '' => $this->getAttribute('placeholder')
      ];
      $this->valueOptions += $this->countriesTable->fetchCountries();
    }
    return $this->valueOptions;
  }
}

States Element (dependent)

The States element is more complicated because of the nature of this tutorial but still fairly self-explanatory. Again, don’t forget to add it to your ServiceManager under form_elements.

class States extends \Application\Table\StatesTable {
  protected $statesTable;
  protected $country = 'USA';

  // This is injected in the ServiceFactory
  public function __construct(StatesTable $states_table) {
    $this->statesTable = $states_table;
  }

  public function getValueOptions() {
    if(!$this->valueOptions) {
      $country = $this->getCountry();

      // We may have been passed an element
      if($country instanceof \Zend\Form\Element) {
        $country = $country->getValue() ?: 'USA';
      }

      $this->valueOptions = [
        '' => $this->getAttribute('placeholder')
      ];
      
      $states = $this->statesTable->fetchStates($country);
      if(empty($states)) {
        $this->valueOptions = ['n/a' => 'N/A'];
      }
      else {
        $this->valueOptions += $states;
      }
    }
    return $this->valueOptions;
  }

  public function setOptions($options) {
    parent::setOptions($options);
    if(isset($options['country'])) {
      $this->setCountry($options['country']);
    }
    return $this;
  }

  public function setCountry($country) {
    $this->country = $country;
    return $this;
  }

  public function getCountry() {
    return $this->country;
  }
}

Let’s take a look at the meat of this code in getValueOptions:

// This was passed in as an option
$country = $this->getCountry();

// We may have been passed an element so let's extract the value
if($country instanceof \Zend\Form\Element) {
  $country = $country->getValue() ?: 'USA';
}

// Create the base of the valueOptions
$this->valueOptions = [
  '' => $this->getAttribute('placeholder')
];

// Get the states from the DB using the selected country
$states = $this->statesTable->fetchStates($country);

// The selected country may not have any states
if(empty($states)) {
  $this->valueOptions = ['n/a' => 'N/A'];
}
else {
  // Use the union operator to append the states to the list
  $this->valueOptions += $states;
}

We are simply creating the list of states to display. The option list contains the placeholder as the first item and then adds injects a key->value list of states where applicable.

FormRow View Helper

We will override the FormRow helper to allow us to inject what we need to target our element in JavaScript. Don’t forget to add it to your ServiceManager under view_helpers, if you are using ZF3, you will need to set the invokable Zend\Form\View\Helper\FormRow to your helper (or to be more ZF3 compliant, make a factory for your helper, add it as a factory using the same key instead) and then set the alias FormRow to Zend\Form\View\Helper\FormRow.

class FormRow extends \Zend\Form\View\Helper\FormRow {
  public function render(ElementInterface $element,$labelPosition=null) {
    // Only modify our target element
    if($element instanceof \Application\Form\Element\States) {
      // $country is our Country form element
      $country = $element->getCountry();
      
      // There are other ways of doing this but we will target the element by ID
      $id = $country->getAttribute('id');
      $id = $id ?: $this->createId($country->getName());
      
      // Set the element up for JS targeting
      $element->setAttribute('data-country-id',$id);
      $element->setAttribute('class',$element->getAttribute('class') . ' form__states-helper');
    }
    return parent::render($element);
  }
}

As you can see, our helper still uses the default rendering return parent::render($element), we are just going to modify the element if it is our element type if($element instanceof \Application\Form\Element\States) {. All we are doing is adding our custom class for JS targeting $element->setAttribute('class',/*snip*/);, and setting the country field’s element ID as a data attribute $element->setAttribute('data-country-id',/*snip*/)so we can link them via JS.

JavaScript

The JavaScript for this helper is outside the scope of this tutorial. It would control how the states dropdown changes based on the value of the country dropdown. This can be done in a variety of ways including an in-memory array or through AJAX. However you choose to implement it, it would in your template, AMD module, or somehow otherwise injected and run, and would look something like this if you are using jQuery:

$('.form__states-helper').each(function(index,element) {
  var $states = $(this);

  // Use the data field that we set in the helper
  var $country = $('#' + $states.data('country-id'));
  $country.on('change',function(e) {
    // ..
    // Load new states into $states.
    // ..
  });
});

Usage

Once they have been created, the usage is actually pretty-standard. Also, it is now super-easy to add other features like setting defaults based on the user’s location.

The form

The usage below assumes you are adding your form to the FormElementManager and retrieving it from there (as seen in “the controller” usage) as well.

class ContactForm extends Form {
  public function init() {
    $this->setPreferFormInputFilter(false);
    $this->add([
      'name' => 'country',
      'type' => 'Countries',
      'attributes' => [
        'placeholder' => 'Country'
      ]
    ]);
    $this->add([
      'name' => 'state',
      'type' => 'States',
      'attributes' => [
        'placeholder' => 'State'
      ],
      'options' => [
        'country' => $this->get('country')
      ]
    ]);
  }
}

The potentially unique usage is that we can pass our country element into the state element as an 'country' => $this->get('country') option. This is what allows the view helper and States element to do it’s thing.

The view:

You may be rendering your form differently but if you are using anything that triggers a render through FormRow (which includes the Form view helper), it will be sufficient.

<!-- [Inside your <form>] -->
<?php echo $this->FormRow($form->get('country'))?>
<?php echo $this->FormRow($form->get('state'))?>
<!-- [/Inside your <form>] -->

The controller:

There’s no special treatment in the controller, just use the form as you normally would:

$form = $sl->get('FormElementManager')->get(\Application\Form\ContactForm::class);
$request = $this->getRequest();
if($request->isPost()) {
  $form->setData($request->getPost());
  if($form->isValid()) {
    // Hurray!
  }
}

That’s it!

That’s pretty much all there is to it.

A Quick Use-Case for Pseudo Elements

This is just to demonstrate a common use-case for a pseudo-element. I needed to have a button divided into two visual pieces separated by a double line that didn’t go all the way to the edges of the button. I also was only able to use a single element.

Here is what I needed to achieve:

The only HTML I could use was:

<a href="#" class="download-button">Download</a>

And the SCSS I ended up with was:

.download-button {
    float: left;
    height: 30px;
    padding: 0 0 0 16px;
    border: 1px solid #0071C5;
    border-radius: 4px;
    color: #FFFFFF;
    font: 14px/30px Roboto;
    text-decoration: none;
    box-shadow: 0 1px 0 0 rgba(255,255,255,0.3) inset;
    background: linear-gradient(to bottom,  #007edc 0%,#007dd9 37%,#006fc1 100%);

    &:after {
        content: "\e313";
        position: relative;
        top: 50%;
        float: right;
        padding: 0 10px;
        margin: -10px 0 0 20px;
        line-height: 21px;
        border-left: 1px solid #006CC1;
        box-shadow: -1px 0 0 0 rgba(255,255,255,0.3);
        font-family: "Material Icons";
        font-size: 20px;
        padding: 0 5px;
    }
}
srcset and sizes

Providing the optimal experience for the user while using images on a page can really suck. There are many problems with serving images to a user that can make the browsing experience slow and bandwidth-heavy.

Fortunately, HTML5’s picture element, along with the srcset attribute and the sizes attribute aims to make this problem much less sucky.

The Old way

Before responsive design was considered important, embedding an image with HTML was a simple as

<img src="image.jpg" />

This could be done as a CSS background as well however, neither are really performant or reasonable options anymore for several reasons:

  • Devices with a higher DPI will render the image by scaling it and it won’t look good.
  • Devices with smaller screens are forced to download the same images even if they can’t render them at the native size which results in significant overhead and suboptimal rendering.
  • Network performance is a huge variable regardless of device and devices with slower connections are forced to download large images regardless of what they might actually need to initially render the page.

There were many different solutions to these problems depending on the needs of the application but usually they were overcome with a combination of JS and CSS. You could generate a series of images and assign them to some attribute on an element and some JS and CSS would resize the container and trigger the appropriate image to be downloaded.

These solutions were better but they still had their own problems:

  • They depended on JS which means that they are naturally going to be slower than any native behavior could be.
  • JS doesn’t have any way of accuratley gauging network speed to make informed decisions when downloading content.
  • The rendering was either blocking or happened too late in the execution order and resulted in flashes of unstyled content.

The New Way

In the most basic form, the example from above can be changed from this:

<img src="image.jpg" />

to this:

<img srcset="image.jpg" />

and browsers (that support srcset) will be using the new attribute. However, this isn’t very useful as there is no difference in behavior.

srcset can be used to provide the browser with a list of images to be used in different circumstances. There are two ways to tell the browser what those circumstances are. The first by specifying the actual image width, and the second by specifying the pixel density that the image is supposed to be supporting.

For example, supposed you have an image that has a fixed size on the page of 100×100 but you want it to look good with a screen that has a 2x pixel density. You would save out the image in two sizes: 100×100 and 200×200 and then can be used with srcset like this:

<img srcset="image_1x.jpg 1x,image_2x.jpg 2x" />

This tells the browser to use image_1x.jpg on devices with 1x pixel density and image_2x.jpg on devices with a 2x pixel density. Easy, right?

Now let’s say that the same image is rendered at different sizes depending on the size of the screen. Again, same exact image but it scales up or down depending on screen size. The browser needs to be able to determine which image to serve at the given screen size. Well, the pixel density method above doesn’t really do the browser much good as it doesn’t know the actual sizes of the images and therefore doesn’t know which one would be appropriate to render at the given screen size. This is where the second method of supplying images to srcset comes in.

Using the same images from the exampe above we can change it to the following:

<img srcset="image_1x.jpg 100w,image_2x.jpg 200w" />

This is an improvement to the situation but the browser still doesn’t know the size the image will actually be rendered at on the page until after it has already needed to download an image file. This is where sizes comes in. sizes allows you to tell the browser what size the image will actually render as at different sizes. The size of the screen is specified using media queries.

Building on the previous example, lets say that our 100×100 image is rendered at 100% of the viewport’s width at and between 320px and 479px. Also, let’s say that our 200×200 image will display at 200px from 480px and above. We can specify that like this:

<img srcset="image_1x.jpg 100w,image_2x.jpg 200w" sizes="(max-width:479px) 100vw,(min-width:780px) 200px"/>

This will allow the browser to choose an image to download based on the device’s width. Hurray!

It is important for me to note that this feature is still very new as of this post and needs to be polyfilled for older browsers if the behavior is desired. If you do not want to polyfill the feature, you can specify the src attribute and older browsers will still have something to render. It is very important to understand that this will cause all browsers to download the src image regardless of whether it is needed and regardless of the polyfill used. This is due to the order in which browsers render a page.