Skip to content

Commit 983a9dc

Browse files
authored
Generate types for event handlers (#827)
1 parent 9b9f006 commit 983a9dc

File tree

9 files changed

+967
-582
lines changed

9 files changed

+967
-582
lines changed

package-lock.json

Lines changed: 25 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"@typescript-eslint/parser": "^4.33.0",
1414
"cpy-cli": "^3.1.1",
1515
"del-cli": "^3.0.1",
16-
"devextreme-internal-tools": "10.0.0-beta.5",
16+
"devextreme-internal-tools": "10.0.0-beta.6",
1717
"eslint": "^7.32.0",
1818
"eslint-config-airbnb-base": "^14.2.1",
1919
"eslint-config-airbnb-typescript": "^12.3.1",

packages/devextreme-react-generator/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"author": "Developer Express Inc.",
33
"name": "devextreme-react-generator",
4-
"version": "4.0.4",
4+
"version": "4.1.0",
55
"description": "DevExtreme React UI and Visualization Components",
66
"repository": {
77
"type": "git",
@@ -27,7 +27,7 @@
2727
"license": "MIT",
2828
"dependencies": {
2929
"dasherize": "^2.0.0",
30-
"devextreme-internal-tools": "10.0.0-beta.5",
30+
"devextreme-internal-tools": "10.0.0-beta.6",
3131
"dot": "^1.1.3"
3232
},
3333
"devDependencies": {

packages/devextreme-react-generator/src/component-generator.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,3 +1351,90 @@ export {
13511351
).toBe(EXPECTED);
13521352
});
13531353
});
1354+
1355+
describe('Event types narrowing', () => {
1356+
it('generated correctly', () => {
1357+
// #region EXPECTED
1358+
const EXPECTED = `
1359+
import dxCLASS_NAME, {
1360+
Properties
1361+
} from "DX/WIDGET/PATH";
1362+
1363+
import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
1364+
1365+
type ReplaceFieldTypes<TSource, TReplacement> = {
1366+
[P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P];
1367+
}
1368+
1369+
type ICLASS_NAMEOptionsNarrowedEvents = {
1370+
onSomethingHappened?: ((e: SomethingHappenedEvent) => void);
1371+
}
1372+
1373+
type ICLASS_NAMEOptions = React.PropsWithChildren<ReplaceFieldTypes<Properties, ICLASS_NAMEOptionsNarrowedEvents> & IHtmlOptions & {
1374+
}>
1375+
1376+
class CLASS_NAME extends BaseComponent<React.PropsWithChildren<ICLASS_NAMEOptions>> {
1377+
1378+
public get instance(): dxCLASS_NAME {
1379+
return this._instance;
1380+
}
1381+
1382+
protected _WidgetClass = dxCLASS_NAME;
1383+
}
1384+
export default CLASS_NAME;
1385+
export {
1386+
CLASS_NAME,
1387+
ICLASS_NAMEOptions
1388+
};
1389+
`.trimLeft();
1390+
// #endregion
1391+
1392+
expect(
1393+
generate({
1394+
name: 'CLASS_NAME',
1395+
baseComponentPath: 'BASE_COMPONENT_PATH',
1396+
extensionComponentPath: 'EXTENSION_COMPONENT_PATH',
1397+
dxExportPath: 'DX/WIDGET/PATH',
1398+
narrowedEvents: [{ name: 'onSomethingHappened', type: '((e: SomethingHappenedEvent) => void)' }],
1399+
}),
1400+
).toBe(EXPECTED);
1401+
});
1402+
it('not generated if narrowedEvents is empty', () => {
1403+
// #region EXPECTED
1404+
const EXPECTED = `
1405+
import dxCLASS_NAME, {
1406+
Properties
1407+
} from "DX/WIDGET/PATH";
1408+
1409+
import { Component as BaseComponent, IHtmlOptions } from "BASE_COMPONENT_PATH";
1410+
1411+
type ICLASS_NAMEOptions = React.PropsWithChildren<Properties & IHtmlOptions & {
1412+
}>
1413+
1414+
class CLASS_NAME extends BaseComponent<React.PropsWithChildren<ICLASS_NAMEOptions>> {
1415+
1416+
public get instance(): dxCLASS_NAME {
1417+
return this._instance;
1418+
}
1419+
1420+
protected _WidgetClass = dxCLASS_NAME;
1421+
}
1422+
export default CLASS_NAME;
1423+
export {
1424+
CLASS_NAME,
1425+
ICLASS_NAMEOptions
1426+
};
1427+
`.trimLeft();
1428+
// #endregion
1429+
1430+
expect(
1431+
generate({
1432+
name: 'CLASS_NAME',
1433+
baseComponentPath: 'BASE_COMPONENT_PATH',
1434+
extensionComponentPath: 'EXTENSION_COMPONENT_PATH',
1435+
dxExportPath: 'DX/WIDGET/PATH',
1436+
narrowedEvents: [],
1437+
}),
1438+
).toBe(EXPECTED);
1439+
});
1440+
});

packages/devextreme-react-generator/src/component-generator.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ type IComponent = {
2424
} & {
2525
nestedComponents?: INestedComponent[];
2626
configComponentPath?: string;
27-
containsReexports?: boolean
27+
containsReexports?: boolean,
28+
narrowedEvents?: IOption[]
2829
};
2930

3031
interface INestedComponent {
@@ -133,6 +134,23 @@ function renderObject(props: IOption[], indent: number): string {
133134
return result;
134135
}
135136

137+
function renderNarrowedEvents(events: IOption[], typeParams?: string[]) {
138+
let result = '{';
139+
const typeArguments = typeParams && typeParams.length
140+
? `<${typeParams.join(', ')}>`
141+
: '';
142+
let indent = 1;
143+
144+
events.forEach((event) => {
145+
const patchedType = event.type.replace(') =>', `${typeArguments}) =>`);
146+
result += `\n${getIndent(indent)}${event.name}?: ${patchedType};`;
147+
});
148+
149+
indent -= 1;
150+
result += `\n${getIndent(indent)}}`;
151+
return result;
152+
}
153+
136154
const renderTemplateOption: (model: {
137155
actualOptionName: string;
138156
render: string;
@@ -336,8 +354,17 @@ const renderOptionsInterface: (model: {
336354
name: string;
337355
type: string;
338356
}>;
357+
renderedNarrowedEvents?: string | undefined
339358
}) => string = createTemplate(
340-
`type <#= it.optionsName #>${TYPE_PARAMS_WITH_DEFAULTS} = React.PropsWithChildren<Properties${TYPE_PARAMS} & IHtmlOptions & {\n`
359+
'<#? it.renderedNarrowedEvents #>'
360+
+ 'type ReplaceFieldTypes<TSource, TReplacement> = {\n'
361+
+ ' [P in keyof TSource]: P extends keyof TReplacement ? TReplacement[P] : TSource[P];\n'
362+
+ '}\n\n'
363+
364+
+ `type <#= it.optionsName #>NarrowedEvents${TYPE_PARAMS_WITH_DEFAULTS} = <#= it.renderedNarrowedEvents #>\n\n`
365+
+ '<#?#>'
366+
367+
+ `type <#= it.optionsName #>${TYPE_PARAMS_WITH_DEFAULTS} = React.PropsWithChildren<<#? it.renderedNarrowedEvents #>ReplaceFieldTypes<<#?#>Properties${TYPE_PARAMS}<#? it.renderedNarrowedEvents #>, <#= it.optionsName #>NarrowedEvents${TYPE_PARAMS}><#?#> & IHtmlOptions & {\n`
341368

342369
+ '<#? it.typeParams #>'
343370
+ ` dataSource?: Properties${TYPE_PARAMS}["dataSource"];\n`
@@ -590,6 +617,7 @@ function generate(
590617
: undefined;
591618

592619
const hasExplicitTypes = !!component.optionsTypeParams?.length;
620+
const typeParams = component.optionsTypeParams?.length ? component.optionsTypeParams : undefined;
593621

594622
return renderModule({
595623

@@ -617,7 +645,9 @@ function generate(
617645
defaultProps: defaultProps || [],
618646
onChangeEvents: onChangeEvents || [],
619647
templates: templates || [],
620-
typeParams: component.optionsTypeParams?.length ? component.optionsTypeParams : undefined,
648+
typeParams,
649+
renderedNarrowedEvents: component.narrowedEvents && component.narrowedEvents.length
650+
? renderNarrowedEvents(component.narrowedEvents, typeParams) : undefined,
621651
}),
622652

623653
renderedComponent: renderComponent({

packages/devextreme-react-generator/src/converter.test.ts

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,27 @@ import { convertTypes } from './converter';
22

33
it('deduplicates', () => {
44
const types = [
5-
{ type: 'Array', isCustomType: false, acceptableValues: [] },
6-
{ type: 'Boolean', isCustomType: false, acceptableValues: [] },
7-
{ type: 'Function', isCustomType: false, acceptableValues: [] },
8-
{ type: 'Boolean', isCustomType: false, acceptableValues: [] },
9-
{ type: 'Number', isCustomType: false, acceptableValues: [] },
10-
{ type: 'Object', isCustomType: false, acceptableValues: [] },
11-
{ type: 'String', isCustomType: false, acceptableValues: [] },
5+
{
6+
type: 'Array', isCustomType: false, acceptableValues: [], importPath: '', isImportedType: false,
7+
},
8+
{
9+
type: 'Boolean', isCustomType: false, acceptableValues: [], importPath: '', isImportedType: false,
10+
},
11+
{
12+
type: 'Function', isCustomType: false, acceptableValues: [], importPath: '', isImportedType: false,
13+
},
14+
{
15+
type: 'Boolean', isCustomType: false, acceptableValues: [], importPath: '', isImportedType: false,
16+
},
17+
{
18+
type: 'Number', isCustomType: false, acceptableValues: [], importPath: '', isImportedType: false,
19+
},
20+
{
21+
type: 'Object', isCustomType: false, acceptableValues: [], importPath: '', isImportedType: false,
22+
},
23+
{
24+
type: 'String', isCustomType: false, acceptableValues: [], importPath: '', isImportedType: false,
25+
},
1226
];
1327

1428
const expected = [
@@ -24,17 +38,27 @@ it('deduplicates', () => {
2438
});
2539

2640
it('returns undefiend if finds Any', () => {
27-
expect(convertTypes([{ type: 'Any', isCustomType: false, acceptableValues: [] }])).toBeUndefined();
41+
expect(convertTypes([{
42+
type: 'Any', isCustomType: false, acceptableValues: [], importPath: '', isImportedType: false,
43+
}])).toBeUndefined();
2844
expect(convertTypes([
29-
{ type: 'String', isCustomType: false, acceptableValues: [] },
30-
{ type: 'Any', isCustomType: false, acceptableValues: [] },
31-
{ type: 'Number', isCustomType: false, acceptableValues: [] },
45+
{
46+
type: 'String', isCustomType: false, acceptableValues: [], importPath: '', isImportedType: false,
47+
},
48+
{
49+
type: 'Any', isCustomType: false, acceptableValues: [], importPath: '', isImportedType: false,
50+
},
51+
{
52+
type: 'Number', isCustomType: false, acceptableValues: [], importPath: '', isImportedType: false,
53+
},
3254
])).toBeUndefined();
3355
});
3456

3557
it('returns object if finds isCustomType', () => {
3658
expect(convertTypes([
37-
{ type: 'CustomType', isCustomType: true, acceptableValues: [] },
59+
{
60+
type: 'CustomType', isCustomType: true, acceptableValues: [], importPath: '', isImportedType: false,
61+
},
3862
])).toEqual(['object']);
3963
});
4064

@@ -57,13 +81,19 @@ it('returns undefined if array is undefined', () => {
5781
it('expands custom types', () => {
5882
expect(convertTypes(
5983
[
60-
{ type: 'CustomType', isCustomType: true, acceptableValues: [] },
84+
{
85+
type: 'CustomType', isCustomType: true, acceptableValues: [], importPath: '', isImportedType: false,
86+
},
6187
], {
6288
CustomType: {
6389
name: 'CustomType',
6490
types: [
65-
{ type: 'String', isCustomType: false, acceptableValues: [] },
66-
{ type: 'Number', isCustomType: false, acceptableValues: [] },
91+
{
92+
type: 'String', isCustomType: false, acceptableValues: [], importPath: '', isImportedType: false,
93+
},
94+
{
95+
type: 'Number', isCustomType: false, acceptableValues: [], importPath: '', isImportedType: false,
96+
},
6797
],
6898
props: [],
6999
templates: [],

0 commit comments

Comments
 (0)