Repository: DevExpress/devextreme-vue
Pull request #122 (2018-07-10 14:38:38)

Add nested option basic implementation


Patch:
diff --git a/README.md b/README.md
index 3fb7a81..c778bc0 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # DevExtreme Vue UI and Visualization Components #
 
-[![Build Status](https://img.shields.io/shippable/5ab4c6354a24a207009ec636/master.svg?maxAge=43200)](https://app.shippable.com/github/DevExpress/devextreme-vue)
+[![Build Status](https://img.shields.io/shippable/5444c5ecb904a4b21567b0ff/master.svg?maxAge=43200)](https://app.shippable.com/github/DevExpress/devextreme-vue)
 ![Project Status](https://img.shields.io/badge/Project%20Status-alpha-orange.svg?maxAge=43200)
 [![NPM](https://img.shields.io/npm/v/devextreme-vue.svg?maxAge=43200)](https://www.npmjs.com/package/devextreme-vue)
 
diff --git a/example/components/grid-example.vue b/example/components/grid-example.vue
index 302d258..9c9ab0f 100644
--- a/example/components/grid-example.vue
+++ b/example/components/grid-example.vue
@@ -10,29 +10,87 @@
           text="Filter Row Visible"
         />
       </div>
+      <div>
+        Country Sort Order:&nbsp;
+        <dx-switch
+          :value.sync="countrySortOrderVal"
+          offText="desc"
+          onText="asc"
+        />
+      </div>
       <dx-data-grid 
           :dataSource="sales"
           keyExpr="orderId"
-          :rowAlternationEnabled="alternateRowColors"
           :allowColumnReordering="true"
-          :grouping="grouping"
-          :groupPanel="groupPanel"
-          :paging="paging"
-          :selection="selection"
-          :filterRow="filterRow"
+          :rowAlternationEnabled="alternateRowColors"
           :selectedRowKeys="selectedRowKeys"
-          :columns="columns"
       >
+
+        <dx-filter-row :visible="filterRowVisible" />
+        <dx-group-panel :visible="true" />
+        <dx-grouping :autoExpandAll="true" />
+        <dx-selection mode="multiple" />
+
+        <dx-column
+          dataField="orderId"
+          caption="Order ID"
+          :allowSorting="false"
+          :allowFiltering="false"
+          :allowGrouping="false"
+          :allowReordering="false"
+          :width="100"
+        />
+        <dx-column
+          dataField="city"
+          cellTemplate="cell-city"
+        />
+        <dx-column
+          dataField="country"
+          :sortOrder="countrySortOrder"
+        />
+        <dx-column
+          dataField="region"
+          :groupIndex="0"
+        />
+        <dx-column
+          dataField="date"
+          dataType="date"
+          selectedFilterOperation=">="
+          filterValue="2013/04/01"
+          :width="150"
+        />
+        <dx-column
+          dataField="amount"
+          format="currency"
+          selectedFilterOperation=">="
+          :filterValue="1000"
+          :width="100"
+        />
+
+        <dx-pager :visible="true" :showPageSizeSelector="true" />
+        <dx-paging :pageSize="10"/>
+
         <dx-button slot="cell-city" slot-scope="data" :text="data.text" />
       </dx-data-grid>
     </example-block>
 </template>
 
 <script>
+import Vue from "vue";
+
 import ExampleBlock from "./example-block";
+import { DxButton, DxCheckBox, DxDataGrid, DxSwitch } from "../../src";
+import {
+  DxColumn,
+  DxFilterRow,
+  DxGrouping,
+  DxGroupPanel,
+  DxPager,
+  DxPaging,
+  DxSelection
+} from "../../src/ui/data-grid";
+
 import { sales } from "../data";
-import { DxDataGrid, DxButton, DxCheckBox } from "../../src";
-import Vue from "vue";
 
 const selectedKeys = [10273, 10277, 10292, 10295, 10300, 10302, 10305, 10308, 10312, 
 10319, 10321, 10323, 10326, 10328, 10331, 10334, 10335, 10341, 10351, 10353, 10356, 
@@ -41,62 +99,31 @@ const selectedKeys = [10273, 10277, 10292, 10295, 10300, 10302, 10305, 10308, 10
 export default {
   components: {
     ExampleBlock,
-    DxDataGrid,
     DxButton,
-    DxCheckBox
+    DxCheckBox,
+    DxDataGrid,
+    DxSwitch,
+    DxColumn,
+    DxFilterRow,
+    DxGrouping,
+    DxGroupPanel,
+    DxPager,
+    DxPaging,
+    DxSelection
   },
   computed: {
-    filterRow() {
-      return { visible: this.filterRowVisible };
+    countrySortOrder() {
+      console.log(this.countrySortOrderVal);
+      return this.countrySortOrderVal ? "asc" : "desc";
     }
   },
   data: function() {
     return {
       sales: sales,
-      alternateRowColors: true,
       selectedRowKeys: selectedKeys,
-      grouping: { autoExpandAll: true },
+      alternateRowColors: true,
       filterRowVisible: true,
-      groupPanel: { visible: true },
-      paging: { pageSize: 10 },
-      selection: { mode: 'multiple' },
-      columns: [
-        {
-          dataField: "orderId",
-          caption: "Order ID",
-          allowSorting: false,
-          allowFiltering: false,
-          allowGrouping: false,
-          allowReordering: false,
-          width: 100
-        },
-        {
-          dataField: "city",
-          cellTemplate: "cell-city"
-        },
-        {
-          dataField: "country",
-          sortOrder: "asc"
-        },
-        {
-          dataField: "region",
-          groupIndex: 0
-        },
-        {
-          dataField: "date",
-          dataType: "date",
-          selectedFilterOperation: ">=",
-          filterValue: "2013/04/01",
-          width: 150
-        },
-        {
-          dataField: "amount",
-          format: "currency",
-          selectedFilterOperation: ">=",
-          filterValue: 1000,
-          width: 100
-        }
-      ]
+      countrySortOrderVal: true
     };
   }
 };
diff --git a/src/core/component.ts b/src/core/component.ts
index 724e44f..8ee21bb 100644
--- a/src/core/component.ts
+++ b/src/core/component.ts
@@ -3,12 +3,18 @@ import { VNode, VueConstructor } from "vue";
 
 import * as events from "devextreme/events";
 
+import Configuration, { bindOptionWatchers, IConfigurable } from "./configuration";
 import { camelize } from "./helpers";
 
+interface IWidgetComponent extends IConfigurable {
+    $_instance: any;
+}
+
+const Vue = VueType.default || VueType;
+
 const DX_TEMPLATE_WRAPPER_CLASS = "dx-template-wrapper";
 const DX_REMOVE_EVENT = "dxremove";
 
-const Vue = VueType.default || VueType;
 const BaseComponent: VueConstructor = Vue.extend({
 
     render(createElement: (...args) => VNode): VNode {
@@ -16,25 +22,36 @@ const BaseComponent: VueConstructor = Vue.extend({
     },
 
     beforeDestroy(): void {
-        const instance = (this as any).$_instance;
+        const instance = (this as any as IWidgetComponent).$_instance;
         if (instance) {
             events.triggerHandler(this.$el, DX_REMOVE_EVENT);
             instance.dispose();
         }
     },
 
+    created(): void {
+        (this as any as IWidgetComponent).$_config = new Configuration(
+            (n: string, v: any) => (this as any as IWidgetComponent).$_instance.option(n, v),
+            null,
+            this.$props && Object.keys(this.$props),
+            this.$options.propsData && { ...this.$options.propsData }
+        );
+    },
+
     methods: {
         $_createWidget(element: any): void {
+            const widgetComponent = this as any as IWidgetComponent;
             const options: object = {
                 ...this.$_getIntegrationOptions(),
-                ...this.$options.propsData
+                ...this.$options.propsData,
+                ...widgetComponent.$_config.getInitialValues()
             };
 
             const instance = new (this as any).$_WidgetClass(element, options);
-            (this as any).$_instance = instance;
+            (this as any as IWidgetComponent).$_instance = instance;
 
             instance.on("optionChanged", this.$_handleOptionChanged.bind(this));
-            this.$_watchProps(instance);
+            bindOptionWatchers(widgetComponent.$_config, this);
             this.$_createEmitters(instance);
         },
         $_getIntegrationOptions(): object {
@@ -88,17 +105,6 @@ const BaseComponent: VueConstructor = Vue.extend({
             this.$emit("update:" + args.name, args.value);
         },
 
-        $_watchProps(instance: any): void {
-            if (!this.$props) {
-                return;
-            }
-            Object.keys(this.$props).forEach((prop: string) => {
-                this.$watch(prop, (value) => {
-                    instance.option(prop, value);
-                });
-            });
-        },
-
         $_createEmitters(instance: any): void {
             Object.keys(this.$listeners).forEach((listenerName: string) => {
                 const eventName = camelize(listenerName);
@@ -125,6 +131,7 @@ const DxExtensionComponent: VueConstructor = BaseComponent.extend({
     created(): void {
         (this as any).$_isExtension = true;
     },
+
     methods: {
         attachTo(element: any) {
             (this as any).$_createWidget(element);
@@ -132,4 +139,31 @@ const DxExtensionComponent: VueConstructor = BaseComponent.extend({
     }
 });
 
-export { DxComponent, DxExtensionComponent };
+const DxConfiguration: VueConstructor = Vue.extend({
+
+    render(createElement: (...args) => VNode): VNode {
+        return createElement();
+    },
+
+    methods: {
+        $_initOption(name: string, isCollectionItem?: boolean): void {
+            const options = Object.keys(this.$props);
+            const initialValues = { ...this.$options.propsData };
+            const config = (this.$parent as IConfigurable).$_config.createNested(
+                name,
+                options,
+                initialValues,
+                isCollectionItem
+            );
+            (this as any as IConfigurable).$_config = config;
+
+            bindOptionWatchers(config, this);
+        },
+
+        $_initCollectionOption(name: string): void {
+            this.$_initOption(name, true);
+        }
+    }
+});
+
+export { DxComponent, DxExtensionComponent, DxConfiguration };
diff --git a/src/core/configuration.ts b/src/core/configuration.ts
new file mode 100644
index 0000000..dfd8e68
--- /dev/null
+++ b/src/core/configuration.ts
@@ -0,0 +1,114 @@
+import { Vue } from "vue/types/vue";
+
+type UpdateFunc = (name: string, value: any) => void;
+
+interface IConfigurable {
+    $_config: Configuration;
+}
+
+class Configuration {
+
+    private readonly _nestedConfigurations: Configuration[];
+    private readonly _name: string | null;
+    private readonly _isCollectionItem: boolean;
+    private readonly _collectionItemIndex: number | undefined;
+    private readonly _options: string[];
+    private readonly _initialValues: Record<string, any>;
+    private readonly _updateFunc: UpdateFunc;
+
+    constructor(
+        updateFunc: UpdateFunc,
+        name: string | null,
+        options: string[],
+        initialValues: Record<string, any>,
+        isCollectionItem?: boolean,
+        collectionItemIndex?: number
+    ) {
+        this._updateFunc = updateFunc;
+        this._name = name;
+        this._options = options ? options : [];
+        this._initialValues = initialValues ? initialValues : {};
+        this._nestedConfigurations = [];
+        this._isCollectionItem = !!isCollectionItem;
+        this._collectionItemIndex = collectionItemIndex;
+
+        this.updateValue = this.updateValue.bind(this);
+    }
+
+    public createNested(
+        name: string,
+        options: string[],
+        initialValues: Record<string, any>,
+        isCollectionItem?: boolean
+    ): Configuration {
+        let collectionItemIndex = -1;
+        if (isCollectionItem && name) {
+            collectionItemIndex = this._nestedConfigurations.filter((c) => c._name && c._name === name).length;
+        }
+
+        const configuration = new Configuration(
+            this.updateValue,
+            name,
+            options,
+            initialValues,
+            isCollectionItem,
+            collectionItemIndex
+        );
+
+        this._nestedConfigurations.push(configuration);
+
+        return configuration;
+    }
+
+    public updateValue(nestedName: string, value: any): void {
+        const name = this._isCollectionItem ? `${this._name}[${this._collectionItemIndex}]` : this._name;
+        const fullName = [name, nestedName].filter((n) => n).join(".");
+        this._updateFunc(fullName, value);
+    }
+
+    public getInitialValues(): Record<string, any> | undefined {
+        const values = {
+            ...this._initialValues
+        };
+
+        this._nestedConfigurations.forEach((o) => {
+            if (!o._name) { return; }
+
+            const nestedValue = o.getInitialValues();
+            if (!nestedValue) { return; }
+
+            if (!o._isCollectionItem) {
+                values[o._name] = nestedValue;
+            } else {
+                let arr = values[o._name];
+                if (!arr || !Array.isArray(arr)) {
+                    arr = [];
+                    values[o._name] = arr;
+                }
+
+                arr.push(nestedValue);
+            }
+        });
+
+        return Object.keys(values).length > 0 ? values : undefined;
+    }
+
+    public getOptionsToWatch(): string[] {
+        const blackList = {};
+        this._nestedConfigurations.forEach((c) => c._name && (blackList[c._name] = true));
+
+        return this._options.filter((o) => !blackList[o]);
+    }
+}
+
+function bindOptionWatchers(config: Configuration, vueInstance: Pick<Vue, "$watch" | "$props">): void {
+    const targets = config.getOptionsToWatch();
+    if (targets) {
+        targets.forEach((optionName: string) => {
+            vueInstance.$watch(optionName, (value) => config.updateValue(optionName, value));
+        });
+    }
+}
+
+export default Configuration;
+export { bindOptionWatchers, IConfigurable, UpdateFunc };
diff --git a/tools/src/component-generator.ts b/tools/src/component-generator.ts
index 718bf41..cd11900 100644
--- a/tools/src/component-generator.ts
+++ b/tools/src/component-generator.ts
@@ -1,23 +1,49 @@
-import { createKeyComparator, lowercaseFirst, uppercaseFirst } from "./helpers";
+import { compareStrings } from "./helpers";
 import createTempate from "./template";
 
 interface IComponent {
     name: string;
-    baseComponentPath: string;
+    widgetName: string;
+    base: {
+        name: string;
+        path: string;
+    };
     dxExportPath: string;
     props?: IProp[];
     hasModel?: boolean;
-    isExtension?: boolean;
+    nestedComponents?: INestedComponent[];
+}
+
+interface IFileImport {
+    path: string;
+    namedImports: string[];
 }
 
 interface IComponentModel {
-    name: string;
+    componentName: string;
+    baseImport: IFileImport;
     baseComponentPath: string;
     baseComponentName: string;
     dxExportPath: string;
-    renderedProps?: string[];
+    renderedProps?: string;
     hasModel?: boolean;
     widgetName: string;
+    nestedComponents?: INestedComponentModel[];
+    namedExports: string[];
+}
+
+interface INestedComponent {
+    name: string;
+    optionName: string;
+    props: IProp[];
+    isCollectionItem: boolean;
+}
+
+interface INestedComponentModel {
+    name: string;
+    optionName: string;
+    renderedProps: string;
+    isCollectionItem: boolean;
 }
 
 interface IProp {
@@ -29,59 +55,205 @@ interface IProp {
 }
 
 function generate(component: IComponent): string {
+    const nestedComponents = component.nestedComponents
+        ? component.nestedComponents.map(createNestedComponentModel)
+        : undefined;
+
+    const namedExports: string[] = [ component.name ];
+    const baseImport: IFileImport = {
+        path: component.base.path,
+        namedImports: [ `${component.base.name} as BaseComponent` ]
+    };
+
+    if (nestedComponents && nestedComponents.length) {
+        namedExports.push(...nestedComponents.map((c) => c.name));
+        baseImport.namedImports.push("DxConfiguration");
+    }
+    baseImport.namedImports = baseImport.namedImports.sort();
+
     const componentModel = {
         ...component,
+        baseImport,
+        componentName: component.name,
+        widgetName: component.widgetName,
+        baseComponentName: component.base.name,
+        baseComponentPath: component.base.path,
+
         renderedProps: component.props
-            ? component.props.sort(createKeyComparator<IProp>((p) => p.name)).map(createPropModel)
+            ? renderProps(component.props)
             : undefined,
-        widgetName: `${uppercaseFirst(component.name)}`,
-        baseComponentName: component.isExtension ? "DxExtensionComponent" : "DxComponent"
+
+        nestedComponents,
+        namedExports
     };
 
     return renderComponent(componentModel);
 }
 
-function createPropModel(p: IProp) {
-    const hasExtendedDeclaration = !p.types || !!p.acceptableValues;
-    return hasExtendedDeclaration ? renderExtendedProp(p) : renderSimpleProp(p);
+function createNestedComponentModel(component: INestedComponent): INestedComponentModel {
+    return {
+        name: component.name,
+        optionName: component.optionName,
+        renderedProps: component.props
+            ? renderProps(component.props)
+            : undefined,
+        isCollectionItem: component.isCollectionItem
+    };
 }
 
 // tslint:disable:max-line-length
-const renderComponent: (model: IComponentModel) => string = createTempate(`
-import * as VueType from "vue";
+
+const L1: string = `\n` + tab(1);
+const L2: string = `\n` + tab(2);
+const L3: string = `\n` + tab(3);
+const L4: string = `\n` + tab(4);
+
+const renderComponent: (model: IComponentModel) => string = createTempate(
+`import * as VueType from "vue";
 const Vue = VueType.default || VueType;
 import <#= it.widgetName #> from "devextreme/<#= it.dxExportPath #>";
 import { VueConstructor } from "vue";
-import { <#= it.baseComponentName #> as BaseComponent } from "<#= it.baseComponentPath #>";
-
-const Dx<#= it.name #>: VueConstructor = Vue.extend({
-  extends: BaseComponent,<#? it.props #>
-  props: {<#= it.renderedProps.join(',') #>
-  },<#?#><#? it.hasModel #>
-  model: { prop: "value", event: "update:value" },<#?#>
-  computed: {
+import { ` +
+
+`<#~ it.baseImport.namedImports :namedImport #>` +
+    `<#= namedImport #>` + `, ` +
+`<#~#>` + `\b\b` +
+
+` } from "<#= it.baseComponentPath #>";` + `\n` + `\n` +
+
+`const <#= it.componentName #>: VueConstructor = Vue.extend({` +
+L1 + `extends: BaseComponent,` +
+
+`<#? it.props #>` +
+    L1 + `props: {` + `\n` +
+    `<#= it.renderedProps #>` +
+    L1 + `},` +
+`<#?#>` +
+
+`<#? it.hasModel #>` +
+  L1 + `model: { prop: "value", event: "update:value" },` +
+`<#?#>` +
+
+  L1 + `computed: {
     instance(): <#= it.widgetName #> {
       return (this as any).$_instance;
     }
-  },
-  beforeCreate() {
+  },` +
+
+  L1 + `beforeCreate() {
     (this as any).$_WidgetClass = <#= it.widgetName #>;
   }
-});
-export { Dx<#= it.name #> };
-`.trimLeft());
-
-const renderSimpleProp: (model: IProp) => string = createTempate(`
-    <#= it.name #>: <#? it.types.length > 1 #>[<#?#><#= it.types.join(', ') #><#? it.types.length > 1 #>]<#?#>
-`.trimRight());
-
-const renderExtendedProp: (model: IProp) => string = createTempate(`
-    <#= it.name #>: {<#? it.types #>
-      type: <#? it.types.length > 1 #>[<#?#><#= it.types.join(', ') #><#? it.types.length > 1 #>]<#?#><#?#><#? it.acceptableValues #>,
-      validator: (v) => <#? it.isArray #>!Array.isArray(v) || v.filter(i => [\n        <#= it.acceptableValues.join(',\\n        ') #>\n      ].indexOf(i) === -1).length === 0<#??#>typeof(v) !== <#? it.acceptableValueType #>"<#= it.acceptableValueType #>"<#??#>"string"<#?#> || [\n        <#= it.acceptableValues.join(',\\n        ') #>\n      ].indexOf(v) !== -1<#?#>
-    <#?#>}
-`.trimRight());
-// tslint:enable:max-line-length
+});` + `\n` +
+
+`<#? it.nestedComponents #>` +
+    `\n` +
+    `<#~ it.nestedComponents : nested #>` +
+        `const <#= nested.name #> = Vue.extend({` +
+        L1 + `extends: DxConfiguration,` +
+        L1 + `props: {` + `\n` +
+        `<#= nested.renderedProps #>` +
+        L1 + `},` +
+        L1 + `beforeMount() {` +
+        L2 + `(this as any).` +
+
+        `<#? nested.isCollectionItem #>` +
+            `$_initCollectionOption` +
+        `<#??#>` +
+            `$_initOption` +
+        `<#?#>` +
+
+        `("<#= nested.optionName #>");` +
+        L1 + `}` + `\n` +
+        `});` + `\n` +
+    `<#~#>` +
+`<#?#>` +
+
+`\n` +
+`export {` + `\n` +
+    `<#~ it.namedExports :namedExport #>` +
+    tab(1) + `<#= namedExport #>,` + `\n` +
+    `<#~#>` + `\b` + `\b` + `\n` +
+`};` + `\n`
+);
+
+function renderProps(props: IProp[]): string {
+    return renderPropsTemplate(props.sort(compareProps));
+}
+const compareProps = (a: IProp, b: IProp) => compareStrings(a.name, b.name);
+const renderPropsTemplate: (props: IProp[]) => string = createTempate(
+`<#~ it :prop #>` +
+
+    tab(2) +
+
+    `<#? prop.types && prop.types.length > 0 && !prop.acceptableValues #>` +
+        `<#= prop.name #>: ` +
+
+        `<#? prop.types.length > 1 #>` + `[` + `<#?#>` +
+
+        `<#= prop.types.join(', ') #>` +
+
+        `<#? prop.types.length > 1 #>` + `]` + `<#?#>` +
+
+    `<#??#>` +
+        `<#= prop.name #>: {` +
+
+        `<#? prop.types #>` +
+            L3 + `type: ` +
+
+            `<#? prop.types.length > 1 #>` + `[` + `<#?#>` +
+
+            `<#= prop.types.join(', ') #>` +
+
+            `<#? prop.types.length > 1 #>` + `]` + `<#?#>` +
+
+        `<#?#>` +
+
+        `<#? prop.acceptableValues #>` +
+            `,` +
+            L3 + `validator: (v) => ` +
+
+            `<#? prop.isArray #>` +
+
+                `!Array.isArray(v) || v.filter(i => [` +
+
+                    `<#~ prop.acceptableValues : value #>` +
+                        L4 + `<#= value #>,` +
+                    `<#~#>` + `\b` +
+
+                L3 + `].indexOf(i) === -1).length === 0` +
+
+            `<#??#>` +
+                `typeof(v) !== ` +
+
+                `<#? prop.acceptableValueType #>` +
+                    `"<#= prop.acceptableValueType #>"` +
+                `<#??#>` +
+                    `"string"` +
+                `<#?#>` +
+
+                ` || [` +
+
+                `<#~ prop.acceptableValues : value #>` +
+                    L4 + `<#= value #>,` +
+                `<#~#>` + `\b` +
+
+                L3 + `].indexOf(v) !== -1` +
+            `<#?#>` +
+
+            L2 +
+
+        `<#?#>` +
+
+         `}` +
+
+    `<#?#>,` + `\n` +
+
+`<#~#>` + `\b` + `\b`
+);
+
+function tab(i: number): string {
+    return Array(i * 2 + 1).join(" ");
+}
 
 export default generate;
-export { IComponent, IProp };
+export { IComponent, INestedComponent, IProp, renderProps };
diff --git a/tools/src/generator.ts b/tools/src/generator.ts
index 8eaba06..5bd76c5 100644
--- a/tools/src/generator.ts
+++ b/tools/src/generator.ts
@@ -1,37 +1,38 @@
 import { writeFileSync as writeFile } from "fs";
 import { dirname as getDirName, join as joinPaths, relative as getRelativePath, sep as pathSeparator } from "path";
 
-import { ICustomType, IModel, IProp as IOption, ITypeDescr, IWidget } from "../integration-data-model";
-import generateComponent, { IComponent, IProp } from "./component-generator";
+import { IComplexProp, ICustomType, IModel, IProp as IOption, ITypeDescr, IWidget } from "../integration-data-model";
+import generateComponent, { IComponent, INestedComponent, IProp } from "./component-generator";
 import { convertTypes } from "./converter";
-import { removeExtension, removePrefix, toKebabCase } from "./helpers";
-import generateIndex from "./index-generator";
+import { removeExtension, removePrefix, toKebabCase, uppercaseFirst } from "./helpers";
+import generateIndex, { IReExport } from "./index-generator";
 
 function generate(
   rawData: IModel,
-  baseComponent: string,
+  baseComponentPath: string,
   out: {
     componentsDir: string,
     indexFileName: string
   }
 ) {
-  const modulePaths: string[] = [];
+  const modulePaths: IReExport[] = [];
 
   rawData.widgets.forEach((data) => {
-    const widgetFile = mapWidget(data, baseComponent, rawData.customTypes);
+    const widgetFile = mapWidget(data, baseComponentPath, rawData.customTypes);
     const widgetFilePath = joinPaths(out.componentsDir, widgetFile.fileName);
     const indexFileDir = getDirName(out.indexFileName);
 
     writeFile(widgetFilePath, generateComponent(widgetFile.component), { encoding: "utf8" });
-    modulePaths.push(
-      "./" + removeExtension(getRelativePath(indexFileDir, widgetFilePath)).replace(pathSeparator, "/")
-    );
+    modulePaths.push({
+      name: widgetFile.component.name,
+      path: "./" + removeExtension(getRelativePath(indexFileDir, widgetFilePath)).replace(pathSeparator, "/")
+    });
   });
 
   writeFile(out.indexFileName, generateIndex(modulePaths), { encoding: "utf8" });
 }
 
-function mapWidget(raw: IWidget, baseComponent: string, customTypes: ICustomType[]): {
+function mapWidget(raw: IWidget, baseComponentPath: string, customTypes: ICustomType[]): {
   fileName: string,
   component: IComponent
 } {
@@ -45,16 +46,31 @@ function mapWidget(raw: IWidget, baseComponent: string, customTypes: ICustomType
   return {
     fileName: `${toKebabCase(name)}.ts`,
     component: {
-      name,
-      baseComponentPath: baseComponent,
+      name: `Dx${name}`,
+      widgetName: name,
+      base: {
+        name: raw.isExtension ? "DxExtensionComponent" : "DxComponent",
+        path: baseComponentPath
+      },
       dxExportPath: raw.exportPath,
       props: raw.options.map((o) => mapProp(o, customTypeHash)),
       hasModel: !!raw.isEditor,
-      isExtension: !!raw.isExtension,
+      nestedComponents: raw.complexOptions
+        ? raw.complexOptions.map((o) => mapNestedComponent(o, customTypeHash))
+        : undefined
     }
   };
 }
 
+function mapNestedComponent(complexOption: IComplexProp, customTypes: Record<string, ICustomType>): INestedComponent {
+  return {
+    name: `Dx${uppercaseFirst(complexOption.name)}`,
+    optionName: complexOption.optionName,
+    props: complexOption.props.map((o) => mapProp(o, customTypes)),
+    isCollectionItem: complexOption.isCollectionItem
+  };
+}
+
 function mapProp(rawOption: IOption, customTypes: Record<string, ICustomType>): IProp {
   const types = convertTypes(rawOption.types, customTypes);
   const restrictedTypes: ITypeDescr[] = rawOption.types.filter(
diff --git a/tools/src/helpers.ts b/tools/src/helpers.ts
index 5bac45f..51925d5 100644
--- a/tools/src/helpers.ts
+++ b/tools/src/helpers.ts
@@ -24,7 +24,3 @@ export function lowercaseFirst(value: string): string {
 export function compareStrings(a: string, b: string) {
     return a.localeCompare(b, undefined, { caseFirst: "upper" });
 }
-
-export function createKeyComparator<T>(keyGetter: (x: T) => string) {
-    return (a: T, b: T) => compareStrings(keyGetter(a), keyGetter(b));
-}
diff --git a/tools/src/index-generator.ts b/tools/src/index-generator.ts
index 3d1fdca..d730a1c 100644
--- a/tools/src/index-generator.ts
+++ b/tools/src/index-generator.ts
@@ -1,12 +1,20 @@
 import createTempate from "./template";
 
-function generate(paths: string[]): string {
+interface IReExport {
+  name: string;
+  path: string;
+}
+
+function generate(paths: IReExport[]): string {
   return render(paths);
 }
 
-const render: (model: string[]) => string = createTempate(`
-<#~ it :modulePath #>export * from "<#= modulePath #>";
+const render: (model: IReExport[]) => string = createTempate(`
+<#~ it :reExport #>export { <#= reExport.name #> } from "<#= reExport.path #>";
 <#~#>
 `.trim());
 
 export default generate;
+export {
+  IReExport
+};
diff --git a/tools/src/template.ts b/tools/src/template.ts
index fcddcf7..61c0824 100644
--- a/tools/src/template.ts
+++ b/tools/src/template.ts
@@ -13,6 +13,12 @@ const settings = {
     varname: "it"
 };
 
-const createTempate = (templateStr: string): ((model: any) => string) => template(templateStr, settings);
+const createTempate = (templateStr: string): ((model: any) => string) => {
+    const templateFunc = template(templateStr, settings);
+
+    return (model: any) => (templateFunc(model) as string)
+        .replace(/[\s\S]{1}\x08{1}|[\s\S]{2}\x08{2}|[\s\S]{3}\x08{3}/g, "")
+        .replace(/\x08/, "");
+};
 
 export default createTempate;


Test patch:
diff --git a/src/core/component.test.ts b/src/core/component.test.ts
index 45c7f47..4ed24b7 100644
--- a/src/core/component.test.ts
+++ b/src/core/component.test.ts
@@ -1,5 +1,5 @@
 import Vue from "vue";
-import { DxComponent, DxExtensionComponent } from "../core/component";
+import { DxComponent, DxConfiguration, DxExtensionComponent } from "../core/component";
 
 import * as events from "devextreme/events";
 
@@ -26,6 +26,18 @@ const TestComponent = Vue.extend({
     }
 });
 
+const TestNestedComponent = Vue.extend({
+    extends: DxConfiguration,
+    props: {
+        prop1: Number,
+        prop2: String
+    },
+    beforeMount() {
+        (this as any).$_initOption("nestedOption");
+    }
+});
+
+jest.setTimeout(1000);
 beforeEach(() => {
     jest.clearAllMocks();
 });
@@ -55,7 +67,8 @@ describe("component rendering", () => {
     });
 });
 
-describe("option processing", () => {
+describe("options", () => {
+
     it("pass props to option on mounting", () => {
         const vm = new TestComponent({
             propsData: {
@@ -91,6 +104,87 @@ describe("option processing", () => {
     });
 });
 
+describe("nested options", () => {
+
+    it("pulls initital values on mounting (template)", () => {
+        const vm = new Vue({
+            template:
+                `<test-component>` +
+                `  <test-nested-component :prop1="123" />` +
+                `</test-component>`,
+            components: {
+                TestComponent,
+                TestNestedComponent
+            }
+        }).$mount();
+
+        expect(WidgetClass).toHaveBeenCalledWith(vm.$children[0].$el, {
+            nestedOption: {
+                prop1: 123
+            }
+        });
+    });
+
+    it("pulls initital values on mounting (render function + createElement)", () => {
+        const widget = new TestComponent({
+            render(createElement) {
+                return createElement(TestNestedComponent, { props: { prop1: 123 } });
+            }
+        }).$mount();
+
+        expect(WidgetClass).toHaveBeenCalledWith(widget.$el, {
+            nestedOption: {
+                prop1: 123
+            }
+        });
+    });
+
+    it("pulls initital values on mounting (manual mounting)", () => {
+        const widget = new TestComponent();
+        const option = new TestNestedComponent({
+            propsData: {
+                prop1: 123
+            },
+            parent: widget
+        });
+
+        option.$mount();
+        widget.$mount();
+
+        expect(WidgetClass).toHaveBeenCalledWith(widget.$el, {
+            nestedOption: {
+                prop1: 123
+            }
+        });
+    });
+
+    it("watches nested option changes", (done) => {
+        const vm = new Vue({
+            template:
+                `<test-component>` +
+                `  <test-nested-component :prop1="value" />` +
+                `</test-component>`,
+            components: {
+                TestComponent,
+                TestNestedComponent
+            },
+            props: ["value"],
+            propsData: {
+                value: 123
+            }
+        }).$mount();
+
+        vm.$props.value = 456;
+
+        Vue.nextTick(() => {
+            expect(Widget.option).toHaveBeenCalledTimes(1);
+            expect(Widget.option).toHaveBeenCalledWith("nestedOption.prop1", 456);
+            done();
+        });
+    });
+
+});
+
 describe("template", () => {
 
     const DX_TEMPLATE_WRAPPER = "dx-template-wrapper";
diff --git a/src/core/configuration.test.ts b/src/core/configuration.test.ts
new file mode 100644
index 0000000..7fd4c6a
--- /dev/null
+++ b/src/core/configuration.test.ts
@@ -0,0 +1,201 @@
+import Configuration, { UpdateFunc } from "./configuration";
+
+function createRootConfig(updateFunc: UpdateFunc): Configuration {
+    return new Configuration(updateFunc, null, [], {});
+}
+
+it("calls update from nested", () => {
+    const callback = jest.fn();
+    const root = createRootConfig(callback);
+
+    const nested = root.createNested("option", [], {});
+    nested.updateValue("prop", 123);
+
+    expect(callback).toHaveBeenCalledTimes(1);
+    expect(callback).toHaveBeenCalledWith("option.prop", 123);
+});
+
+it("calls update from subnested", () => {
+    const callback = jest.fn();
+    const root = createRootConfig(callback);
+    const nested = root.createNested("option", [], {});
+    const subNested = nested.createNested("subOption", [], {});
+
+    subNested.updateValue("prop", 123);
+
+    expect(callback).toHaveBeenCalledTimes(1);
+    expect(callback).toHaveBeenCalledWith("option.subOption.prop", 123);
+});
+
+it("calls update from nested collectionItem (first)", () => {
+    const callback = jest.fn();
+    const root = createRootConfig(callback);
+    const nested = root.createNested("option", [], {}, true);
+    root.createNested("option", [], {}, true);
+
+    nested.updateValue("prop", 123);
+
+    expect(callback).toHaveBeenCalledTimes(1);
+    expect(callback).toHaveBeenCalledWith("option[0].prop", 123);
+});
+
+it("calls update from nested collectionItem (middle)", () => {
+    const callback = jest.fn();
+    const root = createRootConfig(callback);
+
+    root.createNested("option", [], {}, true);
+    const nested = root.createNested("option", [], {}, true);
+    root.createNested("option", [], {}, true);
+
+    nested.updateValue("prop", 123);
+
+    expect(callback).toHaveBeenCalledTimes(1);
+    expect(callback).toHaveBeenCalledWith("option[1].prop", 123);
+});
+
+it("calls update from nested collectionItem (last)", () => {
+    const callback = jest.fn();
+    const root = createRootConfig(callback);
+    root.createNested("option", [], {}, true);
+    root.createNested("option", [], {}, true);
+    const nested = root.createNested("option", [], {}, true);
+
+    nested.updateValue("prop", 123);
+
+    expect(callback).toHaveBeenCalledTimes(1);
+    expect(callback).toHaveBeenCalledWith("option[2].prop", 123);
+});
+
+it("calls update from nested collectionItem (the only)", () => {
+    const callback = jest.fn();
+    const root = createRootConfig(callback);
+    const nested = root.createNested("option", [], {}, true);
+
+    nested.updateValue("prop", 123);
+
+    expect(callback).toHaveBeenCalledTimes(1);
+    expect(callback).toHaveBeenCalledWith("option[0].prop", 123);
+});
+
+describe("initial configuration", () => {
+
+    it("pulls value from nested", () => {
+        const root = createRootConfig(jest.fn());
+
+        const nested = root.createNested("option", [], {});
+        nested.createNested("subOption", ["prop"], { prop: 123 });
+
+        expect(root.getInitialValues()).toMatchObject({
+            option: {
+                subOption: {
+                    prop: 123
+                }
+            }
+        });
+    });
+
+    it("pulls array of values from a coollectionItem nested (single value)", () => {
+        const root = createRootConfig(jest.fn());
+
+        root.createNested("options", [], { propA: 123 }, true);
+
+        expect(root.getInitialValues()).toMatchObject({
+            options: [
+                { propA: 123 }
+            ]
+        });
+    });
+
+    it("pulls array of values from a coollectionItem nested (several values)", () => {
+        const root = createRootConfig(jest.fn());
+
+        root.createNested("options", [], { propA: 123 }, true);
+        root.createNested("options", [], { propA: 456, propB: 789 }, true);
+
+        expect(root.getInitialValues()).toMatchObject({
+            options: [
+                { propA: 123 },
+                { propA: 456, propB: 789 },
+            ]
+        });
+    });
+
+    it("pulls values from the last nested (not a coollectionItem)", () => {
+        const root = createRootConfig(jest.fn());
+
+        root.createNested("option", [], { propA: 123 });
+        root.createNested("option", [], { propA: 456, propB: 789 });
+
+        expect(root.getInitialValues()).toMatchObject({
+            option: { propA: 456, propB: 789 }
+        });
+    });
+
+    it("pulls values from self and nested", () => {
+        const root = new Configuration(jest.fn(), null, [], { propA: 123 });
+
+        const nested = root.createNested("option", [], { propB: 456 });
+        nested.createNested("subOption", [], { propC: 789 });
+
+        expect(root.getInitialValues()).toMatchObject({
+            propA: 123,
+            option: {
+                propB: 456,
+                subOption: {
+                    propC: 789
+                }
+            }
+        });
+    });
+
+    it("pulls undefined if no values provided", () => {
+        const root = createRootConfig(jest.fn());
+
+        const nested = root.createNested("option", [], {});
+        nested.createNested("subOption", [], {});
+
+        expect(root.getInitialValues()).toBeUndefined();
+    });
+
+    it("pulls values and ignores empty nested", () => {
+        const root = createRootConfig(jest.fn());
+
+        const nested = root.createNested("option", ["empty"], {});
+        nested.createNested("subOption", ["prop"], { prop: 123 });
+
+        root.createNested("anotherOption", [], {});
+        nested.createNested("anotherSubOption", [], {});
+
+        expect(root.getInitialValues()).toMatchObject({
+            option: {
+                subOption: {
+                    prop: 123
+                }
+            }
+        });
+    });
+
+});
+
+describe("options watch-list", () => {
+
+    it("includes option with initial values", () => {
+        const config = new Configuration(jest.fn(), null, ["option1"], { option1: 123, option2: 456 });
+
+        expect(config.getOptionsToWatch()).toEqual(["option1"]);
+    });
+
+    it("includes option without initial values", () => {
+        const config = new Configuration(jest.fn(), null, ["option1"], {});
+
+        expect(config.getOptionsToWatch()).toEqual(["option1"]);
+    });
+
+    it("excludes option if finds nested config with the same name", () => {
+        const config = new Configuration(jest.fn(), null, ["option1", "theNestedOption"], {});
+        config.createNested("theNestedOption", [], {});
+
+        expect(config.getOptionsToWatch()).toEqual(["option1"]);
+    });
+
+});
diff --git a/tools/src/component-generator.test.ts b/tools/src/component-generator.test.ts
index 81e08db..768b9dd 100644
--- a/tools/src/component-generator.test.ts
+++ b/tools/src/component-generator.test.ts
@@ -1,202 +1,299 @@
-import generate from "./component-generator";
+import generate, { renderProps } from "./component-generator";
 
 it("generates", () => {
     //#region EXPECTED
     const EXPECTED = `
 import * as VueType from "vue";
 const Vue = VueType.default || VueType;
-import CLASS_NAME from "devextreme/DX/WIDGET/PATH";
+import WIDGET from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import { DxComponent as BaseComponent } from "BASE_COMPONENT_PATH";
+import { BASE_COMPONENT as BaseComponent } from "BASE_COMPONENT_PATH";
 
-const DxCLASS_NAME: VueConstructor = Vue.extend({
+const COMPONENT: VueConstructor = Vue.extend({
   extends: BaseComponent,
   computed: {
-    instance(): CLASS_NAME {
+    instance(): WIDGET {
       return (this as any).$_instance;
     }
   },
   beforeCreate() {
-    (this as any).$_WidgetClass = CLASS_NAME;
+    (this as any).$_WidgetClass = WIDGET;
   }
 });
-export { DxCLASS_NAME };
+
+export {
+  COMPONENT
+};
 `.trimLeft();
     //#endregion
 
     expect(
         generate({
-            name: "CLASS_NAME",
-            baseComponentPath: "BASE_COMPONENT_PATH",
+            name: "COMPONENT",
+            widgetName: "WIDGET",
+            base: {
+                name: "BASE_COMPONENT",
+                path: "BASE_COMPONENT_PATH"
+            },
             dxExportPath: "DX/WIDGET/PATH"
         })
     ).toBe(EXPECTED);
 });
 
-it("generates extension component", () => {
-  //#region EXPECTED
-  const EXPECTED = `
+it("generates component with model", () => {
+    //#region EXPECTED
+    const EXPECTED = `
 import * as VueType from "vue";
 const Vue = VueType.default || VueType;
-import CLASS_NAME from "devextreme/DX/WIDGET/PATH";
+import WIDGET from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import { DxExtensionComponent as BaseComponent } from "BASE_COMPONENT_PATH";
+import { BASE_COMPONENT as BaseComponent } from "BASE_COMPONENT_PATH";
 
-const DxCLASS_NAME: VueConstructor = Vue.extend({
+const COMPONENT: VueConstructor = Vue.extend({
   extends: BaseComponent,
+  model: { prop: "value", event: "update:value" },
   computed: {
-    instance(): CLASS_NAME {
+    instance(): WIDGET {
       return (this as any).$_instance;
     }
   },
   beforeCreate() {
-    (this as any).$_WidgetClass = CLASS_NAME;
+    (this as any).$_WidgetClass = WIDGET;
   }
 });
-export { DxCLASS_NAME };
+
+export {
+  COMPONENT
+};
 `.trimLeft();
-  //#endregion
-
-  expect(
-      generate({
-          name: "CLASS_NAME",
-          baseComponentPath: "BASE_COMPONENT_PATH",
-          dxExportPath: "DX/WIDGET/PATH",
-          isExtension: true
-      })
-  ).toBe(EXPECTED);
-});
+    //#endregion
 
-describe("props generation", () => {
+    expect(
+        generate({
+            name: "COMPONENT",
+            widgetName: "WIDGET",
+            base: {
+                name: "BASE_COMPONENT",
+                path: "BASE_COMPONENT_PATH"
+            },
+            dxExportPath: "DX/WIDGET/PATH",
+            hasModel: true
+        })
+    ).toBe(EXPECTED);
+});
 
-    it("renders props in alphabetic order", () => {
-        //#region EXPECTED
-        const EXPECTED = `
+it("generates option", () => {
+    //#region EXPECTED
+    const EXPECTED = `
 import * as VueType from "vue";
 const Vue = VueType.default || VueType;
-import CLASS_NAME from "devextreme/DX/WIDGET/PATH";
+import WIDGET from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import { DxComponent as BaseComponent } from "BASE_COMPONENT_PATH";
+import { BASE_COMPONENT as BaseComponent } from "BASE_COMPONENT_PATH";
 
-const DxCLASS_NAME: VueConstructor = Vue.extend({
+const COMPONENT: VueConstructor = Vue.extend({
   extends: BaseComponent,
   props: {
-    --PROP: {},
-    a-PROP: {},
-    B-PROP: {},
-    b-PROP: {}
+    PROP: {}
   },
   computed: {
-    instance(): CLASS_NAME {
+    instance(): WIDGET {
       return (this as any).$_instance;
     }
   },
   beforeCreate() {
-    (this as any).$_WidgetClass = CLASS_NAME;
+    (this as any).$_WidgetClass = WIDGET;
   }
 });
-export { DxCLASS_NAME };
+
+export {
+  COMPONENT
+};
 `.trimLeft();
-        //#endregion
+    //#endregion
 
-        expect(
-            generate({
-                name: "CLASS_NAME",
-                baseComponentPath: "BASE_COMPONENT_PATH",
-                dxExportPath: "DX/WIDGET/PATH",
-                props: [{ name: "B-PROP" }, { name: "b-PROP" }, { name: "a-PROP" }, { name: "--PROP" }]
-            })
-        ).toBe(EXPECTED);
-    });
+    expect(
+        generate({
+            name: "COMPONENT",
+            widgetName: "WIDGET",
+            base: {
+                name: "BASE_COMPONENT",
+                path: "BASE_COMPONENT_PATH"
+            },
+            dxExportPath: "DX/WIDGET/PATH",
+            props: [{ name: "PROP" }]
+        })
+    ).toBe(EXPECTED);
+});
 
-    it("renders props without type", () => {
-        //#region EXPECTED
-        const EXPECTED = `
+it("generates nested option component", () => {
+    //#region EXPECTED
+    const EXPECTED = `
 import * as VueType from "vue";
 const Vue = VueType.default || VueType;
-import CLASS_NAME from "devextreme/DX/WIDGET/PATH";
+import WIDGET from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import { DxComponent as BaseComponent } from "BASE_COMPONENT_PATH";
+import { BASE_COMPONENT as BaseComponent, DxConfiguration } from "BASE_COMPONENT_PATH";
 
-const DxCLASS_NAME: VueConstructor = Vue.extend({
+const COMPONENT: VueConstructor = Vue.extend({
   extends: BaseComponent,
-  props: {
-    PROP1: {}
-  },
   computed: {
-    instance(): CLASS_NAME {
+    instance(): WIDGET {
       return (this as any).$_instance;
     }
   },
   beforeCreate() {
-    (this as any).$_WidgetClass = CLASS_NAME;
+    (this as any).$_WidgetClass = WIDGET;
   }
 });
-export { DxCLASS_NAME };
+
+const NESTED_COMPONENT = Vue.extend({
+  extends: DxConfiguration,
+  props: {
+    PROP: {}
+  },
+  beforeMount() {
+    (this as any).$_initOption("NESTED_OPTION_NAME");
+  }
+});
+
+export {
+  COMPONENT,
+  NESTED_COMPONENT
+};
 `.trimLeft();
         //#endregion
 
-        expect(
-            generate({
-                name: "CLASS_NAME",
-                baseComponentPath: "BASE_COMPONENT_PATH",
-                dxExportPath: "DX/WIDGET/PATH",
-                props: [{ name: "PROP1" }]
-            })
-        ).toBe(EXPECTED);
-    });
+    expect(
+        generate({
+            name: "COMPONENT",
+            widgetName: "WIDGET",
+            base: {
+                name: "BASE_COMPONENT",
+                path: "BASE_COMPONENT_PATH"
+            },
+            dxExportPath: "DX/WIDGET/PATH",
+            nestedComponents: [
+              {
+                name: "NESTED_COMPONENT",
+                optionName: "NESTED_OPTION_NAME",
+                props: [
+                  { name: "PROP" }
+                ],
+                isCollectionItem: false
+              }
+            ]
+        })
+    ).toBe(EXPECTED);
+});
 
-    it("renders props with type", () => {
-        //#region EXPECTED
-        const EXPECTED = `
+it("generates nested collection option component", () => {
+    //#region EXPECTED
+    const EXPECTED = `
 import * as VueType from "vue";
 const Vue = VueType.default || VueType;
-import CLASS_NAME from "devextreme/DX/WIDGET/PATH";
+import WIDGET from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import { DxComponent as BaseComponent } from "BASE_COMPONENT_PATH";
+import { BASE_COMPONENT as BaseComponent, DxConfiguration } from "BASE_COMPONENT_PATH";
 
-const DxCLASS_NAME: VueConstructor = Vue.extend({
+const COMPONENT: VueConstructor = Vue.extend({
   extends: BaseComponent,
-  props: {
-    PROP1: TYPE1,
-    PROP2: [TYPE2, TYPE3]
-  },
   computed: {
-    instance(): CLASS_NAME {
+    instance(): WIDGET {
       return (this as any).$_instance;
     }
   },
   beforeCreate() {
-    (this as any).$_WidgetClass = CLASS_NAME;
+    (this as any).$_WidgetClass = WIDGET;
   }
 });
-export { DxCLASS_NAME };
+
+const NESTED_COMPONENT = Vue.extend({
+  extends: DxConfiguration,
+  props: {
+    PROP: {}
+  },
+  beforeMount() {
+    (this as any).$_initCollectionOption("NESTED_OPTION_NAME");
+  }
+});
+
+export {
+  COMPONENT,
+  NESTED_COMPONENT
+};
 `.trimLeft();
         //#endregion
 
+    expect(
+        generate({
+            name: "COMPONENT",
+            widgetName: "WIDGET",
+            base: {
+                name: "BASE_COMPONENT",
+                path: "BASE_COMPONENT_PATH"
+            },
+            dxExportPath: "DX/WIDGET/PATH",
+            nestedComponents: [
+              {
+                name: "NESTED_COMPONENT",
+                optionName: "NESTED_OPTION_NAME",
+                props: [
+                  { name: "PROP" }
+                ],
+                isCollectionItem: true
+              }
+            ]
+        })
+    ).toBe(EXPECTED);
+});
+
+describe("props generation", () => {
+
+    it("generates props in alphabetic order", () => {
+        //#region EXPECTED
+        const EXPECTED =
+        `    --PROP: {},` + `\n` +
+        `    a-PROP: {},` + `\n` +
+        `    B-PROP: {},` + `\n` +
+        `    b-PROP: {}`;
+        //#endregion
+
+        expect(
+          renderProps([
+            { name: "B-PROP" },
+            { name: "b-PROP" },
+            { name: "a-PROP" },
+            { name: "--PROP" }
+          ])
+        ).toBe(EXPECTED);
+    });
+
+    it("renders props without type", () => {
+        const EXPECTED =
+        `    PROP1: {}`;
+
         expect(
-            generate({
-                name: "CLASS_NAME",
-                baseComponentPath: "BASE_COMPONENT_PATH",
-                dxExportPath: "DX/WIDGET/PATH",
-                props: [{ name: "PROP1", types: ["TYPE1"] }, { name: "PROP2", types: ["TYPE2", "TYPE3"] }]
-            })
+            renderProps([{ name: "PROP1" }])
         ).toBe(EXPECTED);
     });
 
+    it("renders props with type", () => {
+
+      const EXPECTED =
+        `    PROP1: TYPE1,` + `\n` +
+        `    PROP2: [TYPE2, TYPE3]`;
+
+      expect(
+          renderProps([{ name: "PROP1", types: ["TYPE1"] }, { name: "PROP2", types: ["TYPE2", "TYPE3"] }])
+      ).toBe(EXPECTED);
+
+    });
+
     it("renders props with acceptable values", () => {
         //#region EXPECTED
-        const EXPECTED = `
-import * as VueType from "vue";
-const Vue = VueType.default || VueType;
-import CLASS_NAME from "devextreme/DX/WIDGET/PATH";
-import { VueConstructor } from "vue";
-import { DxComponent as BaseComponent } from "BASE_COMPONENT_PATH";
-
-const DxCLASS_NAME: VueConstructor = Vue.extend({
-  extends: BaseComponent,
-  props: {
-    PROP1: {
+        const EXPECTED =
+`    PROP1: {
       type: TYPE1,
       validator: (v) => typeof(v) !== "string" || [
         "VAL1",
@@ -209,125 +306,47 @@ const DxCLASS_NAME: VueConstructor = Vue.extend({
         "VAL1",
         "VAL2"
       ].indexOf(v) !== -1
-    }
-  },
-  computed: {
-    instance(): CLASS_NAME {
-      return (this as any).$_instance;
-    }
-  },
-  beforeCreate() {
-    (this as any).$_WidgetClass = CLASS_NAME;
-  }
-});
-export { DxCLASS_NAME };
-`.trimLeft();
+    }`;
         //#endregion
 
         expect(
-            generate({
-                name: "CLASS_NAME",
-                baseComponentPath: "BASE_COMPONENT_PATH",
-                dxExportPath: "DX/WIDGET/PATH",
-                props: [
-                    {
-                        name: "PROP1",
-                        types: ["TYPE1"],
-                        acceptableValues: [`"VAL1"`, `"VAL2"`]
-                    },
-                    {
-                        name: "PROP2",
-                        types: ["TYPE2"],
-                        acceptableValues: [`"VAL1"`, `"VAL2"`],
-                        acceptableValueType: "TYPE3"
-                    }
-                ]
-            })
+            renderProps([
+                {
+                    name: "PROP1",
+                    types: ["TYPE1"],
+                    acceptableValues: [`"VAL1"`, `"VAL2"`]
+                },
+                {
+                    name: "PROP2",
+                    types: ["TYPE2"],
+                    acceptableValues: [`"VAL1"`, `"VAL2"`],
+                    acceptableValueType: "TYPE3"
+                }
+            ])
         ).toBe(EXPECTED);
     });
 
     it("renders props with array of acceptable values", () => {
         //#region EXPECTED
-        const EXPECTED = `
-import * as VueType from "vue";
-const Vue = VueType.default || VueType;
-import CLASS_NAME from "devextreme/DX/WIDGET/PATH";
-import { VueConstructor } from "vue";
-import { DxComponent as BaseComponent } from "BASE_COMPONENT_PATH";
-
-const DxCLASS_NAME: VueConstructor = Vue.extend({
-  extends: BaseComponent,
-  props: {
-    PROP1: {
+        const EXPECTED =
+`    PROP1: {
       type: TYPE1,
       validator: (v) => !Array.isArray(v) || v.filter(i => [
         "VAL1",
         "VAL2"
       ].indexOf(i) === -1).length === 0
-    }
-  },
-  computed: {
-    instance(): CLASS_NAME {
-      return (this as any).$_instance;
-    }
-  },
-  beforeCreate() {
-    (this as any).$_WidgetClass = CLASS_NAME;
-  }
-});
-export { DxCLASS_NAME };
-`.trimLeft();
+    }`;
         //#endregion
 
         expect(
-            generate({
-                name: "CLASS_NAME",
-                baseComponentPath: "BASE_COMPONENT_PATH",
-                dxExportPath: "DX/WIDGET/PATH",
-                props: [
-                    {
-                        name: "PROP1",
-                        types: ["TYPE1"],
-                        acceptableValues: [`"VAL1"`, `"VAL2"`],
-                        isArray: true
-                    }
-                ]
-            })
+            renderProps([
+                {
+                    name: "PROP1",
+                    types: ["TYPE1"],
+                    acceptableValues: [`"VAL1"`, `"VAL2"`],
+                    isArray: true
+                }
+            ])
         ).toBe(EXPECTED);
     });
 });
-
-it("generates class with model", () => {
-    //#region EXPECTED
-    const EXPECTED = `
-import * as VueType from "vue";
-const Vue = VueType.default || VueType;
-import CLASS_NAME from "devextreme/DX/WIDGET/PATH";
-import { VueConstructor } from "vue";
-import { DxComponent as BaseComponent } from "BASE_COMPONENT_PATH";
-
-const DxCLASS_NAME: VueConstructor = Vue.extend({
-  extends: BaseComponent,
-  model: { prop: "value", event: "update:value" },
-  computed: {
-    instance(): CLASS_NAME {
-      return (this as any).$_instance;
-    }
-  },
-  beforeCreate() {
-    (this as any).$_WidgetClass = CLASS_NAME;
-  }
-});
-export { DxCLASS_NAME };
-`.trimLeft();
-    //#endregion
-
-    expect(
-        generate({
-            name: "CLASS_NAME",
-            baseComponentPath: "BASE_COMPONENT_PATH",
-            dxExportPath: "DX/WIDGET/PATH",
-            hasModel: true
-        })
-    ).toBe(EXPECTED);
-});
diff --git a/tools/src/index-generator.test.ts b/tools/src/index-generator.test.ts
index 69b4692..a71b930 100644
--- a/tools/src/index-generator.test.ts
+++ b/tools/src/index-generator.test.ts
@@ -1,10 +1,15 @@
 import generate from "./index-generator";
 
 it("generates", () => {
-    expect(generate([ "./path", "./another/path" ])).toBe(EXPECTED_GENERATES);
+    expect(
+        generate([
+            { name: "widget", path: "./path" },
+            { name: "anotherWidget", path: "./another/path" },
+        ])
+    ).toBe(EXPECTED_GENERATES);
 });
 //#region EXPECTED_GENERATES
 const EXPECTED_GENERATES = `
-export * from "./path";
-export * from "./another/path";
+export { widget } from "./path";
+export { anotherWidget } from "./another/path";
 `.trimLeft();