ngxtension/ngxtension-platform

RFC: Add utility for Router/State Binding

alcaidio opened this issue · 2 comments

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!

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

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 ;)