Skip to content

Commit

Permalink
Merge pull request #221 from amosproj/feat/xd-136
Browse files Browse the repository at this point in the history
feat: simple state management using localStorage
  • Loading branch information
IngoSternberg authored Jul 3, 2024
2 parents 669b64e + 922c338 commit 91ddbd4
Show file tree
Hide file tree
Showing 13 changed files with 225 additions and 33 deletions.
6 changes: 4 additions & 2 deletions apps/frontend/src/app/components/header/header.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
<ix-application-header name="Xcelerator Demo App">
<button class="placeholder-logo" slot="logo" [routerLink]="['/']">
<img
[src]=getCorrectImage()
[src]="lightMode() ?
'https://cdn.c2comms.cloud/images/logo-collection/2.1/sie-logo-black-rgb.svg':
'https://cdn.c2comms.cloud/images/logo-collection/2.1/sie-logo-white-rgb.svg'"
class="h-5"
alt="Siemens logo"
/>
Expand All @@ -26,7 +28,7 @@
<ix-menu-item icon="home" [routerLink]="['/']">Home</ix-menu-item>
<ix-menu-item icon="building1" [routerLink]="['/facilities']">Facilities</ix-menu-item>
<ix-menu-item icon="tasks-open" [routerLink]="['/cases']">Cases</ix-menu-item>
<ix-menu-item [icon]=getCorrectIcon() slot="bottom" (click)="toggleMode()">toggle theme</ix-menu-item>
<ix-menu-item [icon]="lightMode() ? 'sun-filled' : 'sun'" slot="bottom" (click)="toggleMode()">toggle theme</ix-menu-item>

<ix-menu-about>
<app-legal-information></app-legal-information>
Expand Down
22 changes: 5 additions & 17 deletions apps/frontend/src/app/components/header/header.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import {
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterOutlet } from '@angular/router';
import { themeSwitcher } from '@siemens/ix';
import { IxModule } from '@siemens/ix-angular';
import { filter } from 'rxjs';

import { ThemeStorageService } from '../../../models/services/theme-storage.service';
import { LegalInformationComponent } from './legal-information/legal-information.component';

/**
Expand All @@ -27,9 +27,11 @@ import { LegalInformationComponent } from './legal-information/legal-information
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HeaderComponent {

private readonly _activatedRoute: ActivatedRoute = inject(ActivatedRoute);
private readonly _router: Router = inject(Router);
private _lightMode = false;
private themeStorageService = inject(ThemeStorageService);
protected lightMode = this.themeStorageService.getLightMode();

readonly routerEvents = toSignal(
this._router.events.pipe(filter((e) => e instanceof NavigationEnd)),
Expand Down Expand Up @@ -69,21 +71,7 @@ export class HeaderComponent {
}

toggleMode() {
themeSwitcher.toggleMode();
this._lightMode = !this._lightMode;
}

getCorrectImage() {
if (this._lightMode) {
return "https://cdn.c2comms.cloud/images/logo-collection/2.1/sie-logo-black-rgb.svg";
}
return "https://cdn.c2comms.cloud/images/logo-collection/2.1/sie-logo-white-rgb.svg";
this.themeStorageService.toggleTheme();
}

getCorrectIcon() {
if (this._lightMode) {
return "sun-filled";
}
return "sun";
}
}
36 changes: 36 additions & 0 deletions apps/frontend/src/models/services/theme-storage.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { fakeAsync, TestBed, tick } from '@angular/core/testing';

import { ThemeStorageService } from './theme-storage.service';

describe('ThemeStorageService', () => {
let service: ThemeStorageService;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: []
});
service = TestBed.inject(ThemeStorageService);

jest.spyOn(Storage.prototype, 'setItem');
Storage.prototype.setItem = jest.fn();
});

it('should be created', () => {
expect(service).toBeTruthy();
});

it('should set lightmode to false initially and change it when toggled', fakeAsync(() => {
// we need to wait for the effect to be done
tick();
expect(service.getLightMode()()).toBeFalsy();
expect(localStorage.setItem).toHaveBeenCalledWith('lightMode', 'false');

service.toggleTheme();

tick();
expect(service.getLightMode()()).toBeTruthy();
expect(localStorage.setItem).toHaveBeenCalledWith('lightMode', 'true');
}));

});
40 changes: 40 additions & 0 deletions apps/frontend/src/models/services/theme-storage.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { effect, Injectable, signal } from '@angular/core';
import { themeSwitcher } from '@siemens/ix';

/**
* Service that stores the current theme in the browsers localStorage as a boolean "lightMode"
*/
@Injectable({
providedIn: 'root'
})
export class ThemeStorageService {

// like this lightMode is false by default because getItem will return null
private lightMode = signal(window.localStorage.getItem('lightMode') === 'true');

constructor() {
effect(() => {
const lm = this.lightMode();
window.localStorage.setItem('lightMode', lm ? 'true' : 'false');
themeSwitcher.setTheme(lm ? 'theme-classic-light' : 'theme-classic-dark');
})
}

/**
* flips the lightMode signal and thus results in
* the "lightMode" in the localStorage to be changed
* and also calls the Siemens themeSwitcher
*/
toggleTheme() {
this.lightMode.update(value => !value);
}

/**
* returns the lightMode Signal for easy use in components
* f.e. to change an icon depending on the mode
*/
getLightMode() {
return this.lightMode;
}

}
1 change: 1 addition & 0 deletions libs/common/frontend/models/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './lib/interfaces';
export * from './lib/tokens';
export * from './lib/services';
1 change: 1 addition & 0 deletions libs/common/frontend/models/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './interfaces';
export * from './tokens';
export * from './services';
1 change: 1 addition & 0 deletions libs/common/frontend/models/src/lib/services/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './local-storage.service';
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { TestBed } from '@angular/core/testing';

import { LocalStorageService } from './local-storage.service';

describe('LocalStorageService', () => {
let service: LocalStorageService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(LocalStorageService);

jest.spyOn(Storage.prototype, 'setItem');
Storage.prototype.setItem = jest.fn();
});

it('should be created', () => {
expect(service).toBeTruthy();
});

it('register should set it to the default value if it is not set', () => {
service.register('test', 'false');

expect(localStorage.setItem).toHaveBeenCalledWith('test', 'false');
});

it('register should not overwrite values if they are already set', () => {

Storage.prototype.getItem = jest.fn().mockReturnValue("true");

service.register('test', 'false');
expect(localStorage.setItem).toHaveBeenCalledTimes(0);
});

it('set should correctly set the localStorage and signal', () => {
service.set('test', 'true');

expect(localStorage.setItem).toHaveBeenCalledWith('test', 'true');
expect(service.get('test')()).toBe(true);
});

it('set should correctly update signal', () => {

service.set('test', 'true');

const signal = service.get('test')
expect(signal()).toBe('true');

service.set('test', 'false');
expect(signal()).toBe('false');
});


});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Injectable, signal } from '@angular/core';

/**
* Manages string values in localStorage with reactive signals.
* Allows registering keys with default values, setting, and getting values reactively.
*/

@Injectable({
providedIn: 'root'
})
export class LocalStorageService {

private signals = new Map<string, ReturnType<typeof signal>>();

/**
* Registers a key with a default value if not already set.
* @param key The localStorage key.
* @param defaultValue The default value.
*/
register(key: string, defaultValue: string) {
if (localStorage.getItem(key) === null) {
this.set(key, defaultValue);
}
}

/**
* Sets the value for a key and updates its signal.
* @param key The localStorage key.
* @param value The string value to set.
*/
set(key: string, value: string){
localStorage.setItem(key, String(value));
this.getOrCreateSignal(key).set(value);
}

/**
* Returns the signal for a key, creating it if necessary.
* @param key The localStorage key.
* @returns The signal for the key.
*/
get(key: string){
return this.getOrCreateSignal(key);
}

/**
* Creates or retrieves a signal for a key.
* @param key The localStorage key.
* @returns The signal for the key.
*/
private getOrCreateSignal(key: string) {
if (!this.signals.has(key)) {
const initialValue = localStorage.getItem(key);
this.signals.set(key, signal(initialValue));
}
return this.signals.get(key)!;
}

}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<div class="absolute top-20 right-4 !z-[50] flex flex-row-reverse gap-x-4">
<ix-button (click)="toggleView()">
Switch to {{ showCardList ? 'Event List' : 'Card List' }}
Switch to {{ showCardList() ? 'Event List' : 'Card List' }}
</ix-button>
<ix-button (click)="toggleFilter()">
{{ filter() ? 'Remove Filter for Issues' : 'Filter Facilities with Issues' }}
{{ filterIssues() ? 'Remove Filter for Issues' : 'Filter Facilities with Issues' }}
</ix-button>
</div>

Expand All @@ -16,7 +16,7 @@
></ix-content-header>

<ix-content>
@if (showCardList) {
@if (showCardList()) {
@if (facilities(); as facilities) {
<ix-card-list label="Facilities Overview" [showAllCount]="facilities.length" list-style="stack" class="pb-32">
@for (facility of facilities; track facility.id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import {
Component,
computed,
inject,
signal,
ViewEncapsulation,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { XdBrowseFacade } from '@frontend/facilities/frontend/domain';
import { StatusToColorRecord } from '@frontend/facilities/frontend/models';
import { IxModule } from '@siemens/ix-angular';
import { LocalStorageService } from 'common-frontend-models';
import { EPumpStatus } from 'facilities-shared-models';

@Component({
Expand All @@ -25,31 +25,43 @@ import { EPumpStatus } from 'facilities-shared-models';
})
export class XdBrowsePage {

protected showCardList = true;
private showCardListKey = 'showCardList';
protected showCardList = computed(() => {
const val = this.localStorageService.get(this.showCardListKey)();
return val === 'true';
});

private filterIssuesKey = 'filterIssues';
protected filterIssues = computed(() => {
const val = this.localStorageService.get(this.filterIssuesKey)();
return val === 'true';
});

protected readonly StatusToColorRecord = StatusToColorRecord;
protected readonly filter = signal(true);
private readonly _browseFacade = inject(XdBrowseFacade);
private readonly allFacilities = toSignal(this._browseFacade.getAllFacilities());
protected readonly facilities = computed(() => {
const facilities = this.allFacilities();
if(!facilities)
return undefined;

if(this.filter()) {
if(this.filterIssues()) {
return facilities.filter(facility => facility.status != EPumpStatus.REGULAR);
} else {
return facilities;
}
});

constructor(protected readonly router: Router, protected readonly route: ActivatedRoute) {
constructor(protected readonly router: Router, protected readonly route: ActivatedRoute, private readonly localStorageService: LocalStorageService) {
localStorageService.register('showCardList', 'false');
localStorageService.register(this.filterIssuesKey, 'true');
}

toggleView() {
this.showCardList = !this.showCardList;
this.localStorageService.set(this.showCardListKey, (!this.showCardList()).toString());
}

toggleFilter() {
this.filter.set(!this.filter());
this.localStorageService.set(this.filterIssuesKey, (!this.filterIssues()).toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
<div class="details-grid gap-x-6 gap-y-4 pb-32">

@if(this.pumpChart(); as pumpChart){
<div echarts [options]="pumpChart" [theme]="theme"></div>
<div echarts [options]="pumpChart" [theme]="theme()"></div>
}
@if(this.envChart(); as envChart){
<div echarts [options]="envChart" [theme]="theme"></div>
<div echarts [options]="envChart" [theme]="theme()"></div>
}

<ix-card variant="insight" class="h-full w-full p-4">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import LockModalComponent from './lock-modal/lockModal.component';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class XdDetailPage implements OnInit {
protected theme = convertThemeName(themeSwitcher.getCurrentTheme());
protected theme = signal(convertThemeName(themeSwitcher.getCurrentTheme()));
protected readonly locked = signal(true);
protected readonly StatusToColorRecord = StatusToColorRecord;
private readonly _assetId = this.route.snapshot.params['id'];
Expand Down Expand Up @@ -188,7 +188,7 @@ export class XdDetailPage implements OnInit {
registerTheme(echarts);

themeSwitcher.themeChanged.on((theme: string) => {
this.theme = convertThemeName(theme);
this.theme.set(convertThemeName(theme));
});
}

Expand Down

0 comments on commit 91ddbd4

Please sign in to comment.