Repository: DevExpress/devextreme-vue (ts)

Issue:
Unable to use the DevExtreme editors validation
The "Adapter for a DevExpressValidator component cannot be configured" error is thrown when Validator is assigned to an editor

Can be reproduced [here](https://codesandbox.io/s/18nr3311z3).

Pull request:
Add extension components
Fix #78 

Patch:
diff --git a/example/components/text-box-example.vue b/example/components/text-box-example.vue
index 9e8c73d..40c696f 100644
--- a/example/components/text-box-example.vue
+++ b/example/components/text-box-example.vue
@@ -44,22 +44,34 @@
         valueChangeEvent="input"
       />
 
+      <h5 class="text-box-label">Validation (Required)</h5>
+      <dx-text-box value="Required text">
+        <dx-validator :validationRules="rules" />
+      </dx-text-box>
+
   </example-block>
 </template>
 
 <script>
 import ExampleBlock from "./example-block";
 
-import { DxTextBox } from "../../src";
+import { DxTextBox, DxValidator } from "../../src";
 
 export default {
   components: {
     ExampleBlock,
-    DxTextBox
+    DxTextBox, 
+    DxValidator
   },
   data: function() {
     return {
-      text: "text"
+      text: "text",
+      rules: [
+        {
+          type: 'required',
+          message: "this is required"
+        }
+      ]      
     };
   }
 };
diff --git a/src/core/component.ts b/src/core/component.ts
index bb3bcbe..0523d13 100644
--- a/src/core/component.ts
+++ b/src/core/component.ts
@@ -9,33 +9,34 @@ const DX_TEMPLATE_WRAPPER_CLASS = "dx-template-wrapper";
 const DX_REMOVE_EVENT = "dxremove";
 
 const Vue = VueType.default || VueType;
-const DxComponent: VueConstructor = Vue.extend({
+const BaseComponent: VueConstructor = Vue.extend({
 
     render(createElement: (...args) => VNode): VNode {
         return createElement("div", this.$slots.default);
     },
 
-    mounted(): void {
-        const options: object = {
-            ...this.$_getIntegrationOptions(),
-            ...this.$options.propsData
-        };
-
-        const instance = new (this as any).$_WidgetClass(this.$el, options);
-        (this as any).$_instance = instance;
-
-        instance.on("optionChanged", this.$_handleOptionChanged.bind(this));
-        this.$_watchProps(instance);
-        this.$_createEmitters(instance);
-    },
-
     beforeDestroy(): void {
-        events.triggerHandler(this.$el, DX_REMOVE_EVENT);
-        (this as any).$_instance.dispose();
+        const instance = (this as any).$_instance;
+        if (instance) {
+            events.triggerHandler(this.$el, DX_REMOVE_EVENT);
+            instance.dispose();
+        }
     },
 
     methods: {
+        $_createWidget(element: any): void {
+            const options: object = {
+                ...this.$_getIntegrationOptions(),
+                ...this.$options.propsData
+            };
+
+            const instance = new (this as any).$_WidgetClass(element, options);
+            (this as any).$_instance = instance;
 
+            instance.on("optionChanged", this.$_handleOptionChanged.bind(this));
+            this.$_watchProps(instance);
+            this.$_createEmitters(instance);
+        },
         $_getIntegrationOptions(): object {
             if (!this.$scopedSlots || !Object.keys(this.$scopedSlots).length) {
                 return {};
@@ -103,4 +104,26 @@ const DxComponent: VueConstructor = Vue.extend({
     }
 });
 
-export default DxComponent;
+const DxComponent: VueConstructor = BaseComponent.extend({
+    mounted(): void {
+        (this as any).$_createWidget(this.$el);
+        this.$children.forEach((child: any) => {
+            if (child.$_isExtension) {
+                child.attachTo(this.$el);
+            }
+        });
+    }
+});
+
+const DxExtensionComponent: VueConstructor = BaseComponent.extend({
+    created(): void {
+        (this as any).$_isExtension = true;
+    },
+    methods: {
+        attachTo(element: any) {
+            (this as any).$_createWidget(element);
+        }
+    }
+});
+
+export { DxComponent, DxExtensionComponent };
diff --git a/tools/src/component-generator.ts b/tools/src/component-generator.ts
index f60c9c9..718bf41 100644
--- a/tools/src/component-generator.ts
+++ b/tools/src/component-generator.ts
@@ -7,11 +7,13 @@ interface IComponent {
     dxExportPath: string;
     props?: IProp[];
     hasModel?: boolean;
+    isExtension?: boolean;
 }
 
 interface IComponentModel {
     name: string;
     baseComponentPath: string;
+    baseComponentName: string;
     dxExportPath: string;
     renderedProps?: string[];
     hasModel?: boolean;
@@ -32,7 +34,8 @@ function generate(component: IComponent): string {
         renderedProps: component.props
             ? component.props.sort(createKeyComparator<IProp>((p) => p.name)).map(createPropModel)
             : undefined,
-        widgetName: `${uppercaseFirst(component.name)}`
+        widgetName: `${uppercaseFirst(component.name)}`,
+        baseComponentName: component.isExtension ? "DxExtensionComponent" : "DxComponent"
     };
 
     return renderComponent(componentModel);
@@ -49,7 +52,7 @@ import * as VueType from "vue";
 const Vue = VueType.default || VueType;
 import <#= it.widgetName #> from "devextreme/<#= it.dxExportPath #>";
 import { VueConstructor } from "vue";
-import BaseComponent from "<#= it.baseComponentPath #>";
+import { <#= it.baseComponentName #> as BaseComponent } from "<#= it.baseComponentPath #>";
 
 const Dx<#= it.name #>: VueConstructor = Vue.extend({
   extends: BaseComponent,<#? it.props #>
diff --git a/tools/src/generator.ts b/tools/src/generator.ts
index 22400cc..ea3f10e 100644
--- a/tools/src/generator.ts
+++ b/tools/src/generator.ts
@@ -42,6 +42,7 @@ function mapWidget(raw: IWidget, baseComponent: string): { fileName: string, com
       dxExportPath: raw.exportPath,
       props: raw.options.map(mapProp),
       hasModel: !!raw.isEditor,
+      isExtension: !!raw.isExtension,
     }
   };
 }


Test patch:
diff --git a/src/core/component.test.ts b/src/core/component.test.ts
index b208d77..e9e16bc 100644
--- a/src/core/component.test.ts
+++ b/src/core/component.test.ts
@@ -1,5 +1,5 @@
 import Vue from "vue";
-import BaseComponent from "../core/component";
+import { DxComponent, DxExtensionComponent } from "../core/component";
 
 import * as events from "devextreme/events";
 
@@ -20,7 +20,7 @@ const Widget = {
 
 const WidgetClass = jest.fn(() => Widget);
 const TestComponent = Vue.extend({
-    extends: BaseComponent,
+    extends: DxComponent,
     beforeCreate() {
         (this as any).$_WidgetClass = WidgetClass;
     }
@@ -245,6 +245,43 @@ describe("events emitting", () => {
     });
 });
 
+describe("extension component", () => {
+    const ExtensionWidgetClass = jest.fn(() => Widget);
+    const TestExtensionComponent = Vue.extend({
+        extends: DxExtensionComponent,
+        beforeCreate() {
+            (this as any).$_WidgetClass = ExtensionWidgetClass;
+        }
+    });
+
+    it("doesn't render", () => {
+        new TestExtensionComponent().$mount();
+
+        expect(ExtensionWidgetClass).toHaveBeenCalledTimes(0);
+    });
+
+    it("destroys correctly", () => {
+        const component = new TestExtensionComponent().$mount();
+
+        expect(component.$destroy.bind(component)).not.toThrow();
+    });
+
+    it("renders inside component on parent element", () => {
+        new Vue({
+            template: `<test-component>
+                            <test-extension-component/>
+                        </test-component>`,
+            components: {
+                TestComponent,
+                TestExtensionComponent
+            }
+        }).$mount();
+
+        expect(ExtensionWidgetClass).toHaveBeenCalledTimes(1);
+        expect(ExtensionWidgetClass.mock.calls[0][0]).toBe(WidgetClass.mock.calls[0][0]);
+    });
+});
+
 describe("disposing", () => {
 
     it("call dispose", () => {
@@ -264,4 +301,10 @@ describe("disposing", () => {
 
         expect(handleDxRemove).toHaveBeenCalledTimes(1);
     });
+
+    it("destroys correctly", () => {
+        const component = new TestComponent();
+
+        expect(component.$destroy.bind(component)).not.toThrow();
+    });
 });
diff --git a/tools/src/component-generator.test.ts b/tools/src/component-generator.test.ts
index bb3759f..81e08db 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 CLASS_NAME from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import BaseComponent from "BASE_COMPONENT_PATH";
+import { DxComponent as BaseComponent } from "BASE_COMPONENT_PATH";
 
 const DxCLASS_NAME: VueConstructor = Vue.extend({
   extends: BaseComponent,
@@ -33,6 +33,40 @@ export { DxCLASS_NAME };
     ).toBe(EXPECTED);
 });
 
+it("generates extension component", () => {
+  //#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 { DxExtensionComponent as BaseComponent } from "BASE_COMPONENT_PATH";
+
+const DxCLASS_NAME: VueConstructor = Vue.extend({
+  extends: BaseComponent,
+  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",
+          isExtension: true
+      })
+  ).toBe(EXPECTED);
+});
+
 describe("props generation", () => {
 
     it("renders props in alphabetic order", () => {
@@ -42,7 +76,7 @@ import * as VueType from "vue";
 const Vue = VueType.default || VueType;
 import CLASS_NAME from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import BaseComponent from "BASE_COMPONENT_PATH";
+import { DxComponent as BaseComponent } from "BASE_COMPONENT_PATH";
 
 const DxCLASS_NAME: VueConstructor = Vue.extend({
   extends: BaseComponent,
@@ -82,7 +116,7 @@ import * as VueType from "vue";
 const Vue = VueType.default || VueType;
 import CLASS_NAME from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import BaseComponent from "BASE_COMPONENT_PATH";
+import { DxComponent as BaseComponent } from "BASE_COMPONENT_PATH";
 
 const DxCLASS_NAME: VueConstructor = Vue.extend({
   extends: BaseComponent,
@@ -119,7 +153,7 @@ import * as VueType from "vue";
 const Vue = VueType.default || VueType;
 import CLASS_NAME from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import BaseComponent from "BASE_COMPONENT_PATH";
+import { DxComponent as BaseComponent } from "BASE_COMPONENT_PATH";
 
 const DxCLASS_NAME: VueConstructor = Vue.extend({
   extends: BaseComponent,
@@ -157,7 +191,7 @@ import * as VueType from "vue";
 const Vue = VueType.default || VueType;
 import CLASS_NAME from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import BaseComponent from "BASE_COMPONENT_PATH";
+import { DxComponent as BaseComponent } from "BASE_COMPONENT_PATH";
 
 const DxCLASS_NAME: VueConstructor = Vue.extend({
   extends: BaseComponent,
@@ -219,7 +253,7 @@ import * as VueType from "vue";
 const Vue = VueType.default || VueType;
 import CLASS_NAME from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import BaseComponent from "BASE_COMPONENT_PATH";
+import { DxComponent as BaseComponent } from "BASE_COMPONENT_PATH";
 
 const DxCLASS_NAME: VueConstructor = Vue.extend({
   extends: BaseComponent,
@@ -270,7 +304,7 @@ import * as VueType from "vue";
 const Vue = VueType.default || VueType;
 import CLASS_NAME from "devextreme/DX/WIDGET/PATH";
 import { VueConstructor } from "vue";
-import BaseComponent from "BASE_COMPONENT_PATH";
+import { DxComponent as BaseComponent } from "BASE_COMPONENT_PATH";
 
 const DxCLASS_NAME: VueConstructor = Vue.extend({
   extends: BaseComponent,