Repository: DevExpress/devextreme-react
Pull request #366 (2019-07-15 09:32:10)

Rework options handling


Patch:
diff --git a/example/toolbar-example.tsx b/example/toolbar-example.tsx
index fe86ec8..390443e 100644
--- a/example/toolbar-example.tsx
+++ b/example/toolbar-example.tsx
@@ -4,9 +4,9 @@ import Example from "./example-block";
 import { Button } from "../src/button";
 import { Toolbar } from "../src/toolbar";
 
-const ItemComponent  = (data: {text: string}) => {
+const ItemComponent  = (data: {data: {text: string}}) => {
     return (
-        <Button text={data.text}/>
+        <Button text={data.data.text}/>
     );
 };
 
diff --git a/src/core/component-base.ts b/src/core/component-base.ts
index 33b85f8..348d51a 100644
--- a/src/core/component-base.ts
+++ b/src/core/component-base.ts
@@ -1,20 +1,18 @@
 import * as events from "devextreme/events";
-
 import * as React from "react";
 
-import { deferUpdate } from "devextreme/core/utils/common";
-import OptionsManager, { INestedOption } from "./options-manager";
-import { findProps as findNestedTemplateProps, ITemplateMeta } from "./template";
+import { OptionsManager } from "./options-manager";
+import { ITemplateMeta } from "./template";
 import TemplatesManager from "./templates-manager";
+import { TemplatesRenderer } from "./templates-renderer";
 import { TemplatesStore } from "./templates-store";
-import { elementPropNames, getClassName, separateProps } from "./widget-config";
+import { elementPropNames, getClassName } from "./widget-config";
 
-const DX_REMOVE_EVENT = "dxremove";
+import { IConfigNode } from "./configuration/config-node";
+import { IExpectedChild } from "./configuration/react/element";
+import { buildConfigTree } from "./configuration/react/tree";
 
-interface IWidgetConfig {
-    defaults: Record<string, any>;
-    options: Record<string, any>;
-}
+const DX_REMOVE_EVENT = "dxremove";
 
 interface IHtmlOptions {
   id?: string;
@@ -29,29 +27,40 @@ abstract class ComponentBase<P extends IHtmlOptions> extends React.PureComponent
 
   protected readonly _defaults: Record<string, string>;
   protected readonly _templateProps: ITemplateMeta[] = [];
-  protected readonly _expectedChildren: Record<string, INestedOption>;
+  protected readonly _expectedChildren: Record<string, IExpectedChild>;
+
+  private _templatesRendererRef: TemplatesRenderer | null;
 
-  private readonly _templatesManager: TemplatesManager;
-  private readonly _templatesStore: TemplatesStore;
-  private readonly _optionsManager: OptionsManager;
-  private _updateScheduled: boolean = false;
+  private _templatesStore: TemplatesStore;
+  private _templatesManager: TemplatesManager;
+  private _optionsManager: OptionsManager;
 
   constructor(props: P) {
     super(props);
-    this._prepareProps = this._prepareProps.bind(this);
-    this._scheduleUpdate = this._scheduleUpdate.bind(this);
 
-    this._templatesStore = new TemplatesStore(this._scheduleUpdate);
+    this._setTemplatesRendererRef = this._setTemplatesRendererRef.bind(this);
+
+    this._templatesStore = new TemplatesStore(() => {
+      if (this._templatesRendererRef) {
+        this._templatesRendererRef.scheduleUpdate();
+      }
+    });
     this._templatesManager = new TemplatesManager(this._templatesStore);
-    this._optionsManager = new OptionsManager((name) => this.props[name], this._templatesManager);
+    this._optionsManager = new OptionsManager(this._templatesManager);
   }
 
   public render() {
     return React.createElement(
       "div",
       this._getElementProps(),
-      ...this._prepareChildren(),
-      ...this._templatesStore.renderWrappers()
+      this.renderChildren(),
+      React.createElement(
+        TemplatesRenderer,
+        {
+          templatesStore: this._templatesStore,
+          ref: this._setTemplatesRendererRef
+        }
+      )
     );
   }
 
@@ -62,9 +71,11 @@ abstract class ComponentBase<P extends IHtmlOptions> extends React.PureComponent
   public componentDidUpdate(prevProps: P) {
     this._updateCssClasses(prevProps, this.props);
 
-    const preparedProps = this._prepareProps(this.props);
-
-    this._optionsManager.updateOptions(preparedProps.options, prevProps);
+    const config = this._getConfig();
+    this._optionsManager.update(config);
+    if (this._templatesRendererRef) {
+      this._templatesRendererRef.scheduleUpdate();
+    }
   }
 
   public componentWillUnmount() {
@@ -75,61 +86,40 @@ abstract class ComponentBase<P extends IHtmlOptions> extends React.PureComponent
     this._optionsManager.dispose();
   }
 
-  protected _prepareChildren(args: any[] = []): any[] {
-    this._optionsManager.resetNestedElements();
-
-    React.Children.forEach(this.props.children, (child: React.ReactElement<any>) => {
-      if (child) {
-        args.push(this._preprocessChild(child) || child);
-      }
-    });
-
-    return args;
-  }
-
-  protected _preprocessChild(component: React.ReactElement<any>): React.ReactElement<any> {
-    const nestedTemplate = findNestedTemplateProps(component);
-    if (nestedTemplate) {
-      this._templatesManager.addNested(nestedTemplate);
-    }
-    return this._optionsManager.registerNestedOption(component, this._expectedChildren) || component;
+  protected renderChildren() {
+    return this.props.children;
   }
 
   protected _createWidget(element?: Element) {
     element = element || this._element;
-    const nestedProps = this._optionsManager.getNestedOptionsObjects();
-    const props = {
-        ...(this.props as any),
-        ...nestedProps
-    };
-
-    const preparedProps = this._prepareProps(props);
 
-    const options: Record<string, any> = {
-      templatesRenderAsynchronously: true,
-      ...preparedProps.defaults,
-      ...preparedProps.options,
-      ...this._templatesManager.options
-    };
-
-    this._optionsManager.wrapEventHandlers(options);
+    const config = this._getConfig();
+    this._instance = new this._WidgetClass(
+      element,
+      {
+        templatesRenderAsynchronously: true,
+        ...this._optionsManager.getInitialOptions(config)
+      }
+    );
 
-    this._instance = new this._WidgetClass(element, options);
-    this._optionsManager.setInstance(this._instance);
-    this._instance.on("optionChanged", this._optionsManager.handleOptionChange);
+    this._optionsManager.setInstance(this._instance, config);
+    this._instance.on("optionChanged", this._optionsManager.onOptionChanged);
   }
 
-  private _scheduleUpdate() {
-    if (this._updateScheduled) {
-      return;
-    }
-
-    this._updateScheduled = true;
+  private _getConfig(): IConfigNode {
+    return buildConfigTree(
+      {
+        templates: this._templateProps,
+        initialValuesProps: this._defaults,
+        predefinedValuesProps: {},
+        expectedChildren: this._expectedChildren
+      },
+      this.props
+    );
+  }
 
-    deferUpdate(() => {
-      this.forceUpdate();
-      this._updateScheduled = false;
-    });
+  private _setTemplatesRendererRef(instance: TemplatesRenderer | null) {
+    this._templatesRendererRef = instance;
   }
 
   private _getElementProps(): Record<string, any> {
@@ -165,26 +155,6 @@ abstract class ComponentBase<P extends IHtmlOptions> extends React.PureComponent
       }
     }
   }
-
-  private _prepareProps(rawProps: Record<string, any>): IWidgetConfig {
-    const separatedProps = separateProps(rawProps, this._defaults, this._templateProps);
-
-    const templatesOptions = this._templatesManager.add({
-      useChildren: () => false,
-      ownerName: "",
-      templateProps: this._templateProps,
-      props: separatedProps.templates,
-      propsGetter: (prop) => this.props[prop]
-    });
-
-    return {
-        options: {
-          ...separatedProps.options,
-          ...templatesOptions
-        },
-        defaults: separatedProps.defaults
-    };
-  }
 }
 
 export {
diff --git a/src/core/component.ts b/src/core/component.ts
index 56e719f..d2dc60e 100644
--- a/src/core/component.ts
+++ b/src/core/component.ts
@@ -4,7 +4,13 @@ import { ComponentBase, IHtmlOptions } from "./component-base";
 import { ExtensionComponent } from "./extension-component";
 
 class Component<P> extends ComponentBase<P> {
-  private readonly _extensions: Array<(element: Element) => void> = [];
+  private _extensions: Array<(element: Element) => void> = [];
+
+  constructor(props: P) {
+    super(props);
+
+    this.registerExtension = this.registerExtension.bind(this);
+  }
 
   public componentDidMount() {
     super.componentDidMount();
@@ -12,35 +18,25 @@ class Component<P> extends ComponentBase<P> {
     this._extensions.forEach((extension) => extension.call(this, this._element));
   }
 
-  protected _prepareChildren(): any[] {
-    const args: any[] = [];
-    if (isSingleTextNode(this.props.children)) {
-      args.push(React.Fragment);
-    }
-
-    return super._prepareChildren(args);
-  }
-
-  protected _preprocessChild(component: React.ReactElement<any>) {
-    return this._registerExtension(component) || super._preprocessChild(component);
-  }
-
-  private _registerExtension(component: React.ReactElement<any>) {
-    if (!ExtensionComponent.isPrototypeOf(component.type)) {
-      return null;
-    }
-
-    return React.cloneElement(component, {
-      onMounted: (callback: any) => {
-        this._extensions.push(callback);
+  protected renderChildren() {
+    return React.Children.map(
+      this.props.children,
+      (child) => {
+        if (child && ExtensionComponent.isPrototypeOf((child as any).type)) {
+          return React.cloneElement(
+            child as any,
+            { onMounted: this.registerExtension}
+          );
+        }
+
+        return child;
       }
-    });
+    );
   }
-}
 
-function isSingleTextNode(input: React.ReactNode) {
-  const children = React.Children.toArray(input);
-  return children.length === 1 && typeof children[0] === "string";
+  private registerExtension(callback: any) {
+    this._extensions.push(callback);
+  }
 }
 
 export {
diff --git a/src/core/configuration/comparer.ts b/src/core/configuration/comparer.ts
new file mode 100644
index 0000000..3a25774
--- /dev/null
+++ b/src/core/configuration/comparer.ts
@@ -0,0 +1,122 @@
+import { IConfigNode, ITemplate } from "./config-node";
+import { buildNode, buildTemplates } from "./tree";
+import { mergeNameParts } from "./utils";
+
+interface IConfigChanges {
+    options: Record<string, any>;
+    templates: Record<string, ITemplate>;
+}
+
+function getChanges(current: IConfigNode, prev: IConfigNode) {
+    const changesAccum: IConfigChanges = {
+        options: {},
+        templates: {}
+    };
+
+    compare(current, prev, changesAccum);
+
+    return changesAccum;
+}
+
+function compare(current: IConfigNode, prev: IConfigNode, changesAccum: IConfigChanges) {
+    if (!prev) {
+        const config = buildNode(current, changesAccum.templates, true);
+        changesAccum.options[current.fullName] = config.options;
+        return;
+    }
+
+    appendRemovedValues(current.options, prev.options, current.fullName, changesAccum.options);
+    appendRemovedValues(current.configCollections, prev.configCollections, current.fullName, changesAccum.options);
+    appendRemovedValues(current.configs, prev.configs, current.fullName, changesAccum.options);
+
+    compareCollections(current, prev, changesAccum);
+
+    for (const key of Object.keys(current.configs)) {
+        compare(current.configs[key], prev.configs[key], changesAccum);
+    }
+
+    for (const key of Object.keys(current.options)) {
+        if (current.options[key] === prev.options[key]) {
+            continue;
+        }
+
+        changesAccum.options[mergeNameParts(current.fullName, key)] = current.options[key];
+    }
+
+    compareTemplates(current, prev, changesAccum);
+}
+
+function compareTemplates(current: IConfigNode, prev: IConfigNode, changesAccum: IConfigChanges) {
+    const currentTemplatesOptions: Record<string, any> = {};
+    const currentTemplates: Record<string, ITemplate> = {};
+    const prevTemplatesOptions: Record<string, any> = {};
+    const prevTemplates: Record<string, ITemplate> = {};
+
+    buildTemplates(current, currentTemplatesOptions, currentTemplates);
+    buildTemplates(prev, prevTemplatesOptions, prevTemplates);
+
+    appendRemovedValues(currentTemplatesOptions, prevTemplatesOptions, current.fullName, changesAccum.options);
+    // TODO: support switching to default templates
+    // appendRemovedValues(currentTemplates, prevTemplates, "", changesAccum.templates);
+
+    for (const key of Object.keys(currentTemplatesOptions)) {
+        if (currentTemplatesOptions[key] === prevTemplatesOptions[key]) {
+            continue;
+        }
+
+        changesAccum.options[mergeNameParts(current.fullName, key)] = currentTemplatesOptions[key];
+    }
+
+    for (const key of Object.keys(currentTemplates)) {
+        const currentTemplate = currentTemplates[key];
+        const prevTemplate = prevTemplates[key];
+        if (prevTemplate && currentTemplate.content === prevTemplate.content) {
+            continue;
+        }
+
+        changesAccum.templates[key] = currentTemplate;
+    }
+}
+
+function appendRemovedValues(
+    current: Record<string, any>,
+    prev: Record<string, any>,
+    path: string,
+    changesAccum: Record<string, any>
+) {
+    const removedKeys = Object.keys(prev).filter((key) => Object.keys(current).indexOf(key) < 0);
+
+    for (const key of removedKeys) {
+        changesAccum[mergeNameParts(path, key)] = undefined;
+    }
+}
+
+function compareCollections(
+    current: Record<string, any>,
+    prev: Record<string, any>,
+    changesAccum: IConfigChanges
+) {
+    for (const key of Object.keys(current.configCollections)) {
+        const currentCollection = current.configCollections[key];
+        const prevCollection = prev.configCollections[key] || [];
+        if (!currentCollection || currentCollection.length !== prevCollection.length) {
+            const updatedCollection: Array<Record<string, any>> = [];
+            currentCollection.map(
+                (item) => {
+                    const config = buildNode(item, changesAccum.templates, true);
+                    updatedCollection.push(config);
+                }
+            );
+            changesAccum.options[mergeNameParts(current.fullname, key)] = updatedCollection;
+            continue;
+        }
+
+        for (let i = 0; i < currentCollection.length; i++) {
+            compare(currentCollection[i], prevCollection[i], changesAccum);
+        }
+    }
+}
+
+export {
+    getChanges
+};
diff --git a/src/core/configuration/config-node.ts b/src/core/configuration/config-node.ts
new file mode 100644
index 0000000..fe1a8d6
--- /dev/null
+++ b/src/core/configuration/config-node.ts
@@ -0,0 +1,22 @@
+interface IConfigNode {
+    readonly fullName: string;
+    readonly predefinedOptions: Record<string, any>;
+    readonly initialOptions: Record<string, any>;
+    readonly options: Record<string, any>;
+    readonly templates: ITemplate[];
+    readonly configs: Record<string, IConfigNode>;
+    readonly configCollections: Record<string, IConfigNode[]>;
+}
+
+interface ITemplate {
+    optionName: string;
+    isAnonymous: boolean;
+    type: "component" | "render" | "children";
+    content: any;
+    keyFn?: (data: any) => string;
+}
+
+export {
+    IConfigNode,
+    ITemplate
+};
diff --git a/src/core/configuration/react/element.ts b/src/core/configuration/react/element.ts
new file mode 100644
index 0000000..f3e779d
--- /dev/null
+++ b/src/core/configuration/react/element.ts
@@ -0,0 +1,105 @@
+import { ITemplateMeta, Template } from "../../template";
+
+enum ElementType {
+    Option,
+    Template,
+    Unknown
+}
+
+interface IExpectedChild {
+    optionName: string;
+    isCollectionItem: boolean;
+}
+
+interface IOptionDescriptor {
+    isCollection: boolean;
+    name: string;
+    templates: ITemplateMeta[];
+    initialValuesProps: Record<string, string>;
+    predefinedValuesProps: Record<string, any>;
+    expectedChildren: Record<string, IExpectedChild>;
+}
+
+interface IOptionElement {
+    type: ElementType.Option;
+    descriptor: IOptionDescriptor;
+    props: Record<string, any>;
+}
+
+interface ITemplateElement {
+    type: ElementType.Template;
+    props: Record<string, any>;
+}
+
+interface IUnknownElement {
+    type: ElementType.Unknown;
+}
+
+type IElement = IOptionElement | ITemplateElement | IUnknownElement;
+
+function getElementInfo(
+    element: React.ReactChild,
+    parentExpectedChildren?: Record<string, IExpectedChild>
+): IElement {
+    if (!element || typeof element === "string" || typeof element === "number") {
+        return {
+            type: ElementType.Unknown
+        };
+    }
+
+    if (element.type === Template) {
+        return {
+            type: ElementType.Template,
+            props: element.props
+        };
+    }
+
+    const elementDescriptor = element.type as any as IElementDescriptor;
+
+    if (elementDescriptor.OptionName) {
+        let name = elementDescriptor.OptionName;
+        let isCollectionItem =  elementDescriptor.IsCollectionItem;
+
+        const expectation = parentExpectedChildren && parentExpectedChildren[name];
+        if (expectation) {
+            isCollectionItem = expectation.isCollectionItem;
+            if (expectation.optionName) {
+                name = expectation.optionName;
+            }
+        }
+
+        return {
+            type: ElementType.Option,
+            descriptor: {
+                name,
+                isCollection: isCollectionItem,
+                templates: elementDescriptor.TemplateProps || [],
+                initialValuesProps: elementDescriptor.DefaultsProps || {},
+                predefinedValuesProps: elementDescriptor.PredefinedProps || {},
+                expectedChildren: elementDescriptor.ExpectedChildren || {}
+            },
+            props: element.props
+        };
+    }
+
+    return {
+        type: ElementType.Unknown
+    };
+}
+
+interface IElementDescriptor {
+    OptionName: string;
+    IsCollectionItem: boolean;
+    DefaultsProps: Record<string, string>;
+    TemplateProps: ITemplateMeta[];
+    PredefinedProps: Record<string, any>;
+    ExpectedChildren: Record<string, IExpectedChild>;
+}
+
+export {
+    getElementInfo,
+    ElementType,
+    IElement,
+    IOptionElement,
+    IExpectedChild
+};
diff --git a/src/core/configuration/react/templates.ts b/src/core/configuration/react/templates.ts
new file mode 100644
index 0000000..9e3c9f0
--- /dev/null
+++ b/src/core/configuration/react/templates.ts
@@ -0,0 +1,79 @@
+import { ITemplateMeta, ITemplateProps } from "src/core/template";
+import { ITemplate } from "../config-node";
+
+function getAnonymousTemplate(
+    props: Record<string, any>,
+    templateMeta: ITemplateMeta,
+    hasTranscludedContent: boolean
+): ITemplate | null {
+    if (templateMeta.tmplOption === "template" && hasTranscludedContent) {
+        return {
+            optionName: templateMeta.tmplOption,
+            isAnonymous: true,
+            type: "children",
+            content: props.children,
+            keyFn: props[templateMeta.keyFn]
+        };
+    }
+
+    if (props[templateMeta.render]) {
+        return {
+            optionName: templateMeta.tmplOption,
+            isAnonymous: true,
+            type: "render",
+            content: props[templateMeta.render],
+            keyFn: props[templateMeta.keyFn]
+        };
+    }
+
+    if (props[templateMeta.component]) {
+        return {
+            optionName: templateMeta.tmplOption,
+            isAnonymous: true,
+            type: "component",
+            content: props[templateMeta.component],
+            keyFn: props[templateMeta.keyFn]
+        };
+    }
+
+    return null;
+}
+
+function getNamedTemplate(props: ITemplateProps): ITemplate | null {
+    if (!props.name) {
+        return null;
+    }
+
+    if (props.component) {
+        return {
+            optionName: props.name,
+            isAnonymous: false,
+            type: "component",
+            content: props.component,
+            keyFn: props.keyFn
+        };
+    }
+
+    if (props.render) {
+        return {
+            optionName: props.name,
+            isAnonymous: false,
+            type: "render",
+            content: props.render,
+            keyFn: props.keyFn
+        };
+    }
+
+    return {
+        optionName: props.name,
+        isAnonymous: false,
+        type: "children",
+        content: props.children,
+        keyFn: props.keyFn
+    };
+}
+
+export {
+    getAnonymousTemplate,
+    getNamedTemplate
+};
diff --git a/src/core/configuration/react/tree.ts b/src/core/configuration/react/tree.ts
new file mode 100644
index 0000000..131d914
--- /dev/null
+++ b/src/core/configuration/react/tree.ts
@@ -0,0 +1,131 @@
+import * as React from "react";
+
+import { ITemplateMeta, ITemplateProps } from "../../template";
+import { separateProps } from "../../widget-config";
+
+import { ElementType, getElementInfo, IExpectedChild, IOptionElement } from "./element";
+
+import { IConfigNode, ITemplate } from "../config-node";
+import { mergeNameParts } from "../utils";
+import { getAnonymousTemplate, getNamedTemplate } from "./templates";
+
+interface IWidgetDescriptor {
+    templates: ITemplateMeta[];
+    initialValuesProps: Record<string, string>;
+    predefinedValuesProps: Record<string, any>;
+    expectedChildren: Record<string, IExpectedChild>;
+}
+
+function buildConfigTree(widgetDescriptor: IWidgetDescriptor, props: Record<string, any>) {
+    return createConfigNode(
+        {
+            type: ElementType.Option,
+            descriptor: {
+                name: "",
+                isCollection: false,
+                ...widgetDescriptor
+            },
+            props,
+        },
+        ""
+    );
+}
+
+function createConfigNode(element: IOptionElement, path: string): IConfigNode {
+    const fullName = element.descriptor.isCollection
+        ? path
+        : mergeNameParts(path, element.descriptor.name);
+
+    const separatedValues = separateProps(
+        element.props,
+        element.descriptor.initialValuesProps,
+        element.descriptor.templates
+    );
+
+    const childrenData = processChildren(element, fullName);
+
+    for (const templateMeta of element.descriptor.templates) {
+        const template = getAnonymousTemplate(
+            element.props,
+            templateMeta,
+            path.length > 0 ? childrenData.hasTranscludedContent : false
+        );
+        if (template) {
+            childrenData.templates.push(template);
+        }
+    }
+
+    return {
+        fullName,
+        predefinedOptions: element.descriptor.predefinedValuesProps,
+        initialOptions: separatedValues.defaults,
+        options: separatedValues.options,
+        templates: childrenData.templates,
+        configCollections: childrenData.configCollections,
+        configs: childrenData.configs
+    };
+}
+
+function processChildren(parentElement: IOptionElement, parentFullName: string) {
+    const templates: ITemplate[] = [];
+    const configCollections: Record<string, IConfigNode[]> = {};
+    const configs: Record<string, IConfigNode> = {};
+    let hasTranscludedContent: boolean = false;
+
+    React.Children.map(
+        parentElement.props.children,
+        (child) => {
+            const element = getElementInfo(child, parentElement.descriptor.expectedChildren);
+            if (element.type === ElementType.Unknown) {
+                hasTranscludedContent = true;
+                return;
+            }
+
+            if (element.type === ElementType.Template) {
+                const template = getNamedTemplate(element.props as ITemplateProps);
+
+                if (template) {
+                    templates.push(template);
+                }
+                return;
+            }
+
+            if (element.descriptor.isCollection) {
+                let collection = configCollections[element.descriptor.name];
+                if (!collection) {
+                    collection = [];
+                    configCollections[element.descriptor.name] = collection;
+                }
+
+                const collectionItem = createConfigNode(
+                    element,
+                    mergeNameParts(
+                        parentFullName,
+                        element.descriptor.name
+                    ) + "[" + collection.length + "]"
+                );
+
+                collection.push(collectionItem);
+                return;
+            }
+
+            const configNode = createConfigNode(
+                element,
+                parentFullName
+            );
+
+            configs[element.descriptor.name] = configNode;
+        }
+    );
+
+    return {
+        configs,
+        configCollections,
+        templates,
+        hasTranscludedContent
+    };
+}
+
+export {
+    buildConfigTree
+};
diff --git a/src/core/configuration/tree.ts b/src/core/configuration/tree.ts
new file mode 100644
index 0000000..9c10c00
--- /dev/null
+++ b/src/core/configuration/tree.ts
@@ -0,0 +1,156 @@
+import { IConfigNode, ITemplate } from "./config-node";
+import { mergeNameParts, parseOptionName } from "./utils";
+
+interface IConfig {
+    options: Record<string, any>;
+    templates: Record<string, ITemplate>;
+}
+
+function buildConfig(root: IConfigNode, ignoreInitialValues: boolean): IConfig {
+    const templatesAccum: Record<string, ITemplate> = {};
+    const options = buildNode(root, templatesAccum, ignoreInitialValues);
+
+    return {
+        templates: templatesAccum,
+        options
+    };
+}
+
+function buildNode(
+    node: IConfigNode,
+    templatesAccum: Record<string, ITemplate>,
+    ignoreInitialValues: boolean
+): Record<string, any> {
+    const result: Record<string, any> = {};
+
+    for (const key of Object.keys(node.predefinedOptions)) {
+        result[key] = node.predefinedOptions[key];
+    }
+
+    for (const key of Object.keys(node.configs)) {
+        result[key] = buildNode(node.configs[key], templatesAccum, ignoreInitialValues);
+    }
+
+    for (const key of Object.keys(node.configCollections)) {
+        result[key] = node.configCollections[key].map(
+            (item) => buildNode(item, templatesAccum, ignoreInitialValues)
+        );
+    }
+
+    if (!ignoreInitialValues) {
+        for (const key of Object.keys(node.initialOptions)) {
+            result[key] = node.initialOptions[key];
+        }
+    }
+
+    for (const key of Object.keys(node.options)) {
+        result[key] = node.options[key];
+    }
+
+    buildTemplates(node, result, templatesAccum);
+
+    return result;
+}
+
+function buildTemplates(
+    node: IConfigNode,
+    optionsAccum: Record<string, any>,
+    templatesAccum: Record<string, ITemplate>
+) {
+    node.templates.map(
+        (template) => {
+            if (template.isAnonymous) {
+                const templateName = mergeNameParts(node.fullName, template.optionName);
+                optionsAccum[template.optionName] = templateName;
+                templatesAccum[templateName] = template;
+            } else {
+                templatesAccum[template.optionName] = template;
+            }
+        }
+    );
+}
+
+interface IValueDescriptor {
+    value: any;
+    type: ValueType;
+}
+
+enum ValueType {
+    Simple,
+    Complex,
+    Array
+}
+
+function findValue(node: IConfigNode, path: string[]): undefined | IValueDescriptor {
+    const name = path.shift();
+
+    if (!name) {
+        return {
+            value: buildConfig(node, true).options,
+            type: ValueType.Complex
+        };
+    }
+
+    const optionInfo = parseOptionName(name);
+    if (optionInfo.isCollectionItem) {
+        const collection = node.configCollections[optionInfo.name];
+        if (!collection) {
+            return;
+        }
+
+        const item = collection[optionInfo.index];
+        if (!item) {
+            return;
+        }
+
+        return findValue(item, path);
+    }
+
+    const child = node.configs[optionInfo.name];
+    if (child) {
+        return findValue(child, path);
+    }
+
+    const childCollection = node.configCollections[optionInfo.name];
+    if (childCollection) {
+        if (path.length !== 0) {
+            return;
+        }
+
+        return {
+            value: childCollection.map((item) => buildNode(item, {}, true)),
+            type: ValueType.Array
+        };
+    }
+
+    const value = node.options[optionInfo.name];
+    if (value) {
+        return findValueInObject(value, path);
+    }
+
+    return;
+}
+
+function findValueInObject(obj: any, path: string[]): undefined | IValueDescriptor {
+    if (!obj) {
+        return;
+    }
+
+    const key = path.shift();
+    if (!key) {
+        return {
+            value: obj,
+            type: ValueType.Simple
+        };
+    }
+
+    return findValueInObject(obj[key], path);
+}
+
+export {
+    ValueType,
+    buildConfig,
+    buildNode,
+    buildTemplates,
+    findValue
+};
diff --git a/src/core/configuration/utils.ts b/src/core/configuration/utils.ts
new file mode 100644
index 0000000..be788a8
--- /dev/null
+++ b/src/core/configuration/utils.ts
@@ -0,0 +1,31 @@
+export function mergeNameParts(...args: string[]) {
+    return args.filter((value) => value).join(".");
+}
+
+export function parseOptionName(name: string): IOptionInfo | ICollectionOptionInfo {
+    const parts = name.split("[");
+
+    if (parts.length === 1) {
+        return {
+            isCollectionItem: false,
+            name
+        };
+    }
+
+    return {
+        isCollectionItem: true,
+        name: parts[0],
+        index: Number(parts[1].slice(0, -1))
+    };
+}
+
+interface IOptionInfo {
+    isCollectionItem: false;
+    name: string;
+}
+
+interface ICollectionOptionInfo {
+    isCollectionItem: true;
+    name: string;
+    index: number;
+}
diff --git a/src/core/dx-template.ts b/src/core/dx-template.ts
index 5cce725..98459f2 100644
--- a/src/core/dx-template.ts
+++ b/src/core/dx-template.ts
@@ -55,7 +55,7 @@ function createDxTemplate(
                         container,
                         onRemoved: () => {
                             templatesStore.remove(templateId);
-                            renderedTemplates.delete({ key1: props, key2: container });
+                            renderedTemplates.delete({ key1: data.model, key2: container });
                         },
                         onRendered: data.onRendered,
                         key: templateId
diff --git a/src/core/extension-component.ts b/src/core/extension-component.ts
index e6f7e82..e8b94e7 100644
--- a/src/core/extension-component.ts
+++ b/src/core/extension-component.ts
@@ -1,4 +1,3 @@
-
 import { ComponentBase } from "./component-base";
 
 class ExtensionComponent<P> extends ComponentBase<P> {
diff --git a/src/core/helpers.ts b/src/core/helpers.ts
index 0a71be9..cd6315a 100644
--- a/src/core/helpers.ts
+++ b/src/core/helpers.ts
@@ -1,33 +1,7 @@
-export function getNestedValue(obj: object, keyParts: string[]): any {
-    if (!obj) {
-        return;
-    }
-
-    let current = obj;
-    keyParts.some((part) => {
-        current = current[part];
-
-        return current === undefined || current === null;
-    });
-
-    return current;
-}
-
-export function addPrefixToKeys(obj: Record<string, any>, prefix: string): Record<string, any> {
-    const result = {};
-    Object.keys(obj).forEach((key) => result[prefix + key] = obj[key]);
-
-    return result;
-}
-
 export function generateID(): string {
     return Math.random().toString(36).substr(2);
 }
 
-export function isEmptyObject(obj: Record<any, any>): boolean {
-    return Object.keys(obj).length === 0;
-}
-
 export class DoubleKeyMap<TKey1, TKey2, TValue> {
     private readonly _map: Map<TKey1, Map<TKey2, TValue>> = new Map();
 
diff --git a/src/core/nested-option.ts b/src/core/nested-option.ts
index 288ee0c..1411a9c 100644
--- a/src/core/nested-option.ts
+++ b/src/core/nested-option.ts
@@ -1,8 +1,5 @@
 import * as React from "react";
-
-interface INestedOptionProps {
-    meta?: INestedOptionMeta;
-}
+import { ElementType, getElementInfo } from "./configuration/react/element";
 
 interface INestedOptionMeta {
     optionName: string;
@@ -13,64 +10,24 @@ interface INestedOptionMeta {
 
 class NestedOption<P> extends React.PureComponent<P, any> {
 
-    private readonly _meta?: INestedOptionMeta;
-
-    constructor(props: P & INestedOptionProps) {
-        super(props);
-
-        this._meta = props.meta;
-
-        this._markOptionAsDirty();
-    }
-
     public render() {
-        if (!this.props.children || !this._meta) { return null; }
-
-        const registerNestedOption = this._meta.registerNestedOption;
+        const children = React.Children.map(
+            this.props.children,
+            (child) => {
+                const childElementInfo = getElementInfo(child);
 
-        const children: any[] = [];
-        React.Children.forEach(this.props.children, (c: React.ReactElement<any>) => {
-            const processedChild = registerNestedOption(c);
-            if (processedChild) {
-                children.push(processedChild);
+                return childElementInfo.type === ElementType.Option ? child : null;
             }
-        });
-
-        return children.length === 0
-            ? null
-            : React.createElement(React.Fragment, {}, ...children);
-    }
-
-    public componentDidUpdate(prevProps: P) {
-        if (this._meta) {
-            this._meta.updateFunc(getCleanProps(this.props), prevProps);
-        }
-    }
-
-    public componentWillUnmount() {
-        this._markOptionAsDirty();
-    }
-
-    private _markOptionAsDirty() {
-        if (this._meta) {
-            this._meta.makeDirty();
-        }
+        );
+        return React.createElement(
+            React.Fragment,
+            {},
+            children
+        );
     }
 }
 
-function getCleanProps(props: any) {
-    const { meta, ...cleanProps } = props;
-
-    return cleanProps;
-}
-
-function createOptionComponent<P>(rawElement: React.ReactElement<P>, meta: INestedOptionMeta) {
-    const props: INestedOptionProps = { meta };
-    return React.cloneElement(rawElement, props as any);
-}
-
 export default NestedOption;
 export {
-    createOptionComponent,
     INestedOptionMeta
 };
diff --git a/src/core/options-manager.ts b/src/core/options-manager.ts
index 3ceb159..0a998d6 100644
--- a/src/core/options-manager.ts
+++ b/src/core/options-manager.ts
@@ -1,385 +1,130 @@
-import { ITemplateMeta } from "./template";
-
-import { Children as ReactChildren } from "react";
-import { addPrefixToKeys, getNestedValue } from "./helpers";
-import { createOptionComponent, INestedOptionMeta } from "./nested-option";
 import TemplatesManager from "./templates-manager";
-import { separateProps } from "./widget-config";
-
-interface INestedOption {
-    optionName: string;
-    isCollectionItem: boolean;
-}
-
-interface INestedConfigDescr extends INestedOption {
-    optionFullName: string;
-    defaults: Record<string, any>;
-    templates: ITemplateMeta[];
-    elementEntries: Array<{
-        element: React.ReactElement<any>;
-        children: Record<string, INestedConfigDescr>;
-        predefinedProps: Record<string, any>;
-    }>;
-}
-
-interface INestedConfigClass {
-    type: {
-        IsCollectionItem: boolean;
-        OptionName: string;
-        DefaultsProps: Record<string, string>;
-        TemplateProps: ITemplateMeta[];
-        PredefinedProps: Record<string, any>;
-        ExpectedChildren: Record<string, INestedOption>;
-    };
-    props: object;
-}
 
-function isEventHanlder(optionName: string, optionValue: any) {
-    return optionName.substr(0, 2) === "on" && typeof optionValue === "function";
-}
+import { getChanges } from "./configuration/comparer";
+import { IConfigNode } from "./configuration/config-node";
+import { buildConfig, findValue, ValueType } from "./configuration/tree";
+import { mergeNameParts } from "./configuration/utils";
 
 class OptionsManager {
-
     private readonly _guards: Record<string, number> = {};
-    private readonly _nestedOptions: Record<string, INestedConfigDescr> = {};
-    private readonly _optionValueGetter: (name: string) => any;
-    private readonly _templatesManager: TemplatesManager;
-
+    private _templatesManager: TemplatesManager;
     private _instance: any;
+    private _isUpdating = false;
+    private _currentConfig: IConfigNode;
 
-    private _updatingProps: boolean;
-    private _dirtyOptions: Record<string, INestedConfigDescr> = {};
-
-    constructor(optionValueGetter: (name: string) => any, templateHost: TemplatesManager) {
-        this._optionValueGetter = optionValueGetter;
-        this._templatesManager = templateHost;
+    constructor(templatesManager: TemplatesManager) {
+        this._templatesManager = templatesManager;
 
-        this._setOption = this._setOption.bind(this);
-        this._syncOptions = this._syncOptions.bind(this);
-        this._registerNestedOption = this._registerNestedOption.bind(this);
-        this.registerNestedOption = this.registerNestedOption.bind(this);
-        this.handleOptionChange = this.handleOptionChange.bind(this);
-        this.updateOptions = this.updateOptions.bind(this);
-    }
-
-    public resetNestedElements() {
-        Object.keys(this._nestedOptions).forEach((optionName) => {
-            this._nestedOptions[optionName].elementEntries.length = 0;
-        });
+        this.onOptionChanged = this.onOptionChanged.bind(this);
+        this._wrapOptionValue = this._wrapOptionValue.bind(this);
     }
 
-    public setInstance(instance: any) {
+    public setInstance(instance: any, config: IConfigNode) {
         this._instance = instance;
+        this._currentConfig = config;
     }
 
-    public wrapEventHandlers(options: Record<string, any>) {
-        Object.keys(options).forEach((name) => {
-            const value = options[name];
-            if (isEventHanlder(name, value)) {
-                options[name] = this._wrapEventHandler(value);
-            }
-        });
-    }
+    public getInitialOptions(rootNode: IConfigNode) {
+        const config = buildConfig(rootNode, false);
 
-    public handleOptionChange(e: { name: string, fullName: string, value: any }) {
-        if (this._updatingProps) {
-            return;
+        for (const key of Object.keys(config.templates)) {
+            this._templatesManager.add(key, config.templates[key]);
         }
 
-        let optionValue;
-        let optionName;
-
-        const nestedOption = this._nestedOptions[e.name];
-        if (nestedOption && nestedOption.elementEntries.length > 0) {
-            optionName = e.fullName;
-            const nestedOptionObj = separateProps(
-                nestedOption.elementEntries[0].element.props,
-                nestedOption.defaults,
-                []
-            ).options;
-
-            if (e.name === e.fullName) {
-                Object.keys(nestedOptionObj).forEach((key) => this.handleOptionChange({
-                    name: e.name,
-                    fullName: `${e.fullName}.${key}`,
-                    value: e.value[key]
-                }));
-
-                return;
-            }
+        const options: Record<string, any> = {};
 
-            if (nestedOption.isCollectionItem) { return; }
-
-            optionValue = getNestedValue(nestedOptionObj, e.fullName.split(".").slice(1));
-        } else {
-            const fullNameParts = e.fullName.split(".");
-
-            if (fullNameParts.length > 1) {
-                optionName = e.fullName;
-                optionValue = getNestedValue(
-                    this._optionValueGetter(e.name),
-                    fullNameParts.slice(1)
-                );
-            } else {
-                optionName = e.name;
-                optionValue = this._optionValueGetter(e.name);
-            }
+        for (const key of Object.keys(config.options)) {
+            options[key] = this._wrapOptionValue(key, config.options[key]);
         }
 
-        if (optionValue === undefined || optionValue === null) {
-            return;
+        if (this._templatesManager.templatesCount > 0) {
+            options.integrationOptions = {
+                templates: this._templatesManager.templates
+            };
         }
 
-        this._setGuard(optionName, optionValue);
-    }
-
-    public updateOptions(newProps: Record<string, any>, prevProps: Record<string, any>): void {
-        const nestedOptions: Record<string, any> = {};
-
-        Object.keys(this._dirtyOptions).forEach((optionName) => {
-            const optionDescr = this._dirtyOptions[optionName];
-            const optionValue = this._getNestedOptionObj(optionDescr, true);
-
-            nestedOptions[optionName] = optionValue;
-        });
-
-        const newOptions: Record<string, any> = {
-            ...this._templatesManager.options,
-            ...nestedOptions,
-            ...newProps
-        };
-
-        this._syncOptions(newOptions, prevProps);
-
-        this._dirtyOptions = {};
+        return options;
     }
 
-    public getNestedOptionsObjects(): Record<string, any> {
-        return this._getNestedOptionsObjects(this._nestedOptions, true);
-    }
+    public update(config: IConfigNode) {
+        const changes = getChanges(config, this._currentConfig);
 
-    public registerNestedOption(
-        component: React.ReactElement<any>,
-        expectedChildren: Record<string, INestedOption>
-    ): any {
-        return this._registerNestedOption(component, expectedChildren, this._nestedOptions);
-    }
-
-    public dispose() {
-        for (const optionName of Object.keys(this._guards)) {
-            window.clearTimeout(this._guards[optionName]);
-            delete this._guards[optionName];
+        for (const key of Object.keys(changes.templates)) {
+            this._templatesManager.add(key, changes.templates[key]);
         }
-    }
-
-    private _syncOptions(
-        newOptions: Record<string, any>,
-        prevOptions: Record<string, any>
-    ): void {
-        this._updatingProps = false;
-
-        for (const optionName of Object.keys(newOptions)) {
-            if (newOptions[optionName] === prevOptions[optionName]) {
-                continue;
-            }
-
-            if (this._guards[optionName]) {
-                window.clearTimeout(this._guards[optionName]);
-                delete this._guards[optionName];
-            }
 
-            if (!this._updatingProps) {
-                this._instance.beginUpdate();
-                this._updatingProps = true;
-            }
-            this._setOption(optionName, newOptions[optionName]);
+        if (this._templatesManager.templatesCount > 0) {
+            this._setValueInTransaction(
+                "integrationOptions",
+                {
+                    templates: this._templatesManager.templates
+                }
+            );
         }
 
-        if (this._updatingProps) {
-            this._updatingProps = false;
-            this._instance.endUpdate();
+        for (const key of Object.keys(changes.options)) {
+            this._setValueInTransaction(key, changes.options[key]);
         }
-    }
 
-    private _setOption(name: string, value: any): void {
-        let actualValue = value;
-        if (isEventHanlder(name, value)) {
-            actualValue = this._wrapEventHandler(value);
+        if (this._isUpdating) {
+            this._isUpdating = false;
+            this._instance.endUpdate();
         }
 
-        this._instance.option(name, actualValue);
+        this._currentConfig = config;
     }
 
-    private _wrapEventHandler(handler: any) {
-        return (...args: any[]) => {
-            if (!this._updatingProps) {
-                handler(...args);
-            }
-        };
-    }
-
-    private _getNestedOptionsObjects(
-        optionsCollection: Record<string, INestedConfigDescr>,
-        templateRegistrationRequired: boolean
-    ): Record<string, any> {
-        const configComponents: Record<string, any> = {};
-
-        Object.keys(optionsCollection).forEach((key) => {
-            const configComponent = optionsCollection[key];
-            configComponents[configComponent.optionName] = this._getNestedOptionObj(
-                configComponent,
-                templateRegistrationRequired
-            );
-        });
-
-        return configComponents;
-    }
-
-    private _getNestedOptionObj(
-        configComponent: INestedConfigDescr,
-        templateRegistrationRequired: boolean
-    ): Record<string, any> | any {
-        const options = configComponent.elementEntries.map((e, index) => {
-            const props = separateProps(
-                e.element.props,
-                configComponent.defaults,
-                configComponent.templates
-            );
-
-            const nestedObjects = this._getNestedOptionsObjects(e.children, templateRegistrationRequired);
-            const nestedObjectsCount =  Object.keys(nestedObjects).reduce((acc, item) => {
-                const obj = nestedObjects[item];
-                return acc + (Array.isArray(obj) ? obj.length : 1);
-            }, 0);
-
-            const hasChildrenForTemplate =
-                ReactChildren.count(e.element.props.children) > nestedObjectsCount;
-
-            let templatesOptions = {};
-            if (templateRegistrationRequired) {
-                templatesOptions = this._templatesManager.add({
-                    useChildren: (optionName) => {
-                        return optionName === "template" && hasChildrenForTemplate;
-                    },
-                    props: props.templates,
-                    templateProps: configComponent.templates,
-                    ownerName: this.buildOptionItemName(
-                        configComponent,
-                        configComponent.isCollectionItem ? index : undefined,
-                    ),
-                    propsGetter: (prop) => {
-                        const nestedElement = configComponent.elementEntries[index];
-                        if (!nestedElement) {
-                            return;
-                        }
-
-                        return nestedElement.element.props[prop];
-                    }
-                });
-            }
-
-            return {
-                ...e.predefinedProps,
-                ...props.defaults,
-                ...props.options,
-                ...nestedObjects,
-                ...templatesOptions
-            };
-        });
-
-        return configComponent.isCollectionItem
-            ? options
-            : options[options.length - 1];
-    }
-
-    private _registerNestedOption(
-        element: React.ReactElement<any>,
-        expectedChildren: Record<string, INestedOption>,
-        owningCollection: Record<string, INestedConfigDescr>,
-        ownerFullName?: string
-    ): any {
-        const nestedOptionClass = element as any as INestedConfigClass;
-        if (!(nestedOptionClass && nestedOptionClass.type && nestedOptionClass.type.OptionName)) {
-            return null;
+    public onOptionChanged(e: { name: string, fullName: string, value: any }) {
+        if (this._isUpdating) {
+            return;
         }
 
-        const nestedOptionsCollection: Record<string, INestedConfigDescr> = {};
-
-        const resolvedNested = resolveNestedOption(
-            nestedOptionClass.type.OptionName,
-            nestedOptionClass.type.IsCollectionItem,
-            expectedChildren
-        );
-        const optionName = resolvedNested.optionName;
-        const isCollectionItem = resolvedNested.isCollectionItem;
-
-        let optionFullName = optionName;
-        if (ownerFullName) {
-            optionFullName = `${ownerFullName}.${optionFullName}`;
+        const valueDescriptor = findValue(this._currentConfig, e.fullName.split("."));
+        if (!valueDescriptor) {
+            return;
         }
 
-        const option = ensureNestedOption(
-            optionName,
-            optionFullName,
-            owningCollection,
-            nestedOptionClass.type.DefaultsProps,
-            nestedOptionClass.type.TemplateProps,
-            isCollectionItem
-        );
-
-        const optionItemName = this.buildOptionItemName(
-            option,
-            isCollectionItem ? option.elementEntries.length : undefined
-        );
-
-        const nestedOptionMeta: INestedOptionMeta = {
-            optionName,
-            registerNestedOption: (c: React.ReactElement<any>) => {
-                return this._registerNestedOption(
-                    c,
-                    nestedOptionClass.type.ExpectedChildren,
-                    nestedOptionsCollection,
-                    optionItemName
-                );
-            },
-            updateFunc: (newProps, prevProps) => {
-                const newOptions = separateProps(
-                    newProps,
-                    nestedOptionClass.type.DefaultsProps,
-                    nestedOptionClass.type.TemplateProps
-                ).options;
-
-                this._syncOptions(
-                    addPrefixToKeys(newOptions, optionItemName + "."),
-                    addPrefixToKeys(prevProps, optionItemName + ".")
-                );
-            },
-            makeDirty: () => {
-                if (this._instance && option.isCollectionItem) {
-                    this._dirtyOptions[option.optionName] = option;
+        const { value, type } = valueDescriptor;
+        if (type === ValueType.Complex) {
+            for (const key of Object.keys(value)) {
+                if (
+                    value[key] === null ||
+                    value[key] === undefined ||
+                    value[key] === e.value[key]
+                ) {
+                    continue;
                 }
+                this._setGuard(mergeNameParts(e.fullName, key), value[key]);
             }
-        };
-
-        const optionComponent = createOptionComponent(element, nestedOptionMeta);
-
-        option.elementEntries.push({
-            element,
-            children: nestedOptionsCollection,
-            predefinedProps: nestedOptionClass.type.PredefinedProps
-        });
-
-        return optionComponent;
+        } else {
+            if (
+                value === null ||
+                value === undefined ||
+                value === e.value
+            ) {
+                return;
+            }
+            this._setGuard(e.fullName, value);
+        }
     }
 
-    private buildOptionItemName(descr: INestedConfigDescr, index?: number): string {
-        let optionItemName = descr.optionFullName;
+    public dispose() {
+        for (const optionName of Object.keys(this._guards)) {
+            window.clearTimeout(this._guards[optionName]);
+            delete this._guards[optionName];
+        }
+    }
 
-        if (index !== undefined) {
-            optionItemName += `[${index}]`;
+    private _wrapOptionValue(name: string, value: any) {
+        if (name.substr(0, 2) === "on" && typeof value === "function") {
+            return (...args: any[]) => {
+                if (!this._isUpdating) {
+                    value(...args);
+                }
+            };
         }
 
-        return optionItemName;
+        return value;
     }
 
     private _setGuard(optionName: string, optionValue: any): void {
@@ -388,61 +133,36 @@ class OptionsManager {
         }
 
         const guardId = window.setTimeout(() => {
-            this._setOption(optionName, optionValue);
+            this._setValue(optionName, optionValue);
             window.clearTimeout(guardId);
             delete this._guards[optionName];
         });
 
         this._guards[optionName] = guardId;
     }
-}
 
-function ensureNestedOption(
-    optionName: string,
-    optionFullName: string,
-    optionsCollection: Record<string, INestedConfigDescr>,
-    defaults: Record<string, any>,
-    templates: ITemplateMeta[],
-    isCollectionItem: boolean
-): INestedConfigDescr {
+    private _setValueInTransaction(name: string, value: any) {
+        if (!this._isUpdating) {
+            this._instance.beginUpdate();
+            this._isUpdating = true;
+        }
 
-    if (optionsCollection[optionName] === null ||
-        optionsCollection[optionName] === undefined
-    ) {
-        optionsCollection[optionName] = {
-            optionName,
-            optionFullName,
-            defaults,
-            templates,
-            elementEntries: [],
-            isCollectionItem
-        };
+        this._setValue(name, value);
     }
 
-    return optionsCollection[optionName];
-}
-
-function resolveNestedOption(
-    componentName: string,
-    canBeCollectionItem: boolean,
-    expectations: Record<string, INestedOption>
-): INestedOption {
-    let optionName = componentName;
-    let isCollectionItem = canBeCollectionItem;
-
-    const expectation = expectations && expectations[componentName];
-    if (expectation) {
-        isCollectionItem = expectation.isCollectionItem;
-        if (expectation.optionName) {
-            optionName = expectation.optionName;
+    private _setValue(name: string, value: any) {
+        if (this._guards[name]) {
+            window.clearTimeout(this._guards[name]);
+            delete this._guards[name];
         }
-    }
 
-    return { optionName, isCollectionItem };
+        this._instance.option(
+            name,
+            this._wrapOptionValue(name, value)
+        );
+    }
 }
 
-export default OptionsManager;
 export {
-    INestedOption,
-    resolveNestedOption
+    OptionsManager
 };
diff --git a/src/core/template.ts b/src/core/template.ts
index 81ac652..ac0ac63 100644
--- a/src/core/template.ts
+++ b/src/core/template.ts
@@ -59,5 +59,5 @@ export {
     ITemplateProps,
     ITemplateArgs,
     Template,
-    findProps
+    findProps,
 };
diff --git a/src/core/templates-manager.ts b/src/core/templates-manager.ts
index cee3d13..6d1720b 100644
--- a/src/core/templates-manager.ts
+++ b/src/core/templates-manager.ts
@@ -1,20 +1,11 @@
 import * as React from "react";
 
 import { getOption as getConfigOption } from "./config";
-import { createDxTemplate, IDxTemplate } from "./dx-template";
-import { ITemplateArgs, ITemplateMeta, ITemplateProps } from "./template";
+import { ITemplate } from "./configuration/config-node";
+import { createDxTemplate } from "./dx-template";
+import { ITemplateArgs } from "./template";
 import { TemplatesStore } from "./templates-store";
 
-type PropsGetter = (propName: string) => any;
-
-interface IIntegrationDescr {
-    props: Record<string, any>;
-    templateProps: ITemplateMeta[];
-    ownerName: string;
-    propsGetter: PropsGetter;
-    useChildren: (name: string) => boolean;
-}
-
 function normalizeProps(props: ITemplateArgs): ITemplateArgs | ITemplateArgs["data"] {
     if (getConfigOption("useLegacyTemplateEngine")) {
         const model = props.data;
@@ -26,105 +17,45 @@ function normalizeProps(props: ITemplateArgs): ITemplateArgs | ITemplateArgs["da
     return props;
 }
 
+type ContentGetter = () => any;
+
 const contentCreators = {
-    component: (name: string, propsGetter: PropsGetter) => (props: ITemplateArgs) => {
+    component: (contentGetter: ContentGetter) => (props: ITemplateArgs) => {
         props = normalizeProps(props);
-        return React.createElement.bind(null, propsGetter(name))(props);
+        return React.createElement.bind(null, contentGetter())(props);
     },
-    render: (name: string, propsGetter: PropsGetter) => (props: ITemplateArgs) => {
+    render: (contentGetter: ContentGetter) => (props: ITemplateArgs) => {
         normalizeProps(props);
-        return propsGetter(name)(props.data, props.index);
+        return contentGetter()(props.data, props.index);
     },
-    children: (_: string, propsGetter: PropsGetter) => () => propsGetter("children")
+    children: (contentGetter: ContentGetter) => () => contentGetter()
 };
 
 class TemplatesManager {
     private _templatesStore: TemplatesStore;
     private _templates: Record<string, any> = {};
-    private _nestedTemplateProps: Record<string, {
-        render: any;
-        component: any;
-        children: any;
-    }> = {};
+    private _templatesContent: Record<string, any> = {};
 
     constructor(templatesStore: TemplatesStore) {
         this._templatesStore = templatesStore;
     }
 
-    public add(meta: IIntegrationDescr): Record<string, any> {
-        const templates: Record<string, IDxTemplate> = {};
-        const templatesOptions: Record<string, any> = {};
-
-        const props = meta.props;
-        const templateProps = meta.templateProps || [];
-        const ownerName = meta.ownerName;
-
-        for (const tmpl of templateProps) {
-            let contentCreator;
-            let propName;
-
-            if (meta.useChildren(tmpl.tmplOption)) {
-                contentCreator = contentCreators.children;
-            }
-
-            if (props[tmpl.render]) {
-                propName = tmpl.render;
-                contentCreator = contentCreators.render;
-            }
-
-            if (props[tmpl.component]) {
-                propName = tmpl.component;
-                contentCreator = contentCreators.component;
-            }
-
-            if (!contentCreator) {
-                continue;
-            }
-
-            contentCreator = contentCreator.bind(this, propName, meta.propsGetter);
-
-            const name = ownerName ? `${ownerName}.${tmpl.tmplOption}` : tmpl.tmplOption;
-            templatesOptions[tmpl.tmplOption] = name;
-            templates[name] = createDxTemplate(
-                contentCreator,
-                this._templatesStore,
-                meta.propsGetter(tmpl.keyFn)
-            );
-        }
-
-        this._templates = {
-            ...this._templates,
-            ...templates
-        };
-
-        return templatesOptions;
+    public add(name: string, template: ITemplate) {
+        this._templatesContent[name] = template.content;
+        const contentCreator = contentCreators[template.type].bind(this, () => this._templatesContent[name]);
+        this._templates[name] = createDxTemplate(
+            contentCreator,
+            this._templatesStore,
+            template.keyFn
+        );
     }
 
-    public addNested(props: ITemplateProps): void {
-        const name: string = props.name;
-        this._nestedTemplateProps[name] = {
-            component: props.component,
-            render: props.render,
-            children: props.children
-        };
-
-        const type = !!props.component ? "component" : !!props.render ? "render" : "children";
-        const propsGetter: PropsGetter = (prop) => this._nestedTemplateProps[name][prop];
-
-        const contentCreator = contentCreators[type].bind(this, type, propsGetter);
-        this._templates[name] = createDxTemplate(contentCreator, this._templatesStore, props.keyFn);
+    public get templatesCount(): number {
+        return Object.keys(this._templates).length;
     }
 
-    public get options(): Record<string, any> | undefined {
-        if (!Object.keys(this._templates).length) {
-            return;
-        }
-
-        return {
-            integrationOptions: {
-                templates: this._templates
-            }
-        };
+    public get templates(): Record<string, any> {
+        return this._templates;
     }
 }
 
diff --git a/src/core/templates-renderer.tsx b/src/core/templates-renderer.tsx
new file mode 100644
index 0000000..2176fe1
--- /dev/null
+++ b/src/core/templates-renderer.tsx
@@ -0,0 +1,33 @@
+import { deferUpdate } from "devextreme/core/utils/common";
+import * as React from "react";
+
+import { TemplatesStore } from "./templates-store";
+
+class TemplatesRenderer extends React.PureComponent<{ templatesStore: TemplatesStore }> {
+    private _updateScheduled: boolean = false;
+
+    public scheduleUpdate() {
+        if (this._updateScheduled) {
+            return;
+        }
+
+        this._updateScheduled = true;
+
+        deferUpdate(() => {
+            this.forceUpdate();
+            this._updateScheduled = false;
+        });
+    }
+
+    public render() {
+        return React.createElement(
+            React.Fragment,
+            {},
+            this.props.templatesStore.renderWrappers()
+        );
+    }
+}
+
+export {
+    TemplatesRenderer
+};


Test patch:
diff --git a/src/core/__tests__/component.test.tsx b/src/core/__tests__/component.test.tsx
index 07f724d..50d7053 100644
--- a/src/core/__tests__/component.test.tsx
+++ b/src/core/__tests__/component.test.tsx
@@ -28,14 +28,6 @@ describe("rendering", () => {
         expect(WidgetClass.mock.calls[0][1]).toEqual({ templatesRenderAsynchronously: true });
     });
 
-    it("does not create empty children prop", () => {
-        const component = shallow(
-            <TestComponent />
-        );
-
-        expect(Object.keys(component.props())).toEqual([]);
-    });
-
     it("creates nested component", () => {
         mount(
             <TestComponent>
diff --git a/src/core/__tests__/props-updating.test.tsx b/src/core/__tests__/props-updating.test.tsx
index 77b0ead..f1cc89a 100644
--- a/src/core/__tests__/props-updating.test.tsx
+++ b/src/core/__tests__/props-updating.test.tsx
@@ -222,6 +222,17 @@ describe("option control", () => {
         expect(Widget.option.mock.calls[0]).toEqual(["everyOption", 123]);
     });
 
+    it("rolls back controlled complex option", () => {
+        shallow(
+            <ControlledComponent complexOption={{a: 123, b: 234}} />
+        );
+
+        fireOptionChange("complexOption", {});
+        jest.runAllTimers();
+        expect(Widget.option.mock.calls.length).toBe(1);
+        expect(Widget.option.mock.calls[0]).toEqual(["complexOption", {a: 123, b: 234}]);
+    });
+
     it("rolls back complex option controlled field", () => {
         shallow(
             <ControlledComponent complexOption={{a: 123}} />
@@ -299,7 +310,6 @@ describe("option control", () => {
         jest.runAllTimers();
         expect(Widget.option.mock.calls.length).toBe(0);
     });
-
 });
 
 describe("option defaults control", () => {
@@ -351,6 +361,34 @@ describe("cfg-component option control", () => {
         expect(Widget.option.mock.calls[0]).toEqual(["nestedOption.a", 123]);
     });
 
+    it("rolls nested collection value back", () => {
+        mount(
+            <TestComponentWithExpectation>
+                <CollectionNestedComponent a={1} />
+                <CollectionNestedComponent a={2} />
+            </TestComponentWithExpectation>
+        );
+
+        fireOptionChange("items", []);
+        jest.runAllTimers();
+        expect(Widget.option.mock.calls.length).toBe(1);
+        expect(Widget.option.mock.calls[0]).toEqual(["items", [{a: 1}, {a: 2}]]);
+    });
+
+    it("rolls nested collection item value back", () => {
+        mount(
+            <TestComponentWithExpectation>
+                <CollectionNestedComponent a={1} />
+                <CollectionNestedComponent a={2} />
+            </TestComponentWithExpectation>
+        );
+
+        fireOptionChange("items[0]", {a: 3});
+        jest.runAllTimers();
+        expect(Widget.option.mock.calls.length).toBe(1);
+        expect(Widget.option.mock.calls[0]).toEqual(["items[0].a", 1]);
+    });
+
     it("rolls cfg-component option value if parent object changes another field", () => {
         mount(
             <ControlledComponent>
diff --git a/src/core/__tests__/template.test.tsx b/src/core/__tests__/template.test.tsx
index 62f4a29..1f85f87 100644
--- a/src/core/__tests__/template.test.tsx
+++ b/src/core/__tests__/template.test.tsx
@@ -125,13 +125,10 @@ function testTemplateOption(testedOption: string) {
         component.setProps(changedElementOptions);
         jest.runAllTimers();
         const optionCalls = Widget.option.mock.calls;
-        expect(optionCalls.length).toBe(2);
+        expect(optionCalls.length).toBe(1);
 
         expect(optionCalls[0][0]).toBe("integrationOptions");
         expect(typeof optionCalls[0][1].templates.item.render).toBe("function");
-
-        expect(optionCalls[1][0]).toBe("item");
-        expect(optionCalls[1][1]).toBe("item");
     });
 
     it("renders inside unwrapped container", () => {
@@ -541,11 +538,11 @@ describe("component/render in nested options", () => {
         const integrationOptions = options.integrationOptions;
 
         expect(Object.keys(integrationOptions.templates)).toEqual([
+            "option.collection[0].template",
             "collection[0].template",
             "collection[1].template",
             "collection[2].option.item",
-            "collection[3].template",
-            "option.collection[0].template"
+            "collection[3].template"
         ]);
     });
 
diff --git a/src/core/configuration/react/__tests__/element.test.ts b/src/core/configuration/react/__tests__/element.test.ts
new file mode 100644
index 0000000..153b404
--- /dev/null
+++ b/src/core/configuration/react/__tests__/element.test.ts
@@ -0,0 +1,102 @@
+// tslint:disable:max-classes-per-file
+import { mount, React } from "../../../__tests__/setup";
+
+import ConfigurationComponent from "../../../../core/nested-option";
+import { Template } from "../../../template";
+
+import { ElementType, getElementInfo } from "../element";
+
+class MinimalConfigurationComponent extends ConfigurationComponent<any> {
+    public static OptionName = "option";
+    public static IsCollectionItem = false;
+}
+
+class RichConfigurationComponent extends ConfigurationComponent<any> {
+    public static OptionName = "option";
+    public static IsCollectionItem = false;
+    public static DefaultsProps = { defaultValue: "value" };
+    public static TemplateProps = [{
+        tmplOption: "template",
+        render: "render",
+        component: "component",
+        keyFn: "keyFn"
+    }];
+    public static PredefinedProps = { type: "numeric" };
+}
+
+class CollectionConfigurationComponent extends ConfigurationComponent<any> {
+    public static OptionName = "option";
+    public static IsCollectionItem = true;
+    public static DefaultsProps = { defaultValue: "value" };
+    public static TemplateProps = [{
+        tmplOption: "template",
+        render: "render",
+        component: "component",
+        keyFn: "keyFn"
+    }];
+    public static PredefinedProps = { type: "numeric" };
+}
+
+const configurationComponents: any[] = [
+    MinimalConfigurationComponent,
+    RichConfigurationComponent,
+    CollectionConfigurationComponent
+];
+
+const otherComponents = [
+    "div",
+    ConfigurationComponent,
+    () => React.createElement("div", {}, "text")
+];
+
+describe("getElementInfo", () => {
+    configurationComponents.map((component) => {
+        it("parses Configuration component", () => {
+            const wrapper = mount(React.createElement(component));
+            const elementInfo = getElementInfo(wrapper.getElement());
+
+            if (elementInfo.type !== ElementType.Option) {
+                expect(elementInfo.type).toEqual(ElementType.Option);
+                return;
+            }
+
+            expect(elementInfo.props).toEqual(wrapper.getElement().props);
+
+            const descriptor = elementInfo.descriptor;
+            expect(descriptor.name).toEqual(component.OptionName);
+            expect(descriptor.isCollection).toEqual(component.IsCollectionItem);
+            expect(descriptor.templates).toEqual(component.TemplateProps || []);
+            expect(descriptor.initialValuesProps).toEqual(component.DefaultsProps || {});
+            expect(descriptor.predefinedValuesProps).toEqual(component.PredefinedProps || {});
+        });
+    });
+
+    it("parses Template component", () => {
+        const wrapper = mount(
+            React.createElement(
+                Template,
+                {
+                    name: "template-name"
+                },
+                "Template content"
+            )
+        );
+
+        const elementInfo = getElementInfo(wrapper.getElement());
+        if (elementInfo.type !== ElementType.Template) {
+            expect(elementInfo.type).toEqual(ElementType.Template);
+            return;
+        }
+
+        expect(elementInfo.props).toEqual(wrapper.getElement().props);
+    });
+
+    otherComponents.map((component) => {
+        it("parses Other components", () => {
+            const wrapper = mount(React.createElement(component));
+            const elementInfo = getElementInfo(wrapper.getElement());
+
+            expect(elementInfo.type).toEqual(ElementType.Unknown);
+        });
+    });
+});
diff --git a/src/core/configuration/react/__tests__/templates.test.ts b/src/core/configuration/react/__tests__/templates.test.ts
new file mode 100644
index 0000000..789bc29
--- /dev/null
+++ b/src/core/configuration/react/__tests__/templates.test.ts
@@ -0,0 +1,268 @@
+import { ITemplateMeta, ITemplateProps } from "../../../template";
+
+import { getAnonymousTemplate, getNamedTemplate } from "../templates";
+
+const children = "some text";
+const render = () => null;
+const component = () => null;
+const keyFn = () => null;
+
+const positiveCasesForAnonymous = [
+    {
+        props: { children },
+        templateMeta: { tmplOption: "template" },
+        hasTranscludedContent: true,
+        expected: {
+            optionName: "template",
+            type: "children",
+            isAnonymous: true,
+            content: children,
+            keyFn: undefined
+        }
+    },
+    {
+        props: { children, itemKeyFn: keyFn },
+        templateMeta: { tmplOption: "template", keyFn: "itemKeyFn" },
+        hasTranscludedContent: true,
+        expected: {
+            optionName: "template",
+            type: "children",
+            isAnonymous: true,
+            content: children,
+            keyFn
+        }
+    },
+    {
+        props: { renderItem: render },
+        templateMeta: { tmplOption: "itemTemplate", render: "renderItem" },
+        hasTranscludedContent: false,
+        expected: {
+            optionName: "itemTemplate",
+            type: "render",
+            isAnonymous: true,
+            content: render,
+            keyFn: undefined
+        }
+    },
+    {
+        props: { renderItem: render, itemKeyFn: keyFn },
+        templateMeta: { tmplOption: "template", render: "renderItem", keyFn: "itemKeyFn" },
+        hasTranscludedContent: false,
+        expected: {
+            optionName: "template",
+            type: "render",
+            isAnonymous: true,
+            content: render,
+            keyFn
+        }
+    },
+    {
+        props: { itemComponent: component },
+        templateMeta: { tmplOption: "template", component: "itemComponent" },
+        hasTranscludedContent: false,
+        expected: {
+            optionName: "template",
+            type: "component",
+            isAnonymous: true,
+            content: component,
+            keyFn: undefined
+        }
+    },
+    {
+        props: { itemComponent: component, itemKeyFn: keyFn },
+        templateMeta: { tmplOption: "template", component: "itemComponent", keyFn: "itemKeyFn" },
+        hasTranscludedContent: false,
+        expected: {
+            optionName: "template",
+            type: "component",
+            isAnonymous: true,
+            content: component,
+            keyFn
+        }
+    }
+];
+
+const negativeCasesForAnonymous = [
+    {
+        props: { children },
+        templateMeta: { tmplOption: "template" },
+        hasTranscludedContent: false
+    },
+    {
+        props: { renderItem: render },
+        templateMeta: { tmplOption: "template" },
+        hasTranscludedContent: false
+    },
+    {
+        props: { renderItem: render },
+        templateMeta: { tmplOption: "template", component: "itemComponent" },
+        hasTranscludedContent: false
+    },
+    {
+        props: { itemComponent: component },
+        templateMeta: { tmplOption: "template", render: "renderItem" },
+        hasTranscludedContent: false
+    }
+];
+
+describe("getAnonymousTemplate", () => {
+    positiveCasesForAnonymous.map(
+        (testCase) => {
+            it("returns template", () => {
+                const template = getAnonymousTemplate(
+                    testCase.props,
+                    testCase.templateMeta as any as ITemplateMeta,
+                    testCase.hasTranscludedContent
+                );
+
+                if (template === null) {
+                    expect(template).not.toBe(null);
+                    return;
+                }
+
+                expect(template.optionName).toBe(testCase.expected.optionName);
+                expect(template.type).toBe(testCase.expected.type);
+                expect(template.isAnonymous).toBe(testCase.expected.isAnonymous);
+                expect(template.content).toBe(testCase.expected.content);
+                expect(template.keyFn).toBe(testCase.expected.keyFn);
+            });
+        }
+    );
+
+    negativeCasesForAnonymous.map(
+        (testCase) => {
+            it("returns null", () => {
+                const template = getAnonymousTemplate(
+                    testCase.props,
+                    testCase.templateMeta as any as ITemplateMeta,
+                    testCase.hasTranscludedContent
+                );
+
+                expect(template).toBe(null);
+            });
+        }
+    );
+});
+
+const casesForNamed = [
+    {
+        props: {
+            name: "template1",
+            component
+        },
+        expected: {
+            optionName: "template1",
+            type: "component",
+            isAnonymous: false,
+            content: component,
+            keyFn: undefined
+        }
+    },
+    {
+        props: {
+            name: "template2",
+            component,
+            keyFn
+        },
+        expected: {
+            optionName: "template2",
+            type: "component",
+            isAnonymous: false,
+            content: component,
+            keyFn
+        }
+    },
+    {
+        props: {
+            name: "template3",
+            render
+        },
+        expected: {
+            optionName: "template3",
+            type: "render",
+            isAnonymous: false,
+            content: render,
+            keyFn: undefined
+        }
+    },
+    {
+        props: {
+            name: "template4",
+            render,
+            keyFn
+        },
+        expected: {
+            optionName: "template4",
+            type: "render",
+            isAnonymous: false,
+            content: render,
+            keyFn
+        }
+    },
+    {
+        props: {
+            name: "template5",
+            children
+        },
+        expected: {
+            optionName: "template5",
+            type: "children",
+            isAnonymous: false,
+            content: children,
+            keyFn: undefined
+        }
+    },
+    {
+        props: {
+            name: "template6",
+            children,
+            keyFn
+        },
+        expected: {
+            optionName: "template6",
+            type: "children",
+            isAnonymous: false,
+            content: children,
+            keyFn
+        }
+    },
+    {
+        props: {
+            name: "template7",
+        },
+        expected: {
+            optionName: "template7",
+            type: "children",
+            isAnonymous: false,
+            content: undefined,
+            keyFn: undefined
+        }
+    },
+];
+
+describe("getNamedTemplate", () => {
+    casesForNamed.map(
+        (testCase) => {
+            it("returns template", () => {
+                const template = getNamedTemplate(testCase.props as ITemplateProps);
+
+                if (template === null) {
+                    expect(template).not.toBe(null);
+                    return;
+                }
+
+                expect(template.optionName).toBe(testCase.expected.optionName);
+                expect(template.type).toBe(testCase.expected.type);
+                expect(template.isAnonymous).toBe(testCase.expected.isAnonymous);
+                expect(template.content).toBe(testCase.expected.content);
+                expect(template.keyFn).toBe(testCase.expected.keyFn);
+            });
+        }
+    );
+
+    it("returns null", () => {
+        const template = getNamedTemplate({} as ITemplateProps);
+
+        expect(template).toBe(null);
+    });
+});