Create Multiple Checkboxes Instead of a Multiple-Select in your Views
From a usablitiy stand point, multiple-select boxes are a nightmare. Forget to hold down the modifier key when adding an option and you loose all your selections. When the number of options are managable, multiple checkboxs are a better choice for average users. This functionality is coming to cake in future versions but you can have it now with this helper.
Save this as habtm.php in app/views/helpers
Download code
Add this snip of css to your stylesheet and adjust the li width to your taste
Download code
And add these tag templates to app/config/tags.ini.php
Download code
Then you can use the new helper exactly the same way as $html->selectTag(...) just change your method call to $habtm->checkboxMultiple(..) instead.
Acknowledgment: This is based off code by MrRio in the Trac system:
https://trac.cakephp.org/ticket/1260
Download code
<?php
class HabtmHelper extends HtmlHelper {
/**
* Returns a list of checkboxes.
*
* @param string $fieldName Name attribute of the SELECT
* @param array $options Array of the elements (as 'value'=>'Text' pairs)
* @param array $selected Selected checkboxes
* @param string $inbetween String that separates the checkboxes.
* @param array $htmlAttributes Array of HTML options
* @param boolean $return Whether this method should return a value
* @return string List of checkboxes
*/
function checkboxMultiple($fieldName, $options, $selected = null, $inbetween = null, $htmlAttributes = null, $return = false) {
$this->setFormTag($fieldName);
if ($this->tagIsInvalid($this->model, $this->field)) {
if (isset($htmlAttributes['class']) && trim($htmlAttributes['class']) != "") {
$htmlAttributes['class'] .= ' form_error';
} else {
$htmlAttributes['class'] = 'form_error';
}
}
if (!is_array($options)) {
return null;
}
if (!isset($selected)) {
$selected = $this->tagValue($fieldName);
}
foreach($options as $name => $title) {
$optionsHere = $htmlAttributes;
if (($selected !== null) && ($selected == $name)) {
$optionsHere['checked'] = 'checked';
} else if (is_array($selected) && array_key_exists($name, $selected)) {
$optionsHere['checked'] = 'checked';
}
$optionsHere['value'] = $name;
$checkbox[] = "<li>" . sprintf($this->tags['checkboxmultiple'], $this->model, $this->field, $this->parseHtmlOptions($optionsHere), $title) . "</li>\n";
}
return "\n" . sprintf($this->tags['hiddenmultiple'], $this->model, $this->field, null, $title) . "\n<ul class=\"checkboxMultiple\">\n" . $this->output(implode($checkbox), $return) . "</ul>\n";
}
}
?>
Add this snip of css to your stylesheet and adjust the li width to your taste
Download code
ul.checkboxMultiple {
margin:0;
padding:0;
list-style-type:none;
}
ul.checkboxMultiple li {
display:block;
float:left;
width: 220px;
margin: 0 8px 0 0;
}
And add these tag templates to app/config/tags.ini.php
Download code
; Tag template for an input type='hidden' tag.
hiddenmultiple = "<input type="hidden" name="data[%s][%s][]" %s/>"
; Tag template for a input type='checkbox ' tag.
checkboxmultiple = "<input type="checkbox" name="data[%s][%s][]" %s/>%s"
Then you can use the new helper exactly the same way as $html->selectTag(...) just change your method call to $habtm->checkboxMultiple(..) instead.
Acknowledgment: This is based off code by MrRio in the Trac system:
https://trac.cakephp.org/ticket/1260
Comments
Comment
1 Displaying many options
ul.checkboxMultiple {
margin:0;
padding:0;
height: 8em;
overflow: auto;
list-style-type:none;
}
This will limit the list to 8(ish) visible items and provide display scrollbar to reach the others.
Comment
2 Tags and Naming
-ascendvisual
Comment
3 Tag Templates Added Above
Comment
4 labels for checkboxes
i.e. removed the final %s and modified the helper function internals to:checkboxmultiple=<input type="checkbox" name="data[%s][%s][]" %s/>
As you can see I've added the labelTag() method of the form helper to it, so we need to include the 'Form' helper in the array of helpers that this helper uses./**
* Returns a list of checkboxes.
*
* @param string $fieldName Name attribute of the SELECT
* @param array $options Array of the elements (as 'value'=>'Text' pairs)
* @param array $selected Selected checkboxes
* @param array $htmlAttributes Array of HTML options
* @param array $containerAttributes Array of HTML attributes used for the containing tag
* @param boolean $showEmpty If true, the empty checkbox option is shown
* @param boolean $return Whether this method should return a value
* @return string List of checkboxes
*/
function checkboxMultiple($fieldName, $options, $selected = null, $htmlAttributes = null, $containerAttributes = null, $showEmpty = true, $return = false) {
$this->Html->setFormTag($fieldName);
if ($this->Html->tagIsInvalid($this->Html->model, $this->Html->field)) {
if (isset($htmlAttributes['class']) && trim($htmlAttributes['class']) != "") {
$htmlAttributes['class'] .= ' form_error';
} else {
$htmlAttributes['class'] = 'form_error';
}
}
if (!is_array($options)) {
return null;
}
if (!isset($selected)) {
$selected = $this->Html->tagValue($fieldName);
}
if ($showEmpty == true) {
$checkbox[] = sprintf($this->tags['hiddenmultiple'], $this->Html->model, $this->Html->field, $this->Html->parseHtmlOptions($htmlAttributes));
}
foreach($options as $name => $title) {
$optionsHere = $htmlAttributes;
if (!isset($htmlAttributes['id'])) {
$optionsHere['id'] = $this->Html->model . Inflector::camelize($this->Html->field) . $name;
}
$optionsHere['value'] = $name;
if (($selected !== null) && ($selected == $name)) {
$optionsHere['checked'] = 'checked';
} else if (is_array($selected) && array_key_exists($name, $selected)) {
$optionsHere['checked'] = 'checked';
}
$checkbox[] = "<li>" . sprintf($this->tags['checkboxmultiple'], $this->Html->model, $this->Html->field, $this->Html->parseHtmlOptions($optionsHere), $title)
. $this->Form->labelTag($fieldName . $name, $title) . "</li>\n";
}
return "\n<ul".$this->Html->parseHtmlOptions($containerAttributes).">\n" . $this->Html->output(implode($checkbox), $return) . "</ul>\n";
}
I've also added the id attribute to the optionsHere array for each option, so that the label has something to attach itself to.
I also wanted its use to be transparent with the Html helper's selectTag() method for multiple selects so modified the prototype by removing $inbetween and adding a $containerAttributes param that corresponds to the $optionAttributes param in selectTag(). It can be used to specify the class of the
. I also added the $showEmpty param just like in selectTag().
To invoke it you might use something like this:
$habtm->checkboxMultiple('YourModel/YourModel', $options, $selected, array('class' => 'checkbox'), array('class' => 'checkboxMultiple'), true, true);
Hope its useful
Comment
5 Very nice
Something like this (along with Neil's label modification) should make it's way into Cake I think.
Comment
6 Working Example to download
Hope it comes in useful to somebody.
http://www.flipflops.org/example/habtm_checkbox_example.zip
Question
7 Unexpected Behaviour
Anyway I ended up rewriting part of the function (using while instead of foreach) - I was wondering if you had any ideas what the problem was - perhaps PHP4 vs 5?
/**
* Returns a list of checkboxes.
*
* @param string $fieldName Name attribute of the SELECT
* @param array $options Array of the elements (as 'value'=>'Text' pairs)
* @param array $selected Selected checkboxes
* @param string $inbetween String that separates the checkboxes.
* @param array $htmlAttributes Array of HTML options
* @param boolean $return Whether this method should return a value
* @return string List of checkboxes
*/
function checkboxMultiple($fieldName, $options, $selected = null, $inbetween = null, $htmlAttributes = null, $return = false)
{
$this->setFormTag($fieldName);
if ($this->tagIsInvalid($this->model, $this->field))
{
if (isset($htmlAttributes['class']) && trim($htmlAttributes['class']) != "")
{
$htmlAttributes['class'] .= ' form_error';
}
else
{
$htmlAttributes['class'] = 'form_error';
}
}
if (!is_array($options))
{
return null;
}
if (!isset($selected))
{
$selected = $this->tagValue($fieldName);
}
while(list($key, $name) = each($options))
{
$optionsHere = $htmlAttributes;
if(in_array($key, $selected))
{
$optionsHere['checked'] = 'checked';
}
$optionsHere['value'] = $key;
$checkbox[] = "
}
return "\n" . sprintf($this->tags['hiddenmultiple'], $this->model, $this->field, null, $name) . "\n
\n" . $this->output(implode($checkbox), $return) . "
\n";}
}
?>
Question
8 Help with error messages in 1.2.0.4798alpha
Notice (8): Undefined property: HabtmHelper::$model [CORE/app/views/helpers/habtm.php, line 20]
Notice (8): Undefined property: HabtmHelper::$field [CORE/app/views/helpers/habtm.php, line 20]
Any help is appreciated. Thanks again!
Comment
9 Partial solution.
But I get two other errors as well.
Notice (8): Undefined index: checkboxmultiple [CORE/app/views/helpers/habtm.php, line 51]
And:
Notice (8): Undefined index: hiddenmultiple [CORE/app/views/helpers/habtm.php, line 55]
They are not defined anywhere. I can't figure out why it works in 1.1 either. Well i solved it this way.
I added
$this->tags['checkboxmultiple'] = '%s';
AND
$this->tags['hiddenmultiple'] = '%s';
in the beginning of the code directly above $this->setForm.
Comment
10 Quick Tips
Thanks for the heads up on the changes to "model()" and "field()". Regarding the other two errors, I solved those by putting the tag definitions right at the top of the function definition for checkboxMultiple.
Like this:
function checkboxMultiple($fieldName, $options, $selected = null, $inbetween = null, $htmlAttributes = null, $return = false) {
# Tag template for an input type='hidden' tag.
$hiddenmultiple = '';
# Tag template for a input type='checkbox ' tag.
$checkboxmultiple = '%s';
$this->setFormTag($fieldName);
----------------------------------------->
I came up with this quick solution in order to get the checkboxes to recognize existing selections. In my setup cars and sites have a habtm relationship between each other.
if($this->data['Site'])
{
foreach($this->data['Site'] as $sv)
{
$selectedSites[$sv['id']] = $sv['name'];
}
}
else
{
$selectedSites = '';
}
I put that in the "edit" method of my controller. Hope that helps.
Comment
11 Final version that works for 1.2
I had to make all the changes you suggested to get it to work with 1.2
The final code that works for 1.2 would be the following.
function checkboxMultiple($fieldName, $options, $selected = null, $inbetween = null, $htmlAttributes = null, $return = false)
{
# Tag template for a input type='checkbox ' tag.
$checkboxmultiple = '<input type="checkbox" name="data[%s][%s][]" %s/>%s';
# Tag template for an input type='hidden' tag.
$hiddenmultiple = '<input type="hidden" name="data[%s][%s][]" %s/>';
$this->setFormTag($fieldName);
if ($this->tagIsInvalid($this->model(), $this->field())) {
if (isset($htmlAttributes['class']) && trim($htmlAttributes['class']) != "") {
$htmlAttributes['class'] .= ' form_error';
} else {
$htmlAttributes['class'] = 'form_error';
}
}
if (!is_array($options)) {
return null;
}
if (!isset($selected)) {
$selected = $this->tagValue($fieldName);
}
foreach($options as $name => $title) {
$optionsHere = $htmlAttributes;
if (($selected !== null) && ($selected == $name)) {
$optionsHere['checked'] = 'checked';
} else if (is_array($selected) && array_key_exists($name, $selected)) {
$optionsHere['checked'] = 'checked';
}
$optionsHere['value'] = $name;
$checkbox[] = "<li>" . sprintf($checkboxmultiple, $this->model(), $this->field(), $this->_parseAttributes($optionsHere), $title) . "</li>\n";
}
return "\n" . sprintf($hiddenmultiple, $this->model(), $this->field(), null, $title) . "\n<ul class=\"checkboxMultiple\">\n" . $this->output(implode($checkbox), $return) . "</ul>\n";
} /// checkboxMultiple()
And this is how you use it...
echo $myHtml->checkboxMultiple('MyModel/MyModel', $arrAllBoxes, $selectedBoxes, array('multiple' => 'multiple', 'class' => 'selectMultiple'));
But the addition I had to make to the controller method was following.
if($this->data['MyModel']['MyModel'])
{
foreach($this->data['MyModel']['MyModel'] as $selectedItem)
{
$selectedBoxes[$selectedItem] = $selectedItem;
}
}
else
{
$selectedBoxes = '';
}
Comment
12 A little hack.
It has to use FormHelper::input .. and actually this could be done by a little hack.
$form->input('Model.field][');
By appending ][ to the end the result will be
[Model][field][] Something like we wanted all along.
Comment
13 tagValue() is deprecated
Using value insterad of tagValue seems to work great. Thanks to all for putting this together.
Bug
14 When a form invalidates...
The error message is:
Notice: Uninitialized string offset: 0 in D:\TYPO3\Apache\htdocs\cake\app\views\helpers\habtm.php on line 36
EDIT: It seems I based this bug on older code... which I don't understand because I copied it just last week. Anyway, the problem I have using the code mentioned in the article is that the checkboxes don't keep their current state when the form does not validates.
This does seem to work for me:
<?php
class HabtmHelper extends HtmlHelper {
/**
* Returns a list of checkboxes.
*
* @param string $fieldName Name attribute of the SELECT
* @param array $options Array of the elements (as 'value'=>'Text' pairs)
* @param array $selected Selected checkboxes
* @param string $inbetween String that separates the checkboxes.
* @param array $htmlAttributes Array of HTML options
* @param boolean $return Whether this method should return a value
* @return string List of checkboxes
*/
function checkboxMultiple($fieldName, $options, $selected = null, $inbetween = null, $htmlAttributes = null, $return = false) {
$this->setFormTag($fieldName);
if ($this->tagIsInvalid($this->model, $this->field)) {
if (isset($htmlAttributes['class']) && trim($htmlAttributes['class']) != "") {
$htmlAttributes['class'] .= ' form_error';
} else {
$htmlAttributes['class'] = 'form_error';
}
}
if (!is_array($options)) {
return null;
}
if (!isset($selected)) {
$selected = $this->tagValue($fieldName);
}
foreach($options as $name => $title) {
$optionsHere = $htmlAttributes;
if (($selected !== null) && ($selected == $name)) {
$optionsHere['checked'] = 'checked';
} else if (is_array($selected) && count($selected) >0) {
foreach($selected as $array) {
if($array == $name) {
$optionsHere['checked'] = 'checked';
}
}
}
$optionsHere['value'] = $name;
$checkbox[] = "<li>" . sprintf($this->tags['checkboxmultiple'], $this->model, $this->field, $this->parseHtmlOptions($optionsHere), $title) . "</li>\n";
}
return "\n" . sprintf($this->tags['hiddenmultiple'], $this->model, $this->field, null, $title) . "\n<ul class=\"checkboxMultiple\">\n" . $this->output(implode($checkbox), $return) . "</ul>\n";
}
}
?>
Comment
15 Solution for wrong check boxes being selected
If your checkboxes are being selected in wierd ways (ie: if you select 3 our of 10 at random spots, after a save error the first 3 are ticket not the ones you wanted, there is a simple solution.
Change the following line:
} else if (is_array($selected) && array_key_exists($name,
$selected)) {
To:
} else if (is_array($selected) && array_search($name, $selected)) {
Comment
16 array search
sometimes it returns 0 - which is not false, so manually check for false
} else if (is_array($selected) && array_search($name , $selected)!==false) {
Question
17 Hidden fields and empty array elements
Cheers!
Comment
18 CakePHP 1.2 update
View:
<?php echo $form->input('Post.Tag', array('type'=>'select', 'multiple'=>'checkbox', 'options'=>$tags, 'label'=>'Tags:'));?>
Selecting list data is a bit different in this most recent version as well.
Model:
<?php
class Tag {
// The column value to show in SELECT OPTION's.
var $displayField = 'name';
}
?>
Controller:
<?php
function index() {
$this->set('tags', $this->Tag->find('list'));
}
?>
Question
19 i think i was missing something
View Template:
<?php echo $form->input('Post.Tag', array('type'=>'select', 'multiple'=>'checkbox', 'options'=>$tags, 'label'=>'Tags:'));?>
didn't we talk about checkbox ???
i'm sorry if i was mistaken
Comment
20 check your cake version
this exact snippet works a-ok for me.
Comment
21 im sorry
previously, i was using cake 1.2.0.5875 pre-beta then i try it on cake 1.2.0.6311 beta and its working..
thanx ambiguator
Comment
22 How to do this without the helper...
Controller Class:
<?php$options = $this->Model->find('list');
$this->set('options', $options);
?>
View Template:
<?php
foreach ($options as $id=>$label) {
echo $form->input("Model.id.$id",
array(
'label'=>$label,
'type'=>'checkbox',
'checked'=>($this->data["Model"]["id"][$id] == 1 ? 'checked' : false)
)
);
}
?>
Works fine if you'd rather not use yet another helper.
Comment
23 Problem with multiple checkbox when saving data
foreach($this->data as $user){
foreach($user['user_id'] as $userid){
// If User already assigned then Update else Insert
if($this->Project->query("SELECT id,user_id,project_id FROM tts_users_projects ProjectUser WHERE user_id = '".$userid."' AND project_id = '".$id."'")){
//$this->Project->query("UPDATE tts_users_projects SET user_id = '".$userid."' AND project_id = '".$id."' WHERE project_id = ".$id." AND user_id = ".$userid);
}else{
if (!$this->Project->query("INSERT INTO tts_users_projects(`user_id`,`project_id`) VALUES ('".$userid."','".$id."')")){
$error[] = $user; // fetch Error(s)
}
}
}
}
I dont understand why when I save data, and forexample I have checkboxes in the sequence below:
[ ] Waqar
[X] Mohsin
[X] Mubashir
[ ] Test
if i check the "Test" it will be saved, but if I check "Waqar" which is above the checked checkboxes, it doesn't save.
Any idea? Its confusing me.
Question
24 Problem with multiple checkbox when saving data
foreach($this->data as $user){
foreach($user['user_id'] as $userid){
// If User already assigned then Update else Insert
if($this->Project->query("SELECT id,user_id,project_id FROM tts_users_projects ProjectUser WHERE user_id = '".$userid."' AND project_id = '".$id."'")){
//$this->Project->query("UPDATE tts_users_projects SET user_id = '".$userid."' AND project_id = '".$id."' WHERE project_id = ".$id." AND user_id = ".$userid);
}else{
if (!$this->Project->query("INSERT INTO tts_users_projects(`user_id`,`project_id`) VALUES ('".$userid."','".$id."')")){
$error[] = $user; // fetch Error(s)
}
}
}
}
I dont understand why when I save data, and forexample I have checkboxes in the sequence below:
[ ] Waqar
[X] Mohsin
[X] Mubashir
[ ] Test
if i check the "Test" it will be saved, but if I check "Waqar" which is above the checked checkboxes, it doesn't save.
Any idea? Its confusing me.
Comment
25 Re: How to do this without the helper
I can't use input('Post.Tag', array('type'=>'select', 'multiple'=>'checkbox', 'options'=>$tags, 'label'=>'Tags:'));?> since it automatically generates the checkboxes and doesn't allow me to add anything beside them.
Anyway, I think there's something wrong with the view code if used on an edit view.
View Template:
<?php
foreach ($options as $id=>$label) {
echo $form->input("Model.id.$id",
array(
'label'=>$label,
'type'=>'checkbox',
'checked'=>($this->data["Model"]["id"][$id] == 1 ? 'checked' : false)
)
);
}
?>
It does not display the already checked checkboxes and it also doesn't pass the correct $this-data['Field'] data to the controller when an edit is submitted.
I had to modify it to the ff to make it work
View Template:
<?php
foreach ($options as $id=>$label) {
$check = false;
//Determine if the checkbox is already checked or not
foreach($this->data['Model'] as $data)
{
if($data['id'] == $id) {
$check = true;
break;
}
}
//First param is weird but it works
echo $form->input('Model.Model][',
array(
'label'=>$label,
'type'=>'checkbox',
'checked'=> $check,
'value' => $id
)
);
echo $html->link('My link beside the checkbox','/');
}
?>
Would there be an easier way to put a link (or whatever text) that's associated with a checkbox BESIDE the checkbox without having to do the above?
-pacfan
Question
26 $this->data["Model"]["id"][$id]?
'checked'=>($this->data["Model"]["id"][$id] == 1 ? 'checked' : false
really OK?
If I pr($this->data), the HABTM array is like:
[Model][0][id] [Model][1][id]
In my case, list is like
$options = $this->Model1->Model->find('list');
tho.
Comment
27 Automagic
echo $form->input('Model', array('multiple'=>'checkbox'));Comment
28 How to make it prettier in 1.2
in the view, add:
$html->tags['checkboxmultiplestart'] = '<div class="checkboxmultiple">';or i guess you could use $this->HtmlHelper->tags in the controller. that will put a div wrapper around the list of checkboxes so you can format it.$html->tags['checkboxmultipleend'] = '</div>';
then add a css blob to your css file, or inline, or whatever:
div.checkboxmultiple {of course those values might need to be tweaked for your site.clear: none;
height: 6em;
overflow: auto;
max-width: 250px;
border: 1px solid gray;
}