Problem in model implementation

Hi all,

I’ve been debugging quite hard in the last few hours until I realised the flaw of my implementation and so I decided to come here to seek help from more experienced Yii users which you are :)

My model has got an attribute "location"

In the form I want to have a drop down list with a bunch of values that are already present in the db, but I want to give the user the ability to freely enter a new value in case it wasn’t in the list.

On creation the text field will take precedence over the drop down list.

On update the drop down will update the value if the text field will has not been touched, other wise -again- the text field will take precedence.

Now I’ve got the dropDownList and the textField in the form, which is fine, but I don’t know how to bind the two fields.

My solution (which has been proved to be faulty) is this:

the dropDownList has got its own attribute name "locationProvided"

the textField is the original one "location". (these cannot be the same as the second field will override the first one).

Now locationProvided has got setLocationProvided() and getLocationProvided() that will update location if a value has been selected.

This solution will not work, as location when is processed, will override the value setLocationProvided() has given.

If you’re still with me, the solution I’m trying to find is something that will impact nothing or less the controller and live mainly in the model (this way I could implement the solution without refactoring too much any other model that needs such thing)

A possibile alternative solution, which I haven’t tried yet, could be using an array of values for location, instead of two different attributes, but I don’t know whether that could cause any other issues.

Thanks for any suggestions that can come through.

what I suggest is to use an autocomplete solution and have one textfield.

http://jqueryui.com/demos/autocomplete/

That would work, but in terms of usability it’s an impediment for the client, this means that he wants to browse the entries to see what’s already in.

Regarding that, the two fields can be improved quite a bit, as show only the drop down list and have the text field appear only if the entry is not found, and stuff like that. but that wouldn’t solve the problem anyway.

Apart from that another solution proposed is to have only a "beforeSave()" function, to save the right value etc.

I’m starting thinking that the solution with the array would probably be the best, and have only a getter for location that retrieves the correct and more updated value upon request… but still I need to try it and I’m not completely sure about it.

How about this.

  1. Build Dropdown list with existing entries.

  2. Have a textfield next to it with an ‘add’ submit button.

  3. Use ajax to clear the textfield and refresh the dropdown after record is saved

Ok, reporting back on the solutions that I choose (simplest in terms of code refactoring).

Still the solution does not use any Ajax aid, which can be built on top of it (as clearing the field or showing/hiding any drop down list or edit field).

The controller wasn’t modified (this means that it is still working with just


$model->attributes=$_POST['Show'];

The model instead had a couple of tweaks (theoretically these can be brought up in a common model class with some improvements.

NOTE: The only physical attribute the model had was location.


Class Show extends CActiveRectord {

    public $locationProvided = null;

    ...


    public function rules() {

        return array(

             array('..., location, locationProvided', 'safe')

        );

    }


    /**

     * updates the $attribute based on the Provided$attribute and $value

     *

     * @param string $attribute attribute name

     * @param string $newValue  attribute value

     */

    public function setSpecificAttribute($attribute, $newValue) {

        $providedAttr = $attribute . 'Provided';

        // no changes to the attribute

        if ($this->$attribute === $newValue) {

            // use provided attribe over the old ones

            if ($this->$providedAttr !== '') {

                $this->$attribute = $this->$providedAttr;

                $this->$providedAttr = '';

            }

            // else leave it as it is

        } else {

            $this->$attribute = $newValue;

        }

    }

    

    /**

     * call an internal function to do what's needed

     *

     * @param string $name  attribute name

     * @param string $value attribute value

     */

    public function __set($name,$value) {

        if ($name === 'location') {

            $this->setSpecificAttribute($name, $value);

        } else {

            parent::__set($name, $value);

        }

    }

    

    /**

     * adds some specific initialisation for certain values in case their value

     * is empty.

     *

     * @return bool

     */

    public function beforeValidate() {

        // update the right attributes with provided values in case they're empty

        if ($this->location === '') {

            $this->location = $this->locationProvided;

        }

        return parent::beforeValidate();

    }

    

    ...

}

in the view I just added the right drop down list, which can be done with something like:

in _form.php


	<div class="row">

		<?php echo $form->labelEx($model,'location'); ?>

        <?php

         $data = CHtml::listData(

            $model->findAll(array('condition' => 'location != ""','order' => 'location')),

            'location',

            'location'

        );

        echo $form->dropDownList(

            $model, 

            'locationProvided', 

            $data,

            array('prompt' => 'Select location')

        ); 

        ?><br />

        or enter the location manually:<br />

		<?php echo $form->textField($model,'location', array('size'=>60)); ?><br />

		<?php echo $form->error($model,'location'); ?>

	</div>



Hope this helps someone else.

Found interesting how the virtual attributes and the automatic assignment works behind the scenes.