Skip to content

Commit

Permalink
[ngssm-data] Add directive to help with scoped data sources (#228)
Browse files Browse the repository at this point in the history
  • Loading branch information
LionMarc authored Jun 6, 2024
1 parent 3f4a147 commit d1567c1
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 63 deletions.
1 change: 1 addition & 0 deletions projects/ngssm-data/src/lib/ngssm-data/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './is-ngssm-data-source-value-status.pipe';
export * from './ngssm-data-reload-button/ngssm-data-reload-button.component';
export * from './ngssm-scoped-data-source.directive';
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { of } from 'rxjs';

import { StoreMock } from 'ngssm-store/testing';

import { NgssmScopedDataSourceDirective } from './ngssm-scoped-data-source.directive';
import { NgssmDataActionType, NgssmUnregisterDataSourceAction } from '../actions';

describe('NgssmScopedDataSourceDirective', () => {
let store: StoreMock;

beforeEach(() => {
store = new StoreMock({});
spyOn(store, 'dispatchAction');
});

it('should register the source when created', () => {
const directive = new NgssmScopedDataSourceDirective(store as any);
directive.ngssmScopedDataSource = {
key: 'test',
dataLoadingFunc: () => of([])
};
const recentCallArgs = (store.dispatchAction as any).calls.mostRecent().args[0];
expect(recentCallArgs.type).toEqual(NgssmDataActionType.registerDataSource);
expect(recentCallArgs.dataSource.key).toEqual('test');
});

it('should unregister the source when deleted', () => {
const directive = new NgssmScopedDataSourceDirective(store as any);
directive.ngssmScopedDataSource = {
key: 'test',
dataLoadingFunc: () => of([])
};
directive.ngOnDestroy();
expect(store.dispatchAction).toHaveBeenCalledWith(new NgssmUnregisterDataSourceAction('test'));
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Directive, Input, OnDestroy } from '@angular/core';

import { Store } from 'ngssm-store';

import { NgssmDataSource } from '../model';
import { NgssmRegisterDataSourceAction, NgssmUnregisterDataSourceAction } from '../actions';

@Directive({
selector: '[ngssmScopedDataSource]',
standalone: true
})
export class NgssmScopedDataSourceDirective implements OnDestroy {
private _dataSource: NgssmDataSource | undefined;

constructor(private store: Store) {}

@Input() set ngssmScopedDataSource(value: NgssmDataSource) {
if (this._dataSource) {
throw new Error('Data source is already set.');
}
this._dataSource = value;
this.store.dispatchAction(new NgssmRegisterDataSourceAction(this._dataSource));
}

public ngOnDestroy(): void {
const key = this._dataSource?.key;
if (key) {
this.store.dispatchAction(new NgssmUnregisterDataSourceAction(key));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,19 @@ describe('RemoteDataLoadingEffect', () => {
[RemoteDataStateSpecification.featureStateKey]: RemoteDataStateSpecification.initialState
});
TestBed.configureTestingModule({
imports: [MatSnackBarModule],
providers: [
imports: [MatSnackBarModule],
providers: [
RemoteDataLoadingEffect,
provideRemoteDataFunc(remoteDataKeyForFunc, loadingFunc),
{
provide: NGSSM_REMOTE_DATA_PROVIDER,
useClass: RemoteDataTesting,
multi: true
provide: NGSSM_REMOTE_DATA_PROVIDER,
useClass: RemoteDataTesting,
multi: true
},
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting()
]
});
]
});
effect = TestBed.inject(RemoteDataLoadingEffect);
httpTestingController = TestBed.inject(HttpTestingController);
});
Expand Down
6 changes: 2 additions & 4 deletions projects/ngssm-schematics/schematics/add-eslint/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { Rule, SchematicContext, Tree, chain, externalSchematic } from '@angular-devkit/schematics';
import { addPackageJsonDependency, NodeDependencyType, NodeDependency } from '@schematics/angular/utility/dependencies';

function addEslint():Rule {
function addEslint(): Rule {
return (host: Tree, context: SchematicContext) => {
const dependencies: NodeDependency[] = [
{ type: NodeDependencyType.Dev, version: '^8.57.0', name: 'eslint' }
];
const dependencies: NodeDependency[] = [{ type: NodeDependencyType.Dev, version: '^8.57.0', name: 'eslint' }];

dependencies.forEach((dependency) => {
addPackageJsonDependency(host, dependency);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<p [ngssmScopedDataSource]="dataSource">component-with-scoped-data-source works!</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { of } from 'rxjs';

import { NgSsmComponent, Store } from 'ngssm-store';
import { NgssmDataSource, NgssmScopedDataSourceDirective } from 'ngssm-data';

@Component({
selector: 'app-component-with-scoped-data-source',
standalone: true,
imports: [CommonModule, NgssmScopedDataSourceDirective],
templateUrl: './component-with-scoped-data-source.component.html',
styleUrls: ['./component-with-scoped-data-source.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ComponentWithScopedDataSourceComponent extends NgSsmComponent {
private static nextId = 1;
public readonly dataSource: NgssmDataSource<string[], unknown> = {
key: `scoped-${ComponentWithScopedDataSourceComponent.nextId++}`,
dataLoadingFunc: () => of([])
};

constructor(store: Store) {
super(store);
}
}
Original file line number Diff line number Diff line change
@@ -1,51 +1,81 @@
<mat-card class="fxFlex flex-column-stretch subcard">
<mat-card-header>
<mat-card-title class="flex-row-center">
Data source registered at startup

@if(store.state() | isNgssmDataSourceValueStatus:teamsKey:'loading') {
<mat-spinner [diameter]="24"></mat-spinner>
}

</mat-card-title>
</mat-card-header>
<mat-card-content class="flex-column-stretch fxFlex"
[ngssmDisplayOverlay]="store.state() | isNgssmDataSourceValueStatus:teamsKey:'loading'">
<div class="flex-row-center">
<ngssm-data-reload-button [dataSourceKeys]="[teamsKey]"></ngssm-data-reload-button>
</div>
<div class="flex-row-center">
Reload all: <ngssm-data-reload-button [dataSourceKeys]="[teamsKey, playersKey]"></ngssm-data-reload-button>
</div>

{{teamsSourceValue() | json}}
</mat-card-content>
</mat-card>

<mat-card class="fxFlex flex-column-stretch subcard">
<mat-card-header>
<mat-card-title class="flex-row-center">
Data source not registered at startup

@if(store.state() | isNgssmDataSourceValueStatus:playersKey:'loading') {
<mat-spinner [diameter]="24"></mat-spinner>
} @else if(store.state() | isNgssmDataSourceValueStatus:playersKey:'error':'none') {
[No data]
} @else if(store.state() | isNgssmDataSourceValueStatus:playersKey:'notRegistered') {
[Not registered]
}

</mat-card-title>
</mat-card-header>
<mat-card-content class="flex-column-stretch fxFlex">
<div class="flex-row-center">
<ngssm-data-reload-button [dataSourceKeys]="[playersKey]"></ngssm-data-reload-button>
<button mat-stroked-button color="primary" (click)="registerPlayers()" class="with-margin-right-12">
Register players source
</button>
</div>


{{playersSourceValue() | json}}
</mat-card-content>
</mat-card>
<div class="fxFlex flex-row-stretch">

<div class="fxFlex flex-column-stretch">


<mat-card class="fxFlex flex-column-stretch subcard">
<mat-card-header>
<mat-card-title class="flex-row-center">
Data source registered at startup

@if(store.state() | isNgssmDataSourceValueStatus:teamsKey:'loading') {
<mat-spinner [diameter]="24"></mat-spinner>
}

</mat-card-title>
</mat-card-header>
<mat-card-content class="flex-column-stretch fxFlex"
[ngssmDisplayOverlay]="store.state() | isNgssmDataSourceValueStatus:teamsKey:'loading'">
<div class="flex-row-center">
<ngssm-data-reload-button [dataSourceKeys]="[teamsKey]"></ngssm-data-reload-button>
</div>
<div class="flex-row-center">
Reload all: <ngssm-data-reload-button
[dataSourceKeys]="[teamsKey, playersKey]"></ngssm-data-reload-button>
</div>

{{teamsSourceValue() | json}}
</mat-card-content>
</mat-card>

<mat-card class="fxFlex flex-column-stretch subcard">
<mat-card-header>
<mat-card-title class="flex-row-center">
Data source not registered at startup

@if(store.state() | isNgssmDataSourceValueStatus:playersKey:'loading') {
<mat-spinner [diameter]="24"></mat-spinner>
} @else if(store.state() | isNgssmDataSourceValueStatus:playersKey:'error':'none') {
[No data]
} @else if(store.state() | isNgssmDataSourceValueStatus:playersKey:'notRegistered') {
[Not registered]
}

</mat-card-title>
</mat-card-header>
<mat-card-content class="flex-column-stretch fxFlex">
<div class="flex-row-center">
<ngssm-data-reload-button [dataSourceKeys]="[playersKey]"></ngssm-data-reload-button>
<button mat-stroked-button color="primary" (click)="registerPlayers()" class="with-margin-right-12">
Register players source
</button>
</div>


{{playersSourceValue() | json}}
</mat-card-content>
</mat-card>

<mat-card class="fxFlex flex-column-stretch subcard">
<mat-card-header>
<mat-card-title>Component with scoped data source</mat-card-title>
</mat-card-header>
<mat-card-content class="fxFlex flex-column-stretch">
<button mat-stroked-button color="primary"
(click)="componentWithScopedDatasourceRendered.set(!componentWithScopedDatasourceRendered())">
@if(componentWithScopedDatasourceRendered()) {
Remove
} @else {
Display
}
component with scoped source
</button>
@if(componentWithScopedDatasourceRendered()) {
<app-component-with-scoped-data-source class="fxFlex"></app-component-with-scoped-data-source>
}
</mat-card-content>
</mat-card>
</div>
<ngssm-ace-editor class="fxFlex" [content]="state()" [editorMode]="'ace/mode/json'" [readonly]="true">
</ngssm-ace-editor>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import {
NgssmDataReloadButtonComponent
} from 'ngssm-data';
import { NgssmComponentOverlayDirective } from 'ngssm-toolkit';
import { NgssmAceEditorComponent } from 'ngssm-ace-editor';

import { playersKey, playersLoader, teamsKey } from '../../model';
import { ComponentWithScopedDataSourceComponent } from '../component-with-scoped-data-source/component-with-scoped-data-source.component';

@Component({
selector: 'app-ngssm-data-demo',
Expand All @@ -26,7 +28,9 @@ import { playersKey, playersLoader, teamsKey } from '../../model';
MatButtonModule,
IsNgssmDataSourceValueStatusPipe,
NgssmDataReloadButtonComponent,
NgssmComponentOverlayDirective
NgssmComponentOverlayDirective,
NgssmAceEditorComponent,
ComponentWithScopedDataSourceComponent
],
templateUrl: './ngssm-data-demo.component.html',
styleUrls: ['./ngssm-data-demo.component.scss'],
Expand All @@ -38,12 +42,15 @@ export class NgssmDataDemoComponent extends NgSsmComponent {

public readonly teamsSourceValue = signal<any>({});
public readonly playersSourceValue = signal<any>({});
public readonly componentWithScopedDatasourceRendered = signal<boolean>(false);
public readonly state = signal<string>('{}');

constructor(store: Store) {
super(store);

this.watch((s) => selectNgssmDataSourceValue(s, teamsKey)).subscribe((v) => this.teamsSourceValue.set(v));
this.watch((s) => selectNgssmDataSourceValue(s, playersKey)).subscribe((v) => this.playersSourceValue.set(v));
this.watch((s) => s).subscribe((s) => this.state.set(JSON.stringify(s, undefined, 4)));
}

public reloadTeams(): void {
Expand Down

0 comments on commit d1567c1

Please sign in to comment.