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.