Repository: DevExpress/devextreme-vue
Pull request #127 (2018-07-18 11:55:27)

Extract configuration and extension components


Patch:
diff --git a/build.config.js b/build.config.js
index bf479b8..60f55ff 100644
--- a/build.config.js
+++ b/build.config.js
@@ -19,5 +19,7 @@ module.exports = {
   metadataPath: './tools/integration-data.json',
   componentFolder: './src/ui',
   indexFileName: './src/index.ts',
-  baseComponent: '../core/component'
+  baseComponent: '../core/component',
+  configComponent: '../core/configuration-component',
+  extensionComponent: '../core/extension-component'
 };
\ No newline at end of file
diff --git a/example/components/grid-example.vue b/example/components/grid-example.vue
index 9c9ab0f..7c83fb1 100644
--- a/example/components/grid-example.vue
+++ b/example/components/grid-example.vue
@@ -113,7 +113,6 @@ export default {
   },
   computed: {
     countrySortOrder() {
-      console.log(this.countrySortOrderVal);
       return this.countrySortOrderVal ? "asc" : "desc";
     }
   },
diff --git a/gulpfile.js b/gulpfile.js
index 07a45fc..59419e5 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -69,6 +69,8 @@ gulp.task(GEN_RUN, (done) => {
   generateSync(
     JSON.parse(fs.readFileSync(config.metadataPath).toString()),
     config.baseComponent,
+    config.configComponent,
+    config.extensionComponent,
     {
       componentsDir: config.componentFolder,
       indexFileName: config.indexFileName
diff --git a/src/core/component.ts b/src/core/component.ts
index 89be628..20afb32 100644
--- a/src/core/component.ts
+++ b/src/core/component.ts
@@ -128,43 +128,4 @@ const DxComponent: VueConstructor = BaseComponent.extend({
     }
 });
 
-const DxExtensionComponent: VueConstructor = BaseComponent.extend({
-    created(): void {
-        (this as any).$_isExtension = true;
-    },
-
-    methods: {
-        attachTo(element: any) {
-            (this as any).$_createWidget(element);
-        }
-    }
-});
-
-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 };
+export { DxComponent, BaseComponent };
diff --git a/src/core/configuration-component.ts b/src/core/configuration-component.ts
new file mode 100644
index 0000000..6a50840
--- /dev/null
+++ b/src/core/configuration-component.ts
@@ -0,0 +1,35 @@
+import * as VueType from "vue";
+import { VNode, VueConstructor } from "vue";
+
+const Vue = VueType.default || VueType;
+
+import { bindOptionWatchers, IConfigurable } from "./configuration";
+
+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 { DxConfiguration };
diff --git a/src/core/extension-component.ts b/src/core/extension-component.ts
new file mode 100644
index 0000000..c96bfc4
--- /dev/null
+++ b/src/core/extension-component.ts
@@ -0,0 +1,17 @@
+import { VueConstructor } from "vue";
+import { BaseComponent } from "./component";
+
+const DxExtensionComponent: VueConstructor = BaseComponent.extend({
+
+    created(): void {
+        (this as any).$_isExtension = true;
+    },
+
+    methods: {
+        attachTo(element: any) {
+            (this as any).$_createWidget(element);
+        }
+    }
+});
+
+export { DxExtensionComponent };
diff --git a/tools/src/component-generator.ts b/tools/src/component-generator.ts
index c586968..a46931a 100644
--- a/tools/src/component-generator.ts
+++ b/tools/src/component-generator.ts
@@ -1,30 +1,27 @@
 import { compareStrings } from "./helpers";
 import createTempate from "./template";
 
-interface INamedImport {
+interface IImport {
     name: string;
     path: string;
 }
 
 interface IComponent {
     name: string;
-    widget: INamedImport;
-    base: INamedImport;
+    widgetComponent: IImport;
+    baseComponent: IImport;
+    configComponent: IImport;
     props?: IProp[];
     hasModel?: boolean;
     nestedComponents?: INestedComponent[];
 }
 
-interface IFileImport {
-    path: string;
-    namedImports: string[];
-}
-
 interface IComponentModel {
     component: string;
+    widgetImport: IImport;
+    namedImports: IImport[];
     baseComponent: string;
-    baseImport: IFileImport;
-    widgetImport: INamedImport;
+    configComponent: string;
     renderedProps?: string;
     hasModel?: boolean;
     nestedComponents?: INestedComponentModel[];
@@ -58,24 +55,29 @@ function generate(component: IComponent): string {
         ? component.nestedComponents.map(createNestedComponentModel)
         : undefined;
 
-    const namedExports: string[] = [ component.name ];
-    const baseImport: IFileImport = {
-        path: component.base.path,
-        namedImports: [ component.base.name ]
-    };
+    const namedExports: string[] = [component.name];
+    const namedImports = [
+        {
+            name: "VueConstructor",
+            path: "vue"
+        },
+        component.baseComponent
+    ];
 
     if (nestedComponents && nestedComponents.length) {
         namedExports.push(...nestedComponents.map((c) => c.name));
-        baseImport.namedImports.push("DxConfiguration");
+        namedImports.push(component.configComponent);
     }
-    baseImport.namedImports = baseImport.namedImports.sort();
 
+    namedImports.sort(compareImports);
     const componentModel = {
         ...component,
-        widgetImport: component.widget,
-        baseImport,
+
+        namedImports,
+        widgetImport: component.widgetComponent,
         component: component.name,
-        baseComponent: component.base.name,
+        baseComponent: component.baseComponent.name,
+        configComponent: component.configComponent.name,
 
         renderedProps: component.props
             ? renderProps(component.props)
@@ -107,23 +109,19 @@ 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.widgetImport.name #> from "devextreme/<#= it.widgetImport.path #>";
-import { VueConstructor } from "vue";
-import { ` +
-
-`<#~ it.baseImport.namedImports :namedImport #>` +
-    `<#= namedImport #>` + `, ` +
-`<#~#>` + `\b\b` +
+`import * as VueType from "vue";\n` +
+`const Vue = VueType.default || VueType;\n` +
+`import <#= it.widgetImport.name #> from "devextreme/<#= it.widgetImport.path #>";\n` +
 
-` } from "<#= it.baseImport.path #>";` + `\n` + `\n` +
+`<#~ it.namedImports :namedImport #>` +
+`import { <#= namedImport.name #> } from "<#= namedImport.path #>";\n` +
+`<#~#>` + `\n` +
 
 `const <#= it.component #>: VueConstructor = Vue.extend({` +
 L1 + `extends: <#= it.baseComponent #>,` +
 
 `<#? it.props #>` +
-    L1 + `props: {` + `\n` +
+    L1 + `props: {\n` +
     `<#= it.renderedProps #>` +
     L1 + `},` +
 `<#?#>` +
@@ -141,14 +139,14 @@ L1 + `extends: <#= it.baseComponent #>,` +
   L1 + `beforeCreate() {
     (this as any).$_WidgetClass = <#= it.widgetImport.name #>;
   }
-});` + `\n` +
+});\n` +
 
 `<#? it.nestedComponents #>` +
     `\n` +
     `<#~ it.nestedComponents : nested #>` +
         `const <#= nested.name #> = Vue.extend({` +
-        L1 + `extends: DxConfiguration,` +
-        L1 + `props: {` + `\n` +
+        L1 + `extends: <#= it.configComponent #>,` +
+        L1 + `props: {\n` +
         `<#= nested.renderedProps #>` +
         L1 + `},` +
         L1 + `beforeMount() {` +
@@ -161,23 +159,33 @@ L1 + `extends: <#= it.baseComponent #>,` +
         `<#?#>` +
 
         `("<#= nested.optionName #>");` +
-        L1 + `}` + `\n` +
-        `});` + `\n` +
+        L1 + `}\n` +
+        `});\n` +
     `<#~#>` +
 `<#?#>` +
 
 `\n` +
-`export {` + `\n` +
+`export {\n` +
     `<#~ it.namedExports :namedExport #>` +
-    tab(1) + `<#= namedExport #>,` + `\n` +
-    `<#~#>` + `\b` + `\b` + `\n` +
-`};` + `\n`
+    tab(1) + `<#= namedExport #>,\n` +
+    `<#~#>` + `\b\b` + `\n` +
+`};\n`
 );
 
+function compareProps(a: IProp, b: IProp): number {
+    return compareStrings(a.name, b.name);
+}
+function compareImports(a: IImport, b: IImport): number {
+    if (a.path.startsWith(".") && !b.path.startsWith(".")) { return 1; }
+
+    if (!a.path.startsWith(".") && b.path.startsWith(".")) { return -1; }
+
+    return compareStrings(a.path, b.path);
+}
+
 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 #>` +
 
@@ -244,9 +252,9 @@ const renderPropsTemplate: (props: IProp[]) => string = createTempate(
 
          `}` +
 
-    `<#?#>,` + `\n` +
+    `<#?#>,\n` +
 
-`<#~#>` + `\b` + `\b`
+`<#~#>` + `\b\b`
 );
 
 function tab(i: number): string {
diff --git a/tools/src/generator.ts b/tools/src/generator.ts
index 11519d2..b9a996a 100644
--- a/tools/src/generator.ts
+++ b/tools/src/generator.ts
@@ -10,6 +10,8 @@ import generateIndex, { IReExport } from "./index-generator";
 function generate(
   rawData: IModel,
   baseComponentPath: string,
+  configComponentPath: string,
+  extensionComponentPath: string,
   out: {
     componentsDir: string,
     indexFileName: string
@@ -18,7 +20,13 @@ function generate(
   const modulePaths: IReExport[] = [];
 
   rawData.widgets.forEach((data) => {
-    const widgetFile = mapWidget(data, baseComponentPath, rawData.customTypes);
+    const widgetFile = mapWidget(
+      data,
+      baseComponentPath,
+      configComponentPath,
+      extensionComponentPath,
+      rawData.customTypes
+    );
     const widgetFilePath = joinPaths(out.componentsDir, widgetFile.fileName);
     const indexFileDir = getDirName(out.indexFileName);
 
@@ -32,7 +40,13 @@ function generate(
   writeFile(out.indexFileName, generateIndex(modulePaths), { encoding: "utf8" });
 }
 
-function mapWidget(raw: IWidget, baseComponentPath: string, customTypes: ICustomType[]): {
+function mapWidget(
+  raw: IWidget,
+  baseComponentPath: string,
+  configComponentPath: string,
+  extensionComponentPath: string,
+  customTypes: ICustomType[]
+): {
   fileName: string,
   component: IComponent
 } {
@@ -47,13 +61,17 @@ function mapWidget(raw: IWidget, baseComponentPath: string, customTypes: ICustom
     fileName: `${toKebabCase(name)}.ts`,
     component: {
       name: `Dx${name}`,
-      widget: {
+      widgetComponent: {
         name,
         path: raw.exportPath
       },
-      base: {
+      baseComponent: {
         name: raw.isExtension ? "DxExtensionComponent" : "DxComponent",
-        path: baseComponentPath
+        path: raw.isExtension ? extensionComponentPath : baseComponentPath,
+      },
+      configComponent: {
+        name: "DxConfiguration",
+        path: configComponentPath
       },
       props: raw.options.map((o) => mapProp(o, customTypeHash)),
       hasModel: !!raw.isEditor,


Test patch:
diff --git a/src/core/component.test.ts b/src/core/component.test.ts
index 4ed24b7..0ce389f 100644
--- a/src/core/component.test.ts
+++ b/src/core/component.test.ts
@@ -1,5 +1,7 @@
 import Vue from "vue";
-import { DxComponent, DxConfiguration, DxExtensionComponent } from "../core/component";
+import { DxComponent } from "../core/component";
+import { DxConfiguration } from "../core/configuration-component";
+import { DxExtensionComponent } from "../core/extension-component";
 
 import * as events from "devextreme/events";
 
diff --git a/tools/src/component-generator.test.ts b/tools/src/component-generator.test.ts
index 11bee35..2fbe427 100644
--- a/tools/src/component-generator.test.ts
+++ b/tools/src/component-generator.test.ts
@@ -7,7 +7,7 @@ import * as VueType from "vue";
 const Vue = VueType.default || VueType;
 import WIDGET from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import { BASE_COMPONENT } from "BASE_COMPONENT_PATH";
+import { BASE_COMPONENT } from "./BASE_COMPONENT_PATH";
 
 const COMPONENT: VueConstructor = Vue.extend({
   extends: BASE_COMPONENT,
@@ -30,13 +30,17 @@ export {
     expect(
         generate({
             name: "COMPONENT",
-            widget: {
+            widgetComponent: {
               name: "WIDGET",
               path: "DX/WIDGET/PATH"
             },
-            base: {
+            configComponent: {
+                name: "CONFIG_COMPONENT",
+                path: "./CONFIG_COMPONENT_PATH"
+            },
+            baseComponent: {
                 name: "BASE_COMPONENT",
-                path: "BASE_COMPONENT_PATH"
+                path: "./BASE_COMPONENT_PATH"
             }
         })
     ).toBe(EXPECTED);
@@ -49,7 +53,7 @@ import * as VueType from "vue";
 const Vue = VueType.default || VueType;
 import WIDGET from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import { BASE_COMPONENT } from "BASE_COMPONENT_PATH";
+import { BASE_COMPONENT } from "./BASE_COMPONENT_PATH";
 
 const COMPONENT: VueConstructor = Vue.extend({
   extends: BASE_COMPONENT,
@@ -73,13 +77,17 @@ export {
     expect(
         generate({
             name: "COMPONENT",
-            widget: {
+            widgetComponent: {
               name: "WIDGET",
               path: "DX/WIDGET/PATH"
             },
-            base: {
+            baseComponent: {
                 name: "BASE_COMPONENT",
-                path: "BASE_COMPONENT_PATH"
+                path: "./BASE_COMPONENT_PATH"
+            },
+            configComponent: {
+                name: "CONFIG_COMPONENT",
+                path: "./CONFIG_COMPONENT_PATH"
             },
             hasModel: true
         })
@@ -93,7 +101,7 @@ import * as VueType from "vue";
 const Vue = VueType.default || VueType;
 import WIDGET from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import { BASE_COMPONENT } from "BASE_COMPONENT_PATH";
+import { BASE_COMPONENT } from "./BASE_COMPONENT_PATH";
 
 const COMPONENT: VueConstructor = Vue.extend({
   extends: BASE_COMPONENT,
@@ -119,13 +127,17 @@ export {
     expect(
         generate({
             name: "COMPONENT",
-            widget: {
+            widgetComponent: {
               name: "WIDGET",
               path: "DX/WIDGET/PATH"
             },
-            base: {
+            baseComponent: {
                 name: "BASE_COMPONENT",
-                path: "BASE_COMPONENT_PATH"
+                path: "./BASE_COMPONENT_PATH"
+            },
+            configComponent: {
+                name: "CONFIG_COMPONENT",
+                path: "./CONFIG_COMPONENT_PATH"
             },
             props: [{ name: "PROP" }]
         })
@@ -139,7 +151,8 @@ import * as VueType from "vue";
 const Vue = VueType.default || VueType;
 import WIDGET from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import { BASE_COMPONENT, DxConfiguration } from "BASE_COMPONENT_PATH";
+import { BASE_COMPONENT } from "./BASE_COMPONENT_PATH";
+import { CONFIG_COMPONENT } from "./CONFIG_COMPONENT_PATH";
 
 const COMPONENT: VueConstructor = Vue.extend({
   extends: BASE_COMPONENT,
@@ -154,7 +167,7 @@ const COMPONENT: VueConstructor = Vue.extend({
 });
 
 const NESTED_COMPONENT = Vue.extend({
-  extends: DxConfiguration,
+  extends: CONFIG_COMPONENT,
   props: {
     PROP: {}
   },
@@ -173,13 +186,17 @@ export {
     expect(
         generate({
             name: "COMPONENT",
-            widget: {
+            widgetComponent: {
               name: "WIDGET",
               path: "DX/WIDGET/PATH"
             },
-            base: {
+            baseComponent: {
                 name: "BASE_COMPONENT",
-                path: "BASE_COMPONENT_PATH"
+                path: "./BASE_COMPONENT_PATH"
+            },
+            configComponent: {
+                name: "CONFIG_COMPONENT",
+                path: "./CONFIG_COMPONENT_PATH"
             },
             nestedComponents: [
               {
@@ -202,7 +219,8 @@ import * as VueType from "vue";
 const Vue = VueType.default || VueType;
 import WIDGET from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import { BASE_COMPONENT, DxConfiguration } from "BASE_COMPONENT_PATH";
+import { BASE_COMPONENT } from "./BASE_COMPONENT_PATH";
+import { CONFIG_COMPONENT } from "./CONFIG_COMPONENT_PATH";
 
 const COMPONENT: VueConstructor = Vue.extend({
   extends: BASE_COMPONENT,
@@ -217,7 +235,7 @@ const COMPONENT: VueConstructor = Vue.extend({
 });
 
 const NESTED_COMPONENT = Vue.extend({
-  extends: DxConfiguration,
+  extends: CONFIG_COMPONENT,
   props: {
     PROP: {}
   },
@@ -236,13 +254,17 @@ export {
     expect(
         generate({
             name: "COMPONENT",
-            widget: {
+            widgetComponent: {
               name: "WIDGET",
               path: "DX/WIDGET/PATH"
             },
-            base: {
+            baseComponent: {
                 name: "BASE_COMPONENT",
-                path: "BASE_COMPONENT_PATH"
+                path: "./BASE_COMPONENT_PATH"
+            },
+            configComponent: {
+                name: "CONFIG_COMPONENT",
+                path: "./CONFIG_COMPONENT_PATH"
             },
             nestedComponents: [
               {