Skip to content

Commit

Permalink
Fix merging of deeply nested tour options.
Browse files Browse the repository at this point in the history
  • Loading branch information
hakimio committed Oct 25, 2023
1 parent 25d585b commit 458182e
Show file tree
Hide file tree
Showing 5 changed files with 581 additions and 304 deletions.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@
"zone.js": "~0.13.1"
},
"devDependencies": {
"@angular-builders/jest": "^16.0.0",
"@angular-builders/jest": "^16.0.1",
"@angular-devkit/build-angular": "^16.1.4",
"@angular-eslint/builder": "^16.1.0",
"@angular-eslint/eslint-plugin": "^16.1.0",
Expand All @@ -92,13 +92,13 @@
"@angular/compiler": "^16.1.5",
"@angular/compiler-cli": "^16.1.5",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.3",
"@types/jest": "^29.5.6",
"@types/node": "^18.16.19",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.44.0",
"gh-pages": "^5.0.0",
"jest": "^29.6.1",
"jest": "^29.7.0",
"ng-packagr": "^16.1.0",
"raw-loader": "^4.0.2",
"shx": "^0.3.4",
Expand Down
98 changes: 98 additions & 0 deletions projects/ngx-ui-tour-core/src/lib/deep-merge.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {deepMerge} from './deep-merge';

class FooBar {
}

describe('deepMerge', () => {

it('should merge multiple objects', () => {
const obj1 = {a: 1, b: {c: 3}} as object,
obj2 = {a: 2, b: {d: 4}, e: 5} as object,
obj3 = {a: 6, f: {g: 7}, h: 8} as object,
expectedResult = {a: 6, b: {c: 3, d: 4}, e: 5, f: {g: 7}, h: 8};

const result = deepMerge(obj1, obj2, obj3);

expect(result).toMatchObject(expectedResult);
});

it('should copy class instance by reference', () => {
const fooBar = new FooBar(),
obj1 = {a: 1, b: fooBar, d: {e: 'a', f: 'b'}} as object,
obj2 = {a: 2, d: {g: 'c'}} as object,
expectedResult = {a: 2, b: fooBar, d: {e: 'a', f: 'b', g: 'c'}};

const result = deepMerge(obj1, obj2);

expect(result).toMatchObject(expectedResult);
expect((result as { b: FooBar }).b).toBe(fooBar);
});

it('should copy array instance by reference', () => {
const someArray = [1, 2, 3],
obj1 = {a: 1, b: someArray, d: {e: 'a', f: 'b'}} as object,
obj2 = {a: 2, d: {g: 'c'}} as object,
expectedResult = {a: 2, b: someArray, d: {e: 'a', f: 'b', g: 'c'}};

const result = deepMerge(obj1, obj2);

expect(result).toMatchObject(expectedResult);
expect((result as { b: number[] }).b).toBe(someArray);
});

it('should work when one of the parameters is undefined', () => {
const fooBar = new FooBar(),
obj1 = {a: 1, b: fooBar, d: {e: 'a', f: 'b'}} as object,
obj2 = {a: 2, d: {g: 'c'}} as object,
expectedResult = {a: 2, b: fooBar, d: {e: 'a', f: 'b', g: 'c'}};

const result = deepMerge(undefined, obj1, obj2);

expect(result).toMatchObject(expectedResult);
});

it('should overwrite arrays', () => {
const array1 = ['a'],
array2 = ['b'],
obj1 = {a: array1},
obj2 = {a: array2},
expectedResult = {a: array2};

const result = deepMerge(obj1, obj2);

expect(result).toMatchObject(expectedResult);
expect((result as { a: string[] }).a).toBe(array2);
});

it('should overwrite objects with non-objects', () => {
const obj1 = {a: {}} as object,
obj2 = {a: null} as object,
expectedResult = {a: null} as object;

const result = deepMerge(obj1, obj2);

expect(result).toMatchObject(expectedResult);
expect((result as { a?: object }).a).toBeNull();
});

it('should overwrite non-objects with empty objects', () => {
const obj1 = {a: 'b'},
obj2 = {a: {}},
expectedResult = {a: {}};

const result = deepMerge(obj1, obj2);

expect(result).toMatchObject(expectedResult);
});

it('should merge empty objects into other objects', () => {
const obj1 = {a: {b: 'c'}},
obj2 = {a: {}},
expectedResult = {a: {b: 'c'}};

const result = deepMerge(obj1, obj2);

expect(result).toMatchObject(expectedResult);
});

});
26 changes: 26 additions & 0 deletions projects/ngx-ui-tour-core/src/lib/deep-merge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
type PlainObject = Record<string | number | symbol, unknown>;

export function deepMerge<T>(...objects: T[]): T {
return objects.reduce((acc: T, cur: T) => {

cur ??= {} as T;
const keys = Object.keys(cur) as (keyof T)[];

for (const key of keys) {
const accValue = acc[key] as PlainObject | unknown,
curValue = cur[key] as PlainObject | unknown;

if (isPlainObject(accValue) && isPlainObject(curValue)) {
acc[key] = deepMerge(accValue, curValue) as T[keyof T];
} else {
acc[key] = curValue as T[keyof T];
}
}

return acc;
}, {} as T);
}

function isPlainObject(value: unknown | PlainObject): value is PlainObject {
return value instanceof Object && value.constructor === Object;
}
15 changes: 8 additions & 7 deletions projects/ngx-ui-tour-core/src/lib/tour.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {ScrollingService} from './scrolling.service';
import {BackdropConfig, TourBackdropService} from './tour-backdrop.service';
import {AnchorClickService} from './anchor-click.service';
import {ScrollBlockingService} from './scroll-blocking.service';
import {deepMerge} from './deep-merge';

export interface StepDimensions {
width?: string;
Expand Down Expand Up @@ -65,7 +66,7 @@ export interface StepChangeParams<T extends IStepOption = IStepOption> {
direction: Direction;
}

const DEFAULT_STEP_OPTIONS: Partial<IStepOption> = {
const DEFAULT_STEP_OPTIONS: IStepOption = {
disableScrollToAnchor: false,
prevBtnTitle: 'Prev',
nextBtnTitle: 'Next',
Expand Down Expand Up @@ -151,12 +152,12 @@ export class TourService<T extends IStepOption = IStepOption> {
if (steps && steps.length > 0) {
this.status = TourState.OFF;
this.steps = steps.map(
step => ({
...DEFAULT_STEP_OPTIONS,
...this.userDefaults,
...stepDefaults,
...step
})
step => deepMerge(
DEFAULT_STEP_OPTIONS as T,
this.userDefaults,
stepDefaults,
step
)
);
this.validateSteps();
this.initialize$.next(this.steps);
Expand Down
Loading

0 comments on commit 458182e

Please sign in to comment.