Last active
June 23, 2017 15:26
-
-
Save jtheisen/14e76e8973fae5095b44b37a6d85ebd6 to your computer and use it in GitHub Desktop.
DevExtreme React wrappers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
/* | |
This is a crude integration of DevExtreme widgets with React. There are many things that are missing, | |
but it is usable if you're willing to tinker with it once you encounter a problem. | |
In particular, watch out for the following issues: | |
* All options that are arrays or objects must be passed as reference, as | |
in dataSource={myDataSource} and not inline, as in dataSource={[1, 2, 3]}. The latter | |
would make the option be updated every time something unrelated changes because in general | |
the inline object or array can only be compared by reference. | |
* An exception are the many DevExtreme widget's nested option objects such as | |
DataGrid's searchPanel option object. Those can be given inline rather than by reference | |
(and in case you use mobx, you should as you want to evaluate all dependencies in the render method) ... | |
* ... **if** they are already mentioned in the list used by the getFlattenedOptions method. I don't want | |
to maintain one as they are many. Add the ones you want to use. | |
* Function props, usually event properties, are never updated as they cannot be distinguished properly. | |
Instead of changing them, implement a wrapper function that calls down to the correct version. | |
* As you can see below, only a few widgets are defined. Add definitions as you need them. | |
*/ | |
interface DevExtremeWidgetConstructorFunction<TWidget extends DevExpress.ui.Widget> { | |
new (el: JQuery, options: DevExpress.ui.WidgetOptions): TWidget; | |
} | |
/** | |
* React component wrapper over a DevExpress DevExtreme component. | |
Dev Extreme React Component Lifecycle: | |
1. HTML div is mounted on DOM | |
2. DevExtreme component in instantiated over JQuery element on mounted div | |
3. When any property changes in the component, the property is checked for changes and the DevExtreme instance is notified of any property changes | |
4. When the component is unmounted, the JQuery element instance is destroyed | |
*/ | |
abstract class DevExtremeComponent<TWidget extends DevExpress.ui.Widget, DevExtrememeOptions> extends React.Component<DevExtrememeOptions & { validation?: DevExpress.ui.dxValidatorOptions }, any> { | |
refs: { | |
[key: string]: React.ReactInstance; | |
container: React.ReactInstance; | |
}; | |
public widget: TWidget; | |
private previousFlattenedOptions: any; | |
protected validator: DevExpress.ui.dxValidator; | |
abstract widgetCtor: { new (element: JQuery, options?: DevExpress.ui.dxButtonOptions): TWidget }; | |
devExtremeComponentDidMount(props: DevExtrememeOptions) | |
{ | |
this.instantiateDevExtremeComponent(this.widgetCtor, props); | |
this.previousFlattenedOptions = this.getFlattenedOptions(props); | |
} | |
private componentType() { | |
let name = (this as any).__proto__.constructor.toString().substr(9); | |
return name.substr(0, name.indexOf("(")); | |
} | |
private shouldLog = true; | |
private log(message: string) { | |
console.log(message); | |
} | |
private getFlattenedOptions(a) { | |
var nestedKeysInArray = ['searchPanel']; | |
var nestedKeys = {}; | |
for (let key of nestedKeysInArray) | |
nestedKeys[key] = null; | |
var keys = {}; | |
function getImpl(a, prefix) { | |
for (var key in a) { | |
var av = a[key]; | |
if (typeof av == 'object' && !prefix && key in nestedKeys) | |
getImpl(av, key + '.'); | |
else if (typeof av !== 'function') | |
keys[prefix + key] = av; | |
} | |
} | |
getImpl(a, ''); | |
return keys; | |
} | |
componentWillMount() { | |
if (this.shouldLog) | |
this.log(`${this.componentType()}.componentWillMount`); | |
} | |
componentWillReceiveProps() { | |
if (this.shouldLog) | |
this.log(`${this.componentType()}.componentWillReceiveProps`); | |
} | |
componentWillUpdate() { | |
if (this.shouldLog) | |
this.log(`${this.componentType()}.componentWillUpdate`); | |
} | |
componentDidUpdate() { | |
if (this.shouldLog) | |
this.log(`${this.componentType()}.componentDidUpdate`); | |
} | |
componentDidMount() { | |
if (this.shouldLog) | |
this.log(`${this.componentType()}.componentDidMount`); | |
this.devExtremeComponentDidMount(this.props); | |
} | |
protected instantiateDevExtremeComponent(builder: DevExtremeWidgetConstructorFunction<TWidget>, props: DevExtrememeOptions & { validation?: DevExpress.ui.dxValidatorOptions }) { | |
this.widget = new builder(this.$container(), props); | |
if (props.validation) | |
this.validator = new DevExpress.ui.dxValidator(this.$container(), props.validation); | |
} | |
componentWillUnmount() { | |
if (this.shouldLog) | |
this.log(`${this.componentType()}.componentWillUnmount`); | |
if (!this.widget) { return; } | |
const $elementInstance = this.widget.element(); | |
$elementInstance.remove(); | |
} | |
private $container(): JQuery { | |
return $(this.refs.container); | |
} | |
private updateDevExtremeComponentIfNecessary(nextProps: DevExtrememeOptions) { | |
var newFlattenedOptions = this.getFlattenedOptions(nextProps); | |
this.widget.beginUpdate(); | |
for (var key in newFlattenedOptions) { | |
var value = newFlattenedOptions[key]; | |
if (this.previousFlattenedOptions[key] !== value) { | |
this.widget.option(key, value); | |
if (this.shouldLog) | |
this.log(`Updated ${this.componentType()} property '${key}' = ${value}`); | |
} | |
} | |
this.widget.endUpdate(); | |
this.previousFlattenedOptions = newFlattenedOptions; | |
} | |
/** | |
* DevExtreme components should never re-render on update because the control is instantiated only once. | |
* However, if dx property has changed, we have to check for it and update the instance itself apart from react. | |
*/ | |
shouldComponentUpdate(nextProps: DevExtrememeOptions) { | |
if (this.shouldLog) | |
this.log(`${this.componentType()}.shouldComponentUpdate`); | |
this.updateDevExtremeComponentIfNecessary(nextProps); | |
return false; | |
} | |
/** | |
* Render a div which will be replaced by the DX JQuery component when instantiated | |
*/ | |
render() { | |
return <div className="dx-component" ref="container" /> | |
} | |
} | |
interface DevExtremeWidgetValueChangedEventArgs<T> { | |
component: any; | |
element: HTMLElement; | |
model: any; | |
value: T; | |
previousValue: T; | |
jQueryEvent: JQueryEventObject; | |
} | |
interface DevExtremeComponentExtraProps<T> { | |
onValueChanged: { (e: DevExtremeWidgetValueChangedEventArgs<T>): void } | |
} | |
class DxButton extends DevExtremeComponent<DevExpress.ui.dxButton, DevExpress.ui.dxButtonOptions> { widgetCtor = DevExpress.ui.dxButton; } | |
class DxDataGrid extends DevExtremeComponent<DevExpress.ui.dxDataGrid, DevExpress.ui.dxDataGridOptions> { widgetCtor = DevExpress.ui.dxDataGrid; } | |
class DxSelectBox extends DevExtremeComponent<DevExpress.ui.dxSelectBox, DevExpress.ui.dxSelectBoxOptions & DevExtremeComponentExtraProps<any>> { widgetCtor = DevExpress.ui.dxSelectBox; } | |
class DxCheckBox extends DevExtremeComponent<DevExpress.ui.dxCheckBox, DevExpress.ui.dxCheckBoxOptions & DevExtremeComponentExtraProps<boolean | undefined>> { widgetCtor = DevExpress.ui.dxCheckBox; } | |
class DxRadioGroup extends DevExtremeComponent<DevExpress.ui.dxRadioGroup, DevExpress.ui.dxRadioGroupOptions & DevExtremeComponentExtraProps<any>> { widgetCtor = DevExpress.ui.dxRadioGroup; } | |
class DxTextBox extends DevExtremeComponent<DevExpress.ui.dxTextBox, DevExpress.ui.dxTextBoxOptions & DevExtremeComponentExtraProps<string>> { | |
widgetCtor = DevExpress.ui.dxTextBox; | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
hey so I know it's been a while from when this gist was put here but I'm trying to figure out a way to do exactly this (create React wrappers for the DxComponens) and so far this is the only place where I have found some info on it but I'm fairly new to React so I was wondering if you could maybe include an simple example of how you would use this to like put a DateBox Component on another custom component you created or something like that or least point me in the correct direction :) Thank you so much for this :)