Skip to content

Commit

Permalink
Add extension component (#116)
Browse files Browse the repository at this point in the history
  • Loading branch information
lukyanovas authored Jun 5, 2018
1 parent 076bee4 commit de8d677
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 57 deletions.
11 changes: 11 additions & 0 deletions example/text-box-example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ import Example from "./example-block";
import dxTextBox from "devextreme/ui/text_box";
import { Button } from "../src/ui/button";
import { TextBox } from "../src/ui/text-box";
import { Validator } from "../src/ui/validator";

export default class extends React.Component<any, { text: string; uncontrolledText: string; }> {

private textBox: dxTextBox;

private validationRules = [{
type: "required",
message: "this is required"
}];

constructor(props: any) {
super(props);
this.state = {
Expand Down Expand Up @@ -39,6 +45,11 @@ export default class extends React.Component<any, { text: string; uncontrolledTe
<br />
controlled state value with change handling
<TextBox value={this.state.text} onValueChanged={this.handleChange} valueChangeEvent="input" />
<br />
validation (required)
<TextBox valueChangeEvent="input" defaultValue={"required text"}>
<Validator validationRules={this.validationRules} />
</TextBox>
</Example>
);
}
Expand Down
45 changes: 43 additions & 2 deletions src/core/component.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as events from "devextreme/events";
import { configure as configureEnzyme, mount, shallow } from "enzyme";
import * as Adapter from "enzyme-adapter-react-16";
import * as React from "react";
import Component from "../core/component";
import { Component, ExtensionComponent } from "../core/component";
import ConfigurationComponent from "../core/nested-option";
import { Template } from "../core/template";

Expand Down Expand Up @@ -52,7 +52,7 @@ describe("component rendering", () => {
expect(component.type()).toBe("div");
});

it("create widget then on componnetDidMount", () => {
it("create widget on componentDidMount", () => {
shallow(
<TestComponent />
);
Expand Down Expand Up @@ -98,6 +98,47 @@ describe("component rendering", () => {
});
});

describe("extension component", () => {
const ExtensionWidgetClass = jest.fn(() => Widget);

// tslint:disable-next-line:max-classes-per-file
class TestExtensionComponent<P = any> extends ExtensionComponent<P> {

constructor(props: P) {
super(props);

this._WidgetClass = ExtensionWidgetClass;
}
}

it("does not create widget on componentDidMount", () => {
shallow(
<TestExtensionComponent />
);

expect(ExtensionWidgetClass).toHaveBeenCalledTimes(0);
});

it("creates widget on componentDidMount inside another component on same element", () => {
mount(
<TestComponent>
<TestExtensionComponent />
</TestComponent>
);

expect(ExtensionWidgetClass).toHaveBeenCalledTimes(1);
expect(ExtensionWidgetClass.mock.calls[0][0]).toBe(WidgetClass.mock.calls[0][0]);
});

it("unmounts without errors", () => {
const component = shallow(
<TestExtensionComponent/>
);

expect(component.unmount.bind(component)).not.toThrow();
});
});

describe("templates", () => {
function renderTemplate(name: string, model?: any, container?: any): Element {
model = model || {};
Expand Down
119 changes: 82 additions & 37 deletions src/core/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,18 @@ interface IState {
templates: Record<string, IWrappedTemplateInfo>;
}

class Component<P> extends React.PureComponent<P, IState> {

abstract class ComponentBase<P> extends React.PureComponent<P, IState> {
protected _WidgetClass: any;
protected _instance: any;
protected _element: any;

protected readonly _defaults: Record<string, string>;

protected readonly _templateProps: ITemplateMeta[] = [];

private readonly _templateHelper: TemplateHelper;
private readonly _optionsManager: OptionsManager;

private _element: any;

constructor(props: P) {
super(props);
this._prepareProps = this._prepareProps.bind(this);
Expand Down Expand Up @@ -80,7 +79,7 @@ class Component<P> extends React.PureComponent<P, IState> {
...nestedTemplates,
...this.findNestedTemplate(child)
};
args.push(this._registerNestedOption(child) || child);
args.push(this._preprocessChild(child) || child);
});
}

Expand All @@ -100,7 +99,17 @@ class Component<P> extends React.PureComponent<P, IState> {
return React.createElement.apply(this, args);
}

public componentDidMount() {
public componentWillUnmount() {
if (this._instance) {
events.triggerHandler(this._element, DX_REMOVE_EVENT);
this._instance.dispose();
}
}

protected abstract _preprocessChild(component: React.ReactElement<any>): React.ReactElement<any>;

protected _createWidget(element?: Element) {
element = element || this._element;
const nestedProps = this._optionsManager.getNestedOptionsObjects();
const props = {
...(this.props as any),
Expand All @@ -116,14 +125,40 @@ class Component<P> extends React.PureComponent<P, IState> {
...this._getIntegrationOptions(preparedProps.templates, preparedProps.nestedTemplates)
};

this._instance = new this._WidgetClass(this._element, options);
this._instance = new this._WidgetClass(element, options);
this._optionsManager.setInstance(this._instance);
this._instance.on("optionChanged", this._optionsManager.handleOptionChange);
}

public componentWillUnmount() {
events.triggerHandler(this._element, DX_REMOVE_EVENT);
this._instance.dispose();
protected _registerNestedOption(component: React.ReactElement<any>): any {
const configComponent = component as any as INestedOption;
if (
configComponent && configComponent.type &&
configComponent.type.OptionName &&
configComponent.type.OwnerType && this instanceof configComponent.type.OwnerType
) {
const optionName = configComponent.type.OptionName;

this._optionsManager.addNestedOption(
optionName,
component,
configComponent.type.DefaultsProps,
configComponent.type.IsCollectionItem
);

return createConfigurationComponent(
component,
(newProps, prevProps) => {
const newOptions = separateProps(newProps, configComponent.type.DefaultsProps, []).options;
this._optionsManager.processChangedValues(
addPrefixToKeys(newOptions, optionName + "."),
addPrefixToKeys(prevProps, optionName + ".")
);
}
);
}

return null;
}

private _prepareProps(rawProps: Record<string, any>): IWidgetConfig {
Expand Down Expand Up @@ -215,38 +250,48 @@ class Component<P> extends React.PureComponent<P, IState> {
return result;
}
}
}

private _registerNestedOption(component: React.ReactElement<any>): any {
const configComponent = component as any as INestedOption;
if (
configComponent && configComponent.type &&
configComponent.type.OptionName &&
configComponent.type.OwnerType && this instanceof configComponent.type.OwnerType
) {
const optionName = configComponent.type.OptionName;
// tslint:disable-next-line:max-classes-per-file
class Component<P> extends ComponentBase<P> {
private readonly _extensions: Array<(element: Element) => void> = [];

this._optionsManager.addNestedOption(
optionName,
component,
configComponent.type.DefaultsProps,
configComponent.type.IsCollectionItem
);
public componentDidMount() {
this._createWidget();
this._extensions.forEach((extension) => extension.call(this, this._element));
}

return createConfigurationComponent(
component,
(newProps, prevProps) => {
const newOptions = separateProps(newProps, configComponent.type.DefaultsProps, []).options;
this._optionsManager.processChangedValues(
addPrefixToKeys(newOptions, optionName + "."),
addPrefixToKeys(prevProps, optionName + ".")
);
}
);
protected _preprocessChild(component: React.ReactElement<any>) {
return this._registerExtension(component) || this._registerNestedOption(component) || component;
}

private _registerExtension(component: React.ReactElement<any>) {
if (!ExtensionComponent.isPrototypeOf(component.type)) {
return null;
}

return null;
return React.cloneElement(component, {
onMounted: (callback: any) => {
this._extensions.push(callback);
}
});
}
}

// tslint:disable-next-line:max-classes-per-file
class ExtensionComponent<P> extends ComponentBase<P> {
public componentDidMount() {
const onMounted = (this.props as Record<string, any>).onMounted;
if (onMounted) {
onMounted((element) => {
this._createWidget(element);
});
}
}

protected _preprocessChild(component: React.ReactElement<any>) {
return component;
}
}

export default Component;
export { IState, ITemplateMeta };
export { IState, ITemplateMeta, ComponentBase, Component, ExtensionComponent };
6 changes: 3 additions & 3 deletions src/core/template-helper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from "react";

import Component, { IState } from "./component";
import { ComponentBase, IState } from "./component";
import { generateID } from "./helpers";
import { ITemplateWrapperProps, TemplateWrapper } from "./template-wrapper";

Expand All @@ -26,9 +26,9 @@ interface IWrappedTemplateInfo extends ITemplateInfo {
}

class TemplateHelper {
private readonly _component: Component<any>;
private readonly _component: ComponentBase<any>;

constructor(component: Component<any>) {
constructor(component: ComponentBase<any>) {
this._component = component;

this.wrapTemplate = this.wrapTemplate.bind(this);
Expand Down
1 change: 1 addition & 0 deletions tools/integration-data-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface IModel {
export interface IWidget {
exportPath: string;
isEditor: boolean;
isExtension: boolean;
name: string;
options: IProp[];
templates: string[];
Expand Down
3 changes: 2 additions & 1 deletion tools/integration-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -91348,7 +91348,8 @@
}
]
}
]
],
"isExtension": true
},
{
"name": "DxVectorMap",
Expand Down
Loading

0 comments on commit de8d677

Please sign in to comment.