Skip to content

Commit

Permalink
fix: If the same action was included in multiple `ViewStateActionsCon…
Browse files Browse the repository at this point in the history
…fig` configs, only last action config would be used. (#4)
  • Loading branch information
yurakhomitsky authored Jun 27, 2024
1 parent f4eb9f2 commit a00330c
Show file tree
Hide file tree
Showing 10 changed files with 547 additions and 202 deletions.
4 changes: 4 additions & 0 deletions projects/ngx-view-state/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
## Changelog

## 2.1.0

- fix: If the same action was included in multiple `ViewStateActionsConfig` configs, only last action config would be used. The store, effects and service now correctly handles multiple actions across different configs.

## 2.0.0

- Rename ViewStateActionsConfig properties
Expand Down
2 changes: 1 addition & 1 deletion projects/ngx-view-state/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ngx-view-state",
"version": "2.0.0",
"version": "2.1.0",
"license": "MIT",
"description": "ngx-view-state is a library for managing the Loading/Success/Error states of views in Angular applications that use Ngrx or HttpClient",
"author": "Yurii Khomitskyi <yura.khomitsky8@gmail.com>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,86 +4,119 @@ import { Action } from '@ngrx/store';
import { ViewStateActionsConfig, ViewStateActionsService } from './view-state-actions.service';

describe('ViewStateActionsService', () => {
let service: ViewStateActionsService;

const loadData: Action = { type: 'load data' };
const loadDataSuccess: Action = { type: 'data loaded success' };
const lodDataFailure: Action = { type: 'data loaded failure' };

const actionsConfig: ViewStateActionsConfig[] = [
{
startLoadingOn: loadData,
resetOn: [loadDataSuccess],
errorOn: [lodDataFailure],
},
];

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ViewStateActionsService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});

beforeEach(() => {
service.add(actionsConfig);
});

describe('isStartLoadingAction', () => {
it('should return true for loadData action', () => {
expect(service.isStartLoadingAction(loadData)).toBe(true);
});

it('should return false for loadDataSuccess', () => {
expect(service.isStartLoadingAction(loadDataSuccess)).toBe(false);
});

it('should return false for lodDataFailure', () => {
expect(service.isStartLoadingAction(lodDataFailure)).toBe(false);
});
});

describe('isResetLoadingAction', () => {
it('should return true for loadDataSuccess', () => {
expect(service.isResetLoadingAction(loadDataSuccess)).toBe(true);
});

it('should return true for lodDataFailure', () => {
expect(service.isResetLoadingAction(lodDataFailure)).toBe(false);
});

it('should return false for loadData', () => {
expect(service.isResetLoadingAction(loadData)).toBe(false);
});
});

describe('isErrorAction', () => {
it('should return true for loadDataFailure', () => {
expect(service.isErrorAction(lodDataFailure)).toBe(true);
});

it('should return false for loadDataSuccess', () => {
expect(service.isErrorAction(loadDataSuccess)).toBe(false);
});

it('should return false for loadData', () => {
expect(service.isErrorAction(loadData)).toBe(false);
});
});

describe('getResetLoadingId', () => {
it('should return null', () => {
expect(service.getActionType({ type: 'some action' })).toBe(null);
});

it('should get correct resetLoadingId for loadDataSuccess', () => {
expect(service.getActionType(loadDataSuccess)).toBe(loadData.type);
});

it('should get correct resetLoadingId for lodDataFailure', () => {
expect(service.getActionType(lodDataFailure)).toBe(loadData.type);
});
});
let service: ViewStateActionsService;

const loadData: Action = { type: 'load data' };
const loadDataSuccess: Action = { type: 'data loaded success' };
const lodDataFailure: Action = { type: 'data loaded failure' };

const loadData2: Action = { type: 'load data 2' };
const loadDataSuccess2: Action = { type: 'data loaded success 2' };
const lodDataFailure2: Action = { type: 'data loaded failure 2' };

const actionsConfig: ViewStateActionsConfig[] = [
{
startLoadingOn: loadData,
resetOn: [loadDataSuccess, loadData2],
errorOn: [lodDataFailure, lodDataFailure2]
},
{
startLoadingOn: loadData2,
resetOn: [loadDataSuccess2],
errorOn: [lodDataFailure2]
}
];

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ViewStateActionsService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});

beforeEach(() => {
service.add(actionsConfig);
});

it('should contain correct actions', () => {
expect(service.actionsMap.get(loadData.type)).toEqual([{ viewState: 'startLoading' }]);
expect(service.actionsMap.get(loadDataSuccess.type)).toEqual([{ viewState: 'reset', actionType: loadData.type }]);
expect(service.actionsMap.get(lodDataFailure.type)).toEqual([{ viewState: 'error', actionType: loadData.type }]);

expect(service.actionsMap.get(loadData2.type))
.toEqual(jasmine.arrayContaining(
[
{ viewState: 'startLoading' }, { viewState: 'reset', actionType: loadData.type }
]
)
);
expect(service.actionsMap.get(loadDataSuccess2.type)).toEqual([{ viewState: 'reset', actionType: loadData2.type }]);
expect(service.actionsMap.get(lodDataFailure2.type))
.toEqual(jasmine.arrayContaining(
[
{ viewState: 'error', actionType: loadData2.type }, { viewState: 'error', actionType: loadData.type }
]
)
);
});

describe('isStartLoadingAction', () => {
it('should return true for loadData action', () => {
expect(service.isStartLoadingAction(loadData)).toBe(true);
});

it('should return false for loadDataSuccess', () => {
expect(service.isStartLoadingAction(loadDataSuccess)).toBe(false);
});

it('should return false for lodDataFailure', () => {
expect(service.isStartLoadingAction(lodDataFailure)).toBe(false);
});

it('should return true for loadData2', () => {
expect(service.isStartLoadingAction(loadData2)).toBe(true);
});
});

describe('isResetLoadingAction', () => {
it('should return true for loadDataSuccess', () => {
expect(service.isResetLoadingAction(loadDataSuccess)).toBe(true);
});

it('should return true for lodDataFailure', () => {
expect(service.isResetLoadingAction(lodDataFailure)).toBe(false);
});

it('should return false for loadData', () => {
expect(service.isResetLoadingAction(loadData)).toBe(false);
});

it('should return true for loadDataSuccess2', () => {
expect(service.isResetLoadingAction(loadDataSuccess2)).toBe(true);
});

it('should return true for loadData2', () => {
expect(service.isResetLoadingAction(loadData2)).toBe(true);
});
});

describe('isErrorAction', () => {
it('should return true for loadDataFailure', () => {
expect(service.isErrorAction(lodDataFailure)).toBe(true);
});

it('should return false for loadDataSuccess', () => {
expect(service.isErrorAction(loadDataSuccess)).toBe(false);
});

it('should return false for loadData', () => {
expect(service.isErrorAction(loadData)).toBe(false);
});

it('should return true for loadDataFailure2', () => {
expect(service.isErrorAction(lodDataFailure2)).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { Action } from '@ngrx/store';


export type ActionsMapConfig = { viewState: 'startLoading' } | { viewState: 'resetLoading', actionType: string } | { viewState: 'error', actionType: string };
export type ActionsMapConfig = { viewState: 'startLoading' } | { viewState: 'reset', actionType: string } | { viewState: 'error', actionType: string };

export interface ViewStateActionsConfig {
startLoadingOn: Action;
Expand All @@ -15,44 +15,71 @@ export interface ViewStateActionsConfig {
providedIn: 'root',
})
export class ViewStateActionsService {
private actionsMap = new Map<string, ActionsMapConfig>();
public readonly actionsMap = new Map<string, ActionsMapConfig[]>();

public isViewStateAction(action: Action): boolean {
return this.actionsMap.has(action.type);
}

public getActionConfigs(action: Action): ActionsMapConfig[] {
return this.actionsMap.get(action.type) ?? [];
}

public isStartLoadingAction(action: Action): boolean {
return this.actionsMap.get(action.type)?.viewState === 'startLoading';
return this.checkViewState(action, 'startLoading');
}

public isResetLoadingAction(action: Action): boolean {
return this.actionsMap.get(action.type)?.viewState === 'resetLoading';
return this.checkViewState(action, 'reset');
}

public isErrorAction(action: Action): boolean {
return this.actionsMap.get(action.type)?.viewState === 'error';
return this.checkViewState(action, 'error');
}

public getActionType(action: Action): string | null {
const actionConfig = this.actionsMap.get(action.type);
if (!actionConfig) {
return null;
}
public getErrorActionTypes(action: Action): string[] {
const configs = this.getActionConfigs(action);

return configs.reduce((acc: string[], config: ActionsMapConfig) => {
if (config.viewState === 'error') {
acc.push(config.actionType)
}
return acc;
}, []);
}

if (actionConfig.viewState === 'startLoading') {
return null;
}
public getResetActionTypes(action: Action): string[] {
const configs = this.actionsMap.get(action.type) ?? []

return actionConfig.actionType
return configs.reduce((acc: string[], config: ActionsMapConfig) => {
if (config.viewState === 'reset') {
acc.push(config.actionType)
}
return acc;
}, []);
}

public add(actions: ViewStateActionsConfig[]): void {
actions.forEach((action: ViewStateActionsConfig) => {
this.actionsMap.set(action.startLoadingOn.type, { viewState: 'startLoading' });
this.addActionToMap(action.startLoadingOn.type, { viewState: 'startLoading' });

action.resetOn.forEach((resetLoading: Action) => {
this.actionsMap.set(resetLoading.type, { viewState: 'resetLoading', actionType: action.startLoadingOn.type });
this.addActionToMap(resetLoading.type, { viewState: 'reset', actionType: action.startLoadingOn.type });
});

action.errorOn.forEach((errorAction: Action) => {
this.actionsMap.set(errorAction.type, { viewState: 'error', actionType: action.startLoadingOn.type });
this.addActionToMap(errorAction.type, { viewState: 'error', actionType: action.startLoadingOn.type });
});
});
}

private addActionToMap(actionType: string, actionConfig: ActionsMapConfig): void {
const existingConfigs = this.actionsMap.get(actionType) || [];
this.actionsMap.set(actionType, [...existingConfigs, actionConfig]);
}

private checkViewState(action: Action, viewState: string): boolean {
const configs = this.getActionConfigs(action);
return configs.some(config => config.viewState === viewState);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createActionGroup, props } from '@ngrx/store';

export const ViewStateActions = createActionGroup({
source: 'ViewState',
events: {
startLoading: props<{ actionType: string }>(),
reset: props<{ actionType: string }>(),
error: props<{ actionType: string; error?: unknown }>(),
},
source: 'ViewState',
events: {
startLoading: props<{ actionType: string }>(),
reset: props<{ actionType: string }>(),
resetMany: props<{ actionTypes: string[] }>(),
error: props<{ actionType: string; error?: unknown }>(),
errorMany: props<{ actionTypes: { actionType: string, error?: unknown }[] }>()
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ describe('ViewStateEffects', () => {
'isStartLoadingAction',
'isErrorAction',
'isResetLoadingAction',
'getActionType',
'getResetActionTypes',
'getErrorActionTypes',
'isViewStateAction',
]);

beforeEach(() => {
Expand All @@ -31,6 +33,8 @@ describe('ViewStateEffects', () => {
],
});

viewStateActionsServiceSpy.isViewStateAction.and.returnValue(true);

effects = TestBed.inject(ViewStateEffects);
});

Expand Down Expand Up @@ -68,16 +72,16 @@ describe('ViewStateEffects', () => {
});

describe('reset$', () => {
it('should map to reset action', (done) => {
it('should map to resetMany action', (done) => {
const loadDataSuccess: Action = { type: 'loadDataSuccess' };

effects.reset$.subscribe((action) => {
expect(action).toEqual(ViewStateActions.reset({ actionType: 'loadData' }));
expect(action).toEqual(ViewStateActions.resetMany({ actionTypes: ['loadData'] }));
done();
});

viewStateActionsServiceSpy.isResetLoadingAction.and.returnValue(true);
viewStateActionsServiceSpy.getActionType.and.returnValue('loadData');
viewStateActionsServiceSpy.getResetActionTypes.and.returnValue(['loadData']);

actions$.next(loadDataSuccess);
});
Expand All @@ -97,16 +101,16 @@ describe('ViewStateEffects', () => {
});

describe('error$', () => {
it('should map to error action', (done) => {
it('should map to errorMany action', (done) => {
const loadDataFailure: Action & ViewStateErrorProps<string> = { type: 'loadDataFailure', viewStateError: 'custom error message' };

effects.error$.subscribe((action) => {
expect(action).toEqual(ViewStateActions.error({ actionType: 'loadData', error: loadDataFailure.viewStateError ?? '' }));
expect(action).toEqual(ViewStateActions.errorMany({actionTypes: [{ actionType: 'loadData', error: loadDataFailure.viewStateError ?? '' }]}));
done();
});

viewStateActionsServiceSpy.isErrorAction.and.returnValue(true);
viewStateActionsServiceSpy.getActionType.and.returnValue('loadData');
viewStateActionsServiceSpy.getErrorActionTypes.and.returnValue(['loadData']);

actions$.next(loadDataFailure);
});
Expand Down
Loading

0 comments on commit a00330c

Please sign in to comment.