Skip to content

Commit

Permalink
Renovation: implement Button.template (#11899)
Browse files Browse the repository at this point in the history
  • Loading branch information
LazyLahtak committed Feb 25, 2020
1 parent b91afca commit d84fb85
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 24 deletions.
9 changes: 8 additions & 1 deletion js/renovation/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,13 @@ export const viewFunction = (viewModel: ButtonViewModel) => {
>
<div className="dx-button-content" ref={viewModel.contentRef}>
{viewModel.contentRender &&
<viewModel.contentRender icon={icon} text={viewModel.text} />}
<viewModel.contentRender
model={{
icon: viewModel.icon,
text: viewModel.text,
}}
parentRef={viewModel.contentRef}
/>}
{!viewModel.contentRender && isIconLeft && icon}
{!viewModel.contentRender && viewModel.text &&
<span className="dx-button-text">{viewModel.text}</span>
Expand All @@ -125,6 +131,7 @@ export class ButtonInput extends WidgetInput {
@OneWay() onSubmit?: (e: any) => any = (() => undefined);
@OneWay() pressed?: boolean;
@OneWay() stylingMode?: 'outlined' | 'text' | 'contained';
@OneWay() template?: any = '';
@OneWay() text?: string = '';
@OneWay() type?: string;
@OneWay() useInkRipple: boolean = false;
Expand Down
49 changes: 35 additions & 14 deletions js/renovation/dist/button.j.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@

import $ from '../../core/renderer';
import * as Preact from 'preact';
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 { wrapElement } from '../preact-wrapper/utils';
import { useLayoutEffect } from 'preact/hooks';
import { getPublicElement } from '../../core/utils/dom';

const TEMPLATE_WRAPPER_CLASS = 'dx-template-wrapper';

class Button extends Widget {
getView() {
Expand All @@ -10,22 +18,33 @@ 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) {
const template = this._getTemplate(props.template);

// TODO: rename 'contentRender' => 'template' after fix generator bug
// (renames 'template' => 'render' in declaration)
props.contentRender = ({ parentRef, ...restProps }) => {
useLayoutEffect(() => {
const $parent = $(parentRef.current);
let $template = template.render({
container: getPublicElement($parent),
...restProps,
});

if($template.hasClass(TEMPLATE_WRAPPER_CLASS)) {
$template = wrapElement($parent, $template);
}
}}/>);

return () => {
$template.remove();
};
}, Object.keys(props).map(key => props[key]));

return (<Preact.Fragment/>);
};
}

props.onClick = this._createActionByOption('onClick', {
excludeValidators: ['readOnly'],
afterExecute: () => {
Expand All @@ -34,6 +53,7 @@ class Button extends Widget {
useSubmitBehavior && setTimeout(() => this._submitInput().click());
}
});

return props;
}

Expand All @@ -44,6 +64,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 if(attributes.id) {
// NOTE: workaround to save container id
options.elementAttr = extend({ [attributes.id.name]: attributes.id.value }, options.elementAttr);
}

return options;
Expand Down
16 changes: 16 additions & 0 deletions js/renovation/preact-wrapper/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// NOTE: function for jQuery templates
export const wrapElement = ($element, $wrapper) => {
const attributes = [...$wrapper.get(0).attributes];
attributes.forEach(({ name, value }) => {
if(name === 'class') {
$element.addClass(value);
} else {
$element.attr(name, value);
}
});

const children = $wrapper.contents();
$wrapper.replaceWith(children);

return children;
};
1 change: 1 addition & 0 deletions styles/widgets/common/button.less
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
width: 0;
font-size: 0;
opacity: 0;
display: none;
}

.dx-state-disabled {
Expand Down
41 changes: 36 additions & 5 deletions testing/jest/button.tests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,15 +218,46 @@ describe('Button', () => {
});

describe('contentRender', () => {
it('should render template', () => {
const contentRender = ({ model: { 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().model.text).toBe('My button');
expect(customRender.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().model.text).toBe('My button');
expect(buttonContent.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().model.text).toBe('New value');
expect(buttonContent.text()).toBe('New value123');
});
});

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');
});

0 comments on commit d84fb85

Please sign in to comment.