Browse Source

Merge pull request #2640 from markstory/3.0-radio-widget

3.0 - Create radio widget
José Lorenzo Rodríguez 12 years ago
parent
commit
39075a7365
2 changed files with 617 additions and 0 deletions
  1. 193 0
      src/View/Input/Radio.php
  2. 424 0
      tests/TestCase/View/Input/RadioTest.php

+ 193 - 0
src/View/Input/Radio.php

@@ -0,0 +1,193 @@
+<?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\Utility\Inflector;
+use Cake\View\StringTemplate;
+use Traversable;
+
+/**
+ * Input widget class for generating a set of radio buttons.
+ *
+ * This class is intended as an internal implementation detail
+ * of Cake\View\Helper\FormHelper and is not intended for direct use.
+ */
+class Radio {
+
+/**
+ * Template instance.
+ *
+ * @var Cake\View\StringTemplate
+ */
+	protected $_templates;
+
+/**
+ * Constructor
+ *
+ * This class uses a few templates:
+ *
+ * - `radio` Used to generate the input for a radio button.
+ *   Can use the following variables `name`, `value`, `attrs`.
+ * - `label` Used to generate the label for a radio button.
+ *   Can use the following variables `attrs`, `text` and `input`.
+ * - `radioContainer` Used to generate the container element for
+ *   the radio + input element. Can use the `input` and `label`
+ *   variables.
+ *
+ * @param Cake\View\StringTemplate $templates
+ */
+	public function __construct($templates) {
+		$this->_templates = $templates;
+	}
+
+/**
+ * Render a set of radio buttons.
+ *
+ * Data supports the following keys:
+ *
+ * - `name` - Set the input name.
+ * - `options` - An array of options. See below for more information.
+ * - `disabled` - Either true or an array of inputs to disable.
+ *    When true, the select element will be disabled.
+ * - `value` - A string of the option to mark as selected.
+ * - `label` - Either false to disable label generation, or
+ *   an array of attributes for all labels.
+ *
+ * @param array $data The data to build radio buttons with.
+ * @return string
+ */
+	public function render($data) {
+		$data += [
+			'name' => '',
+			'options' => [],
+			'disabled' => null,
+			'value' => null,
+			'escape' => true,
+			'label' => true,
+			'empty' => false,
+		];
+		if ($data['options'] instanceof Traversable) {
+			$options = iterator_to_array($data['options']);
+		} else {
+			$options = (array)$data['options'];
+		}
+
+		if (!empty($data['empty'])) {
+			$empty = $data['empty'] === true ? 'empty' : $data['empty'];
+			$options = ['' => $empty] + $options;
+		}
+		unset($data['empty']);
+
+		$opts = [];
+		foreach ($options as $val => $text) {
+			$opts[] = $this->_renderInput($val, $text, $data);
+		}
+		return implode('', $opts);
+	}
+
+/**
+ * Disabled attribute detection.
+ *
+ * @param array $radio
+ * @param array|null|true $disabled
+ * @return boolean
+ */
+	protected function _isDisabled($radio, $disabled) {
+		if (!$disabled) {
+			return false;
+		}
+		if ($disabled === true) {
+			return true;
+		}
+		$isNumeric = is_numeric($radio['value']);
+		return (!is_array($disabled) || in_array((string)$radio['value'], $disabled, !$isNumeric));
+	}
+
+/**
+ * Renders a single radio input and label.
+ *
+ * @param string|int $val The value of the radio input.
+ * @param string|array $text The label text, or complex radio type.
+ * @param array $data Additional options for input generation.
+ * @return string.
+ */
+	protected function _renderInput($val, $text, $data) {
+		$escape = $data['escape'];
+		if (is_int($val) && isset($text['text'], $text['value'])) {
+			$radio = $text;
+			$text = $radio['text'];
+		} else {
+			$radio = ['value' => $val, 'text' => $text];
+		}
+		$radio['name'] = $data['name'];
+
+		if (empty($radio['id'])) {
+			$radio['id'] = Inflector::slug($radio['name'] . '_' . $radio['value']);
+		}
+
+		if (isset($data['value']) && strval($data['value']) === strval($radio['value'])) {
+			$radio['checked'] = true;
+		}
+
+		if ($this->_isDisabled($radio, $data['disabled'])) {
+			$radio['disabled'] = true;
+		}
+
+		$input = $this->_templates->format('radio', [
+			'name' => $radio['name'],
+			'value' => $escape ? h($radio['value']) : $radio['value'],
+			'attrs' => $this->_templates->formatAttributes($radio, ['name', 'value', 'text']),
+		]);
+
+		$label = $this->_renderLabel(
+			$radio,
+			$data['label'],
+			$input,
+			$escape
+		);
+
+		return $this->_templates->format('radioContainer', [
+			'input' => $input,
+			'label' => $label,
+		]);
+	}
+
+/**
+ * Renders a label element for a given radio button.
+ *
+ * In the future this might be refactored into a separate widget as other
+ * input types (multi-checkboxes) will also need labels generated.
+ *
+ * @param array $radio The input properties.
+ * @param false|string|array $label The properties for a label.
+ * @param string $input The input widget.
+ * @param boolean $escape Whether or not to HTML escape the label.
+ * @return string Generated label.
+ */
+	protected function _renderLabel($radio, $label, $input, $escape) {
+		if (!$label) {
+			return false;
+		}
+		$labelAttrs = is_array($label) ? $label : [];
+		$labelAttrs += ['for' => $radio['id'], 'escape' => $escape];
+
+		return $this->_templates->format('label', [
+			'text' => $escape ? h($radio['text']) : $radio['text'],
+			'input' => $input,
+			'attrs' => $this->_templates->formatAttributes($labelAttrs),
+		]);
+	}
+
+}

+ 424 - 0
tests/TestCase/View/Input/RadioTest.php

@@ -0,0 +1,424 @@
+<?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\Collection\Collection;
+use Cake\TestSuite\TestCase;
+use Cake\View\Input\Radio;
+use Cake\View\StringTemplate;
+
+/**
+ * Radio test case
+ */
+class RadioTest extends TestCase {
+
+	public function setUp() {
+		parent::setUp();
+		$templates = [
+			'radio' => '<input type="radio" name="{{name}}" value="{{value}}"{{attrs}}>',
+			'label' => '<label{{attrs}}>{{text}}</label>',
+			'radioContainer' => '{{input}}{{label}}',
+		];
+		$this->templates = new StringTemplate();
+		$this->templates->add($templates);
+	}
+
+/**
+ * Test rendering basic radio buttons.
+ *
+ * @return void
+ */
+	public function testRenderSimple() {
+		$radio = new Radio($this->templates);
+		$data = [
+			'name' => 'Crayons[color]',
+			'options' => ['r' => 'Red', 'b' => 'Black']
+		];
+		$result = $radio->render($data);
+		$expected = [
+			['input' => [
+				'type' => 'radio',
+				'name' => 'Crayons[color]',
+				'value' => 'r',
+				'id' => 'Crayons_color_r'
+			]],
+			['label' => ['for' => 'Crayons_color_r']],
+			'Red',
+			'/label',
+			['input' => [
+				'type' => 'radio',
+				'name' => 'Crayons[color]',
+				'value' => 'b',
+				'id' => 'Crayons_color_b'
+			]],
+			['label' => ['for' => 'Crayons_color_b']],
+			'Black',
+			'/label',
+		];
+		$this->assertTags($result, $expected);
+
+		$data = [
+			'name' => 'Crayons[color]',
+			'options' => new Collection(['r' => 'Red', 'b' => 'Black'])
+		];
+		$result = $radio->render($data);
+		$this->assertTags($result, $expected);
+	}
+
+/**
+ * Test rendering inputs with the complex option form.
+ *
+ * @return void
+ */
+	public function testRenderComplex() {
+		$radio = new Radio($this->templates);
+		$data = [
+			'name' => 'Crayons[color]',
+			'options' => [
+				['value' => 'r', 'text' => 'Red', 'id' => 'my_id'],
+				['value' => 'b', 'text' => 'Black', 'id' => 'my_id_2', 'data-test' => 'test'],
+			]
+		];
+		$result = $radio->render($data);
+		$expected = [
+			['input' => [
+				'type' => 'radio',
+				'name' => 'Crayons[color]',
+				'value' => 'r',
+				'id' => 'my_id'
+			]],
+			['label' => ['for' => 'my_id']],
+			'Red',
+			'/label',
+			['input' => [
+				'type' => 'radio',
+				'name' => 'Crayons[color]',
+				'value' => 'b',
+				'id' => 'my_id_2',
+				'data-test' => 'test'
+			]],
+			['label' => ['for' => 'my_id_2']],
+			'Black',
+			'/label',
+		];
+		$this->assertTags($result, $expected);
+	}
+
+/**
+ * Test rendering the empty option.
+ *
+ * @return void
+ */
+	public function testRenderEmptyOption() {
+		$radio = new Radio($this->templates);
+		$data = [
+			'name' => 'Crayons[color]',
+			'options' => ['r' => 'Red'],
+			'empty' => true,
+		];
+		$result = $radio->render($data);
+		$expected = [
+			['input' => [
+				'type' => 'radio',
+				'name' => 'Crayons[color]',
+				'value' => '',
+				'id' => 'Crayons_color'
+			]],
+			['label' => ['for' => 'Crayons_color']],
+			'empty',
+			'/label',
+			['input' => [
+				'type' => 'radio',
+				'name' => 'Crayons[color]',
+				'value' => 'r',
+				'id' => 'Crayons_color_r'
+			]],
+			['label' => ['for' => 'Crayons_color_r']],
+			'Red',
+			'/label',
+		];
+		$this->assertTags($result, $expected);
+
+		$data['empty'] = 'Choose one';
+		$result = $radio->render($data);
+		$expected = [
+			['input' => [
+				'type' => 'radio',
+				'name' => 'Crayons[color]',
+				'value' => '',
+				'id' => 'Crayons_color'
+			]],
+			['label' => ['for' => 'Crayons_color']],
+			'Choose one',
+			'/label',
+			['input' => [
+				'type' => 'radio',
+				'name' => 'Crayons[color]',
+				'value' => 'r',
+				'id' => 'Crayons_color_r'
+			]],
+			['label' => ['for' => 'Crayons_color_r']],
+			'Red',
+			'/label',
+		];
+		$this->assertTags($result, $expected);
+	}
+
+/**
+ * Test rendering the input inside the label.
+ *
+ * @return void
+ */
+	public function testRenderInputInsideLabel() {
+		$this->templates->add([
+			'label' => '<label{{attrs}}>{{input}}{{text}}</label>',
+			'radioContainer' => '{{label}}',
+		]);
+		$radio = new Radio($this->templates);
+		$data = [
+			'name' => 'Crayons[color]',
+			'options' => ['r' => 'Red'],
+		];
+		$result = $radio->render($data);
+		$expected = [
+			['label' => ['for' => 'Crayons_color_r']],
+			['input' => [
+				'type' => 'radio',
+				'name' => 'Crayons[color]',
+				'value' => 'r',
+				'id' => 'Crayons_color_r'
+			]],
+			'Red',
+			'/label',
+		];
+		$this->assertTags($result, $expected);
+	}
+
+/**
+ * test render() and selected inputs.
+ *
+ * @return void
+ */
+	public function testRenderSelected() {
+		$radio = new Radio($this->templates);
+		$data = [
+			'name' => 'Versions[ver]',
+			'value' => '1',
+			'options' => [
+				1 => 'one',
+				'1x' => 'one x',
+				'2' => 'two',
+			]
+		];
+		$result = $radio->render($data);
+		$expected = [
+			['input' => [
+				'id' => 'Versions_ver_1',
+				'name' => 'Versions[ver]',
+				'type' => 'radio',
+				'value' => '1',
+				'checked' => 'checked'
+			]],
+			['label' => ['for' => 'Versions_ver_1']],
+			'one',
+			'/label',
+			['input' => [
+				'id' => 'Versions_ver_1x',
+				'name' => 'Versions[ver]',
+				'type' => 'radio',
+				'value' => '1x'
+			]],
+			['label' => ['for' => 'Versions_ver_1x']],
+			'one x',
+			'/label',
+			['input' => [
+				'id' => 'Versions_ver_2',
+				'name' => 'Versions[ver]',
+				'type' => 'radio',
+				'value' => '2'
+			]],
+			['label' => ['for' => 'Versions_ver_2']],
+			'two',
+			'/label',
+		];
+		$this->assertTags($result, $expected);
+	}
+
+/**
+ * Test rendering with disable inputs
+ *
+ * @return void
+ */
+	public function testRenderDisabled() {
+		$radio = new Radio($this->templates);
+		$data = [
+			'name' => 'Versions[ver]',
+			'options' => [
+				1 => 'one',
+				'1x' => 'one x',
+				'2' => 'two',
+			],
+			'disabled' => true,
+		];
+		$result = $radio->render($data);
+		$expected = [
+			['input' => [
+				'id' => 'Versions_ver_1',
+				'name' => 'Versions[ver]',
+				'type' => 'radio',
+				'value' => '1',
+				'disabled' => 'disabled'
+			]],
+			['label' => ['for' => 'Versions_ver_1']],
+			'one',
+			'/label',
+			['input' => [
+				'id' => 'Versions_ver_1x',
+				'name' => 'Versions[ver]',
+				'type' => 'radio',
+				'value' => '1x',
+				'disabled' => 'disabled'
+			]],
+			['label' => ['for' => 'Versions_ver_1x']],
+			'one x',
+			'/label',
+		];
+		$this->assertTags($result, $expected);
+
+		$data['disabled'] = ['1'];
+		$result = $radio->render($data);
+		$expected = [
+			['input' => [
+				'id' => 'Versions_ver_1',
+				'name' => 'Versions[ver]',
+				'type' => 'radio',
+				'value' => '1',
+				'disabled' => 'disabled'
+			]],
+			['label' => ['for' => 'Versions_ver_1']],
+			'one',
+			'/label',
+			['input' => [
+				'id' => 'Versions_ver_1x',
+				'name' => 'Versions[ver]',
+				'type' => 'radio',
+				'value' => '1x',
+			]],
+			['label' => ['for' => 'Versions_ver_1x']],
+			'one x',
+			'/label',
+		];
+		$this->assertTags($result, $expected);
+	}
+
+/**
+ * Test rendering with label options.
+ *
+ * @return void
+ */
+	public function testRenderLabelOptions() {
+		$radio = new Radio($this->templates);
+		$data = [
+			'name' => 'Versions[ver]',
+			'options' => [
+				1 => 'one',
+				'1x' => 'one x',
+				'2' => 'two',
+			],
+			'label' => false,
+		];
+		$result = $radio->render($data);
+		$expected = [
+			['input' => [
+				'id' => 'Versions_ver_1',
+				'name' => 'Versions[ver]',
+				'type' => 'radio',
+				'value' => '1',
+			]],
+			['input' => [
+				'id' => 'Versions_ver_1x',
+				'name' => 'Versions[ver]',
+				'type' => 'radio',
+				'value' => '1x',
+			]],
+		];
+		$this->assertTags($result, $expected);
+
+		$data = [
+			'name' => 'Versions[ver]',
+			'options' => [
+				1 => 'one',
+				'1x' => 'one x',
+				'2' => 'two',
+			],
+			'label' => [
+				'class' => 'my-class',
+			]
+		];
+		$result = $radio->render($data);
+		$expected = [
+			['input' => [
+				'id' => 'Versions_ver_1',
+				'name' => 'Versions[ver]',
+				'type' => 'radio',
+				'value' => '1',
+			]],
+			['label' => ['class' => 'my-class', 'for' => 'Versions_ver_1']],
+			'one',
+			'/label',
+			['input' => [
+				'id' => 'Versions_ver_1x',
+				'name' => 'Versions[ver]',
+				'type' => 'radio',
+				'value' => '1x',
+			]],
+			['label' => ['class' => 'my-class', 'for' => 'Versions_ver_1x']],
+			'one x',
+			'/label',
+		];
+		$this->assertTags($result, $expected);
+	}
+
+/**
+ * Ensure that the input + label are composed with
+ * a template.
+ *
+ * @return void
+ */
+	public function testRenderContainerTemplate() {
+		$this->templates->add([
+			'radioContainer' => '<div class="radio">{{input}}{{label}}</div>'
+		]);
+		$radio = new Radio($this->templates);
+		$data = [
+			'name' => 'Versions[ver]',
+			'options' => [
+				1 => 'one',
+				'1x' => 'one x',
+				'2' => 'two',
+			],
+		];
+		$result = $radio->render($data);
+		$this->assertContains(
+			'<div class="radio"><input type="radio"',
+			$result
+		);
+		$this->assertContains(
+			'</label></div>',
+			$result
+		);
+	}
+
+}