Repository: DevExpress/devextreme-react (js)

Issue:
Style and className props missing on widgets
<!--
  Note: if the issue is a general React question, please check React Docs first: https://reactjs.org/docs
  If your question is about  DevExtreme widgets' APIs, use the DevExpress Support Center: https://www.devexpress.com/Support/Center
-->

### Issue Type
Feature request

### Description
Widgets like Button, TextBox and others are not accepting style and className props. If I want to place two Buttons next to each other with a margin between them, I have to wrap them in a div.

### Environment Details
devextreme 18.1.1-pre-18093
devextreme-react 18.1.1-alpha.4
react 16.3.0
typescript 2.8


Pull request:
Add id, style and className to props
resolves #89 

Patch:
diff --git a/example/app.tsx b/example/app.tsx
index 0dd1fb3..b94e123 100644
--- a/example/app.tsx
+++ b/example/app.tsx
@@ -63,7 +63,7 @@ ReactDOM.render(
     <ChartExample />
 
     <Example title="Element attributes">
-      <Button text="Button with style attr" elementAttr={{ style: "background-color: #ffc" }} />
+      <Button text="Button with style attr" style={{backgroundColor: "#ffc"}}/>
     </Example>
 
   </div>,
diff --git a/src/core/component.ts b/src/core/component.ts
index ff51614..8e004b3 100644
--- a/src/core/component.ts
+++ b/src/core/component.ts
@@ -3,7 +3,7 @@ import * as React from "react";
 
 import OptionsManager from "./options-manager";
 import { findProps as findNestedTemplateProps, ITemplateMeta } from "./template";
-import { separateProps } from "./widget-config";
+import { elementPropNames, separateProps } from "./widget-config";
 
 import {
   getTemplateOptions,
@@ -22,7 +22,13 @@ interface IState {
   templates: Record<string, TemplateGetter>;
 }
 
-abstract class ComponentBase<P> extends React.PureComponent<P, IState> {
+interface IHtmlOptions {
+  id?: string;
+  className?: string;
+  style?: any;
+}
+
+abstract class ComponentBase<P extends IHtmlOptions> extends React.PureComponent<P, IState> {
   protected _WidgetClass: any;
   protected _instance: any;
   protected _element: any;
@@ -56,9 +62,15 @@ abstract class ComponentBase<P> extends React.PureComponent<P, IState> {
   }
 
   public render() {
+      const elementProps = { ref: (element: any) => this._element = element };
+      elementPropNames.forEach((name) => {
+        if (name in this.props) {
+          elementProps[name] = this.props[name];
+        }
+      });
       const args: any[] = [
         "div",
-        { ref: (element: any) => this._element = element }
+        elementProps
       ];
       this._optionsManager.resetNestedElements();
       let nestedTemplates: Record<string, any> = {};
@@ -166,7 +178,7 @@ function getNestedTemplates(rawProps: Record<string, any>): Record<string, any>
 }
 
 // tslint:disable-next-line:max-classes-per-file
-class Component<P> extends ComponentBase<P> {
+class Component<P extends IHtmlOptions> extends ComponentBase<P> {
   private readonly _extensions: Array<(element: Element) => void> = [];
 
   public componentDidMount() {
@@ -205,6 +217,7 @@ class ExtensionComponent<P> extends ComponentBase<P> {
 
 export {
   IState,
+  IHtmlOptions,
   ComponentBase,
   Component,
   ExtensionComponent
diff --git a/src/core/widget-config.ts b/src/core/widget-config.ts
index adeeaac..dae9a20 100644
--- a/src/core/widget-config.ts
+++ b/src/core/widget-config.ts
@@ -1,5 +1,7 @@
 import { ITemplateMeta } from "./template";
 
+const elementPropNames = ["style", "className", "id"];
+
 function separateProps(
     props: Record<string, any>,
     defaultsProps: Record<string, string>,
@@ -24,7 +26,7 @@ function separateProps(
     Object.keys(props).forEach((key) => {
         const defaultOptionName = defaultsProps ? defaultsProps[key] : null;
 
-        if (key === "children") {
+        if (isIgnoredProp(key)) {
             return;
         }
 
@@ -40,6 +42,11 @@ function separateProps(
     return { options, defaults, templates };
 }
 
+function isIgnoredProp(name) {
+    return name === "children" || elementPropNames.indexOf(name) > 0;
+}
+
 export {
+    elementPropNames,
     separateProps
 };
diff --git a/tools/src/component-generator.ts b/tools/src/component-generator.ts
index c702c1e..a6019d7 100644
--- a/tools/src/component-generator.ts
+++ b/tools/src/component-generator.ts
@@ -116,7 +116,7 @@ function generate(component: IComponent): string {
         }))
         : null;
 
-    const hasExtraOptions = !!templates || !!defaultProps;
+    const hasExtraOptions = !component.isExtension;
     const widgetName =  `dx${uppercaseFirst(component.name)}`;
 
     const renderedPropTypings = component.propTypings
@@ -252,7 +252,11 @@ const renderImports: (model: {
     `import { PropTypes } from "prop-types";` + `\n` +
 `<#?#>` +
 
-`import { <#= it.baseComponentName #> as BaseComponent } from "<#= it.baseComponentPath #>";` + `\n` +
+`import { <#= it.baseComponentName #> as BaseComponent` +
+    `<#? it.hasExtraOptions #>` +
+        `, IHtmlOptions` +
+    `<#?#>` +
+` } from "<#= it.baseComponentPath #>";` + `\n` +
 
 `<#? it.hasNestedComponents #>` +
     `import NestedOption from "<#= it.configComponentPath #>";` + `\n` +
@@ -270,7 +274,7 @@ const renderOptionsInterface: (model: {
         type: string;
     }>;
 }) => string = createTempate(
-`interface <#= it.optionsName #> extends IOptions {` + `\n` +
+`interface <#= it.optionsName #> extends IOptions, IHtmlOptions {` + `\n` +
 
 `<#~ it.templates :template #>` +
     `  <#= template.render #>?: ${TYPE_RENDER};` + `\n` +


Test patch:
diff --git a/src/core/__tests__/component.test.tsx b/src/core/__tests__/component.test.tsx
index b031b99..3468d77 100644
--- a/src/core/__tests__/component.test.tsx
+++ b/src/core/__tests__/component.test.tsx
@@ -36,6 +36,18 @@ describe("rendering", () => {
         expect(Object.keys(component.props())).toEqual([]);
     });
 
+    it("passes id, className and style to element", () => {
+        const component = mount(
+            <TestComponent id={"id1"} className={"class1"} style={{ background: "red" }} />
+        );
+
+        const node = component.getDOMNode();
+
+        expect(node.id).toBe("id1");
+        expect(node.className).toBe("class1");
+        expect((node as HTMLElement).style.background).toEqual("red");
+    });
+
     it("creates nested component", () => {
         mount(
             <TestComponent>
diff --git a/tools/src/component-generator.test.ts b/tools/src/component-generator.test.ts
index 31efe6c..d68ba2a 100644
--- a/tools/src/component-generator.test.ts
+++ b/tools/src/component-generator.test.ts
@@ -4,10 +4,13 @@ it("generates", () => {
     //#region EXPECTED
     const EXPECTED = `
 import dxCLASS_NAME, {
-    IOptions as ICLASS_NAMEOptions
+    IOptions
 } from "devextreme/DX/WIDGET/PATH";
 
-import { Component as BaseComponent } from "BASE_COMPONENT_PATH";
+import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
+
+interface ICLASS_NAMEOptions extends IOptions, IHtmlOptions {
+}
 
 class CLASS_NAME extends BaseComponent<ICLASS_NAMEOptions> {
 
@@ -80,9 +83,9 @@ import dxCLASS_NAME, {
     IOptions
 } from "devextreme/DX/WIDGET/PATH";
 
-import { Component as BaseComponent } from "BASE_COMPONENT_PATH";
+import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
 
-interface ICLASS_NAMEOptions extends IOptions {
+interface ICLASS_NAMEOptions extends IOptions, IHtmlOptions {
   optionRender?: (props: any) => React.ReactNode;
   optionComponent?: React.ComponentType<any>;
 }
@@ -127,9 +130,9 @@ import dxCLASS_NAME, {
     IOptions
 } from "devextreme/DX/WIDGET/PATH";
 
-import { Component as BaseComponent } from "BASE_COMPONENT_PATH";
+import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
 
-interface ICLASS_NAMEOptions extends IOptions {
+interface ICLASS_NAMEOptions extends IOptions, IHtmlOptions {
   optionRender?: (props: any) => React.ReactNode;
   optionComponent?: React.ComponentType<any>;
   anotherOptionRender?: (props: any) => React.ReactNode;
@@ -180,9 +183,9 @@ import dxCLASS_NAME, {
     IOptions
 } from "devextreme/DX/WIDGET/PATH";
 
-import { Component as BaseComponent } from "BASE_COMPONENT_PATH";
+import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
 
-interface ICLASS_NAMEOptions extends IOptions {
+interface ICLASS_NAMEOptions extends IOptions, IHtmlOptions {
   render?: (props: any) => React.ReactNode;
   component?: React.ComponentType<any>;
 }
@@ -230,9 +233,9 @@ import dxCLASS_NAME, {
     IOptions
 } from "devextreme/DX/WIDGET/PATH";
 
-import { Component as BaseComponent } from "BASE_COMPONENT_PATH";
+import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
 
-interface ICLASS_NAMEOptions extends IOptions {
+interface ICLASS_NAMEOptions extends IOptions, IHtmlOptions {
   defaultOption1?: someType;
 }
 
@@ -276,9 +279,9 @@ import dxCLASS_NAME, {
     IOptions
 } from "devextreme/DX/WIDGET/PATH";
 
-import { Component as BaseComponent } from "BASE_COMPONENT_PATH";
+import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
 
-interface ICLASS_NAMEOptions extends IOptions {
+interface ICLASS_NAMEOptions extends IOptions, IHtmlOptions {
   defaultOption1?: someType;
   defaultOption2?: anotherType;
 }
@@ -324,12 +327,15 @@ describe("nested options", () => {
         //#region EXPECTED
         const EXPECTED = `
 import dxCLASS_NAME, {
-    IOptions as ICLASS_NAMEOptions
+    IOptions
 } from "devextreme/DX/WIDGET/PATH";
 
-import { Component as BaseComponent } from "BASE_COMPONENT_PATH";
+import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
 import NestedOption from "CONFIG_COMPONENT_PATH";
 
+interface ICLASS_NAMEOptions extends IOptions, IHtmlOptions {
+}
+
 class CLASS_NAME extends BaseComponent<ICLASS_NAMEOptions> {
 
   public get instance(): dxCLASS_NAME {
@@ -429,12 +435,15 @@ export {
         //#region EXPECTED
         const EXPECTED = `
 import dxCLASS_NAME, {
-    IOptions as ICLASS_NAMEOptions
+    IOptions
 } from "devextreme/DX/WIDGET/PATH";
 
-import { Component as BaseComponent } from "BASE_COMPONENT_PATH";
+import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
 import NestedOption from "CONFIG_COMPONENT_PATH";
 
+interface ICLASS_NAMEOptions extends IOptions, IHtmlOptions {
+}
+
 class CLASS_NAME extends BaseComponent<ICLASS_NAMEOptions> {
 
   public get instance(): dxCLASS_NAME {
@@ -512,12 +521,15 @@ export {
         //#region EXPECTED
         const EXPECTED = `
 import dxCLASS_NAME, {
-    IOptions as ICLASS_NAMEOptions
+    IOptions
 } from "devextreme/DX/WIDGET/PATH";
 
-import { Component as BaseComponent } from "BASE_COMPONENT_PATH";
+import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
 import NestedOption from "CONFIG_COMPONENT_PATH";
 
+interface ICLASS_NAMEOptions extends IOptions, IHtmlOptions {
+}
+
 class CLASS_NAME extends BaseComponent<ICLASS_NAMEOptions> {
 
   public get instance(): dxCLASS_NAME {
@@ -592,12 +604,15 @@ export {
         //#region EXPECTED
         const EXPECTED = `
 import dxCLASS_NAME, {
-    IOptions as ICLASS_NAMEOptions
+    IOptions
 } from "devextreme/DX/WIDGET/PATH";
 
-import { Component as BaseComponent } from "BASE_COMPONENT_PATH";
+import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
 import NestedOption from "CONFIG_COMPONENT_PATH";
 
+interface ICLASS_NAMEOptions extends IOptions, IHtmlOptions {
+}
+
 class CLASS_NAME extends BaseComponent<ICLASS_NAMEOptions> {
 
   public get instance(): dxCLASS_NAME {
@@ -664,11 +679,14 @@ describe("prop typings", () => {
         //#region EXPECTED
         const EXPECTED = `
 import dxCLASS_NAME, {
-    IOptions as ICLASS_NAMEOptions
+    IOptions
 } from "devextreme/DX/WIDGET/PATH";
 
 import { PropTypes } from "prop-types";
-import { Component as BaseComponent } from "BASE_COMPONENT_PATH";
+import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
+
+interface ICLASS_NAMEOptions extends IOptions, IHtmlOptions {
+}
 
 class CLASS_NAME extends BaseComponent<ICLASS_NAMEOptions> {
 
@@ -709,11 +727,14 @@ export {
         //#region EXPECTED
         const EXPECTED = `
 import dxCLASS_NAME, {
-    IOptions as ICLASS_NAMEOptions
+    IOptions
 } from "devextreme/DX/WIDGET/PATH";
 
 import { PropTypes } from "prop-types";
-import { Component as BaseComponent } from "BASE_COMPONENT_PATH";
+import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
+
+interface ICLASS_NAMEOptions extends IOptions, IHtmlOptions {
+}
 
 class CLASS_NAME extends BaseComponent<ICLASS_NAMEOptions> {
 
@@ -758,11 +779,14 @@ export {
         //#region EXPECTED
         const EXPECTED = `
 import dxCLASS_NAME, {
-    IOptions as ICLASS_NAMEOptions
+    IOptions
 } from "devextreme/DX/WIDGET/PATH";
 
 import { PropTypes } from "prop-types";
-import { Component as BaseComponent } from "BASE_COMPONENT_PATH";
+import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
+
+interface ICLASS_NAMEOptions extends IOptions, IHtmlOptions {
+}
 
 class CLASS_NAME extends BaseComponent<ICLASS_NAMEOptions> {
 
@@ -806,11 +830,14 @@ export {
         //#region EXPECTED
         const EXPECTED = `
 import dxCLASS_NAME, {
-    IOptions as ICLASS_NAMEOptions
+    IOptions
 } from "devextreme/DX/WIDGET/PATH";
 
 import { PropTypes } from "prop-types";
-import { Component as BaseComponent } from "BASE_COMPONENT_PATH";
+import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
+
+interface ICLASS_NAMEOptions extends IOptions, IHtmlOptions {
+}
 
 class CLASS_NAME extends BaseComponent<ICLASS_NAMEOptions> {