dojo dragon main logo

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 VNodes, 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 VNodes, 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>
        );
    }
}