All files / common/services si-uistate.service.ts

75.86% Statements 22/29
76.92% Branches 10/13
54.54% Functions 6/11
76.92% Lines 20/26

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140              1x                                                                                                 1x             1x 18x                   14x                                           13x 13x 1x   12x   12x 12x 12x             12x               13x 11x 11x 11x       2x             1x 16x          
/**
 * Copyright (c) Siemens 2016 - 2025
 * SPDX-License-Identifier: MIT
 */
import { isPlatformBrowser } from '@angular/common';
import { inject, Injectable, InjectionToken, PLATFORM_ID, Provider, Type } from '@angular/core';
 
const SI_UI_STATE_STORAGE = new InjectionToken<UIStateStorage>('si.ui-state.storage', {
  providedIn: 'root',
  factory: () => {
    Iif (isPlatformBrowser(inject(PLATFORM_ID))) {
      return new LocalStorageUIStateStorage();
    }
    return { save: () => {}, load: () => null };
  }
});
 
/**
 * Interface that defines a UIStateStore.
 * It must be provided via {@link provideSiUiState}.
 */
export interface UIStateStorage {
  /**
   * Saves data under a specific stateId.
   * Already existing data for that stateId should be overridden.
   * Asynchronous implementations must return a {@link Promise} and resolve it once saving was completed.
   *
   * Errors should be handled by the implementation.
   * The `SiUIStateService` does not handle errors.
   */
  save(stateId: string, data: string): void | Promise<void>;
 
  /**
   * Loads data for a specific stateId.
   * Can be asynchronous.
   *
   * Errors should be handled by the implementation.
   * The `SiUIStateService` does not handle errors.
   *
   * @returns The data or undefined if the data does not exist.
   * Asynchronous implementations must return a {@link Promise} containing the data.
   */
  load(stateId: string): string | undefined | null | Promise<string | undefined | null>;
}
 
class LocalStorageUIStateStorage implements UIStateStorage {
  save(stateId: string, data: string): void {
    localStorage.setItem(`si.ui-state.${stateId}`, data);
  }
 
  load(stateId: string): string | null {
    return localStorage.getItem(`si.ui-state.${stateId}`);
  }
}
 
/** @internal */
export const SI_UI_STATE_SERVICE = new InjectionToken<SiUIStateService>('si.ui-state.service');
 
/**
 * Service to save and load UI states.
 * @internal
 */
@Injectable()
class SiUIStateService {
  private storage = inject(SI_UI_STATE_STORAGE);
 
  /**
   * Saves the provided state in the storage.
   * @param stateId - The unique id or key under which the state shall be saved.
   * @param state - The state to be saved.
   * @param version - The version of the state object.
   * This can be used to migrate state objects.
   */
  save<TState>(stateId: string, state: TState, version = 0): Promise<void> {
    return (
      this.storage.save(
        stateId,
        JSON.stringify({
          version,
          state
        })
      ) ?? Promise.resolve()
    );
  }
 
  /**
   * Loads and returns the state for the given stateId and version.
   * @param stateId - The unique id or key for which the state shall be loaded.
   * @param version - The version of the state object.
   * This can be used to migrate state objects.
   * @returns A Promise containing the state or undefined if the state does not exist or the version did not match.
   */
  load<TState = unknown>(stateId: string, version = 0): PromiseLike<TState | undefined> {
    // DO NOT ADD async keyword.
    // This method should stay synchronous if the storage is synchronous.
    // Otherwise, the navbar will play expand animations on load.
    const dataOrPromise = this.storage.load(stateId);
    if (dataOrPromise instanceof Promise) {
      return dataOrPromise.then(data => this.readData(data, version));
    } else {
      const promiseLike = {
        then: onfulfilled => {
          const data = this.readData<TState>(dataOrPromise, version);
          if (onfulfilled) {
            return onfulfilled(data);
          } else E{
            return promiseLike;
          }
        }
      } as PromiseLike<TState | undefined>;
 
      return promiseLike;
    }
  }
 
  private readData<TState = unknown>(
    data: string | null | undefined,
    version: number
  ): TState | undefined {
    if (data) {
      const parsed = JSON.parse(data);
      if (parsed.version === version) {
        return parsed.state;
      }
    }
 
    return undefined;
  }
}
 
export type { SiUIStateService };
 
/** Enables the automatic storage of UI state for enabled components. */
export const provideSiUiState = (config?: { store?: Type<UIStateStorage> }): Provider[] => {
  return [
    { provide: SI_UI_STATE_SERVICE, useClass: SiUIStateService },
    config?.store ? { provide: SI_UI_STATE_STORAGE, useClass: config.store } : []
  ];
};