Introduction ¶
How many of us has wondered how to create an autocomplete that will display the names of a related models but do require the id of that selected name to be submitted for model creation/update?
I was looking around wiki and found that was no approach as the one I did so I guessed this is worth to write.
Requirements ¶
For our example, I want to be able to:
- Have an autocomplete field in our form
- Once user selects an item in the dropdown list and fill a hidden box with the id of the selected item for submission
Making the right choice ¶
To setup the autocomplete was a very straight forward operation, but I couldn't figure out how to get values from a custom JSON response and then fill the correspondent hidden fields.
CAutoComplete does have a way to do it, but I wanted to use CJuiAutoComplete to get all the cool features of JQuery UI and by looking at his code there was no method chain, something that is required to work with custom JSON responses as we need to override some methods.
My Solution ¶
After doing some research I decided to:
- extend from CJuiAutoComplete
- include the required property for method chain and modify its 'run' function
- then initialize the newly created property with the javascript functions that handle my custom JSON
Extending from CJuiAutoComplete and make required modifications ¶
Very simple, we are going to add a methodChain property and modify the run function to include it (zii is not a major concern to Yii, but main developers should think about this minor change).
class myAutoComplete extends CJuiAutoComplete
{
/**
* @var string the chain of method calls that would be appended at the end of the autocomplete constructor.
* For example, ".result(function(...){})" would cause the specified js function to execute
* when the user selects an option.
*/
public $methodChain;
/**
* Run this widget.
* This method registers necessary javascript and renders the needed HTML code.
*/
public function run()
{
list($name,$id)=$this->resolveNameID();
if(isset($this->htmlOptions['id']))
$id=$this->htmlOptions['id'];
else
$this->htmlOptions['id']=$id;
if(isset($this->htmlOptions['name']))
$name=$this->htmlOptions['name'];
if($this->hasModel())
echo CHtml::activeTextField($this->model,$this->attribute,$this->htmlOptions);
else
echo CHtml::textField($name,$this->value,$this->htmlOptions);
if($this->sourceUrl!==null)
$this->options['source']=CHtml::normalizeUrl($this->sourceUrl);
else
$this->options['source']=$this->source;
$options=CJavaScript::encode($this->options);
$js = "jQuery('#{$id}').autocomplete($options){$this->methodChain};";
$cs = Yii::app()->getClientScript();
$cs->registerScript(__CLASS__.'#'.$id, $js);
}
}
Using our widget ¶
Now that we have our beautiful widget that handles method chain in our Autocomplete, let's assume a couple of things:
- We saved our class in our application folder - ie protected/extensions
- We have a hidden INPUT HTML element with model's attribute_id
- We have created an action on our testController named autocomplete that returns a JSON object of the following format:
// This function will echo a JSON object
// of this format:
// [{id:id, name: 'name'}]
public function actionAutocomplete(){
$res = array();
$term = Yii::app()->getRequest()->getParam('term', false);
if ($term)
{
// test table is for the sake of this example
$sql = 'SELECT id, name FROM {{test}} where LCASE(name) LIKE :name';
$cmd = Yii::app()->db->createCommand($sql);
$cmd->bindValue(":name","%".strtolower($term)."%", PDO::PARAM_STR);
$res = $cmd->queryAll();
}
echo CJSON::encode($res);
Yii::app()->end();
}
We have everything, let's use our widget in our view:
// REMEMBER, we have a hidden
// input HTML element with model's attribute_id
<?php echo $form->hiddenField($model, 'attribute_id'); ?>
<?php
// ext is a shortcut for application.extensions
$this->widget('ext.myAutoComplete', array(
'name' => 'test_autocomplete',
'source' => $this->createUrl('test/autocomplete'),
// attribute_value is a custom property that returns the
// name of our related object -ie return $model->related_model->name
'value' => $model->isNewRecord ? '': $model->attribute_value,
'options' => array(
'minLength'=>3,
'autoFill'=>false,
'focus'=> 'js:function( event, ui ) {
$( "#test_autocomplete" ).val( ui.item.name );
return false;
}',
'select'=>'js:function( event, ui ) {
$("#'.CHtml::activeId($model,'attribute_id').'")
.val(ui.item.id);
return false;
}'
),
'htmlOptions'=>array('class'=>'input-1', 'autocomplete'=>'off'),
'methodChain'=>'.data( "autocomplete" )._renderItem = function( ul, item ) {
return $( "<li></li>" )
.data( "item.autocomplete", item )
.append( "<a>" + item.name + "</a>" )
.appendTo( ul );
};'
));
?>
Done! Just make sure that when you do submit your form, you get the value from the hidden field instead of the autocomplete element :)
Final Notes ¶
I do not know if there are other ways of doing the same thing (apart from pure Javascript) to have the same results. If you know, with CJuiAutoComplete widget, let us know here.
Hope you find this wiki useful.
Cheers
hidden field
For this solution to be part of the core... there would need to be a way to handle the hidden field automatically... any idea for this ?
Other ways to do it
A yii'er told me another way to do it without method chain:
$this->widget('zii.widgets.jui.CJuiAutoComplete', array( //'model'=>$model, //'attribute'=>'name', 'id'=>'country-chain', 'name'=>'country_chain', 'source'=>$this->createUrl('request/suggestCountry'), 'options'=>array( 'delay'=>300, 'minLength'=>2, 'showAnim'=>'fold', 'select'=>"js:function(event, ui) { $('#label').val(ui.item.label); $('#code').val(ui.item.code); $('#call_code').val(ui.item.call_code); }" ), 'htmlOptions'=>array( 'size'=>'40' ), ));
Source: http://www.eha.ee/labs/yiiplay/index.php/en/site/widget?view=autocomplete
More about another way
I just saw to late your post, I spend few hours reading Yii source code, and I come with similar solution:
$this->widget('zii.widgets.jui.CJuiAutoComplete', array( 'name'=>'customer_name', 'sourceUrl' => Yii::app()->createUrl('customer/autocomplete'), 'options'=>array( 'minLength'=>'2', 'select'=>'js:function( event, ui ) { $("#'.CHtml::activeId($model,'customer_id').'").val(ui.item.id); return false; }', ), 'htmlOptions'=>array( 'class'=>'input-1' ), ));
while data(json) from 'customer/autocomplete' must be in next format:
var customers = [ {label:'customer1', id: '1'}, {label:'customer2', id: '4'}, {label:'customer3', id: '5'}, ]
I used 'id' as key for id, but I think you should get same results using value for key.
custom render
i hade to add this before the custom class declaration to get it work
Yii::import('zii.widgets.jui.CJuiAutoComplete');
the other ways without method chain can't do a custom render of the list
http://jqueryui.com/demos/autocomplete/#custom-data
cgridview
is there a way to implement this into cgridview? I'm trying but it's not working, but I'm only a beginner. can you please help? thanks
in newer version yii1.1.16
in method chain use : .autocomplete("instance") . like down
'.autocomplete("instance")._renderItem = function( ul, item ){
return $( "<li></li>" ) .data( "item.autocomplete", item ) .append( "<a><img src=\'" + item.imgsrc + "\' />" + item.label+ "</a>" ) .appendTo( ul ); }'
If you have any questions, please ask in the forum instead.
Signup or Login in order to comment.