Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Renovation: implement Button.template #11899

Merged
merged 29 commits into from
Feb 25, 2020
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
33202c4
Implement base template
LazyLahtak Feb 5, 2020
6665e9b
Merge remote-tracking branch 'upstream/preact-button' into renovation…
LazyLahtak Feb 5, 2020
4f1ee7d
Fix template rerender
LazyLahtak Feb 7, 2020
9b83fb3
Refactor
LazyLahtak Feb 7, 2020
030ba01
Merge remote-tracking branch 'upstream/preact-button' into renovation…
LazyLahtak Feb 10, 2020
0258402
Fix wrong render on set empty template property
LazyLahtak Feb 10, 2020
cfddfb4
Add some tests
LazyLahtak Feb 10, 2020
89883f4
Implement wrapper behaviour
LazyLahtak Feb 11, 2020
c976873
Merge remote-tracking branch 'upstream/preact-button' into renovation…
LazyLahtak Feb 11, 2020
3a091e4
Refactor template render
LazyLahtak Feb 12, 2020
0c84358
Correct className assignment
LazyLahtak Feb 12, 2020
f834f77
Remove useless code
LazyLahtak Feb 12, 2020
a0b2e77
Put converter function in separate file
LazyLahtak Feb 13, 2020
a1c7829
Merge remote-tracking branch 'upstream/preact-button' into renovation…
LazyLahtak Feb 13, 2020
43079c0
Pass input for submit into template
LazyLahtak Feb 13, 2020
f319a55
Add jest tests
LazyLahtak Feb 13, 2020
f59d47e
Refactor render and jest-test
LazyLahtak Feb 13, 2020
ad4014b
Add QUnit tests
LazyLahtak Feb 13, 2020
adc6aaa
Add tests
LazyLahtak Feb 13, 2020
f83a7d2
Merge remote-tracking branch 'upstream/preact-button' into renovation…
LazyLahtak Feb 13, 2020
3ef2adb
Correct jest tests
LazyLahtak Feb 14, 2020
8c7493b
Refactor template render
LazyLahtak Feb 17, 2020
0741e16
Fix classes for tests
LazyLahtak Feb 17, 2020
c9ca1e8
Fix jest tests
LazyLahtak Feb 17, 2020
bb5769b
Rework templates
LazyLahtak Feb 21, 2020
f9e931a
Refactor
LazyLahtak Feb 21, 2020
2b0585c
Fix tests
LazyLahtak Feb 21, 2020
e4dda1d
Fix tests
LazyLahtak Feb 21, 2020
26749b2
Make button submit input display=none
LazyLahtak Feb 21, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 32 additions & 14 deletions js/renovation/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,33 +98,44 @@ export const viewFunction = (viewModel: ButtonViewModel) => {
visible={viewModel.visible}
width={viewModel.width}
>
<div className="dx-button-content" ref={viewModel.contentRef}>
{viewModel.contentRender &&
<viewModel.contentRender icon={icon} text={viewModel.text} />}
{!viewModel.contentRender && isIconLeft && icon}
{!viewModel.contentRender && viewModel.text &&
<span className="dx-button-text">{viewModel.text}</span>
}
{!viewModel.contentRender && !isIconLeft && icon}
{viewModel.useSubmitBehavior &&
<input ref={viewModel.submitInputRef} type="submit" tabIndex={-1} className="dx-button-submit-input"/>
}
</div>
{(viewModel.contentRender &&
<viewModel.contentRender
icon={viewModel.icon}
text={viewModel.text}
contentRef={viewModel.contentRef}
>
{viewModel.useSubmitBehavior &&
<input ref={viewModel.submitInputRef} type="submit" tabIndex={-1} className="dx-button-submit-input"/>
}
</viewModel.contentRender>
) || (
<div className="dx-button-content" ref={viewModel.contentRef}>
{isIconLeft && icon}
{viewModel.text &&
<span className="dx-button-text">{viewModel.text}</span>
}
{!isIconLeft && icon}
{viewModel.useSubmitBehavior &&
<input ref={viewModel.submitInputRef} type="submit" tabIndex={-1} className="dx-button-submit-input"/>
}
</div>
)}
</Widget>;
};

@ComponentBindings()
export class ButtonInput extends WidgetInput {
@OneWay() activeStateEnabled?: boolean = true;
@OneWay() classNames?: string[];
@OneWay() contentRender?: any;
@OneWay() contentRender?: ButtonTemplateFn;
@OneWay() focusStateEnabled?: boolean = true;
@OneWay() hoverStateEnabled?: boolean = true;
@OneWay() icon?: string = '';
@OneWay() iconPosition?: string = 'left';
@OneWay() onSubmit?: (e: any) => any = (() => undefined);
@OneWay() pressed?: boolean;
@OneWay() stylingMode?: 'outlined' | 'text' | 'contained';
@OneWay() template?: ButtonTemplate = '';
@OneWay() text?: string = '';
@OneWay() type?: string;
@OneWay() useInkRipple: boolean = false;
Expand All @@ -140,7 +151,7 @@ export class ButtonInput extends WidgetInput {
})

export default class Button extends JSXComponent<ButtonInput> {
@Ref() contentRef!: HTMLDivElement;
@Ref() contentRef!: HTMLElement;
@Ref() submitInputRef!: HTMLInputElement;

onActive(event: Event) {
Expand Down Expand Up @@ -185,3 +196,10 @@ export default class Button extends JSXComponent<ButtonInput> {
return () => click.off(this.submitInputRef, { namespace });
}
}

declare type ButtonContent = {
text: string,
icon: string,
};
declare type ButtonTemplateFn = (data: ButtonContent, container: any) => any;
declare type ButtonTemplate = ButtonTemplateFn | string;
43 changes: 28 additions & 15 deletions js/renovation/dist/button.j.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@

import $ from '../../core/renderer';
import registerComponent from '../../core/component_registrator';
import Widget from '../preact_wrapper';
import Widget from '../preact-wrapper/component';
import { extend } from '../../core/utils/extend';
import ButtonView from '../button.p';
import { HTMLToPreact } from '../preact-wrapper/utils';
import { getPublicElement } from '../../core/utils/dom';

class Button extends Widget {
getView() {
Expand All @@ -10,22 +14,30 @@ class Button extends Widget {

getProps(isFirstRender) {
const props = super.getProps(isFirstRender);
if(props.contentRender) {
props.contentRender = (data) => {
const template = this._getTemplate(props.contentRender);

return (<div style={{ display: 'none' }} ref={(element) => {
if(element && element.parentElement) {
const parent = element.parentElement;
while(parent.firstChild) {
parent.removeChild(parent.firstChild);
}
template.render({ model: data, container: parent });
parent.appendChild(element);
}
}}/>);

if(props.template) {
props.contentRender = ({ text, icon, contentRef, children }) => {
const data = { text, icon };
let $content = $('<div>');
$content.addClass('dx-button-content');
LazyLahtak marked this conversation as resolved.
Show resolved Hide resolved
const $template = $(
this._getTemplate(this.option('template')).render({
model: data, container: getPublicElement($content)
})
);

if($template.hasClass('dx-template-wrapper')) {
$template.addClass('dx-button-content');
$content = $template;
}

const result = HTMLToPreact($content.get(0), children);
result.ref = contentRef;

return result;
};
}

props.onClick = this._createActionByOption('onClick', {
excludeValidators: ['readOnly'],
afterExecute: () => {
Expand All @@ -44,6 +56,7 @@ class Button extends Widget {
hoverStateEnabled: true,
icon: '',
iconPosition: 'left',
template: '',
text: '',
});
}
Expand Down
2 changes: 1 addition & 1 deletion js/renovation/dist/widget.j.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import registerComponent from '../../core/component_registrator';
import WidgetBase from '../preact_wrapper';
import WidgetBase from '../preact-wrapper/component';
import { extend } from '../../core/utils/extend';
import WidgetView from '../widget.p';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Widget from '../ui/widget/ui.widget';
import Widget from '../../ui/widget/ui.widget';
import * as Preact from 'preact';
import { extend } from '../core/utils/extend';
import { extend } from '../../core/utils/extend';

export default class PreactWrapper extends Widget {
getInstance() {
Expand All @@ -23,15 +23,18 @@ export default class PreactWrapper extends Widget {

getProps(isFirstRender) {
const options = extend({}, this.option());
const attributes = this.$element()[0].attributes;

if(isFirstRender) {
const attributes = this.$element()[0].attributes;
options.elementAttr = extend(Object.keys(attributes).reduce((a, key) => {
if(attributes[key].specified) {
a[attributes[key].name] = attributes[key].value;
}
return a;
}, {}), options.elementAttr);
} else {
// NOTE: workaround to save container id
options.elementAttr = extend({ [attributes.id.name]: attributes.id.value }, options.elementAttr);
}

return options;
Expand Down
23 changes: 23 additions & 0 deletions js/renovation/preact-wrapper/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { h } from 'preact';

export const HTMLToPreact = (node, ...restChildren) => {
// NOTE: nodeType === 3 => text node
if(node.nodeType === 3) {
return node.wholeText;
}

const tag = node.tagName;
const childNodes = node.childNodes;

const children = [];
for(let i = 0; i < childNodes.length; i++) {
children.push(HTMLToPreact(childNodes[i]));
}

const attributes = [...node.attributes].reduce((result, attr) => {
result[attr.name] = attr.value;
return result;
}, {});

return h(tag, attributes, [...children, ...restChildren]);
};
41 changes: 36 additions & 5 deletions testing/jest/button.tests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,15 +210,46 @@ describe('Button', () => {
});

describe('contentRender', () => {
it('should render template', () => {
const contentRender = ({ text }) => <div className={'custom-content'}>{text + 123}</div>;

it('should render contentRender', () => {
const button = render({
text: 'My button',
contentRender: ({ text }) => <div className={'custom-content'}>{text + 123}</div>,
contentRender,
});
const buttonContentChildren = button.find('.dx-button-content').children();
const customRender = button.find(contentRender);

expect(customRender.exists()).toBe(true);
expect(customRender.exists('.custom-content')).toBe(true);

expect(customRender.props().text).toBe('My button');
expect(customRender.render().text()).toBe('My button123');
});

it('should rerender contentRender in runtime', () => {
const button = mount(<Button text='My button' />);

expect(button.exists(contentRender)).toBe(false);

button.setProps({ contentRender });
expect(button.exists(contentRender)).toBe(true);

button.setProps({ contentRender: undefined });
expect(button.exists(contentRender)).toBe(false);
});

it('should change properties in runtime', () => {
const button = mount(<Button text='My button' contentRender={contentRender} />);
let buttonContent = button.find(contentRender);

expect(buttonContent.props().text).toBe('My button');
expect(buttonContent.render().text()).toBe('My button123');

button.setProps({ text: 'New value' });
buttonContent = button.find(contentRender);

expect(buttonContentChildren.props().text).toBe('My button');
expect(buttonContentChildren.render().text()).toBe('My button123');
expect(buttonContent.props().text).toBe('New value');
expect(buttonContent.render().text()).toBe('New value123');
LazyLahtak marked this conversation as resolved.
Show resolved Hide resolved
});
});

Expand Down
121 changes: 121 additions & 0 deletions testing/tests/Renovation/button.tests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import $ from 'jquery';
import 'renovation/dist/button.j';
import { isRenderer } from 'core/utils/type';
import config from 'core/config';

QUnit.testStart(function() {
$('#qunit-fixture').html(`
<div id="component"></div>
<div id="anotherComponent"></div>
`);
});

const buttonConfig = {
beforeEach: function(module) {
// it needs for Preact timers https://github.com/preactjs/preact/blob/master/hooks/src/index.js#L273
this.clock = sinon.useFakeTimers();
},
afterEach: function() {
this.clock.tick(100);
this.clock.restore();
}
};

QUnit.module('Props: template', buttonConfig);

QUnit.test('should render button with default template', function(assert) {
const $element = $('#component');
$element.Button({ text: 'test', icon: 'check' });
const $contentElements = $element.find('.dx-button-content').children();

assert.strictEqual($element.Button('instance').option('template'), '', 'default template value');
assert.ok($contentElements.eq(0).hasClass('dx-icon'), 'render icon');
assert.ok($contentElements.eq(1).hasClass('dx-button-text'), 'render test');
});

QUnit.test('should pass correct container', function(assert) {
const $element = $('#component');

$element.Button({
template: function(data, container) {
assert.strictEqual(isRenderer(container), !!config().useJQuery, 'container is correct');
return $('<div>');
}
});
});

QUnit.test('should pass correct data', function(assert) {
const $element = $('#component');

$element.Button({
text: 'My button',
icon: 'test',
template: function(data, container) {
assert.strictEqual(data.text, 'My button', 'text is correct');
assert.strictEqual(data.icon, 'test', 'icon is correct');
const $template = $('<div>');
$template.text(`${data.text}123`);
return $template;
}
});

assert.strictEqual($element.text(), 'My button123', 'render correct text');
});

QUnit.test('should render jQuery', function(assert) {
const $element = $('#component');

$element.Button({
template: (data, container) => $('<div id="custom-template">'),
});
assert.strictEqual($element.find('.dx-button-content').length, 1, 'render content');
assert.strictEqual($element.find('#custom-template').length, 1, 'render custom template');
});

QUnit.test('should render dom node', function(assert) {
const $element = $('#component');

$element.Button({
template: (data, container) => $('<div id="custom-template">').get(0),
});
assert.strictEqual($element.find('.dx-button-content').length, 1, 'render content');
assert.strictEqual($element.find('#custom-template').length, 1, 'render custom template');
});

QUnit.test('should replace content if has "dx-template-wrapper" class', function(assert) {
const $element = $('#component');

$element.Button({
template: (data, container) => {
const $element = $('<span>')
.addClass('dx-template-wrapper');

return $element.get(0);
},
});
assert.ok($element.find('.dx-button-content').hasClass('dx-template-wrapper'), 'template has "dx-button-content" class');
});

QUnit.test('should rerender template in runtime', function(assert) {
const template = (data, container) => $('<div id="custom-template">');
const templateNew = (data, container) => $('<div id="new-template">');
const $element = $('#component');

$element.Button({ template: template });
assert.strictEqual($element.find('#custom-template').length, 1, 'render custom template');

$element.Button('instance').option('template', templateNew);
assert.strictEqual($element.find('#custom-template').length, 0, 'not render old template');
assert.strictEqual($element.find('#new-template').length, 1, 'render new template');
});

QUnit.test('should render submit input with custom template', function(assert) {
const $element = $('#component');

$element.Button({
useSubmitBehavior: true,
template: (data, container) => $('<span>'),
});

assert.strictEqual($element.find('.dx-button-submit-input').length, 1, 'render submit input');
});