At work, we’re building out a mobile version of our replicated sites. As anyone who’s used an iPhone or Android browser to fill out a web form knows… using an iPhone or Android browser to fill out a web form is really annoying, and it’s best to make things as user-friendly as possible. That’s why I wanted to leverage the power of HTML5 form elements to minimize that annoyance.
On the iPhone, Mobile Safari renders its on-screen keyboard differently for “email,” “tel” and “url” form fields — you automatically get the keys that make the most sense for each specific situation.
![iphone email HTML5 email form input rendered on an iPhone](http://www.curtisgibby.com/blog/wp-content/uploads/2011/07/iphone-email1.png)
![iphone tel HTML5 tel form input rendered on an iPhone](http://www.curtisgibby.com/blog/wp-content/uploads/2011/07/iphone-tel.png)
![iphone url HTML5 url form input rendered on an iPhone](http://www.curtisgibby.com/blog/wp-content/uploads/2011/07/iphone-url.png)
However, the version of CakePHP that we’re running our replicated sites on (1.2.5) doesn’t have support for these awesome HTML5 inputs — any unknown input type is automatically rendered as a textarea. (What kind of weird default is that?)
The HTML5 inputs are supported in CakePHP 2.0 (currently in beta), but we’re stuck with 1.2.5 for the time being. Lennaert Ekelmans back-ported that support to 1.3.6, but his solution didn’t work for me out of the box either — apparently there were a couple of changes in the Form helper between 1.2.5 and 1.3.6. So I back-back-ported his code to work with 1.2.5. Here it is:
<?php # app/views/helpers/html5_form.php App::import('Helper', 'Form'); class Html5FormHelper extends FormHelper { /** * Generates a form input element complete with label and wrapper div * * Options - See each field type method for more information. Any options that are part of * $attributes or $options for the different type methods can be included in $options for input(). * * - 'type' - Force the type of widget you want. e.g. ```type => 'select'``` * - 'label' - control the label * - 'div' - control the wrapping div element * - 'options' - for widgets that take options e.g. radio, select * - 'error' - control the error message that is produced * * @param string $fieldName This should be "Modelname.fieldname" * @param array $options Each type of input takes different options. * @return string Completed form widget * * different from regular form helper only in the textarea/default case -- adds support for HTML5 form types and attributes like placeholder * */ function input($fieldName, $options = array()) { $view =& ClassRegistry::getObject('view'); $this->setEntity($fieldName); $entity = join('.', $view->entity()); $defaults = array('before' => null, 'between' => null, 'after' => null); $options = array_merge($defaults, $options); if (!isset($options['type'])) { $options['type'] = 'text'; if (isset($options['options'])) { $options['type'] = 'select'; } elseif (in_array($this->field(), array('psword', 'passwd', 'password'))) { $options['type'] = 'password'; } elseif (isset($this->fieldset['fields'][$entity])) { $fieldDef = $this->fieldset['fields'][$entity]; $type = $fieldDef['type']; $primaryKey = $this->fieldset['key']; } elseif (ClassRegistry::isKeySet($this->model())) { $model =& ClassRegistry::getObject($this->model()); $type = $model->getColumnType($this->field()); $fieldDef = $model->schema(); if (isset($fieldDef[$this->field()])) { $fieldDef = $fieldDef[$this->field()]; } else { $fieldDef = array(); } $primaryKey = $model->primaryKey; } if (isset($type)) { $map = array( 'string' => 'text', 'datetime' => 'datetime', 'boolean' => 'checkbox', 'timestamp' => 'datetime', 'text' => 'textarea', 'time' => 'time', 'date' => 'date', 'float' => 'text' ); if (isset($this->map[$type])) { $options['type'] = $this->map[$type]; } elseif (isset($map[$type])) { $options['type'] = $map[$type]; } if ($this->field() == $primaryKey) { $options['type'] = 'hidden'; } } if ($this->model() === $this->field()) { $options['type'] = 'select'; if (!isset($options['multiple'])) { $options['multiple'] = 'multiple'; } } } $types = array('text', 'checkbox', 'radio', 'select'); if (!isset($options['options']) && in_array($options['type'], $types)) { $view =& ClassRegistry::getObject('view'); $varName = Inflector::variable( Inflector::pluralize(preg_replace('/_id$/', '', $this->field())) ); $varOptions = $view->getVar($varName); if (is_array($varOptions)) { if ($options['type'] !== 'radio') { $options['type'] = 'select'; } $options['options'] = $varOptions; } } $autoLength = (!array_key_exists('maxlength', $options) && isset($fieldDef['length'])); if ($autoLength && $options['type'] == 'text') { $options['maxlength'] = $fieldDef['length']; } if ($autoLength && $fieldDef['type'] == 'float') { $options['maxlength'] = array_sum(explode(',', $fieldDef['length']))+1; } $out = ''; $div = true; $divOptions = array(); if (array_key_exists('div', $options)) { $div = $options['div']; unset($options['div']); } if (!empty($div)) { $divOptions['class'] = 'input'; $divOptions = $this->addClass($divOptions, $options['type']); if (is_string($div)) { $divOptions['class'] = $div; } elseif (is_array($div)) { $divOptions = array_merge($divOptions, $div); } if (in_array($this->field(), $this->fieldset['validates'])) { $divOptions = $this->addClass($divOptions, 'required'); } if (!isset($divOptions['tag'])) { $divOptions['tag'] = 'div'; } } $label = null; if (isset($options['label']) && $options['type'] !== 'radio') { $label = $options['label']; unset($options['label']); } if ($options['type'] === 'radio') { $label = false; if (isset($options['options'])) { if (is_array($options['options'])) { $radioOptions = $options['options']; } else { $radioOptions = array($options['options']); } unset($options['options']); } } if ($label !== false) { $labelAttributes = $this->domId(array(), 'for'); if (in_array($options['type'], array('date', 'datetime'))) { $labelAttributes['for'] .= 'Month'; } else if ($options['type'] === 'time') { $labelAttributes['for'] .= 'Hour'; } if (is_array($label)) { $labelText = null; if (isset($label['text'])) { $labelText = $label['text']; unset($label['text']); } $labelAttributes = array_merge($labelAttributes, $label); } else { $labelText = $label; } if (isset($options['id'])) { $labelAttributes = array_merge($labelAttributes, array('for' => $options['id'])); } $out = $this->label($fieldName, $labelText, $labelAttributes); } $error = null; if (isset($options['error'])) { $error = $options['error']; unset($options['error']); } $selected = null; if (array_key_exists('selected', $options)) { $selected = $options['selected']; unset($options['selected']); } if (isset($options['rows']) || isset($options['cols'])) { $options['type'] = 'textarea'; } $empty = false; if (isset($options['empty'])) { $empty = $options['empty']; unset($options['empty']); } $timeFormat = 12; if (isset($options['timeFormat'])) { $timeFormat = $options['timeFormat']; unset($options['timeFormat']); } $dateFormat = 'MDY'; if (isset($options['dateFormat'])) { $dateFormat = $options['dateFormat']; unset($options['dateFormat']); } $type = $options['type']; $before = $options['before']; $between = $options['between']; $after = $options['after']; unset($options['type'], $options['before'], $options['between'], $options['after']); switch ($type) { case 'hidden': $out = $this->hidden($fieldName, $options); unset($divOptions); break; case 'checkbox': $out = $before . $this->checkbox($fieldName, $options) . $between . $out; break; case 'radio': $out = $before . $out . $this->radio($fieldName, $radioOptions, $options) . $between; break; case 'text': case 'password': $out = $before . $out . $between . $this->{$type}($fieldName, $options); break; case 'file': $out = $before . $out . $between . $this->file($fieldName, $options); break; case 'select': $options = array_merge(array('options' => array()), $options); $list = $options['options']; unset($options['options']); $out = $before . $out . $between . $this->select( $fieldName, $list, $selected, $options, $empty ); break; case 'time': $out = $before . $out . $between . $this->dateTime( $fieldName, null, $timeFormat, $selected, $options, $empty ); break; case 'date': $out = $before . $out . $between . $this->dateTime( $fieldName, $dateFormat, null, $selected, $options, $empty ); break; case 'datetime': $out = $before . $out . $between . $this->dateTime( $fieldName, $dateFormat, $timeFormat, $selected, $options, $empty ); break; case 'textarea': $out = $before . $out . $between . $this->textarea($fieldName, $options + array('cols' => 30, 'rows' => 6)); break; default: $out = $before . $out . $between . $this->defaultInput($type, $fieldName, $options); break; } if ($type != 'hidden') { $out .= $after; if ($error !== false) { $errMsg = $this->error($fieldName, $error); if ($errMsg) { $out .= $errMsg; $divOptions = $this->addClass($divOptions, 'error'); } } } if (isset($divOptions) && isset($divOptions['tag'])) { $tag = $divOptions['tag']; unset($divOptions['tag']); $out = $this->Html->tag($tag, $out, $divOptions); } return $out; } /** * Creates a default input widget, whose type is determined by $options['type']. * * @param string $fieldName Name of a field, in the form "Modelname.fieldname" * @param array $options Array of HTML attributes. * @return string A generated HTML input element * @access public */ function defaultInput($type, $fieldName, $options = array()) { $options = $this->_initInputField($fieldName, array_merge( array('type' => $type), $options )); return sprintf( $this->Html->tags['input'], $options['name'], $this->_parseAttributes($options, array('name'), null, ' ') ); } } ?>
Unlike Lennaert, I chose to extend the regular form helper by creating my own helper class instead of adding the necessary code to Cake’s own form helper. Now I can get good HTML5 form inputs by adding the Html5Form helper to my controller and then calling the following code in my view:
echo $html5Form->input('email', array( 'autofocus' => 'autofocus', 'placeholder' => 'email@example.com', 'type' => 'email', 'label' => __('Email address', true), 'required' => 'required', 'div' => 'required' ) ); echo $html5Form->input('phone', array( 'placeholder' => '801-867-5309', 'type' => 'tel', 'label' => __('Phone', true), 'required' => 'required', 'div' => 'required' ) ); echo $html5Form->input('phone', array( 'placeholder' => 'http://www.curtisgibby.com', 'type' => 'url', 'label' => __('Web site', true), 'required' => 'required', 'div' => 'required' ) );
That results in the following HTML:
<div> <label for="ContactEmail">Email address</label> <input name="data[Contact][email]" type="email" autofocus="autofocus" placeholder="email@example.com" required="required" value="" id="ContactEmail" /> </div> <div> <label for="ContactPhone">Phone</label> <input name="data[Contact][phone]" type="tel" placeholder="801-867-5309" required="required" value="" id="ContactPhone" /> </div> <div> <label for="ContactPhone">Web site</label> <input name="data[Contact][phone]" type="url" placeholder="http://www.curtisgibby.com" required="required" value="" id="ContactPhone" /> </div>
(There doesn’t seem to be an easy way to output HTML5’s standalone attributes as simply autofocus or required rather than the more verbose XHTML-ish autofocus = ‘autofocus’ or required = ‘true’ — without hacking the CakePHP base helper class. I’m okay with this slightly lengthier version for now though.)
If you find this code useful, let me know in the comments.