Skip to content

Commit 0dc33f8

Browse files
committed
feat(migrations): can now change binding type w/ transform function, #7325
1 parent 04abde0 commit 0dc33f8

File tree

4 files changed

+291
-80
lines changed

4 files changed

+291
-80
lines changed

projects/igniteui-angular/migrations/common/UpdateChanges.spec.ts

Lines changed: 176 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { EmptyTree } from '@angular-devkit/schematics';
44
import { UnitTestTree } from '@angular-devkit/schematics/testing';
55
import * as fs from 'fs';
66
import * as path from 'path';
7-
import { ClassChanges, BindingChanges, SelectorChanges, ThemePropertyChanges, ImportsChanges } from './schema';
8-
import { UpdateChanges } from './UpdateChanges';
7+
import { ClassChanges, BindingChanges, SelectorChanges, ThemePropertyChanges, ImportsChanges, ElementType } from './schema';
8+
import { UpdateChanges, InputPropertyType, BoundPropertyObject } from './UpdateChanges';
99
import * as tsUtils from './tsUtils';
1010

1111
describe('UpdateChanges', () => {
@@ -28,7 +28,7 @@ describe('UpdateChanges', () => {
2828
sourceRoot: '/'
2929
}
3030
}
31-
}));
31+
}));
3232
});
3333

3434
// tslint:disable:arrow-parens
@@ -269,35 +269,35 @@ describe('UpdateChanges', () => {
269269
spyOn<any>(fs, 'readFileSync').and.callFake(() => JSON.stringify(classJson));
270270

271271
const fileContent =
272-
`import { Component, Injectable, ViewChild } from "@angular/core";` +
273-
`import { IgxGridComponent } from "igniteui-angular";` +
274-
`import { IgxColumnComponent, IgxProvided, STRING_FILTERS} from "igniteui-angular";\r\n` +
275-
`import {` +
276-
` IgxCsvExporterService,` +
277-
` IgxExcelExporterOptions,` +
278-
` IgxExporterOptionsBase` +
279-
`} from "igniteui-angular";\r\n` +
280-
`@Component({` +
281-
` providers: [IgxProvided, RemoteService]` +
282-
`})` +
283-
`export class GridSampleComponent {` +
284-
` @ViewChild("grid1", { read: IgxGridComponent }) public grid1: IgxGridComponent;` +
285-
` // prop definitions to ignore:\r\n` +
286-
` NotType: { NotAgain: string; extraProp: IgxExcelExporterOptions, IgxExcelExporterOptions: string } = {` +
287-
` NotAgain: "hai",` +
288-
` extraProp: new IgxExcelExporterOptions(),` +
289-
` IgxExcelExporterOptions: "fake"` +
290-
` }` +
291-
` constructor(private csvExporterService: IgxCsvExporterService) { }` +
292-
` public initColumns(event: IgxColumnComponent) {` +
293-
` const column: IgxColumnComponent = event;` +
294-
` this.grid1.filter("ProductName", "Queso", STRING_FILTERS.contains, true);` +
295-
` }` +
296-
` private getOptions(fileName: string): IgxExporterOptionsBase {` +
297-
` return new IgxExcelExporterOptions(fileName);` +
298-
` }` +
299-
`}`
300-
;
272+
`import { Component, Injectable, ViewChild } from "@angular/core";` +
273+
`import { IgxGridComponent } from "igniteui-angular";` +
274+
`import { IgxColumnComponent, IgxProvided, STRING_FILTERS} from "igniteui-angular";\r\n` +
275+
`import {` +
276+
` IgxCsvExporterService,` +
277+
` IgxExcelExporterOptions,` +
278+
` IgxExporterOptionsBase` +
279+
`} from "igniteui-angular";\r\n` +
280+
`@Component({` +
281+
` providers: [IgxProvided, RemoteService]` +
282+
`})` +
283+
`export class GridSampleComponent {` +
284+
` @ViewChild("grid1", { read: IgxGridComponent }) public grid1: IgxGridComponent;` +
285+
` // prop definitions to ignore:\r\n` +
286+
` NotType: { NotAgain: string; extraProp: IgxExcelExporterOptions, IgxExcelExporterOptions: string } = {` +
287+
` NotAgain: "hai",` +
288+
` extraProp: new IgxExcelExporterOptions(),` +
289+
` IgxExcelExporterOptions: "fake"` +
290+
` }` +
291+
` constructor(private csvExporterService: IgxCsvExporterService) { }` +
292+
` public initColumns(event: IgxColumnComponent) {` +
293+
` const column: IgxColumnComponent = event;` +
294+
` this.grid1.filter("ProductName", "Queso", STRING_FILTERS.contains, true);` +
295+
` }` +
296+
` private getOptions(fileName: string): IgxExporterOptionsBase {` +
297+
` return new IgxExcelExporterOptions(fileName);` +
298+
` }` +
299+
`}`
300+
;
301301
appTree.create('test.component.ts', fileContent);
302302

303303
const update = new UnitUpdateChanges(__dirname, appTree);
@@ -378,7 +378,7 @@ describe('UpdateChanges', () => {
378378
{ name: 'Size', replaceWith: 'IgxSize' },
379379
{ name: 'IgxService', replaceWith: 'IgxService1' },
380380
{ name: 'IgxDiffService', replaceWith: 'IgxNewDiffService' },
381-
{ name: 'Calendar', replaceWith: 'CalendarActual'}
381+
{ name: 'Calendar', replaceWith: 'CalendarActual' }
382382
]
383383
};
384384
const jsonPath = path.join(__dirname, 'changes', 'classes.json');
@@ -609,4 +609,147 @@ export class AppModule { }`);
609609

610610
done();
611611
});
612+
613+
it('should handle changes with valueTransform functions', done => {
614+
const inputsJson: BindingChanges = {
615+
changes: [{
616+
'name': 'someProp',
617+
'replaceWith': 'someOtherProp',
618+
'valueTransform': 'some_prop_transform',
619+
'owner': {
620+
'selector': 'igx-component',
621+
'type': ElementType.component
622+
}
623+
}]
624+
};
625+
const jsonPath = path.join(__dirname, 'changes', 'inputs.json');
626+
spyOn(fs, 'existsSync').and.callFake((filePath: string) => {
627+
if (filePath === jsonPath) {
628+
return true;
629+
}
630+
return false;
631+
});
632+
spyOn(fs, 'readFileSync').and.returnValue(JSON.stringify(inputsJson));
633+
634+
// bracketed
635+
appTree.create(
636+
'test.component.html',
637+
'<igx-component [someProp]="true"></igx-component>'
638+
);
639+
640+
// No brackets
641+
appTree.create(
642+
'test2.component.html',
643+
'<igx-component someProp="otherVal"></igx-component>'
644+
);
645+
646+
// Small quotes
647+
appTree.create(
648+
'test3.component.html',
649+
`<igx-component someProp='otherVal'></igx-component>`
650+
);
651+
652+
// Multiple occurances
653+
appTree.create(
654+
'test4.component.html',
655+
`<igx-component [someProp]="true"><igx-component>
656+
<igx-component [someProp]="false" [someProp]="false" [someProp]="false" [someProp]="false"><igx-component>
657+
<igx-component someProp="true"><igx-component>
658+
<igx-component someProp="false"><igx-component>`
659+
);
660+
661+
const update = new UnitUpdateChanges(__dirname, appTree);
662+
expect(fs.existsSync).toHaveBeenCalledWith(jsonPath);
663+
expect(fs.readFileSync).toHaveBeenCalledWith(jsonPath, 'utf-8');
664+
expect(update.getInputChanges()).toEqual(inputsJson);
665+
update.addValueTransform('some_prop_transform', (args: BoundPropertyObject): void => {
666+
if (args.bindingType === InputPropertyType.EVAL) {
667+
args.value = args.value === 'true' ? '\'trueValue\'' : '\'falseValue\'';
668+
} else {
669+
args.value = args.value === 'true' ? 'trueValue' : 'falseValue';
670+
}
671+
});
672+
673+
update.applyChanges();
674+
expect(appTree.readContent('test.component.html')).toEqual(`<igx-component [someOtherProp]="'trueValue'"></igx-component>`);
675+
expect(appTree.readContent('test2.component.html')).toEqual(`<igx-component someOtherProp="falseValue"></igx-component>`);
676+
expect(appTree.readContent('test3.component.html')).toEqual(`<igx-component someOtherProp='falseValue'></igx-component>`);
677+
expect(appTree.readContent('test4.component.html')).toEqual(`<igx-component [someOtherProp]="'trueValue'"><igx-component>\n` +
678+
// tslint:disable-next-line:max-line-length
679+
`<igx-component [someOtherProp]="'falseValue'" [someOtherProp]="'falseValue'" [someOtherProp]="'falseValue'" [someOtherProp]="'falseValue'"><igx-component>
680+
<igx-component someOtherProp="trueValue"><igx-component>
681+
<igx-component someOtherProp="falseValue"><igx-component>`);
682+
done();
683+
});
684+
685+
it('Should be able to change binding type via transform function', done => {
686+
const inputsJson: BindingChanges = {
687+
changes: [{
688+
'name': 'prop',
689+
'replaceWith': 'newProp',
690+
'valueTransform': 'prop_transform',
691+
'owner': {
692+
'selector': 'igx-component',
693+
'type': ElementType.component
694+
}
695+
}]
696+
};
697+
const jsonPath = path.join(__dirname, 'changes', 'inputs.json');
698+
spyOn(fs, 'existsSync').and.callFake((filePath: string) => {
699+
if (filePath === jsonPath) {
700+
return true;
701+
}
702+
return false;
703+
});
704+
spyOn(fs, 'readFileSync').and.returnValue(JSON.stringify(inputsJson));
705+
706+
// bracketed
707+
appTree.create(
708+
'test-bound-to-string.component.html',
709+
`<igx-component [prop]="true">STRING</igx-component>
710+
<igx-component [prop]="false">STRING</igx-component>
711+
<igx-component [prop]="someOtherProperty">BOUND</igx-component>`
712+
);
713+
appTree.create(
714+
'test-string-to-bound.component.html',
715+
`<igx-component prop="changeThisToBound">BOUND</igx-component>
716+
<igx-component prop="leaveMeBe">STRING</igx-component>`
717+
);
718+
719+
const update = new UnitUpdateChanges(__dirname, appTree);
720+
expect(fs.existsSync).toHaveBeenCalledWith(jsonPath);
721+
expect(fs.readFileSync).toHaveBeenCalledWith(jsonPath, 'utf-8');
722+
expect(update.getInputChanges()).toEqual(inputsJson);
723+
update.addValueTransform('prop_transform', (args: BoundPropertyObject): void => {
724+
if (args.bindingType === InputPropertyType.EVAL) {
725+
switch (args.value) {
726+
case 'true':
727+
args.value = 'TRUTHY-STRING-VALUE';
728+
args.bindingType = InputPropertyType.STRING;
729+
break;
730+
case 'false':
731+
args.value = 'FALSY-STRING-VALUE';
732+
args.bindingType = InputPropertyType.STRING;
733+
break;
734+
default:
735+
args.value += ' ? true : false';
736+
}
737+
} else {
738+
if (args.value === 'changeThisToBound') {
739+
args.bindingType = InputPropertyType.EVAL;
740+
args.value = 'true';
741+
}
742+
}
743+
});
744+
745+
update.applyChanges();
746+
expect(appTree.readContent('test-bound-to-string.component.html')).toEqual(
747+
`<igx-component newProp="TRUTHY-STRING-VALUE">STRING</igx-component>
748+
<igx-component newProp="FALSY-STRING-VALUE">STRING</igx-component>
749+
<igx-component [newProp]="someOtherProperty ? true : false">BOUND</igx-component>`);
750+
expect(appTree.readContent('test-string-to-bound.component.html')).toEqual(
751+
`<igx-component [newProp]="true">BOUND</igx-component>
752+
<igx-component newProp="leaveMeBe">STRING</igx-component>`);
753+
done();
754+
});
612755
});

projects/igniteui-angular/migrations/common/UpdateChanges.ts

Lines changed: 56 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ import { getLanguageService, getRenamePositions } from './tsUtils';
88
import { getProjectPaths, getWorkspace, getProjects, escapeRegExp } from './util';
99
import { LanguageService } from 'typescript';
1010

11+
12+
export enum InputPropertyType {
13+
EVAL = 'eval',
14+
STRING = 'string'
15+
}
16+
declare type TransformFunction = (args: BoundPropertyObject) => void;
17+
export interface BoundPropertyObject {
18+
value: string;
19+
bindingType: InputPropertyType;
20+
}
21+
1122
// tslint:disable:arrow-parens
1223
export class UpdateChanges {
1324
protected workspace: WorkspaceSchema;
@@ -19,7 +30,7 @@ export class UpdateChanges {
1930
protected themePropsChanges: ThemePropertyChanges;
2031
protected importsChanges: ImportsChanges;
2132
protected conditionFunctions: Map<string, Function> = new Map<string, Function>();
22-
protected valueTransforms: Map<string, Function> = new Map<string, Function>();
33+
protected valueTransforms: Map<string, TransformFunction> = new Map<string, TransformFunction>();
2334

2435
private _templateFiles: string[] = [];
2536
public get templateFiles(): string[] {
@@ -131,7 +142,7 @@ export class UpdateChanges {
131142
this.conditionFunctions.set(conditionName, callback);
132143
}
133144

134-
public addValueTransform(functionName: string, callback: (oldValue: string) => string) {
145+
public addValueTransform(functionName: string, callback: TransformFunction) {
135146
this.valueTransforms.set(functionName, callback);
136147
}
137148

@@ -213,8 +224,8 @@ export class UpdateChanges {
213224
let searchPattern;
214225

215226
if (type === BindingType.output) {
216-
base = String.raw`\(${change.name}\)=(["'])`;
217-
replace = `(${change.replaceWith})=$1`;
227+
base = String.raw`\(${change.name}\)=(["'])(.*?)\1`;
228+
replace = `(${change.replaceWith})=$1$2$1`;
218229
} else {
219230
// Match both bound - [name] - and regular - name
220231
base = String.raw`(\s\[?)${change.name}(\s*\]?=)(["'])(.*?)\3`;
@@ -225,21 +236,22 @@ export class UpdateChanges {
225236
let reg = new RegExp(base, 'g');
226237
if (change.remove || change.moveBetweenElementTags) {
227238
// Group match (\1) as variable as it looks like octal escape (error in strict)
228-
reg = new RegExp(String.raw`\s*${base}.*?${'\\' + groups}(?=\s|\>)`, 'g');
239+
reg = new RegExp(String.raw`\s*${base}(?=\s|\>)`, 'g');
229240
replace = '';
230241
}
231242
switch (change.owner.type) {
232-
case 'component':
233-
searchPattern = String.raw`\<${change.owner.selector}[^\>]*\>`;
234-
break;
235-
case 'directive':
236-
searchPattern = String.raw`\<[^\>]*[\s\[]${change.owner.selector}[^\>]*\>`;
237-
break;
243+
case 'component':
244+
searchPattern = String.raw`\<${change.owner.selector}[^\>]*\>`;
245+
break;
246+
case 'directive':
247+
searchPattern = String.raw`\<[^\>]*[\s\[]${change.owner.selector}[^\>]*\>`;
248+
break;
238249
}
239250

240251
const matches = fileContent.match(new RegExp(searchPattern, 'g'));
241252

242253
for (const match of matches) {
254+
let replaceStatement = replace;
243255
if (!this.areConditionsFulfiled(match, change.conditions)) {
244256
continue;
245257
}
@@ -250,16 +262,26 @@ export class UpdateChanges {
250262
}
251263

252264
if (change.valueTransform) {
253-
const oldValue = match.match(reg).groups[3];
254-
const transform = this.valueTransforms.get(change.valueTransform);
255-
const newValue = transform(oldValue);
256-
// Check why the regEx does not return the correct value
257-
replace = replace.replace('$4', newValue);
265+
const regExpMatch = match.match(new RegExp(base));
266+
const bindingType = regExpMatch && regExpMatch[1].endsWith('[') ? InputPropertyType.EVAL : InputPropertyType.STRING;
267+
if (regExpMatch) {
268+
const value = regExpMatch[4];
269+
const transform = this.valueTransforms.get(change.valueTransform);
270+
const args = { value, bindingType };
271+
transform(args);
272+
if (args.bindingType !== bindingType) {
273+
replaceStatement = args.bindingType === InputPropertyType.EVAL ?
274+
replaceStatement.replace(`$1`, `$1[`).replace(`$2`, `]$2`) :
275+
replaceStatement.replace(`$1`, regExpMatch[1].replace('[', '')).replace('$2', regExpMatch[2].replace(']', ''));
276+
277+
}
278+
replaceStatement = replaceStatement.replace('$4', args.value);
279+
}
258280
}
259281

260282
fileContent = fileContent.replace(
261283
match,
262-
match.replace(reg, replace)
284+
match.replace(reg, replaceStatement)
263285
);
264286
}
265287
overwrite = true;
@@ -382,13 +404,13 @@ export class UpdateChanges {
382404
}
383405
}
384406

385-
/**
386-
* Safe split by `','`, considering possible inner function calls. E.g.:
387-
* ```
388-
* prop: inner-func(),
389-
* prop2: inner2(inner-param: 3, inner-param: inner-func(..))
390-
* ```
391-
*/
407+
/**
408+
* Safe split by `','`, considering possible inner function calls. E.g.:
409+
* ```
410+
* prop: inner-func(),
411+
* prop2: inner2(inner-param: 3, inner-param: inner-func(..))
412+
* ```
413+
*/
392414
private splitFunctionProps(body: string): string[] {
393415
const parts = [];
394416
let lastIndex = 0;
@@ -397,16 +419,16 @@ export class UpdateChanges {
397419
for (let i = 0; i < body.length; i++) {
398420
const char = body[i];
399421
switch (char) {
400-
case '(': level++; break;
401-
case ')': level--; break;
402-
case ',':
403-
if (!level) {
404-
parts.push(body.substring(lastIndex, i));
405-
lastIndex = i + 1;
406-
}
407-
break;
408-
default:
409-
break;
422+
case '(': level++; break;
423+
case ')': level--; break;
424+
case ',':
425+
if (!level) {
426+
parts.push(body.substring(lastIndex, i));
427+
lastIndex = i + 1;
428+
}
429+
break;
430+
default:
431+
break;
410432
}
411433
}
412434
parts.push(body.substring(lastIndex));

0 commit comments

Comments
 (0)