Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/dashboard/src/app/widgets/widgets.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</fem-widgets-list>
</div>
<div class="details-component">
<fem-widget-details [widget]="selectedWidget"
<fem-widget-details [widget]="selectedWidget$ | async"
(saved)="saveWidget($event)"
(cancelled)="resetForm()">
</fem-widget-details>
Expand Down
19 changes: 10 additions & 9 deletions apps/dashboard/src/app/widgets/widgets.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Widget } from '@fem/api-interfaces';
import { WidgetsService } from '@fem/core-data';
import { WidgetsFacade } from '@fem/core-state';
import { Observable } from 'rxjs';

const emptyWidget: Widget = {
Expand All @@ -15,10 +16,10 @@ const emptyWidget: Widget = {
styleUrls: ['./widgets.component.scss'],
})
export class WidgetsComponent implements OnInit {
widgets$: Observable<Widget[]>;
selectedWidget: Widget;
widgets$: Observable<Widget[]> = this.widgetsFacade.allWidget$;
selectedWidget$: Observable<Widget> = this.widgetsFacade.selectedWidget$;

constructor(private widgetsService: WidgetsService) {}
constructor(private widgetsFacade: WidgetsFacade ) {}

ngOnInit(): void {
this.reset();
Expand All @@ -30,15 +31,15 @@ export class WidgetsComponent implements OnInit {
}

resetForm() {
this.selectedWidget = emptyWidget;
this.selectWidget(emptyWidget);
}

selectWidget(widget: Widget) {
this.selectedWidget = widget;
this.widgetsFacade.selectWidget(emptyWidget);
}

loadWidgets() {
this.widgets$ = this.widgetsService.all();
this.widgetsFacade.loadWidgets();
}

saveWidget(widget: Widget) {
Expand All @@ -50,14 +51,14 @@ export class WidgetsComponent implements OnInit {
}

createWidget(widget: Widget) {
this.widgetsService.create(widget).subscribe((result) => this.reset());
// this.widgetsService.create(widget).subscribe((result) => this.reset());
}

updateWidget(widget: Widget) {
this.widgetsService.update(widget).subscribe((result) => this.reset());
//this.widgetsService.update(widget).subscribe((result) => this.reset());
}

deleteWidget(widget: Widget) {
this.widgetsService.delete(widget).subscribe((result) => this.reset());
// this.widgetsService.delete(widget).subscribe((result) => this.reset());
}
}
5 changes: 5 additions & 0 deletions libs/core-state/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export * from './lib/widgets/widgets.actions';
export * from './lib/widgets/widgets.reducer';
export * from './lib/widgets/widgets.selectors';
export * from './lib/widgets/widgets.models';
export * from './lib/widgets/widgets.facade';
export * from './lib/core-state.module';
17 changes: 16 additions & 1 deletion libs/core-state/src/lib/core-state.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import * as fromWidgets from './widgets/widgets.reducer';
import { WidgetsEffects } from './widgets/widgets.effects';
import { WidgetsFacade } from './widgets/widgets.facade';
import { CoreDataModule } from '@fem/core-data';

@NgModule({
imports: [CommonModule],
imports: [
CommonModule,
CoreDataModule,
// StoreModule.forFeature(
// fromWidgets.WIDGETS_FEATURE_KEY,
// fromWidgets.reducer
// ),
// EffectsModule.forFeature([WidgetsEffects]),
],
providers: [WidgetsFacade],
})
export class CoreStateModule {}
14 changes: 14 additions & 0 deletions libs/core-state/src/lib/widgets/widgets.actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createAction, props } from '@ngrx/store';
import { WidgetsEntity } from './widgets.models';

export const loadWidgets = createAction('[Widgets] Load Widgets');

export const loadWidgetsSuccess = createAction(
'[Widgets] Load Widgets Success',
props<{ widgets: WidgetsEntity[] }>()
);

export const loadWidgetsFailure = createAction(
'[Widgets] Load Widgets Failure',
props<{ error: any }>()
);
43 changes: 43 additions & 0 deletions libs/core-state/src/lib/widgets/widgets.effects.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { TestBed, async } from '@angular/core/testing';

import { Observable } from 'rxjs';

import { provideMockActions } from '@ngrx/effects/testing';
import { provideMockStore } from '@ngrx/store/testing';

import { NxModule, DataPersistence } from '@nrwl/angular';
import { hot } from '@nrwl/angular/testing';

import { WidgetsEffects } from './widgets.effects';
import * as WidgetsActions from './widgets.actions';

describe('WidgetsEffects', () => {
let actions: Observable<any>;
let effects: WidgetsEffects;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [NxModule.forRoot()],
providers: [
WidgetsEffects,
DataPersistence,
provideMockActions(() => actions),
provideMockStore(),
],
});

effects = TestBed.get(WidgetsEffects);
});

describe('loadWidgets$', () => {
it('should work', () => {
actions = hot('-a-|', { a: WidgetsActions.loadWidgets() });

const expected = hot('-a-|', {
a: WidgetsActions.loadWidgetsSuccess({ widgets: [] }),
});

expect(effects.loadWidgets$).toBeObservable(expected);
});
});
});
28 changes: 28 additions & 0 deletions libs/core-state/src/lib/widgets/widgets.effects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
import { createEffect, Actions, ofType } from '@ngrx/effects';
import { fetch } from '@nrwl/angular';

import * as fromWidgets from './widgets.reducer';
import * as WidgetsActions from './widgets.actions';

@Injectable()
export class WidgetsEffects {
loadWidgets$ = createEffect(() =>
this.actions$.pipe(
ofType(WidgetsActions.loadWidgets),
fetch({
run: (action) => {
// Your custom service 'load' logic goes here. For now just return a success action...
return WidgetsActions.loadWidgetsSuccess({ widgets: [] });
},

onError: (action, error) => {
console.error('Error', error);
return WidgetsActions.loadWidgetsFailure({ error });
},
})
)
);

constructor(private actions$: Actions) {}
}
118 changes: 118 additions & 0 deletions libs/core-state/src/lib/widgets/widgets.facade.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { NgModule } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { readFirst } from '@nrwl/angular/testing';

import { EffectsModule } from '@ngrx/effects';
import { StoreModule, Store } from '@ngrx/store';

import { NxModule } from '@nrwl/angular';

import { WidgetsEntity } from './widgets.models';
import { WidgetsEffects } from './widgets.effects';
import { WidgetsFacade } from './widgets.facade';

import * as WidgetsSelectors from './widgets.selectors';
import * as WidgetsActions from './widgets.actions';
import {
WIDGETS_FEATURE_KEY,
State,
initialState,
reducer,
} from './widgets.reducer';

interface TestSchema {
widgets: State;
}

describe('WidgetsFacade', () => {
let facade: WidgetsFacade;
let store: Store<TestSchema>;
const createWidgetsEntity = (id: string, name = '') =>
({
id,
name: name || `name-${id}`,
} as WidgetsEntity);

beforeEach(() => {});

describe('used in NgModule', () => {
beforeEach(() => {
@NgModule({
imports: [
StoreModule.forFeature(WIDGETS_FEATURE_KEY, reducer),
EffectsModule.forFeature([WidgetsEffects]),
],
providers: [WidgetsFacade],
})
class CustomFeatureModule {}

@NgModule({
imports: [
NxModule.forRoot(),
StoreModule.forRoot({}),
EffectsModule.forRoot([]),
CustomFeatureModule,
],
})
class RootModule {}
TestBed.configureTestingModule({ imports: [RootModule] });

store = TestBed.get(Store);
facade = TestBed.get(WidgetsFacade);
});

/**
* The initially generated facade::loadAll() returns empty array
*/
it('loadAll() should return empty list with loaded == true', async (done) => {
try {
let list = await readFirst(facade.allWidgets$);
let isLoaded = await readFirst(facade.loaded$);

expect(list.length).toBe(0);
expect(isLoaded).toBe(false);

facade.dispatch(WidgetsActions.loadWidgets());

list = await readFirst(facade.allWidgets$);
isLoaded = await readFirst(facade.loaded$);

expect(list.length).toBe(0);
expect(isLoaded).toBe(true);

done();
} catch (err) {
done.fail(err);
}
});

/**
* Use `loadWidgetsSuccess` to manually update list
*/
it('allWidgets$ should return the loaded list; and loaded flag == true', async (done) => {
try {
let list = await readFirst(facade.allWidgets$);
let isLoaded = await readFirst(facade.loaded$);

expect(list.length).toBe(0);
expect(isLoaded).toBe(false);

facade.dispatch(
WidgetsActions.loadWidgetsSuccess({
widgets: [createWidgetsEntity('AAA'), createWidgetsEntity('BBB')],
})
);

list = await readFirst(facade.allWidgets$);
isLoaded = await readFirst(facade.loaded$);

expect(list.length).toBe(2);
expect(isLoaded).toBe(true);

done();
} catch (err) {
done.fail(err);
}
});
});
});
33 changes: 33 additions & 0 deletions libs/core-state/src/lib/widgets/widgets.facade.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';

import { select, Store, Action } from '@ngrx/store';
import { Subject } from 'rxjs';

import * as fromWidgets from './widgets.reducer';
import * as WidgetsSelectors from './widgets.selectors';

import {Widget} from '@fem/api-interfaces'
import { WidgetsService } from '@fem/core-data';

@Injectable()
export class WidgetsFacade {
private allWidgets = new Subject<Widget[]>();
private selectedWidget = new Subject<Widget>();
private mutations = new Subject();

allWidget$ = this.allWidgets.asObservable();
selectedWidget$ = this.selectedWidget.asObservable();
mutations$ = this.mutations.asObservable();

constructor(private widgetsService: WidgetsService) { }

selectWidget(widget: Widget) {
this.selectedWidget.next(widget);
}

loadWidgets() {
this.widgetsService
.all()
.subscribe((widgets: Widget[]) => this.allWidgets.next(widgets));
}
}
6 changes: 6 additions & 0 deletions libs/core-state/src/lib/widgets/widgets.models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* Interface for the 'Widgets' data
*/
export interface WidgetsEntity {
id: string | number; // Primary ID
}
38 changes: 38 additions & 0 deletions libs/core-state/src/lib/widgets/widgets.reducer.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { WidgetsEntity } from './widgets.models';
import * as WidgetsActions from './widgets.actions';
import { State, initialState, reducer } from './widgets.reducer';

describe('Widgets Reducer', () => {
const createWidgetsEntity = (id: string, name = '') =>
({
id,
name: name || `name-${id}`,
} as WidgetsEntity);

beforeEach(() => {});

describe('valid Widgets actions', () => {
it('loadWidgetsSuccess should return set the list of known Widgets', () => {
const widgets = [
createWidgetsEntity('PRODUCT-AAA'),
createWidgetsEntity('PRODUCT-zzz'),
];
const action = WidgetsActions.loadWidgetsSuccess({ widgets });

const result: State = reducer(initialState, action);

expect(result.loaded).toBe(true);
expect(result.ids.length).toBe(2);
});
});

describe('unknown action', () => {
it('should return the previous state', () => {
const action = {} as any;

const result = reducer(initialState, action);

expect(result).toBe(initialState);
});
});
});
Loading