Skip to content

chore(core): migrate bind-to-query directives #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { TestBed } from '@angular/core/testing';
import { Directive } from '@angular/core';
import { Subject } from 'rxjs';
import { Params } from '@angular/router';
import { BindQueryParamsAbstract } from './bind-query-params.abstract';
import { Mocked, MockService } from '../../test-utils/mock-service';
import { ParameterService } from '../../services/paramter/parameter.service';

describe('bind-query-params.abstract', () => {

@Directive({
// eslint-disable-next-line @angular-eslint/directive-selector
selector: '[testBindQueryParams]',
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
class BindQueryParamsImpl extends BindQueryParamsAbstract {

setInitialSpy = jest.fn();

override source$!: Subject<Params>;

constructor(
protected paramService: ParameterService,
) {
super(paramService, new Subject());
}

protected setInitialValue(params: Params) {
this.setInitialSpy(params);
}
}

let instance: BindQueryParamsImpl;
let parameterService: Mocked<ParameterService>;

beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [
BindQueryParamsImpl,
{provide: ParameterService, useClass: MockService(ParameterService)()},
],

}).compileComponents();

instance = TestBed.inject(BindQueryParamsImpl);
parameterService = TestBed.inject(ParameterService) as Mocked<ParameterService>;
});

it('should create', () => {
expect(instance).toBeTruthy();
});

describe('AfterViewInit', () => {
const params = {
x: 'gojira',
y: 'mothra',
};

it('should parse and set initial values from query params', () => {
parameterService.getQueryParams.mockReturnValue(params);
instance.ngAfterViewInit();

expect(instance.setInitialSpy).toHaveBeenCalledWith(params);
});

it('should parse and set new values as query params, when source emits', () => {
instance.ngAfterViewInit();
const values = {
a: 'ghidorah',
b: 'rodan',
};
instance.source$.next(values);

expect(parameterService.setQueryParams).toHaveBeenCalledWith(values);
});

describe('when waiting for parent component', () => {
beforeEach(() => {
parameterService.getQueryParams.mockReturnValue(params);
instance.waitForParent = true;
});

it('should not set the initial value right away', () => {
instance.ngAfterViewInit();
expect(instance.setInitialSpy).not.toBeCalled();
});

it('should set the initial value after the parentInitialized input is set to true', () => {
instance.ngAfterViewInit();
instance.parentInitialized = true;
expect(instance.setInitialSpy).toBeCalledWith(params);
});

it('should set the initial value only once', () => {
instance.ngAfterViewInit();
instance.parentInitialized = true;
instance.parentInitialized = false;
instance.parentInitialized = true;

expect(instance.setInitialSpy).toBeCalledTimes(1);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { AfterViewInit, Directive, Input, OnDestroy } from '@angular/core';
import { Params } from '@angular/router';
import { filter, map, take, takeUntil, tap } from 'rxjs/operators';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { ParameterService } from '../../services/paramter/parameter.service';

@Directive()
export abstract class BindQueryParamsAbstract implements AfterViewInit, OnDestroy {

@Input() waitForParent: boolean = false;

@Input() set parentInitialized(value: boolean) {
this.parentComponentInitialized$.next(value);
}

private parentComponentInitialized$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);

private destroyed$ = new Subject<void>();

constructor(
protected parameterService: ParameterService,
protected source$?: Observable<any>,
) {}

ngAfterViewInit(): void {
// subscribe here to make sure that async pipes have been processed already
const params = this.parameterService.getQueryParams();
const initialValue = this.parseInitialValue(params);

this.parentComponentInitialized$.pipe(
filter(initialized => this.waitForParent ? initialized : true),
take(1),
tap(() => this.setInitialValue(initialValue)),
takeUntil(this.destroyed$),
).subscribe();

this.source$?.pipe(
map((values) => this.parseValuesToParams(values)),
tap((values) => this.parameterService.setQueryParams(values)),
takeUntil(this.destroyed$),
).subscribe();
}

/**
* Parses query parameters and set them on the bound component
* i.e.:
* - in case of a FormGroup we would need to call formGroup.patchValue
* - in case of a MatPaginator we want to call this.paginator.page.next
*
* @param params query parameters as they're seen when loading the page
* @protected
*/
protected abstract setInitialValue(params: unknown): void;

/**
* Parses query parameters so that they can be used to be nexted on `source$`
*
* @param params
* @protected
* @returns the parsed query params
*/
protected parseInitialValue(params: Params): Params {
return params;
}

parseValuesToParams(values: Params): Params {
return values;
}

ngOnDestroy() {
this.destroyed$.next();
this.destroyed$.complete();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NgModule } from '@angular/core';
import { BindQueryParamsMatTabDirective } from './to-mat-tab/bind-query-params-mat-tab.directive';
import { BindQueryParamsMatSortDirective } from './to-mat-sort/bind-query-params-mat-sort.directive';
import { BindQueryParamsMatPaginatorDirective } from './to-mat-paginator/bind-query-params-mat-paginator.directive';
import { MatSortModule } from '@angular/material/sort';

const exportables = [
BindQueryParamsMatTabDirective,
BindQueryParamsMatSortDirective,
BindQueryParamsMatPaginatorDirective,
];

@NgModule({
imports: [
MatSortModule,
],
exports: [
exportables
],
declarations: [
exportables
],
providers: [],
})
export class BindQueryParamsModule {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { Component, ViewChild } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { MatPaginator, MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Mocked, MockService } from '../../../test-utils/mock-service';
import { ParameterService } from '../../../services/paramter/parameter.service';
import { StorageService } from '../../../services/storage/storage.service';
import { BindQueryParamsMatPaginatorDirective } from './bind-query-params-mat-paginator.directive';

describe('bind-query-params-mat-paginator', () => {

@Component({
selector: 'test-component',
template: `
<mat-paginator #paginator
[length]="items.length"
[pageIndex]="pageIndex"
[pageSize]="pageSize"
[pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)"
bindQueryParamsMatPaginator
queryParamsNamespace="namespace"
[sizeStorageKey]="storageKey">

</mat-paginator>
`,
})
class TestComponent {
@ViewChild('paginator')
paginator!: MatPaginator;

items = new Array(100);
pageIndex = 0;
pageSize = 10;
pageSizeOptions = [ 1, 10, 20 ];
storageKey = 'sizeKey';

onPageChange = jest.fn<any, [PageEvent]>();
}

let fixture: ComponentFixture<TestComponent>;
let comp: TestComponent;
let parameterService: Mocked<ParameterService>;
let storageService: Mocked<StorageService>;

beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
MatPaginatorModule,
NoopAnimationsModule,
],
declarations: [
TestComponent,
BindQueryParamsMatPaginatorDirective,
],
providers: [
{provide: ParameterService, useClass: MockService(ParameterService)()},
{provide: StorageService, useClass: MockService(StorageService)()},
],
}).compileComponents();

parameterService = TestBed.inject(ParameterService) as Mocked<ParameterService>;
parameterService.getQueryParams.mockReturnValue({namespace_pageIndex: '2', namespace_pageSize: '20'});

storageService = TestBed.inject(StorageService) as Mocked<StorageService>;
storageService.getStorage.mockReturnValue(100);

fixture = TestBed.createComponent(TestComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(comp).toBeTruthy();
});

it('should set initial values from queryParams', fakeAsync(() => {
tick();
expect(comp.paginator.pageIndex).toEqual(2);
expect(comp.paginator.pageSize).toEqual(20);
expect(comp.onPageChange).toHaveBeenCalledWith({pageIndex: 2, pageSize: 20});
}));

it('should set initial page size from local storage, if no queryParams', fakeAsync(() => {
parameterService.getQueryParams.mockReturnValue({});
fixture = TestBed.createComponent(TestComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
tick();

expect(comp.paginator.pageIndex).toEqual(0);
expect(comp.paginator.pageSize).toEqual(10);
}));

it('should set query params when page changes', () => {
comp.paginator.nextPage();
expect(parameterService.setQueryParams).toHaveBeenCalledWith({namespace_pageIndex: 3, namespace_pageSize: 20});
});

it('should set query params when page changes size', () => {
comp.paginator._changePageSize(10);
expect(parameterService.setQueryParams).toHaveBeenCalledWith({namespace_pageIndex: 4, namespace_pageSize: 10});
});

it('should set local storage when page changes size', () => {
comp.paginator._changePageSize(10);
expect(storageService.setStorage).toHaveBeenCalledWith(comp.storageKey, 10);
});

it('should set query params when page changes programmatically', () => {
comp.pageIndex = 5;
comp.pageSize = 50;
fixture.detectChanges();

expect(parameterService.setQueryParams).toHaveBeenCalledWith({namespace_pageIndex: 5, namespace_pageSize: 50});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Directive, Input, OnChanges, Optional, SimpleChanges } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { Params } from '@angular/router';
import { BindQueryParamsAbstract } from '../bind-query-params.abstract';
import { ParameterService } from '../../../services/paramter/parameter.service';
import { StorageService } from '../../../services/storage/storage.service';

@Directive({selector: '[bindQueryParamsMatPaginator]'})
export class BindQueryParamsMatPaginatorDirective extends BindQueryParamsAbstract implements OnChanges {

private separator = '_';

@Input() pageIndex!: number;
@Input() pageSize!: number;
@Input() queryParamsNamespace!: string;

// eslint-disable-next-line @angular-eslint/no-input-rename
@Input('sizeStorageKey') storageKey!: string;

constructor(
protected override parameterService: ParameterService,
protected paginator: MatPaginator,
@Optional() protected storage: StorageService,
) {
super(parameterService, paginator.page);
}

ngOnChanges({pageIndex, pageSize, }: SimpleChanges): void {
if (!(pageIndex?.firstChange || pageSize.firstChange)) {

if (pageIndex.currentValue || pageSize.currentValue) {
this.paginator.page.next({pageIndex: this.pageIndex, pageSize: this.pageSize} as PageEvent);
}

}

}

protected override parseInitialValue(params: Params): { pageSize: number; pageIndex: number|undefined } {
const pageSizeKey = [ this.queryParamsNamespace, 'pageSize' ].join(this.separator);
const pageIndexKey = [ this.queryParamsNamespace, 'pageIndex' ].join(this.separator);
const pageSize = (params[pageSizeKey] ? Number(params[pageSizeKey]) : (this.storageKey ? this.storage.getStorage(this.storageKey) : undefined)) as number;
const pageIndex = params[pageIndexKey] ? Number(params[pageIndexKey]) : undefined;
return {pageSize, pageIndex};
}

protected setInitialValue({pageIndex = this.pageIndex, pageSize = this.pageSize}: PageEvent): void {
void Promise.resolve().then(() => {

this.pageIndex = pageIndex;
this.pageSize = pageSize;
this.paginator.pageIndex = pageIndex;
this.paginator.pageSize = pageSize;
this.paginator.page.next({pageIndex, pageSize} as PageEvent);
});
}

override parseValuesToParams({pageIndex, pageSize}: PageEvent): Pick<PageEvent, 'pageIndex' & 'pageSize'> {
if (this.storageKey) {
this.storage.setStorage(this.storageKey, pageSize);
}

return {
[[ this.queryParamsNamespace, 'pageIndex' ].join(this.separator)]: pageIndex || null, // Remove if 0
[[ this.queryParamsNamespace, 'pageSize' ].join(this.separator)]: pageSize,
};
}
}
Loading
Loading