Repository: DevExpress/devextreme-vue
Pull request #131 (2018-07-20 17:22:30)

 Fix deep nesting for configuration components


Patch:
diff --git a/src/core/component.ts b/src/core/component.ts
index 20afb32..43479c1 100644
--- a/src/core/component.ts
+++ b/src/core/component.ts
@@ -3,7 +3,8 @@ import { VNode, VueConstructor } from "vue";
 
 import * as events from "devextreme/events";
 
-import Configuration, { bindOptionWatchers, IConfigurable } from "./configuration";
+import Configuration, { bindOptionWatchers } from "./configuration";
+import { IConfigurable, IConfigurationComponent } from "./configuration-component";
 import { camelize } from "./helpers";
 
 interface IWidgetComponent extends IConfigurable {
@@ -18,7 +19,10 @@ const DX_REMOVE_EVENT = "dxremove";
 const BaseComponent: VueConstructor = Vue.extend({
 
     render(createElement: (...args) => VNode): VNode {
-        return createElement("div", this.$slots.default);
+        return createElement(
+            "div",
+            extractChildren(this.$slots.default, (this as any as IWidgetComponent).$_config)
+        );
     },
 
     beforeDestroy(): void {
@@ -30,28 +34,29 @@ const BaseComponent: VueConstructor = Vue.extend({
     },
 
     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 }
         );
+
+        (this as any as IWidgetComponent).$_config.init(this.$props && Object.keys(this.$props));
     },
 
     methods: {
         $_createWidget(element: any): void {
-            const widgetComponent = this as any as IWidgetComponent;
             const options: object = {
                 ...this.$_getIntegrationOptions(),
                 ...this.$options.propsData,
-                ...widgetComponent.$_config.getInitialValues()
+                ...(this as any as IWidgetComponent).$_config.getInitialValues()
             };
 
             const instance = new (this as any).$_WidgetClass(element, options);
             (this as any as IWidgetComponent).$_instance = instance;
 
             instance.on("optionChanged", this.$_handleOptionChanged.bind(this));
-            bindOptionWatchers(widgetComponent.$_config, this);
+            bindOptionWatchers((this as any as IWidgetComponent).$_config, this);
             this.$_createEmitters(instance);
         },
         $_getIntegrationOptions(): object {
@@ -128,4 +133,38 @@ const DxComponent: VueConstructor = BaseComponent.extend({
     }
 });
 
-export { DxComponent, BaseComponent };
+function extractChildren(children: VNode[], config: Configuration): VNode[] {
+    if (!children || children.length === 0) { return children; }
+
+    const nodes: VNode[] = [];
+    pullConfigurations(children, nodes, config);
+
+    return nodes;
+}
+
+function pullConfigurations(children: VNode[], nodes: VNode[], ownerConfig: Configuration): void {
+
+    children.forEach((node) => {
+        nodes.push(node);
+
+        if (
+            node.componentOptions &&
+            (node.componentOptions.Ctor as any as IConfigurationComponent).$_optionName
+        ) {
+            const initialValues = { ...node.componentOptions.propsData };
+            const config = ownerConfig.createNested(
+                (node.componentOptions.Ctor as any as IConfigurationComponent).$_optionName,
+                initialValues,
+                (node.componentOptions.Ctor as any as IConfigurationComponent).$_isCollectionItem
+            );
+
+            (node.componentOptions as any as IConfigurable).$_config = config;
+
+            if (node.componentOptions.children) {
+                pullConfigurations(node.componentOptions.children as VNode[], nodes, config);
+            }
+        }
+    });
+}
+
+export { DxComponent, BaseComponent, IWidgetComponent };
diff --git a/src/core/configuration-component.ts b/src/core/configuration-component.ts
index 6a50840..f5355b7 100644
--- a/src/core/configuration-component.ts
+++ b/src/core/configuration-component.ts
@@ -3,33 +3,27 @@ import { VNode, VueConstructor } from "vue";
 
 const Vue = VueType.default || VueType;
 
-import { bindOptionWatchers, IConfigurable } from "./configuration";
+import Configuration, { bindOptionWatchers } from "./configuration";
+
+interface IConfigurationComponent {
+    $_optionName: string;
+    $_isCollectionItem: boolean;
+}
+
+interface IConfigurable {
+    $_config: Configuration;
+}
 
 const DxConfiguration: VueConstructor = Vue.extend({
 
-    render(createElement: (...args) => VNode): VNode {
-        return createElement();
+    beforeMount() {
+        (this.$vnode.componentOptions as any as IConfigurable).$_config.init(Object.keys(this.$props));
+        bindOptionWatchers((this.$vnode.componentOptions as any as IConfigurable).$_config, this);
     },
 
-    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);
-        }
+    render(createElement: (...args) => VNode): VNode {
+        return createElement();
     }
 });
 
-export { DxConfiguration };
+export { DxConfiguration, IConfigurable, IConfigurationComponent };
diff --git a/src/core/configuration.ts b/src/core/configuration.ts
index dfd8e68..d5d02c1 100644
--- a/src/core/configuration.ts
+++ b/src/core/configuration.ts
@@ -2,31 +2,26 @@ 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;
 
+    private _options: string[];
+
     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;
@@ -35,9 +30,40 @@ class Configuration {
         this.updateValue = this.updateValue.bind(this);
     }
 
+    public get name(): string | null {
+        return this._name;
+    }
+
+    public get options(): string[] {
+        return this._options;
+    }
+
+    public get initialValues(): Record<string, any> {
+        return this._initialValues;
+    }
+
+    public get nested(): Configuration[] {
+        return this._nestedConfigurations;
+    }
+
+    public get collectionItemIndex(): number | undefined {
+        return this._collectionItemIndex;
+    }
+
+    public get isCollectionItem(): boolean {
+        return this._isCollectionItem;
+    }
+
+    public get updateFunc(): UpdateFunc {
+        return this._updateFunc;
+    }
+
+    public init(options: string[]): void {
+        this._options = options ? options : [];
+    }
+
     public createNested(
         name: string,
-        options: string[],
         initialValues: Record<string, any>,
         isCollectionItem?: boolean
     ): Configuration {
@@ -49,7 +75,6 @@ class Configuration {
         const configuration = new Configuration(
             this.updateValue,
             name,
-            options,
             initialValues,
             isCollectionItem,
             collectionItemIndex
@@ -101,7 +126,7 @@ class Configuration {
     }
 }
 
-function bindOptionWatchers(config: Configuration, vueInstance: Pick<Vue, "$watch" | "$props">): void {
+function bindOptionWatchers(config: Configuration, vueInstance: Pick<Vue, "$watch">): void {
     const targets = config.getOptionsToWatch();
     if (targets) {
         targets.forEach((optionName: string) => {
@@ -111,4 +136,4 @@ function bindOptionWatchers(config: Configuration, vueInstance: Pick<Vue, "$watc
 }
 
 export default Configuration;
-export { bindOptionWatchers, IConfigurable, UpdateFunc };
+export { bindOptionWatchers, UpdateFunc };
diff --git a/tools/src/component-generator.ts b/tools/src/component-generator.ts
index a46931a..8186480 100644
--- a/tools/src/component-generator.ts
+++ b/tools/src/component-generator.ts
@@ -144,23 +144,18 @@ L1 + `extends: <#= it.baseComponent #>,` +
 `<#? it.nestedComponents #>` +
     `\n` +
     `<#~ it.nestedComponents : nested #>` +
-        `const <#= nested.name #> = Vue.extend({` +
+        `const <#= nested.name #>: any = Vue.extend({` +
         L1 + `extends: <#= it.configComponent #>,` +
         L1 + `props: {\n` +
         `<#= nested.renderedProps #>` +
-        L1 + `},` +
-        L1 + `beforeMount() {` +
-        L2 + `(this as any).` +
+        L1 + `}\n` +
+        `});\n` +
+        `(<#= nested.name #> as any).$_optionName = "<#= nested.optionName #>";\n` +
 
         `<#? nested.isCollectionItem #>` +
-            `$_initCollectionOption` +
-        `<#??#>` +
-            `$_initOption` +
+            `(<#= nested.name #> as any).$_isCollectionItem = true;\n` +
         `<#?#>` +
 
-        `("<#= nested.optionName #>");` +
-        L1 + `}\n` +
-        `});\n` +
     `<#~#>` +
 `<#?#>` +
 


Test patch:
diff --git a/src/core/component.test.ts b/src/core/component.test.ts
index 0ce389f..4bb9320 100644
--- a/src/core/component.test.ts
+++ b/src/core/component.test.ts
@@ -1,11 +1,11 @@
-import Vue from "vue";
-import { DxComponent } from "../core/component";
-import { DxConfiguration } from "../core/configuration-component";
+import Vue, { VueConstructor } from "vue";
+import { DxComponent, IWidgetComponent } from "../core/component";
+import { DxConfiguration, IConfigurable, IConfigurationComponent } from "../core/configuration-component";
 import { DxExtensionComponent } from "../core/extension-component";
 
 import * as events from "devextreme/events";
 
-const eventHandlers: { [index: string]: (e?: any) => void}  = {};
+const eventHandlers: { [index: string]: (e?: any) => void } = {};
 const Widget = {
     option: jest.fn(),
     dispose: jest.fn(),
@@ -28,16 +28,15 @@ const TestComponent = Vue.extend({
     }
 });
 
-const TestNestedComponent = Vue.extend({
-    extends: DxConfiguration,
-    props: {
-        prop1: Number,
-        prop2: String
-    },
-    beforeMount() {
-        (this as any).$_initOption("nestedOption");
-    }
-});
+function buildTestComponentCtor(): VueConstructor {
+    return Vue.extend({
+        extends: DxConfiguration,
+        props: {
+            prop1: Number,
+            prop2: String
+        }
+    });
+}
 
 jest.setTimeout(1000);
 beforeEach(() => {
@@ -106,17 +105,268 @@ describe("options", () => {
     });
 });
 
+describe("configuration", () => {
+
+    const Nested = buildTestComponentCtor();
+    (Nested as any as IConfigurationComponent).$_optionName = "nestedOption";
+
+    it("creates configuration", () => {
+        const vm = new TestComponent();
+
+        expect((vm as IConfigurable).$_config).not.toBeNull();
+    });
+
+    it("passes configuration initialValues to widget ctor", () => {
+        const initialValues = {
+            a: {},
+            b: {
+                c: {
+                    d: {}
+                }
+            }
+        };
+
+        const vm = new TestComponent();
+        (vm as IConfigurable).$_config = {
+            getInitialValues: jest.fn(() => initialValues),
+            getOptionsToWatch: jest.fn()
+        } as any;
+
+        vm.$mount();
+
+        expect(WidgetClass).toHaveBeenCalledTimes(1);
+        expect(WidgetClass.mock.calls[0][1].a).toBe(initialValues.a);
+        expect(WidgetClass.mock.calls[0][1].b).toBe(initialValues.b);
+        expect(WidgetClass.mock.calls[0][1].b.c).toBe(initialValues.b.c);
+        expect(WidgetClass.mock.calls[0][1].b.c.d).toBe(initialValues.b.c.d);
+    });
+
+    it("calls the option method from a widget component configuration updateFunc", () => {
+        const optionSetter = jest.fn();
+        const vm = new TestComponent();
+
+        (vm as IWidgetComponent).$_instance = {
+            option: optionSetter
+        };
+        const name = "abc";
+        const value = {};
+
+        (vm as IConfigurable).$_config.updateFunc(name, value);
+
+        expect(optionSetter).toHaveBeenCalledTimes(1);
+        expect(optionSetter.mock.calls[0][0]).toBe(name);
+        expect(optionSetter.mock.calls[0][1]).toBe(value);
+    });
+
+    it("initializes nested config", () => {
+        const vm = new Vue({
+            template:
+                `<test-component>` +
+                `  <nested :prop1="123" />` +
+                `</test-component>`,
+            components: {
+                TestComponent,
+                Nested
+            }
+        }).$mount();
+
+        const config = (vm.$children[0] as any as IConfigurable).$_config;
+        expect(config.nested).toHaveLength(1);
+        expect(config.nested[0].name).toBe("nestedOption");
+        expect(config.nested[0].options).toEqual(["prop1", "prop2"]);
+        expect(config.nested[0].initialValues).toEqual({ prop1: 123 });
+        expect(config.nested[0].isCollectionItem).toBeFalsy();
+    });
+
+    it("initializes nested config (collectionItem)", () => {
+        const nestedCollectionItem = buildTestComponentCtor();
+        (nestedCollectionItem as any as IConfigurationComponent).$_optionName = "nestedOption";
+        (nestedCollectionItem as any as IConfigurationComponent).$_isCollectionItem = true;
+
+        const vm = new Vue({
+            template:
+                `<test-component>` +
+                `  <nested-collection-item :prop1="123" />` +
+                `</test-component>`,
+            components: {
+                TestComponent,
+                nestedCollectionItem
+            }
+        }).$mount();
+
+        const config = (vm.$children[0] as any as IConfigurable).$_config;
+        expect(config.nested).toHaveLength(1);
+        expect(config.nested[0].name).toBe("nestedOption");
+        expect(config.nested[0].options).toEqual(["prop1", "prop2"]);
+        expect(config.nested[0].initialValues).toEqual({ prop1: 123 });
+        expect(config.nested[0].isCollectionItem).toBeTruthy();
+        expect(config.nested[0].collectionItemIndex).toBe(0);
+    });
+
+    it("initializes nested config (several collectionItems)", () => {
+        const nestedCollectionItem = buildTestComponentCtor();
+        (nestedCollectionItem as any as IConfigurationComponent).$_optionName = "nestedOption";
+        (nestedCollectionItem as any as IConfigurationComponent).$_isCollectionItem = true;
+
+        const vm = new Vue({
+            template:
+                `<test-component>` +
+                `  <nested-collection-item :prop1="123" />` +
+                `  <nested-collection-item :prop1="456" prop2="abc" />` +
+                `  <nested-collection-item prop2="def" />` +
+                `</test-component>`,
+            components: {
+                TestComponent,
+                nestedCollectionItem
+            }
+        }).$mount();
+
+        const config = (vm.$children[0] as any as IConfigurable).$_config;
+        expect(config.nested).toHaveLength(3);
+
+        expect(config.nested[0].name).toBe("nestedOption");
+        expect(config.nested[0].options).toEqual(["prop1", "prop2"]);
+        expect(config.nested[0].initialValues).toEqual({ prop1: 123 });
+        expect(config.nested[0].isCollectionItem).toBeTruthy();
+        expect(config.nested[0].collectionItemIndex).toBe(0);
+
+        expect(config.nested[1].name).toBe("nestedOption");
+        expect(config.nested[1].options).toEqual(["prop1", "prop2"]);
+        expect(config.nested[1].initialValues).toEqual({ prop1: 456, prop2: "abc" });
+        expect(config.nested[1].isCollectionItem).toBeTruthy();
+        expect(config.nested[1].collectionItemIndex).toBe(1);
+
+        expect(config.nested[2].name).toBe("nestedOption");
+        expect(config.nested[2].options).toEqual(["prop1", "prop2"]);
+        expect(config.nested[2].initialValues).toEqual({ prop2: "def" });
+        expect(config.nested[2].isCollectionItem).toBeTruthy();
+        expect(config.nested[2].collectionItemIndex).toBe(2);
+    });
+
+    it("initializes sub-nested config", () => {
+        const subNested = buildTestComponentCtor();
+        (subNested as any as IConfigurationComponent).$_optionName = "subNestedOption";
+
+        const vm = new Vue({
+            template:
+                `<test-component>` +
+                `  <nested :prop1="123">` +
+                `    <sub-nested prop2="abc"/>` +
+                `  </nested>` +
+                `</test-component>`,
+            components: {
+                TestComponent,
+                Nested,
+                subNested
+            }
+        }).$mount();
+
+        const config = (vm.$children[0] as any as IConfigurable).$_config;
+        expect(config.nested).toHaveLength(1);
+
+        const nestedConfig = config.nested[0];
+        expect(nestedConfig.nested).toHaveLength(1);
+
+        expect(nestedConfig.nested[0].name).toBe("subNestedOption");
+        expect(nestedConfig.nested[0].options).toEqual(["prop1", "prop2"]);
+        expect(nestedConfig.nested[0].initialValues).toEqual({ prop2: "abc" });
+        expect(nestedConfig.nested[0].isCollectionItem).toBeFalsy();
+    });
+
+    it("initializes sub-nested config (collectionItem)", () => {
+        const nestedCollectionItem = buildTestComponentCtor();
+        (nestedCollectionItem as any as IConfigurationComponent).$_optionName = "subNestedOption";
+        (nestedCollectionItem as any as IConfigurationComponent).$_isCollectionItem = true;
+
+        const vm = new Vue({
+            template:
+                `<test-component>` +
+                `  <nested>` +
+                `    <nested-collection-item :prop1="123"/>` +
+                `  </nested>` +
+                `</test-component>`,
+            components: {
+                TestComponent,
+                Nested,
+                nestedCollectionItem
+            }
+        }).$mount();
+
+        const config = (vm.$children[0] as any as IConfigurable).$_config;
+        expect(config.nested).toHaveLength(1);
+
+        const nestedConfig = config.nested[0];
+        expect(nestedConfig.nested).toHaveLength(1);
+
+        expect(nestedConfig.nested[0].name).toBe("subNestedOption");
+        expect(nestedConfig.nested[0].options).toEqual(["prop1", "prop2"]);
+        expect(nestedConfig.nested[0].initialValues).toEqual({ prop1: 123 });
+        expect(nestedConfig.nested[0].isCollectionItem).toBeTruthy();
+        expect(nestedConfig.nested[0].collectionItemIndex).toBe(0);
+    });
+
+    it("initializes sub-nested config (collectionItem)", () => {
+        const nestedCollectionItem = buildTestComponentCtor();
+        (nestedCollectionItem as any as IConfigurationComponent).$_optionName = "subNestedOption";
+        (nestedCollectionItem as any as IConfigurationComponent).$_isCollectionItem = true;
+
+        const vm = new Vue({
+            template:
+                `<test-component>` +
+                `  <nested>` +
+                `    <nested-collection-item :prop1="123" />` +
+                `    <nested-collection-item :prop1="456" prop2="abc" />` +
+                `    <nested-collection-item prop2="def" />` +
+                `  </nested>` +
+                `</test-component>`,
+            components: {
+                TestComponent,
+                Nested,
+                nestedCollectionItem
+            }
+        }).$mount();
+
+        const config = (vm.$children[0] as any as IConfigurable).$_config;
+        expect(config.nested).toHaveLength(1);
+
+        const nestedConfig = config.nested[0];
+        expect(nestedConfig.nested).toHaveLength(3);
+
+        expect(nestedConfig.nested[0].name).toBe("subNestedOption");
+        expect(nestedConfig.nested[0].options).toEqual(["prop1", "prop2"]);
+        expect(nestedConfig.nested[0].initialValues).toEqual({ prop1: 123 });
+        expect(nestedConfig.nested[0].isCollectionItem).toBeTruthy();
+        expect(nestedConfig.nested[0].collectionItemIndex).toBe(0);
+
+        expect(nestedConfig.nested[1].name).toBe("subNestedOption");
+        expect(nestedConfig.nested[1].options).toEqual(["prop1", "prop2"]);
+        expect(nestedConfig.nested[1].initialValues).toEqual({ prop1: 456, prop2: "abc" });
+        expect(nestedConfig.nested[1].isCollectionItem).toBeTruthy();
+        expect(nestedConfig.nested[1].collectionItemIndex).toBe(1);
+
+        expect(nestedConfig.nested[2].name).toBe("subNestedOption");
+        expect(nestedConfig.nested[2].options).toEqual(["prop1", "prop2"]);
+        expect(nestedConfig.nested[2].initialValues).toEqual({ prop2: "def" });
+        expect(nestedConfig.nested[2].isCollectionItem).toBeTruthy();
+        expect(nestedConfig.nested[2].collectionItemIndex).toBe(2);
+    });
+
+});
+
 describe("nested options", () => {
 
-    it("pulls initital values on mounting (template)", () => {
+    const Nested = buildTestComponentCtor();
+    (Nested as any as IConfigurationComponent).$_optionName = "nestedOption";
+
+    it("pulls initital values", () => {
         const vm = new Vue({
             template:
                 `<test-component>` +
-                `  <test-nested-component :prop1="123" />` +
+                `  <nested :prop1="123" />` +
                 `</test-component>`,
             components: {
                 TestComponent,
-                TestNestedComponent
+                Nested
             }
         }).$mount();
 
@@ -127,48 +377,187 @@ describe("nested options", () => {
         });
     });
 
-    it("pulls initital values on mounting (render function + createElement)", () => {
-        const widget = new TestComponent({
-            render(createElement) {
-                return createElement(TestNestedComponent, { props: { prop1: 123 } });
+    it("pulls initital values (collectionItem)", () => {
+        const nestedCollectionItem = buildTestComponentCtor();
+        (nestedCollectionItem as any as IConfigurationComponent).$_optionName = "nestedOption";
+        (nestedCollectionItem as any as IConfigurationComponent).$_isCollectionItem = true;
+
+        const vm = new Vue({
+            template:
+                `<test-component>` +
+                `  <nested-collection-item :prop1="123" />` +
+                `</test-component>`,
+            components: {
+                TestComponent,
+                nestedCollectionItem
+            }
+        }).$mount();
+
+        expect(WidgetClass).toHaveBeenCalledWith(vm.$children[0].$el, {
+            nestedOption: [{
+                prop1: 123
+            }]
+        });
+    });
+
+    it("pulls initital values (subnested)", () => {
+        const subNested = buildTestComponentCtor();
+        (subNested as any as IConfigurationComponent).$_optionName = "subNestedOption";
+
+        const vm = new Vue({
+            template:
+                `<test-component>` +
+                `  <nested :prop1="123">` +
+                `    <sub-nested prop2="abc"/>` +
+                `  </nested>` +
+                `</test-component>`,
+            components: {
+                TestComponent,
+                Nested,
+                subNested
+            }
+        }).$mount();
+
+        expect(WidgetClass).toHaveBeenCalledWith(vm.$children[0].$el, {
+            nestedOption: {
+                prop1: 123,
+                subNestedOption: {
+                    prop2: "abc"
+                }
+            }
+        });
+    });
+
+    it("pulls initital values (subnested collectionItem)", () => {
+        const nestedCollectionItem = buildTestComponentCtor();
+        (nestedCollectionItem as any as IConfigurationComponent).$_optionName = "subNestedOption";
+        (nestedCollectionItem as any as IConfigurationComponent).$_isCollectionItem = true;
+
+        const vm = new Vue({
+            template:
+                `<test-component>` +
+                `  <nested :prop1="123">` +
+                `    <nested-collection-item prop2="abc"/>` +
+                `  </nested>` +
+                `</test-component>`,
+            components: {
+                TestComponent,
+                Nested,
+                nestedCollectionItem
             }
         }).$mount();
 
-        expect(WidgetClass).toHaveBeenCalledWith(widget.$el, {
+        expect(WidgetClass).toHaveBeenCalledWith(vm.$children[0].$el, {
             nestedOption: {
-                prop1: 123
+                prop1: 123,
+                subNestedOption: [{
+                    prop2: "abc"
+                }]
             }
         });
     });
 
-    it("pulls initital values on mounting (manual mounting)", () => {
-        const widget = new TestComponent();
-        const option = new TestNestedComponent({
+    it("watches option changes", (done) => {
+        const vm = new Vue({
+            template:
+                `<test-component>` +
+                `  <nested :prop1="value" />` +
+                `</test-component>`,
+            components: {
+                TestComponent,
+                Nested
+            },
+            props: ["value"],
             propsData: {
-                prop1: 123
+                value: 123
+            }
+        }).$mount();
+
+        vm.$props.value = 456;
+
+        Vue.nextTick(() => {
+            expect(Widget.option).toHaveBeenCalledTimes(1);
+            expect(Widget.option).toHaveBeenCalledWith("nestedOption.prop1", 456);
+            done();
+        });
+    });
+
+    it("watches option changes (collectionItem)", (done) => {
+        const nestedCollectionItem = buildTestComponentCtor();
+        (nestedCollectionItem as any as IConfigurationComponent).$_optionName = "nestedOption";
+        (nestedCollectionItem as any as IConfigurationComponent).$_isCollectionItem = true;
+
+        const vm = new Vue({
+            template:
+                `<test-component>` +
+                `  <nested-collection-item :prop1="value" />` +
+                `</test-component>`,
+            components: {
+                TestComponent,
+                nestedCollectionItem
             },
-            parent: widget
+            props: ["value"],
+            propsData: {
+                value: 123
+            }
+        }).$mount();
+
+        vm.$props.value = 456;
+
+        Vue.nextTick(() => {
+            expect(Widget.option).toHaveBeenCalledTimes(1);
+            expect(Widget.option).toHaveBeenCalledWith("nestedOption[0].prop1", 456);
+            done();
         });
+    });
 
-        option.$mount();
-        widget.$mount();
+    it("watches option changes (subnested)", (done) => {
+        const subNested = buildTestComponentCtor();
+        (subNested as any as IConfigurationComponent).$_optionName = "subNestedOption";
 
-        expect(WidgetClass).toHaveBeenCalledWith(widget.$el, {
-            nestedOption: {
-                prop1: 123
+        const vm = new Vue({
+            template:
+                `<test-component>` +
+                `  <nested>` +
+                `    <sub-nested :prop1="value"/>` +
+                `  </nested>` +
+                `</test-component>`,
+            components: {
+                TestComponent,
+                Nested,
+                subNested
+            },
+            props: ["value"],
+            propsData: {
+                value: 123
             }
+        }).$mount();
+
+        vm.$props.value = 456;
+
+        Vue.nextTick(() => {
+            expect(Widget.option).toHaveBeenCalledTimes(1);
+            expect(Widget.option).toHaveBeenCalledWith("nestedOption.subNestedOption.prop1", 456);
+            done();
         });
     });
 
-    it("watches nested option changes", (done) => {
+    it("watches option changes (subnested collectionItem)", (done) => {
+        const subNestedCollectionItem = buildTestComponentCtor();
+        (subNestedCollectionItem as any as IConfigurationComponent).$_optionName = "subNestedOption";
+        (subNestedCollectionItem as any as IConfigurationComponent).$_isCollectionItem = true;
+
         const vm = new Vue({
             template:
                 `<test-component>` +
-                `  <test-nested-component :prop1="value" />` +
+                `  <nested>` +
+                `    <sub-nested-collection-item :prop1="value"/>` +
+                `  </nested>` +
                 `</test-component>`,
             components: {
                 TestComponent,
-                TestNestedComponent
+                Nested,
+                subNestedCollectionItem
             },
             props: ["value"],
             propsData: {
@@ -180,7 +569,7 @@ describe("nested options", () => {
 
         Vue.nextTick(() => {
             expect(Widget.option).toHaveBeenCalledTimes(1);
-            expect(Widget.option).toHaveBeenCalledWith("nestedOption.prop1", 456);
+            expect(Widget.option).toHaveBeenCalledWith("nestedOption.subNestedOption[0].prop1", 456);
             done();
         });
     });
diff --git a/src/core/configuration.test.ts b/src/core/configuration.test.ts
index 7fd4c6a..76d2778 100644
--- a/src/core/configuration.test.ts
+++ b/src/core/configuration.test.ts
@@ -1,14 +1,14 @@
-import Configuration, { UpdateFunc } from "./configuration";
+import Configuration, { bindOptionWatchers, UpdateFunc } from "./configuration";
 
 function createRootConfig(updateFunc: UpdateFunc): Configuration {
-    return new Configuration(updateFunc, null, [], {});
+    return new Configuration(updateFunc, null, {});
 }
 
 it("calls update from nested", () => {
     const callback = jest.fn();
     const root = createRootConfig(callback);
 
-    const nested = root.createNested("option", [], {});
+    const nested = root.createNested("option", {});
     nested.updateValue("prop", 123);
 
     expect(callback).toHaveBeenCalledTimes(1);
@@ -18,8 +18,8 @@ it("calls update from nested", () => {
 it("calls update from subnested", () => {
     const callback = jest.fn();
     const root = createRootConfig(callback);
-    const nested = root.createNested("option", [], {});
-    const subNested = nested.createNested("subOption", [], {});
+    const nested = root.createNested("option", {});
+    const subNested = nested.createNested("subOption", {});
 
     subNested.updateValue("prop", 123);
 
@@ -30,8 +30,8 @@ it("calls update from subnested", () => {
 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);
+    const nested = root.createNested("option", {}, true);
+    root.createNested("option", {}, true);
 
     nested.updateValue("prop", 123);
 
@@ -43,9 +43,9 @@ 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);
+    root.createNested("option", {}, true);
+    const nested = root.createNested("option", {}, true);
+    root.createNested("option", {}, true);
 
     nested.updateValue("prop", 123);
 
@@ -56,9 +56,9 @@ it("calls update from nested collectionItem (middle)", () => {
 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);
+    root.createNested("option", {}, true);
+    root.createNested("option", {}, true);
+    const nested = root.createNested("option", {}, true);
 
     nested.updateValue("prop", 123);
 
@@ -69,7 +69,7 @@ it("calls update from nested collectionItem (last)", () => {
 it("calls update from nested collectionItem (the only)", () => {
     const callback = jest.fn();
     const root = createRootConfig(callback);
-    const nested = root.createNested("option", [], {}, true);
+    const nested = root.createNested("option", {}, true);
 
     nested.updateValue("prop", 123);
 
@@ -77,13 +77,39 @@ it("calls update from nested collectionItem (the only)", () => {
     expect(callback).toHaveBeenCalledWith("option[0].prop", 123);
 });
 
+it("binds option watchers", () => {
+    const updateValueFunc = jest.fn();
+    const $watchFunc = jest.fn();
+
+    bindOptionWatchers(
+        {
+            updateValue: updateValueFunc,
+            getOptionsToWatch: () => ["prop1"],
+        } as any,
+        {
+            $watch: $watchFunc
+        }
+    );
+
+    expect($watchFunc.mock.calls[0][0]).toBe("prop1");
+
+    const value = {};
+    $watchFunc.mock.calls[0][1](value);
+
+    expect(updateValueFunc).toHaveBeenCalledTimes(1);
+    expect(updateValueFunc.mock.calls[0][0]).toBe("prop1");
+    expect(updateValueFunc.mock.calls[0][1]).toBe(value);
+});
+
 describe("initial configuration", () => {
 
     it("pulls value from nested", () => {
         const root = createRootConfig(jest.fn());
 
-        const nested = root.createNested("option", [], {});
-        nested.createNested("subOption", ["prop"], { prop: 123 });
+        const nested = root.createNested("option", {});
+        nested
+            .createNested("subOption", { prop: 123 })
+            .init(["prop"]);
 
         expect(root.getInitialValues()).toMatchObject({
             option: {
@@ -97,7 +123,7 @@ describe("initial configuration", () => {
     it("pulls array of values from a coollectionItem nested (single value)", () => {
         const root = createRootConfig(jest.fn());
 
-        root.createNested("options", [], { propA: 123 }, true);
+        root.createNested("options", { propA: 123 }, true);
 
         expect(root.getInitialValues()).toMatchObject({
             options: [
@@ -109,8 +135,8 @@ describe("initial configuration", () => {
     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);
+        root.createNested("options", { propA: 123 }, true);
+        root.createNested("options", { propA: 456, propB: 789 }, true);
 
         expect(root.getInitialValues()).toMatchObject({
             options: [
@@ -123,8 +149,8 @@ describe("initial configuration", () => {
     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 });
+        root.createNested("option", { propA: 123 });
+        root.createNested("option", { propA: 456, propB: 789 });
 
         expect(root.getInitialValues()).toMatchObject({
             option: { propA: 456, propB: 789 }
@@ -132,10 +158,10 @@ describe("initial configuration", () => {
     });
 
     it("pulls values from self and nested", () => {
-        const root = new Configuration(jest.fn(), null, [], { propA: 123 });
+        const root = new Configuration(jest.fn(), null, { propA: 123 });
 
-        const nested = root.createNested("option", [], { propB: 456 });
-        nested.createNested("subOption", [], { propC: 789 });
+        const nested = root.createNested("option", { propB: 456 });
+        nested.createNested("subOption", { propC: 789 });
 
         expect(root.getInitialValues()).toMatchObject({
             propA: 123,
@@ -151,8 +177,8 @@ describe("initial configuration", () => {
     it("pulls undefined if no values provided", () => {
         const root = createRootConfig(jest.fn());
 
-        const nested = root.createNested("option", [], {});
-        nested.createNested("subOption", [], {});
+        const nested = root.createNested("option", {});
+        nested.createNested("subOption", {});
 
         expect(root.getInitialValues()).toBeUndefined();
     });
@@ -160,11 +186,14 @@ describe("initial configuration", () => {
     it("pulls values and ignores empty nested", () => {
         const root = createRootConfig(jest.fn());
 
-        const nested = root.createNested("option", ["empty"], {});
-        nested.createNested("subOption", ["prop"], { prop: 123 });
+        const nested = root.createNested("option", {});
+        nested.init(["empty"]);
+        nested
+            .createNested("subOption", { prop: 123 })
+            .init(["prop"]);
 
-        root.createNested("anotherOption", [], {});
-        nested.createNested("anotherSubOption", [], {});
+        root.createNested("anotherOption", {});
+        nested.createNested("anotherSubOption", {});
 
         expect(root.getInitialValues()).toMatchObject({
             option: {
@@ -180,20 +209,23 @@ describe("initial configuration", () => {
 describe("options watch-list", () => {
 
     it("includes option with initial values", () => {
-        const config = new Configuration(jest.fn(), null, ["option1"], { option1: 123, option2: 456 });
+        const config = new Configuration(jest.fn(), null, { option1: 123, option2: 456 });
+        config.init(["option1"]);
 
         expect(config.getOptionsToWatch()).toEqual(["option1"]);
     });
 
     it("includes option without initial values", () => {
-        const config = new Configuration(jest.fn(), null, ["option1"], {});
+        const config = new Configuration(jest.fn(), null, {});
+        config.init(["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", [], {});
+        const config = new Configuration(jest.fn(), null, {});
+        config.init(["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 2fbe427..be8755b 100644
--- a/tools/src/component-generator.test.ts
+++ b/tools/src/component-generator.test.ts
@@ -166,15 +166,13 @@ const COMPONENT: VueConstructor = Vue.extend({
   }
 });
 
-const NESTED_COMPONENT = Vue.extend({
+const NESTED_COMPONENT: any = Vue.extend({
   extends: CONFIG_COMPONENT,
   props: {
     PROP: {}
-  },
-  beforeMount() {
-    (this as any).$_initOption("NESTED_OPTION_NAME");
   }
 });
+(NESTED_COMPONENT as any).$_optionName = "NESTED_OPTION_NAME";
 
 export {
   COMPONENT,
@@ -234,15 +232,14 @@ const COMPONENT: VueConstructor = Vue.extend({
   }
 });
 
-const NESTED_COMPONENT = Vue.extend({
+const NESTED_COMPONENT: any = Vue.extend({
   extends: CONFIG_COMPONENT,
   props: {
     PROP: {}
-  },
-  beforeMount() {
-    (this as any).$_initCollectionOption("NESTED_OPTION_NAME");
   }
 });
+(NESTED_COMPONENT as any).$_optionName = "NESTED_OPTION_NAME";
+(NESTED_COMPONENT as any).$_isCollectionItem = true;
 
 export {
   COMPONENT,