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.

INTEGRATION PARTS
The parts of integration are the following:
harvestDxTemplates.js: a function called by theprocessContenthook 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
/**
* 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
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
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
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
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,
});
}
}