Skip to content

Instantly share code, notes, and snippets.

@JaimeStill
Last active April 19, 2024 15:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JaimeStill/4d9946849802fd875e1b127ca46ac30f to your computer and use it in GitHub Desktop.
Save JaimeStill/4d9946849802fd875e1b127ca46ac30f to your computer and use it in GitHub Desktop.
Signal-based Query Processor
<h1 class="mat-title m8">Home</h1>
<button mat-button class="m8" (click)="search()">Search</button>
import {
Component,
OnInit
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { QuerySource } from '../core/query';
import { HttpClient } from '@angular/common/http';
import { SnackerService } from '../core';
import { environment } from '../../environments/environment';
@Component({
selector: 'home-route',
standalone: true,
templateUrl: 'home.route.html',
styleUrl: 'home.route.scss',
imports: [
MatButtonModule
],
providers: []
})
export class HomeRoute implements OnInit {
it: number = 0
source: QuerySource<string>;
constructor(
http: HttpClient,
snacker: SnackerService
) {
this.source = new QuerySource<any>(http, snacker, environment.api, 'agency/query');
}
ngOnInit() {
this.source.result$.subscribe(result => console.log(++this.it, result));
}
search() {
this.source.onSearch('thing');
console.log(this.source.url$());
}
}
import { SortDirection } from '@angular/material/sort';
export interface QueryOptions {
page: number;
pageSize: number;
search: string | null;
sortProperty: string | null;
sortOrder: SortDirection;
}
export interface QueryResult<T> {
page: number;
pageSize: number;
totalCount: number;
data: T[];
}
import {
effect,
signal,
WritableSignal
} from '@angular/core';
import {
catchError,
Observable,
ReplaySubject,
throwError
} from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { PageEvent } from '@angular/material/paginator';
import { Sort } from '@angular/material/sort';
import { SnackerService } from '../services';
import { QueryUrl } from './query-url';
import { QueryOptions } from './query-options';
import { QueryResult } from './query-result';
export class QuerySource<T> {
private repage: boolean = false;
private defaults: QueryOptions = {
page: 1,
pageSize: 20,
search: null,
sortProperty: null,
sortOrder: 'asc'
}
private url: WritableSignal<QueryUrl>;
private result: ReplaySubject<QueryResult<T>> = new ReplaySubject<QueryResult<T>>(1);
result$: Observable<QueryResult<T>> = this.result.asObservable();
url$ = (): QueryUrl => this.url();
constructor(
private http: HttpClient,
private snacker: SnackerService,
base: string,
endpoint: string,
options: Partial<QueryOptions> = {}
) {
this.url = signal(new QueryUrl(
base,
endpoint,
<QueryOptions>{
...this.defaults,
...options
}
));
effect(() => {
const url = this.url().build(this.repage);
this.repage = false;
this.http.get<QueryResult<T>>(url.toString())
.pipe(
catchError(er => throwError(() => new Error(er)))
)
.subscribe({
next: result => this.result.next(result),
error: err => this.snacker.sendErrorMessage(err)
});
});
}
onPage(event: PageEvent) {
this.url.update((value: QueryUrl) => {
this.repage = event.pageSize !== value.options.pageSize;
value.options.page = event.pageIndex;
value.options.pageSize = event.pageSize;
return new QueryUrl(
value.base,
value.endpoint,
value.options
);
});
}
onSearch(event: string | null) {
this.url.update((value: QueryUrl) => {
this.repage = event !== value.options.search;
value.options.search = event;
return new QueryUrl(
value.base,
value.endpoint,
value.options
);
});
}
onSort(event: Sort | null) {
this.url.update((value: QueryUrl) => {
if (event) {
this.repage = value.options.sortProperty !== event.active;
value.options.sortProperty = event.active;
value.options.sortOrder = event.direction
} else {
this.repage = value.options.sortProperty !== null;
value.options.sortProperty = null;
value.options.sortOrder = 'asc';
}
return new QueryUrl(
value.base,
value.endpoint,
value.options
);
});
}
}
import { QueryOptions } from './query-options';
export class QueryUrl {
constructor(
public base: string,
public endpoint: string,
public options: QueryOptions
) { }
build(repage: boolean = false): URL {
const url = new URL(this.endpoint, this.base);
url.searchParams.set('page', repage ? '1' : this.options.page.toString());
url.searchParams.set('pageSize', `${this.options.sortProperty}_${this.options.sortOrder}`);
if (this.options.sortProperty)
url.searchParams.set('sort', `${this.options.sortProperty}_${this.options.sortOrder}`);
if (this.options.search)
url.searchParams.set('search', this.options.search!);
return url;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment