ngxtension / ngxtension-platform

Utilities for Angular
https://ngxtension.netlify.app/
MIT License
576 stars 83 forks source link

RFC: Add utility for Router/State Binding #330

Open alcaidio opened 4 months ago

alcaidio commented 4 months ago

I have a feature proposal that I'd like to discuss with you all.

The idea is to create a utility to bind our application state with our router, particularly focusing on query parameters. This would enable synchronization, for instance, of filters with the route of an e-commerce site.

Consider the following interfaces:

interface Product {
    id: number;
    name: string;
    brand: string;
    category: string;
    price: number;
}

interface FilterOptions {
    brand: string[];
    category: string[];
    price: {
        min: number;
        max: number;
    };
}

The route could then take the form:

https://example.com/products?brand=nike,adidas&category=shoes&price-min=30&price-max=90

What we aim for is to bind to our route declaratively without dealing with the activatedRoute.params observable directly. For example:

@Injectable({ providedIn: 'root' })
export class ProductService {
    products = signal<Product[]>([]);
    filters = signal<FilterOptions>({});

    bindWithRouter(this.filters); // Naming and configuration to be considered...
}

Here's a proposed implementation for bindWithRouter, which would facilitate developing the feature (to be made generic with strict typing, of course):

    router = inject(Router);
    route = inject(ActivatedRoute);

    constructor() {
        const updateStateFromRoute$ = this.route.queryParams.pipe(
            tap((queryParams) => {
                const reducer = (acc: Filters, curr: [string, string]) => {
                    const [key, values] = curr;
                    return { ...acc, [key]: values.split(',') };
                };
                const filters = Object.entries(queryParams).reduce(
                    reducer,
                    {} as Filters
                );
                this.filters.set(filters);
            })
        );

        const updateRouteFromState$ = toObservable(this.filters).pipe(
            skip(1), // To skip initial state of filters and use state from queryparams if exist
            tap((filters) => {
                const reducer = (acc: Params, curr: [string, string[]]) => {
                    const [key, values] = curr;
                    return { ...acc, [key]: values.join(',') };
                };
                const queryParams = Object.entries(filters).reduce(reducer, {});
                this.router.navigate([], {
                    relativeTo: this.route,
                    queryParams,
                    queryParamsHandling: 'merge',
                });
            })
        );

        merge(updateStateFromRoute$, updateRouteFromState$)
            .pipe(takeUntilDestroyed())
            .subscribe();
    }

This discussion aims to explore whether there is a real need for this feature and to collaboratively define its scope and specifications. After reaching consensus during this initial phase (if there is a need), I'm eager to take the next step and bring this feature to life, realizing my aspiration to contribute meaningfully to the Angular ecosystem. 🤩

Your thoughts and suggestions on this proposal would be highly appreciated!

eneajaho commented 4 months ago

Hi, I tried to do something like this, automatic synchronization of queryParams, but the issues is: There are cases where we want to keep some queryParams, and cases when we want to merge, or remove or do a lot of other things, and that's why navigate method of the router includes a lot of other keys in there that we would need to support or duplicate.

That's why there's nothing in ngxtension that simplifies this.

CleanShot 2024-04-21 at 12 26 26@2x

alcaidio commented 4 months ago

It's a pity, because I think that for simple and common cases as described above it would have been practical, but I note that it's not generic enough to be included in this repo ;)