# Devextreme and Aurelia 2.0: First Results of Integration (33 Widgets So Far)

**URL:** <https://discourse.aurelia.io/t/devextreme-and-aurelia-2-0-first-results-of-integration-33-widgets-so-far/6552>\
**Category:** Uncategorized\
**Created:** [December 9, 2025, 4:32am UTC](https://discourse.aurelia.io/t/devextreme-and-aurelia-2-0-first-results-of-integration-33-widgets-so-far/6552 "2025-12-09T04:32:10Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![estaylorco](https://yyz1.discourse-cdn.com/flex027/user_avatar/discourse.aurelia.io/estaylorco/32/1887_2.png) [@estaylorco](https://discourse.aurelia.io/u/estaylorco)\
**Post date:** [December 9, 2025, 4:32am UTC](https://discourse.aurelia.io/t/devextreme-and-aurelia-2-0-first-results-of-integration-33-widgets-so-far/6552/1 "2025-12-09T04:32:11Z")

</div>

Hello Everyone,

I thought I would share a gif of my first results of integration between Devextreme (Dx) and Aurelia 2.0. I have integrated only 33 of the components so far, and a number of issues need to be ironed out. This post is a little rough as it is another brain dump on this (as I try to migrate from an Aurelia 1.0 app to its Aurelia 2.0 counterpart).

Dx widgets that have at least one **template** option (`rowTemplate`, `contentTemplate`, `titleTemplate`, etc.) fully support Aurelia templates. Through each widget’s `integrationOptions` option (not in the mainstream documentation, but covered in the documentation for 3rd-party integration), Aurelia templates are ingested into the widget. They have all of the goodness of Aurelia templates–interpolation, event-binding, props-binding, etc.

Each widget is implemented as viewless, except **tsi-dx-resizable** (`TsiDxResizable`, corresponding to **Resizable** ), which also has a view.

The gif below is a mess, more like a playground. But I’m more interested in the substance at the moment.

![Aurelia 2.0 and Devextreme](https://canada1.discourse-cdn.com/flex027/uploads/aurelia/original/2X/e/ede8efd177e0b8193459adbaf8488dca7b49986b.gif)

#### **INTEGRATION PARTS**

The parts of integration are the following:

- `harvestDxTemplates.js`: a function called by the `processContent` hook for harvesting DX-TEMPLATEs from the view (the markup), removing them from the DOM, and passing them on to the integration service for enhancement and ingest.
- `TsiCustomEventDispatcher.js`: a very thin wrapper that makes it a little more convenient to dispatch a custom event on an element.
- `TsiDxWidgetBase.js`: a viewless custom element (tsi-dx-widget-base) that serves a base class for all Dx widgets.
- `DxAuTemplateBridgeService.js`: a transient that provides the glue between Dx widgets and Aurelia 2.0 templates.
- The widgets themselves as custom elements.

The code, in order given above:

**harvestDxTemplates.js**

```javascript
/**
 * Harvests markup from the consuming component enclosed within the <tsi-dx-template> tag. The term
 * _harvested_ is used to indicate that the markup will be discarded once it's ingested. This function
 * is executed in the context of an Aurelia `processContent` callback.
 * @param {INode} node The node associated with the content being processed.
 * @param {} _ (Not used).
 * @param {object} instructionData An object (container) on which we can store any data, and which will
 * be available on the IInstruction we resolve in the widgets' base class.
 * @returns {boolean} Returns false to indicate that content should not be processed.
 */
export default function harvestDxTemplates(node, _, instructionData) {
  // Ingest the templates
  // TODO: The tag name should be configurable
  instructionData.templates = Array.from(node.children).filter((_n) => _n.tagName === 'TSI-DX-TEMPLATE');

  // Discard the templates
  // Now that we've harvested the DX-TEMPLATEs from the DOM, we don't need them anymore;
  // We must do this here in the processContent callback
  instructionData.templates.forEach((_template) => {
    _template.remove();
  });

  // Signals the absence of content when we're done
  // Equivalent to processContent(false) in Aurelia 1.0
  return false;

```

**TsiCustomEventDispatcher.js**

```javascript
import { singleton } from 'aurelia';

// Documentation elided
@singleton()
export class TsiCustomEventDispatcher {
  dispatchNonBubbling(element, events = [], detail) {
    this._dispatchEventCore(element, events, detail);
    return this;
  }

  dispatch(element, events = [], detail) {
    this._dispatchEventCore(element, events, detail, true);
    return this;
  }
 
  _dispatchEventCore(element, events, detail, bubbles = false) {
    const evts = typeof events === 'string' ? [events] : events;

    if (element) {
      evts.forEach((_evt) => {
        element.dispatchEvent(
          new CustomEvent(this._toKebabCase(_evt), {
            bubbles: bubbles,
            detail: detail,
          })
        );
      });
    }
  }
  
  _toKebabCase(str = '') {
    return str
      ? str
          .replaceAll('_', '-')
          .replaceAll('--', '-')
          .replace(/([a-z])([A-Z])/g, '$1-$2')
          .toLowerCase()
      : str;
  }
}

```

**TsiDxWidgetBase.js**

```javascript
import { customElement, IController, ILogger, INode, resolve } from 'aurelia';
import { bindable, BindingMode, IHydrationContext, watch } from '@aurelia/runtime-html';

import { DxAuTemplateBridgeService } from 'common/components/dx/DxAuTemplateBridgeService.js';
import { TsiCustomEventDispatcher } from 'common/library/TsiCustomEventDispatcher.js';

import { isPlainObject as __isPlainObject } from 'lodash-es';

/**
 * Dx widget base class. All Dx widgets should inherit from this base class.
 * @dependency {INode}
 * @dependency {IContainer}
 * @dependency {IController}
 * @dependency {DxAuTemplateBridgeService}
 * @dependency {ILogger}
 * @dependency {IDxWidgetConfiguration}
 */
@customElement({
  name: 'tsi-dx-widget-base',
  template: null,
})
export class TsiDxWidgetBase {
  /**
   * @type {object} If events need to be renamed on an _ad hoc_ basis, each entry of this map should
   * contain the built-in name as key, and the new name as value, for each event that needs to be renamed.
   * Descendants can override `configureEvents` and configure the map there for a more permanent configuration.
   */
  @bindable({ mode: BindingMode.twoWay }) eventAliases = {};
  /**
   * @type {boolean} Convenience bindable interpreted into `activeStateEnabled`, `focusStateEnabled`, and `hoverStateEnabled`.
   * @default undefined
   */
  @bindable({ mode: BindingMode.twoWay }) static = undefined;

  // #endregion

  constructor() {
    /**
     * @dependency
     * @type {INode}
     */
    this.node = resolve(INode);
    /**
     * @dependency
     * @type {IContainer}
     */
    this.diContainer = resolve(IHydrationContext);
    /**
     * @dependency
     * @type {IController}
     */
    this.$controller = resolve(IController);
    /**
     * @dependency
     * @type {DxAuTemplateBridgeService}
     */
    this.templateService = resolve(DxAuTemplateBridgeService);
    /**
     * @dependency
     * @type {TsiCustomEventDispatcher}
     */
    this.customEventDispatcher = resolve(TsiCustomEventDispatcher);
    /**
     * @dependency
     * @type {ILogger}
     */
    this.logger = resolve(ILogger).scopeTo(this.constructor.name);
    /**
     * @dependency
     * @type {object}
     */
    this.globalDxConfig = resolve('IDxWidgetConfiguration');
    /**
     * @type {HTMLElement}
     */
    this.widgetHost;
    /**
     * @type {object}
     */
    this.widgetInstance;
    /**
     * @type {IntegrationOptions}
     */
    this.integrationOptions;

    // Create the child container in this context and bring in the view resources wherever
    // the component is being consumed (what we might call the consuming, or owning, component)
    this.diContainer = this.diContainer.controller.container.createChild({ inheritParentResources: true });
    console.log('Creating child container for ' + this.constructor.name);
  }

  /**
   * @type {Array} Custom widget events do not bubble by design and must be relayed. Expressed in camelCase.
   * These custom events are common (normal) to all Dx widgets and are called out separately to avoid duplication.
   * Native events do _not_ have to be relayed. The `valueChanged` event is _not_ listed here by design as it
   * is handled in a special way.
   * @readonly
   * @virtual
   */
  get relayedEvents() {
    return ['contentReady', 'disposing', 'initialized', 'optionChanged'];
  }

  /**
   * @type {object} Bindings that should be mapped to a different, or nested, option on the widget. Can be a function with
   * the side effects of translating the binding into properties on the widget. If the binding is mapped to `null`,
   * it will be ignored by the property change handler.
   * @virtual
   */
  get bindingAliases() {
    return {
      eventAliases: null,
      static: (newValue) => {
        this._setOptions([
          { name: 'activeStateEnabled', newValue, annotation: 'static -> activeStateEnabled' },
          { name: 'focusStateEnabled', newValue, annotation: 'static -> focusStateEnabled' },
          { name: 'hoverStateEnabled', newValue, annotation: 'static -> hoverStateEnabled' },
        ]);
      },
    };
  }

  /**
   * @type {object} Get all of the options on the widget as a plain object, filtering out options that begin with a "_".
   * Descendants should override for a different implementation.
   * @virtual
   */
  @watch('widgetInstance')
  get builtInWidgetOptions() {
    if (!this.widgetInstance) {
      return {};
    }

    return Object.keys(this.widgetInstance.option())
      .filter((_key) => _key[0] !== '_')
      .reduce((_acc, _key) => {
        _acc[_key] = true;
        return _acc;
      }, {});
  }

  /**
   * @type {object} Custom and native events that should not be logged. Descendants should override (and possibly defer)
   * to change configuration.
   * @virtual
   */
  get doNotLog() {
    return { optionChanged: true };
  }

  /**
   * Must override in descendants to mount a Dx widget on `widgetHost`.
   * @abstract
   * @virtual
   */
  mountWidget() {
    throw new Error('mountWidget is not yet implemented');
  }

  /**
   * Should override if events need to be renamed.
   * @abstract
   * @virtual
   */
  configureEvents() {}

  /**
   * @aurelia
   * @lifecycle attached
   * @returns {Promise|void}
   * @virtual
   */
  async attached() {
    this.widgetHost = document.createElement('div');
    this.widgetHost.classList.add('tsi-widget-host');

    this.node.appendChild(this.widgetHost);

    const compilerOptions = {
      container: this.diContainer,
      controller: this.$controller,
    };

    // Compile templates, if there are any
    this.integrationOptions = this.templateService.compileTemplates(compilerOptions);

    this.mountWidget();

    this.configureEvents();
    this.registerEvents();
    this.registerInterceptedEvents();

    this.applyGlobalConfiguration();
  }

  /**
   * @aurelia
   * @lifecycle detaching
   * @returns {Promise|void}
   * @virtual
   */
  async detaching() {
    this.unregisterEvents();
    this.unregisterInterceptedEvents();

    this.widgetInstance.dispose();
    this.widgetHost.remove();
    this.widgetInstance = undefined;
    this.widgetHost = undefined;
  }

  /**
   * @aurelia
   * @lifecycle dispose
   * @virtual
   */
  dispose() {
    this.diContainer?.dispose();
  }

  /**
   * Descendants might override (and possibly defer) to take over event registration and handling.
   * @virtual
   */
  registerEvents() {
    const wi = this.widgetInstance;
    const ced = this.customEventDispatcher;
    const log = this.logger;
    const ea = this.eventAliases;

    this.relayedEvents.forEach((_evt) => {
      wi.on(_evt, (e) => {
        ced.dispatch(this.node, ea[_evt] ?? _evt, e);

        if (this.doNotLog[_evt]) {
          return;
        }

        if (ea[_evt]) {
          log?.info?.(
            `Custom event %c${_evt}\x1b[0m (mapped to %c${ea[_evt]}\x1b[0m) fired`,
            'color: #7dbbe9',
            'color: #fffb00'
          );
        } else {
          log?.info?.(`Custom event %c${_evt}\x1b[0m fired`, 'color: #7dbbe9');
        }
      });
    });
  }

  /**
   * Register events that should be intercepted, handled, and then possibly re-dispatched.
   * To intercept and handle additional events, descendants should override this method, possibly defer
   * to it to pick up `valueChanged`, and then either subsume or re-dispatch the intercepted event.
   * @virtual
   */
  registerInterceptedEvents() {
    // This event is handled separately and differently as we need to intercept the value-changed event so
    // we can tell Aurelia what happened
    this.widgetInstance.on('valueChanged', (e) => this._valueChangedHandler(e));
  }

  /**
   * Might override in descendants. Calling `off` on the widget is not strictly necessary since the widget
   * unregisters all of its events upon dispose, according to DevExpress support, but a good idea nevertheless.
   * @virtual
   */
  unregisterEvents() {
    this.relayedEvents.forEach((_evt) => {
      this.widgetInstance.off(_evt);
    });
  }

  /**
   * Unregister all intercepted events. Decendants should override if `registerInterceptedEvents` was overridden.
   * Additionally registered intercepted events should be unregistered here.
   * @virtual
   */
  unregisterInterceptedEvents() {
    this.widgetInstance.off('valueChanged');
  }

  /**
   * Applies the global dxWidgetConfiguration settings after the binding defaults have been applied, working
   * directly with the widget's Option API.
   */
  applyGlobalConfiguration() {
    const hasGlobalScope = !!this.globalDxConfig.global;
    const wi = this.widgetInstance;
    let options = {};

    Object.keys(this.globalDxConfig).forEach((_scope) => {
      if (_scope === this.constructor.name) {
        options = this.globalDxConfig[_scope];
      }

      if (hasGlobalScope) {
        options = { ...this.globalDxConfig.global, ...options };
      }
    });

    Object.keys(options).forEach((_option) => {
      if (this.builtInWidgetOptions[_option]) {
        const resolvedOption = {};
        let resolvedValue;

        if (__isPlainObject(options[_option])) {
          resolvedValue = { ...options[_option] };
        } else {
          resolvedValue = options[_option];
        }

        resolvedOption[_option] = resolvedValue;
        wi.option(resolvedOption);

        this.logger?.info?.('Defaults applied to ' + this.constructor.name + ' with:', resolvedOption);
      }
    });
  }

  /**
   * Bindables change handler. Changes have to be pushed into the Dx widget through the widget's Option API.
   * When an Aurelia binding changes, the widget doesn't know.
   * @example AU BINDING -> DX WIDGET
   * @param {object} bindableProp Information about the bindable whose property changed.
   */
  propertiesChanged(bindableProp) {
    if (this.widgetInstance) {
      const option = Object.keys(bindableProp)[0];
      this._handlePropertyChanged(option, bindableProp[option].newValue);
    }
  }

  /**
   * Handles the change of a single property. Should override in descendants to customize the handling of
   * a property change.
   * @param {string} option Option on the widget to change.
   * @param {object} newValue The new value to which to set the option.
   * @virtual
   */
  _handlePropertyChanged(option, newValue) {
    // Will be a mapped option, the original option, or null (meaning the the change should be ignored)
    const resolvedOption = this.bindingAliases[option] ?? option;

    // Devextreme's Option API supports nested properties (which allows us to map, say, itemDragging.allowReordering
    // to allowReordering, if we wish)
    if (resolvedOption !== null) {
      if (typeof resolvedOption === 'function') {
        this.logger?.info?.(`Property resolved by function call: %c${option}`, 'color: #7dbbe9;');
        this.bindingAliases[option].call(this, newValue);
      } else {
        this._setOption({ name: resolvedOption, newValue });
      }
    }
  }

  /**
   * Set multiple options on the widget at the same time. Leverages batch updating on the Option API
   * of the widget.
   * @param {Array} options An array of options to set where each option is an object comprising two
   * properties: `name` and `newValue`. The former is the `name` of an option on widget, and `newValue`
   * is the value to which to set the option. An option that does not exist on the widget will be ignored.
   * @private
   */
  _setOptions(options = []) {
    const isMultiple = options.length > 1;
    const wi = this.widgetInstance;

    if (isMultiple) {
      wi.beginUpdate?.();
    }

    options.forEach((_option) => {
      this._setOption({ ..._option });
    });

    if (isMultiple) {
      wi.endUpdate?.();
    }
  }

  /**
   * Set one option on the widget.
   * @param {string} name The name of the option to set.
   * @param {object} newValue The new value of the option.
   * @private
   */
  _setOption(option) {
    this.widgetInstance.option(option.name, option.newValue);
    this.logger?.info?.(
      `Property changed: %c${option.name}\x1b[0m%c${option.annotation ? ' | ' + option.annotation : ' '}`,
      'color: #7dbbe9;',
      'color: #fffb00;'
    );
  }

  /**
   * Value changed handler. Pushes a changed value on the Dx widget into the Aurelia binding so that
   * Aurelia knows about it. Descendants can override for custom behavior.
   * @param {object} e
   * @virtual
   */
  _valueChangedHandler(e) {
    // Aurelia won't know if the widget's value has changed (but the widget will know about itself, of course);
    // When the widget's value changes, we have to tell Aurelia by updating the binding in the component
    // DX WIDGET -> AU BINDING
    this.value = e.value;

    // Now that we've taken care of business, we need to simply relay the widget's value-changed event (let it carrry on)
    this.customEventDispatcher.dispatch(this.node, this.eventAliases.valueChanged ?? 'value-changed', e);
  }
}

```

**DxAuTemplateBridgeService.js**

```javascript
import Aurelia, { resolve, transient } from 'aurelia';
import { onResolve } from '@aurelia/kernel';
import { IInstruction } from '@aurelia/template-compiler';

import { on as dxEventOn } from 'devextreme/events';
import { isEmpty as __isEmpty } from 'lodash-es';

// TODO: This is stateful for now, which is why it needs to be transient; need to look
// at getting rid of the templates member of this class

/**
 * A service to bridge the gap between Aurelia templates and Dx templates. This service will turn standard markup
 * in the consuming component (provided as if it were slotted content), into an Aurelia-enhanced template.
 * The template below will be removed from the DOM by this service (and reinserted by the Dx widget under its
 * own control, but with Aurelia enhancement).
 * @example
 * `<tsi-dx-button>
 * <tsi-dx-template>
 * <div>${data.text}</div>
 * </tsi-dx-template
 * </tsi-dx-button>`
 * @dependency {IInstruction}
 */
@transient()
export class DxAuTemplateBridgeService {
  constructor() {
    // Pull data from the hydrating instruction and grab the templates store
    // there in the processContent callback (harvestDxTemplates)
    const { data } = resolve(IInstruction);

    /**
     * @type {Array} The templates harvested from the DOM, which are stored on the hydrating instruction's data property.
     */
    this.templates = data.templates;
  }

  /**
   * Compiles templates by taking each template harvested from the DOM and ingesting them into a template dictionary
   * whose spec is defined by DevExpress. Each entry in the dictionary is a key (the name of the template) and a render function
   * which ultimately results in an Aurelia-enhanced piece of markup that the Devextreme widget will reinsert into
   * the DOM under its own control.
   * @param {object} compilerOptions
   * @param {IContainer} compilerOptions.container DI container that will be associated with enhance. It contains all
   * of the [view] resources that will be needed by the compiled template.
   * @param {IController} compilerOptions.controller Consuming component's controller
   * @param {string} [compilerOptions.templateName] Template name that will override the name attribute on the template
   * in the markup.
   * @returns {object} A template dictionary with 0..n entries.
   */
  compileTemplates(compilerOptions) {
    const templateDict = { templates: {} };

    if (this.templates?.length) {
      this.templates.forEach((_template) => {
        const { container: diContainer, controller, templateName } = compilerOptions;
        this.ingestTemplate(_template, templateDict, { container: diContainer, controller, templateName });
      });
    }

    return !__isEmpty(templateDict.templates) ? templateDict : {};
  }

  /**
   * The starting point of template ingest. Ultimately, an entry will be made in the template dictionary with the
   * template's name as a key and the template's render function as the value. It is in the render function that
   * the raw template will be enhanced by Aurelia.
   * @param {HTMLElement} rawTemplate The harvested template that should be ingested into the Dx widget via the widget's
   * `integrationOptions` property.
   * @param {object} templateDict The template dictionary instance in which all of the templates for this widget are
   * being accumulated. Each entry's key is the name of a template in the markup, and each value is a render function.
   * @param {object} config Configuration of the context in which the rawTemplate should be enhanced.
   * @param {IContainer} config.container DI container that will be associated with enhance. It contains all
   * of the [view] resources that will be needed by the compiled template.
   * @param {IController} config.controller Consuming component's controller
   * @param {string} [config.templateName] Template Optional template name that will override the name attribute on the
   * template in the markup.
   */
  ingestTemplate(rawTemplate, templateDict, config) {
    const templateRenderer = (renderData) => {
      return this.createRenderer(rawTemplate, renderData, config);
    };

    // Devextreme expects, simply, that for each named template, we have a render function assigned to the *render* property;
    // This is the dictionary we pass to the Devextreme component at the integration point
    const _templateName = config.templateName ?? rawTemplate.getAttribute('name');

    templateDict.templates[_templateName] = {
      render: templateRenderer,
    };
  }

  /**
   * Creates a render function suitable to the Dx widget, part of which is the Aurelia enhancement of the
   * raw template being rendered.
   * @param {HTMLElement} rawTemplate The harvested template that should be ingested into the Dx widget via the widget's
   * `integrationOptions` property.
   * @param {object} renderData
   * @param {HTMLElement} renderData.container The DOM container kicked out by the Dx widget into which a template
   * should be placed.
   * @param {object} renderData.model The component containing the data that will be pushed out by the Dx widget
   * so the enhanced template can present the data.
   * @param {object} config Configuration of the context in which the rawTemplate should be enhanced.
   * @param {IContainer} config.container DI container that will be associated with enhance. It contains all
   * of the [view] resources that will be needed by the compiled template.
   * @param {IController} config.controller Consuming component's controller
   * @param {string} [config.templateName] Template Optional template name that will override the name attribute on the
   * @returns {HTMLElement} The enhanced template (not the enhanced view).
   */
  createRenderer(rawTemplate, renderData, config) {
    // Necessary to avoid a hydration instruction mismatch
    const $template = rawTemplate.cloneNode(true);
    // Place the template into the container the Dx widget provides
    renderData.container.append($template);

    // This is the meat of creating a renderer
    // Will automatically pick up the view resources of the consuming component as we will configure the container with those resources
    // DevExpress chose 'data' as the property on which all of the underlying component's relevant data is stored,
    // but it could be called anything; the component on the enhance config is the component representing the template,
    // not the consuming component
    const enhancedView = Aurelia.enhance({
      container: config.container,
      host: $template,
      component: {
        data: renderData.model,
      },
    });

    // Register a Dx remove event, dxremove, so we can apply Aurelia conventions to deactivating and disposing of an enhanced view
    // The widget will clean this up itself upon its disposal
    dxEventOn($template, 'dxremove', () => {
      if (!enhancedView) {
        return;
      }

      // This will trigger the tear-down lifecycle hooks of the enhanced view.
      onResolve(enhancedView.deactivate(enhancedView, config.controller), () => {
        // enhancedView.dispose();
        this.templates = undefined;
      });
    });

    return $template;
  }
}

```

A sample widget, **tsi-dx-date-box.js**

```javascript
import { bindable, BindingMode, customElement } from 'aurelia';
import { TsiDxWidgetBase } from './tsi-dx-widget-base';
import harvestDxTemplates from './harvestDxTemplates';

import DateBox from 'devextreme/ui/date_box';

/**
 * DateBox allows users to enter or modify date and time values.
 * {@link https://js.devexpress.com/jQuery/Documentation/Guide/UI_Components/DateBox/Overview/}
 */
@customElement({
  name: 'tsi-dx-date-box',
  template: null,
  processContent(node, _, instructionData) {
    return harvestDxTemplates(node, _, instructionData);
  },
})
export class TsiDxDateBox extends TsiDxWidgetBase {
  // #region Properties (Bindables) on the Dx Widget

  @bindable({ mode: BindingMode.twoWay }) acceptCustomValue = true;
  @bindable({ mode: BindingMode.twoWay }) accessKey = undefined;
  @bindable({ mode: BindingMode.twoWay }) activeStateEnabled = true;
  @bindable({ mode: BindingMode.twoWay }) adaptivityEnabled = false;
  @bindable({ mode: BindingMode.twoWay }) applyButtonText = 'OK';
  @bindable({ mode: BindingMode.twoWay }) applyValueMode = 'instantly';
  @bindable({ mode: BindingMode.twoWay }) buttons = undefined;
  @bindable({ mode: BindingMode.twoWay }) calendarOptions = {};
  @bindable({ mode: BindingMode.twoWay }) cancelButtonText = 'Cancel';
  @bindable({ mode: BindingMode.twoWay }) dateOutOfRangeMessage = 'Value is out of range';
  @bindable({ mode: BindingMode.twoWay }) dateSerializationFormat = undefined;
  @bindable({ mode: BindingMode.twoWay }) deferRendering = true;
  @bindable({ mode: BindingMode.twoWay }) disabled = false;
  @bindable({ mode: BindingMode.twoWay }) disabledDates = null;
  @bindable({ mode: BindingMode.twoWay }) displayFormat = null;
  @bindable({ mode: BindingMode.twoWay }) dropDownButtonTemplate = 'dropDownButton';
  @bindable({ mode: BindingMode.twoWay }) dropDownOptions = {};
  @bindable({ mode: BindingMode.twoWay }) elementAttr = {};
  @bindable({ mode: BindingMode.twoWay }) focusStateEnabled = true;
  @bindable({ mode: BindingMode.twoWay }) height = undefined;
  @bindable({ mode: BindingMode.twoWay }) hint = undefined;
  @bindable({ mode: BindingMode.twoWay }) hoverStateEnabled = true;
  @bindable({ mode: BindingMode.twoWay }) inputAttr = {};
  @bindable({ mode: BindingMode.twoWay }) interval = 30;
  @bindable({ mode: BindingMode.twoWay }) invalidDateMessage = 'Value must be a date or time';
  @bindable({ mode: BindingMode.twoWay }) isDirty = false;
  @bindable({ mode: BindingMode.twoWay }) isValid = true;
  @bindable({ mode: BindingMode.twoWay }) label = '';
  @bindable({ mode: BindingMode.twoWay }) labelMode = 'static';
  @bindable({ mode: BindingMode.twoWay }) max = undefined;
  @bindable({ mode: BindingMode.twoWay }) maxLength = null;
  @bindable({ mode: BindingMode.twoWay }) min = undefined;
  @bindable({ mode: BindingMode.twoWay }) name = '';
  @bindable({ mode: BindingMode.twoWay }) opened = false;
  @bindable({ mode: BindingMode.twoWay }) openOnFieldClick = true; // Deviates from the Dx default
  @bindable({ mode: BindingMode.twoWay }) pickerType = 'calendar';
  @bindable({ mode: BindingMode.twoWay }) placeholder = '';
  @bindable({ mode: BindingMode.twoWay }) readOnly = false;
  @bindable({ mode: BindingMode.twoWay }) rtlEnabled = false;
  @bindable({ mode: BindingMode.twoWay }) showAnalogClock = true;
  @bindable({ mode: BindingMode.twoWay }) showClearButton = true; // Deviates from the Dx default
  @bindable({ mode: BindingMode.twoWay }) showDropDownButton = true;
  @bindable({ mode: BindingMode.twoWay }) spellcheck = false;
  @bindable({ mode: BindingMode.twoWay }) stylingMode = 'contained';
  @bindable({ mode: BindingMode.twoWay }) tabIndex = 0;
  @bindable({ mode: BindingMode.twoWay }) todayButtonText = 'Today';
  @bindable({ mode: BindingMode.twoWay }) type = 'date';
  @bindable({ mode: BindingMode.twoWay }) useMaskBehavior = false;
  @bindable({ mode: BindingMode.twoWay }) validationError = null;
  @bindable({ mode: BindingMode.twoWay }) validationErrors = null;
  @bindable({ mode: BindingMode.twoWay }) validationMessageMode = 'auto';
  @bindable({ mode: BindingMode.twoWay }) validationMessagePosition = 'auto';
  @bindable({ mode: BindingMode.twoWay }) validationStatus = 'valid';
  @bindable({ mode: BindingMode.twoWay }) value = null;
  @bindable({ mode: BindingMode.twoWay }) valueChangeEvent = 'input'; // Deviates from the Dx default
  @bindable({ mode: BindingMode.twoWay }) width = undefined;

  // #endregion

  constructor() {
    super();
  }

  /**
   * @override
   * @inheritdoc
   */
  get relayedEvents() {
    return super.relayedEvents.concat(['closed', 'enterKey', 'focusIn', 'focusOut', 'keyDown', 'keyUp', 'opened']);
  }

  /**
   * @override
   * @inheritdoc
   */
  mountWidget() {
    this.widgetInstance = new DateBox(this.widgetHost, {
      acceptCustomValue: this.acceptCustomValue,
      accessKey: this.accessKey,
      activeStateEnabled: this.static ?? this.activeStateEnabled,
      adaptivityEnabled: this.adaptivityEnabled,
      applyButtonText: this.applyButtonText,
      applyValueMode: this.applyValueMode,
      buttons: this.buttons,
      calendarOptions: this.calendarOptions,
      cancelButtonText: this.cancelButtonText,
      dateOutOfRangeMessage: this.dateOutOfRangeMessage,
      dateSerializationFormat: this.dateSerializationFormat,
      deferRendering: this.deferRendering,
      disabled: this.disabled,
      disabledDates: this.disabledDates,
      displayFormat: this.displayFormat,
      dropDownButtonTemplate: this.dropDownButtonTemplate,
      dropDownOptions: this.dropDownOptions,
      elementAttr: this.elementAttr,
      focusStateEnabled: this.static ?? this.focusStateEnabled,
      height: this.height,
      hint: this.hint,
      hoverStateEnabled: this.static ?? this.hoverStateEnabled,
      inputAttr: this.inputAttr,
      interval: this.interval,
      invalidDateMessage: this.invalidDateMessage,
      isDirty: this.isDirty,
      isValid: this.isValid,
      label: this.label,
      labelMode: this.labelMode,
      max: this.max,
      maxLength: this.maxLength,
      min: this.min,
      name: this.name,
      opened: this.opened,
      openOnFieldClick: this.openOnFieldClick,
      pickerType: this.pickerType,
      placeholder: this.placeholder,
      readOnly: this.readOnly,
      rtlEnabled: this.rtlEnabled,
      showAnalogClock: this.showAnalogClock,
      showClearButton: this.showClearButton,
      showDropDownButton: this.showDropDownButton,
      spellcheck: this.spellcheck,
      stylingMode: this.stylingMode,
      tabIndex: this.tabIndex,
      todayButtonText: this.todayButtonText,
      type: this.type,
      useMaskBehavior: this.useMaskBehavior,
      validationError: this.validationError,
      validationErrors: this.nvalidationErrorsull,
      validationMessageMode: this.validationMessageMode,
      validationMessagePosition: this.validationMessagePosition,
      validationStatus: this.validationStatus,
      value: this.value,
      valueChangeEvent: this.valueChangeEvent,
      width: this.width,
      integrationOptions: this.integrationOptions,
    });
  }
}

```

---

<div class="post-metadata">

**Author:** ![estaylorco](https://yyz1.discourse-cdn.com/flex027/user_avatar/discourse.aurelia.io/estaylorco/32/1887_2.png) [@estaylorco](https://discourse.aurelia.io/u/estaylorco)\
**Post date:** [December 9, 2025, 4:37am UTC](https://discourse.aurelia.io/t/devextreme-and-aurelia-2-0-first-results-of-integration-33-widgets-so-far/6552/2 "2025-12-09T04:37:10Z")

</div>

Hello Everyone,

With respect to my previous post, there are some dependencies to be aware of:

- Either a Devextreme license or a valid trial edition
- lodash-es
- Global configuration (given below in the form of an example) in **main.js**

From **main.js** :

```auto
...

const globalDxConfig = {
  global: {
    animation: null,
    dropDownOptions: { animation: null },
  },
  TsiDxCheckBox: {
    activeStateEnabled: false,
    focusStateEnabled: false,
  },
};

...

Aurelia.register(
  ...
  Registration.instance('IDxWidgetConfiguration', globalDxConfig)
)

```
