Two years ago, we published an article called, Creating Wijmo Controls. The article described the implementation of the MultiSelect control, a drop-down control showing a list with items that can be individually selected by users.
The implementation is relatively simple. All we had to do was extend the ComboBox control and customize the ListBox displayed in the drop-down. The MultiSelect control described in that article was deemed useful enough to be included in the wijmo.input module, so you don’t have to worry about including it explicitly in your projects.
Recently, a customer requested a similar control that would display a multi-select TreeView in the drop-down. This article describes the implementation of the DropDownTree control.
When the control is ready, it will look like this:
This time, the control uses functionality from two separate Wijmo modules: input and nav. The steps required are the same we took when developing the MultiSelect control:
Selecting the Base Class
In this case, we decided to extend the DropDown control, which contains all the logic needed to implement an input element with a drop-down button and a generic drop-down that may be used to host any controls. The DropDown control is used as a base class for the ComboBox, InputColor, and InputDate controls.
Defining the Object Model
Because the DropDownTree control hosts a TreeView in its drop-down, we decided to expose the main properties of the TreeView control directly from the DropDownTree:
- treeView gets a reference to the TreeView control displayed in the drop-down.
- itemsSource gets or sets a reference to the array of objects used to populate the TreeView.
- displayMemberPath gets or sets the name of the property used as the visual representation of the items (defaults to
'header'). - childItemsPath gets or sets the name of the property that contains the child items for each item in the data source (defaults to 'items').
- showCheckboxes gets or sets a value that determines whether the control should add checkboxes to each item so users may select multiple items (defaults to true).
We also added a few extra properties and events to define the current selection and how it is represented in the control header. These properties mirror the corresponding ones in the MultiSelect control:
- checkedItems gets or sets an array containing the items that are currently selected.
- checkedItemsChanged is the event that occurs when the value of the CheckedItems property changes.
- maxHeaderItems is the maximum number of selected items to display in the control header.
- headerFormat gets or sets the format string used to create the header content when the control has more than * maxHeaderItems items checked.
- headerFormatter gets or sets a function that gets the text displayed in the control header. This overrides the settings of the maxHeaderItems and headerFormat properties.
Implementing the Control
We'll start by declaring the control as an extension of the base class:
namespace wijmo.input {
export class DropDownTree extends DropDown {
}
}
The "extends DropDown" statement ensures our control inherits all functionality from the base DropDown class, including properties, events, methods, and all internal/private members.
Creating the TreeView
Next, we override the _createDropDown method in the DropDown class to create the TreeView control that will be displayed in the drop-down.
In addition to creating the TreeView, we override the onIsDroppedDownChanged method to transfer the focus to the tree when the drop-down opens and the control has the focus. This allows users to navigate the tree using the keyboard. They can search for items by typing their content, check items by pressing space, or navigate the tree using the cursor keys.
namespace wijmo.input {
export class DropDownTree extends DropDown {
private _tree: wijmo.nav.TreeView;
// create the drop-down element
protected _createDropDown() {
// create child TreeView control
let lbHost = document.createElement('div');
setCss(lbHost, {
width: '100%',
border: 'none'
});
this._tree = new wijmo.nav.TreeView(lbHost, {
showCheckboxes: true
});
}
// switch focus to the tree when the drop-down opens
onIsDroppedDownChanged(e?: EventArgs) {
if (this.containsFocus() && this.isDroppedDown) {
this._tree.focus();
}
super.onIsDroppedDownChanged(e);
}
}
}
Exposing the TreeView and its Properties
The next step is adding the properties that expose the TreeView and its main properties:
namespace wijmo.input {
export class DropDownTree extends DropDown {
private _tree: wijmo.nav.TreeView;
get treeView(): wijmo.nav.TreeView {
return this._tree;
}
get itemsSource(): any[] {
return this._tree.itemsSource;
}
set itemsSource(value: any[]) {
this._tree.itemsSource = value;
}
// same for displayMemberPath, childItemsPath,
// and showCheckboxes
// create the drop-down element
protected _createDropDown() {…}
}
}
These properties are just shortcuts that get or set the corresponding properties on the contained TreeView. As such, they are very simple. We don't even bother with type-checking since the TreeView will handle that for us.
The checkedItems Property
The main property in the control is checkedItems, which gets or sets an array with the user’s current selection. We could implement it as a pass-through property like the ones above, but in that case the control would only work for multiple selection. We decided to make it work also for single selection, so we must check the value of the showCheckboxes property and use either the tree’s checkedItems or selectedItem properties.
In addition to the checkedItems property, we also implement the checkedItemsChanged event and its companion method onCheckedItemsChanged. This is the standard pattern for Wijmo events. Every event X has a corresponding onX method that is responsible for raising the event.
namespace wijmo.input {
export class DropDownTree extends DropDown {
private _tree: wijmo.nav.TreeView;
// TreeView pass-through properties…
get checkedItems(): any[] {
let tree = this._tree;
if (tree.showCheckboxes) {
return tree.checkedItems;
} else {
return tree.selectedItem
? [tree.selectedItem] : [];
}
}
set checkedItems(value: any[]) {
let tree = this._tree;
if (tree.showCheckboxes) {
tree.checkedItems = asArray(value);
} else {
tree.selectedItem = isArray(value)
? value[0] : value;
}
}
readonly checkedItemsChanged = new Event();
onCheckedItemsChanged(e?: EventArgs) {
this.checkedItemsChanged.raise(this, e);
}
// create the drop-down element
protected _createDropDown() {…}
}
}
Note that even in the case of single selection, the checkedItems property returns an array (which is either empty or contains a single element).
Updating the Control Header
We will not discuss the implementation of the maxHeaderItems, headerFormat, or headerFormatter properties because they are trivial. The interesting logic is contained in the _updateHeader function, which uses these properties and is automatically invoked to update the control header when their value or the selection change:
namespace wijmo.input {
export class DropDownTree extends DropDown {
private _tree: wijmo.nav.TreeView;
// TreeView pass-through properties…
// checketItems property…
private _updateHeader() {
let items = this.checkedItems;
if (isFunction(this._hdrFormatter)) {
this.inputElement.value = this._hdrFormatter();
} else {
let hdr = '';
if (items.length > 0) {
if (items.length <= this._maxHdrItems) {
if (this.displayMemberPath) {
let dmp = this.displayMemberPath,
binding = new Binding(dmp);
items = items.map((item) => {
return binding.getValue(item);
});
}
hdr = items.join(', ');
} else {
hdr = format(this.headerFormat, {
count: items.length
});
}
}
this.inputElement.value = hdr;
}
}
// create the drop-down element
protected _createDropDown() {…}
}
}
The Constructor
The control is almost done. The final step is to implement the constructor, which connects the parts with event listeners and calls the initialize method to initialize properties and event handlers with values provided by the user in the options parameter:
namespace wijmo.input {
export class DropDownTree extends DropDown {
private _tree: wijmo.nav.TreeView;
private _readOnly: boolean;
private _maxHdrItems = 2;
private _hdrFmt = wijmo.culture.MultiSelect.itemsSelected;
private _hdrFormatter: Function;
constructor(element: HTMLElement, options?: any) {
super(element);
addClass(this.hostElement, 'wj-dropdowntree');
// make header element read-only
this._tbx.readOnly = true;
// update header now, when the itemsSource changes,
// and when items are selected
this._updateHeader();
let tree = this._tree;
tree.checkedItemsChanged.addHandler(() => {
this._updateHeader();
this.onCheckedItemsChanged();
});
tree.selectedItemChanged.addHandler(() => {
if (!tree.showCheckboxes) {
this._updateHeader();
this.onCheckedItemsChanged();
}
});
tree.loadedItems.addHandler(() => {
this._updateHeader();
});
// initialize control options
this.initialize(options);
}
// TreeView pass-through properties…
// checketItems property…
// _updateHeader implementation…
// _createDropDown implementation…
}
}
Testing the Control
Now that the control is ready, we can test it and check that it behaves the way we want. This fiddle shows a simple example. It allows users to pick items from a tree and shows the selection on the console:
When you open the fiddle, click the drop-down button to open the TreeView. Once the tree is open, click a couple of items to select them and notice how the control header is updated:
Toggling a parent item (by clicking the checkbox with the mouse or by pressing the space bar) toggles the checked state of all its children:
We hope you find the DropDownTree control useful. More importantly, we hope you now feel comfortable extending the DropDown control to host other types of elements, while you create your own custom controls.
As always, we are interested in your feedback! Please leave questions and suggestions below. Happy coding!