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
Add this snip of css to your stylesheet and adjust the li width to your taste
And add these tag templates to app/config/tags.ini.php
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
<?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
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
; 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








here's the solution.
replace the setFormTag line with this.
$this->_initInputField($fieldName);
and you're done.
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;
}
echo $form->input('Model', array('multiple'=>'checkbox'));'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.
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.
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.
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.
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
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
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
this exact snippet works a-ok for me.
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'));
}
?>
Cheers!
sometimes it returns 0 - which is not false, so manually check for false
} else if (is_array($selected) && array_search($name , $selected)!==false) {
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)) {
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";
}
}
?>
Using value insterad of tagValue seems to work great. Thanks to all for putting this together.
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.
function _explode($model, $field)
{ return((explode(",",$this->data[$model][$field])));
}
function _populateChkBoxList($model, $field){
$tmp1=$this->_explode($model, $field);
$tmpArr=array();
foreach($tmp1 as $val){
$tmpArr[$val]=$val;
}
return $tmpArr;
}
This is working fine..
but how do i get a CSV from the add action ?
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.
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 = '';
}
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.
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!
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";}
}
?>
Hope it comes in useful to somebody.
http://www.flipflops.org/example/habtm_checkbox_example.zip
Something like this (along with Neil's label modification) should make it's way into Cake I think.
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
-ascendvisual
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.
Comments are closed for articles over a year old