Archive

Posts Tagged ‘ComboBoxItem’

Field dependencies

January 17, 2009 Comments off

A form will typically have a number of fields that are dependent on conditions with another field. Consider a selection that is required if another field is used. Or maybe a field is disabled if a certain combo value is chosen. No matter, these dependencies have to be managed somewhere. In traditional client development this is likely to be handled by a controller (i.e. MVC) but there are times the dependencies are not complex and a full blown controller is just not necessary.

Take the example I posted in my previous blog entry shown again below.

fields: [
    { name: "major", title: "Major", editorType: "ComboBoxItem", type: "select",
       width: "*",
       optionDataSource: "majorValuesDS",
       allowEmptyValue: true,
       valueField: "major",
       displayField: "description",
       completeOnTab: true,
       changed: function(form, item, value) {
             var field = form.getField('minor');
             field.setValue(null);
             field.setDisabled(!value);
       },
       pickListWidth: 450,
       pickListFields: [
             { name: "major", width: 50 },
             { name: "description" }
       ]
    },
    { name: "minor", title: "Minor", editorType: "ComboBoxItem", type: "select",
       width: "*",
       optionDataSource: "minorValuesDS",
       autoFetchData: false,
       allowEmptyValue: true,
       disabled: true,
       valueField: "minor",
       displayField: "description",
       completeOnTab: true,
       pickListWidth: 450,
       pickListFields: [
             { name: "minor", width: 50 },
             { name: "description" }
       ],
       getPickListFilterCriteria: function() {
             return {
                   major: this.form.getValue("major"),
                   minor: this.form.getValue("minor")
             }
       }
     }
]

Notice that when the major is changed the minor is automatically cleared and is enabled only if the major actually has a value. All of this is handled in the changed handler of the major. Finally, note also that the minor puts the pick list criteria together using the value from major. So the dependency is dealt with by both fields. I would rather see all of this in one place: the minor.

So, how can we accomplish this? We can use an observer. The minor field just needs to register an observer on the parent field (major) changed event. The observer can then be written as:

    { name: "minor", title: "Minor", editorType: "ComboBoxItem", type: "select",
    ...
    majorChanged: function(item, majorItem) {
        // Major changed - clear our selection, if any
        item.setValue(null);
        // Disable this field if major is empty
        var major = majorItem.getValue();
        item.setDisabled(!major);
    },
    ...

That looks nice. Now when majorChanged is called the minor field can update itself as needed. But how is this method called? We need to register an observer on the major field changed event handler. Something like:
{ name: "minor", title: "Minor", editorType: "ComboBoxItem", type: "select",
    ...
    init: function() {
        this.Super("init", arguments);
        // Register observer on major
        var field = this.getField('major');
        this.observe(field, "changed", "observer.itemIdChanged(observer,  observed)");
    },
    ...

That’s easy enough! But, wait! It doesn’t work. Apparently it’s too early at init() time to register an observer on another field. So we need a later event to make this observation. Once the fields have been defined on the form all fields are initialized with a call to setValue(null) by the form. So we can use this event to register our observer.

Final working code ends up as:

{ name: "minor", title: "Minor", editorType: "ComboBoxItem", type: "select",
    ...
    setValue: function(newValue) {
        this.Super("setValue", arguments);
        // Register observer on major
        if (!this._observing) {
            var field = this.getField('major');
            this.observe(field, "changed", "observer.majorChanged(observer, observed)");
            this._observing = true;
        }
    },
    majorChanged: function(item, majorItem) {
        // Major changed - clear our selection, if any
        item.setValue(null);
        // Disable this field if major is empty
        var major = majorItem.getValue();
        item.setDisabled(!major);
    },
    ...

Now, after removing the major’s changed event handler only the minor field knows about its dependency on the contents of major.

I don’t particularly like using setValue in this way but I haven’t found a better solution. If you have a suggestion, please let me know. Otherwise, happy SmartClienting.

Tags: ,

Dependent ComboBoxes

January 15, 2009 Comments off

The application which is the target of my prototyping with SmartClient uses a number of dependent combo boxes to enter information or search criteria. In some cases the selection list is short and static so value maps can be used. Other times the list is larger and it makes more sense to pull the values from a dataSource using the optionDataSource configuration. Setting these dependencies up is very straigtforward and one of the excellent features of SmartClient.

Here is an example of two combo box fields in a form:

fields: [
    { name: "major", title: "Major", editorType: "ComboBoxItem", type: "select",
      width: "*",
      optionDataSource: "majorValuesDS",
      allowEmptyValue: true,
      valueField: "major",
      displayField: "description",
      completeOnTab: true,
      changed: function(form, item, value) {
          var field = form.getField('minor');
          field.setValue(null);
          field.setDisabled(!value);
      },
      pickListWidth: 450,
      pickListFields: [
          { name: "major", width: 50 },
          { name: "description" }
      ]
    },
    { name: "minor", title: "Minor", editorType: "ComboBoxItem", type: "select",
      width: "*",
      optionDataSource: "minorValuesDS",
      autoFetchData: false,
      allowEmptyValue: true,
      disabled: true,
      valueField: "minor",
      displayField: "description",
      completeOnTab: true,
      pickListWidth: 450,
      pickListFields: [
          { name: "minor", width: 50 },
          { name: "description" }
      ],
      getPickListFilterCriteria: function() {
          return {
              major: this.form.getValue("major"),
              minor: this.form.getValue("minor")
          }
      }
    }
]


In this example there is one combo to select a major and one to select a minor. Minor values are unique to the major so fetching the available minors requires the major to be part of the criteria. The way that is handled is overriding the getPickListFilterCriteria() on the minor field. Note also in the example that when the major changes, the minor is cleared and then enabled if there is a valid major.

The above example works great for data entry. When a value is selected in either combo, the description is shown as expected. However, what if we want to edit an existing entity that has major and minor values and we expect the description to be shown at load time?

editItem: function(form) {
   form.fetchData();
   var field = form.getField('minor');
   field.setDisabled(false);
}


Above is an example fetch on the form for an exiting item. Criteria would normally be included in the fetchData() call but is excluded here for clarity. The same can be accomplished with an editRecord() call also.

Well, this example doesn’t work so well in our case because the minor is not the primary key for our dependent combo. Unfortunately, as I posted on the SmartClient forums it turns out that upon setting the initial value for minor does trigger a fetch on the optionDataSource but getPickListFilterCriteria() is not called so the search criteria is wrong. Thus the description is not found to be displayed.

A workaround that I have previously used successfully is to add a call to minor.fetchData() after loading the record which forces the combo to refresh its list. At that time it does use our dynamic criteria and the result is correct.

This has been bothering me for a while so I finally dug into the code to find a fix for this situation. A patch for the FormItem base class is included below that fixes the issue. I cannot vouch for this patch not having side-effects so I would be interested to know if anyone does find issue with it.

isc.FormItem.addMethods({

//_checkForDisplayFieldValue : function (newValue) {
$43f: function(newValue) {
    //var inValueMap = (this._mapKey(newValue, true) != null);
    var inValueMap = (this.$17b(newValue, true) != null);

    if (!inValueMap) {
        var ods = this.getOptionDataSource();
        if (ods) {
            // Check for the case where we're already fetching the value
            var recordCrit = {};
            if (!this.filterLocally) {
                if (this.getPickListFilterCriteria)
                    recordCrit = this.getPickListFilterCriteria();
                else
                    recordCrit[this.getValueFieldName()] = newValue;
            }
            ods.fetchData(recordCrit,
                { target: this,
                  methodName: "fetchMissingValueReply"
                },
                { showPrompt: false, clientContext: { dataValue: newValue} }
            );
        }
    }
}

});


This change is to an internal method called FormItem._checkForDisplayFieldValue so I had to use it obfuscated name $43f instead. Normally these internal methods are not meant to be replaced so they are therefore obfuscated as part of the minimization process. The contents of the patch above are from the distributed source (with the _mapKey obfuscated method replaced as well).

The change I made is to call getPickListFilterCriteria() if it is defined on the derived form item (ex. ComboBoxItem) instead of creating the criteria solely from the field’s value.

Now, as suggested by the editItem function above, if there was only an enableIf() method on form items…

If this fix is of value to you, please let me know your results.

Tags: ,
Follow

Get every new post delivered to your Inbox.