Enabling interactivity
Event listeners
Event listener functions can be assigned to virtual nodes in the same way as specifying any other property when instantiating the node. When outputting VNode
s, naming of event listeners in VNodeProperties
mirrors the equivalent events on HTMLElement
. Authors of custom widgets can name their events however they choose, but typically also follow a similar onEventName
naming convention.
Function properties such as event handlers are automatically bound to the this
context of the widget that instantiated the virtual node. However, if an already-bound function is given as a property value, this
will not be bound again.
Handling focus
When outputting VNode
s, widgets can use VNodeProperties
‘s focus
property to control whether the resulting DOM element should receive focus when rendering. This is a special property that accepts either a boolean
or a function that returns a boolean
.
When passing true
directly, the element will only receive focus when the previous value was something other than true
(similar to regular property change detection). When passing a function, the element will receive focus when true
is returned, regardless of what the previous return value was.
For example:
Given element ordering, the following ‘firstFocus’ input will receive focus on the initial render, whereas the ‘subsequentFocus’ input will receive focus for all future renders as it uses a function for its focus
property.
src/widgets/FocusExample.tsx
Function-based variant:
import { create, tsx, invalidator } from '@dojo/framework/core/vdom';
const factory = create({ invalidator });
export default factory(function FocusExample({ middleware: { invalidator } }) {
return (
<div>
<input key="subsequentFocus" type="text" focus={() => true} />
<input key="firstFocus" type="text" focus={true} />
<button onclick={() => invalidator()}>Re-render</button>
</div>
);
});
Class-based variant:
import WidgetBase from '@dojo/framework/core/WidgetBase';
import { tsx } from '@dojo/framework/core/vdom';
export default class FocusExample extends WidgetBase {
protected render() {
return (
<div>
<input key="subsequentFocus" type="text" focus={() => true} />
<input key="firstFocus" type="text" focus={true} />
<button onclick={() => this.invalidate()}>Re-render</button>
</div>
);
}
}
Delegating focus
Function-based widgets can use the focus
middleware to provide focus to their children or to accept focus from a parent widget. Class-based widgets can use the FocusMixin
(from @dojo/framework/core/mixins/Focus
) to delegate focus in a similar way.
FocusMixin
adds a this.shouldFocus()
method to a widget’s class, whereas function-based widgets use the focus.shouldFocus()
middleware method for the same purpose. This method checks if the widget is in a state to perform a focus action and will only return true
for a single invocation, until the widget’s this.focus()
method has been called again (function-based widgets use the focus.focus()
middleware equivalent).
FocusMixin
or the focus
middleware also add a focus
function property to a widget’s API. The framework uses the boolean result from this property to determine if the widget (or one of its children) should receive focus when rendering. Typically, widgets pass the shouldFocus
method to a specific child widget or an output node via their focus
property, allowing parent widgets to delegate focus to their children.
See the focus
middleware delegation example in the Dojo middleware reference guide for an example for function-based widgets.
The following shows an example of delegating and controlling focus across a class-based widget hierarchy and output VNodes:
src/widgets/FocusableWidget.tsx
import WidgetBase from '@dojo/framework/core/WidgetBase';
import { tsx } from '@dojo/framework/core/vdom';
import Focus from '@dojo/framework/core/mixins/Focus';
interface FocusInputChildProperties {
onFocus: () => void;
}
class FocusInputChild extends Focus(WidgetBase)<FocusInputChildProperties> {
protected render() {
/*
The child widget's `this.shouldFocus()` method is assigned directly to the
input node's `focus` property, allowing focus to be delegated from a higher
level containing parent widget.
The input's `onfocus()` event handler is also assigned to a method passed
in from a parent widget, allowing user-driven focus changes to propagate back
into the application.
*/
return <input onfocus={this.properties.onFocus} focus={this.shouldFocus} />;
}
}
export default class FocusableWidget extends Focus(WidgetBase) {
private currentlyFocusedKey = 0;
private childCount = 5;
private onFocus(key: number) {
this.currentlyFocusedKey = key;
this.invalidate();
}
/*
Calling `this.focus()` resets the widget so that `this.shouldFocus()` will return true when it is next invoked.
*/
private focusPreviousChild() {
--this.currentlyFocusedKey;
if (this.currentlyFocusedKey < 0) {
this.currentlyFocusedKey = this.childCount - 1;
}
this.focus();
}
private focusNextChild() {
++this.currentlyFocusedKey;
if (this.currentlyFocusedKey === this.childCount) {
this.currentlyFocusedKey = 0;
}
this.focus();
}
protected render() {
/*
The parent widget's `this.shouldFocus()` method is passed to the relevant child element
that requires focus, based on the simple previous/next widget selection logic.
This allows focus to be delegated to a specific child node based on higher-level logic in
a container/parent widget.
*/
return (
<div>
<button onclick={this.focusPreviousChild}>Previous</button>
<button onclick={this.focusNextChild}>Next</button>
<FocusInputChild
key={0}
focus={this.currentlyFocusedKey === 0 ? this.shouldFocus : undefined}
onFocus={() => this.onFocus(0)}
/>
<FocusInputChild
key={1}
focus={this.currentlyFocusedKey === 1 ? this.shouldFocus : undefined}
onFocus={() => this.onFocus(1)}
/>
<FocusInputChild
key={2}
focus={this.currentlyFocusedKey === 2 ? this.shouldFocus : undefined}
onFocus={() => this.onFocus(2)}
/>
<FocusInputChild
key={3}
focus={this.currentlyFocusedKey === 3 ? this.shouldFocus : undefined}
onFocus={() => this.onFocus(3)}
/>
<FocusInputChild
key={4}
focus={this.currentlyFocusedKey === 4 ? this.shouldFocus : undefined}
onFocus={() => this.onFocus(4)}
/>
</div>
);
}
}