Form.php 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2013, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\template\helper;
  9. use lithium\util\Set;
  10. use lithium\util\Inflector;
  11. /**
  12. * A helper class to facilitate generating, processing and securing HTML forms. By default, `Form`
  13. * will simply generate HTML forms and widgets, but by creating a form with a _binding object_,
  14. * the helper can pre-fill form input values, render error messages, and introspect column types.
  15. *
  16. * For example, assuming you have created a `Posts` model in your application:
  17. * {{{// In controller code:
  18. * use app\models\Posts;
  19. * $post = Posts::find(1);
  20. * return compact('post');
  21. *
  22. * // In view code:
  23. * <?=$this->form->create($post); // Echoes a <form> tag and binds the helper to $post ?>
  24. * <?=$this->form->text('title'); // Echoes an <input /> element, pre-filled with $post's title ?>
  25. * <?=$this->form->submit('Update'); // Echoes a submit button with the title 'Update' ?>
  26. * <?=$this->form->end(); // Echoes a </form> tag & unbinds the form ?>
  27. * }}}
  28. */
  29. class Form extends \lithium\template\Helper {
  30. /**
  31. * String templates used by this helper.
  32. *
  33. * @var array
  34. */
  35. protected $_strings = array(
  36. 'button' => '<button{:options}>{:title}</button>',
  37. 'checkbox' => '<input type="checkbox" name="{:name}"{:options} />',
  38. 'checkbox-multi' => '<input type="checkbox" name="{:name}[]"{:options} />',
  39. 'checkbox-multi-group' => '{:raw}',
  40. 'error' => '<div{:options}>{:content}</div>',
  41. 'errors' => '{:raw}',
  42. 'input' => '<input type="{:type}" name="{:name}"{:options} />',
  43. 'file' => '<input type="file" name="{:name}"{:options} />',
  44. 'form' => '<form action="{:url}"{:options}>{:append}',
  45. 'form-end' => '</form>',
  46. 'hidden' => '<input type="hidden" name="{:name}"{:options} />',
  47. 'field' => '<div{:wrap}>{:label}{:input}{:error}</div>',
  48. 'field-checkbox' => '<div{:wrap}>{:input}{:label}{:error}</div>',
  49. 'field-radio' => '<div{:wrap}>{:input}{:label}{:error}</div>',
  50. 'label' => '<label for="{:id}"{:options}>{:title}</label>',
  51. 'legend' => '<legend>{:content}</legend>',
  52. 'option-group' => '<optgroup label="{:label}"{:options}>{:raw}</optgroup>',
  53. 'password' => '<input type="password" name="{:name}"{:options} />',
  54. 'radio' => '<input type="radio" name="{:name}"{:options} />',
  55. 'select' => '<select name="{:name}"{:options}>{:raw}</select>',
  56. 'select-empty' => '<option value=""{:options}>&nbsp;</option>',
  57. 'select-multi' => '<select name="{:name}[]"{:options}>{:raw}</select>',
  58. 'select-option' => '<option value="{:value}"{:options}>{:title}</option>',
  59. 'submit' => '<input type="submit" value="{:title}"{:options} />',
  60. 'submit-image' => '<input type="image" src="{:url}"{:options} />',
  61. 'text' => '<input type="text" name="{:name}"{:options} />',
  62. 'textarea' => '<textarea name="{:name}"{:options}>{:value}</textarea>',
  63. 'fieldset' => '<fieldset{:options}><legend>{:content}</legend>{:raw}</fieldset>'
  64. );
  65. /**
  66. * Maps method names to template string names, allowing the default template strings to be set
  67. * permanently on a per-method basis.
  68. *
  69. * For example, if all text input fields should be wrapped in `<span />` tags, you can configure
  70. * the template string mappings per the following:
  71. *
  72. * {{{
  73. * $this->form->config(array('templates' => array(
  74. * 'text' => '<span><input type="text" name="{:name}"{:options} /></span>'
  75. * )));
  76. * }}}
  77. *
  78. * Alternatively, you can re-map one type as another. This is useful if, for example, you
  79. * include your own helper with custom form template strings which do not match the default
  80. * template string names.
  81. *
  82. * {{{
  83. * // Renders all password fields as text fields
  84. * $this->form->config(array('templates' => array('password' => 'text')));
  85. * }}}
  86. *
  87. * @var array
  88. * @see lithium\template\helper\Form::config()
  89. */
  90. protected $_templateMap = array(
  91. 'create' => 'form',
  92. 'end' => 'form-end'
  93. );
  94. /**
  95. * The data object or list of data objects to which the current form is bound. In order to
  96. * be a custom data object, a class must implement the following methods:
  97. *
  98. * - schema(): Returns an array defining the objects fields and their data types.
  99. * - data(): Returns an associative array of the data that this object represents.
  100. * - errors(): Returns an associate array of validation errors for the current data set, where
  101. * the keys match keys from `schema()`, and the values are either strings (in cases
  102. * where a field only has one error) or an array (in case of multiple errors),
  103. *
  104. * For an example of how to implement these methods, see the `lithium\data\Entity` object.
  105. *
  106. * @see lithium\data\Entity
  107. * @see lithium\data\Collection
  108. * @see lithium\template\helper\Form::create()
  109. * @var mixed A single data object, a `Collection` of multiple data objects, or an array of data
  110. * objects/`Collection`s.
  111. */
  112. protected $_binding = null;
  113. /**
  114. * Array of options used to create the form to which `$_binding` is currently bound.
  115. * Overwritten when `end()` is called.
  116. *
  117. * @var array
  118. */
  119. protected $_bindingOptions = array();
  120. public function __construct(array $config = array()) {
  121. $self =& $this;
  122. $defaults = array(
  123. 'base' => array(),
  124. 'text' => array(),
  125. 'textarea' => array(),
  126. 'select' => array('multiple' => false),
  127. 'attributes' => array(
  128. 'id' => function($method, $name, $options) use (&$self) {
  129. if (in_array($method, array('create', 'end', 'label', 'error'))) {
  130. return;
  131. }
  132. if (!$name || ($method === 'hidden' && $name === '_method')) {
  133. return;
  134. }
  135. $info = $self->binding($name);
  136. $model = $info->class;
  137. $id = Inflector::camelize(Inflector::slug($info->name));
  138. return $model ? basename(str_replace('\\', '/', $model)) . $id : $id;
  139. },
  140. 'name' => function($method, $name, $options) {
  141. if (!strpos($name, '.')) {
  142. return $name;
  143. }
  144. $name = explode('.', $name);
  145. $first = array_shift($name);
  146. return $first . '[' . join('][', $name) . ']';
  147. }
  148. ),
  149. 'binding' => function($object, $name = null) {
  150. $result = compact('name') + array(
  151. 'data' => null, 'errors' => null, 'class' => null
  152. );
  153. if (is_object($object)) {
  154. $result = compact('name') + array(
  155. 'data' => $object->data($name),
  156. 'errors' => $object->errors($name),
  157. 'class' => $object->model()
  158. );
  159. }
  160. return (object) $result;
  161. }
  162. );
  163. parent::__construct(Set::merge($defaults, $config));
  164. }
  165. /**
  166. * Object initializer. Adds a content handler for the `wrap` key in the `field()` method, which
  167. * converts an array of properties to an attribute string.
  168. *
  169. * @return void
  170. */
  171. protected function _init() {
  172. parent::_init();
  173. if ($this->_context) {
  174. $this->_context->handlers(array('wrap' => '_attributes'));
  175. }
  176. }
  177. /**
  178. * Allows you to configure a default set of options which are included on a per-method basis,
  179. * and configure method template overrides.
  180. *
  181. * To force all `<label />` elements to have a default `class` attribute value of `"foo"`,
  182. * simply do the following:
  183. *
  184. * {{{
  185. * $this->form->config(array('label' => array('class' => 'foo')));
  186. * }}}
  187. *
  188. * Note that this can be overridden on a case-by-case basis, and when overriding, values are
  189. * not merged or combined. Therefore, if you wanted a particular `<label />` to have both `foo`
  190. * and `bar` as classes, you would have to specify `'class' => 'foo bar'`.
  191. *
  192. * You can also use this method to change the string template that a method uses to render its
  193. * content. For example, the default template for rendering a checkbox is
  194. * `'<input type="checkbox" name="{:name}"{:options} />'`. However, suppose you implemented your
  195. * own custom UI elements, and you wanted to change the markup used, you could do the following:
  196. *
  197. * {{{
  198. * $this->form->config(array('templates' => array(
  199. * 'checkbox' => '<div id="{:name}" class="ui-checkbox-element"{:options}></div>'
  200. * )));
  201. * }}}
  202. *
  203. * Now, for any calls to `$this->form->checkbox()`, your custom markup template will be applied.
  204. * This works for any `Form` method that renders HTML elements.
  205. *
  206. * @see lithium\template\helper\Form::$_templateMap
  207. * @param array $config An associative array where the keys are `Form` method names (or
  208. * `'templates'`, to include a template-overriding sub-array), and the
  209. * values are arrays of configuration options to be included in the `$options`
  210. * parameter of each method specified.
  211. * @return array Returns an array containing the currently set per-method configurations, and
  212. * an array of the currently set template overrides (in the `'templates'` array key).
  213. */
  214. public function config(array $config = array()) {
  215. if (!$config) {
  216. $keys = array('base' => '', 'text' => '', 'textarea' => '', 'attributes' => '');
  217. return array('templates' => $this->_templateMap) + array_intersect_key(
  218. $this->_config, $keys
  219. );
  220. }
  221. if (isset($config['templates'])) {
  222. $this->_templateMap = $config['templates'] + $this->_templateMap;
  223. unset($config['templates']);
  224. }
  225. return ($this->_config = Set::merge($this->_config, $config)) + array(
  226. 'templates' => $this->_templateMap
  227. );
  228. }
  229. /**
  230. * Creates an HTML form, and optionally binds it to a data object which contains information on
  231. * how to render form fields, any data to pre-populate the form with, and any validation errors.
  232. * Typically, a data object will be a `Record` object returned from a `Model`, but you can
  233. * define your own custom objects as well. For more information on custom data objects, see
  234. * `lithium\template\helper\Form::$_binding`.
  235. *
  236. * @see lithium\template\helper\Form::$_binding
  237. * @see lithium\data\Entity
  238. * @param mixed $bindings List of objects, or the object to bind the form to. This is usually an
  239. * instance of `Record` or `Document`, or some other class that extends
  240. * `lithium\data\Entity`.
  241. * @param array $options Other parameters for creating the form. Available options are:
  242. * - `'url'` _mixed_: A string URL or URL array parameters defining where in the
  243. * application the form should be submitted to.
  244. * - `'action'` _string_: This is a shortcut to be used if you wish to only
  245. * specify the name of the action to submit to, and use the default URL
  246. * parameters (i.e. the current controller, etc.) for generating the remainder
  247. * of the URL. Ignored if the `'url'` key is set.
  248. * - `'type'` _string_: Currently the only valid option is `'file'`. Set this if
  249. * the form will be used for file uploads.
  250. * - `'method'` _string_: Represents the HTTP method with which the form will be
  251. * submitted (`'get'`, `'post'`, `'put'` or `'delete'`). If `'put'` or
  252. * `'delete'`, the request method is simulated using a hidden input field.
  253. * @return string Returns a `<form />` open tag with the `action` attribute defined by either
  254. * the `'action'` or `'url'` options (defaulting to the current page if none is
  255. * specified), the HTTP method is defined by the `'method'` option, and any HTML
  256. * attributes passed in `$options`.
  257. * @filter
  258. */
  259. public function create($bindings = null, array $options = array()) {
  260. $request = $this->_context ? $this->_context->request() : null;
  261. $binding = is_array($bindings) ? reset($bindings) : $bindings;
  262. $defaults = array(
  263. 'url' => $request ? $request->params : array(),
  264. 'type' => null,
  265. 'action' => null,
  266. 'method' => $binding ? ($binding->exists() ? 'put' : 'post') : 'post'
  267. );
  268. list(, $options, $tpl) = $this->_defaults(__FUNCTION__, null, $options);
  269. list($scope, $options) = $this->_options($defaults, $options);
  270. $_binding =& $this->_binding;
  271. $_options =& $this->_bindingOptions;
  272. $params = compact('scope', 'options', 'bindings');
  273. $extra = array('method' => __METHOD__) + compact('tpl', 'defaults');
  274. $filter = function($self, $params) use ($extra, &$_binding, &$_options) {
  275. $scope = $params['scope'];
  276. $options = $params['options'];
  277. $_binding = $params['bindings'];
  278. $append = null;
  279. $scope['method'] = strtolower($scope['method']);
  280. if ($scope['type'] === 'file') {
  281. if ($scope['method'] === 'get') {
  282. $scope['method'] = 'post';
  283. }
  284. $options['enctype'] = 'multipart/form-data';
  285. }
  286. if (!($scope['method'] === 'get' || $scope['method'] === 'post')) {
  287. $append = $self->hidden('_method', array('value' => strtoupper($scope['method'])));
  288. $scope['method'] = 'post';
  289. }
  290. $url = $scope['action'] ? array('action' => $scope['action']) : $scope['url'];
  291. $options['method'] = strtolower($scope['method']);
  292. $args = array($extra['method'], $extra['tpl'], compact('url', 'options', 'append'));
  293. $_options = $scope + $options;
  294. return $self->invokeMethod('_render', $args);
  295. };
  296. return $this->_filter(__METHOD__, $params, $filter);
  297. }
  298. /**
  299. * Echoes a closing `</form>` tag and unbinds the `Form` helper from any `Record` or `Document`
  300. * object used to generate the corresponding form.
  301. *
  302. * @return string Returns a closing `</form>` tag.
  303. * @filter
  304. */
  305. public function end() {
  306. list(, $options, $template) = $this->_defaults(__FUNCTION__, null, array());
  307. $params = compact('options', 'template');
  308. $_context =& $this->_context;
  309. $_options =& $this->_bindingOptions;
  310. $filter = function($self, $params) use (&$_context, &$_options, $template) {
  311. $_options = array();
  312. return $self->invokeMethod('_render', array('end', $params['template'], array()));
  313. };
  314. $result = $this->_filter(__METHOD__, $params, $filter);
  315. unset($this->_binding);
  316. $this->_binding = null;
  317. return $result;
  318. }
  319. /**
  320. * Returns the entity that the `Form` helper is currently bound to.
  321. *
  322. * @see lithium\template\helper\Form::$_binding
  323. * @param string $name If specified, match this field name against the list of bindings
  324. * @param string $key If $name specified, where to store relevant $_binding key
  325. * @return object Returns an object, usually an instance of `lithium\data\Entity`.
  326. */
  327. public function binding($name = null) {
  328. if (!$this->_binding) {
  329. return $this->_config['binding'](null, $name);
  330. }
  331. $binding = $this->_binding;
  332. $model = null;
  333. $key = $name;
  334. if (is_array($binding)) {
  335. switch (true) {
  336. case strpos($name, '.'):
  337. list($model, $key) = explode('.', $name, 2);
  338. $binding = isset($binding[$model]) ? $binding[$model] : reset($binding);
  339. break;
  340. case isset($binding[$name]):
  341. $binding = $binding[$name];
  342. $key = null;
  343. break;
  344. default:
  345. $binding = reset($binding);
  346. break;
  347. }
  348. }
  349. return $key ? $this->_config['binding']($binding, $key) : $binding;
  350. }
  351. /**
  352. * Implements alternative input types as method calls against `Form` helper. Enables the
  353. * generation of HTML5 input types and other custom input types:
  354. *
  355. * {{{ embed:lithium\tests\cases\template\helper\FormTest::testCustomInputTypes(1-2) }}}
  356. *
  357. * @param string $type The method called, which represents the `type` attribute of the
  358. * `<input />` tag.
  359. * @param array $params An array of method parameters passed to the method call. The first
  360. * element should be the name of the input field, and the second should be an array
  361. * of element attributes.
  362. * @return string Returns an `<input />` tag of the type specified in `$type`.
  363. */
  364. public function __call($type, array $params = array()) {
  365. $params += array(null, array());
  366. list($name, $options) = $params;
  367. list($name, $options, $template) = $this->_defaults($type, $name, $options);
  368. $template = $this->_context->strings($template) ? $template : 'input';
  369. return $this->_render($type, $template, compact('type', 'name', 'options', 'value'));
  370. }
  371. /**
  372. * Custom check to determine if our given magic methods can be responded to.
  373. *
  374. * @param string $method Method name.
  375. * @param bool $internal Interal call or not.
  376. * @return bool
  377. */
  378. public function respondsTo($method, $internal = false) {
  379. return is_callable(array($this, $method), true);
  380. }
  381. /**
  382. * Generates a form field with a label, input, and error message (if applicable), all contained
  383. * within a wrapping element.
  384. *
  385. * {{{
  386. * echo $this->form->field('name');
  387. * echo $this->form->field('present', array('type' => 'checkbox'));
  388. * echo $this->form->field(array('email' => 'Enter a valid email'));
  389. * echo $this->form->field(array('name','email','phone'), array('div' => false));
  390. * }}}
  391. * @param mixed $name The name of the field to render. If the form was bound to an object
  392. * passed in `create()`, `$name` should be the name of a field in that object.
  393. * Otherwise, can be any arbitrary field name, as it will appear in POST data.
  394. * Alternatively supply an array of fields that will use the same options
  395. * array($field1 => $label1, $field2, $field3 => $label3)
  396. * @param array $options Rendering options for the form field. The available options are as
  397. * follows:
  398. * - `'label'` _mixed_: A string or array defining the label text and / or
  399. * parameters. By default, the label text is a human-friendly version of `$name`.
  400. * However, you can specify the label manually as a string, or both the label
  401. * text and options as an array, i.e.:
  402. * `array('Your Label Title' => array('class' => 'foo', 'other' => 'options'))`.
  403. * - `'type'` _string_: The type of form field to render. Available default options
  404. * are: `'text'`, `'textarea'`, `'select'`, `'checkbox'`, `'password'` or
  405. * `'hidden'`, as well as any arbitrary type (i.e. HTML5 form fields).
  406. * - `'template'` _string_: Defaults to `'template'`, but can be set to any named
  407. * template string, or an arbitrary HTML fragment. For example, to change the
  408. * default wrapper tag from `<div />` to `<li />`, you can pass the following:
  409. * `'<li{:wrap}>{:label}{:input}{:error}</li>'`.
  410. * - `'wrap'` _array_: An array of HTML attributes which will be embedded in the
  411. * wrapper tag.
  412. * - `list` _array_: If `'type'` is set to `'select'`, `'list'` is an array of
  413. * key/value pairs representing the `$list` parameter of the `select()` method.
  414. * @return string Returns a form input (the input type is based on the `'type'` option), with
  415. * label and error message, wrapped in a `<div />` element.
  416. */
  417. public function field($name, array $options = array()) {
  418. if (is_array($name)) {
  419. return $this->_fields($name, $options);
  420. }
  421. list(, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  422. $defaults = array(
  423. 'label' => null,
  424. 'type' => isset($options['list']) ? 'select' : 'text',
  425. 'template' => $template,
  426. 'wrap' => array(),
  427. 'list' => null
  428. );
  429. list($options, $field) = $this->_options($defaults, $options);
  430. $label = $input = null;
  431. $wrap = $options['wrap'];
  432. $type = $options['type'];
  433. $list = $options['list'];
  434. $template = $options['template'];
  435. $notText = $template === 'field' && $type !== 'text';
  436. if ($notText && $this->_context->strings('field-' . $type)) {
  437. $template = 'field-' . $type;
  438. }
  439. if (($options['label'] === null || $options['label']) && $options['type'] !== 'hidden') {
  440. if (!$options['label']) {
  441. $options['label'] = Inflector::humanize(preg_replace('/[\[\]\.]/', '_', $name));
  442. }
  443. $label = $this->label(isset($options['id']) ? $options['id'] : '', $options['label']);
  444. }
  445. $call = ($type === 'select') ? array($name, $list, $field) : array($name, $field);
  446. $input = call_user_func_array(array($this, $type), $call);
  447. $error = ($this->_binding) ? $this->error($name) : null;
  448. return $this->_render(__METHOD__, $template, compact('wrap', 'label', 'input', 'error'));
  449. }
  450. /**
  451. * Helper method used by `Form::field()` for iterating over an array of multiple fields.
  452. *
  453. * @see lithium\template\helper\Form::field()
  454. * @param array $fields An array of fields to render.
  455. * @param array $options The array of options to apply to all fields in the `$fields` array. See
  456. * the `$options` parameter of the `field` method for more information.
  457. * @return string Returns the fields rendered by `field()`, each separated by a newline.
  458. */
  459. protected function _fields(array $fields, array $options = array()) {
  460. $result = array();
  461. foreach ($fields as $field => $label) {
  462. if (is_numeric($field)) {
  463. $field = $label;
  464. unset($label);
  465. }
  466. $result[] = $this->field($field, compact('label') + $options);
  467. }
  468. return join("\n", $result);
  469. }
  470. /**
  471. * Generates an HTML button `<button></button>`.
  472. *
  473. * @param string $title The title of the button.
  474. * @param array $options Any options passed are converted to HTML attributes within the
  475. * `<button></button>` tag.
  476. * @return string Returns a `<button></button>` tag with the given title and HTML attributes.
  477. */
  478. public function button($title = null, array $options = array()) {
  479. $defaults = array('escape' => true);
  480. list($scope, $options) = $this->_options($defaults, $options);
  481. list($title, $options, $template) = $this->_defaults(__METHOD__, $title, $options);
  482. $arguments = compact('type', 'title', 'options', 'value');
  483. return $this->_render(__METHOD__, 'button', $arguments, $scope);
  484. }
  485. /**
  486. * Generates an HTML `<input type="submit" />` object.
  487. *
  488. * @param string $title The title of the submit button.
  489. * @param array $options Any options passed are converted to HTML attributes within the
  490. * `<input />` tag.
  491. * @return string Returns a submit `<input />` tag with the given title and HTML attributes.
  492. */
  493. public function submit($title = null, array $options = array()) {
  494. list($name, $options, $template) = $this->_defaults(__FUNCTION__, null, $options);
  495. return $this->_render(__METHOD__, $template, compact('title', 'options'));
  496. }
  497. /**
  498. * Generates an HTML `<textarea>...</textarea>` object.
  499. *
  500. * @param string $name The name of the field.
  501. * @param array $options The options to be used when generating the `<textarea />` tag pair,
  502. * which are as follows:
  503. * - `'value'` _string_: The content value of the field.
  504. * - Any other options specified are rendered as HTML attributes of the element.
  505. * @return string Returns a `<textarea>` tag with the given name and HTML attributes.
  506. */
  507. public function textarea($name, array $options = array()) {
  508. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  509. list($scope, $options) = $this->_options(array('value' => null), $options);
  510. $value = isset($scope['value']) ? $scope['value'] : '';
  511. return $this->_render(__METHOD__, $template, compact('name', 'options', 'value'));
  512. }
  513. /**
  514. * Generates an HTML `<input type="text" />` object.
  515. *
  516. * @param string $name The name of the field.
  517. * @param array $options All options passed are rendered as HTML attributes.
  518. * @return string Returns a `<input />` tag with the given name and HTML attributes.
  519. */
  520. public function text($name, array $options = array()) {
  521. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  522. return $this->_render(__METHOD__, $template, compact('name', 'options'));
  523. }
  524. /**
  525. * Generates a `<select />` list using the `$list` parameter for the `<option />` tags. The
  526. * default selection will be set to the value of `$options['value']`, if specified.
  527. *
  528. * For example: {{{
  529. * $this->form->select('colors', array(1 => 'red', 2 => 'green', 3 => 'blue'), array(
  530. * 'id' => 'Colors', 'value' => 2
  531. * ));
  532. * // Renders a '<select />' list with options 'red', 'green' and 'blue', with the 'green'
  533. * // option as the selection
  534. * }}}
  535. *
  536. * @param string $name The `name` attribute of the `<select />` element.
  537. * @param array $list An associative array of key/value pairs, which will be used to render the
  538. * list of options.
  539. * @param array $options Any HTML attributes that should be associated with the `<select />`
  540. * element. If the `'value'` key is set, this will be the value of the option
  541. * that is selected by default.
  542. * @return string Returns an HTML `<select />` element.
  543. */
  544. public function select($name, $list = array(), array $options = array()) {
  545. $defaults = array('empty' => false, 'value' => null);
  546. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  547. list($scope, $options) = $this->_options($defaults, $options);
  548. if ($scope['empty']) {
  549. $list = array('' => ($scope['empty'] === true) ? '' : $scope['empty']) + $list;
  550. }
  551. if ($template === __FUNCTION__ && $scope['multiple']) {
  552. $template = 'select-multi';
  553. }
  554. $raw = $this->_selectOptions($list, $scope);
  555. return $this->_render(__METHOD__, $template, compact('name', 'options', 'raw'));
  556. }
  557. /**
  558. * Generator method used by `select()` to produce `<option />` and `<optgroup />` elements.
  559. * Generally, this method should not need to be called directly, but through `select()`.
  560. *
  561. * @param array $list Either a flat key/value array of select menu options, or an array which
  562. * contains key/value elements and/or elements where the keys are `<optgroup />`
  563. * titles and the values are sub-arrays of key/value pairs representing nested
  564. * `<option />` elements.
  565. * @param array $scope An array of options passed to the parent scope, including the currently
  566. * selected value of the associated form element.
  567. * @return string Returns a string of `<option />` and (optionally) `<optgroup />` tags to be
  568. * embedded in a select element.
  569. */
  570. protected function _selectOptions(array $list, array $scope) {
  571. $result = "";
  572. foreach ($list as $value => $title) {
  573. if (is_array($title)) {
  574. $label = $value;
  575. $options = array();
  576. $raw = $this->_selectOptions($title, $scope);
  577. $params = compact('label', 'options', 'raw');
  578. $result .= $this->_render('select', 'option-group', $params);
  579. continue;
  580. }
  581. $selected = (
  582. (is_array($scope['value']) && in_array($value, $scope['value'])) ||
  583. ($scope['empty'] && empty($scope['value']) && $value === '') ||
  584. (is_scalar($scope['value']) && ((string) $scope['value'] === (string) $value))
  585. );
  586. $options = $selected ? array('selected' => true) : array();
  587. $params = compact('value', 'title', 'options');
  588. $result .= $this->_render('select', 'select-option', $params);
  589. }
  590. return $result;
  591. }
  592. /**
  593. * Generates an HTML `<input type="checkbox" />` object.
  594. *
  595. * @param string $name The name of the field.
  596. * @param array $options Options to be used when generating the checkbox `<input />` element:
  597. * - `'checked'` _boolean_: Whether or not the field should be checked by default.
  598. * - `'value'` _mixed_: if specified, it will be used as the 'value' html
  599. * attribute and no hidden input field will be added.
  600. * - Any other options specified are rendered as HTML attributes of the element.
  601. * @return string Returns a `<input />` tag with the given name and HTML attributes.
  602. */
  603. public function checkbox($name, array $options = array()) {
  604. $defaults = array('value' => '1', 'hidden' => true);
  605. $options += $defaults;
  606. $default = $options['value'];
  607. $key = $name;
  608. $out = '';
  609. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  610. list($scope, $options) = $this->_options($defaults, $options);
  611. if (!isset($options['checked'])) {
  612. $options['checked'] = ($this->binding($key)->data == $default);
  613. }
  614. if ($scope['hidden']) {
  615. $out = $this->hidden($name, array('value' => '', 'id' => false));
  616. }
  617. $options['value'] = $scope['value'];
  618. return $out . $this->_render(__METHOD__, $template, compact('name', 'options'));
  619. }
  620. /**
  621. * Generates an HTML `<input type="radio" />` object.
  622. *
  623. * @param string $name The name of the field
  624. * @param array $options All options to be used when generating the radio `<input />` element:
  625. * - `'checked'` _boolean_: Whether or not the field should be selected by default.
  626. * - `'value'` _mixed_: if specified, it will be used as the 'value' html
  627. * attribute. Defaults to `1`
  628. * - Any other options specified are rendered as HTML attributes of the element.
  629. * @return string Returns a `<input />` tag with the given name and attributes
  630. */
  631. public function radio($name, array $options = array()) {
  632. $defaults = array('value' => '1');
  633. $options += $defaults;
  634. $default = $options['value'];
  635. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  636. list($scope, $options) = $this->_options($defaults, $options);
  637. if (!isset($options['checked'])) {
  638. $options['checked'] = ($this->binding($name)->data == $default);
  639. }
  640. $options['value'] = $scope['value'];
  641. return $this->_render(__METHOD__, $template, compact('name', 'options'));
  642. }
  643. /**
  644. * Generates an HTML `<input type="password" />` object.
  645. *
  646. * @param string $name The name of the field.
  647. * @param array $options An array of HTML attributes with which the field should be rendered.
  648. * @return string Returns a `<input />` tag with the given name and HTML attributes.
  649. */
  650. public function password($name, array $options = array()) {
  651. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  652. unset($options['value']);
  653. return $this->_render(__METHOD__, $template, compact('name', 'options'));
  654. }
  655. /**
  656. * Generates an HTML `<input type="hidden" />` object.
  657. *
  658. * @param string $name The name of the field.
  659. * @param array $options An array of HTML attributes with which the field should be rendered.
  660. * @return string Returns a `<input />` tag with the given name and HTML attributes.
  661. */
  662. public function hidden($name, array $options = array()) {
  663. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  664. return $this->_render(__METHOD__, $template, compact('name', 'options'));
  665. }
  666. /**
  667. * Generates an HTML `<label></label>` object.
  668. *
  669. * @param string $id The DOM ID of the field that the label is for.
  670. * @param string $title The content inside the `<label></label>` object.
  671. * @param array $options Besides HTML attributes, this parameter allows one additional flag:
  672. * - `'escape'` _boolean_: Defaults to `true`. Indicates whether the title of the
  673. * label should be escaped. If `false`, it will be treated as raw HTML.
  674. * @return string Returns a `<label>` tag for the name and with HTML attributes.
  675. */
  676. public function label($id, $title = null, array $options = array()) {
  677. $defaults = array('escape' => true);
  678. if (is_array($title)) {
  679. list($title, $options) = each($title);
  680. }
  681. $title = $title ?: Inflector::humanize(str_replace('.', '_', $id));
  682. list($name, $options, $template) = $this->_defaults(__FUNCTION__, $id, $options);
  683. list($scope, $options) = $this->_options($defaults, $options);
  684. if (strpos($id, '.')) {
  685. $generator = $this->_config['attributes']['id'];
  686. $id = $generator(__METHOD__, $id, $options);
  687. }
  688. return $this->_render(__METHOD__, $template, compact('id', 'title', 'options'), $scope);
  689. }
  690. /**
  691. * Generates an error message for a field which is part of an object bound to a form in
  692. * `create()`.
  693. *
  694. * @param string $name The name of the field for which to render an error.
  695. * @param mixed $key If more than one error is present for `$name`, a key may be specified.
  696. * If `$key` is not set in the array of errors, or if `$key` is `true`, the first
  697. * available error is used.
  698. * @param array $options Any rendering options or HTML attributes to be used when rendering
  699. * the error.
  700. * @return string Returns a rendered error message based on the `'error'` string template.
  701. */
  702. public function error($name, $key = null, array $options = array()) {
  703. $defaults = array('class' => 'error');
  704. list(, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
  705. $options += $defaults;
  706. $params = compact('name', 'key', 'options', 'template');
  707. return $this->_filter(__METHOD__, $params, function($self, $params) {
  708. $options = $params['options'];
  709. $template = $params['template'];
  710. if (isset($options['value'])) {
  711. unset($options['value']);
  712. }
  713. if (!$content = $self->binding($params['name'])->errors) {
  714. return null;
  715. }
  716. $result = '';
  717. if (!is_array($content)) {
  718. $args = array(__METHOD__, $template, compact('content', 'options'));
  719. return $self->invokeMethod('_render', $args);
  720. }
  721. $errors = $content;
  722. if ($params['key'] === null) {
  723. foreach ($errors as $content) {
  724. $args = array(__METHOD__, $template, compact('content', 'options'));
  725. $result .= $self->invokeMethod('_render', $args);
  726. }
  727. return $result;
  728. }
  729. $key = $params['key'];
  730. $content = !isset($errors[$key]) || $key === true ? reset($errors) : $errors[$key];
  731. $args = array(__METHOD__, $template, compact('content', 'options'));
  732. return $self->invokeMethod('_render', $args);
  733. });
  734. }
  735. /**
  736. * Builds the defaults array for a method by name, according to the config.
  737. *
  738. * @param string $method The name of the method to create defaults for.
  739. * @param string $name The `$name` supplied to the original method.
  740. * @param string $options `$options` from the original method.
  741. * @return array Defaults array contents.
  742. */
  743. protected function _defaults($method, $name, $options) {
  744. $methodConfig = isset($this->_config[$method]) ? $this->_config[$method] : array();
  745. $options += $methodConfig + $this->_config['base'];
  746. $options = $this->_generators($method, $name, $options);
  747. $hasValue = (
  748. (!isset($options['value']) || $options['value'] === null) &&
  749. $name && $value = $this->binding($name)->data
  750. );
  751. $isZero = (isset($value) && ($value === 0 || $value === "0"));
  752. if ($hasValue || $isZero) {
  753. $options['value'] = $value;
  754. }
  755. if (isset($options['value']) && !$isZero) {
  756. $isZero = ($options['value'] === 0 || $options['value'] === "0");
  757. }
  758. if (isset($options['default']) && empty($options['value']) && !$isZero) {
  759. $options['value'] = $options['default'];
  760. }
  761. unset($options['default']);
  762. $generator = $this->_config['attributes']['name'];
  763. $name = $generator($method, $name, $options);
  764. $tplKey = isset($options['template']) ? $options['template'] : $method;
  765. $template = isset($this->_templateMap[$tplKey]) ? $this->_templateMap[$tplKey] : $tplKey;
  766. return array($name, $options, $template);
  767. }
  768. /**
  769. * Iterates over the configured attribute generators, and modifies the settings for a tag.
  770. *
  771. * @param string $method The name of the helper method which was called, i.e. `'text'`,
  772. * `'select'`, etc.
  773. * @param string $name The name of the field whose attributes are being generated. Some helper
  774. * methods, such as `create()` and `end()`, are not field-based, and therefore
  775. * will have no name.
  776. * @param array $options The options and HTML attributes that will be used to generate the
  777. * helper output.
  778. * @return array Returns the value of the `$options` array, modified by the attribute generators
  779. * added in the `'attributes'` key of the helper's configuration. Note that if a
  780. * generator is present for a field whose value is `false`, that field will be removed
  781. * from the array.
  782. */
  783. protected function _generators($method, $name, $options) {
  784. foreach ($this->_config['attributes'] as $key => $generator) {
  785. if ($key === 'name') {
  786. continue;
  787. }
  788. if ($generator && !isset($options[$key])) {
  789. if (($attr = $generator($method, $name, $options)) !== null) {
  790. $options[$key] = $attr;
  791. }
  792. continue;
  793. }
  794. if ($generator && $options[$key] === false) {
  795. unset($options[$key]);
  796. }
  797. }
  798. return $options;
  799. }
  800. }
  801. ?>