We will now examine the Selection facet API more closely, and explain how callbacks are installed that assist in implementing the facet operations.
import { DefineCbs, withCbs } from 'aspiration';
import { stub } from 'skandha';
export class Selection<T = any> {
static className = () => 'Selection';
callbackMap_ = {} as DefineCbs<{
selectItem: {
selectItem: () => void;
};
}>;
@input selectableIds: Array<string> = stub;
@data ids: Array<string> = [];
@data anchorId?: string;
@output items: Array<T> = stub;
@operation select(args: {
itemId: string | undefined;
isShift?: boolean;
isCtrl?: boolean;
}) {
return withCbs(this.callbackMap, 'selectItem', args, (cbs) => {
cbs.selectItem();
});
}
};This Selection facet is part of the skandha-facets library that you can install from npm. However, you can also
write your own facet classes.
As you can see, the Selection class has almost no logic. It describes how a selection is stored, and shows the signature
for selecting items, but doesn't specify how selection is implemented. This is by design, so that we can
reuse Selection in different scenarios that require different types of selection.
Of course, we do have to implement the select operation. This is achieved by implementing the callbacks that are
described in the callbackMap member of the Selection type.
In the body of the select operation, we use withCbs to obtain a so-called callbacks object. This object
(cbs) contains:
To be able to use the Selection.select operation, we must implement these callbacks.
We'll do this in the initClips function, that installs all callbacks for all
facets of clipsCtr.
import { Cbs } from 'aspiration';
export const initClips = (clipsCtr: any) => {
const ctr = clipsCtr;
ctr.addition.callbackMap = ...; {/* omitted for brevity */};
ctr.deletion.callbackMap = ...; {/* omitted for brevity */};
ctr.edit.callbackMap = ...; {/* omitted for brevity */};
ctr.highlight.callbackMap = ...; {/* omitted for brevity */};
ctr.insertion.callbackMap = ...; {/* omitted for brevity */};
ctr.move.callbackMap = ...; {/* omitted for brevity */};
ctr.selection.callbackMap = {
select: {
selectItem(this: Cbs<Selection['select']>) {
handleSelectItem(ctr.selection, this.args);
// Highlight follows selection
if (!this.args.isCtrl && !this.args.isShift) {
ctr.highlight.highlightItem({ id: this.args.itemId });
}
},
},
};
};The example code shows how the selectItem callback is implemented. Remember that selectItem is called
when we execute cbs.selectItem() in the select operation of the Selection facet. Inside of selectItem,
the this variable points to the callbacks object (cbs), which contains a copy of the input arguments of the
operation. That is why we can use this.args inside selectItem to access the selection arguments.
Our callback also takes care of highlighting the selected element. This demonstrates how we can use a callback to add interactions between behaviours. To get a better impression, I recommend to look at the sample code mentioned at the beginning of this article.
I'm a big fan of using MobX for making the UI respond to changes in the UI state. However, I didn't want a
tight coupling between Skandha and MobX. Therefore, there is a separate library called skandha-mobx that makes a Skandha
container observable with MobX. It offers a registerCtr function that applies observable to all
facet data fields, and computed to all facet operations.
import { registerCtr } from 'skandha-mobx';
registerCtr({
ctr: clipsCtr,
options: { name: 'Clips' },
initCtr: () => {
initClips(this, props);
mapClipsData(props);
}
});So far, we haven't discussed how user interactions such as mouse clicks and key-presses are handled. This is the
responsibility of the UI components. When the component receives an event, it should call the correct facet operation.
For example, when the user clicks on an item in a ListBox component, then the component must call the selection.select() operation.
I have written generic (reusable) code to make this happen, but I see an opportunity to use a headless UI such as
Adobe Aria:
useListBox() hook function from Aria creates the event handlers for the ListBox component;selection.select())
to update the shared UI state;selection.ids) changes then we update the local Aria state.In other words: we can synchronize the shared UI state with the local Aria state. This allows us to get the benefits that Adobe Aria provides.