Hone logo
Hone
Problems

Angular Query Parameter Management

This challenge focuses on implementing robust query parameter handling within an Angular application. You will create a reusable service to manage, retrieve, and update query parameters in the URL, enabling dynamic data filtering and state management for your components.

Problem Description

Your task is to build an Angular service that simplifies the process of interacting with URL query parameters. This service should allow components to:

  • Get all current query parameters: Retrieve all key-value pairs currently present in the URL's query string.
  • Get a specific query parameter value: Retrieve the value associated with a given parameter key.
  • Update or add a query parameter: Set a new parameter or update the value of an existing one.
  • Remove a query parameter: Delete a specific parameter from the query string.
  • Apply changes to the URL: Synchronize the updated query parameters with the browser's URL without causing a full page reload.

The service should be designed for reusability and ease of use within various Angular components.

Key Requirements

  1. Create an Angular Service: Develop a TypeScript class that acts as a service for query parameter management.
  2. Inject ActivatedRoute and Router: The service will need to inject Angular's ActivatedRoute to access current route information and Router to navigate and update the URL.
  3. getQueryParams() Method: This method should return an observable that emits the current query parameters as an object (e.g., { key1: 'value1', key2: 'value2' }).
  4. getParam(key: string) Method: This method should return an observable that emits the value of a specific query parameter identified by key. If the parameter doesn't exist, it should emit null or undefined.
  5. updateQueryParams(params: { [key: string]: string | string[] | null }) Method: This method should accept an object where keys are query parameter names and values are their new values.
    • string: Sets or updates a single value for a parameter.
    • string[]: Handles multiple values for a single parameter (e.g., ?tags=angular&tags=typescript).
    • null: Removes the parameter if it exists.
    • The method should merge the provided changes with existing query parameters, not overwrite them entirely, unless a parameter is explicitly set to null.
  6. applyChanges() Method (Implicitly via updateQueryParams): The updateQueryParams method should internally use Router.navigate with queryParams and queryParamsHandling: 'merge' to update the URL without page reloads.

Expected Behavior

  • When a component subscribes to getQueryParams() or getParam(), it should receive the current query parameters from the URL.
  • When updateQueryParams() is called, the URL should be updated to reflect the changes, and any active subscriptions to getQueryParams() or getParam() should be notified of the update.
  • If a parameter is set to null in updateQueryParams(), it should be removed from the URL.
  • If multiple values are provided for a single parameter key (e.g., tags: ['angular', 'typescript']), the URL should reflect this (e.g., ?tags=angular&tags=typescript).

Edge Cases to Consider

  • No query parameters: The service should gracefully handle URLs without any query parameters.
  • Empty query string: Similar to no query parameters.
  • URL encoding/decoding: Angular's Router typically handles this, but be aware of potential nuances.
  • Type safety: While query parameters are inherently strings, consider how to handle different expected data types if necessary (though for this challenge, string representation is sufficient).

Examples

Example 1: Initial State and Retrieving Parameters

Scenario: A user navigates to /products?category=electronics&sort=price&limit=10.

// In a component:
this.queryParamService.getQueryParams().subscribe(params => {
  console.log(params);
  // Expected Output: { category: 'electronics', sort: 'price', limit: '10' }
});

this.queryParamService.getParam('category').subscribe(category => {
  console.log(category);
  // Expected Output: 'electronics'
});

this.queryParamService.getParam('nonexistent').subscribe(value => {
  console.log(value);
  // Expected Output: null (or undefined, depending on implementation)
});

Explanation: The service correctly reads the existing query parameters from the URL and provides them via observables.

Example 2: Updating and Removing Parameters

Scenario: Starting from /products?category=electronics&sort=price&limit=10.

// In a component:

// 1. Change limit and add a new parameter 'page'
this.queryParamService.updateQueryParams({ limit: '20', page: '2' });
// URL becomes: /products?category=electronics&sort=price&limit=20&page=2

// 2. Remove 'sort' parameter
this.queryParamService.updateQueryParams({ sort: null });
// URL becomes: /products?category=electronics&limit=20&page=2

// 3. Get updated parameters
this.queryParamService.getQueryParams().subscribe(params => {
  console.log(params);
  // Expected Output: { category: 'electronics', limit: '20', page: '2' }
});

Explanation: The updateQueryParams method successfully modifies the URL by changing an existing parameter, adding a new one, and removing another. The getQueryParams observable reflects these changes.

Example 3: Handling Multiple Values for a Parameter

Scenario: User navigates to /articles?tags=angular&tags=typescript&status=published.

// In a component:
this.queryParamService.getQueryParams().subscribe(params => {
  console.log(params);
  // Expected Output: { tags: ['angular', 'typescript'], status: 'published' }
  // Note: Depending on how you parse, 'tags' might be an array if multiple exist.
  // For this challenge, let's assume the service can handle this and potentially return
  // an array if multiple values are detected for a single key. If the expectation is to
  // always return strings and handle the array logic in the component, adjust accordingly.
  // Let's refine the expectation for simplicity: the service returns strings.
  // The Router itself might represent multiple values as string[] in its internal state.
  // For this challenge, let's focus on the service providing a structured object.
});

// Let's refine the requirement for multiple values slightly:
// The `getQueryParams` method will return an object where values are strings.
// If a parameter appears multiple times, the service will return the *last* encountered value for that parameter.
// For Example 3, if we consider the standard behavior of `queryParams` from `ActivatedRoute`,
// it will flatten multiple values into a single string if they are identical or the last one if different.
// To keep it manageable, let's adjust the service's `getQueryParams` to return the last value if multiple exist.
// So, for '?tags=angular&tags=typescript', `getParam('tags')` would return 'typescript'.

// Let's assume a scenario where we *want* to store multiple values, and the component will handle it.
// A more advanced version might return { tags: ['angular', 'typescript'] }.
// For THIS challenge, let's aim for the simpler: `getQueryParams` returns { key: string }.
// If a key is repeated, the service returns the *last* value.
// If we *update* with an array, the service should create multiple entries in the URL.

// Revised Example 3 using updateQueryParams with an array:
// Scenario: Starting from `/items`

// Add 'ids' with multiple values
this.queryParamService.updateQueryParams({ ids: ['101', '102', '103'] });
// URL becomes: /items?ids=101&ids=102&ids=103

this.queryParamService.getQueryParams().subscribe(params => {
  console.log(params);
  // Expected Output: { ids: '103' } (or whichever is the last one based on URL parsing)
  // The challenge implies simplicity in the GET, so last value is fine.
});

Explanation: The updateQueryParams method can handle an array of strings, translating them into multiple query parameter entries in the URL. The getQueryParams method will then return the last encountered value for that parameter.

Constraints

  • The Angular application version should be 10 or higher.
  • The service should be implemented in TypeScript.
  • The solution must leverage Angular's dependency injection mechanism.
  • Avoid using external libraries for query parameter management; use Angular's built-in Router and ActivatedRoute.
  • The service should be designed to be a singleton.

Notes

  • Consider how you will parse the query parameters from ActivatedRoute.snapshot.queryParams and observe changes via ActivatedRoute.queryParams.
  • The Router.navigate method with queryParamsHandling: 'merge' is crucial for updating the URL without full page reloads and for merging new parameters with existing ones.
  • Think about how to effectively transform the array of strings provided to updateQueryParams into the correct format for the Router.
  • For the getParam method, ensure it handles cases where the parameter might not exist. Returning null or undefined is acceptable.
  • The updateQueryParams method should handle both adding new parameters and updating existing ones. Setting a parameter to null should remove it.
Loading editor...
typescript