Archive

Archive for January, 2009

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: ,

Extended TextItem

January 13, 2009 Comments off

Along with the MaskedTextItem I detailed in a previous blog entry the desire for an enhanced text field to support automatic case conversion on the fly and keystroke filtering. Well I finally broke down and wrote one detail here.

The new ExtTextItem can be used in place of the TextItem and it will function identically if not configured for the enhanced features. To enable automatic case conversion, set the characterCasing property to either “Upper” or “Lower”. All typed alphabetic characters will then be translated as desired. Additionally, any value placed into the field via setValue() will also be translated.

Keypress filtering can also be used to limit entry in the field to just the desired characters. This serves as a preemptive form of validation. Assign a string representing a regular expression to keyPressFilter that defines the characters desired in the field. All others are rejected. For example, a filter of “[0-9]” would only allow numeric entry.

Finally as prompted by a recent Ajaxian posting, ExtTextItem also includes support for embedding the hint within the field instead of trailing. This is made possible by using a new CSS style textItemHint and is enabled by setting the hintInField property to true. Here is an example of the embedded hints:

When the field receives focus the hint is hidden. On loss of focus the hint is shown if the value is still empty.

The source for the control can be downloaded from ExtTextItem.js.

Tags:

Masked TextItem

January 12, 2009 7 comments

I detailed in a previous blog entry the desire for a masked edit field for SmartClient. After some consideration I decided to write a basic MaskedTextItem control. It is derived from the standard TextItem.


As you can see from the sample form on the right, it is very easy to case conversion, limit key entry to letters or digits and include literal values. Note that the phone number field is currently in edit mode.
The actual value saved can include the literal characters or not. So it is possible to have the date field saved as 01172008 but show in the mask as 01/17/2008. To include literals in the value set includeLiterals: true on the field.

The source for the control can be downloaded at MaskedTextItem.js.

[2009/01/16] The MaskedTextItem does not work in Safari. The reason is that the text box selections cannot be read or updated from SmartClient because these methods were previously not supported in earlier versions of Safari. It appears that Safari can now be supported (at least version 3.2.5 which I tested) so I have put in a request to Isomorphic to enable this support.

There are still changes required to MaskedTextItem that are specific to Safari once the get/setSelectionRange is supported. A new version will be made available at that time.

Tags:
Follow

Get every new post delivered to your Inbox.