Displaying cacheable content with a partial component in Angular
Tags: angular, webdev, tutorial, ui
Published: 2023-09-12 10:00:00

After creating a [localstroage wrapper](https://garage.sekrab.com/posts/localstorage-wrapper-service-in-angular), and a [data cache decorator](https://garage.sekrab.com/posts/a-cache-decorator-in-angular), let's put them together to create a data service that handles all cacheable data. Also, let's create a a component that displays the cacheable content based on a specific target.

Here is a quick summary of what we previously accomplished

#### LocalStorage wrapper service in Angular

We created a `window.localStorage` wrapper in Angular to control the data modeling of what gets saved in `localStorage`, the data model we came to was the following

```ts
interface IStorage {
  value: any;
  expiresin: number;
  timestamp: number;
}
```

Then we created a data service specifically to save recurring data with specific data model, and cache it instead of querying the server every time.

```ts
export interface IData {
  value: string;
  id: string;
  key: string;
}
```

So we ended up with a line like this

`this.countries$ = this.dataService.GetCountries();`

That created entries like this

`garage.en.countries.0`

#### A cache decorator in Angular

Then we created a decorator to save any `http` call with any data model in the `localStorage`. So we had lines like this

```ts
@DataCache()
GetCountries(options) {
	return http.get(...);
}
```

Today, we will use the decorator for the data service, and go beyond that to create a partial component that understands it should display the value, of a specific data key.

### An example of Foods

Say the cacheable content is food group, the groups are known: "vegetables, milk, fruits, grain". The value to be displayed (could be multilingual comes from a unique point in an API, let's assume it is like this:

```json
{
	"id": "32424234",
	"key": "fruits",
	"value": "Fruits"
}
```

Let's not bring in all languages, only one language really matters for the user. So the API should handle this properly by reading `Accept-Language` header.

Then let's assume an API call (e.g. `/api/foods`) returns a list of items, one of properties is `group`

```json
[
	{
		"id": "344234",
		"name": "Breakfast",
		"group": "fruits"
		// ...
	},
	// ...
]
```

I want to be able to display the `value` by fetching the correct `key` like this

```html
<ul>
	<li *ngFor="let food in foods">
		{{ food.name }} <span>{{ getValue(food.group) }} 
      <span>
  <li>
</ul>
```

The target, is to figure out what this `getValue` really is, let's begin with the service itself.

### The service

I will continue where we left off in our previous data caching hunt in [StackBlitz](https://stackblitz.com/edit/angular-cachable-content?file=src%2Fapp%2Fservices%2Fdata.service.ts). We will create a new API call for food groups. To get groups, we need to implement this function

`this.groups$ = this.dataService.GetGroups();`

So in our data service, we will create a new `GetGroups` method, and we will also adapt the service to use the decorator instead. (Notice how we use the argument `withArgs` to pass an `id` to the entry key.)

```ts
// new data service (services/data.service)
// now that we are using data decorator, we can create a new function for every data group
export class DataService {
constructor(
  private _http: HttpService,
  // make this public to use with decorator
  public storageService: StorageService
) {}

  @DataCache({ key: 'Countries', expiresin: 8000 })
  GetCountries(): Observable<IData[]> {
    return this._http.get(this._countriesUrl).pipe(
      map((response: any) => {
        return DataClass.NewInstances(<any>response);
      })
    );
  }

  // if we need an id, we just pass withArgs
  @DataCache({ key: 'Cities', withArgs: true })
  GetCities(id: string): Observable<IData[]> {
    return this._http.get(this._citiesUrl.replace(':id', id)).pipe(
      map((response: any) => {
        return DataClass.NewInstances(<any>response);
      })
    );
  }

  // our new groups here
  @DataCache({ key: 'Groups' })
  GetGroups(): Observable<IData[]> {
    return this._http.get(this._groupsUrl).pipe(
      map((response: any) => {
        return DataClass.NewInstances(<any>response);
      })
    );
  }
}
```

So far we recreated what we already have in data service. The use of which did not change a bit in our component. Now we are going to focus on groups.

```ts
// This still acts the same way, gets countries and caches them
this.groups$ = this.dataService.GetGroups();
```

### Get single food by key

My goal is to end up with a partial in which I pass the `key`, and the `type` (identifier), to get the `value` from the cache. **If cache is not initiated yet, it should initiate it and save it.**

`<cr-data type="Groups" key="milk"></cr-data>`

We expect the API to return the `key` of the group, instead of the whole package of `key`, `id`, and `value`.

### Type versus Enum

We previously used `Enums` and it was fine, except that in our case if we are to use an `Enum`, we must get reference to it in our template code, like this:

```ts
// in data model
export enum EnumDataType {
  Groups,
	 // ...
}

// in component template code
enumDataType = EnumDataType;

// then in template
`
<cr-data [type]="enumDataType.Groups" ...>
`
```

What I found out is that `Types` in `typescript` are easier to work with. They validate, and they don't need to be brought in to component.

```ts
// in data model
export type TypeDataType = 'Country' | 'Category' | 'Rate' | 'City' | 'Groups';

// then in template
`
<cr-data type='Groups' ...>
`
```

This is much cleaner. Why `Groups` and not `Group`? Coming right up.

### Data partial component

Let's create the code for the **partial component**, we need two **inputs** at least (we can combine them into an object but it doesn't really itch that much). We also need to inject the `dataService`. The template is simply an `async` `observable` that displays the `value` when the data is available.

```ts
// data.partial
@Component({
    selector: 'cr-data',
    template: `{{ (data$ | async)?.value }}`,
    //...
})
export class DataPartialComponent {
    // an observable
    data$: Observable<IData | undefined>;

		// we need key, and type
    @Input() key: string;
    @Input() type: TypeDataType;

		// inject the service
    constructor(private _dataService: DataService) {}
}
```

Now whenever the `key` is set, we need to get data by `key` and `type`. We can do that `AfterViewInit` or `OnInit`, but a `setter` is more ready for change detection.

> Note, a setter alone won't guarantee that the property will change, to implement proper change detection mechanism, other strategies are needed, like changing the value `changeDetection` to `Default` instead of `OnPush`, or injecting `ChangeDetectorRef` and explicitly calling `detectChanges`(). But this is another Tuesday.

```ts
@Input() set key(value: string) {
  this.data$ = this._dataService.GetSingleDataByKey(this.type, value);
}
```

Right. Let's implement the `GetSingleDataBykey`.

### GetSingleDataByKey

So back to our data service, let's create a generic get item by key. We have the `type`, and we are going to use it to **construct a method name** and call it. In `JavaScript`, calling a method in a class is as simple as this:

`className[sometVar]();`

Let's also check if the function exists before we call it. Because we are using types, we can add extra checks to align the type `Country` with `GetCountries`, but today, I am happy with using `Countries` as the type all the way. So that is why I chose `Groups` instead of `Group`.

```ts
GetSingleDataByKey(type: TypeDataType, key: string): Observable<IData | undefined> {
  // WATCH: observable of null
  if (key === null) {
    return of(undefined);
  }
  // use the type to find GetSomething() then call it.
  const getFunction = this['Get'+type];
  if (!getFunction) {
    return of(undefined);
  }
  return getFunction.call(this).pipe(
    // find element with the same key
    map((data: IData[]) => data.find(n => n.key === key)));
}

// changed types to be aligned with method names in services/data.model
export type TypeDataType = 'Countries' | 'Categories' | 'Rates' | 'Cities' | 'Groups';
```

Putting this to test:

```html
Groups names:
<cr-data type="Groups" key="fruits"></cr-data>.
<cr-data type="Groups" key="milk"></cr-data>.
<cr-data type="Groups" key="vegetables"></cr-data>.

// Displays:
Groups names: Fruits. Milk. Vegetables.
```

> PS. keep cleaning the `localStorage` before testing, this one always drives me crazy.

### Ranting: pipes

To have a pipe we must mimic the function of an `async` pipe and build on it. Since we need to subscribe, I'd rather save the data locally in the pipe until next time. Here is the thing, in [Angular documentation](https://angular.io/guide/pipes#caching-http-requests) you can read those two lines:

-   Each binding gets its own pipe instance.
-   Each pipe instance caches its own URL and data and calls the server only once.

**This sounds better than it really means.** It does in no way mean the data is cached in client throughout the application session, nor does it mean that multiple pipes on the same page get to use the cached version, and no, it also does not mean that the same props will trigger a reuse of the cached data. It just means that **on the same route, for the same pipe usage**, when change detection take place,** if the properties did not change, the cached data will be returned**.

PS: pure pipes by definition do that out of the box, but this is an `impure` pipe that must be told to do so.

Verdict: Pipes are smelly. Partial components are good for the job.

### Decorator enhancement

Going back to our decorator, there is a slight enhancement we can introduce. When there is a list of items that will use the cached data concurrently, the API call will be made multiple times.

How to prevent concurrent calls when the data in storage is not set yet? Well, we can be sophisticated, and we can be really simple. The simple solution is to add a **lock**, and `timeout` the call if the lock is true. The second call, is between 50% to 100% cached. 50% if you have a slow API, 100% if you have an adequate `timeout`. We don't want to complicate things, so it should do.

```ts
// services/data.decorator
// enhancement to prevent concurrent calls

// add a basket of locks
const locks: { [key: string]: boolean } = {};

export function DataCache<T extends IStorageService>(options?: Partial<ICached>) {
  return function (target: T, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    const cacheKey = options?.key || `${target.constructor.name}.${propertyKey}`;

    descriptor.value = function (...args: any[]): Observable<any> {

      const key = options?.withArgs ? `${cacheKey}_${JSON.stringify(args)}` : cacheKey;

      // wrap it in a fireOff function
      const fireOff = () => {
        const _data: any = this.storageService.getItem(key);
        if (_data) {
          return of(_data).pipe(debug('Cached ' + cacheKey));
        } else {
          // lock first by adding a new entry
          locks[cacheKey] = true;
          return originalMethod.apply(this, args).pipe(
            tap(response => {
              this.storageService.setItem(key, response, options?.expiresin);
            }),
            finalize(() => {
              // unlock by removing
              delete locks[cacheKey];
            })
          );
        }
      };

      // check if locked, wait some miliseconds and fireOff anyway (no second check)
      // the timeout can be configurable and depends on project needs
      return locks[cacheKey] ? timer(500).pipe(switchMap(fireOff)) : fireOff();
    };
    return descriptor;
  };
}
```

We can keep the `timeout` value in a **configuration** file, or better yet pass it as an option. I find this to be quite project-specific, and an abstraction I do not wish to add to the basic seed. So we'll leave it at that.

Now watching the network tab in a page that has a list of items that call the data service, the first call is an **API call**, all the rest are** local fetching**. (Cannot showcase that in StackBlitz because there is no real `http` call, but a simple console log in the mock data file, will show that the groups are fetched once). This might produce a nasty effect of **slower label display**, it feels like a nasty cloud provider with baggage and things to do 😎. No seriously, the effect is acceptable and does not hinder experience. To get rid of it however, we just need to check the cache first and return in case anything exists.

```ts
// services/data.decorator

// ...
  // check data first
  const _data: any = this.storageService.getItem(key);
  if (_data) {
    // if localStroage exist, return
    return of(_data);
  }

  // then fireoff
  const fireOff = () => {
    // ...
  }

```

That's it, that's enough. Thanks for reading this far though.

### RESOURCES

-   [Angular documentation](https://angular.io/guide/pipes#caching-http-requests)
-   [StackBlitz project](https://stackblitz.com/edit/angular-cachable-content)

### RELATED POSTS

- [LocalStorage wrapper service in Angular](https://garage.sekrab.com/posts/localstorage-wrapper-service-in-angular)

- [A cache decorator in Angular](https://garage.sekrab.com/posts/a-cache-decorator-in-angular)