Skip to content

Commit

Permalink
Hacky start to building a selectbox input widget.
Browse files Browse the repository at this point in the history
I think splitting FormHelper into smaller parts will make the code
more manageable and extensible. I know for sure that a 3000 line class
is neither of the above, so smaller classes can't be worse.

This change also puts a stub in place for the Context class which will
eventually contain all the relevant context for building forms from the ORM
and other sources like the request. Hopefully this class can insulate the
rest of the input libraries from the ORM and request objects.
  • Loading branch information
markstory committed Jan 7, 2014
1 parent 4fa33ef commit 569856d
Show file tree
Hide file tree
Showing 3 changed files with 339 additions and 0 deletions.
38 changes: 38 additions & 0 deletions Cake/View/Input/Context.php
@@ -0,0 +1,38 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since CakePHP(tm) v3.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\View\Input;

/**
* Form input generation context.
*
* Coaleseces request data, form entities. Also provides methods
* for checking if fields are required, and schema introspection.
*
* The context class insulates the FormHelper and Input classes
* from various ORM implementations making them ORM independent.
*/
class Context {

protected $_requestData;
protected $_entities;
protected $_schema;

public function __construct($requestData = [], $entities = [], $schema = []) {
$this->_requestData = $requestData;
$this->_entities = $entities;
$this->_schema = $schema;
}

}
154 changes: 154 additions & 0 deletions Cake/View/Input/SelectBox.php
@@ -0,0 +1,154 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since CakePHP(tm) v3.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\View\Input;

use Cake\View\Input\Context;
use Cake\View\StringTemplate;

/**
* Input widget class for generating a selectbox.
*/
class SelectBox {

/**
* Minimized attributes
*
* @var array
*/
protected $_minimizedAttributes = array(
'compact', 'checked', 'declare', 'readonly', 'disabled', 'selected',
'defer', 'ismap', 'nohref', 'noshade', 'nowrap', 'multiple', 'noresize',
'autoplay', 'controls', 'loop', 'muted', 'required', 'novalidate', 'formnovalidate'
);

/**
* Format to attribute
*
* @var string
*/
protected $_attributeFormat = '%s="%s"';

/**
* Format to attribute
*
* @var string
*/
protected $_minimizedAttributeFormat = '%s="%s"';

protected $_templates;
protected $_context;

public function __construct($templates, $context) {
$this->_templates = $templates;
$this->_context = $context;
}

public function render($data) {
if (empty($data['name'])) {
throw new \RuntimeException('Cannot make inputs with empty name attributes.');
}
$options = $this->_renderOptions($data);
$name = $data['name'];
unset($data['name'], $data['options'], $data['empty'], $data['value']);
$attrs = $this->_parseAttributes($data);
return $this->_templates->format('select', [
'name' => $name,
'attrs' => $attrs,
'content' => implode('', $options),
]);
}

protected function _renderOptions($data) {
$out = [];
if (!empty($data['empty'])) {
// TODO
}
if (empty($data['options'])) {
return $out;
}

$selected = isset($data['value']) ? $data['value'] : null;
$selectedArray = is_array($selected);

foreach ($data['options'] as $key => $val) {
$template = 'option';
$strict = !is_numeric($key);

if (
isset($selected) &&
(!$selectedArray && (string)$key === (string)$selected) ||
($selectedArray && in_array((string)$key, $selected, $strict))
) {
$template = 'optionSelected';
}

$out[] = $this->_templates->format($template, [
'name' => $key,
'value' => $val
]);
}
return $out;
}

protected function _parseAttributes($options, $exclude = null) {
$insertBefore = ' ';
$options = (array)$options + array('escape' => true);

if (!is_array($exclude)) {
$exclude = array();
}

$exclude = array('escape' => true) + array_flip($exclude);
$escape = $options['escape'];
$attributes = array();

foreach ($options as $key => $value) {
if (!isset($exclude[$key]) && $value !== false && $value !== null) {
$attributes[] = $this->_formatAttribute($key, $value, $escape);
}
}
$out = implode(' ', $attributes);
return $out ? $insertBefore . $out : '';
}

/**
* Formats an individual attribute, and returns the string value of the composed attribute.
* Works with minimized attributes that have the same value as their name such as 'disabled' and 'checked'
*
* @param string $key The name of the attribute to create
* @param string $value The value of the attribute to create.
* @param boolean $escape Define if the value must be escaped
* @return string The composed attribute.
* @deprecated This method will be moved to HtmlHelper in 3.0
*/
protected function _formatAttribute($key, $value, $escape = true) {
if (is_array($value)) {
$value = implode(' ', $value);
}
if (is_numeric($key)) {
return sprintf($this->_minimizedAttributeFormat, $value, $value);
}
$truthy = array(1, '1', true, 'true', $key);
$isMinimized = in_array($key, $this->_minimizedAttributes);
if ($isMinimized && in_array($value, $truthy, true)) {
return sprintf($this->_minimizedAttributeFormat, $key, $key);
}
if ($isMinimized) {
return '';
}
return sprintf($this->_attributeFormat, $key, ($escape ? h($value) : $value));
}

}
147 changes: 147 additions & 0 deletions Test/TestCase/View/Input/SelectBoxTest.php
@@ -0,0 +1,147 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since CakePHP(tm) v3.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Test\TestCase\View\Input;

use Cake\TestSuite\TestCase;
use Cake\View\Input\Context;
use Cake\View\Input\SelectBox;
use Cake\View\StringTemplate;

/**
* SelectBox test case
*/
class SelectBoxTest extends TestCase {

public function setUp() {
parent::setUp();
$templates = [
'select' => '<select name="{{name}}" {{attrs}}>{{content}}</select>',
'selectMultiple' => '<select name="{{name}}" multiple="multiple" {{attrs}}>{{content}}</select>',
'option' => '<option value="{{name}}">{{value}}</option>',
'optionSelected' => '<option value="{{name}}" selected="selected">{{value}}</option>',
'optgroup' => '<optgroup label="{{label}}">{{content}}</optgroup>',
];
$this->templates = new StringTemplate();
$this->templates->add($templates);
}

/**
* test render no options
*
* @return void
*/
public function testRenderNoOptions() {
$context = new Context();
$select = new SelectBox($this->templates, $context);
$data = [
'id' => 'BirdName',
'name' => 'Birds[name]',
'options' => []
];
$result = $select->render($data);
$expected = [
'select' => ['name' => 'Birds[name]', 'id' => 'BirdName'],
'/select'
];
$this->assertTags($result, $expected);
}

/**
* test simple rendering
*
* @return void
*/
public function testRenderSimple() {
$context = new Context();
$select = new SelectBox($this->templates, $context);
$data = [
'id' => 'BirdName',
'name' => 'Birds[name]',
'options' => ['a' => 'Albatross', 'b' => 'Budgie']
];
$result = $select->render($data);
$expected = [
'select' => ['name' => 'Birds[name]', 'id' => 'BirdName'],
['option' => ['value' => 'a']], 'Albatross', '/option',
['option' => ['value' => 'b']], 'Budgie', '/option',
'/select'
];
$this->assertTags($result, $expected);
}

/**
* test rendering with a selected value
*
* @return void
*/
public function testRenderSelected() {
$this->markTestIncomplete('Not done');
}

/**
* test rendering a multi select
*
* @return void
*/
public function testRenderMultipleSelect() {
$this->markTestIncomplete('Not done');
}

/**
* test rendering multi select & selected values
*
* @return void
*/
public function testRenderMultipleSelected() {
$this->markTestIncomplete('Not done');
}

/**
* test rendering with option groups
*
* @return void
*/
public function testRenderOptionGroups() {
$this->markTestIncomplete('Not done');
}

/**
* test rendering option groups and selected values
*
* @return void
*/
public function testRenderOptionGroupsSelected() {
$this->markTestIncomplete('Not done');
}

/**
* test rendering a disabled element
*
* @return void
*/
public function testRenderDisabled() {
$this->markTestIncomplete('Not done');
}

/**
* test rendering with an empty value
*
* @return void
*/
public function testRenderEmptyOption() {
$this->markTestIncomplete('Not done');
}

}

0 comments on commit 569856d

Please sign in to comment.