@wincc-oa/wui-shared
v2.0.1
Published
WinCC Open Architecture Dashboard project.
Maintainers
Readme
WinCCOA WebComponent Dashboard
This package is part of the workspace for the WinCC Open Architecture WebComponent Dashboard, built using Lit and managed with Nx.
Usage information and reference details can be found in the WinCC OA documentation.
wui-cleanup-service
The wui-cleanup-service provides centralized resource cleanup during application lifecycle events such as user logout, session timeout, or application shutdown. It automatically discovers cleanable services and executes registered cleanup handlers.
The cleanup service follows a two-phase cleanup approach:
- Service Discovery: Automatically finds services extending
WuiCleanableService - Handler Execution: Runs registered cleanup handlers for custom cleanup logic
Creating a Cleanable Service
import { WuiCleanableService } from '@wincc-oa/wui-shared/services/wui-cleanup/wui-cleanable-service.abstract.js';
import { singleton } from 'tsyringe';
@singleton()
export class UserSessionService extends WuiCleanableService {
/**
* Performs service-specific cleanup operations.
* Called automatically during application lifecycle cleanup.
*
* @returns Promise resolving to true if cleanup succeeded
*/
async cleanup(): Promise<boolean> {
try {
localStorage.removeItem('userSession');
sessionStorage.removeItem('tempData');
console.log('UserSessionService cleanup completed');
return true;
} catch (error) {
console.error('UserSessionService cleanup failed:', error);
return false;
}
}
}Handler Registration
Register custom cleanup handlers for operations that don't fit into service cleanup:
NOTE!
All registered handlers are removed after cleanup.
import { WuiCleanupService } from '@wincc-oa/wui-shared/services/wui-cleanup/wui-cleanup.service.js';
import { container } from 'tsyringe';
/**
* Registers various cleanup handlers for different application resources.
*/
export function registerApplicationCleanupHandlers(): void {
const cleanupService = container.resolve<WuiCleanupService>(WuiCleanupService);
// Handler with no arguments - simple cache clearing
cleanupService.registerCleanupHandler(async () => {
console.log('Clearing application localStorage');
localStorage.clear();
});
// Async handler
cleanupService.registerCleanupHandler(
async (userId: string, sessionId: string) => {
await fetch('/api/cleanup', {
body: JSON.stringify({ userId, sessionId }),
headers: { 'Content-Type': 'application/json' }
});
},
['current-user-id', 'current-session-id']
);
}
// Register handlers during application initialization
registerApplicationCleanupHandlers();Executing Cleanup
import { WuiCleanupService } from '@wincc-oa/wui-shared/services/wui-cleanup/wui-cleanup.service.js';
import { container } from 'tsyringe';
/**
* Executes cleanup during user logout.
* Discovers and cleans up all registered services and handlers.
*/
export async function handleUserLogout(): Promise<void> {
const cleanupService = container.resolve<WuiCleanupService>(WuiCleanupService);
try {
const cleanupSuccess = await cleanupService.cleanup();
if (cleanupSuccess) {
console.log('Logout cleanup completed successfully');
// Redirect to login page
window.location.href = '/login';
} else {
console.warn('Some cleanup operations failed during logout');
// Handle partial cleanup failure
}
} catch (error) {
console.error('Critical error during logout cleanup:', error);
}
}BeforeLeaveController
Blocks in-app navigation before it happens — the canonical way to guard a page with unsaved changes (dirty form, editor session).
It works because the router commits the URL only after the navigateTo event
reaches the window-level listener. BeforeLeaveController (via
NavigateToController) listens on document, which is earlier in the bubble
path, so calling stopPropagation() there cancels the navigation cleanly — the
URL is never committed and screen and address bar stay in sync.
class MyEditor extends LitElement {
private isDirty = false;
constructor() {
super();
new BeforeLeaveController(this, (event) => this.onBeforeLeave(event));
}
private onBeforeLeave(event: RouterEvent | BeforeUnloadEvent): void {
if (!this.isDirty) return; // let navigation proceed
if (event instanceof RouterEvent) {
if (event.detail.userConfirmed) {
this.isDirty = false; // second pass after the user confirmed
} else {
event.stopPropagation(); // cancel this navigation
void this.confirmThenRenavigate(event.detail);
}
}
}
}Confirm asynchronously, then re-dispatch the same RouterEvent with
userConfirmed: true to let it through. See wui-dashboard-edit for the full
reference pattern.
This is the ONLY way to cancel navigation. The
<wui-router-outlet>lifecycle hooks (onBeforeLeave, etc.) run after the URL is committed and are notify-only — their return values cannot stop a navigation. See thewui-routerREADME.
BeforeUnloadController(browser tab close / reload) is currently disabled: no clean way was found to avoid conflicting with thebeforeunloadlistener inoa-rx-js-api. Only the in-appnavigateTopath is guarded today.
outletInject
outletInject is a property decorator and a drop-in replacement for
container.resolve(TOKEN) in a field initializer. It works on any LitElement
(ReactiveControllerHost & HTMLElement). Signature:
outletInject<T>(token: InjectionToken<T>).
The decorator replaces the field with a lazy getter. On each access it resolves
the token from the nearest router outlet's child container, falling back to the
global container when the host is used outside any outlet. Resolution happens per
access through a per-host OutletContainerController (created once per element),
not once at construction, so the value tracks the outlet the host is currently
mounted in.
class MyPage extends LitElement {
@outletInject(WuiRouterFacadeToken)
private readonly routerService!: WuiRouterFacade;
render() {
// read post-connect: resolves the current outlet's router view
const path = this.routerService.location.pathname;
const tab = this.routerService.getSearchParam('tab');
return html`<div>${path} (tab: ${tab})</div>`;
}
}When it matters
outletInject differs from container.resolve() for any token an outlet
registers per-outlet in its child container. For such a token the decorator
resolves the outlet-scoped value instead of the global one. For example, the
router registers an outlet-scoped router view under WuiRouterFacadeToken, so a
component in a side outlet reads that outlet's router state instead of main's.
See the routing concept in the wui-router README.
For plain global singletons (tokens no outlet overrides), resolving through the
child container returns the same instance as the global container, so
outletInject is behaviorally identical to container.resolve(). Use it for
outlet-scoped tokens; for pure globals it is future-proofing, not a behavior
change.
How a token becomes outlet-scoped
A token gains an outlet-scoped value on the registration side in one of two ways,
and @outletInject consumes both identically:
- The outlet registers a value per child container
(
child.register(TOKEN, { useValue: ... })), as the router does forWuiRouterFacadeToken. - A service marks itself
@scoped(Lifecycle.ContainerScoped)(fromtsyringe), so tsyringe creates one instance per child container automatically.
Either way, a component reads it the same way with @outletInject(TOKEN):
import { scoped, Lifecycle } from 'tsyringe';
@scoped(Lifecycle.ContainerScoped)
class MyOutletState {}
class MyPanel extends LitElement {
@outletInject(MyOutletState)
private readonly state!: MyOutletState;
}For the outlet-scoping concept and the router registration example, see the wui-router README.
The sharp edge: read post-connect
The outlet child container is published via @lit/context, and the
OutletContainerController's ContextConsumer connects in hostConnected. The
outlet container is therefore only available at or after connectedCallback.
Reading an outletInject property in a constructor, or in the field initializer
of another field (both run before connect), resolves from the global container,
not the outlet's. Because the getter re-resolves lazily, reading the same
property later returns the correct outlet instance.
Rule: read outlet-scoped injected properties post-connect (in render, event
handlers, or firstUpdated), never in the constructor.
License
MIT
